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