1 module workspaced.com.dub;
2 
3 import core.exception;
4 import core.sync.mutex;
5 import core.thread;
6 
7 import std.algorithm;
8 import std.array : appender;
9 import std.conv;
10 import std.exception;
11 import std.parallelism;
12 import std.regex;
13 import std.stdio;
14 import std.string;
15 
16 import workspaced.api;
17 
18 import dub.description;
19 import dub.dub;
20 import dub.package_;
21 import dub.project;
22 
23 import dub.compilers.buildsettings;
24 import dub.compilers.compiler;
25 import dub.dependency;
26 import dub.generators.build;
27 import dub.generators.generator;
28 
29 import dub.internal.vibecompat.core.log;
30 import dub.internal.vibecompat.inet.url;
31 
32 import dub.recipe.io;
33 
34 @component("dub")
35 class DubComponent : ComponentWrapper
36 {
37 	mixin DefaultComponentWrapper;
38 
39 	enum WarningId
40 	{
41 		invalidDefaultConfig,
42 		unexpectedError,
43 		failedListingImportPaths
44 	}
45 
46 	static void registered()
47 	{
48 		setLogLevel(LogLevel.none);
49 	}
50 
51 	protected void load()
52 	{
53 		if (!refInstance)
54 			throw new Exception("dub requires to be instanced");
55 
56 		if (config.get!bool("dub", "registerImportProvider", true))
57 			importPathProvider = &imports;
58 		if (config.get!bool("dub", "registerStringImportProvider", true))
59 			stringImportPathProvider = &stringImports;
60 		if (config.get!bool("dub", "registerImportFilesProvider", false))
61 			importFilesProvider = &fileImports;
62 		if (config.get!bool("dub", "registerProjectVersionsProvider", true))
63 			projectVersionsProvider = &versions;
64 		if (config.get!bool("dub", "registerDebugSpecificationsProvider", true))
65 			debugSpecificationsProvider = &debugVersions;
66 
67 		try
68 		{
69 			start();
70 
71 			_configuration = _dub.project.getDefaultConfiguration(_platform);
72 			if (!_dub.project.configurations.canFind(_configuration))
73 			{
74 				workspaced.messageHandler.warn(refInstance, "dub",
75 					cast(int)WarningId.invalidDefaultConfig,
76 					"Dub Error: No configuration available");
77 			}
78 			else
79 				updateImportPaths(false);
80 		}
81 		catch (Exception e)
82 		{
83 			if (!_dub || !_dub.project)
84 				throw e;
85 			workspaced.messageHandler.warn(refInstance, "dub",
86 				cast(int)WarningId.unexpectedError,
87 				"Dub Error (ignored)",
88 				e.toString);
89 		}
90 		/*catch (AssertError e)
91 		{
92 			if (!_dub || !_dub.project)
93 				throw e;
94 			workspaced.messageHandler.warn(refInstance, "dub",
95 				cast(int)WarningId.unexpectedError,
96 				"Dub Error (ignored): " ~ e.toString);
97 		}*/
98 	}
99 
100 	private void start()
101 	{
102 		_dubRunning = false;
103 		_dub = new Dub(instance.cwd, null, SkipPackageSuppliers.none);
104 		_dub.packageManager.getOrLoadPackage(NativePath(instance.cwd));
105 		_dub.loadPackage();
106 		_dub.project.validate();
107 
108 		// mark all packages as optional so we don't crash
109 		int missingPackages;
110 		auto optionalified = optionalifyPackages;
111 		foreach (ref pkg; _dub.project.getTopologicalPackageList())
112 		{
113 			optionalifyRecipe(pkg);
114 			foreach (dep; pkg.getAllDependencies()
115 				.filter!(a => optionalified.canFind(a.name)))
116 			{
117 				auto d = _dub.project.getDependency(dep.name, true);
118 				if (!d)
119 					missingPackages++;
120 				else
121 					optionalifyRecipe(d);
122 			}
123 		}
124 
125 		if (!_compilerBinaryName.length)
126 			_compilerBinaryName = _dub.defaultCompiler;
127 		setCompiler(_compilerBinaryName);
128 
129 		_settingsTemplate = cast() _dub.project.rootPackage.getBuildSettings();
130 
131 		if (missingPackages > 0)
132 		{
133 			upgrade(false);
134 			optionalifyPackages();
135 		}
136 
137 		_dubRunning = true;
138 	}
139 
140 	private string[] optionalifyPackages()
141 	{
142 		bool[Package] visited;
143 		string[] optionalified;
144 		foreach (pkg; _dub.project.dependencies)
145 			optionalified ~= optionalifyRecipe(cast() pkg);
146 		return optionalified;
147 	}
148 
149 	private string[] optionalifyRecipe(Package pkg)
150 	{
151 		string[] optionalified;
152 		foreach (key, ref value; pkg.recipe.buildSettings.dependencies)
153 		{
154 			if (!value.optional)
155 			{
156 				value.optional = true;
157 				value.default_ = true;
158 				optionalified ~= key;
159 			}
160 		}
161 		foreach (ref config; pkg.recipe.configurations)
162 			foreach (key, ref value; config.buildSettings.dependencies)
163 			{
164 				if (!value.optional)
165 				{
166 					value.optional = true;
167 					value.default_ = true;
168 					optionalified ~= key;
169 				}
170 			}
171 		return optionalified;
172 	}
173 
174 	private void restart()
175 	{
176 		_dub.destroy();
177 		_dubRunning = false;
178 		start();
179 	}
180 
181 	bool isRunning()
182 	{
183 		return _dub !is null
184 			&& _dub.project !is null
185 			&& _dub.project.rootPackage !is null
186 			&& _dubRunning;
187 	}
188 
189 	/// Reloads the dub.json or dub.sdl file from the cwd
190 	/// Returns: `false` if there are no import paths available
191 	Future!bool update()
192 	{
193 		restart();
194 		mixin(gthreadsAsyncProxy!`updateImportPaths(false)`);
195 	}
196 
197 	bool updateImportPaths(bool restartDub = true)
198 	{
199 		validateConfiguration();
200 
201 		if (restartDub)
202 			restart();
203 
204 		GeneratorSettings settings;
205 		settings.platform = _platform;
206 		settings.config = _configuration;
207 		settings.buildType = _buildType;
208 		settings.compiler = _compiler;
209 		settings.buildSettings = _settings;
210 		settings.buildSettings.addOptions(BuildOption.syntaxOnly);
211 		settings.combined = true;
212 		settings.run = false;
213 
214 		try
215 		{
216 			auto paths = _dub.project.listBuildSettings(settings, [
217 					"import-paths", "string-import-paths", "source-files", "versions", "debug-versions"
218 					], ListBuildSettingsFormat.listNul);
219 			_importPaths = paths[0].split('\0');
220 			_stringImportPaths = paths[1].split('\0');
221 			_importFiles = paths[2].split('\0');
222 			_versions = paths[3].split('\0');
223 			_debugVersions = paths[4].split('\0');
224 			return _importPaths.length > 0 || _importFiles.length > 0;
225 		}
226 		catch (Exception e)
227 		{
228 			workspaced.messageHandler.error(refInstance, "dub",
229 				cast(int)WarningId.failedListingImportPaths,
230 				"Error while listing import paths",
231 				e.toString);
232 			_importPaths = [];
233 			_stringImportPaths = [];
234 			return false;
235 		}
236 	}
237 
238 	/// Calls `dub upgrade`
239 	void upgrade(bool save = true)
240 	{
241 		if (save)
242 			_dub.upgrade(UpgradeOptions.select | UpgradeOptions.upgrade);
243 		else
244 			_dub.upgrade(UpgradeOptions.noSaveSelections);
245 	}
246 
247 	/// Throws if configuration is invalid, otherwise does nothing.
248 	void validateConfiguration() const
249 	{
250 		if (!_dub.project.configurations.canFind(_configuration))
251 			throw new Exception("Cannot use dub with invalid configuration");
252 	}
253 
254 	/// Throws if configuration is invalid or targetType is none or source library, otherwise does nothing.
255 	void validateBuildConfiguration()
256 	{
257 		if (!_dub.project.configurations.canFind(_configuration))
258 			throw new Exception("Cannot use dub with invalid configuration");
259 		if (_settings.targetType == TargetType.none)
260 			throw new Exception("Cannot build with dub with targetType == none");
261 		if (_settings.targetType == TargetType.sourceLibrary)
262 			throw new Exception("Cannot build with dub with targetType == sourceLibrary");
263 	}
264 
265 	/// 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.
266 	/// Returns: `[{dependencies: string[string], ver: string, name: string}]`
267 	auto dependencies() @property const
268 	{
269 		validateConfiguration();
270 
271 		return listDependencies(_dub.project);
272 	}
273 
274 	/// Lists dependencies of the root package. This can be used as a base to create a tree structure.
275 	string[] rootDependencies() @property const
276 	{
277 		validateConfiguration();
278 
279 		return listDependencies(_dub.project.rootPackage);
280 	}
281 
282 	/// Returns the path to the root package recipe (dub.json/dub.sdl)
283 	///
284 	/// Note that this can be empty if the package is not in the local file system.
285 	string recipePath() @property
286 	{
287 		return _dub.project.rootPackage.recipePath.toString;
288 	}
289 
290 	/// Re-parses the package recipe on the file system and returns if the syntax is valid.
291 	/// Returns: empty string/null if no error occured, error message if an error occured.
292 	string validateRecipeSyntaxOnFileSystem()
293 	{
294 		auto p = recipePath;
295 		if (!p.length)
296 			return "Package is not in local file system";
297 
298 		try
299 		{
300 			readPackageRecipe(p);
301 			return null;
302 		}
303 		catch (Exception e)
304 		{
305 			return e.msg;
306 		}
307 	}
308 
309 	/// Lists all import paths
310 	string[] imports() @property nothrow
311 	{
312 		return _importPaths;
313 	}
314 
315 	/// Lists all string import paths
316 	string[] stringImports() @property nothrow
317 	{
318 		return _stringImportPaths;
319 	}
320 
321 	/// Lists all import paths to files
322 	string[] fileImports() @property nothrow
323 	{
324 		return _importFiles;
325 	}
326 
327 	/// Lists the currently defined versions
328 	string[] versions() @property nothrow
329 	{
330 		return _versions;
331 	}
332 
333 	/// Lists the currently defined debug versions (debug specifications)
334 	string[] debugVersions() @property nothrow
335 	{
336 		return _debugVersions;
337 	}
338 
339 	/// Lists all configurations defined in the package description
340 	string[] configurations() @property
341 	{
342 		return _dub.project.configurations;
343 	}
344 
345 	PackageBuildSettings rootPackageBuildSettings() @property
346 	{
347 		auto pkg = _dub.project.rootPackage;
348 		BuildSettings settings = pkg.getBuildSettings(_platform, _configuration);
349 		return PackageBuildSettings(settings,
350 				pkg.path.toString,
351 				pkg.name,
352 				_dub.project.rootPackage.recipePath.toNativeString());
353 	}
354 
355 	/// 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")
356 	string[] buildTypes() const @property
357 	{
358 		string[] types = [
359 			"plain", "debug", "release", "release-debug", "release-nobounds",
360 			"unittest", "docs", "ddox", "profile", "profile-gc", "cov", "unittest-cov"
361 		];
362 		foreach (type, info; _dub.project.rootPackage.recipe.buildTypes)
363 			types ~= type;
364 		return types;
365 	}
366 
367 	/// Gets the current selected configuration
368 	string configuration() const @property
369 	{
370 		return _configuration;
371 	}
372 
373 	/// Selects a new configuration and updates the import paths accordingly
374 	/// Returns: `false` if there are no import paths in the new configuration
375 	bool setConfiguration(string configuration)
376 	{
377 		if (!_dub.project.configurations.canFind(configuration))
378 			return false;
379 		_configuration = configuration;
380 		_settingsTemplate = cast() _dub.project.rootPackage.getBuildSettings(configuration);
381 		return updateImportPaths(false);
382 	}
383 
384 	/// List all possible arch types for current set compiler
385 	string[] archTypes() const @property
386 	{
387 		auto types = appender!(string[]);
388 		types ~= ["x86_64", "x86"];
389 
390 		string compilerName = _compiler.name;
391 
392 		if (compilerName == "dmd")
393 		{
394 			// https://github.com/dlang/dub/blob/master/source/dub/compilers/dmd.d#L110
395 			version (Windows)
396 			{
397 				types ~= ["x86_omf", "x86_mscoff"];
398 			}
399 		}
400 		else if (compilerName == "gdc")
401 		{
402 			// https://github.com/dlang/dub/blob/master/source/dub/compilers/gdc.d#L69
403 			types ~= ["arm", "arm_thumb"];
404 		}
405 		else if (compilerName == "ldc")
406 		{
407 			// https://github.com/dlang/dub/blob/master/source/dub/compilers/ldc.d#L80
408 			types ~= ["aarch64", "powerpc64"];
409 		}
410 
411 		return types.data;
412 	}
413 
414 	/// ditto
415 	ArchType[] extendedArchTypes() const @property
416 	{
417 		auto types = appender!(ArchType[]);
418 		string compilerName = _compiler.name;
419 
420 		if (compilerName == "dmd")
421 		{
422 			types ~= [
423 				ArchType("", "(compiler default)"),
424 				ArchType("x86_64"),
425 				ArchType("x86")
426 			];
427 			// https://github.com/dlang/dub/blob/master/source/dub/compilers/dmd.d#L110
428 			version (Windows)
429 			{
430 				types ~= [ArchType("x86_omf"), ArchType("x86_mscoff")];
431 			}
432 		}
433 		else if (compilerName == "gdc")
434 		{
435 			// https://github.com/dlang/dub/blob/master/source/dub/compilers/gdc.d#L69
436 			types ~= [
437 				ArchType("", "(compiler default)"),
438 				ArchType("x86_64", "64-bit (current platform)"),
439 				ArchType("x86", "32-bit (current platform)"),
440 				ArchType("arm"),
441 				ArchType("arm_thumb")
442 			];
443 		}
444 		else if (compilerName == "ldc")
445 		{
446 			types ~= [
447 				ArchType("", "(compiler default)"),
448 				ArchType("x86_64"),
449 				ArchType("x86")
450 			];
451 			// https://github.com/dlang/dub/blob/master/source/dub/compilers/ldc.d#L80
452 			types ~= [
453 				ArchType("aarch64"),
454 				ArchType("powerpc64"),
455 				ArchType("wasm32-unknown-unknown-wasm", "WebAssembly")
456 			];
457 		}
458 
459 		return types.data;
460 	}
461 
462 	/// Returns the current selected arch type, or empty string for compiler default.
463 	string archType() const @property
464 	{
465 		return _archType;
466 	}
467 
468 	/// Selects a new arch type and updates the import paths accordingly
469 	/// Returns: `false` if there are no import paths in the new arch type
470 	bool setArchType(string type)
471 	{
472 		try
473 		{
474 			_platform = _compiler.determinePlatform(_settings, _compilerBinaryName, type);
475 		}
476 		catch (Exception e)
477 		{
478 			return false;
479 		}
480 
481 		_archType = type;
482 		return updateImportPaths(false);
483 	}
484 
485 	/// Returns the current selected build type
486 	string buildType() const @property
487 	{
488 		return _buildType;
489 	}
490 
491 	/// Selects a new build type and updates the import paths accordingly
492 	/// Returns: `false` if there are no import paths in the new build type
493 	bool setBuildType(string type)
494 	{
495 		if (buildTypes.canFind(type))
496 		{
497 			_buildType = type;
498 			return updateImportPaths(false);
499 		}
500 		else
501 		{
502 			return false;
503 		}
504 	}
505 
506 	/// Returns the current selected compiler
507 	string compiler() const @property
508 	{
509 		return _compilerBinaryName;
510 	}
511 
512 	/// Selects a new compiler for building
513 	/// Returns: `false` if the compiler does not exist or some setting is
514 	/// invalid.
515 	///
516 	/// If the current architecture does not exist with this compiler it will be
517 	/// reset to the compiler default. (empty string)
518 	bool setCompiler(string compiler)
519 	{
520 		try
521 		{
522 			_compilerBinaryName = compiler;
523 			_compiler = getCompiler(compiler); // make sure it gets a valid compiler
524 		}
525 		catch (Exception e)
526 		{
527 			return false;
528 		}
529 
530 		try
531 		{
532 			_platform = _compiler.determinePlatform(_settings, _compilerBinaryName, _archType);
533 		}
534 		catch (UnsupportedArchitectureException e)
535 		{
536 			if (_archType.length)
537 			{
538 				_archType = "";
539 				return setCompiler(compiler);
540 			}
541 			return false;
542 		}
543 
544 		_settingsTemplate.getPlatformSettings(_settings, _platform,
545 			_dub.project.rootPackage.path);
546 		return _compiler !is null;
547 	}
548 
549 	/// Returns the project name
550 	string name() const @property
551 	{
552 		return _dub.projectName;
553 	}
554 
555 	/// Returns the project path
556 	auto path() const @property
557 	{
558 		return _dub.projectPath;
559 	}
560 
561 	/// Returns whether there is a target set to build. If this is false then build will throw an exception.
562 	bool canBuild() const @property
563 	{
564 		if (_settings.targetType == TargetType.none || _settings.targetType == TargetType.sourceLibrary
565 				|| !_dub.project.configurations.canFind(_configuration))
566 			return false;
567 		return true;
568 	}
569 
570 	/// Asynchroniously builds the project WITHOUT OUTPUT. This is intended for linting code and showing build errors quickly inside the IDE.
571 	Future!(BuildIssue[]) build()
572 	{
573 		import std.process : thisProcessID;
574 		import std.file : tempDir;
575 		import std.random : uniform;
576 
577 		validateBuildConfiguration();
578 
579 		// copy to this thread
580 		auto compiler = _compiler;
581 		auto buildPlatform = _platform;
582 
583 		GeneratorSettings settings;
584 		settings.platform = buildPlatform;
585 		settings.config = _configuration;
586 		settings.buildType = _buildType;
587 		settings.compiler = compiler;
588 		settings.buildSettings = _settings;
589 
590 		auto ret = new typeof(return);
591 		new Thread({
592 			try
593 			{
594 				auto issues = appender!(BuildIssue[]);
595 
596 				settings.compileCallback = (status, output) {
597 					string[] lines = output.splitLines;
598 					foreach (line; lines)
599 					{
600 						auto match = line.matchFirst(errorFormat);
601 						if (match)
602 						{
603 							issues ~= BuildIssue(match[2].to!int, match[3].toOr!int(0),
604 								match[1], match[4].to!ErrorType, match[5]);
605 						}
606 						else
607 						{
608 							auto contMatch = line.matchFirst(errorFormatCont);
609 							if (issues.data.length && contMatch)
610 							{
611 								issues ~= BuildIssue(contMatch[2].to!int,
612 									contMatch[3].toOr!int(1), contMatch[1],
613 									issues.data[$ - 1].type, contMatch[4], true);
614 							}
615 							else if (line.canFind("is deprecated"))
616 							{
617 								auto deprMatch = line.matchFirst(deprecationFormat);
618 								if (deprMatch)
619 								{
620 									issues ~= BuildIssue(deprMatch[2].to!int, deprMatch[3].toOr!int(1),
621 										deprMatch[1], ErrorType.Deprecation,
622 										deprMatch[4] ~ " is deprecated, use " ~ deprMatch[5] ~ " instead.");
623 								}
624 							}
625 						}
626 					}
627 				};
628 				try
629 				{
630 					import workspaced.dub.lintgenerator : DubLintGenerator;
631 
632 					new DubLintGenerator(_dub.project).generate(settings);
633 				}
634 				catch (Exception e)
635 				{
636 					if (!e.msg.matchFirst(harmlessExceptionFormat))
637 						throw e;
638 				}
639 				ret.finish(issues.data);
640 			}
641 			catch (Throwable t)
642 			{
643 				ret.error(t);
644 			}
645 		}).start();
646 		return ret;
647 	}
648 
649 	/// Converts the root package recipe to another format.
650 	/// Params:
651 	///     format = either "json" or "sdl".
652 	string convertRecipe(string format)
653 	{
654 		import dub.recipe.io : serializePackageRecipe;
655 		import std.array : appender;
656 
657 		auto dst = appender!string;
658 		serializePackageRecipe(dst, _dub.project.rootPackage.rawRecipe, "dub." ~ format);
659 		return dst.data;
660 	}
661 
662 	/// Tries to find a suitable code byte range where a given dub build issue
663 	/// applies to.
664 	/// Returns: `[pos, pos]` if not found, otherwise range in bytes which might
665 	/// not contain the position at all.
666 	int[2] resolveDiagnosticRange(scope const(char)[] code, int position,
667 			scope const(char)[] diagnostic)
668 	{
669 		import dparse.lexer : getTokensForParser, LexerConfig;
670 		import dparse.parser : parseModule;
671 		import dparse.rollback_allocator : RollbackAllocator;
672 		import workspaced.dub.diagnostics : resolveDubDiagnosticRange;
673 
674 		LexerConfig config;
675 		RollbackAllocator rba;
676 		auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache);
677 		auto parsed = parseModule(tokens, "equal_finder.d", &rba);
678 
679 		return resolveDubDiagnosticRange(code, tokens, parsed, position, diagnostic);
680 	}
681 
682 private:
683 	Dub _dub;
684 	bool _dubRunning = false;
685 	string _configuration;
686 	string _archType = "";
687 	string _buildType = "debug";
688 	string _compilerBinaryName;
689 	Compiler _compiler;
690 	BuildSettingsTemplate _settingsTemplate;
691 	BuildSettings _settings;
692 	BuildPlatform _platform;
693 	string[] _importPaths, _stringImportPaths, _importFiles, _versions, _debugVersions;
694 }
695 
696 ///
697 enum ErrorType : ubyte
698 {
699 	///
700 	Error = 0,
701 	///
702 	Warning = 1,
703 	///
704 	Deprecation = 2
705 }
706 
707 /// Returned by build
708 struct BuildIssue
709 {
710 	///
711 	int line, column;
712 	///
713 	string file;
714 	/// The error type (Error/Warning/Deprecation) outputted by dmd or inherited from the last error if this is additional information of the last issue. (indicated by cont)
715 	ErrorType type;
716 	///
717 	string text;
718 	/// true if this is additional error information for the last error.
719 	bool cont;
720 }
721 
722 private enum ignoreCopy; // UDA for ignored values on copy
723 /// returned by rootPackageBuildSettings
724 struct PackageBuildSettings
725 {
726 	/// construct from dub build settings
727 	this(BuildSettings dubBuildSettings, string packagePath, string packageName, string recipePath)
728 	{
729 		foreach (i, ref val; this.tupleof)
730 		{
731 			alias attr = __traits(getAttributes, this.tupleof[i]);
732 			static if (attr.length == 0 || !__traits(isSame, attr[0], ignoreCopy))
733 			{
734 				enum name = __traits(identifier, this.tupleof[i]);
735 				val = __traits(getMember, dubBuildSettings, name);
736 			}
737 		}
738 		this.packagePath = packagePath;
739 		this.packageName = packageName;
740 		this.recipePath = recipePath;
741 
742 		if (!targetName.length)
743 			targetName = packageName;
744 
745 		version (Windows)
746 			targetName ~= ".exe";
747 
748 		this.targetType = dubBuildSettings.targetType.to!string;
749 		foreach (enumMember; __traits(allMembers, BuildOption))
750 		{
751 			enum value = __traits(getMember, BuildOption, enumMember);
752 			if (value != 0 && dubBuildSettings.options.opDispatch!enumMember)
753 				this.buildOptions ~= enumMember;
754 		}
755 		foreach (enumMember; __traits(allMembers, BuildRequirement))
756 		{
757 			enum value = __traits(getMember, BuildRequirement, enumMember);
758 			if (value != 0 && dubBuildSettings.requirements.opDispatch!enumMember)
759 				this.buildRequirements ~= enumMember;
760 		}
761 	}
762 
763 	@ignoreCopy
764 	string packagePath;
765 	@ignoreCopy
766 	string packageName;
767 	@ignoreCopy
768 	string recipePath;
769 
770 	string targetPath; /// same as dub BuildSettings
771 	string targetName; /// same as dub BuildSettings
772 	string workingDirectory; /// same as dub BuildSettings
773 	string mainSourceFile; /// same as dub BuildSettings
774 	string[] dflags; /// same as dub BuildSettings
775 	string[] lflags; /// same as dub BuildSettings
776 	string[] libs; /// same as dub BuildSettings
777 	string[] linkerFiles; /// same as dub BuildSettings
778 	string[] sourceFiles; /// same as dub BuildSettings
779 	string[] copyFiles; /// same as dub BuildSettings
780 	string[] extraDependencyFiles; /// same as dub BuildSettings
781 	string[] versions; /// same as dub BuildSettings
782 	string[] debugVersions; /// same as dub BuildSettings
783 	string[] versionFilters; /// same as dub BuildSettings
784 	string[] debugVersionFilters; /// same as dub BuildSettings
785 	string[] importPaths; /// same as dub BuildSettings
786 	string[] stringImportPaths; /// same as dub BuildSettings
787 	string[] importFiles; /// same as dub BuildSettings
788 	string[] stringImportFiles; /// same as dub BuildSettings
789 	string[] preGenerateCommands; /// same as dub BuildSettings
790 	string[] postGenerateCommands; /// same as dub BuildSettings
791 	string[] preBuildCommands; /// same as dub BuildSettings
792 	string[] postBuildCommands; /// same as dub BuildSettings
793 	string[] preRunCommands; /// same as dub BuildSettings
794 	string[] postRunCommands; /// same as dub BuildSettings
795 
796 	@ignoreCopy:
797 
798 	string targetType; /// same as dub BuildSettings
799 	string[] buildOptions; /// same as dub BuildSettings
800 	string[] buildRequirements; /// same as dub BuildSettings
801 }
802 
803 private:
804 
805 T toOr(T)(string s, T defaultValue)
806 {
807 	if (!s || !s.length)
808 		return defaultValue;
809 	return s.to!T;
810 }
811 
812 enum harmlessExceptionFormat = ctRegex!(`failed with exit code`, "g");
813 enum errorFormat = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\): (Deprecation|Warning|Error): (.*)`, "gi");
814 enum errorFormatCont = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\):[ ]{6,}(.*)`, "g");
815 enum deprecationFormat = ctRegex!(
816 			`(.*?)\((\d+)(?:,(\d+))?\): (.*?) is deprecated, use (.*?) instead.$`, "g");
817 
818 struct DubPackageInfo
819 {
820 	string[string] dependencies;
821 	string ver;
822 	string name;
823 	string path;
824 	string description;
825 	string homepage;
826 	const(string)[] authors;
827 	string copyright;
828 	string license;
829 	DubPackageInfo[] subPackages;
830 
831 	void fill(in PackageRecipe recipe)
832 	{
833 		description = recipe.description;
834 		homepage = recipe.homepage;
835 		authors = recipe.authors;
836 		copyright = recipe.copyright;
837 		license = recipe.license;
838 
839 		foreach (subpackage; recipe.subPackages)
840 		{
841 			DubPackageInfo info;
842 			info.ver = subpackage.recipe.version_;
843 			info.name = subpackage.recipe.name;
844 			info.path = subpackage.path;
845 			info.fill(subpackage.recipe);
846 		}
847 	}
848 }
849 
850 DubPackageInfo getInfo(in Package dep)
851 {
852 	DubPackageInfo info;
853 	info.name = dep.name;
854 	info.ver = dep.version_.toString;
855 	info.path = dep.path.toString;
856 	info.fill(dep.recipe);
857 	foreach (subDep; dep.getAllDependencies())
858 	{
859 		info.dependencies[subDep.name] = subDep.spec.toString;
860 	}
861 	return info;
862 }
863 
864 auto listDependencies(scope const Project project)
865 {
866 	auto deps = project.dependencies;
867 	DubPackageInfo[] dependencies;
868 	if (deps is null)
869 		return dependencies;
870 	foreach (dep; deps)
871 	{
872 		dependencies ~= getInfo(dep);
873 	}
874 	return dependencies;
875 }
876 
877 string[] listDependencies(scope const Package pkg)
878 {
879 	auto deps = pkg.getAllDependencies();
880 	string[] dependencies;
881 	if (deps is null)
882 		return dependencies;
883 	foreach (dep; deps)
884 		dependencies ~= dep.name;
885 	return dependencies;
886 }
887 
888 ///
889 struct ArchType
890 {
891 	/// Value to pass into other calls
892 	string value;
893 	/// UI label override or null if none
894 	string label;
895 }