1 /**
2 	Defines the behavior of the DUB command line client.
3 
4 	Copyright: © 2012-2013 Matthias Dondorff, Copyright © 2012-2014 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.inet.url;
17 import dub.package_;
18 import dub.packagemanager;
19 import dub.packagesupplier;
20 import dub.platform : determineCompiler;
21 import dub.project;
22 import dub.internal.utils : getDUBVersion, getClosestMatch;
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 
37 
38 int runDubCommandLine(string[] args)
39 {
40 	logDiagnostic("DUB version %s", getDUBVersion());
41 
42 	version(Windows){
43 		// rdmd uses $TEMP to compute a temporary path. since cygwin substitutes backslashes
44 		// with slashes, this causes OPTLINK to fail (it thinks path segments are options)
45 		// we substitute the other way around here to fix this.
46 		environment["TEMP"] = environment["TEMP"].replace("/", "\\");
47 	}
48 
49 	// split application arguments from DUB arguments
50 	string[] app_args;
51 	auto app_args_idx = args.countUntil("--");
52 	if (app_args_idx >= 0) {
53 		app_args = args[app_args_idx+1 .. $];
54 		args = args[0 .. app_args_idx];
55 	}
56 	args = args[1 .. $]; // strip the application name
57 
58 	// handle direct dub options
59 	if (args.length) switch (args[0])
60 	{
61 	case "--version":
62 		showVersion();
63 		return 0;
64 
65 	default:
66 		break;
67 	}
68 
69 	// parse general options
70 	bool verbose, vverbose, quiet, vquiet;
71 	bool help, annotate;
72 	LogLevel loglevel = LogLevel.info;
73 	string[] registry_urls;
74 	string root_path = getcwd();
75 
76 	auto common_args = new CommandArgs(args);
77 	try {
78 		common_args.getopt("h|help", &help, ["Display general or command specific help"]);
79 		common_args.getopt("root", &root_path, ["Path to operate in instead of the current working dir"]);
80 		common_args.getopt("registry", &registry_urls, ["Search the given DUB registry URL first when resolving dependencies. Can be specified multiple times."]);
81 		common_args.getopt("annotate", &annotate, ["Do not perform any action, just print what would be done"]);
82 		common_args.getopt("v|verbose", &verbose, ["Print diagnostic output"]);
83 		common_args.getopt("vverbose", &vverbose, ["Print debug output"]);
84 		common_args.getopt("q|quiet", &quiet, ["Only print warnings and errors"]);
85 		common_args.getopt("vquiet", &vquiet, ["Print no messages"]);
86 		common_args.getopt("cache", &defaultPlacementLocation, ["Puts any fetched packages in the specified location [local|system|user]."]);
87 
88 		if( vverbose ) loglevel = LogLevel.debug_;
89 		else if( verbose ) loglevel = LogLevel.diagnostic;
90 		else if( vquiet ) loglevel = LogLevel.none;
91 		else if( quiet ) loglevel = LogLevel.warn;
92 		setLogLevel(loglevel);
93 	} catch (Throwable e) {
94 		logError("Error processing arguments: %s", e.msg);
95 		logDiagnostic("Full exception: %s", e.toString().sanitize);
96 		logInfo("Run 'dub help' for usage information.");
97 		return 1;
98 	}
99 
100 	// create the list of all supported commands
101 
102 	CommandGroup[] commands = [
103 		CommandGroup("Package creation",
104 			new InitCommand
105 		),
106 		CommandGroup("Build, test and run",
107 			new RunCommand,
108 			new BuildCommand,
109 			new TestCommand,
110 			new GenerateCommand,
111 			new DescribeCommand,
112 			new CleanCommand,
113 			new DustmiteCommand
114 		),
115 		CommandGroup("Package management",
116 			new FetchCommand,
117 			new InstallCommand,
118 			new RemoveCommand,
119 			new UninstallCommand,
120 			new UpgradeCommand,
121 			new AddPathCommand,
122 			new RemovePathCommand,
123 			new AddLocalCommand,
124 			new RemoveLocalCommand,
125 			new ListCommand,
126 			new ListInstalledCommand,
127 			new AddOverrideCommand,
128 			new RemoveOverrideCommand,
129 			new ListOverridesCommand,
130 			new CleanCachesCommand,
131 		)
132 	];
133 
134 	// extract the command
135 	string cmdname;
136 	args = common_args.extractRemainingArgs();
137 	if (args.length >= 1 && !args[0].startsWith("-")) {
138 		cmdname = args[0];
139 		args = args[1 .. $];
140 	} else {
141 		if (help) {
142 			showHelp(commands, common_args);
143 			return 0;
144 		}
145 		cmdname = "run";
146 	}
147 	auto command_args = new CommandArgs(args);
148 
149 	if (cmdname == "help") {
150 		showHelp(commands, common_args);
151 		return 0;
152 	}
153 
154 	// find the selected command
155 	Command cmd;
156 	foreach (grp; commands)
157 		foreach (c; grp.commands)
158 			if (c.name == cmdname) {
159 				cmd = c;
160 				break;
161 			}
162 
163 	if (!cmd) {
164 		logError("Unknown command: %s", cmdname);
165 		writeln();
166 		showHelp(commands, common_args);
167 		return 1;
168 	}
169 
170 	// process command line options for the selected command
171 	try {
172 		cmd.prepare(command_args);
173 		enforceUsage(cmd.acceptsAppArgs || app_args.length == 0, cmd.name ~ " doesn't accept application arguments.");
174 	} catch (Throwable e) {
175 		logError("Error processing arguments: %s", e.msg);
176 		logDiagnostic("Full exception: %s", e.toString().sanitize);
177 		logInfo("Run 'dub help' for usage information.");
178 		return 1;
179 	}
180 
181 	if (help) {
182 		showCommandHelp(cmd, command_args, common_args);
183 		return 0;
184 	}
185 
186 	auto remaining_args = command_args.extractRemainingArgs();
187 	if (remaining_args.any!(a => a.startsWith("-"))) {
188 		logError("Unknown command line flags: %s", remaining_args.filter!(a => a.startsWith("-")).array.join(" "));
189 		logError(`Type "dub %s -h" to get a list of all supported flags.`, cmdname);
190 		return 1;
191 	}
192 
193 	Dub dub;
194 
195 	// initialize the root package
196 	if (!cmd.skipDubInitialization) {
197 		// initialize DUB
198 		auto package_suppliers = registry_urls.map!(url => cast(PackageSupplier)new RegistryPackageSupplier(URL(url))).array;
199 		dub = new Dub(package_suppliers, root_path);
200 		dub.dryRun = annotate;
201 
202 		// make the CWD package available so that for example sub packages can reference their
203 		// parent package.
204 		try dub.packageManager.getOrLoadPackage(Path(root_path));
205 		catch (Exception e) { logDiagnostic("No package found in current working directory."); }
206 	}
207 
208 	// execute the command
209 	int rc;
210 	try {
211 		rc = cmd.execute(dub, remaining_args, app_args);
212 	}
213 	catch (UsageException e) {
214 		logError("%s", e.msg);
215 		logDebug("Full exception: %s", e.toString().sanitize);
216 		logInfo(`Run "dub %s -h" for more information about the "%s" command.`, cmdname, cmdname);
217 		return 1;
218 	}
219 	catch (Throwable e) {
220 		logError("Error executing command %s:", cmd.name);
221 		logError("%s", e.msg);
222 		logDebug("Full exception: %s", e.toString().sanitize);
223 		return 2;
224 	}
225 
226 	if (!cmd.skipDubInitialization)
227 		dub.shutdown();
228 	return rc;
229 }
230 
231 class CommandArgs {
232 	struct Arg {
233 		Variant defaultValue;
234 		Variant value;
235 		string names;
236 		string[] helpText;
237 	}
238 	private {
239 		string[] m_args;
240 		Arg[] m_recognizedArgs;
241 	}
242 
243 	this(string[] args)
244 	{
245 		m_args = "dummy" ~ args;
246 	}
247 
248 	@property const(Arg)[] recognizedArgs() { return m_recognizedArgs; }
249 
250 	void getopt(T)(string names, T* var, string[] help_text = null)
251 	{
252 		foreach (ref arg; m_recognizedArgs)
253 			if (names == arg.names) {
254 				assert(help_text is null);
255 				*var = arg.value.get!T;
256 				return;
257 			}
258 		assert(help_text.length > 0);
259 		Arg arg;
260 		arg.defaultValue = *var;
261 		arg.names = names;
262 		arg.helpText = help_text;
263 		m_args.getopt(config.passThrough, names, var);
264 		arg.value = *var;
265 		m_recognizedArgs ~= arg;
266 	}
267 
268 	void dropAllArgs()
269 	{
270 		m_args = null;
271 	}
272 
273 	string[] extractRemainingArgs()
274 	{
275 		auto ret = m_args[1 .. $];
276 		m_args = null;
277 		return ret;
278 	}
279 }
280 
281 class Command {
282 	string name;
283 	string argumentsPattern;
284 	string description;
285 	string[] helpText;
286 	bool acceptsAppArgs;
287 	bool hidden = false; // used for deprecated commands
288 	bool skipDubInitialization = false;
289 
290 	abstract void prepare(scope CommandArgs args);
291 	abstract int execute(Dub dub, string[] free_args, string[] app_args);
292 }
293 
294 struct CommandGroup {
295 	string caption;
296 	Command[] commands;
297 
298 	this(string caption, Command[] commands...)
299 	{
300 		this.caption = caption;
301 		this.commands = commands.dup;
302 	}
303 }
304 
305 
306 /******************************************************************************/
307 /* INIT                                                                       */
308 /******************************************************************************/
309 
310 class InitCommand : Command {
311 	private{
312 		string m_buildType = "minimal";
313 	}
314 	this()
315 	{
316 		this.name = "init";
317 		this.argumentsPattern = "[<directory> [<dependency>...]]";
318 		this.description = "Initializes an empty package skeleton";
319 		this.helpText = [
320 			"Initializes an empty package of the specified type in the given directory. By default, the current working dirctory is used."
321 		];
322 	}
323 
324 	override void prepare(scope CommandArgs args)
325 	{
326 		args.getopt("t|type", &m_buildType, [
327 			"Set the type of project to generate. Available types:",
328 			"",
329 			"minimal - simple \"hello world\" project (default)",
330 			"vibe.d  - minimal HTTP server based on vibe.d",
331 			"deimos  - skeleton for C header bindings",
332 		]);
333 	}
334 
335 	override int execute(Dub dub, string[] free_args, string[] app_args)
336 	{
337 		string dir;
338 		enforceUsage(app_args.empty, "Unexpected application arguments.");
339 		if (free_args.length)
340 		{
341 			dir = free_args[0];
342 			free_args = free_args[1 .. $];
343 		}
344 		//TODO: Remove this block in next version
345 		// Checks if argument uses current method of specifying project type.
346 		if (free_args.length)
347 		{
348 			if (["vibe.d", "deimos", "minimal"].canFind(free_args[0]))
349 			{
350 				m_buildType = free_args[0];
351 				free_args = free_args[1 .. $];
352 				logInfo("Deprecated use of init type. Use --type=[vibe.d | deimos | minimal] in future.");
353 			}
354 		}
355 		dub.createEmptyPackage(Path(dir), free_args, m_buildType);
356 		return 0;
357 	}
358 }
359 
360 
361 /******************************************************************************/
362 /* GENERATE / BUILD / RUN / TEST / DESCRIBE                                   */
363 /******************************************************************************/
364 
365 abstract class PackageBuildCommand : Command {
366 	protected {
367 		string m_buildType;
368 		BuildMode m_buildMode;
369 		string m_buildConfig;
370 		string m_compilerName;
371 		string m_arch;
372 		string[] m_debugVersions;
373 		Compiler m_compiler;
374 		BuildPlatform m_buildPlatform;
375 		BuildSettings m_buildSettings;
376 		string m_defaultConfig;
377 		bool m_nodeps;
378 		bool m_forceRemove = false;
379 	}
380 
381 	this()
382 	{
383 		m_compilerName = defaultCompiler();
384 	}
385 
386 	override void prepare(scope CommandArgs args)
387 	{
388 		args.getopt("b|build", &m_buildType, [
389 			"Specifies the type of build to perform. Note that setting the DFLAGS environment variable will override the build type with custom flags.",
390 			"Possible names:",
391 			"  debug (default), plain, release, release-nobounds, unittest, profile, docs, ddox, cov, unittest-cov and custom types"
392 		]);
393 		args.getopt("c|config", &m_buildConfig, [
394 			"Builds the specified configuration. Configurations can be defined in dub.json"
395 		]);
396 		args.getopt("compiler", &m_compilerName, [
397 			"Specifies the compiler binary to use (can be a path).",
398 			"Arbitrary pre- and suffixes to the identifiers below are recognized (e.g. ldc2 or dmd-2.063) and matched to the proper compiler type:",
399 			"  "~["dmd", "gdc", "ldc", "gdmd", "ldmd"].join(", "),
400 			"Default value: "~m_compilerName,
401 		]);
402 		args.getopt("a|arch", &m_arch, [
403 			"Force a different architecture (e.g. x86 or x86_64)"
404 		]);
405 		args.getopt("d|debug", &m_debugVersions, [
406 			"Define the specified debug version identifier when building - can be used multiple times"
407 		]);
408 		args.getopt("nodeps", &m_nodeps, [
409 			"Do not check/update dependencies before building"
410 		]);
411 		args.getopt("force-remove", &m_forceRemove, [
412 			"Force deletion of fetched packages with untracked files when upgrading"
413 		]);
414 		args.getopt("build-mode", &m_buildMode, [
415 			"Specifies the way the compiler and linker are invoked. Valid values:",
416 			"  separate (default), allAtOnce, singleFile"
417 		]);
418 	}
419 
420 	protected void setupPackage(Dub dub, string package_name)
421 	{
422 		m_compiler = getCompiler(m_compilerName);
423 		m_buildPlatform = m_compiler.determinePlatform(m_buildSettings, m_compilerName, m_arch);
424 		m_buildSettings.addDebugVersions(m_debugVersions);
425 
426 		m_defaultConfig = null;
427 		enforce (loadSpecificPackage(dub, package_name), "Failed to load package.");
428 
429 		if (m_buildConfig.length != 0 && !dub.configurations.canFind(m_buildConfig))
430 		{
431 			string msg = "Unknown build configuration: "~m_buildConfig;
432 			enum distance = 3;
433 			if (auto match = dub.configurations.getClosestMatch(m_buildConfig, distance))
434 				msg ~= ". Did you mean '" ~ match ~ "'?";
435 			enforce(0, msg);
436 		}
437 
438 		if (m_buildType.length == 0) {
439 			if (environment.get("DFLAGS")) m_buildType = "$DFLAGS";
440 			else m_buildType = "debug";
441 		}
442 
443 		if (!m_nodeps) {
444 			// TODO: only upgrade(select) if necessary, only upgrade(upgrade) every now and then
445 
446 			// retrieve missing packages
447 			logDiagnostic("Checking for missing dependencies.");
448 			dub.upgrade(UpgradeOptions.select);
449 			// check for updates
450 			logDiagnostic("Checking for upgrades.");
451 			dub.upgrade(UpgradeOptions.upgrade|UpgradeOptions.printUpgradesOnly|UpgradeOptions.useCachedResult);
452 		}
453 
454 		dub.project.validate();
455 	}
456 
457 	private bool loadSpecificPackage(Dub dub, string package_name)
458 	{
459 		Package pack;
460 		if (!package_name.empty) {
461 			// load package in root_path to enable searching for sub packages
462 			if (loadCwdPackage(dub, null, false)) {
463 				if (package_name.startsWith(":"))
464 					package_name = dub.projectName ~ package_name;
465 			}
466 			pack = dub.packageManager.getFirstPackage(package_name);
467 			enforce(pack, "Failed to find a package named '"~package_name~"'.");
468 			logInfo("Building package %s in %s", pack.name, pack.path.toNativeString());
469 			dub.rootPath = pack.path;
470 		}
471 		if (!loadCwdPackage(dub, pack, true)) return false;
472 		return true;
473 	}
474 
475 	private bool loadCwdPackage(Dub dub, Package pack, bool warn_missing_package)
476 	{
477 		if (warn_missing_package) {
478 			bool found = existsFile(dub.rootPath ~ "source/app.d");
479 			if (!found)
480 				foreach (f; packageInfoFiles)
481 					if (existsFile(dub.rootPath ~ f.filename)) {
482 						found = true;
483 						break;
484 					}
485 			if (!found) {
486 				logInfo("");
487 				logInfo("Neither a package description file, nor source/app.d was found in");
488 				logInfo(dub.rootPath.toNativeString());
489 				logInfo("Please run DUB from the root directory of an existing package, or run");
490 				logInfo("\"dub init --help\" to get information on creating a new package.");
491 				logInfo("");
492 				return false;
493 			}
494 		}
495 
496 		if (pack) dub.loadPackage(pack);
497 		else dub.loadPackageFromCwd();
498 
499 		return true;
500 	}
501 }
502 
503 class GenerateCommand : PackageBuildCommand {
504 	protected {
505 		string m_generator;
506 		bool m_rdmd = false;
507 		bool m_tempBuild = false;
508 		bool m_run = false;
509 		bool m_force = false;
510 		bool m_combined = false;
511 		bool m_parallel = false;
512 		bool m_printPlatform, m_printBuilds, m_printConfigs;
513 	}
514 
515 	this()
516 	{
517 		this.name = "generate";
518 		this.argumentsPattern = "<generator> [<package>]";
519 		this.description = "Generates project files using the specified generator";
520 		this.helpText = [
521 			"Generates project files using one of the supported generators:",
522 			"",
523 			"visuald - VisualD project files",
524 			"sublimetext - SublimeText project file",
525 			"cmake - CMake build scripts",
526 			"build - Builds the package directly",
527 			"",
528 			"An optional package name can be given to generate a different package than the root/CWD package."
529 		];
530 	}
531 
532 	override void prepare(scope CommandArgs args)
533 	{
534 		super.prepare(args);
535 
536 		args.getopt("combined", &m_combined, [
537 			"Tries to build the whole project in a single compiler run."
538 		]);
539 
540 		args.getopt("print-builds", &m_printBuilds, [
541 			"Prints the list of available build types"
542 		]);
543 		args.getopt("print-configs", &m_printConfigs, [
544 			"Prints the list of available configurations"
545 		]);
546 		args.getopt("print-platform", &m_printPlatform, [
547 			"Prints the identifiers for the current build platform as used for the build fields in dub.json"
548 		]);
549 		args.getopt("parallel", &m_parallel, [
550 			"Runs multiple compiler instances in parallel, if possible."
551 		]);
552 	}
553 
554 	override int execute(Dub dub, string[] free_args, string[] app_args)
555 	{
556 		string package_name;
557 		if (!m_generator.length) {
558 			enforceUsage(free_args.length >= 1 && free_args.length <= 2, "Expected one or two arguments.");
559 			m_generator = free_args[0];
560 			if (free_args.length >= 2) package_name = free_args[1];
561 		} else {
562 			enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
563 			if (free_args.length >= 1) package_name = free_args[0];
564 		}
565 
566 		setupPackage(dub, package_name);
567 
568 		if (m_printBuilds) { // FIXME: use actual package data
569 			logInfo("Available build types:");
570 			foreach (tp; ["debug", "release", "unittest", "profile"])
571 				logInfo("  %s", tp);
572 			logInfo("");
573 		}
574 
575 		m_defaultConfig = dub.project.getDefaultConfiguration(m_buildPlatform);
576 		if (m_printConfigs) {
577 			logInfo("Available configurations:");
578 			foreach (tp; dub.configurations)
579 				logInfo("  %s%s", tp, tp == m_defaultConfig ? " [default]" : null);
580 			logInfo("");
581 		}
582 
583 		GeneratorSettings gensettings;
584 		gensettings.platform = m_buildPlatform;
585 		gensettings.config = m_buildConfig.length ? m_buildConfig : m_defaultConfig;
586 		gensettings.buildType = m_buildType;
587 		gensettings.buildMode = m_buildMode;
588 		gensettings.compiler = m_compiler;
589 		gensettings.buildSettings = m_buildSettings;
590 		gensettings.combined = m_combined;
591 		gensettings.run = m_run;
592 		gensettings.runArgs = app_args;
593 		gensettings.force = m_force;
594 		gensettings.rdmd = m_rdmd;
595 		gensettings.tempBuild = m_tempBuild;
596 		gensettings.parallelBuild = m_parallel;
597 
598 		logDiagnostic("Generating using %s", m_generator);
599 		if (m_generator == "visuald-combined") {
600 			gensettings.combined = true;
601 			m_generator = "visuald";
602 			logWarn(`The generator "visuald-combined" is deprecated, please use the --combined switch instead.`);
603 		}
604 		dub.generateProject(m_generator, gensettings);
605 		if (m_buildType == "ddox") dub.runDdox(gensettings.run);
606 		return 0;
607 	}
608 }
609 
610 class BuildCommand : GenerateCommand {
611 	this()
612 	{
613 		this.name = "build";
614 		this.argumentsPattern = "[<package>]";
615 		this.description = "Builds a package (uses the main package in the current working directory by default)";
616 		this.helpText = [
617 			"Builds a package (uses the main package in the current working directory by default)"
618 		];
619 	}
620 
621 	override void prepare(scope CommandArgs args)
622 	{
623 		args.getopt("rdmd", &m_rdmd, [
624 			"Use rdmd instead of directly invoking the compiler"
625 		]);
626 
627 		args.getopt("f|force", &m_force, [
628 			"Forces a recompilation even if the target is up to date"
629 		]);
630 		super.prepare(args);
631 		m_generator = "build";
632 	}
633 
634 	override int execute(Dub dub, string[] free_args, string[] app_args)
635 	{
636 		return super.execute(dub, free_args, app_args);
637 	}
638 }
639 
640 class RunCommand : BuildCommand {
641 	this()
642 	{
643 		this.name = "run";
644 		this.argumentsPattern = "[<package>]";
645 		this.description = "Builds and runs a package (default command)";
646 		this.helpText = [
647 			"Builds and runs a package (uses the main package in the current working directory by default)"
648 		];
649 		this.acceptsAppArgs = true;
650 	}
651 
652 	override void prepare(scope CommandArgs args)
653 	{
654 		args.getopt("temp-build", &m_tempBuild, [
655 			"Builds the project in the temp folder if possible."
656 		]);
657 
658 		super.prepare(args);
659 		m_run = true;
660 	}
661 
662 	override int execute(Dub dub, string[] free_args, string[] app_args)
663 	{
664 		return super.execute(dub, free_args, app_args);
665 	}
666 }
667 
668 class TestCommand : PackageBuildCommand {
669 	private {
670 		string m_mainFile;
671 		bool m_combined = false;
672 		bool m_force = false;
673 	}
674 
675 	this()
676 	{
677 		this.name = "test";
678 		this.argumentsPattern = "[<package>]";
679 		this.description = "Executes the tests of the selected package";
680 		this.helpText = [
681 			`Builds the package and executes all contained unit tests.`,
682 			``,
683 			`If no explicit configuration is given, an existing "unittest" ` ~
684 			`configuration will be preferred for testing. If none exists, the ` ~
685 			`first library type configuration will be used, and if that doesn't ` ~
686 			`exist either, the first executable configuration is chosen.`,
687 			``,
688 			`When a custom main file (--main-file) is specified, only library ` ~
689 			`configurations can be used. Otherwise, depending on the type of ` ~
690 			`the selected configuration, either an existing main file will be ` ~
691 			`used (and needs to be properly adjusted to just run the unit ` ~
692 			`tests for 'version(unittest)'), or DUB will generate one for ` ~
693 			`library type configurations.`,
694 			``,
695 			`Finally, if the package contains a dependency to the "tested" ` ~
696 			`package, the automatically generated main file will use it to ` ~
697 			`run the unit tests.`
698 		];
699 		this.acceptsAppArgs = true;
700 
701 		m_buildType = "unittest";
702 	}
703 
704 	override void prepare(scope CommandArgs args)
705 	{
706 		args.getopt("main-file", &m_mainFile, [
707 			"Specifies a custom file containing the main() function to use for running the tests."
708 		]);
709 		args.getopt("combined", &m_combined, [
710 			"Tries to build the whole project in a single compiler run."
711 		]);
712 		args.getopt("f|force", &m_force, [
713 			"Forces a recompilation even if the target is up to date"
714 		]);
715 		bool coverage = false;
716 		args.getopt("coverage", &coverage, [
717 			"Enables code coverage statistics to be generated."
718 		]);
719 		if (coverage) m_buildType = "unittest-cov";
720 
721 		super.prepare(args);
722 	}
723 
724 	override int execute(Dub dub, string[] free_args, string[] app_args)
725 	{
726 		string package_name;
727 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
728 		if (free_args.length >= 1) package_name = free_args[0];
729 
730 		setupPackage(dub, package_name);
731 
732 		GeneratorSettings settings;
733 		settings.platform = m_buildPlatform;
734 		settings.compiler = getCompiler(m_buildPlatform.compilerBinary);
735 		settings.buildType = m_buildType;
736 		settings.buildMode = m_buildMode;
737 		settings.buildSettings = m_buildSettings;
738 		settings.combined = m_combined;
739 		settings.force = m_force;
740 		settings.run = true;
741 		settings.runArgs = app_args;
742 
743 		dub.testProject(settings, m_buildConfig, Path(m_mainFile));
744 		return 0;
745 	}
746 }
747 
748 class DescribeCommand : PackageBuildCommand {
749 	this()
750 	{
751 		this.name = "describe";
752 		this.argumentsPattern = "[<package>]";
753 		this.description = "Prints a JSON description of the project and its dependencies";
754 		this.helpText = [
755 			"Prints a JSON build description for the root package an all of their dependencies in a format similar to a JSON package description file. This is useful mostly for IDEs.",
756 			"All usual options that are also used for build/run/generate apply."
757 		];
758 	}
759 
760 	override void prepare(scope CommandArgs args)
761 	{
762 		super.prepare(args);
763 	}
764 
765 	override int execute(Dub dub, string[] free_args, string[] app_args)
766 	{
767 		// disable all log output and use "writeln" to output the JSON description
768 		auto ll = getLogLevel();
769 		setLogLevel(LogLevel.none);
770 		scope (exit) setLogLevel(ll);
771 
772 		string package_name;
773 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
774 		if (free_args.length >= 1) package_name = free_args[0];
775 		setupPackage(dub, package_name);
776 
777 		m_defaultConfig = dub.project.getDefaultConfiguration(m_buildPlatform);
778 
779 		dub.describeProject(m_buildPlatform, m_buildConfig.length ? m_buildConfig : m_defaultConfig);
780 		return 0;
781 	}
782 }
783 
784 class CleanCommand : Command {
785 	private {
786 		bool m_allPackages;
787 	}
788 
789 	this()
790 	{
791 		this.name = "clean";
792 		this.argumentsPattern = "[<package>]";
793 		this.description = "Removes intermediate build files and cached build results";
794 		this.helpText = [
795 			"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.",
796 			"Without arguments, the package in the current working directory will be cleaned."
797 		];
798 	}
799 
800 	override void prepare(scope CommandArgs args)
801 	{
802 		args.getopt("all-packages", &m_allPackages, [
803 			"Cleans up *all* known packages (dub list)"
804 		]);
805 	}
806 
807 	override int execute(Dub dub, string[] free_args, string[] app_args)
808 	{
809 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
810 		enforceUsage(app_args.length == 0, "Application arguments are not supported for the clean command.");
811 		enforceUsage(!m_allPackages || !free_args.length, "The --all-packages flag may not be used together with an explicit package name.");
812 
813 		enforce(free_args.length == 0, "Cleaning a specific package isn't possible right now.");
814 
815 		if (m_allPackages) {
816 			foreach (p; dub.packageManager.getPackageIterator())
817 				dub.cleanPackage(p.path);
818 		} else {
819 			dub.cleanPackage(dub.rootPath);
820 		}
821 
822 		return 0;
823 	}
824 }
825 
826 
827 /******************************************************************************/
828 /* FETCH / REMOVE / UPGRADE                                                   */
829 /******************************************************************************/
830 
831 class UpgradeCommand : Command {
832 	private {
833 		bool m_prerelease = false;
834 		bool m_forceRemove = false;
835 		bool m_missingOnly = false;
836 		bool m_verify = false;
837 	}
838 
839 	this()
840 	{
841 		this.name = "upgrade";
842 		this.argumentsPattern = "[<package>]";
843 		this.description = "Forces an upgrade of all dependencies";
844 		this.helpText = [
845 			"Upgrades all dependencies of the package by querying the package registry(ies) for new versions.",
846 			"",
847 			"This will also update the versions stored in the selections file ("~SelectedVersions.defaultFile~") accordingly.",
848 			"",
849 			"If a package specified, (only) that package will be upgraded. Otherwise all direct and indirect dependencies of the current package will get upgraded."
850 		];
851 	}
852 
853 	override void prepare(scope CommandArgs args)
854 	{
855 		args.getopt("prerelease", &m_prerelease, [
856 			"Uses the latest pre-release version, even if release versions are available"
857 		]);
858 		args.getopt("force-remove", &m_forceRemove, [
859 			"Force deletion of fetched packages with untracked files"
860 		]);
861 		args.getopt("verify", &m_verify, [
862 			"Updates the project and performs a build. If successfull, rewrites the selected versions file <to be implemeted>."
863 		]);
864 		args.getopt("missing-only", &m_missingOnly, [
865 			"Performs an upgrade only for dependencies that don't yet have a version selected. This is also done automatically before each build."
866 		]);
867 	}
868 
869 	override int execute(Dub dub, string[] free_args, string[] app_args)
870 	{
871 		enforceUsage(free_args.length <= 1, "Unexpected arguments.");
872 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
873 		enforceUsage(!m_verify, "--verify is not yet implemented.");
874 		dub.loadPackageFromCwd();
875 		logInfo("Upgrading project in %s", dub.projectPath.toNativeString());
876 		auto options = UpgradeOptions.upgrade|UpgradeOptions.select;
877 		if (m_missingOnly) options &= ~UpgradeOptions.upgrade;
878 		if (m_prerelease) options |= UpgradeOptions.preRelease;
879 		if (m_forceRemove) options |= UpgradeOptions.forceRemove;
880 		enforceUsage(app_args.length == 0, "Upgrading a specific package is not yet implemented.");
881 		dub.upgrade(options);
882 		return 0;
883 	}
884 }
885 
886 class FetchRemoveCommand : Command {
887 	protected {
888 		string m_version;
889 		bool m_forceRemove = false;
890 		bool m_system = false;
891 		bool m_local = false;
892 	}
893 
894 	override void prepare(scope CommandArgs args)
895 	{
896 		args.getopt("version", &m_version, [
897 			"Use the specified version/branch instead of the latest available match",
898 			"The remove command also accepts \"*\" here as a wildcard to remove all versions of the package from the specified location"
899 		]);
900 
901 		args.getopt("system", &m_system, ["Deprecated: Puts the package into the system wide package cache instead of the user local one."]);
902 		args.getopt("local", &m_local, ["Deprecated: Puts the package into a sub folder of the current working directory. Cannot be mixed with --system."]);
903 		args.getopt("force-remove", &m_forceRemove, [
904 			"Force deletion of fetched packages with untracked files"
905 		]);
906 	}
907 
908 	abstract override int execute(Dub dub, string[] free_args, string[] app_args);
909 }
910 
911 class FetchCommand : FetchRemoveCommand {
912 	this()
913 	{
914 		this.name = "fetch";
915 		this.argumentsPattern = "<name>";
916 		this.description = "Manually retrieves and caches a package";
917 		this.helpText = [
918 			"Note: Use the \"dependencies\" field in the package description file (e.g. dub.json) if you just want to use a certain package as a dependency, you don't have to explicitly fetch packages.",
919 			"",
920 			"Explicit retrieval/removal of packages is only needed when you want to put packages to a place where several applications can share these. If you just have an dependency to a package, just add it to your dub.json, dub will do the rest for you.",
921 			"",
922 			"Without specified options, placement/removal will default to a user wide shared location.",
923 			"",
924 			"Complete applications can be retrieved and run easily by e.g.",
925 			"$ dub fetch vibelog --local",
926 			"$ cd vibelog",
927 			"$ dub",
928 			"",
929 			"This will grab all needed dependencies and compile and run the application.",
930 			"",
931 			"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."
932 		];
933 	}
934 
935 	override void prepare(scope CommandArgs args)
936 	{
937 		super.prepare(args);
938 	}
939 
940 	override int execute(Dub dub, string[] free_args, string[] app_args)
941 	{
942 		enforceUsage(!m_local || !m_system, "--local and --system are exclusive to each other.");
943 		enforceUsage(free_args.length == 1, "Expecting exactly one argument.");
944 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
945 
946 		auto location = defaultPlacementLocation;
947 		if (m_local)
948 		{
949 			logWarn("--local is deprecated. Use --cache=local instead.");
950 			location = PlacementLocation.local;
951 		}
952 		else if (m_system)
953 		{
954 			logWarn("--system is deprecated. Use --cache=system instead.");
955 			location = PlacementLocation.system;
956 		}
957 
958 		auto name = free_args[0];
959 
960 		FetchOptions fetchOpts;
961 		fetchOpts |= FetchOptions.forceBranchUpgrade;
962 		fetchOpts |= m_forceRemove ? FetchOptions.forceRemove : FetchOptions.none;
963 		if (m_version.length) dub.fetch(name, Dependency(m_version), location, fetchOpts);
964 		else {
965 			try {
966 				dub.fetch(name, Dependency(">=0.0.0"), location, fetchOpts);
967 				logInfo(
968 					"Please note that you need to use `dub run <pkgname>` " ~
969 					"or add it to dependencies of your package to actually use/run it. " ~
970 					"dub does not do actual installation of packages outside of its own ecosystem.");
971 			}
972 			catch(Exception e){
973 				logInfo("Getting a release version failed: %s", e.msg);
974 				logInfo("Retry with ~master...");
975 				dub.fetch(name, Dependency("~master"), location, fetchOpts);
976 			}
977 		}
978 		return 0;
979 	}
980 }
981 
982 class InstallCommand : FetchCommand {
983 	this() { this.name = "install"; hidden = true; }
984 	override void prepare(scope CommandArgs args) { super.prepare(args); }
985 	override int execute(Dub dub, string[] free_args, string[] app_args)
986 	{
987 		warnRenamed("install", "fetch");
988 		return super.execute(dub, free_args, app_args);
989 	}
990 }
991 
992 class RemoveCommand : FetchRemoveCommand {
993 	this()
994 	{
995 		this.name = "remove";
996 		this.argumentsPattern = "<name>";
997 		this.description = "Removes a cached package";
998 		this.helpText = [
999 			"Removes a package that is cached on the local system."
1000 		];
1001 	}
1002 
1003 	override void prepare(scope CommandArgs args)
1004 	{
1005 		super.prepare(args);
1006 	}
1007 
1008 	override int execute(Dub dub, string[] free_args, string[] app_args)
1009 	{
1010 		enforceUsage(free_args.length == 1, "Expecting exactly one argument.");
1011 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1012 
1013 		auto package_id = free_args[0];
1014 		auto location = defaultPlacementLocation;
1015 		if (m_local)
1016 		{
1017 			logWarn("--local is deprecated. Use --cache=local instead.");
1018 			location = PlacementLocation.local;
1019 		}
1020 		else if (m_system)
1021 		{
1022 			logWarn("--system is deprecated. Use --cache=system instead.");
1023 			location = PlacementLocation.system;
1024 		}
1025 
1026 		dub.remove(package_id, m_version, location, m_forceRemove);
1027 		return 0;
1028 	}
1029 }
1030 
1031 class UninstallCommand : RemoveCommand {
1032 	this() { this.name = "uninstall"; hidden = true; }
1033 	override void prepare(scope CommandArgs args) { super.prepare(args); }
1034 	override int execute(Dub dub, string[] free_args, string[] app_args)
1035 	{
1036 		warnRenamed("uninstall", "remove");
1037 		return super.execute(dub, free_args, app_args);
1038 	}
1039 }
1040 
1041 
1042 /******************************************************************************/
1043 /* ADD/REMOVE PATH/LOCAL                                                      */
1044 /******************************************************************************/
1045 
1046 abstract class RegistrationCommand : Command {
1047 	private {
1048 		bool m_system;
1049 	}
1050 
1051 	override void prepare(scope CommandArgs args)
1052 	{
1053 		args.getopt("system", &m_system, [
1054 			"Register system-wide instead of user-wide"
1055 		]);
1056 	}
1057 
1058 	abstract override int execute(Dub dub, string[] free_args, string[] app_args);
1059 }
1060 
1061 class AddPathCommand : RegistrationCommand {
1062 	this()
1063 	{
1064 		this.name = "add-path";
1065 		this.argumentsPattern = "<path>";
1066 		this.description = "Adds a default package search path";
1067 		this.helpText = ["Adds a default package search path"];
1068 	}
1069 
1070 	override int execute(Dub dub, string[] free_args, string[] app_args)
1071 	{
1072 		enforceUsage(free_args.length == 1, "Missing search path.");
1073 		dub.addSearchPath(free_args[0], m_system);
1074 		return 0;
1075 	}
1076 }
1077 
1078 class RemovePathCommand : RegistrationCommand {
1079 	this()
1080 	{
1081 		this.name = "remove-path";
1082 		this.argumentsPattern = "<path>";
1083 		this.description = "Removes a package search path";
1084 		this.helpText = ["Removes a package search path"];
1085 	}
1086 
1087 	override int execute(Dub dub, string[] free_args, string[] app_args)
1088 	{
1089 		enforceUsage(free_args.length == 1, "Expected one argument.");
1090 		dub.removeSearchPath(free_args[0], m_system);
1091 		return 0;
1092 	}
1093 }
1094 
1095 class AddLocalCommand : RegistrationCommand {
1096 	this()
1097 	{
1098 		this.name = "add-local";
1099 		this.argumentsPattern = "<path> [<version>]";
1100 		this.description = "Adds a local package directory (e.g. a git repository)";
1101 		this.helpText = ["Adds a local package directory (e.g. a git repository)"];
1102 	}
1103 
1104 	override int execute(Dub dub, string[] free_args, string[] app_args)
1105 	{
1106 		enforceUsage(free_args.length == 1 || free_args.length == 2, "Expecting one or two arguments.");
1107 		string ver = free_args.length == 2 ? free_args[1] : null;
1108 		dub.addLocalPackage(free_args[0], ver, m_system);
1109 		return 0;
1110 	}
1111 }
1112 
1113 class RemoveLocalCommand : RegistrationCommand {
1114 	this()
1115 	{
1116 		this.name = "remove-local";
1117 		this.argumentsPattern = "<path>";
1118 		this.description = "Removes a local package directory";
1119 		this.helpText = ["Removes a local package directory"];
1120 	}
1121 
1122 	override int execute(Dub dub, string[] free_args, string[] app_args)
1123 	{
1124 		enforceUsage(free_args.length == 1, "Missing path to package.");
1125 		dub.removeLocalPackage(free_args[0], m_system);
1126 		return 0;
1127 	}
1128 }
1129 
1130 class ListCommand : Command {
1131 	this()
1132 	{
1133 		this.name = "list";
1134 		this.argumentsPattern = "";
1135 		this.description = "Prints a list of all local packages dub is aware of";
1136 		this.helpText = [
1137 			"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\")."
1138 		];
1139 	}
1140 	override void prepare(scope CommandArgs args) {}
1141 	override int execute(Dub dub, string[] free_args, string[] app_args)
1142 	{
1143 		logInfo("Packages present in the system and known to dub:");
1144 		foreach (p; dub.packageManager.getPackageIterator())
1145 			logInfo("  %s %s: %s", p.name, p.ver, p.path.toNativeString());
1146 		logInfo("");
1147 		return 0;
1148 	}
1149 }
1150 
1151 class ListInstalledCommand : ListCommand {
1152 	this() { this.name = "list-installed"; hidden = true; }
1153 	override void prepare(scope CommandArgs args) { super.prepare(args); }
1154 	override int execute(Dub dub, string[] free_args, string[] app_args)
1155 	{
1156 		warnRenamed("list-installed", "list");
1157 		return super.execute(dub, free_args, app_args);
1158 	}
1159 }
1160 
1161 
1162 /******************************************************************************/
1163 /* OVERRIDES                                                                  */
1164 /******************************************************************************/
1165 
1166 class AddOverrideCommand : Command {
1167 	private {
1168 		bool m_system = false;
1169 	}
1170 
1171 	this()
1172 	{
1173 		this.name = "add-override";
1174 		this.argumentsPattern = "<package> <version-spec> <target-path/target-version>";
1175 		this.description = "Adds a new package override.";
1176 		this.helpText = [
1177 		];
1178 	}
1179 
1180 	override void prepare(scope CommandArgs args)
1181 	{
1182 		args.getopt("system", &m_system, [
1183 			"Register system-wide instead of user-wide"
1184 		]);
1185 	}
1186 
1187 	override int execute(Dub dub, string[] free_args, string[] app_args)
1188 	{
1189 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1190 		enforceUsage(free_args.length == 3, "Expected three arguments, not "~free_args.length.to!string);
1191 		auto scope_ = m_system ? LocalPackageType.system : LocalPackageType.user;
1192 		auto pack = free_args[0];
1193 		auto ver = Dependency(free_args[1]);
1194 		if (existsFile(Path(free_args[2]))) {
1195 			auto target = Path(free_args[2]);
1196 			dub.packageManager.addOverride(scope_, pack, ver, target);
1197 			logInfo("Added override %s %s => %s", pack, ver, target);
1198 		} else {
1199 			auto target = Version(free_args[2]);
1200 			dub.packageManager.addOverride(scope_, pack, ver, target);
1201 			logInfo("Added override %s %s => %s", pack, ver, target);
1202 		}
1203 		return 0;
1204 	}
1205 }
1206 
1207 class RemoveOverrideCommand : Command {
1208 	private {
1209 		bool m_system = false;
1210 	}
1211 
1212 	this()
1213 	{
1214 		this.name = "remove-override";
1215 		this.argumentsPattern = "<package> <version-spec>";
1216 		this.description = "Removes an existing package override.";
1217 		this.helpText = [
1218 		];
1219 	}
1220 
1221 	override void prepare(scope CommandArgs args)
1222 	{
1223 		args.getopt("system", &m_system, [
1224 			"Register system-wide instead of user-wide"
1225 		]);
1226 	}
1227 
1228 	override int execute(Dub dub, string[] free_args, string[] app_args)
1229 	{
1230 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1231 		enforceUsage(free_args.length == 2, "Expected two arguments, not "~free_args.length.to!string);
1232 		auto scope_ = m_system ? LocalPackageType.system : LocalPackageType.user;
1233 		dub.packageManager.removeOverride(scope_, free_args[0], Dependency(free_args[1]));
1234 		return 0;
1235 	}
1236 }
1237 
1238 class ListOverridesCommand : Command {
1239 	this()
1240 	{
1241 		this.name = "list-overrides";
1242 		this.argumentsPattern = "";
1243 		this.description = "Prints a list of all local package overrides";
1244 		this.helpText = [
1245 			"Prints a list of all overriden packages added via \"dub add-override\"."
1246 		];
1247 	}
1248 	override void prepare(scope CommandArgs args) {}
1249 	override int execute(Dub dub, string[] free_args, string[] app_args)
1250 	{
1251 		void printList(in PackageOverride[] overrides, string caption)
1252 		{
1253 			if (overrides.length == 0) return;
1254 			logInfo("# %s", caption);
1255 			foreach (ovr; overrides) {
1256 				if (!ovr.targetPath.empty) logInfo("%s %s => %s", ovr.package_, ovr.version_, ovr.targetPath);
1257 				else logInfo("%s %s => %s", ovr.package_, ovr.version_, ovr.targetVersion);
1258 			}
1259 		}
1260 		printList(dub.packageManager.getOverrides(LocalPackageType.user), "User wide overrides");
1261 		printList(dub.packageManager.getOverrides(LocalPackageType.system), "System wide overrides");
1262 		return 0;
1263 	}
1264 }
1265 
1266 /******************************************************************************/
1267 /* Cache cleanup                                                              */
1268 /******************************************************************************/
1269 
1270 class CleanCachesCommand : Command {
1271 	this()
1272 	{
1273 		this.name = "clean-caches";
1274 		this.argumentsPattern = "";
1275 		this.description = "Removes cached metadata";
1276 		this.helpText = [
1277 			"This command removes any cached metadata like the list of available packages and their latest version."
1278 		];
1279 	}
1280 
1281 	override void prepare(scope CommandArgs args) {}
1282 
1283 	override int execute(Dub dub, string[] free_args, string[] app_args)
1284 	{
1285 		dub.cleanCaches();
1286 		return 0;
1287 	}
1288 }
1289 
1290 /******************************************************************************/
1291 /* DUSTMITE                                                                   */
1292 /******************************************************************************/
1293 
1294 class DustmiteCommand : PackageBuildCommand {
1295 	private {
1296 		int m_compilerStatusCode = int.min;
1297 		int m_linkerStatusCode = int.min;
1298 		int m_programStatusCode = int.min;
1299 		string m_compilerRegex;
1300 		string m_linkerRegex;
1301 		string m_programRegex;
1302 		string m_testPackage;
1303 		bool m_combined;
1304 	}
1305 
1306 	this()
1307 	{
1308 		this.name = "dustmite";
1309 		this.argumentsPattern = "<destination-path>";
1310 		this.acceptsAppArgs = true;
1311 		this.description = "Create reduced test cases for build errors";
1312 		this.helpText = [
1313 			"This command uses the Dustmite utility to isolate the cause of build errors in a DUB project.",
1314 			"",
1315 			"It will create a copy of all involved packages and run dustmite on this copy, leaving a reduced test case.",
1316 			"",
1317 			"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."
1318 		];
1319 	}
1320 
1321 	override void prepare(scope CommandArgs args)
1322 	{
1323 		args.getopt("compiler-status", &m_compilerStatusCode, ["The expected status code of the compiler run"]);
1324 		args.getopt("compiler-regex", &m_compilerRegex, ["A regular expression used to match against the compiler output"]);
1325 		args.getopt("linker-status", &m_linkerStatusCode, ["The expected status code of the liner run"]);
1326 		args.getopt("linker-regex", &m_linkerRegex, ["A regular expression used to match against the linker output"]);
1327 		args.getopt("program-status", &m_programStatusCode, ["The expected status code of the built executable"]);
1328 		args.getopt("program-regex", &m_programRegex, ["A regular expression used to match against the program output"]);
1329 		args.getopt("test-package", &m_testPackage, ["Perform a test run - usually only used internally"]);
1330 		args.getopt("combined", &m_combined, ["Builds multiple packages with one compiler run"]);
1331 		super.prepare(args);
1332 
1333 		// speed up loading when in test mode
1334 		if (m_testPackage.length) {
1335 			skipDubInitialization = true;
1336 			m_nodeps = true;
1337 		}
1338 	}
1339 
1340 	override int execute(Dub dub, string[] free_args, string[] app_args)
1341 	{
1342 		if (m_testPackage.length) {
1343 			dub = new Dub(Path(getcwd()));
1344 
1345 			setupPackage(dub, m_testPackage);
1346 			m_defaultConfig = dub.project.getDefaultConfiguration(m_buildPlatform);
1347 
1348 			GeneratorSettings gensettings;
1349 			gensettings.platform = m_buildPlatform;
1350 			gensettings.config = m_buildConfig.length ? m_buildConfig : m_defaultConfig;
1351 			gensettings.buildType = m_buildType;
1352 			gensettings.compiler = m_compiler;
1353 			gensettings.buildSettings = m_buildSettings;
1354 			gensettings.combined = m_combined;
1355 			gensettings.run = m_programStatusCode != int.min || m_programRegex.length;
1356 			gensettings.runArgs = app_args;
1357 			gensettings.force = true;
1358 			gensettings.compileCallback = check(m_compilerStatusCode, m_compilerRegex);
1359 			gensettings.linkCallback = check(m_linkerStatusCode, m_linkerRegex);
1360 			gensettings.runCallback = check(m_programStatusCode, m_programRegex);
1361 			try dub.generateProject("build", gensettings);
1362 			catch (DustmiteMismatchException) {
1363 				logInfo("Dustmite test doesn't match.");
1364 				return 3;
1365 			}
1366 			catch (DustmiteMatchException) {
1367 				logInfo("Dustmite test matches.");
1368 				return 0;
1369 			}
1370 		} else {
1371 			enforceUsage(free_args.length == 1, "Expected destination path.");
1372 			auto path = Path(free_args[0]);
1373 			path.normalize();
1374 			enforceUsage(path.length > 0, "Destination path must not be empty.");
1375 			if (!path.absolute) path = Path(getcwd()) ~ path;
1376 			enforceUsage(!path.startsWith(dub.rootPath), "Destination path must not be a sub directory of the tested package!");
1377 
1378 			setupPackage(dub, null);
1379 			auto prj = dub.project;
1380 			if (m_buildConfig.empty)
1381 				m_buildConfig = prj.getDefaultConfiguration(m_buildPlatform);
1382 
1383 			void copyFolderRec(Path folder, Path dstfolder)
1384 			{
1385 				mkdirRecurse(dstfolder.toNativeString());
1386 				foreach (de; iterateDirectory(folder.toNativeString())) {
1387 					if (de.name.startsWith(".")) continue;
1388 					if (de.isDirectory) {
1389 						copyFolderRec(folder ~ de.name, dstfolder ~ de.name);
1390 					} else {
1391 						if (de.name.endsWith(".o") || de.name.endsWith(".obj")) continue;
1392 						if (de.name.endsWith(".exe")) continue;
1393 						try copyFile(folder ~ de.name, dstfolder ~ de.name);
1394 						catch (Exception e) {
1395 							logWarn("Failed to copy file %s: %s", (folder ~ de.name).toNativeString(), e.msg);
1396 						}
1397 					}
1398 				}
1399 			}
1400 
1401 			bool[string] visited;
1402 			foreach (pack_; prj.getTopologicalPackageList()) {
1403 				auto pack = pack_.basePackage;
1404 				if (pack.name in visited) continue;
1405 				visited[pack.name] = true;
1406 				logInfo("Copy package '%s' to destination folder...", pack.name);
1407 				copyFolderRec(pack.path, path ~ pack.name);
1408 			}
1409 			logInfo("Executing dustmite...");
1410 			auto testcmd = format("dub dustmite --vquiet --test-package=%s", prj.name);
1411 			if (m_compilerStatusCode != int.min) testcmd ~= format(" --compiler-status=%s", m_compilerStatusCode);
1412 			if (m_compilerRegex.length) testcmd ~= format(" \"--compiler-regex=%s\"", m_compilerRegex);
1413 			if (m_linkerStatusCode != int.min) testcmd ~= format(" --linker-status=%s", m_linkerStatusCode);
1414 			if (m_linkerRegex.length) testcmd ~= format(" \"--linker-regex=%s\"", m_linkerRegex);
1415 			if (m_programStatusCode != int.min) testcmd ~= format(" --program-status=%s", m_programStatusCode);
1416 			if (m_programRegex.length) testcmd ~= format(" \"--program-regex=%s\"", m_programRegex);
1417 			if (m_combined) testcmd ~= " --combined";
1418 			// TODO: pass *all* original parameters
1419 			logDiagnostic("Running dustmite: %s", testcmd);
1420 			auto dmpid = spawnProcess(["dustmite", path.toNativeString(), testcmd]);
1421 			return dmpid.wait();
1422 		}
1423 		return 0;
1424 	}
1425 
1426 	void delegate(int, string) check(int code_match, string regex_match)
1427 	{
1428 		return (code, output) {
1429 			import std.encoding;
1430 			import std.regex;
1431 
1432 			logInfo("%s", output);
1433 
1434 			if (code_match != int.min && code != code_match) {
1435 				logInfo("Exit code %s doesn't match expected value %s", code, code_match);
1436 				throw new DustmiteMismatchException;
1437 			}
1438 
1439 			if (regex_match.length > 0 && !match(output.sanitize, regex_match)) {
1440 				logInfo("Output doesn't match regex:");
1441 				logInfo("%s", output);
1442 				throw new DustmiteMismatchException;
1443 			}
1444 
1445 			if (code != 0 && code_match != int.min || regex_match.length > 0) {
1446 				logInfo("Tool failed, but matched either exit code or output - counting as match.");
1447 				throw new DustmiteMatchException;
1448 			}
1449 		};
1450 	}
1451 
1452 	static class DustmiteMismatchException : Exception {
1453 		this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null)
1454 		{
1455 			super(message, file, line, next);
1456 		}
1457 	}
1458 
1459 	static class DustmiteMatchException : Exception {
1460 		this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null)
1461 		{
1462 			super(message, file, line, next);
1463 		}
1464 	}
1465 }
1466 
1467 
1468 /******************************************************************************/
1469 /* HELP                                                                       */
1470 /******************************************************************************/
1471 
1472 private {
1473 	enum shortArgColumn = 2;
1474 	enum longArgColumn = 6;
1475 	enum descColumn = 24;
1476 	enum lineWidth = 80 - 1;
1477 }
1478 
1479 private void showHelp(in CommandGroup[] commands, CommandArgs common_args)
1480 {
1481 	writeln(
1482 `USAGE: dub [--version] [<command>] [<options...>] [-- [<application arguments...>]]
1483 
1484 Manages the DUB project in the current directory. If the command is omitted,
1485 DUB will default to "run". When running an application, "--" can be used to
1486 separate DUB options from options passed to the application.
1487 
1488 Run "dub <command> --help" to get help for a specific command.
1489 
1490 You can use the "http_proxy" environment variable to configure a proxy server
1491 to be used for fetching packages.
1492 
1493 
1494 Available commands
1495 ==================`);
1496 
1497 	foreach (grp; commands) {
1498 		writeln();
1499 		writeWS(shortArgColumn);
1500 		writeln(grp.caption);
1501 		writeWS(shortArgColumn);
1502 		writerep!'-'(grp.caption.length);
1503 		writeln();
1504 		foreach (cmd; grp.commands) {
1505 			if (cmd.hidden) continue;
1506 			writeWS(shortArgColumn);
1507 			writef("%s %s", cmd.name, cmd.argumentsPattern);
1508 			auto chars_output = cmd.name.length + cmd.argumentsPattern.length + shortArgColumn + 1;
1509 			if (chars_output < descColumn) {
1510 				writeWS(descColumn - chars_output);
1511 			} else {
1512 				writeln();
1513 				writeWS(descColumn);
1514 			}
1515 			writeWrapped(cmd.description, descColumn, descColumn);
1516 		}
1517 	}
1518 	writeln();
1519 	writeln();
1520 	writeln(`Common options`);
1521 	writeln(`==============`);
1522 	writeln();
1523 	writeOptions(common_args);
1524 	writeln();
1525 	showVersion();
1526 }
1527 
1528 private void showVersion()
1529 {
1530 	writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__);
1531 }
1532 
1533 private void showCommandHelp(Command cmd, CommandArgs args, CommandArgs common_args)
1534 {
1535 	writefln(`USAGE: dub %s %s [<options...>]%s`, cmd.name, cmd.argumentsPattern, cmd.acceptsAppArgs ? " [-- <application arguments...>]": null);
1536 	writeln();
1537 	foreach (ln; cmd.helpText)
1538 		ln.writeWrapped();
1539 
1540 	if (args.recognizedArgs.length) {
1541 		writeln();
1542 		writeln();
1543 		writeln("Command specific options");
1544 		writeln("========================");
1545 		writeln();
1546 		writeOptions(args);
1547 	}
1548 
1549 	writeln();
1550 	writeln();
1551 	writeln("Common options");
1552 	writeln("==============");
1553 	writeln();
1554 	writeOptions(common_args);
1555 	writeln();
1556 	writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__);
1557 }
1558 
1559 private void writeOptions(CommandArgs args)
1560 {
1561 	foreach (arg; args.recognizedArgs) {
1562 		auto names = arg.names.split("|");
1563 		assert(names.length == 1 || names.length == 2);
1564 		string sarg = names[0].length == 1 ? names[0] : null;
1565 		string larg = names[0].length > 1 ? names[0] : names.length > 1 ? names[1] : null;
1566 		if (sarg) {
1567 			writeWS(shortArgColumn);
1568 			writef("-%s", sarg);
1569 			writeWS(longArgColumn - shortArgColumn - 2);
1570 		} else writeWS(longArgColumn);
1571 		size_t col = longArgColumn;
1572 		if (larg) {
1573 			if (arg.defaultValue.peek!bool) {
1574 				writef("--%s", larg);
1575 				col += larg.length + 2;
1576 			} else {
1577 				writef("--%s=VALUE", larg);
1578 				col += larg.length + 8;
1579 			}
1580 		}
1581 		if (col < descColumn) {
1582 			writeWS(descColumn - col);
1583 		} else {
1584 			writeln();
1585 			writeWS(descColumn);
1586 		}
1587 		foreach (i, ln; arg.helpText) {
1588 			if (i > 0) writeWS(descColumn);
1589 			ln.writeWrapped(descColumn, descColumn);
1590 		}
1591 	}
1592 }
1593 
1594 private void writeWrapped(string string, size_t indent = 0, size_t first_line_pos = 0)
1595 {
1596 	auto wrapped = string.wrap(lineWidth, getRepString!' '(first_line_pos), getRepString!' '(indent));
1597 	wrapped = wrapped[first_line_pos .. $];
1598 	foreach (ln; wrapped.splitLines())
1599 		writeln(ln);
1600 }
1601 
1602 private void writeWS(size_t num) { writerep!' '(num); }
1603 private void writerep(char ch)(size_t num) { write(getRepString!ch(num)); }
1604 
1605 private string getRepString(char ch)(size_t len)
1606 {
1607 	static string buf;
1608 	if (len > buf.length) buf ~= [ch].replicate(len-buf.length);
1609 	return buf[0 .. len];
1610 }
1611 
1612 /***
1613 */
1614 
1615 
1616 private void enforceUsage(bool cond, string text)
1617 {
1618 	if (!cond) throw new UsageException(text);
1619 }
1620 
1621 private class UsageException : Exception {
1622 	this(string message, string file = __FILE__, int line = __LINE__, Throwable next = null)
1623 	{
1624 		super(message, file, line, next);
1625 	}
1626 }
1627 
1628 private void warnRenamed(string prev, string curr)
1629 {
1630 	logWarn("The '%s' Command was renamed to '%s'. Please update your scripts.", prev, curr);
1631 }