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