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