1 module workspaced.com.dub;
2 
3 import core.sync.mutex;
4 import core.exception;
5 import core.thread;
6 
7 import std.algorithm;
8 import std.conv;
9 import std.exception;
10 import std.json : JSONValue, JSON_TYPE;
11 import std.parallelism;
12 import std.regex;
13 import std.stdio;
14 import std.string;
15 
16 import painlessjson : toJSON, fromJSON;
17 
18 import workspaced.api;
19 
20 import dub.description;
21 import dub.dub;
22 import dub.package_;
23 import dub.project;
24 
25 import dub.compilers.compiler;
26 import dub.generators.generator;
27 
28 import dub.compilers.buildsettings;
29 
30 import dub.internal.vibecompat.core.log;
31 import dub.internal.vibecompat.inet.url;
32 
33 @component("dub")
34 class DubComponent : ComponentWrapper
35 {
36 	mixin DefaultComponentWrapper;
37 
38 	static void registered()
39 	{
40 		setLogLevel(LogLevel.none);
41 	}
42 
43 	protected void load()
44 	{
45 		if (!refInstance)
46 			throw new Exception("dub requires to be instanced");
47 
48 		if (config.get!bool("dub", "registerImportProvider", true))
49 			importPathProvider = &imports;
50 		if (config.get!bool("dub", "registerStringImportProvider", true))
51 			stringImportPathProvider = &stringImports;
52 		if (config.get!bool("dub", "registerImportFilesProvider", false))
53 			importFilesProvider = &fileImports;
54 
55 		try
56 		{
57 			start();
58 			//upgrade();
59 
60 			_compilerBinaryName = _dub.defaultCompiler;
61 			Compiler compiler = getCompiler(_compilerBinaryName);
62 			BuildSettings settings;
63 			auto platform = compiler.determinePlatform(settings, _compilerBinaryName);
64 
65 			_configuration = _dub.project.getDefaultConfiguration(platform);
66 			if (!_dub.project.configurations.canFind(_configuration))
67 			{
68 				stderr.writeln("Dub Error: No configuration available");
69 				workspaced.broadcast(refInstance, JSONValue(["type" : JSONValue("warning"),
70 						"component" : JSONValue("dub"), "detail" : JSONValue("invalid-default-config")]));
71 			}
72 			else
73 				updateImportPaths(false);
74 		}
75 		catch (Exception e)
76 		{
77 			if (!_dub || !_dub.project)
78 				throw e;
79 			stderr.writeln("Dub Error (ignored): ", e);
80 		}
81 		catch (AssertError e)
82 		{
83 			if (!_dub || !_dub.project)
84 				throw e;
85 			stderr.writeln("Dub Error (ignored): ", e);
86 		}
87 	}
88 
89 	private void start()
90 	{
91 		_dub = new Dub(instance.cwd, null, SkipPackageSuppliers.none);
92 		_dub.packageManager.getOrLoadPackage(NativePath(instance.cwd));
93 		_dub.loadPackage();
94 		_dub.project.validate();
95 	}
96 
97 	private void restart()
98 	{
99 		_dub.destroy();
100 		start();
101 	}
102 
103 	/// Reloads the dub.json or dub.sdl file from the cwd
104 	/// Returns: `false` if there are no import paths available
105 	Future!bool update()
106 	{
107 		restart();
108 		auto ret = new Future!bool;
109 		new Thread({ /**/
110 			try
111 			{
112 				auto result = updateImportPaths(false);
113 				ret.finish(result);
114 			}
115 			catch (Throwable t)
116 			{
117 				ret.error(t);
118 			}
119 		}).start();
120 		return ret;
121 	}
122 
123 	bool updateImportPaths(bool restartDub = true)
124 	{
125 		validateConfiguration();
126 
127 		if (restartDub)
128 			restart();
129 
130 		auto compiler = getCompiler(_compilerBinaryName);
131 		BuildSettings buildSettings;
132 		auto buildPlatform = compiler.determinePlatform(buildSettings, _compilerBinaryName, _archType);
133 
134 		GeneratorSettings settings;
135 		settings.platform = buildPlatform;
136 		settings.config = _configuration;
137 		settings.buildType = _buildType;
138 		settings.compiler = compiler;
139 		settings.buildSettings = buildSettings;
140 		settings.buildSettings.options |= BuildOption.syntaxOnly;
141 		settings.combined = true;
142 		settings.run = false;
143 
144 		try
145 		{
146 			auto paths = _dub.project.listBuildSettings(settings, ["import-paths",
147 					"string-import-paths", "source-files"], ListBuildSettingsFormat.listNul);
148 			_importPaths = paths[0].split('\0');
149 			_stringImportPaths = paths[1].split('\0');
150 			_importFiles = paths[2].split('\0');
151 			return _importPaths.length > 0 || _importFiles.length > 0;
152 		}
153 		catch (Exception e)
154 		{
155 			workspaced.broadcast(refInstance, JSONValue(["type" : JSONValue("error"), "component"
156 					: JSONValue("dub"), "detail"
157 					: JSONValue("Error while listing import paths: " ~ e.toString)]));
158 			_importPaths = [];
159 			_stringImportPaths = [];
160 			return false;
161 		}
162 	}
163 
164 	/// Calls `dub upgrade`
165 	void upgrade()
166 	{
167 		_dub.upgrade(UpgradeOptions.select | UpgradeOptions.upgrade);
168 	}
169 
170 	/// Throws if configuration is invalid, otherwise does nothing.
171 	void validateConfiguration()
172 	{
173 		if (!_dub.project.configurations.canFind(_configuration))
174 			throw new Exception("Cannot use dub with invalid configuration");
175 	}
176 
177 	/// Lists all dependencies. This will go through all dependencies and contain the dependencies of dependencies. You need to create a tree structure from this yourself.
178 	/// Returns: `[{dependencies: [string], ver: string, name: string}]`
179 	auto dependencies() @property
180 	{
181 		validateConfiguration();
182 
183 		return _dub.project.listDependencies();
184 	}
185 
186 	/// Lists dependencies of the root package. This can be used as a base to create a tree structure.
187 	/// Returns: `[string]`
188 	auto rootDependencies() @property
189 	{
190 		validateConfiguration();
191 
192 		return _dub.project.rootPackage.listDependencies();
193 	}
194 
195 	/// Lists all import paths
196 	string[] imports() @property
197 	{
198 		return _importPaths;
199 	}
200 
201 	/// Lists all string import paths
202 	string[] stringImports() @property
203 	{
204 		return _stringImportPaths;
205 	}
206 
207 	/// Lists all import paths to files
208 	string[] fileImports() @property
209 	{
210 		return _importFiles;
211 	}
212 
213 	/// Lists all configurations defined in the package description
214 	string[] configurations() @property
215 	{
216 		return _dub.project.configurations;
217 	}
218 
219 	/// 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")
220 	string[] buildTypes() @property
221 	{
222 		string[] types = [
223 			"plain", "debug", "release", "release-debug", "release-nobounds",
224 			"unittest", "docs", "ddox", "profile", "profile-gc", "cov", "unittest-cov"
225 		];
226 		foreach (type, info; _dub.project.rootPackage.recipe.buildTypes)
227 			types ~= type;
228 		return types;
229 	}
230 
231 	/// Gets the current selected configuration
232 	string configuration() @property
233 	{
234 		return _configuration;
235 	}
236 
237 	/// Selects a new configuration and updates the import paths accordingly
238 	/// Returns: `false` if there are no import paths in the new configuration
239 	bool setConfiguration(string configuration)
240 	{
241 		if (!_dub.project.configurations.canFind(configuration))
242 			return false;
243 		_configuration = configuration;
244 		return updateImportPaths(false);
245 	}
246 
247 	/// List all possible arch types for current set compiler
248 	string[] archTypes() @property
249 	{
250 		string[] types = ["x86_64", "x86"];
251 
252 		string compilerName = getCompiler(_compilerBinaryName).name;
253 
254 		version (Windows)
255 		{
256 			if (compilerName == "dmd")
257 			{
258 				types ~= "x86_mscoff";
259 			}
260 		}
261 		if (compilerName == "gdc")
262 		{
263 			types ~= ["arm", "arm_thumb"];
264 		}
265 
266 		return types;
267 	}
268 
269 	/// Returns the current selected arch type
270 	string archType() @property
271 	{
272 		return _archType;
273 	}
274 
275 	/// Selects a new arch type and updates the import paths accordingly
276 	/// Returns: `false` if there are no import paths in the new arch type
277 	bool setArchType(JSONValue request)
278 	{
279 		enforce(request.type == JSON_TYPE.OBJECT && "arch-type" in request, "arch-type not in request");
280 		auto type = request["arch-type"].fromJSON!string;
281 		if (archTypes.canFind(type))
282 		{
283 			_archType = type;
284 			return updateImportPaths(false);
285 		}
286 		else
287 		{
288 			return false;
289 		}
290 	}
291 
292 	/// Returns the current selected build type
293 	string buildType() @property
294 	{
295 		return _buildType;
296 	}
297 
298 	/// Selects a new build type and updates the import paths accordingly
299 	/// Returns: `false` if there are no import paths in the new build type
300 	bool setBuildType(JSONValue request)
301 	{
302 		enforce(request.type == JSON_TYPE.OBJECT && "build-type" in request,
303 				"build-type not in request");
304 		auto type = request["build-type"].fromJSON!string;
305 		if (buildTypes.canFind(type))
306 		{
307 			_buildType = type;
308 			return updateImportPaths(false);
309 		}
310 		else
311 		{
312 			return false;
313 		}
314 	}
315 
316 	/// Returns the current selected compiler
317 	string compiler() @property
318 	{
319 		return _compilerBinaryName;
320 	}
321 
322 	/// Selects a new compiler for building
323 	/// Returns: `false` if the compiler does not exist
324 	bool setCompiler(string compiler)
325 	{
326 		try
327 		{
328 			_compilerBinaryName = compiler;
329 			Compiler comp = getCompiler(compiler); // make sure it gets a valid compiler
330 			return true;
331 		}
332 		catch (Exception e)
333 		{
334 			return false;
335 		}
336 	}
337 
338 	/// Returns the project name
339 	string name() @property
340 	{
341 		return _dub.projectName;
342 	}
343 
344 	/// Returns the project path
345 	auto path() @property
346 	{
347 		return _dub.projectPath;
348 	}
349 
350 	/// Returns whether there is a target set to build. If this is false then build will throw an exception.
351 	bool canBuild() @property
352 	{
353 		if (!_dub.project.configurations.canFind(_configuration))
354 			return false;
355 		auto compiler = getCompiler(_compilerBinaryName);
356 		BuildSettings buildSettings;
357 		compiler.determinePlatform(buildSettings, _compilerBinaryName, _archType);
358 		return true;
359 	}
360 
361 	/// Asynchroniously builds the project WITHOUT OUTPUT. This is intended for linting code and showing build errors quickly inside the IDE.
362 	Future!(BuildIssue[]) build()
363 	{
364 		validateConfiguration();
365 
366 		auto ret = new Future!(BuildIssue[]);
367 		new Thread({
368 			try
369 			{
370 				auto compiler = getCompiler(_compilerBinaryName);
371 				BuildSettings buildSettings;
372 				auto buildPlatform = compiler.determinePlatform(buildSettings,
373 					_compilerBinaryName, _archType);
374 
375 				GeneratorSettings settings;
376 				settings.platform = buildPlatform;
377 				settings.config = _configuration;
378 				settings.buildType = _buildType;
379 				settings.compiler = compiler;
380 				settings.tempBuild = true;
381 				settings.buildSettings = buildSettings;
382 				settings.buildSettings.addOptions(BuildOption.syntaxOnly);
383 				settings.buildSettings.addDFlags("-o-");
384 
385 				BuildIssue[] issues;
386 
387 				settings.compileCallback = (status, output) {
388 					string[] lines = output.splitLines;
389 					foreach (line; lines)
390 					{
391 						auto match = line.matchFirst(errorFormat);
392 						if (match)
393 						{
394 							issues ~= BuildIssue(match[2].to!int, match[3].toOr!int(0),
395 								match[1], match[4].to!ErrorType, match[5]);
396 						}
397 						else
398 						{
399 							if (line.canFind("from"))
400 							{
401 								auto contMatch = line.matchFirst(errorFormatCont);
402 								if (contMatch)
403 								{
404 									issues ~= BuildIssue(contMatch[2].to!int,
405 										contMatch[3].toOr!int(1), contMatch[1], ErrorType.Error, contMatch[4]);
406 								}
407 							}
408 							if (line.canFind("is deprecated"))
409 							{
410 								auto deprMatch = line.matchFirst(deprecationFormat);
411 								if (deprMatch)
412 								{
413 									issues ~= BuildIssue(deprMatch[2].to!int, deprMatch[3].toOr!int(1),
414 										deprMatch[1], ErrorType.Deprecation,
415 										deprMatch[4] ~ " is deprecated, use " ~ deprMatch[5] ~ " instead.");
416 									// TODO: maybe add special type or output
417 								}
418 							}
419 						}
420 					}
421 				};
422 				try
423 				{
424 					_dub.generateProject("build", settings);
425 				}
426 				catch (Exception e)
427 				{
428 					if (!e.msg.matchFirst(harmlessExceptionFormat))
429 						throw e;
430 				}
431 				ret.finish(issues);
432 			}
433 			catch (Throwable t)
434 			{
435 				ret.error(t);
436 			}
437 		}).start();
438 		return ret;
439 	}
440 
441 private:
442 	Dub _dub;
443 	string _configuration;
444 	string _archType = "x86_64";
445 	string _buildType = "debug";
446 	string _compilerBinaryName;
447 	BuildSettings _settings;
448 	Compiler _compiler;
449 	BuildPlatform _platform;
450 	string[] _importPaths, _stringImportPaths, _importFiles;
451 }
452 
453 ///
454 enum ErrorType : ubyte
455 {
456 	///
457 	Error = 0,
458 	///
459 	Warning = 1,
460 	///
461 	Deprecation = 2
462 }
463 
464 /// Returned by build
465 struct BuildIssue
466 {
467 	///
468 	int line, column;
469 	///
470 	string file;
471 	///
472 	ErrorType type;
473 	///
474 	string text;
475 }
476 
477 private:
478 
479 T toOr(T)(string s, T defaultValue)
480 {
481 	if (!s || !s.length)
482 		return defaultValue;
483 	return s.to!T;
484 }
485 
486 enum harmlessExceptionFormat = ctRegex!(`failed with exit code`, "g");
487 enum errorFormat = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\): (Deprecation|Warning|Error): (.*)`, "gi");
488 enum errorFormatCont = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\): (.*)`, "g");
489 enum deprecationFormat = ctRegex!(
490 			`(.*?)\((\d+)(?:,(\d+))?\): (.*?) is deprecated, use (.*?) instead.$`, "g");
491 
492 struct DubPackageInfo
493 {
494 	string[string] dependencies;
495 	string ver;
496 	string name;
497 	string path;
498 	string description;
499 	string homepage;
500 	const(string)[] authors;
501 	string copyright;
502 	string license;
503 	DubPackageInfo[] subPackages;
504 
505 	void fill(in PackageRecipe recipe)
506 	{
507 		description = recipe.description;
508 		homepage = recipe.homepage;
509 		authors = recipe.authors;
510 		copyright = recipe.copyright;
511 		license = recipe.license;
512 
513 		foreach (subpackage; recipe.subPackages)
514 		{
515 			DubPackageInfo info;
516 			info.ver = subpackage.recipe.version_;
517 			info.name = subpackage.recipe.name;
518 			info.path = subpackage.path;
519 			info.fill(subpackage.recipe);
520 		}
521 	}
522 }
523 
524 DubPackageInfo getInfo(in Package dep)
525 {
526 	DubPackageInfo info;
527 	info.name = dep.name;
528 	info.ver = dep.version_.toString;
529 	info.path = dep.path.toString;
530 	info.fill(dep.recipe);
531 	foreach (subDep; dep.getAllDependencies())
532 	{
533 		info.dependencies[subDep.name] = subDep.spec.toString;
534 	}
535 	return info;
536 }
537 
538 auto listDependencies(Project project)
539 {
540 	auto deps = project.dependencies;
541 	DubPackageInfo[] dependencies;
542 	if (deps is null)
543 		return dependencies;
544 	foreach (dep; deps)
545 	{
546 		dependencies ~= getInfo(dep);
547 	}
548 	return dependencies;
549 }
550 
551 string[] listDependencies(Package pkg)
552 {
553 	auto deps = pkg.getAllDependencies();
554 	string[] dependencies;
555 	if (deps is null)
556 		return dependencies;
557 	foreach (dep; deps)
558 		dependencies ~= dep.name;
559 	return dependencies;
560 }