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