1 /**
2 	Defines the behavior of the DUB command line client.
3 
4 	Copyright: © 2012-2013 Matthias Dondorff, Copyright © 2012-2016 Sönke Ludwig
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Matthias Dondorff, Sönke Ludwig
7 */
8 module dub.commandline;
9 
10 import dub.compilers.compiler;
11 import dub.dependency;
12 import dub.dub;
13 import dub.generators.generator;
14 import dub.internal.vibecompat.core.file;
15 import dub.internal.vibecompat.core.log;
16 import dub.internal.vibecompat.data.json;
17 import dub.internal.vibecompat.inet.url;
18 import dub.package_;
19 import dub.packagemanager;
20 import dub.packagesuppliers;
21 import dub.project;
22 import dub.internal.utils : getDUBVersion, getClosestMatch, getTempFile;
23 
24 import std.algorithm;
25 import std.array;
26 import std.conv;
27 import std.encoding;
28 import std.exception;
29 import std.file;
30 import std.getopt;
31 import std.process;
32 import std.stdio;
33 import std..string;
34 import std.typecons : Tuple, tuple;
35 import std.variant;
36 import std.path: setExtension;
37 
38 /** Retrieves a list of all available commands.
39 
40 	Commands are grouped by category.
41 */
42 CommandGroup[] getCommands()
43 {
44 	return [
45 		CommandGroup("Package creation",
46 			new InitCommand
47 		),
48 		CommandGroup("Build, test and run",
49 			new RunCommand,
50 			new BuildCommand,
51 			new TestCommand,
52 			new LintCommand,
53 			new GenerateCommand,
54 			new DescribeCommand,
55 			new CleanCommand,
56 			new DustmiteCommand
57 		),
58 		CommandGroup("Package management",
59 			new FetchCommand,
60 			new InstallCommand,
61 			new AddCommand,
62 			new RemoveCommand,
63 			new UninstallCommand,
64 			new UpgradeCommand,
65 			new AddPathCommand,
66 			new RemovePathCommand,
67 			new AddLocalCommand,
68 			new RemoveLocalCommand,
69 			new ListCommand,
70 			new SearchCommand,
71 			new AddOverrideCommand,
72 			new RemoveOverrideCommand,
73 			new ListOverridesCommand,
74 			new CleanCachesCommand,
75 			new ConvertCommand,
76 		)
77 	];
78 }
79 
80 
81 /** Processes the given command line and executes the appropriate actions.
82 
83 	Params:
84 		args = This command line argument array as received in `main`. The first
85 			entry is considered to be the name of the binary invoked.
86 
87 	Returns:
88 		Returns the exit code that is supposed to be returned to the system.
89 */
90 int runDubCommandLine(string[] args)
91 {
92 	logDiagnostic("DUB version %s", getDUBVersion());
93 
94 	version(Windows){
95 		// rdmd uses $TEMP to compute a temporary path. since cygwin substitutes backslashes
96 		// with slashes, this causes OPTLINK to fail (it thinks path segments are options)
97 		// we substitute the other way around here to fix this.
98 		environment["TEMP"] = environment["TEMP"].replace("/", "\\");
99 	}
100 
101 	// special stdin syntax
102 	if (args.length >= 2 && args[1] == "-")
103 	{
104 		auto path = getTempFile("app", ".d");
105 		stdin.byChunk(4096).joiner.toFile(path.toNativeString());
106 		args = args[0] ~ [path.toNativeString()] ~ args[2..$];
107 	}
108 
109 	// create the list of all supported commands
110 	CommandGroup[] commands = getCommands();
111 	string[] commandNames = commands.map!(g => g.commands.map!(c => c.name).array).join.array;
112 
113 	// Shebang syntax support for files without .d extension
114 	if (args.length >= 2 && !args[1].endsWith(".d") && !args[1].startsWith("-") && !commandNames.canFind(args[1])) {
115 		if (exists(args[1])) {
116 			auto path = getTempFile("app", ".d");
117 			copy(args[1], path.toNativeString());
118 			args[1] = path.toNativeString();
119 		} else if (exists(args[1].setExtension(".d"))) {
120 			args[1] = args[1].setExtension(".d");
121 		}
122 	}
123 
124 	// special single-file package shebang syntax
125 	if (args.length >= 2 && args[1].endsWith(".d")) {
126 		args = args[0] ~ ["run", "-q", "--temp-build", "--single", args[1], "--"] ~ args[2 ..$];
127 	}
128 
129 	// split application arguments from DUB arguments
130 	string[] app_args;
131 	auto app_args_idx = args.countUntil("--");
132 	if (app_args_idx >= 0) {
133 		app_args = args[app_args_idx+1 .. $];
134 		args = args[0 .. app_args_idx];
135 	}
136 	args = args[1 .. $]; // strip the application name
137 
138 	// handle direct dub options
139 	if (args.length) switch (args[0])
140 	{
141 	case "--version":
142 		showVersion();
143 		return 0;
144 
145 	default:
146 		break;
147 	}
148 
149 	// parse general options
150 	CommonOptions options;
151 	LogLevel loglevel = LogLevel.info;
152 
153 	auto common_args = new CommandArgs(args);
154 	try {
155 		options.prepare(common_args);
156 
157 		if (options.vverbose) loglevel = LogLevel.debug_;
158 		else if (options.verbose) loglevel = LogLevel.diagnostic;
159 		else if (options.vquiet) loglevel = LogLevel.none;
160 		else if (options.quiet) loglevel = LogLevel.warn;
161 		else if (options.verror) loglevel = LogLevel.error;
162 		setLogLevel(loglevel);
163 	} catch (Throwable e) {
164 		logError("Error processing arguments: %s", e.msg);
165 		logDiagnostic("Full exception: %s", e.toString().sanitize);
166 		logInfo("Run 'dub help' for usage information.");
167 		return 1;
168 	}
169 
170 	if (options.root_path.empty)
171 		options.root_path = getcwd();
172 	else
173 	{
174 		import std.path : absolutePath, buildNormalizedPath;
175 
176 		options.root_path = options.root_path.absolutePath.buildNormalizedPath;
177 	}
178 
179 	// extract the command
180 	string cmdname;
181 	args = common_args.extractRemainingArgs();
182 	if (args.length >= 1 && !args[0].startsWith("-")) {
183 		cmdname = args[0];
184 		args = args[1 .. $];
185 	} else {
186 		if (options.help) {
187 			showHelp(commands, common_args);
188 			return 0;
189 		}
190 		cmdname = "run";
191 	}
192 	auto command_args = new CommandArgs(args);
193 
194 	if (cmdname == "help") {
195 		showHelp(commands, common_args);
196 		return 0;
197 	}
198 
199 	// find the selected command
200 	Command cmd;
201 	foreach (grp; commands)
202 		foreach (c; grp.commands)
203 			if (c.name == cmdname) {
204 				cmd = c;
205 				break;
206 			}
207 
208 	if (!cmd) {
209 		logError("Unknown command: %s", cmdname);
210 		writeln();
211 		showHelp(commands, common_args);
212 		return 1;
213 	}
214 
215 	// process command line options for the selected command
216 	try {
217 		cmd.prepare(command_args);
218 		enforceUsage(cmd.acceptsAppArgs || app_args.length == 0, cmd.name ~ " doesn't accept application arguments.");
219 	} catch (Throwable e) {
220 		logError("Error processing arguments: %s", e.msg);
221 		logDiagnostic("Full exception: %s", e.toString().sanitize);
222 		logInfo("Run 'dub help' for usage information.");
223 		return 1;
224 	}
225 
226 	if (options.help) {
227 		showCommandHelp(cmd, command_args, common_args);
228 		return 0;
229 	}
230 
231 	auto remaining_args = command_args.extractRemainingArgs();
232 	if (remaining_args.any!(a => a.startsWith("-"))) {
233 		logError("Unknown command line flags: %s", remaining_args.filter!(a => a.startsWith("-")).array.join(" "));
234 		logError(`Type "dub %s -h" to get a list of all supported flags.`, cmdname);
235 		return 1;
236 	}
237 
238 	Dub dub;
239 
240 	// initialize the root package
241 	if (!cmd.skipDubInitialization) {
242 		if (options.bare) {
243 			dub = new Dub(NativePath(getcwd()));
244 			dub.rootPath = NativePath(options.root_path);
245 			dub.defaultPlacementLocation = options.placementLocation;
246 		} else {
247 			// initialize DUB
248 			auto package_suppliers = options.registry_urls
249 				.map!((url) {
250 					// Allow to specify fallback mirrors as space separated urls. Undocumented as we
251 					// should simply retry over all registries instead of using a special
252 					// FallbackPackageSupplier.
253 					auto urls = url.splitter(' ');
254 					PackageSupplier ps = getRegistryPackageSupplier(urls.front);
255 					urls.popFront;
256 					if (!urls.empty)
257 						ps = new FallbackPackageSupplier(ps ~ urls.map!getRegistryPackageSupplier.array);
258 					return ps;
259 				})
260 				.array;
261 			dub = new Dub(options.root_path, package_suppliers, options.skipRegistry);
262 			dub.dryRun = options.annotate;
263 			dub.defaultPlacementLocation = options.placementLocation;
264 
265 			// make the CWD package available so that for example sub packages can reference their
266 			// parent package.
267 			try dub.packageManager.getOrLoadPackage(NativePath(options.root_path));
268 			catch (Exception e) { logDiagnostic("No valid package found in current working directory: %s", e.msg); }
269 		}
270 	}
271 
272 	// execute the command
273 	try return cmd.execute(dub, remaining_args, app_args);
274 	catch (UsageException e) {
275 		logError("%s", e.msg);
276 		logDebug("Full exception: %s", e.toString().sanitize);
277 		logInfo(`Run "dub %s -h" for more information about the "%s" command.`, cmdname, cmdname);
278 		return 1;
279 	}
280 	catch (Throwable e) {
281 		logError("%s", e.msg);
282 		logDebug("Full exception: %s", e.toString().sanitize);
283 		return 2;
284 	}
285 }
286 
287 
288 /** Contains and parses options common to all commands.
289 */
290 struct CommonOptions {
291 	bool verbose, vverbose, quiet, vquiet, verror;
292 	bool help, annotate, bare;
293 	string[] registry_urls;
294 	string root_path;
295 	SkipPackageSuppliers skipRegistry = SkipPackageSuppliers.none;
296 	PlacementLocation placementLocation = PlacementLocation.user;
297 
298 	/// Parses all common options and stores the result in the struct instance.
299 	void prepare(CommandArgs args)
300 	{
301 		args.getopt("h|help", &help, ["Display general or command specific help"]);
302 		args.getopt("root", &root_path, ["Path to operate in instead of the current working dir"]);
303 		args.getopt("registry", &registry_urls, [
304 			"Search the given registry URL first when resolving dependencies. Can be specified multiple times. Available registry types:",
305 			"  DUB: URL to DUB registry (default)",
306 			"  Maven: URL to Maven repository + group id containing dub packages as artifacts. E.g. mvn+http://localhost:8040/maven/libs-release/dubpackages",
307 			]);
308 		args.getopt("skip-registry", &skipRegistry, [
309 			"Sets a mode for skipping the search on certain package registry types:",
310 			"  none: Search all configured or default registries (default)",
311 			"  standard: Don't search the main registry (e.g. "~defaultRegistryURLs[0]~")",
312 			"  configured: Skip all default and user configured registries",
313 			"  all: Only search registries specified with --registry",
314 			]);
315 		args.getopt("annotate", &annotate, ["Do not perform any action, just print what would be done"]);
316 		args.getopt("bare", &bare, ["Read only packages contained in the current directory"]);
317 		args.getopt("v|verbose", &verbose, ["Print diagnostic output"]);
318 		args.getopt("vverbose", &vverbose, ["Print debug output"]);
319 		args.getopt("q|quiet", &quiet, ["Only print warnings and errors"]);
320 		args.getopt("verror", &verror, ["Only print errors"]);
321 		args.getopt("vquiet", &vquiet, ["Print no messages"]);
322 		args.getopt("cache", &placementLocation, ["Puts any fetched packages in the specified location [local|system|user]."]);
323 	}
324 }
325 
326 /** Encapsulates a set of application arguments.
327 
328 	This class serves two purposes. The first is to provide an API for parsing
329 	command line arguments (`getopt`). At the same time it records all calls
330 	to `getopt` and provides a list of all possible options using the
331 	`recognizedArgs` property.
332 */
333 class CommandArgs {
334 	struct Arg {
335 		Variant defaultValue;
336 		Variant value;
337 		string names;
338 		string[] helpText;
339 	}
340 	private {
341 		string[] m_args;
342 		Arg[] m_recognizedArgs;
343 	}
344 
345 	/** Initializes the list of source arguments.
346 
347 		Note that all array entries are considered application arguments (i.e.
348 		no application name entry is present as the first entry)
349 	*/
350 	this(string[] args)
351 	{
352 		m_args = "dummy" ~ args;
353 	}
354 
355 	/** Returns the list of all options recognized.
356 
357 		This list is created by recording all calls to `getopt`.
358 	*/
359 	@property const(Arg)[] recognizedArgs() { return m_recognizedArgs; }
360 
361 	void getopt(T)(string names, T* var, string[] help_text = null)
362 	{
363 		foreach (ref arg; m_recognizedArgs)
364 			if (names == arg.names) {
365 				assert(help_text is null);
366 				*var = arg.value.get!T;
367 				return;
368 			}
369 		assert(help_text.length > 0);
370 		Arg arg;
371 		arg.defaultValue = *var;
372 		arg.names = names;
373 		arg.helpText = help_text;
374 		m_args.getopt(config.passThrough, names, var);
375 		arg.value = *var;
376 		m_recognizedArgs ~= arg;
377 	}
378 
379 	/** Resets the list of available source arguments.
380 	*/
381 	void dropAllArgs()
382 	{
383 		m_args = null;
384 	}
385 
386 	/** Returns the list of unprocessed arguments and calls `dropAllArgs`.
387 	*/
388 	string[] extractRemainingArgs()
389 	{
390 		auto ret = m_args[1 .. $];
391 		m_args = null;
392 		return ret;
393 	}
394 }
395 
396 
397 /** Base class for all commands.
398 
399 	This cass contains a high-level description of the command, including brief
400 	and full descriptions and a human readable command line pattern. On top of
401 	that it defines the two main entry functions for command execution.
402 */
403 class Command {
404 	string name;
405 	string argumentsPattern;
406 	string description;
407 	string[] helpText;
408 	bool acceptsAppArgs;
409 	bool hidden = false; // used for deprecated commands
410 	bool skipDubInitialization = false;
411 
412 	/** Parses all known command line options without executing any actions.
413 
414 		This function will be called prior to execute, or may be called as
415 		the only method when collecting the list of recognized command line
416 		options.
417 
418 		Only `args.getopt` should be called within this method.
419 	*/
420 	abstract void prepare(scope CommandArgs args);
421 
422 	/** Executes the actual action.
423 
424 		Note that `prepare` will be called before any call to `execute`.
425 	*/
426 	abstract int execute(Dub dub, string[] free_args, string[] app_args);
427 
428 	private bool loadCwdPackage(Dub dub, bool warn_missing_package)
429 	{
430 		bool found;
431 		foreach (f; packageInfoFiles)
432 			if (existsFile(dub.rootPath ~ f.filename))
433 			{
434 				found = true;
435 				break;
436 			}
437 
438 		if (!found) {
439 			if (warn_missing_package) {
440 				logInfo("");
441 				logInfo("No package manifest (dub.json or dub.sdl) was found in");
442 				logInfo(dub.rootPath.toNativeString());
443 				logInfo("Please run DUB from the root directory of an existing package, or run");
444 				logInfo("\"dub init --help\" to get information on creating a new package.");
445 				logInfo("");
446 			}
447 			return false;
448 		}
449 
450 		dub.loadPackage();
451 
452 		return true;
453 	}
454 }
455 
456 
457 /** Encapsulates a group of commands that fit into a common category.
458 */
459 struct CommandGroup {
460 	/// Caption of the command category
461 	string caption;
462 
463 	/// List of commands contained inthis group
464 	Command[] commands;
465 
466 	this(string caption, Command[] commands...)
467 	{
468 		this.caption = caption;
469 		this.commands = commands.dup;
470 	}
471 }
472 
473 
474 /******************************************************************************/
475 /* INIT                                                                       */
476 /******************************************************************************/
477 
478 class InitCommand : Command {
479 	private{
480 		string m_templateType = "minimal";
481 		PackageFormat m_format = PackageFormat.json;
482 		bool m_nonInteractive;
483 	}
484 	this()
485 	{
486 		this.name = "init";
487 		this.argumentsPattern = "[<directory> [<dependency>...]]";
488 		this.description = "Initializes an empty package skeleton";
489 		this.helpText = [
490 			"Initializes an empty package of the specified type in the given directory. By default, the current working directory is used."
491 		];
492 		this.acceptsAppArgs = true;
493 	}
494 
495 	override void prepare(scope CommandArgs args)
496 	{
497 		args.getopt("t|type", &m_templateType, [
498 			"Set the type of project to generate. Available types:",
499 			"",
500 			"minimal - simple \"hello world\" project (default)",
501 			"vibe.d  - minimal HTTP server based on vibe.d",
502 			"deimos  - skeleton for C header bindings",
503 			"custom  - custom project provided by dub package",
504 		]);
505 		args.getopt("f|format", &m_format, [
506 			"Sets the format to use for the package description file. Possible values:",
507 			"  " ~ [__traits(allMembers, PackageFormat)].map!(f => f == m_format.init.to!string ? f ~ " (default)" : f).join(", ")
508 		]);
509 		args.getopt("n|non-interactive", &m_nonInteractive, ["Don't enter interactive mode."]);
510 	}
511 
512 	override int execute(Dub dub, string[] free_args, string[] app_args)
513 	{
514 		string dir;
515 		if (free_args.length)
516 		{
517 			dir = free_args[0];
518 			free_args = free_args[1 .. $];
519 		}
520 
521 		static string input(string caption, string default_value)
522 		{
523 			writef("%s [%s]: ", caption, default_value);
524 			stdout.flush();
525 			auto inp = readln();
526 			return inp.length > 1 ? inp[0 .. $-1] : default_value;
527 		}
528 
529 		void depCallback(ref PackageRecipe p, ref PackageFormat fmt) {
530 			import std.datetime: Clock;
531 
532 			if (m_nonInteractive) return;
533 
534 			while (true) {
535 				string rawfmt = input("Package recipe format (sdl/json)", fmt.to!string);
536 				if (!rawfmt.length) break;
537 				try {
538 					fmt = rawfmt.to!PackageFormat;
539 					break;
540 				} catch (Exception) {
541 					logError("Invalid format, \""~rawfmt~"\", enter either \"sdl\" or \"json\".");
542 				}
543 			}
544 			auto author = p.authors.join(", ");
545 			while (true) {
546 				// Tries getting the name until a valid one is given.
547 				import std.regex;
548 				auto nameRegex = regex(`^[a-z0-9\-_]+$`);
549 				string triedName = input("Name", p.name);
550 				if (triedName.matchFirst(nameRegex).empty) {
551 					logError("Invalid name, \""~triedName~"\", names should consist only of lowercase alphanumeric characters, - and _.");
552 				} else {
553 					p.name = triedName;
554 					break;
555 				}
556 			}
557 			p.description = input("Description", p.description);
558 			p.authors = input("Author name", author).split(",").map!(a => a.strip).array;
559 			p.license = input("License", p.license);
560 			string copyrightString = .format("Copyright © %s, %-(%s, %)", Clock.currTime().year, p.authors);
561 			p.copyright = input("Copyright string", copyrightString);
562 
563 			while (true) {
564 				auto depspec = input("Add dependency (leave empty to skip)", null);
565 				if (!depspec.length) break;
566 				addDependency(dub, p, depspec);
567 			}
568 		}
569 
570 		if (!["vibe.d", "deimos", "minimal"].canFind(m_templateType))
571 		{
572 			free_args ~= m_templateType;
573 		}
574 		dub.createEmptyPackage(NativePath(dir), free_args, m_templateType, m_format, &depCallback, app_args);
575 
576 		logInfo("Package successfully created in %s", dir.length ? dir : ".");
577 		return 0;
578 	}
579 }
580 
581 
582 /******************************************************************************/
583 /* GENERATE / BUILD / RUN / TEST / DESCRIBE                                   */
584 /******************************************************************************/
585 
586 abstract class PackageBuildCommand : Command {
587 	protected {
588 		string m_buildType;
589 		BuildMode m_buildMode;
590 		string m_buildConfig;
591 		string m_compilerName;
592 		string m_arch;
593 		string[] m_debugVersions;
594 		string[] m_overrideConfigs;
595 		Compiler m_compiler;
596 		BuildPlatform m_buildPlatform;
597 		BuildSettings m_buildSettings;
598 		string m_defaultConfig;
599 		bool m_nodeps;
600 		bool m_forceRemove = false;
601 		bool m_single;
602 		bool m_filterVersions = false;
603 	}
604 
605 	override void prepare(scope CommandArgs args)
606 	{
607 		args.getopt("b|build", &m_buildType, [
608 			"Specifies the type of build to perform. Note that setting the DFLAGS environment variable will override the build type with custom flags.",
609 			"Possible names:",
610 			"  debug (default), plain, release, release-debug, release-nobounds, unittest, profile, profile-gc, docs, ddox, cov, unittest-cov, syntax and custom types"
611 		]);
612 		args.getopt("c|config", &m_buildConfig, [
613 			"Builds the specified configuration. Configurations can be defined in dub.json"
614 		]);
615 		args.getopt("override-config", &m_overrideConfigs, [
616 			"Uses the specified configuration for a certain dependency. Can be specified multiple times.",
617 			"Format: --override-config=<dependency>/<config>"
618 		]);
619 		args.getopt("compiler", &m_compilerName, [
620 			"Specifies the compiler binary to use (can be a path).",
621 			"Arbitrary pre- and suffixes to the identifiers below are recognized (e.g. ldc2 or dmd-2.063) and matched to the proper compiler type:",
622 			"  "~["dmd", "gdc", "ldc", "gdmd", "ldmd"].join(", ")
623 		]);
624 		args.getopt("a|arch", &m_arch, [
625 			"Force a different architecture (e.g. x86 or x86_64)"
626 		]);
627 		args.getopt("d|debug", &m_debugVersions, [
628 			"Define the specified debug version identifier when building - can be used multiple times"
629 		]);
630 		args.getopt("nodeps", &m_nodeps, [
631 			"Do not resolve missing dependencies before building"
632 		]);
633 		args.getopt("build-mode", &m_buildMode, [
634 			"Specifies the way the compiler and linker are invoked. Valid values:",
635 			"  separate (default), allAtOnce, singleFile"
636 		]);
637 		args.getopt("single", &m_single, [
638 			"Treats the package name as a filename. The file must contain a package recipe comment."
639 		]);
640 		args.getopt("force-remove", &m_forceRemove, [
641 			"Deprecated option that does nothing."
642 		]);
643 		args.getopt("filter-versions", &m_filterVersions, [
644 			"[Experimental] Filter version identifiers and debug version identifiers to improve build cache efficiency."
645 		]);
646 	}
647 
648 	protected void setupPackage(Dub dub, string package_name, string default_build_type = "debug")
649 	{
650 		if (!m_compilerName.length) m_compilerName = dub.defaultCompiler;
651 		if (!m_arch.length) m_arch = dub.defaultArchitecture;
652 		m_compiler = getCompiler(m_compilerName);
653 		m_buildPlatform = m_compiler.determinePlatform(m_buildSettings, m_compilerName, m_arch);
654 		m_buildSettings.addDebugVersions(m_debugVersions);
655 
656 		m_defaultConfig = null;
657 		enforce (loadSpecificPackage(dub, package_name), "Failed to load package.");
658 
659 		if (m_buildConfig.length != 0 && !dub.configurations.canFind(m_buildConfig))
660 		{
661 			string msg = "Unknown build configuration: "~m_buildConfig;
662 			enum distance = 3;
663 			auto match = dub.configurations.getClosestMatch(m_buildConfig, distance);
664 			if (match !is null) msg ~= ". Did you mean '" ~ match ~ "'?";
665 			enforce(0, msg);
666 		}
667 
668 		if (m_buildType.length == 0) {
669 			if (environment.get("DFLAGS") !is null) m_buildType = "$DFLAGS";
670 			else m_buildType = default_build_type;
671 		}
672 
673 		if (!m_nodeps) {
674 			// retrieve missing packages
675 			dub.project.reinit();
676 			if (!dub.project.hasAllDependencies) {
677 				logDiagnostic("Checking for missing dependencies.");
678 				if (m_single) dub.upgrade(UpgradeOptions.select | UpgradeOptions.noSaveSelections);
679 				else dub.upgrade(UpgradeOptions.select);
680 			}
681 		}
682 
683 		dub.project.validate();
684 
685 		foreach (sc; m_overrideConfigs) {
686 			auto idx = sc.indexOf('/');
687 			enforceUsage(idx >= 0, "Expected \"<package>/<configuration>\" as argument to --override-config.");
688 			dub.project.overrideConfiguration(sc[0 .. idx], sc[idx+1 .. $]);
689 		}
690 	}
691 
692 	private bool loadSpecificPackage(Dub dub, string package_name)
693 	{
694 		if (m_single) {
695 			enforce(package_name.length, "Missing file name of single-file package.");
696 			dub.loadSingleFilePackage(package_name);
697 			return true;
698 		}
699 
700 		bool from_cwd = package_name.length == 0 || package_name.startsWith(":");
701 		// load package in root_path to enable searching for sub packages
702 		if (loadCwdPackage(dub, from_cwd)) {
703 			if (package_name.startsWith(":"))
704 			{
705 				auto pack = dub.packageManager.getSubPackage(dub.project.rootPackage, package_name[1 .. $], false);
706 				dub.loadPackage(pack);
707 				return true;
708 			}
709 			if (from_cwd) return true;
710 		}
711 
712 		enforce(package_name.length, "No valid root package found - aborting.");
713 
714 		auto pack = dub.packageManager.getFirstPackage(package_name);
715 		enforce(pack, "Failed to find a package named '"~package_name~"' locally.");
716 		logInfo("Building package %s in %s", pack.name, pack.path.toNativeString());
717 		dub.rootPath = pack.path;
718 		dub.loadPackage(pack);
719 		return true;
720 	}
721 }
722 
723 class GenerateCommand : PackageBuildCommand {
724 	protected {
725 		string m_generator;
726 		bool m_rdmd = false;
727 		bool m_tempBuild = false;
728 		bool m_run = false;
729 		bool m_force = false;
730 		bool m_combined = false;
731 		bool m_parallel = false;
732 		bool m_printPlatform, m_printBuilds, m_printConfigs;
733 	}
734 
735 	this()
736 	{
737 		this.name = "generate";
738 		this.argumentsPattern = "<generator> [<package>]";
739 		this.description = "Generates project files using the specified generator";
740 		this.helpText = [
741 			"Generates project files using one of the supported generators:",
742 			"",
743 			"visuald - VisualD project files",
744 			"sublimetext - SublimeText project file",
745 			"cmake - CMake build scripts",
746 			"build - Builds the package directly",
747 			"",
748 			"An optional package name can be given to generate a different package than the root/CWD package."
749 		];
750 	}
751 
752 	override void prepare(scope CommandArgs args)
753 	{
754 		super.prepare(args);
755 
756 		args.getopt("combined", &m_combined, [
757 			"Tries to build the whole project in a single compiler run."
758 		]);
759 
760 		args.getopt("print-builds", &m_printBuilds, [
761 			"Prints the list of available build types"
762 		]);
763 		args.getopt("print-configs", &m_printConfigs, [
764 			"Prints the list of available configurations"
765 		]);
766 		args.getopt("print-platform", &m_printPlatform, [
767 			"Prints the identifiers for the current build platform as used for the build fields in dub.json"
768 		]);
769 		args.getopt("parallel", &m_parallel, [
770 			"Runs multiple compiler instances in parallel, if possible."
771 		]);
772 	}
773 
774 	override int execute(Dub dub, string[] free_args, string[] app_args)
775 	{
776 		string package_name;
777 		if (!m_generator.length) {
778 			enforceUsage(free_args.length >= 1 && free_args.length <= 2, "Expected one or two arguments.");
779 			m_generator = free_args[0];
780 			if (free_args.length >= 2) package_name = free_args[1];
781 		} else {
782 			enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
783 			if (free_args.length >= 1) package_name = free_args[0];
784 		}
785 
786 		setupPackage(dub, package_name);
787 
788 		if (m_printBuilds) { // FIXME: use actual package data
789 			logInfo("Available build types:");
790 			foreach (tp; ["debug", "release", "unittest", "profile"])
791 				logInfo("  %s", tp);
792 			logInfo("");
793 		}
794 
795 		m_defaultConfig = dub.project.getDefaultConfiguration(m_buildPlatform);
796 		if (m_printConfigs) {
797 			logInfo("Available configurations:");
798 			foreach (tp; dub.configurations)
799 				logInfo("  %s%s", tp, tp == m_defaultConfig ? " [default]" : null);
800 			logInfo("");
801 		}
802 
803 		GeneratorSettings gensettings;
804 		gensettings.platform = m_buildPlatform;
805 		gensettings.config = m_buildConfig.length ? m_buildConfig : m_defaultConfig;
806 		gensettings.buildType = m_buildType;
807 		gensettings.buildMode = m_buildMode;
808 		gensettings.compiler = m_compiler;
809 		gensettings.buildSettings = m_buildSettings;
810 		gensettings.combined = m_combined;
811 		gensettings.filterVersions = m_filterVersions;
812 		gensettings.run = m_run;
813 		gensettings.runArgs = app_args;
814 		gensettings.force = m_force;
815 		gensettings.rdmd = m_rdmd;
816 		gensettings.tempBuild = m_tempBuild;
817 		gensettings.parallelBuild = m_parallel;
818 		gensettings.single = m_single;
819 
820 		logDiagnostic("Generating using %s", m_generator);
821 		dub.generateProject(m_generator, gensettings);
822 		if (m_buildType == "ddox") dub.runDdox(gensettings.run, app_args);
823 		return 0;
824 	}
825 }
826 
827 class BuildCommand : GenerateCommand {
828 	protected {
829 		bool m_yes; // automatic yes to prompts;
830 		bool m_nonInteractive;
831 	}
832 	this()
833 	{
834 		this.name = "build";
835 		this.argumentsPattern = "[<package>]";
836 		this.description = "Builds a package (uses the main package in the current working directory by default)";
837 		this.helpText = [
838 			"Builds a package (uses the main package in the current working directory by default)"
839 		];
840 	}
841 
842 	override void prepare(scope CommandArgs args)
843 	{
844 		args.getopt("rdmd", &m_rdmd, [
845 			"Use rdmd instead of directly invoking the compiler"
846 		]);
847 
848 		args.getopt("f|force", &m_force, [
849 			"Forces a recompilation even if the target is up to date"
850 		]);
851 		args.getopt("y|yes", &m_yes, [
852 			`Automatic yes to prompts. Assume "yes" as answer to all interactive prompts.`
853 		]);
854 		args.getopt("n|non-interactive", &m_nonInteractive, [
855 			"Don't enter interactive mode."
856 		]);
857 		super.prepare(args);
858 		m_generator = "build";
859 	}
860 
861 	override int execute(Dub dub, string[] free_args, string[] app_args)
862 	{
863 		// single package files don't need to be downloaded, they are on the disk.
864 		if (free_args.length < 1 || m_single)
865 			return super.execute(dub, free_args, app_args);
866 
867 		if (!m_nonInteractive)
868 		{
869 			const packageParts = splitPackageName(free_args[0]);
870 			if (auto rc = fetchMissingPackages(dub, packageParts))
871 				return rc;
872 			free_args[0] = packageParts.name;
873 		}
874 		return super.execute(dub, free_args, app_args);
875 	}
876 
877 	private int fetchMissingPackages(Dub dub, in PackageAndVersion packageParts)
878 	{
879 
880 		static bool input(string caption, bool default_value = true) {
881 			writef("%s [%s]: ", caption, default_value ? "Y/n" : "y/N");
882 			auto inp = readln();
883 			string userInput = "y";
884 			if (inp.length > 1)
885 				userInput = inp[0 .. $ - 1].toLower;
886 
887 			switch (userInput) {
888 				case "no", "n", "0":
889 					return false;
890 				case "yes", "y", "1":
891 				default:
892 					return true;
893 			}
894 		}
895 
896 		Dependency dep;
897 
898 		if (packageParts.version_.length > 0) {
899 			// the user provided a version manually
900 			dep = Dependency(packageParts.version_);
901 		} else {
902 			if (packageParts.name.startsWith(":") ||
903 				dub.packageManager.getFirstPackage(packageParts.name))
904 				// found locally
905 				return 0;
906 
907 			// search for the package and filter versions for exact matches
908 			auto basePackageName = getBasePackageName(packageParts.name);
909 			auto search = dub.searchPackages(basePackageName)
910 				.map!(tup => tup[1].find!(p => p.name == basePackageName))
911 				.filter!(ps => !ps.empty);
912 			if (search.empty) {
913 				logWarn("Package '%s' was neither found locally nor online.", packageParts.name);
914 				return 2;
915 			}
916 
917 			const p = search.front.front;
918 			logInfo("Package '%s' was not found locally but is available online:", packageParts.name);
919 			logInfo("---");
920 			logInfo("Description: %s", p.description);
921 			logInfo("Version: %s", p.version_);
922 			logInfo("---");
923 
924 			const answer = m_yes ? true : input("Do you want to fetch '%s' now?".format(packageParts.name));
925 			if (!answer)
926 				return 0;
927 			dep = Dependency(p.version_);
928 		}
929 
930 		dub.fetch(packageParts.name, dep, dub.defaultPlacementLocation, FetchOptions.none);
931 		return 0;
932 	}
933 }
934 
935 class RunCommand : BuildCommand {
936 	this()
937 	{
938 		this.name = "run";
939 		this.argumentsPattern = "[<package>]";
940 		this.description = "Builds and runs a package (default command)";
941 		this.helpText = [
942 			"Builds and runs a package (uses the main package in the current working directory by default)"
943 		];
944 		this.acceptsAppArgs = true;
945 	}
946 
947 	override void prepare(scope CommandArgs args)
948 	{
949 		args.getopt("temp-build", &m_tempBuild, [
950 			"Builds the project in the temp folder if possible."
951 		]);
952 
953 		super.prepare(args);
954 		m_run = true;
955 	}
956 
957 	override int execute(Dub dub, string[] free_args, string[] app_args)
958 	{
959 		return super.execute(dub, free_args, app_args);
960 	}
961 }
962 
963 class TestCommand : PackageBuildCommand {
964 	private {
965 		string m_mainFile;
966 		bool m_combined = false;
967 		bool m_parallel = false;
968 		bool m_force = false;
969 	}
970 
971 	this()
972 	{
973 		this.name = "test";
974 		this.argumentsPattern = "[<package>]";
975 		this.description = "Executes the tests of the selected package";
976 		this.helpText = [
977 			`Builds the package and executes all contained unit tests.`,
978 			``,
979 			`If no explicit configuration is given, an existing "unittest" ` ~
980 			`configuration will be preferred for testing. If none exists, the ` ~
981 			`first library type configuration will be used, and if that doesn't ` ~
982 			`exist either, the first executable configuration is chosen.`,
983 			``,
984 			`When a custom main file (--main-file) is specified, only library ` ~
985 			`configurations can be used. Otherwise, depending on the type of ` ~
986 			`the selected configuration, either an existing main file will be ` ~
987 			`used (and needs to be properly adjusted to just run the unit ` ~
988 			`tests for 'version(unittest)'), or DUB will generate one for ` ~
989 			`library type configurations.`,
990 			``,
991 			`Finally, if the package contains a dependency to the "tested" ` ~
992 			`package, the automatically generated main file will use it to ` ~
993 			`run the unit tests.`
994 		];
995 		this.acceptsAppArgs = true;
996 	}
997 
998 	override void prepare(scope CommandArgs args)
999 	{
1000 		args.getopt("main-file", &m_mainFile, [
1001 			"Specifies a custom file containing the main() function to use for running the tests."
1002 		]);
1003 		args.getopt("combined", &m_combined, [
1004 			"Tries to build the whole project in a single compiler run."
1005 		]);
1006 		args.getopt("parallel", &m_parallel, [
1007 			"Runs multiple compiler instances in parallel, if possible."
1008 		]);
1009 		args.getopt("f|force", &m_force, [
1010 			"Forces a recompilation even if the target is up to date"
1011 		]);
1012 		bool coverage = false;
1013 		args.getopt("coverage", &coverage, [
1014 			"Enables code coverage statistics to be generated."
1015 		]);
1016 		if (coverage) m_buildType = "unittest-cov";
1017 
1018 		super.prepare(args);
1019 	}
1020 
1021 	override int execute(Dub dub, string[] free_args, string[] app_args)
1022 	{
1023 		string package_name;
1024 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1025 		if (free_args.length >= 1) package_name = free_args[0];
1026 
1027 		setupPackage(dub, package_name, "unittest");
1028 
1029 		GeneratorSettings settings;
1030 		settings.platform = m_buildPlatform;
1031 		settings.compiler = getCompiler(m_buildPlatform.compilerBinary);
1032 		settings.buildType = m_buildType;
1033 		settings.buildMode = m_buildMode;
1034 		settings.buildSettings = m_buildSettings;
1035 		settings.combined = m_combined;
1036 		settings.filterVersions = m_filterVersions;
1037 		settings.parallelBuild = m_parallel;
1038 		settings.force = m_force;
1039 		settings.tempBuild = m_single;
1040 		settings.run = true;
1041 		settings.runArgs = app_args;
1042 
1043 		dub.testProject(settings, m_buildConfig, NativePath(m_mainFile));
1044 		return 0;
1045 	}
1046 }
1047 
1048 class LintCommand : PackageBuildCommand {
1049 	private {
1050 		bool m_syntaxCheck = false;
1051 		bool m_styleCheck = false;
1052 		string m_errorFormat;
1053 		bool m_report = false;
1054 		string m_reportFormat;
1055 		string[] m_importPaths;
1056 		string m_config;
1057 	}
1058 
1059 	this()
1060 	{
1061 		this.name = "lint";
1062 		this.argumentsPattern = "[<package>]";
1063 		this.description = "Executes the linter tests of the selected package";
1064 		this.helpText = [
1065 			`Builds the package and executes D-Scanner linter tests.`
1066 		];
1067 		this.acceptsAppArgs = true;
1068 	}
1069 
1070 	override void prepare(scope CommandArgs args)
1071 	{
1072 		args.getopt("syntax-check", &m_syntaxCheck, [
1073 			"Lexes and parses sourceFile, printing the line and column number of " ~
1074 			"any syntax errors to stdout."
1075 		]);
1076 
1077 		args.getopt("style-check", &m_styleCheck, [
1078 			"Lexes and parses sourceFiles, printing the line and column number of " ~
1079 			"any static analysis check failures stdout."
1080 		]);
1081 
1082 		args.getopt("error-format", &m_errorFormat, [
1083 			"Format errors produced by the style/syntax checkers."
1084 		]);
1085 
1086 		args.getopt("report", &m_report, [
1087 			"Generate a static analysis report in JSON format."
1088 		]);
1089 
1090 		args.getopt("report-format", &m_reportFormat, [
1091 			"Specifies the format of the generated report."
1092 		]);
1093 
1094 		if (m_reportFormat) m_report = true;
1095 
1096 		args.getopt("import-paths", &m_importPaths, [
1097 			"Import paths"
1098 		]);
1099 
1100 		args.getopt("config", &m_config, [
1101 			"Use the given configuration file."
1102 		]);
1103 
1104 		super.prepare(args);
1105 	}
1106 
1107 	override int execute(Dub dub, string[] free_args, string[] app_args)
1108 	{
1109 		string package_name;
1110 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1111 		if (free_args.length >= 1) package_name = free_args[0];
1112 
1113 		string[] args;
1114 		if (!m_syntaxCheck && !m_styleCheck && !m_report && app_args.length == 0) { m_styleCheck = true; }
1115 
1116 		if (m_syntaxCheck) args ~= "--syntaxCheck";
1117 		if (m_styleCheck) args ~= "--styleCheck";
1118 		if (m_errorFormat) args ~= ["--errorFormat", m_errorFormat];
1119 		if (m_report) args ~= "--report";
1120 		if (m_reportFormat) args ~= ["--reportFormat", m_reportFormat];
1121 		foreach (import_path; m_importPaths) args ~= ["-I", import_path];
1122 		if (m_config) args ~= ["--config", m_config];
1123 
1124 		setupPackage(dub, package_name);
1125 		dub.lintProject(args ~ app_args);
1126 		return 0;
1127 	}
1128 }
1129 
1130 class DescribeCommand : PackageBuildCommand {
1131 	private {
1132 		bool m_importPaths = false;
1133 		bool m_stringImportPaths = false;
1134 		bool m_dataList = false;
1135 		bool m_dataNullDelim = false;
1136 		string[] m_data;
1137 	}
1138 
1139 	this()
1140 	{
1141 		this.name = "describe";
1142 		this.argumentsPattern = "[<package>]";
1143 		this.description = "Prints a JSON description of the project and its dependencies";
1144 		this.helpText = [
1145 			"Prints a JSON build description for the root package an all of " ~
1146 			"their dependencies in a format similar to a JSON package " ~
1147 			"description file. This is useful mostly for IDEs.",
1148 			"",
1149 			"All usual options that are also used for build/run/generate apply.",
1150 			"",
1151 			"When --data=VALUE is supplied, specific build settings for a project " ~
1152 			"will be printed instead (by default, formatted for the current compiler).",
1153 			"",
1154 			"The --data=VALUE option can be specified multiple times to retrieve " ~
1155 			"several pieces of information at once. A comma-separated list is " ~
1156 			"also acceptable (ex: --data=dflags,libs). The data will be output in " ~
1157 			"the same order requested on the command line.",
1158 			"",
1159 			"The accepted values for --data=VALUE are:",
1160 			"",
1161 			"main-source-file, dflags, lflags, libs, linker-files, " ~
1162 			"source-files, versions, debug-versions, import-paths, " ~
1163 			"string-import-paths, import-files, options",
1164 			"",
1165 			"The following are also accepted by --data if --data-list is used:",
1166 			"",
1167 			"target-type, target-path, target-name, working-directory, " ~
1168 			"copy-files, string-import-files, pre-generate-commands, " ~
1169 			"post-generate-commands, pre-build-commands, post-build-commands, " ~
1170 			"requirements",
1171 		];
1172 	}
1173 
1174 	override void prepare(scope CommandArgs args)
1175 	{
1176 		super.prepare(args);
1177 
1178 		args.getopt("import-paths", &m_importPaths, [
1179 			"Shortcut for --data=import-paths --data-list"
1180 		]);
1181 
1182 		args.getopt("string-import-paths", &m_stringImportPaths, [
1183 			"Shortcut for --data=string-import-paths --data-list"
1184 		]);
1185 
1186 		args.getopt("data", &m_data, [
1187 			"Just list the values of a particular build setting, either for this "~
1188 			"package alone or recursively including all dependencies. Accepts a "~
1189 			"comma-separated list. See above for more details and accepted "~
1190 			"possibilities for VALUE."
1191 		]);
1192 
1193 		args.getopt("data-list", &m_dataList, [
1194 			"Output --data information in list format (line-by-line), instead "~
1195 			"of formatting for a compiler command line.",
1196 		]);
1197 
1198 		args.getopt("data-0", &m_dataNullDelim, [
1199 			"Output --data information using null-delimiters, rather than "~
1200 			"spaces or newlines. Result is usable with, ex., xargs -0.",
1201 		]);
1202 	}
1203 
1204 	override int execute(Dub dub, string[] free_args, string[] app_args)
1205 	{
1206 		enforceUsage(
1207 			!(m_importPaths && m_stringImportPaths),
1208 			"--import-paths and --string-import-paths may not be used together."
1209 		);
1210 
1211 		enforceUsage(
1212 			!(m_data && (m_importPaths || m_stringImportPaths)),
1213 			"--data may not be used together with --import-paths or --string-import-paths."
1214 		);
1215 
1216 		// disable all log output to stdout and use "writeln" to output the JSON description
1217 		auto ll = getLogLevel();
1218 		setLogLevel(max(ll, LogLevel.warn));
1219 		scope (exit) setLogLevel(ll);
1220 
1221 		string package_name;
1222 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1223 		if (free_args.length >= 1) package_name = free_args[0];
1224 		setupPackage(dub, package_name);
1225 
1226 		m_defaultConfig = dub.project.getDefaultConfiguration(m_buildPlatform);
1227 
1228 		auto config = m_buildConfig.length ? m_buildConfig : m_defaultConfig;
1229 
1230 		GeneratorSettings settings;
1231 		settings.platform = m_buildPlatform;
1232 		settings.config = config;
1233 		settings.buildType = m_buildType;
1234 		settings.compiler = m_compiler;
1235 		settings.filterVersions = m_filterVersions;
1236 
1237 		if (m_importPaths) { m_data = ["import-paths"]; m_dataList = true; }
1238 		else if (m_stringImportPaths) { m_data = ["string-import-paths"]; m_dataList = true; }
1239 
1240 		if (m_data.length) {
1241 			ListBuildSettingsFormat lt;
1242 			with (ListBuildSettingsFormat)
1243 				lt = m_dataList ? (m_dataNullDelim ? listNul : list) : (m_dataNullDelim ? commandLineNul : commandLine);
1244 			dub.listProjectData(settings, m_data, lt);
1245 		} else {
1246 			auto desc = dub.project.describe(settings);
1247 			writeln(desc.serializeToPrettyJson());
1248 		}
1249 
1250 		return 0;
1251 	}
1252 }
1253 
1254 class CleanCommand : Command {
1255 	private {
1256 		bool m_allPackages;
1257 	}
1258 
1259 	this()
1260 	{
1261 		this.name = "clean";
1262 		this.argumentsPattern = "[<package>]";
1263 		this.description = "Removes intermediate build files and cached build results";
1264 		this.helpText = [
1265 			"This command removes any cached build files of the given package(s). The final target file, as well as any copyFiles are currently not removed.",
1266 			"Without arguments, the package in the current working directory will be cleaned."
1267 		];
1268 	}
1269 
1270 	override void prepare(scope CommandArgs args)
1271 	{
1272 		args.getopt("all-packages", &m_allPackages, [
1273 			"Cleans up *all* known packages (dub list)"
1274 		]);
1275 	}
1276 
1277 	override int execute(Dub dub, string[] free_args, string[] app_args)
1278 	{
1279 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1280 		enforceUsage(app_args.length == 0, "Application arguments are not supported for the clean command.");
1281 		enforceUsage(!m_allPackages || !free_args.length, "The --all-packages flag may not be used together with an explicit package name.");
1282 
1283 		enforce(free_args.length == 0, "Cleaning a specific package isn't possible right now.");
1284 
1285 		if (m_allPackages) {
1286 			bool any_error = false;
1287 
1288 			foreach (p; dub.packageManager.getPackageIterator()) {
1289 				try dub.cleanPackage(p.path);
1290 				catch (Exception e) {
1291 					logWarn("Failed to clean package %s at %s: %s", p.name, p.path, e.msg);
1292 					any_error = true;
1293 				}
1294 
1295 				foreach (sp; p.subPackages.filter!(sp => !sp.path.empty)) {
1296 					try dub.cleanPackage(p.path ~ sp.path);
1297 					catch (Exception e) {
1298 						logWarn("Failed to clean sub package of %s at %s: %s", p.name, p.path ~ sp.path, e.msg);
1299 						any_error = true;
1300 					}
1301 				}
1302 			}
1303 
1304 			if (any_error) return 1;
1305 		} else {
1306 			dub.cleanPackage(dub.rootPath);
1307 		}
1308 
1309 		return 0;
1310 	}
1311 }
1312 
1313 
1314 /******************************************************************************/
1315 /* FETCH / ADD / REMOVE / UPGRADE                                             */
1316 /******************************************************************************/
1317 
1318 class AddCommand : Command {
1319 	this()
1320 	{
1321 		this.name = "add";
1322 		this.argumentsPattern = "<package>[@<version-spec>] [<packages...>]";
1323 		this.description = "Adds dependencies to the package file.";
1324 		this.helpText = [
1325 			"Adds <packages> as dependencies.",
1326 			"",
1327 			"Running \"dub add <package>\" is the same as adding <package> to the \"dependencies\" section in dub.json/dub.sdl.",
1328 			"If no version is specified for one of the packages, dub will query the registry for the latest version."
1329 		];
1330 	}
1331 
1332 	override void prepare(scope CommandArgs args) {}
1333 
1334 	override int execute(Dub dub, string[] free_args, string[] app_args)
1335 	{
1336 		import dub.recipe.io : readPackageRecipe, writePackageRecipe;
1337 		import dub.internal.vibecompat.core.file : existsFile;
1338 		enforceUsage(free_args.length != 0, "Expected one or more arguments.");
1339 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1340 
1341 		if (!loadCwdPackage(dub, true)) return 1;
1342 		auto recipe = dub.project.rootPackage.rawRecipe.clone;
1343 
1344 		foreach (depspec; free_args) {
1345 			if (!addDependency(dub, recipe, depspec))
1346 				return 1;
1347 		}
1348 		writePackageRecipe(dub.project.rootPackage.recipePath, recipe);
1349 
1350 		return 0;
1351 	}
1352 }
1353 
1354 class UpgradeCommand : Command {
1355 	private {
1356 		bool m_prerelease = false;
1357 		bool m_forceRemove = false;
1358 		bool m_missingOnly = false;
1359 		bool m_verify = false;
1360 		bool m_dryRun = false;
1361 	}
1362 
1363 	this()
1364 	{
1365 		this.name = "upgrade";
1366 		this.argumentsPattern = "[<packages...>]";
1367 		this.description = "Forces an upgrade of the dependencies";
1368 		this.helpText = [
1369 			"Upgrades all dependencies of the package by querying the package registry(ies) for new versions.",
1370 			"",
1371 			"This will update the versions stored in the selections file ("~SelectedVersions.defaultFile~") accordingly.",
1372 			"",
1373 			"If one or more package names are specified, only those dependencies will be upgraded. Otherwise all direct and indirect dependencies of the root package will get upgraded."
1374 		];
1375 	}
1376 
1377 	override void prepare(scope CommandArgs args)
1378 	{
1379 		args.getopt("prerelease", &m_prerelease, [
1380 			"Uses the latest pre-release version, even if release versions are available"
1381 		]);
1382 		args.getopt("verify", &m_verify, [
1383 			"Updates the project and performs a build. If successful, rewrites the selected versions file <to be implemented>."
1384 		]);
1385 		args.getopt("dry-run", &m_dryRun, [
1386 			"Only print what would be upgraded, but don't actually upgrade anything."
1387 		]);
1388 		args.getopt("missing-only", &m_missingOnly, [
1389 			"Performs an upgrade only for dependencies that don't yet have a version selected. This is also done automatically before each build."
1390 		]);
1391 		args.getopt("force-remove", &m_forceRemove, [
1392 			"Deprecated option that does nothing."
1393 		]);
1394 	}
1395 
1396 	override int execute(Dub dub, string[] free_args, string[] app_args)
1397 	{
1398 		enforceUsage(free_args.length <= 1, "Unexpected arguments.");
1399 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1400 		enforceUsage(!m_verify, "--verify is not yet implemented.");
1401 		dub.loadPackage();
1402 		logInfo("Upgrading project in %s", dub.projectPath.toNativeString());
1403 		auto options = UpgradeOptions.upgrade|UpgradeOptions.select;
1404 		if (m_missingOnly) options &= ~UpgradeOptions.upgrade;
1405 		if (m_prerelease) options |= UpgradeOptions.preRelease;
1406 		if (m_dryRun) options |= UpgradeOptions.dryRun;
1407 		dub.upgrade(options, free_args);
1408 		return 0;
1409 	}
1410 }
1411 
1412 class FetchRemoveCommand : Command {
1413 	protected {
1414 		string m_version;
1415 		bool m_forceRemove = false;
1416 	}
1417 
1418 	override void prepare(scope CommandArgs args)
1419 	{
1420 		args.getopt("version", &m_version, [
1421 			"Use the specified version/branch instead of the latest available match",
1422 			"The remove command also accepts \"*\" here as a wildcard to remove all versions of the package from the specified location"
1423 		]);
1424 
1425 		args.getopt("force-remove", &m_forceRemove, [
1426 			"Deprecated option that does nothing"
1427 		]);
1428 	}
1429 
1430 	abstract override int execute(Dub dub, string[] free_args, string[] app_args);
1431 }
1432 
1433 class FetchCommand : FetchRemoveCommand {
1434 	this()
1435 	{
1436 		this.name = "fetch";
1437 		this.argumentsPattern = "<name>[@<version-spec>]";
1438 		this.description = "Manually retrieves and caches a package";
1439 		this.helpText = [
1440 			"Note: Use \"dub add <dependency>\" if you just want to use a certain package as a dependency, you don't have to explicitly fetch packages.",
1441 			"",
1442 			"Explicit retrieval/removal of packages is only needed when you want to put packages in a place where several applications can share them. If you just have a dependency to add, use the `add` command. Dub will do the rest for you.",
1443 			"",
1444 			"Without specified options, placement/removal will default to a user wide shared location.",
1445 			"",
1446 			"Complete applications can be retrieved and run easily by e.g.",
1447 			"$ dub fetch vibelog --cache=local",
1448 			"$ cd vibelog",
1449 			"$ dub",
1450 			"",
1451 			"This will grab all needed dependencies and compile and run the application.",
1452 			"",
1453 			"Note: DUB does not do a system installation of packages. Packages are instead only registered within DUB's internal ecosystem. Generation of native system packages/installers may be added later as a separate feature."
1454 		];
1455 	}
1456 
1457 	override void prepare(scope CommandArgs args)
1458 	{
1459 		super.prepare(args);
1460 	}
1461 
1462 	override int execute(Dub dub, string[] free_args, string[] app_args)
1463 	{
1464 		enforceUsage(free_args.length == 1, "Expecting exactly one argument.");
1465 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1466 
1467 		auto location = dub.defaultPlacementLocation;
1468 
1469 		auto name = free_args[0];
1470 
1471 		FetchOptions fetchOpts;
1472 		fetchOpts |= FetchOptions.forceBranchUpgrade;
1473 		if (m_version.length) dub.fetch(name, Dependency(m_version), location, fetchOpts);
1474 		else if (name.canFind("@", "=")) {
1475 			const parts = name.splitPackageName;
1476 			dub.fetch(parts.name, Dependency(parts.version_), location, fetchOpts);
1477 		} else {
1478 			try {
1479 				dub.fetch(name, Dependency(">=0.0.0"), location, fetchOpts);
1480 				logInfo(
1481 					"Please note that you need to use `dub run <pkgname>` " ~
1482 					"or add it to dependencies of your package to actually use/run it. " ~
1483 					"dub does not do actual installation of packages outside of its own ecosystem.");
1484 			}
1485 			catch(Exception e){
1486 				logInfo("Getting a release version failed: %s", e.msg);
1487 				logInfo("Retry with ~master...");
1488 				dub.fetch(name, Dependency("~master"), location, fetchOpts);
1489 			}
1490 		}
1491 		return 0;
1492 	}
1493 }
1494 
1495 class InstallCommand : FetchCommand {
1496 	this() { this.name = "install"; hidden = true; }
1497 	override void prepare(scope CommandArgs args) { super.prepare(args); }
1498 	override int execute(Dub dub, string[] free_args, string[] app_args)
1499 	{
1500 		warnRenamed("install", "fetch");
1501 		return super.execute(dub, free_args, app_args);
1502 	}
1503 }
1504 
1505 class RemoveCommand : FetchRemoveCommand {
1506 	private {
1507 		bool m_nonInteractive;
1508 	}
1509 
1510 	this()
1511 	{
1512 		this.name = "remove";
1513 		this.argumentsPattern = "<name>";
1514 		this.description = "Removes a cached package";
1515 		this.helpText = [
1516 			"Removes a package that is cached on the local system."
1517 		];
1518 	}
1519 
1520 	override void prepare(scope CommandArgs args)
1521 	{
1522 		super.prepare(args);
1523 		args.getopt("n|non-interactive", &m_nonInteractive, ["Don't enter interactive mode."]);
1524 	}
1525 
1526 	override int execute(Dub dub, string[] free_args, string[] app_args)
1527 	{
1528 		enforceUsage(free_args.length == 1, "Expecting exactly one argument.");
1529 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1530 
1531 		auto package_id = free_args[0];
1532 		auto location = dub.defaultPlacementLocation;
1533 
1534 		size_t resolveVersion(in Package[] packages) {
1535 			// just remove only package version
1536 			if (packages.length == 1)
1537 				return 0;
1538 
1539 			writeln("Select version of '", package_id, "' to remove from location '", location, "':");
1540 			foreach (i, pack; packages)
1541 				writefln("%s) %s", i + 1, pack.version_);
1542 			writeln(packages.length + 1, ") ", "all versions");
1543 			while (true) {
1544 				writef("> ");
1545 				auto inp = readln();
1546 				if (!inp.length) // Ctrl+D
1547 					return size_t.max;
1548 				inp = inp.stripRight;
1549 				if (!inp.length) // newline or space
1550 					continue;
1551 				try {
1552 					immutable selection = inp.to!size_t - 1;
1553 					if (selection <= packages.length)
1554 						return selection;
1555 				} catch (ConvException e) {
1556 				}
1557 				logError("Please enter a number between 1 and %s.", packages.length + 1);
1558 			}
1559 		}
1560 
1561 		if (m_nonInteractive || !m_version.empty)
1562 			dub.remove(package_id, m_version, location);
1563 		else
1564 			dub.remove(package_id, location, &resolveVersion);
1565 		return 0;
1566 	}
1567 }
1568 
1569 class UninstallCommand : RemoveCommand {
1570 	this() { this.name = "uninstall"; hidden = true; }
1571 	override void prepare(scope CommandArgs args) { super.prepare(args); }
1572 	override int execute(Dub dub, string[] free_args, string[] app_args)
1573 	{
1574 		warnRenamed("uninstall", "remove");
1575 		return super.execute(dub, free_args, app_args);
1576 	}
1577 }
1578 
1579 
1580 /******************************************************************************/
1581 /* ADD/REMOVE PATH/LOCAL                                                      */
1582 /******************************************************************************/
1583 
1584 abstract class RegistrationCommand : Command {
1585 	private {
1586 		bool m_system;
1587 	}
1588 
1589 	override void prepare(scope CommandArgs args)
1590 	{
1591 		args.getopt("system", &m_system, [
1592 			"Register system-wide instead of user-wide"
1593 		]);
1594 	}
1595 
1596 	abstract override int execute(Dub dub, string[] free_args, string[] app_args);
1597 }
1598 
1599 class AddPathCommand : RegistrationCommand {
1600 	this()
1601 	{
1602 		this.name = "add-path";
1603 		this.argumentsPattern = "<path>";
1604 		this.description = "Adds a default package search path";
1605 		this.helpText = [
1606 			"Adds a default package search path. All direct sub folders of this path will be searched for package descriptions and will be made available as packages. Using this command has the equivalent effect as calling 'dub add-local' on each of the sub folders manually.",
1607 			"",
1608 			"Any packages registered using add-path will be preferred over packages downloaded from the package registry when searching for dependencies during a build operation.",
1609 			"",
1610 			"The version of the packages will be determined by one of the following:",
1611 			"  - For GIT working copies, the last tag (git describe) is used to determine the version",
1612 			"  - If the package contains a \"version\" field in the package description, this is used",
1613 			"  - If neither of those apply, \"~master\" is assumed"
1614 		];
1615 	}
1616 
1617 	override int execute(Dub dub, string[] free_args, string[] app_args)
1618 	{
1619 		enforceUsage(free_args.length == 1, "Missing search path.");
1620 		dub.addSearchPath(free_args[0], m_system);
1621 		return 0;
1622 	}
1623 }
1624 
1625 class RemovePathCommand : RegistrationCommand {
1626 	this()
1627 	{
1628 		this.name = "remove-path";
1629 		this.argumentsPattern = "<path>";
1630 		this.description = "Removes a package search path";
1631 		this.helpText = ["Removes a package search path previously added with add-path."];
1632 	}
1633 
1634 	override int execute(Dub dub, string[] free_args, string[] app_args)
1635 	{
1636 		enforceUsage(free_args.length == 1, "Expected one argument.");
1637 		dub.removeSearchPath(free_args[0], m_system);
1638 		return 0;
1639 	}
1640 }
1641 
1642 class AddLocalCommand : RegistrationCommand {
1643 	this()
1644 	{
1645 		this.name = "add-local";
1646 		this.argumentsPattern = "<path> [<version>]";
1647 		this.description = "Adds a local package directory (e.g. a git repository)";
1648 		this.helpText = [
1649 			"Adds a local package directory to be used during dependency resolution. This command is useful for registering local packages, such as GIT working copies, that are either not available in the package registry, or are supposed to be overwritten.",
1650 			"",
1651 			"The version of the package is either determined automatically (see the \"add-path\" command, or can be explicitly overwritten by passing a version on the command line.",
1652 			"",
1653 			"See 'dub add-path -h' for a way to register multiple local packages at once."
1654 		];
1655 	}
1656 
1657 	override int execute(Dub dub, string[] free_args, string[] app_args)
1658 	{
1659 		enforceUsage(free_args.length == 1 || free_args.length == 2, "Expecting one or two arguments.");
1660 		string ver = free_args.length == 2 ? free_args[1] : null;
1661 		dub.addLocalPackage(free_args[0], ver, m_system);
1662 		return 0;
1663 	}
1664 }
1665 
1666 class RemoveLocalCommand : RegistrationCommand {
1667 	this()
1668 	{
1669 		this.name = "remove-local";
1670 		this.argumentsPattern = "<path>";
1671 		this.description = "Removes a local package directory";
1672 		this.helpText = ["Removes a local package directory"];
1673 	}
1674 
1675 	override int execute(Dub dub, string[] free_args, string[] app_args)
1676 	{
1677 		enforceUsage(free_args.length >= 1, "Missing package path argument.");
1678 		enforceUsage(free_args.length <= 1, "Expected the package path to be the only argument.");
1679 		dub.removeLocalPackage(free_args[0], m_system);
1680 		return 0;
1681 	}
1682 }
1683 
1684 class ListCommand : Command {
1685 	this()
1686 	{
1687 		this.name = "list";
1688 		this.argumentsPattern = "";
1689 		this.description = "Prints a list of all local packages dub is aware of";
1690 		this.helpText = [
1691 			"Prints a list of all local packages. This includes all cached packages (user or system wide), all packages in the package search paths (\"dub add-path\") and all manually registered packages (\"dub add-local\")."
1692 		];
1693 	}
1694 	override void prepare(scope CommandArgs args) {}
1695 	override int execute(Dub dub, string[] free_args, string[] app_args)
1696 	{
1697 		enforceUsage(free_args.length == 0, "Expecting no extra arguments.");
1698 		enforceUsage(app_args.length == 0, "The list command supports no application arguments.");
1699 		logInfo("Packages present in the system and known to dub:");
1700 		foreach (p; dub.packageManager.getPackageIterator())
1701 			logInfo("  %s %s: %s", p.name, p.version_, p.path.toNativeString());
1702 		logInfo("");
1703 		return 0;
1704 	}
1705 }
1706 
1707 class SearchCommand : Command {
1708 	this()
1709 	{
1710 		this.name = "search";
1711 		this.argumentsPattern = "<query>";
1712 		this.description = "Search for available packages.";
1713 		this.helpText = [
1714 			"Search all specified DUB registries for packages matching query."
1715 		];
1716 	}
1717 	override void prepare(scope CommandArgs args) {}
1718 	override int execute(Dub dub, string[] free_args, string[] app_args)
1719 	{
1720 		enforce(free_args.length == 1, "Expected one argument.");
1721 		auto res = dub.searchPackages(free_args[0]);
1722 		if (res.empty)
1723 		{
1724 			logError("No matches found.");
1725 			return 1;
1726 		}
1727 		auto justify = res
1728 			.map!((descNmatches) => descNmatches[1])
1729 			.joiner
1730 			.map!(m => m.name.length + m.version_.length)
1731 			.reduce!max + " ()".length;
1732 		justify += (~justify & 3) + 1; // round to next multiple of 4
1733 		foreach (desc, matches; res)
1734 		{
1735 			logInfo("==== %s ====", desc);
1736 			foreach (m; matches)
1737 				logInfo("%s%s", leftJustify(m.name ~ " (" ~ m.version_ ~ ")", justify), m.description);
1738 		}
1739 		return 0;
1740 	}
1741 }
1742 
1743 
1744 /******************************************************************************/
1745 /* OVERRIDES                                                                  */
1746 /******************************************************************************/
1747 
1748 class AddOverrideCommand : Command {
1749 	private {
1750 		bool m_system = false;
1751 	}
1752 
1753 	this()
1754 	{
1755 		this.name = "add-override";
1756 		this.argumentsPattern = "<package> <version-spec> <target-path/target-version>";
1757 		this.description = "Adds a new package override.";
1758 		this.helpText = [
1759 		];
1760 	}
1761 
1762 	override void prepare(scope CommandArgs args)
1763 	{
1764 		args.getopt("system", &m_system, [
1765 			"Register system-wide instead of user-wide"
1766 		]);
1767 	}
1768 
1769 	override int execute(Dub dub, string[] free_args, string[] app_args)
1770 	{
1771 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1772 		enforceUsage(free_args.length == 3, "Expected three arguments, not "~free_args.length.to!string);
1773 		auto scope_ = m_system ? LocalPackageType.system : LocalPackageType.user;
1774 		auto pack = free_args[0];
1775 		auto ver = Dependency(free_args[1]);
1776 		if (existsFile(NativePath(free_args[2]))) {
1777 			auto target = NativePath(free_args[2]);
1778 			if (!target.absolute) target = NativePath(getcwd()) ~ target;
1779 			dub.packageManager.addOverride(scope_, pack, ver, target);
1780 			logInfo("Added override %s %s => %s", pack, ver, target);
1781 		} else {
1782 			auto target = Version(free_args[2]);
1783 			dub.packageManager.addOverride(scope_, pack, ver, target);
1784 			logInfo("Added override %s %s => %s", pack, ver, target);
1785 		}
1786 		return 0;
1787 	}
1788 }
1789 
1790 class RemoveOverrideCommand : Command {
1791 	private {
1792 		bool m_system = false;
1793 	}
1794 
1795 	this()
1796 	{
1797 		this.name = "remove-override";
1798 		this.argumentsPattern = "<package> <version-spec>";
1799 		this.description = "Removes an existing package override.";
1800 		this.helpText = [
1801 		];
1802 	}
1803 
1804 	override void prepare(scope CommandArgs args)
1805 	{
1806 		args.getopt("system", &m_system, [
1807 			"Register system-wide instead of user-wide"
1808 		]);
1809 	}
1810 
1811 	override int execute(Dub dub, string[] free_args, string[] app_args)
1812 	{
1813 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1814 		enforceUsage(free_args.length == 2, "Expected two arguments, not "~free_args.length.to!string);
1815 		auto scope_ = m_system ? LocalPackageType.system : LocalPackageType.user;
1816 		dub.packageManager.removeOverride(scope_, free_args[0], Dependency(free_args[1]));
1817 		return 0;
1818 	}
1819 }
1820 
1821 class ListOverridesCommand : Command {
1822 	this()
1823 	{
1824 		this.name = "list-overrides";
1825 		this.argumentsPattern = "";
1826 		this.description = "Prints a list of all local package overrides";
1827 		this.helpText = [
1828 			"Prints a list of all overridden packages added via \"dub add-override\"."
1829 		];
1830 	}
1831 	override void prepare(scope CommandArgs args) {}
1832 	override int execute(Dub dub, string[] free_args, string[] app_args)
1833 	{
1834 		void printList(in PackageOverride[] overrides, string caption)
1835 		{
1836 			if (overrides.length == 0) return;
1837 			logInfo("# %s", caption);
1838 			foreach (ovr; overrides) {
1839 				if (!ovr.targetPath.empty) logInfo("%s %s => %s", ovr.package_, ovr.version_, ovr.targetPath);
1840 				else logInfo("%s %s => %s", ovr.package_, ovr.version_, ovr.targetVersion);
1841 			}
1842 		}
1843 		printList(dub.packageManager.getOverrides(LocalPackageType.user), "User wide overrides");
1844 		printList(dub.packageManager.getOverrides(LocalPackageType.system), "System wide overrides");
1845 		return 0;
1846 	}
1847 }
1848 
1849 /******************************************************************************/
1850 /* Cache cleanup                                                              */
1851 /******************************************************************************/
1852 
1853 class CleanCachesCommand : Command {
1854 	this()
1855 	{
1856 		this.name = "clean-caches";
1857 		this.argumentsPattern = "";
1858 		this.description = "Removes cached metadata";
1859 		this.helpText = [
1860 			"This command removes any cached metadata like the list of available packages and their latest version."
1861 		];
1862 	}
1863 
1864 	override void prepare(scope CommandArgs args) {}
1865 
1866 	override int execute(Dub dub, string[] free_args, string[] app_args)
1867 	{
1868 		return 0;
1869 	}
1870 }
1871 
1872 /******************************************************************************/
1873 /* DUSTMITE                                                                   */
1874 /******************************************************************************/
1875 
1876 class DustmiteCommand : PackageBuildCommand {
1877 	private {
1878 		int m_compilerStatusCode = int.min;
1879 		int m_linkerStatusCode = int.min;
1880 		int m_programStatusCode = int.min;
1881 		string m_compilerRegex;
1882 		string m_linkerRegex;
1883 		string m_programRegex;
1884 		string m_testPackage;
1885 		bool m_combined;
1886 	}
1887 
1888 	this()
1889 	{
1890 		this.name = "dustmite";
1891 		this.argumentsPattern = "<destination-path>";
1892 		this.acceptsAppArgs = true;
1893 		this.description = "Create reduced test cases for build errors";
1894 		this.helpText = [
1895 			"This command uses the Dustmite utility to isolate the cause of build errors in a DUB project.",
1896 			"",
1897 			"It will create a copy of all involved packages and run dustmite on this copy, leaving a reduced test case.",
1898 			"",
1899 			"Determining the desired error condition is done by checking the compiler/linker status code, as well as their output (stdout and stderr combined). If --program-status or --program-regex is given and the generated binary is an executable, it will be executed and its output will also be incorporated into the final decision."
1900 		];
1901 	}
1902 
1903 	override void prepare(scope CommandArgs args)
1904 	{
1905 		args.getopt("compiler-status", &m_compilerStatusCode, ["The expected status code of the compiler run"]);
1906 		args.getopt("compiler-regex", &m_compilerRegex, ["A regular expression used to match against the compiler output"]);
1907 		args.getopt("linker-status", &m_linkerStatusCode, ["The expected status code of the linker run"]);
1908 		args.getopt("linker-regex", &m_linkerRegex, ["A regular expression used to match against the linker output"]);
1909 		args.getopt("program-status", &m_programStatusCode, ["The expected status code of the built executable"]);
1910 		args.getopt("program-regex", &m_programRegex, ["A regular expression used to match against the program output"]);
1911 		args.getopt("test-package", &m_testPackage, ["Perform a test run - usually only used internally"]);
1912 		args.getopt("combined", &m_combined, ["Builds multiple packages with one compiler run"]);
1913 		super.prepare(args);
1914 
1915 		// speed up loading when in test mode
1916 		if (m_testPackage.length) {
1917 			skipDubInitialization = true;
1918 			m_nodeps = true;
1919 		}
1920 	}
1921 
1922 	override int execute(Dub dub, string[] free_args, string[] app_args)
1923 	{
1924 		import std.format : formattedWrite;
1925 
1926 		if (m_testPackage.length) {
1927 			dub = new Dub(NativePath(getcwd()));
1928 
1929 			setupPackage(dub, m_testPackage);
1930 			m_defaultConfig = dub.project.getDefaultConfiguration(m_buildPlatform);
1931 
1932 			GeneratorSettings gensettings;
1933 			gensettings.platform = m_buildPlatform;
1934 			gensettings.config = m_buildConfig.length ? m_buildConfig : m_defaultConfig;
1935 			gensettings.buildType = m_buildType;
1936 			gensettings.compiler = m_compiler;
1937 			gensettings.buildSettings = m_buildSettings;
1938 			gensettings.combined = m_combined;
1939 			gensettings.filterVersions = m_filterVersions;
1940 			gensettings.run = m_programStatusCode != int.min || m_programRegex.length;
1941 			gensettings.runArgs = app_args;
1942 			gensettings.force = true;
1943 			gensettings.compileCallback = check(m_compilerStatusCode, m_compilerRegex);
1944 			gensettings.linkCallback = check(m_linkerStatusCode, m_linkerRegex);
1945 			gensettings.runCallback = check(m_programStatusCode, m_programRegex);
1946 			try dub.generateProject("build", gensettings);
1947 			catch (DustmiteMismatchException) {
1948 				logInfo("Dustmite test doesn't match.");
1949 				return 3;
1950 			}
1951 			catch (DustmiteMatchException) {
1952 				logInfo("Dustmite test matches.");
1953 				return 0;
1954 			}
1955 		} else {
1956 			enforceUsage(free_args.length == 1, "Expected destination path.");
1957 			auto path = NativePath(free_args[0]);
1958 			path.normalize();
1959 			enforceUsage(!path.empty, "Destination path must not be empty.");
1960 			if (!path.absolute) path = NativePath(getcwd()) ~ path;
1961 			enforceUsage(!path.startsWith(dub.rootPath), "Destination path must not be a sub directory of the tested package!");
1962 
1963 			setupPackage(dub, null);
1964 			auto prj = dub.project;
1965 			if (m_buildConfig.empty)
1966 				m_buildConfig = prj.getDefaultConfiguration(m_buildPlatform);
1967 
1968 			void copyFolderRec(NativePath folder, NativePath dstfolder)
1969 			{
1970 				mkdirRecurse(dstfolder.toNativeString());
1971 				foreach (de; iterateDirectory(folder.toNativeString())) {
1972 					if (de.name.startsWith(".")) continue;
1973 					if (de.isDirectory) {
1974 						copyFolderRec(folder ~ de.name, dstfolder ~ de.name);
1975 					} else {
1976 						if (de.name.endsWith(".o") || de.name.endsWith(".obj")) continue;
1977 						if (de.name.endsWith(".exe")) continue;
1978 						try copyFile(folder ~ de.name, dstfolder ~ de.name);
1979 						catch (Exception e) {
1980 							logWarn("Failed to copy file %s: %s", (folder ~ de.name).toNativeString(), e.msg);
1981 						}
1982 					}
1983 				}
1984 			}
1985 
1986 			static void fixPathDependency(string pack, ref Dependency dep) {
1987 				if (!dep.path.empty) {
1988 					auto mainpack = getBasePackageName(pack);
1989 					dep.path = NativePath("../") ~ mainpack;
1990 				}
1991 			}
1992 
1993 			void fixPathDependencies(ref PackageRecipe recipe, NativePath base_path)
1994 			{
1995 				foreach (name, ref dep; recipe.buildSettings.dependencies)
1996 					fixPathDependency(name, dep);
1997 
1998 				foreach (ref cfg; recipe.configurations)
1999 					foreach (name, ref dep; cfg.buildSettings.dependencies)
2000 						fixPathDependency(name, dep);
2001 
2002 				foreach (ref subp; recipe.subPackages)
2003 					if (subp.path.length) {
2004 						auto sub_path = base_path ~ NativePath(subp.path);
2005 						auto pack = prj.packageManager.getOrLoadPackage(sub_path);
2006 						fixPathDependencies(pack.recipe, sub_path);
2007 						pack.storeInfo(sub_path);
2008 					} else fixPathDependencies(subp.recipe, base_path);
2009 			}
2010 
2011 			bool[string] visited;
2012 			foreach (pack_; prj.getTopologicalPackageList()) {
2013 				auto pack = pack_.basePackage;
2014 				if (pack.name in visited) continue;
2015 				visited[pack.name] = true;
2016 				auto dst_path = path ~ pack.name;
2017 				logInfo("Copy package '%s' to destination folder...", pack.name);
2018 				copyFolderRec(pack.path, dst_path);
2019 
2020 				// adjust all path based dependencies
2021 				fixPathDependencies(pack.recipe, dst_path);
2022 
2023 				// overwrite package description file with additional version information
2024 				pack.storeInfo(dst_path);
2025 			}
2026 
2027 			logInfo("Executing dustmite...");
2028 			auto testcmd = appender!string();
2029 			testcmd.formattedWrite("%s dustmite --vquiet --test-package=%s --build=%s --config=%s",
2030 				thisExePath, prj.name, m_buildType, m_buildConfig);
2031 
2032 			if (m_compilerName.length) testcmd.formattedWrite(" \"--compiler=%s\"", m_compilerName);
2033 			if (m_arch.length) testcmd.formattedWrite(" --arch=%s", m_arch);
2034 			if (m_compilerStatusCode != int.min) testcmd.formattedWrite(" --compiler-status=%s", m_compilerStatusCode);
2035 			if (m_compilerRegex.length) testcmd.formattedWrite(" \"--compiler-regex=%s\"", m_compilerRegex);
2036 			if (m_linkerStatusCode != int.min) testcmd.formattedWrite(" --linker-status=%s", m_linkerStatusCode);
2037 			if (m_linkerRegex.length) testcmd.formattedWrite(" \"--linker-regex=%s\"", m_linkerRegex);
2038 			if (m_programStatusCode != int.min) testcmd.formattedWrite(" --program-status=%s", m_programStatusCode);
2039 			if (m_programRegex.length) testcmd.formattedWrite(" \"--program-regex=%s\"", m_programRegex);
2040 			if (m_combined) testcmd ~= " --combined";
2041 			// TODO: pass *all* original parameters
2042 			logDiagnostic("Running dustmite: %s", testcmd);
2043 			auto dmpid = spawnProcess(["dustmite", path.toNativeString(), testcmd.data]);
2044 			return dmpid.wait();
2045 		}
2046 		return 0;
2047 	}
2048 
2049 	void delegate(int, string) check(int code_match, string regex_match)
2050 	{
2051 		return (code, output) {
2052 			import std.encoding;
2053 			import std.regex;
2054 
2055 			logInfo("%s", output);
2056 
2057 			if (code_match != int.min && code != code_match) {
2058 				logInfo("Exit code %s doesn't match expected value %s", code, code_match);
2059 				throw new DustmiteMismatchException;
2060 			}
2061 
2062 			if (regex_match.length > 0 && !match(output.sanitize, regex_match)) {
2063 				logInfo("Output doesn't match regex:");
2064 				logInfo("%s", output);
2065 				throw new DustmiteMismatchException;
2066 			}
2067 
2068 			if (code != 0 && code_match != int.min || regex_match.length > 0) {
2069 				logInfo("Tool failed, but matched either exit code or output - counting as match.");
2070 				throw new DustmiteMatchException;
2071 			}
2072 		};
2073 	}
2074 
2075 	static class DustmiteMismatchException : Exception {
2076 		this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null)
2077 		{
2078 			super(message, file, line, next);
2079 		}
2080 	}
2081 
2082 	static class DustmiteMatchException : Exception {
2083 		this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null)
2084 		{
2085 			super(message, file, line, next);
2086 		}
2087 	}
2088 }
2089 
2090 
2091 /******************************************************************************/
2092 /* CONVERT command                                                               */
2093 /******************************************************************************/
2094 
2095 class ConvertCommand : Command {
2096 	private {
2097 		string m_format;
2098 		bool m_stdout;
2099 	}
2100 
2101 	this()
2102 	{
2103 		this.name = "convert";
2104 		this.argumentsPattern = "";
2105 		this.description = "Converts the file format of the package recipe.";
2106 		this.helpText = [
2107 			"This command will convert between JSON and SDLang formatted package recipe files.",
2108 			"",
2109 			"Warning: Beware that any formatting and comments within the package recipe will get lost in the conversion process."
2110 		];
2111 	}
2112 
2113 	override void prepare(scope CommandArgs args)
2114 	{
2115 		args.getopt("f|format", &m_format, ["Specifies the target package recipe format. Possible values:", "  json, sdl"]);
2116 		args.getopt("s|stdout", &m_stdout, ["Outputs the converted package recipe to stdout instead of writing to disk."]);
2117 	}
2118 
2119 	override int execute(Dub dub, string[] free_args, string[] app_args)
2120 	{
2121 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
2122 		enforceUsage(free_args.length == 0, "Unexpected arguments: "~free_args.join(" "));
2123 		enforceUsage(m_format.length > 0, "Missing target format file extension (--format=...).");
2124 		if (!loadCwdPackage(dub, true)) return 1;
2125 		dub.convertRecipe(m_format, m_stdout);
2126 		return 0;
2127 	}
2128 }
2129 
2130 
2131 /******************************************************************************/
2132 /* HELP                                                                       */
2133 /******************************************************************************/
2134 
2135 private {
2136 	enum shortArgColumn = 2;
2137 	enum longArgColumn = 6;
2138 	enum descColumn = 24;
2139 	enum lineWidth = 80 - 1;
2140 }
2141 
2142 private void showHelp(in CommandGroup[] commands, CommandArgs common_args)
2143 {
2144 	writeln(
2145 `USAGE: dub [--version] [<command>] [<options...>] [-- [<application arguments...>]]
2146 
2147 Manages the DUB project in the current directory. If the command is omitted,
2148 DUB will default to "run". When running an application, "--" can be used to
2149 separate DUB options from options passed to the application.
2150 
2151 Run "dub <command> --help" to get help for a specific command.
2152 
2153 You can use the "http_proxy" environment variable to configure a proxy server
2154 to be used for fetching packages.
2155 
2156 
2157 Available commands
2158 ==================`);
2159 
2160 	foreach (grp; commands) {
2161 		writeln();
2162 		writeWS(shortArgColumn);
2163 		writeln(grp.caption);
2164 		writeWS(shortArgColumn);
2165 		writerep!'-'(grp.caption.length);
2166 		writeln();
2167 		foreach (cmd; grp.commands) {
2168 			if (cmd.hidden) continue;
2169 			writeWS(shortArgColumn);
2170 			writef("%s %s", cmd.name, cmd.argumentsPattern);
2171 			auto chars_output = cmd.name.length + cmd.argumentsPattern.length + shortArgColumn + 1;
2172 			if (chars_output < descColumn) {
2173 				writeWS(descColumn - chars_output);
2174 			} else {
2175 				writeln();
2176 				writeWS(descColumn);
2177 			}
2178 			writeWrapped(cmd.description, descColumn, descColumn);
2179 		}
2180 	}
2181 	writeln();
2182 	writeln();
2183 	writeln(`Common options`);
2184 	writeln(`==============`);
2185 	writeln();
2186 	writeOptions(common_args);
2187 	writeln();
2188 	showVersion();
2189 }
2190 
2191 private void showVersion()
2192 {
2193 	writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__);
2194 }
2195 
2196 private void showCommandHelp(Command cmd, CommandArgs args, CommandArgs common_args)
2197 {
2198 	writefln(`USAGE: dub %s %s [<options...>]%s`, cmd.name, cmd.argumentsPattern, cmd.acceptsAppArgs ? " [-- <application arguments...>]": null);
2199 	writeln();
2200 	foreach (ln; cmd.helpText)
2201 		ln.writeWrapped();
2202 
2203 	if (args.recognizedArgs.length) {
2204 		writeln();
2205 		writeln();
2206 		writeln("Command specific options");
2207 		writeln("========================");
2208 		writeln();
2209 		writeOptions(args);
2210 	}
2211 
2212 	writeln();
2213 	writeln();
2214 	writeln("Common options");
2215 	writeln("==============");
2216 	writeln();
2217 	writeOptions(common_args);
2218 	writeln();
2219 	writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__);
2220 }
2221 
2222 private void writeOptions(CommandArgs args)
2223 {
2224 	foreach (arg; args.recognizedArgs) {
2225 		auto names = arg.names.split("|");
2226 		assert(names.length == 1 || names.length == 2);
2227 		string sarg = names[0].length == 1 ? names[0] : null;
2228 		string larg = names[0].length > 1 ? names[0] : names.length > 1 ? names[1] : null;
2229 		if (sarg !is null) {
2230 			writeWS(shortArgColumn);
2231 			writef("-%s", sarg);
2232 			writeWS(longArgColumn - shortArgColumn - 2);
2233 		} else writeWS(longArgColumn);
2234 		size_t col = longArgColumn;
2235 		if (larg !is null) {
2236 			if (arg.defaultValue.peek!bool) {
2237 				writef("--%s", larg);
2238 				col += larg.length + 2;
2239 			} else {
2240 				writef("--%s=VALUE", larg);
2241 				col += larg.length + 8;
2242 			}
2243 		}
2244 		if (col < descColumn) {
2245 			writeWS(descColumn - col);
2246 		} else {
2247 			writeln();
2248 			writeWS(descColumn);
2249 		}
2250 		foreach (i, ln; arg.helpText) {
2251 			if (i > 0) writeWS(descColumn);
2252 			ln.writeWrapped(descColumn, descColumn);
2253 		}
2254 	}
2255 }
2256 
2257 private void writeWrapped(string string, size_t indent = 0, size_t first_line_pos = 0)
2258 {
2259 	// handle pre-indented strings and bullet lists
2260 	size_t first_line_indent = 0;
2261 	while (string.startsWith(" ")) {
2262 		string = string[1 .. $];
2263 		indent++;
2264 		first_line_indent++;
2265 	}
2266 	if (string.startsWith("- ")) indent += 2;
2267 
2268 	auto wrapped = string.wrap(lineWidth, getRepString!' '(first_line_pos+first_line_indent), getRepString!' '(indent));
2269 	wrapped = wrapped[first_line_pos .. $];
2270 	foreach (ln; wrapped.splitLines())
2271 		writeln(ln);
2272 }
2273 
2274 private void writeWS(size_t num) { writerep!' '(num); }
2275 private void writerep(char ch)(size_t num) { write(getRepString!ch(num)); }
2276 
2277 private string getRepString(char ch)(size_t len)
2278 {
2279 	static string buf;
2280 	if (len > buf.length) buf ~= [ch].replicate(len-buf.length);
2281 	return buf[0 .. len];
2282 }
2283 
2284 /***
2285 */
2286 
2287 
2288 private void enforceUsage(bool cond, string text)
2289 {
2290 	if (!cond) throw new UsageException(text);
2291 }
2292 
2293 private class UsageException : Exception {
2294 	this(string message, string file = __FILE__, int line = __LINE__, Throwable next = null)
2295 	{
2296 		super(message, file, line, next);
2297 	}
2298 }
2299 
2300 private void warnRenamed(string prev, string curr)
2301 {
2302 	logWarn("The '%s' Command was renamed to '%s'. Please update your scripts.", prev, curr);
2303 }
2304 
2305 private bool addDependency(Dub dub, ref PackageRecipe recipe, string depspec)
2306 {
2307 	Dependency dep;
2308 	const parts = splitPackageName(depspec);
2309 	const depname = parts.name;
2310 	if (parts.version_)
2311 		dep = Dependency(parts.version_);
2312 	else
2313 	{
2314 		try {
2315 			const ver = dub.getLatestVersion(depname);
2316 			dep = ver.isBranch ? Dependency(ver) : Dependency("~>" ~ ver.toString());
2317 		} catch (Exception e) {
2318 			logError("Could not find package '%s'.", depname);
2319 			logDebug("Full error: %s", e.toString().sanitize);
2320 			return false;
2321 		}
2322 	}
2323 	recipe.buildSettings.dependencies[depname] = dep;
2324 	logInfo("Adding dependency %s %s", depname, dep.versionSpec);
2325 	return true;
2326 }
2327 
2328 private struct PackageAndVersion
2329 {
2330 	string name;
2331 	string version_;
2332 }
2333 
2334 /* Split <package>=<version-specifier> and <package>@<version-specifier>
2335    into `name` and `version_`. */
2336 private PackageAndVersion splitPackageName(string packageName)
2337 {
2338 	// split <package>@<version-specifier>
2339 	auto parts = packageName.findSplit("@");
2340 	if (parts[1].empty) {
2341 		// split <package>=<version-specifier>
2342 		parts = packageName.findSplit("=");
2343 	}
2344 
2345 	PackageAndVersion p;
2346 	p.name = parts[0];
2347 	if (!parts[1].empty)
2348 		p.version_ = parts[2];
2349 	return p;
2350 }
2351 
2352 unittest
2353 {
2354 	// https://github.com/dlang/dub/issues/1681
2355 	assert(splitPackageName("") == PackageAndVersion("", null));
2356 
2357 	assert(splitPackageName("foo=1.0.1") == PackageAndVersion("foo", "1.0.1"));
2358 	assert(splitPackageName("foo@1.0.1") == PackageAndVersion("foo", "1.0.1"));
2359 	assert(splitPackageName("foo@==1.0.1") == PackageAndVersion("foo", "==1.0.1"));
2360 	assert(splitPackageName("foo@>=1.0.1") == PackageAndVersion("foo", ">=1.0.1"));
2361 	assert(splitPackageName("foo@~>1.0.1") == PackageAndVersion("foo", "~>1.0.1"));
2362 	assert(splitPackageName("foo@<1.0.1") == PackageAndVersion("foo", "<1.0.1"));
2363 }