1 module workspaced.com.dub;
2 
3 import core.sync.mutex;
4 import core.thread;
5 
6 import std.json : JSONValue;
7 import std.conv;
8 import std.stdio;
9 import std.regex;
10 import std.string;
11 import std.parallelism;
12 import std.algorithm;
13 
14 import painlessjson : toJSON, fromJSON;
15 
16 import workspaced.api;
17 
18 import dub.dub;
19 import dub.project;
20 import dub.package_;
21 import dub.description;
22 
23 import dub.generators.generator;
24 import dub.compilers.compiler;
25 
26 import dub.compilers.buildsettings;
27 
28 import dub.internal.vibecompat.inet.url;
29 import dub.internal.vibecompat.core.log;
30 
31 @component("dub") :
32 
33 /// Load function for dub. Call with `{"cmd": "load", "components": ["dub"]}`
34 /// This will start dub and load all import paths. All dub methods are used with `"cmd": "dub"`
35 /// Note: This will block any incoming requests while loading.
36 @load void startup(string dir, bool registerImportProvider = true,
37 		bool registerStringImportProvider = true)
38 {
39 	setLogLevel(LogLevel.none);
40 
41 	if (registerImportProvider)
42 		importPathProvider = &imports;
43 	if (registerStringImportProvider)
44 		stringImportPathProvider = &stringImports;
45 
46 	_cwdStr = dir;
47 	_cwd = Path(dir);
48 
49 	start();
50 	upgrade();
51 
52 	_compilerBinaryName = _dub.defaultCompiler;
53 	Compiler compiler = getCompiler(_compilerBinaryName);
54 	BuildSettings settings;
55 	auto platform = compiler.determinePlatform(settings, _compilerBinaryName);
56 
57 	_configuration = _dub.project.getDefaultConfiguration(platform);
58 	assert(_dub.project.configurations.canFind(_configuration), "No configuration available");
59 	updateImportPaths(false);
60 }
61 
62 /// Stops dub when called.
63 @unload void stop()
64 {
65 	_dub.destroy();
66 }
67 
68 private void start()
69 {
70 	_dub = new Dub(_cwdStr, null, SkipPackageSuppliers.none);
71 	_dub.packageManager.getOrLoadPackage(_cwd);
72 	_dub.loadPackage();
73 	_dub.project.validate();
74 }
75 
76 private void restart()
77 {
78 	stop();
79 	start();
80 }
81 
82 /// Reloads the dub.json or dub.sdl file from the cwd
83 /// Returns: `false` if there are no import paths available
84 /// Call_With: `{"subcmd": "update"}`
85 @arguments("subcmd", "update")
86 @async void update(AsyncCallback callback)
87 {
88 	restart();
89 	new Thread({ /**/
90 		try
91 		{
92 			auto result = updateImportPaths(false);
93 			callback(null, result.toJSON);
94 		}
95 		catch (Throwable t)
96 		{
97 			callback(t, null.toJSON);
98 		}
99 	}).start();
100 }
101 
102 bool updateImportPaths(bool restartDub = true)
103 {
104 	if (restartDub)
105 		restart();
106 
107 	auto compiler = getCompiler(_compilerBinaryName);
108 	BuildSettings buildSettings;
109 	auto buildPlatform = compiler.determinePlatform(buildSettings, _compilerBinaryName, _archType);
110 
111 	GeneratorSettings settings;
112 	settings.platform = buildPlatform;
113 	settings.config = _configuration;
114 	settings.buildType = _buildType;
115 	settings.compiler = compiler;
116 	settings.buildSettings = buildSettings;
117 	settings.buildSettings.options |= BuildOption.syntaxOnly;
118 	settings.combined = true;
119 	settings.run = false;
120 
121 	try
122 	{
123 		auto paths = _dub.project.listBuildSettings(settings, ["import-paths",
124 				"string-import-paths"], ListBuildSettingsFormat.listNul);
125 		_importPaths = paths[0].split('\0');
126 		_stringImportPaths = paths[1].split('\0');
127 		return _importPaths.length > 0;
128 	}
129 	catch (Exception e)
130 	{
131 		stderr.writeln("Exception while listing import paths: ", e);
132 		_importPaths = [];
133 		_stringImportPaths = [];
134 		return false;
135 	}
136 }
137 
138 /// Calls `dub upgrade`
139 /// Call_With: `{"subcmd": "upgrade"}`
140 @arguments("subcmd", "upgrade")
141 void upgrade()
142 {
143 	_dub.upgrade(UpgradeOptions.select | UpgradeOptions.upgrade);
144 }
145 
146 /// Lists all dependencies
147 /// Returns: `[{dependencies: [string], ver: string, name: string}]`
148 /// Call_With: `{"subcmd": "list:dep"}`
149 @arguments("subcmd", "list:dep")
150 auto dependencies() @property
151 {
152 	return _dub.project.listDependencies();
153 }
154 
155 /// Lists all import paths
156 /// Call_With: `{"subcmd": "list:import"}`
157 @arguments("subcmd", "list:import")
158 string[] imports() @property
159 {
160 	return _importPaths;
161 }
162 
163 /// Lists all string import paths
164 /// Call_With: `{"subcmd": "list:string-import"}`
165 @arguments("subcmd", "list:string-import")
166 string[] stringImports() @property
167 {
168 	return _stringImportPaths;
169 }
170 
171 /// Lists all configurations defined in the package description
172 /// Call_With: `{"subcmd": "list:configurations"}`
173 @arguments("subcmd", "list:configurations")
174 string[] configurations() @property
175 {
176 	return _dub.project.configurations;
177 }
178 
179 /// Lists all build types defined in the package description AND the predefined ones from dub ("plain", "debug", "release", "release-debug", "release-nobounds", "unittest", "docs", "ddox", "profile", "profile-gc", "cov", "unittest-cov")
180 /// Call_With: `{"subcmd": "list:build-types"}`
181 @arguments("subcmd", "list:build-types")
182 string[] buildTypes() @property
183 {
184 	string[] types = [
185 		"plain", "debug", "release", "release-debug", "release-nobounds", "unittest", "docs",
186 		"ddox", "profile", "profile-gc", "cov", "unittest-cov"
187 	];
188 	foreach (type, info; _dub.project.rootPackage.recipe.buildTypes)
189 		types ~= type;
190 	return types;
191 }
192 
193 /// Gets the current selected configuration
194 /// Call_With: `{"subcmd": "get:configuration"}`
195 @arguments("subcmd", "get:configuration")
196 string configuration() @property
197 {
198 	return _configuration;
199 }
200 
201 /// Selects a new configuration and updates the import paths accordingly
202 /// Returns: `false` if there are no import paths in the new configuration
203 /// Call_With: `{"subcmd": "set:configuration"}`
204 @arguments("subcmd", "set:configuration")
205 bool setConfiguration(string configuration)
206 {
207 	if (!_dub.project.configurations.canFind(configuration))
208 		return false;
209 	_configuration = configuration;
210 	return updateImportPaths(false);
211 }
212 
213 /// List all possible arch types for current set compiler
214 /// Call_With: `{"subcmd": "list:arch-types"}`
215 @arguments("subcmd", "list:arch-types")
216 string[] archTypes() @property
217 {
218 	string[] types = ["x86_64", "x86"];
219 
220 	if (getCompiler(_compilerBinaryName).name == "gdc")
221 	{
222 		types ~= ["arm", "arm_thumb"];
223 	}
224 
225 	return types;
226 }
227 
228 /// Returns the current selected arch type
229 /// Call_With: `{"subcmd": "get:arch-type"}`
230 @arguments("subcmd", "get:arch-type")
231 string archType() @property
232 {
233 	return _archType;
234 }
235 
236 /// Selects a new arch type and updates the import paths accordingly
237 /// Returns: `false` if there are no import paths in the new arch type
238 /// Call_With: `{"subcmd": "set:arch-type"}`
239 @arguments("subcmd", "set:arch-type")
240 bool setArchType(JSONValue request)
241 {
242 	assert("arch-type" in request, "arch-type not in request");
243 	auto type = request["arch-type"].fromJSON!string;
244 	if (archTypes.canFind(type))
245 	{
246 		_archType = type;
247 		return updateImportPaths(false);
248 	}
249 	else
250 	{
251 		return false;
252 	}
253 }
254 
255 /// Returns the current selected build type
256 /// Call_With: `{"subcmd": "get:build-type"}`
257 @arguments("subcmd", "get:build-type")
258 string buildType() @property
259 {
260 	return _buildType;
261 }
262 
263 /// Selects a new build type and updates the import paths accordingly
264 /// Returns: `false` if there are no import paths in the new build type
265 /// Call_With: `{"subcmd": "set:build-type"}`
266 @arguments("subcmd", "set:build-type")
267 bool setBuildType(JSONValue request)
268 {
269 	assert("build-type" in request, "build-type not in request");
270 	auto type = request["build-type"].fromJSON!string;
271 	if (buildTypes.canFind(type))
272 	{
273 		_buildType = type;
274 		return updateImportPaths(false);
275 	}
276 	else
277 	{
278 		return false;
279 	}
280 }
281 
282 /// Returns the current selected compiler
283 /// Call_With: `{"subcmd": "get:compiler"}`
284 @arguments("subcmd", "get:compiler")
285 string compiler() @property
286 {
287 	return _compilerBinaryName;
288 }
289 
290 /// Selects a new compiler for building
291 /// Returns: `false` if the compiler does not exist
292 /// Call_With: `{"subcmd": "set:compiler"}`
293 @arguments("subcmd", "set:compiler")
294 bool setCompiler(string compiler)
295 {
296 	try
297 	{
298 		_compilerBinaryName = compiler;
299 		Compiler comp = getCompiler(compiler); // make sure it gets a valid compiler
300 		return true;
301 	}
302 	catch (Exception e)
303 	{
304 		return false;
305 	}
306 }
307 
308 /// Returns the project name
309 /// Call_With: `{"subcmd": "get:name"}`
310 @arguments("subcmd", "get:name")
311 string name() @property
312 {
313 	return _dub.projectName;
314 }
315 
316 /// Returns the project path
317 /// Call_With: `{"subcmd": "get:path"}`
318 @arguments("subcmd", "get:path")
319 auto path() @property
320 {
321 	return _dub.projectPath;
322 }
323 
324 /// Asynchroniously builds the project WITHOUT OUTPUT. This is intended for linting code and showing build errors quickly inside the IDE.
325 /// Returns: `[{line: int, column: int, type: ErrorType, text: string}]` where type is an integer
326 /// Call_With: `{"subcmd": "build"}`
327 @arguments("subcmd", "build")
328 @async void build(AsyncCallback cb)
329 {
330 	new Thread({
331 		try
332 		{
333 			auto compiler = getCompiler(_compilerBinaryName);
334 			BuildSettings buildSettings;
335 			auto buildPlatform = compiler.determinePlatform(buildSettings,
336 				_compilerBinaryName, _archType);
337 
338 			GeneratorSettings settings;
339 			settings.platform = buildPlatform;
340 			settings.config = _configuration;
341 			settings.buildType = _buildType;
342 			settings.compiler = compiler;
343 			settings.buildSettings = buildSettings;
344 			settings.buildSettings.options |= BuildOption.syntaxOnly;
345 
346 			BuildIssue[] issues;
347 
348 			settings.compileCallback = (status, output) {
349 				string[] lines = output.splitLines;
350 				foreach (line; lines)
351 				{
352 					auto match = line.matchFirst(errorFormat);
353 					if (match)
354 					{
355 						issues ~= BuildIssue(match[2].to!int,
356 							match[3].toOr!int(0), match[1], match[4].to!ErrorType, match[5]);
357 					}
358 					else
359 					{
360 						if (line.canFind("from"))
361 						{
362 							auto contMatch = line.matchFirst(errorFormatCont);
363 							if (contMatch)
364 							{
365 								issues ~= BuildIssue(contMatch[2].to!int, contMatch[3].toOr!int(1),
366 									contMatch[1], ErrorType.Error, contMatch[4]);
367 							}
368 						}
369 						if (line.canFind("is deprecated"))
370 						{
371 							auto deprMatch = line.matchFirst(deprecationFormat);
372 							if (deprMatch)
373 							{
374 								issues ~= BuildIssue(deprMatch[2].to!int, deprMatch[3].toOr!int(1),
375 									deprMatch[1], ErrorType.Deprecation, deprMatch[4] ~ " is deprecated, use " ~ deprMatch[5] ~ " instead.");
376 								// TODO: maybe add special type or output
377 							}
378 						}
379 					}
380 				}
381 			};
382 			try
383 			{
384 				_dub.generateProject("build", settings);
385 			}
386 			catch (Exception e)
387 			{
388 				if (!e.msg.matchFirst(harmlessExceptionFormat))
389 					throw e;
390 			}
391 			cb(null, issues.toJSON);
392 		}
393 		catch (Throwable t)
394 		{
395 			ubyte[] empty;
396 			cb(t, empty.toJSON);
397 		}
398 	}).start();
399 }
400 
401 ///
402 enum ErrorType : ubyte
403 {
404 	///
405 	Error = 0,
406 	///
407 	Warning = 1,
408 	///
409 	Deprecation = 2
410 }
411 
412 private:
413 
414 __gshared
415 {
416 	Dub _dub;
417 	Path _cwd;
418 	string _configuration;
419 	string _archType = "x86_64";
420 	string _buildType = "debug";
421 	string _cwdStr;
422 	string _compilerBinaryName;
423 	string[] _importPaths, _stringImportPaths;
424 }
425 
426 T toOr(T)(string s, T defaultValue)
427 {
428 	if (!s || !s.length)
429 		return defaultValue;
430 	return s.to!T;
431 }
432 
433 enum harmlessExceptionFormat = ctRegex!(`failed with exit code`, "g");
434 enum errorFormat = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\): (Deprecation|Warning|Error): (.*)`, "gi"); // `
435 enum errorFormatCont = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\): (.*)`, "g"); // `
436 enum deprecationFormat = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\): (.*?) is deprecated, use (.*?) instead.$`, "g"); // `
437 
438 struct BuildIssue
439 {
440 	int line, column;
441 	string file;
442 	ErrorType type;
443 	string text;
444 }
445 
446 struct DubPackageInfo
447 {
448 	string[string] dependencies;
449 	string ver;
450 	string name;
451 }
452 
453 DubPackageInfo getInfo(in Package dep)
454 {
455 	DubPackageInfo info;
456 	info.name = dep.name;
457 	info.ver = dep.version_.toString;
458 	foreach (subDep; dep.getAllDependencies())
459 	{
460 		info.dependencies[subDep.name] = subDep.spec.toString;
461 	}
462 	return info;
463 }
464 
465 auto listDependencies(Project project)
466 {
467 	auto deps = project.dependencies;
468 	DubPackageInfo[] dependencies;
469 	if (deps is null)
470 		return dependencies;
471 	foreach (dep; deps)
472 	{
473 		dependencies ~= getInfo(dep);
474 	}
475 	return dependencies;
476 }