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() @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() @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 		version (Windows)
383 		{
384 			if (compilerName == "dmd")
385 			{
386 				types ~= "x86_mscoff";
387 			}
388 		}
389 		if (compilerName == "gdc")
390 		{
391 			types ~= ["arm", "arm_thumb"];
392 		}
393 
394 		return types;
395 	}
396 
397 	/// Returns the current selected arch type
398 	string archType() @property
399 	{
400 		return _archType;
401 	}
402 
403 	/// Selects a new arch type and updates the import paths accordingly
404 	/// Returns: `false` if there are no import paths in the new arch type
405 	bool setArchType(JSONValue request)
406 	{
407 		enforce(request.type == JSONType.object && "arch-type" in request, "arch-type not in request");
408 		auto type = request["arch-type"].fromJSON!string;
409 		if (archTypes.canFind(type))
410 		{
411 			_archType = type;
412 			return updateImportPaths(false);
413 		}
414 		else
415 		{
416 			return false;
417 		}
418 	}
419 
420 	/// Returns the current selected build type
421 	string buildType() @property
422 	{
423 		return _buildType;
424 	}
425 
426 	/// Selects a new build type and updates the import paths accordingly
427 	/// Returns: `false` if there are no import paths in the new build type
428 	bool setBuildType(JSONValue request)
429 	{
430 		enforce(request.type == JSONType.object && "build-type" in request, "build-type not in request");
431 		auto type = request["build-type"].fromJSON!string;
432 		if (buildTypes.canFind(type))
433 		{
434 			_buildType = type;
435 			return updateImportPaths(false);
436 		}
437 		else
438 		{
439 			return false;
440 		}
441 	}
442 
443 	/// Returns the current selected compiler
444 	string compiler() @property
445 	{
446 		return _compilerBinaryName;
447 	}
448 
449 	/// Selects a new compiler for building
450 	/// Returns: `false` if the compiler does not exist
451 	bool setCompiler(string compiler)
452 	{
453 		try
454 		{
455 			_compilerBinaryName = compiler;
456 			_compiler = getCompiler(compiler); // make sure it gets a valid compiler
457 		}
458 		catch (Exception e)
459 		{
460 			return false;
461 		}
462 		_platform = _compiler.determinePlatform(_settings, _compilerBinaryName, _archType);
463 		_settingsTemplate.getPlatformSettings(_settings, _platform, _dub.project.rootPackage.path);
464 		return _compiler !is null;
465 	}
466 
467 	/// Returns the project name
468 	string name() @property
469 	{
470 		return _dub.projectName;
471 	}
472 
473 	/// Returns the project path
474 	auto path() @property
475 	{
476 		return _dub.projectPath;
477 	}
478 
479 	/// Returns whether there is a target set to build. If this is false then build will throw an exception.
480 	bool canBuild() @property
481 	{
482 		if (_settings.targetType == TargetType.none || _settings.targetType == TargetType.sourceLibrary
483 				|| !_dub.project.configurations.canFind(_configuration))
484 			return false;
485 		return true;
486 	}
487 
488 	/// Asynchroniously builds the project WITHOUT OUTPUT. This is intended for linting code and showing build errors quickly inside the IDE.
489 	Future!(BuildIssue[]) build()
490 	{
491 		import std.process : thisProcessID;
492 		import std.file : tempDir;
493 		import std.random : uniform;
494 
495 		validateBuildConfiguration();
496 
497 		// copy to this thread
498 		auto compiler = _compiler;
499 		auto buildPlatform = _platform;
500 
501 		GeneratorSettings settings;
502 		settings.platform = buildPlatform;
503 		settings.config = _configuration;
504 		settings.buildType = _buildType;
505 		settings.compiler = compiler;
506 		settings.buildSettings = _settings;
507 
508 		auto ret = new typeof(return);
509 		new Thread({
510 			try
511 			{
512 				auto issues = appender!(BuildIssue[]);
513 
514 				settings.compileCallback = (status, output) {
515 					string[] lines = output.splitLines;
516 					foreach (line; lines)
517 					{
518 						auto match = line.matchFirst(errorFormat);
519 						if (match)
520 						{
521 							issues ~= BuildIssue(match[2].to!int, match[3].toOr!int(0),
522 								match[1], match[4].to!ErrorType, match[5]);
523 						}
524 						else
525 						{
526 							auto contMatch = line.matchFirst(errorFormatCont);
527 							if (issues.data.length && contMatch)
528 							{
529 								issues ~= BuildIssue(contMatch[2].to!int,
530 									contMatch[3].toOr!int(1), contMatch[1],
531 									issues.data[$ - 1].type, contMatch[4], true);
532 							}
533 							else if (line.canFind("is deprecated"))
534 							{
535 								auto deprMatch = line.matchFirst(deprecationFormat);
536 								if (deprMatch)
537 								{
538 									issues ~= BuildIssue(deprMatch[2].to!int, deprMatch[3].toOr!int(1),
539 										deprMatch[1], ErrorType.Deprecation,
540 										deprMatch[4] ~ " is deprecated, use " ~ deprMatch[5] ~ " instead.");
541 								}
542 							}
543 						}
544 					}
545 				};
546 				try
547 				{
548 					import workspaced.dub.lintgenerator : DubLintGenerator;
549 
550 					new DubLintGenerator(_dub.project).generate(settings);
551 				}
552 				catch (Exception e)
553 				{
554 					if (!e.msg.matchFirst(harmlessExceptionFormat))
555 						throw e;
556 				}
557 				ret.finish(issues.data);
558 			}
559 			catch (Throwable t)
560 			{
561 				ret.error(t);
562 			}
563 		}).start();
564 		return ret;
565 	}
566 
567 	/// Converts the root package recipe to another format.
568 	/// Params:
569 	///     format = either "json" or "sdl".
570 	string convertRecipe(string format)
571 	{
572 		import dub.recipe.io : serializePackageRecipe;
573 		import std.array : appender;
574 
575 		auto dst = appender!string;
576 		serializePackageRecipe(dst, _dub.project.rootPackage.rawRecipe, "dub." ~ format);
577 		return dst.data;
578 	}
579 
580 	/// Tries to find a suitable code byte range where a given dub build issue
581 	/// applies to.
582 	/// Returns: `[pos, pos]` if not found, otherwise range in bytes which might
583 	/// not contain the position at all.
584 	int[2] resolveDiagnosticRange(scope const(char)[] code, int position,
585 		scope const(char)[] diagnostic)
586 	{
587 		import dparse.lexer : getTokensForParser, LexerConfig;
588 		import dparse.parser : parseModule;
589 		import dparse.rollback_allocator : RollbackAllocator;
590 		import workspaced.dub.diagnostics : resolveDubDiagnosticRange;
591 
592 		LexerConfig config;
593 		RollbackAllocator rba;
594 		auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache);
595 		auto parsed = parseModule(tokens, "equal_finder.d", &rba);
596 
597 		return resolveDubDiagnosticRange(code, tokens, parsed, position, diagnostic);
598 	}
599 
600 private:
601 	Dub _dub;
602 	bool _dubRunning = false;
603 	string _configuration;
604 	string _archType = "x86_64";
605 	string _buildType = "debug";
606 	string _compilerBinaryName;
607 	Compiler _compiler;
608 	BuildSettingsTemplate _settingsTemplate;
609 	BuildSettings _settings;
610 	BuildPlatform _platform;
611 	string[] _importPaths, _stringImportPaths, _importFiles, _versions, _debugVersions;
612 }
613 
614 ///
615 enum ErrorType : ubyte
616 {
617 	///
618 	Error = 0,
619 	///
620 	Warning = 1,
621 	///
622 	Deprecation = 2
623 }
624 
625 /// Returned by build
626 struct BuildIssue
627 {
628 	///
629 	int line, column;
630 	///
631 	string file;
632 	/// 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)
633 	ErrorType type;
634 	///
635 	string text;
636 	/// true if this is additional error information for the last error.
637 	bool cont;
638 }
639 
640 /// returned by rootPackageBuildSettings
641 struct PackageBuildSettings
642 {
643 	/// construct from dub build settings
644 	this(BuildSettings dubBuildSettings, string packagePath, string packageName, string recipePath)
645 	{
646 		foreach (i, ref val; this.tupleof[0 .. __IGNORE_TRAIL])
647 		{
648 			enum name = __traits(identifier, this.tupleof[i]);
649 			static if (__traits(hasMember, dubBuildSettings, name))
650 				val = __traits(getMember, dubBuildSettings, name);
651 		}
652 		this.packagePath = packagePath;
653 		this.packageName = packageName;
654 		this.recipePath = recipePath;
655 
656 		if (!targetName.length)
657 			targetName = packageName;
658 
659 		version (Windows)
660 			targetName ~= ".exe";
661 
662 		this.targetType = dubBuildSettings.targetType.to!string;
663 		foreach (enumMember; __traits(allMembers, BuildOption))
664 		{
665 			enum value = __traits(getMember, BuildOption, enumMember);
666 			if (value != 0 && dubBuildSettings.options.opDispatch!enumMember)
667 				this.buildOptions ~= enumMember;
668 		}
669 		foreach (enumMember; __traits(allMembers, BuildRequirement))
670 		{
671 			enum value = __traits(getMember, BuildRequirement, enumMember);
672 			if (value != 0 && dubBuildSettings.requirements.opDispatch!enumMember)
673 				this.buildRequirements ~= enumMember;
674 		}
675 	}
676 
677 	string packagePath;
678 	string packageName;
679 	string recipePath;
680 
681 	string targetPath; /// same as dub BuildSettings
682 	string targetName; /// same as dub BuildSettings
683 	string workingDirectory; /// same as dub BuildSettings
684 	string mainSourceFile; /// same as dub BuildSettings
685 	string[] dflags; /// same as dub BuildSettings
686 	string[] lflags; /// same as dub BuildSettings
687 	string[] libs; /// same as dub BuildSettings
688 	string[] linkerFiles; /// same as dub BuildSettings
689 	string[] sourceFiles; /// same as dub BuildSettings
690 	string[] copyFiles; /// same as dub BuildSettings
691 	string[] extraDependencyFiles; /// same as dub BuildSettings
692 	string[] versions; /// same as dub BuildSettings
693 	string[] debugVersions; /// same as dub BuildSettings
694 	string[] versionFilters; /// same as dub BuildSettings
695 	string[] debugVersionFilters; /// same as dub BuildSettings
696 	string[] importPaths; /// same as dub BuildSettings
697 	string[] stringImportPaths; /// same as dub BuildSettings
698 	string[] importFiles; /// same as dub BuildSettings
699 	string[] stringImportFiles; /// same as dub BuildSettings
700 	string[] preGenerateCommands; /// same as dub BuildSettings
701 	string[] postGenerateCommands; /// same as dub BuildSettings
702 	string[] preBuildCommands; /// same as dub BuildSettings
703 	string[] postBuildCommands; /// same as dub BuildSettings
704 	string[] preRunCommands; /// same as dub BuildSettings
705 	string[] postRunCommands; /// same as dub BuildSettings
706 
707 	private enum __IGNORE_TRAIL = 2; // number of ignored settings below this line
708 
709 	string targetType; /// same as dub BuildSettings
710 	string[] buildOptions; /// same as dub BuildSettings
711 	string[] buildRequirements; /// same as dub BuildSettings
712 }
713 
714 private:
715 
716 T toOr(T)(string s, T defaultValue)
717 {
718 	if (!s || !s.length)
719 		return defaultValue;
720 	return s.to!T;
721 }
722 
723 enum harmlessExceptionFormat = ctRegex!(`failed with exit code`, "g");
724 enum errorFormat = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\): (Deprecation|Warning|Error): (.*)`, "gi");
725 enum errorFormatCont = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\):[ ]{6,}(.*)`, "g");
726 enum deprecationFormat = ctRegex!(
727 			`(.*?)\((\d+)(?:,(\d+))?\): (.*?) is deprecated, use (.*?) instead.$`, "g");
728 
729 struct DubPackageInfo
730 {
731 	string[string] dependencies;
732 	string ver;
733 	string name;
734 	string path;
735 	string description;
736 	string homepage;
737 	const(string)[] authors;
738 	string copyright;
739 	string license;
740 	DubPackageInfo[] subPackages;
741 
742 	void fill(in PackageRecipe recipe)
743 	{
744 		description = recipe.description;
745 		homepage = recipe.homepage;
746 		authors = recipe.authors;
747 		copyright = recipe.copyright;
748 		license = recipe.license;
749 
750 		foreach (subpackage; recipe.subPackages)
751 		{
752 			DubPackageInfo info;
753 			info.ver = subpackage.recipe.version_;
754 			info.name = subpackage.recipe.name;
755 			info.path = subpackage.path;
756 			info.fill(subpackage.recipe);
757 		}
758 	}
759 }
760 
761 DubPackageInfo getInfo(in Package dep)
762 {
763 	DubPackageInfo info;
764 	info.name = dep.name;
765 	info.ver = dep.version_.toString;
766 	info.path = dep.path.toString;
767 	info.fill(dep.recipe);
768 	foreach (subDep; dep.getAllDependencies())
769 	{
770 		info.dependencies[subDep.name] = subDep.spec.toString;
771 	}
772 	return info;
773 }
774 
775 auto listDependencies(scope const Project project)
776 {
777 	auto deps = project.dependencies;
778 	DubPackageInfo[] dependencies;
779 	if (deps is null)
780 		return dependencies;
781 	foreach (dep; deps)
782 	{
783 		dependencies ~= getInfo(dep);
784 	}
785 	return dependencies;
786 }
787 
788 string[] listDependencies(scope const Package pkg)
789 {
790 	auto deps = pkg.getAllDependencies();
791 	string[] dependencies;
792 	if (deps is null)
793 		return dependencies;
794 	foreach (dep; deps)
795 		dependencies ~= dep.name;
796 	return dependencies;
797 }