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.data.json;
16 import dub.internal.vibecompat.inet.path;
17 import dub.internal.logging;
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 dub.internal.dyaml.stdsumtype;
25 
26 import std.algorithm;
27 import std.array;
28 import std.conv;
29 import std.encoding;
30 import std.exception;
31 import std.file;
32 import std.getopt;
33 import std.path : absolutePath, buildNormalizedPath, expandTilde, setExtension;
34 import std.process : environment, spawnProcess, wait;
35 import std.stdio;
36 import std.string;
37 import std.typecons : Tuple, tuple;
38 
39 /** Retrieves a list of all available commands.
40 
41 	Commands are grouped by category.
42 */
43 CommandGroup[] getCommands() @safe pure nothrow
44 {
45 	return [
46 		CommandGroup("Package creation",
47 			new InitCommand
48 		),
49 		CommandGroup("Build, test and run",
50 			new RunCommand,
51 			new BuildCommand,
52 			new TestCommand,
53 			new LintCommand,
54 			new GenerateCommand,
55 			new DescribeCommand,
56 			new CleanCommand,
57 			new DustmiteCommand
58 		),
59 		CommandGroup("Package management",
60 			new FetchCommand,
61 			new AddCommand,
62 			new RemoveCommand,
63 			new UpgradeCommand,
64 			new AddPathCommand,
65 			new RemovePathCommand,
66 			new AddLocalCommand,
67 			new RemoveLocalCommand,
68 			new ListCommand,
69 			new SearchCommand,
70 			new AddOverrideCommand,
71 			new RemoveOverrideCommand,
72 			new ListOverridesCommand,
73 			new CleanCachesCommand,
74 			new ConvertCommand,
75 		)
76 	];
77 }
78 
79 /** Extract the command name from the argument list
80 
81 	Params:
82 		args = a list of string arguments that will be processed
83 
84 	Returns:
85 		A structure with two members. `value` is the command name
86 		`remaining` is a list of unprocessed arguments
87 */
88 auto extractCommandNameArgument(string[] args)
89 {
90 	struct Result {
91 		string value;
92 		string[] remaining;
93 	}
94 
95 	if (args.length >= 1 && !args[0].startsWith("-") && !args[0].canFind(":")) {
96 		return Result(args[0], args[1 .. $]);
97 	}
98 
99 	return Result(null, args);
100 }
101 
102 /// test extractCommandNameArgument usage
103 unittest {
104 	/// It returns an empty string on when there are no args
105 	assert(extractCommandNameArgument([]).value == "");
106 	assert(extractCommandNameArgument([]).remaining == []);
107 
108 	/// It returns the first argument when it does not start with `-`
109 	assert(extractCommandNameArgument(["test"]).value == "test");
110 
111 	/// There is nothing to extract when the arguments only contain the `test` cmd
112 	assert(extractCommandNameArgument(["test"]).remaining == []);
113 
114 	/// It extracts two arguments when they are not a command
115 	assert(extractCommandNameArgument(["-a", "-b"]).remaining == ["-a", "-b"]);
116 
117 	/// It returns the an empty string when it starts with `-`
118 	assert(extractCommandNameArgument(["-test"]).value == "");
119 
120 	// Sub package names are ignored as command names
121 	assert(extractCommandNameArgument(["foo:bar"]).value == "");
122 	assert(extractCommandNameArgument([":foo"]).value == "");
123 }
124 
125 /** Handles the Command Line options and commands.
126 */
127 struct CommandLineHandler
128 {
129 	/// The list of commands that can be handled
130 	CommandGroup[] commandGroups;
131 
132 	/// General options parser
133 	CommonOptions options;
134 
135 	/** Create the list of all supported commands
136 
137 	Returns:
138 		Returns the list of the supported command names
139 	*/
140 	string[] commandNames()
141 	{
142 		return commandGroups.map!(g => g.commands.map!(c => c.name).array).join;
143 	}
144 
145 	/** Parses the general options and sets up the log level
146 		and the root_path
147 	*/
148 	void prepareOptions(CommandArgs args) {
149 		LogLevel loglevel = LogLevel.info;
150 
151 		options.prepare(args);
152 
153 		if (options.vverbose) loglevel = LogLevel.debug_;
154 		else if (options.verbose) loglevel = LogLevel.diagnostic;
155 		else if (options.vquiet) loglevel = LogLevel.none;
156 		else if (options.quiet) loglevel = LogLevel.warn;
157 		else if (options.verror) loglevel = LogLevel.error;
158 		setLogLevel(loglevel);
159 
160 		if (options.root_path.empty)
161 		{
162 			options.root_path = getcwd();
163 		}
164 		else
165 		{
166 			options.root_path = options.root_path.expandTilde.absolutePath.buildNormalizedPath;
167 		}
168 
169 		final switch (options.colorMode) with (options.Color)
170 		{
171 			case automatic:
172 				// Use default determined in internal.logging.initLogging().
173 				break;
174 			case on:
175 				foreach (ref grp; commandGroups)
176 					foreach (ref cmd; grp.commands)
177 						if (auto pc = cast(PackageBuildCommand)cmd)
178 							pc.baseSettings.buildSettings.options |= BuildOption.color;
179 				setLoggingColorsEnabled(true);  // enable colors, no matter what
180 				break;
181 			case off:
182 				foreach (ref grp; commandGroups)
183 					foreach (ref cmd; grp.commands)
184 						if (auto pc = cast(PackageBuildCommand)cmd)
185 							pc.baseSettings.buildSettings.options &= ~BuildOption.color;
186 				setLoggingColorsEnabled(false); // disable colors, no matter what
187 				break;
188 		}
189 	}
190 
191 	/** Get an instance of the requested command.
192 
193 	If there is no command in the argument list, the `run` command is returned
194 	by default.
195 
196 	If the `--help` argument previously handled by `prepareOptions`,
197 	`this.options.help` is already `true`, with this returning the requested
198 	command. If no command was requested (just dub --help) this returns the
199 	help command.
200 
201 	Params:
202 		name = the command name
203 
204 	Returns:
205 		Returns the command instance if it exists, null otherwise
206 	*/
207 	Command getCommand(string name) {
208 		if (name == "help" || (name == "" && options.help))
209 		{
210 			return new HelpCommand();
211 		}
212 
213 		if (name == "")
214 		{
215 			name = "run";
216 		}
217 
218 		foreach (grp; commandGroups)
219 			foreach (c; grp.commands)
220 				if (c.name == name) {
221 					return c;
222 				}
223 
224 		return null;
225 	}
226 
227 	/** Get an instance of the requested command after the args are sent.
228 
229 	It uses getCommand to get the command instance and then calls prepare.
230 
231 	Params:
232 		name = the command name
233 		args = the command arguments
234 
235 	Returns:
236 		Returns the command instance if it exists, null otherwise
237 	*/
238 	Command prepareCommand(string name, CommandArgs args) {
239 		auto cmd = getCommand(name);
240 
241 		if (cmd !is null && !(cast(HelpCommand)cmd))
242 		{
243 			// process command line options for the selected command
244 			cmd.prepare(args);
245 			enforceUsage(cmd.acceptsAppArgs || !args.hasAppArgs, name ~ " doesn't accept application arguments.");
246 		}
247 
248 		return cmd;
249 	}
250 }
251 
252 /// Can get the command names
253 unittest {
254 	CommandLineHandler handler;
255 	handler.commandGroups = getCommands();
256 
257 	assert(handler.commandNames == ["init", "run", "build", "test", "lint", "generate",
258 		"describe", "clean", "dustmite", "fetch", "add", "remove",
259 		"upgrade", "add-path", "remove-path", "add-local", "remove-local", "list", "search",
260 		"add-override", "remove-override", "list-overrides", "clean-caches", "convert"]);
261 }
262 
263 /// It sets the cwd as root_path by default
264 unittest {
265 	CommandLineHandler handler;
266 
267 	auto args = new CommandArgs([]);
268 	handler.prepareOptions(args);
269 	assert(handler.options.root_path == getcwd());
270 }
271 
272 /// It can set a custom root_path
273 unittest {
274 	CommandLineHandler handler;
275 
276 	auto args = new CommandArgs(["--root=/tmp/test"]);
277 	handler.prepareOptions(args);
278 	assert(handler.options.root_path == "/tmp/test".absolutePath.buildNormalizedPath);
279 
280 	args = new CommandArgs(["--root=./test"]);
281 	handler.prepareOptions(args);
282 	assert(handler.options.root_path == "./test".absolutePath.buildNormalizedPath);
283 }
284 
285 /// It sets the info log level by default
286 unittest {
287 	scope(exit) setLogLevel(LogLevel.info);
288 	CommandLineHandler handler;
289 
290 	auto args = new CommandArgs([]);
291 	handler.prepareOptions(args);
292 	assert(getLogLevel() == LogLevel.info);
293 }
294 
295 /// It can set a custom error level
296 unittest {
297 	scope(exit) setLogLevel(LogLevel.info);
298 	CommandLineHandler handler;
299 
300 	auto args = new CommandArgs(["--vverbose"]);
301 	handler.prepareOptions(args);
302 	assert(getLogLevel() == LogLevel.debug_);
303 
304 	handler = CommandLineHandler();
305 	args = new CommandArgs(["--verbose"]);
306 	handler.prepareOptions(args);
307 	assert(getLogLevel() == LogLevel.diagnostic);
308 
309 	handler = CommandLineHandler();
310 	args = new CommandArgs(["--vquiet"]);
311 	handler.prepareOptions(args);
312 	assert(getLogLevel() == LogLevel.none);
313 
314 	handler = CommandLineHandler();
315 	args = new CommandArgs(["--quiet"]);
316 	handler.prepareOptions(args);
317 	assert(getLogLevel() == LogLevel.warn);
318 
319 	handler = CommandLineHandler();
320 	args = new CommandArgs(["--verror"]);
321 	handler.prepareOptions(args);
322 	assert(getLogLevel() == LogLevel.error);
323 }
324 
325 /// It returns the `run` command by default
326 unittest {
327 	CommandLineHandler handler;
328 	handler.commandGroups = getCommands();
329 	assert(handler.getCommand("").name == "run");
330 }
331 
332 /// It returns the `help` command when there is none set and the --help arg
333 /// was set
334 unittest {
335 	CommandLineHandler handler;
336 	auto args = new CommandArgs(["--help"]);
337 	handler.prepareOptions(args);
338 	handler.commandGroups = getCommands();
339 	assert(cast(HelpCommand)handler.getCommand("") !is null);
340 }
341 
342 /// It returns the `help` command when the `help` command is sent
343 unittest {
344 	CommandLineHandler handler;
345 	handler.commandGroups = getCommands();
346 	assert(cast(HelpCommand) handler.getCommand("help") !is null);
347 }
348 
349 /// It returns the `init` command when the `init` command is sent
350 unittest {
351 	CommandLineHandler handler;
352 	handler.commandGroups = getCommands();
353 	assert(handler.getCommand("init").name == "init");
354 }
355 
356 /// It returns null when a missing command is sent
357 unittest {
358 	CommandLineHandler handler;
359 	handler.commandGroups = getCommands();
360 	assert(handler.getCommand("missing") is null);
361 }
362 
363 /** Processes the given command line and executes the appropriate actions.
364 
365 	Params:
366 		args = This command line argument array as received in `main`. The first
367 			entry is considered to be the name of the binary invoked.
368 
369 	Returns:
370 		Returns the exit code that is supposed to be returned to the system.
371 */
372 int runDubCommandLine(string[] args)
373 {
374 	static string[] toSinglePackageArgs (string args0, string file, string[] trailing)
375 	{
376 		return [args0, "run", "-q", "--temp-build", "--single", file, "--"] ~ trailing;
377 	}
378 
379 	// Initialize the logging module, ensure that whether stdout/stderr are a TTY
380 	// or not is detected in order to disable colors if the output isn't a console
381 	initLogging();
382 
383 	logDiagnostic("DUB version %s", getDUBVersion());
384 
385 	version(Windows){
386 		// rdmd uses $TEMP to compute a temporary path. since cygwin substitutes backslashes
387 		// with slashes, this causes OPTLINK to fail (it thinks path segments are options)
388 		// we substitute the other way around here to fix this.
389 		environment["TEMP"] = environment["TEMP"].replace("/", "\\");
390 	}
391 
392 	auto handler = CommandLineHandler(getCommands());
393 	auto commandNames = handler.commandNames();
394 
395 	// Special syntaxes need to be handled before regular argument parsing
396 	if (args.length >= 2)
397 	{
398 		// Read input source code from stdin
399 		if (args[1] == "-")
400 		{
401 			auto path = getTempFile("app", ".d");
402 			stdin.byChunk(4096).joiner.toFile(path.toNativeString());
403 			args = toSinglePackageArgs(args[0], path.toNativeString(), args[2 .. $]);
404 		}
405 
406 		// Dub has a shebang syntax to be able to use it as script, e.g.
407 		// #/usr/bin/env dub
408 		// With this approach, we need to support the file having
409 		// both the `.d` extension, or having none at all.
410 		// We also need to make sure arguments passed to the script
411 		// are passed to the program, not `dub`, e.g.:
412 		// ./my_dub_script foo bar
413 		// Gives us `args = [ "dub", "./my_dub_script" "foo", "bar" ]`,
414 		// which we need to interpret as:
415 		// `args = [ "dub", "./my_dub_script", "--", "foo", "bar" ]`
416 		else if (args[1].endsWith(".d"))
417 			args = toSinglePackageArgs(args[0], args[1], args[2 .. $]);
418 
419 		// Here we have a problem: What if the script name is a command name ?
420 		// We have to assume it isn't, and to reduce the risk of false positive
421 		// we only consider the case where the file name is the first argument,
422 		// as the shell invocation cannot be controlled.
423 		else if (!commandNames.canFind(args[1]) && !args[1].startsWith("-")) {
424 			if (exists(args[1])) {
425 				auto path = getTempFile("app", ".d");
426 				copy(args[1], path.toNativeString());
427 				args = toSinglePackageArgs(args[0], path.toNativeString(), args[2 .. $]);
428 			} else if (exists(args[1].setExtension(".d"))) {
429 				args = toSinglePackageArgs(args[0], args[1].setExtension(".d"), args[2 .. $]);
430 			}
431 		}
432 	}
433 
434 	auto common_args = new CommandArgs(args[1..$]);
435 
436 	try handler.prepareOptions(common_args);
437 	catch (Exception e) {
438 		logError("Error processing arguments: %s", e.msg);
439 		logDiagnostic("Full exception: %s", e.toString().sanitize);
440 		logInfo("Run 'dub help' for usage information.");
441 		return 1;
442 	}
443 
444 	if (handler.options.version_)
445 	{
446 		showVersion();
447 		return 0;
448 	}
449 
450 	// extract the command
451 	args = common_args.extractAllRemainingArgs();
452 
453 	auto command_name_argument = extractCommandNameArgument(args);
454 
455 	auto command_args = new CommandArgs(command_name_argument.remaining);
456 	Command cmd;
457 
458 	try {
459 		cmd = handler.prepareCommand(command_name_argument.value, command_args);
460 	} catch (Exception e) {
461 		logError("Error processing arguments: %s", e.msg);
462 		logDiagnostic("Full exception: %s", e.toString().sanitize);
463 		logInfo("Run 'dub help' for usage information.");
464 		return 1;
465 	}
466 
467 	if (cmd is null) {
468 		logInfoNoTag("USAGE: dub [--version] [<command>] [<options...>] [-- [<application arguments...>]]");
469 		logInfoNoTag("");
470 		logError("Unknown command: %s", command_name_argument.value);
471 		import std.algorithm.iteration : filter;
472 		import std.uni : toUpper;
473 		foreach (CommandGroup key; handler.commandGroups)
474 		{
475 			foreach (Command command; key.commands)
476 			{
477 				if (levenshteinDistance(command_name_argument.value, command.name) < 4) {
478 					logInfo("Did you mean '%s'?", command.name);
479 				}
480 			}
481 		}
482 
483 		logInfoNoTag("");
484 		return 1;
485 	}
486 
487 	if (cast(HelpCommand)cmd !is null) {
488 		showHelp(handler.commandGroups, common_args);
489 		return 0;
490 	}
491 
492 	if (handler.options.help) {
493 		showCommandHelp(cmd, command_args, common_args);
494 		return 0;
495 	}
496 
497 	auto remaining_args = command_args.extractRemainingArgs();
498 	if (remaining_args.any!(a => a.startsWith("-"))) {
499 		logError("Unknown command line flags: %s", remaining_args.filter!(a => a.startsWith("-")).array.join(" ").color(Mode.bold));
500 		logInfo(`Type "%s" to get a list of all supported flags.`, text("dub ", cmd.name, " -h").color(Mode.bold));
501 		return 1;
502 	}
503 
504 	// initialize the root package
505 	Dub dub = cmd.prepareDub(handler.options);
506 
507 	// execute the command
508 	try return cmd.execute(dub, remaining_args, command_args.appArgs);
509 	catch (UsageException e) {
510 		// usage exceptions get thrown before any logging, so we are
511 		// making the errors more narrow to better fit on small screens.
512 		tagWidth.push(5);
513 		logError("%s", e.msg);
514 		logDebug("Full exception: %s", e.toString().sanitize);
515 		logInfo(`Run "%s" for more information about the "%s" command.`,
516 			text("dub ", cmd.name, " -h").color(Mode.bold), cmd.name.color(Mode.bold));
517 		return 1;
518 	}
519 	catch (Exception e) {
520 		// most exceptions get thrown before logging, so same thing here as
521 		// above. However this might be subject to change if it results in
522 		// weird behavior anywhere.
523 		tagWidth.push(5);
524 		logError("%s", e.msg);
525 		logDebug("Full exception: %s", e.toString().sanitize);
526 		return 2;
527 	}
528 }
529 
530 
531 /** Contains and parses options common to all commands.
532 */
533 struct CommonOptions {
534 	bool verbose, vverbose, quiet, vquiet, verror, version_;
535 	bool help, annotate, bare;
536 	string[] registry_urls;
537 	string root_path;
538 	enum Color { automatic, on, off }
539 	Color colorMode = Color.automatic;
540 	SkipPackageSuppliers skipRegistry = SkipPackageSuppliers.none;
541 	PlacementLocation placementLocation = PlacementLocation.user;
542 
543 	deprecated("Use `Color` instead, the previous naming was a limitation of error message formatting")
544 	alias color = Color;
545 	deprecated("Use `colorMode` instead")
546 	alias color_mode = colorMode;
547 
548 	private void parseColor(string option, string value) @safe
549 	{
550 		// `automatic`, `on`, `off` are there for backwards compatibility
551 		// `auto`, `always`, `never` is being used for compatibility with most
552 		// other development and linux tools, after evaluating what other tools
553 		// are doing, to help users intuitively pick correct values.
554 		// See https://github.com/dlang/dub/issues/2410 for discussion
555 		if (!value.length || value == "auto" || value == "automatic")
556 			colorMode = Color.automatic;
557 		else if (value == "always" || value == "on")
558 			colorMode = Color.on;
559 		else if (value == "never" || value == "off")
560 			colorMode = Color.off;
561 		else
562 			throw new ConvException("Unable to parse argument '--" ~ option ~ "=" ~ value
563 				~ "', supported values: --color[=auto], --color=always, --color=never");
564 	}
565 
566 	/// Parses all common options and stores the result in the struct instance.
567 	void prepare(CommandArgs args)
568 	{
569 		args.getopt("h|help", &help, ["Display general or command specific help"]);
570 		args.getopt("root", &root_path, ["Path to operate in instead of the current working dir"]);
571 		args.getopt("registry", &registry_urls, [
572 			"Search the given registry URL first when resolving dependencies. Can be specified multiple times. Available registry types:",
573 			"  DUB: URL to DUB registry (default)",
574 			"  Maven: URL to Maven repository + group id containing dub packages as artifacts. E.g. mvn+http://localhost:8040/maven/libs-release/dubpackages",
575 			]);
576 		args.getopt("skip-registry", &skipRegistry, [
577 			"Sets a mode for skipping the search on certain package registry types:",
578 			"  none: Search all configured or default registries (default)",
579 			"  standard: Don't search the main registry (e.g. "~defaultRegistryURLs[0]~")",
580 			"  configured: Skip all default and user configured registries",
581 			"  all: Only search registries specified with --registry",
582 			]);
583 		args.getopt("annotate", &annotate, ["Do not perform any action, just print what would be done"]);
584 		args.getopt("bare", &bare, ["Read only packages contained in the current directory"]);
585 		args.getopt("v|verbose", &verbose, ["Print diagnostic output"]);
586 		args.getopt("vverbose", &vverbose, ["Print debug output"]);
587 		args.getopt("q|quiet", &quiet, ["Only print warnings and errors"]);
588 		args.getopt("verror", &verror, ["Only print errors"]);
589 		args.getopt("vquiet", &vquiet, ["Print no messages"]);
590 		args.getopt("color", &colorMode, &parseColor, [
591 			"Configure colored output. Accepted values:",
592 			"       auto: Colored output on console/terminal,",
593 			"             unless NO_COLOR is set and non-empty (default)",
594 			"     always: Force colors enabled",
595 			"      never: Force colors disabled"
596 			]);
597 		args.getopt("cache", &placementLocation, ["Puts any fetched packages in the specified location [local|system|user]."]);
598 
599 		version_ = args.hasAppVersion;
600 	}
601 }
602 
603 /** Encapsulates a set of application arguments.
604 
605 	This class serves two purposes. The first is to provide an API for parsing
606 	command line arguments (`getopt`). At the same time it records all calls
607 	to `getopt` and provides a list of all possible options using the
608 	`recognizedArgs` property.
609 */
610 class CommandArgs {
611 	struct Arg {
612 		alias Value = SumType!(string[], string, bool, int, uint);
613 
614 		Value defaultValue;
615 		Value value;
616 		string names;
617 		string[] helpText;
618 		bool hidden;
619 	}
620 	private {
621 		string[] m_args;
622 		Arg[] m_recognizedArgs;
623 		string[] m_appArgs;
624 	}
625 
626 	/** Initializes the list of source arguments.
627 
628 		Note that all array entries are considered application arguments (i.e.
629 		no application name entry is present as the first entry)
630 	*/
631 	this(string[] args) @safe pure nothrow
632 	{
633 		auto app_args_idx = args.countUntil("--");
634 
635 		m_appArgs = app_args_idx >= 0 ? args[app_args_idx+1 .. $] : [];
636 		m_args = "dummy" ~ (app_args_idx >= 0 ? args[0..app_args_idx] : args);
637 	}
638 
639 	/** Checks if the app arguments are present.
640 
641 	Returns:
642 		true if an -- argument is given with arguments after it, otherwise false
643 	*/
644 	@property bool hasAppArgs() { return m_appArgs.length > 0; }
645 
646 
647 	/** Checks if the `--version` argument is present on the first position in
648 	the list.
649 
650 	Returns:
651 		true if the application version argument was found on the first position
652 	*/
653 	@property bool hasAppVersion() { return m_args.length > 1 && m_args[1] == "--version"; }
654 
655 	/** Returns the list of app args.
656 
657 		The app args are provided after the `--` argument.
658 	*/
659 	@property string[] appArgs() { return m_appArgs; }
660 
661 	/** Returns the list of all options recognized.
662 
663 		This list is created by recording all calls to `getopt`.
664 	*/
665 	@property const(Arg)[] recognizedArgs() { return m_recognizedArgs; }
666 
667 	void getopt(T)(string names, T* var, string[] help_text = null, bool hidden=false)
668 	{
669 		getopt!T(names, var, null, help_text, hidden);
670 	}
671 
672 	void getopt(T)(string names, T* var, void delegate(string, string) @safe parseValue, string[] help_text = null, bool hidden=false)
673 	{
674 		import std.traits : OriginalType;
675 
676 		foreach (ref arg; m_recognizedArgs)
677 			if (names == arg.names) {
678 				assert(help_text is null, format!("Duplicated argument '%s' must not change helptext, consider to remove the duplication")(names));
679 				*var = arg.value.match!(
680 					(OriginalType!T v) => cast(T)v,
681 					(_) {
682 						if (false)
683 							return T.init;
684 						assert(false, "value from previous getopt has different type than the current getopt call");
685 					}
686 				);
687 				return;
688 			}
689 		assert(help_text.length > 0);
690 		Arg arg;
691 		arg.defaultValue = cast(OriginalType!T)*var;
692 		arg.names = names;
693 		arg.helpText = help_text;
694 		arg.hidden = hidden;
695 		if (parseValue is null)
696 			m_args.getopt(config.passThrough, names, var);
697 		else
698 			m_args.getopt(config.passThrough, names, parseValue);
699 		arg.value = cast(OriginalType!T)*var;
700 		m_recognizedArgs ~= arg;
701 	}
702 
703 	/** Resets the list of available source arguments.
704 	*/
705 	void dropAllArgs()
706 	{
707 		m_args = null;
708 	}
709 
710 	/** Returns the list of unprocessed arguments, ignoring the app arguments,
711 	and resets the list of available source arguments.
712 	*/
713 	string[] extractRemainingArgs()
714 	{
715 		assert(m_args !is null, "extractRemainingArgs must be called only once.");
716 
717 		auto ret = m_args[1 .. $];
718 		m_args = null;
719 		return ret;
720 	}
721 
722 	/** Returns the list of unprocessed arguments, including the app arguments
723 		and resets the list of available source arguments.
724 	*/
725 	string[] extractAllRemainingArgs()
726 	{
727 		auto ret = extractRemainingArgs();
728 
729 		if (this.hasAppArgs)
730 		{
731 			ret ~= "--" ~ m_appArgs;
732 		}
733 
734 		return ret;
735 	}
736 }
737 
738 /// Using CommandArgs
739 unittest {
740 	/// It should not find the app version for an empty arg list
741 	assert(new CommandArgs([]).hasAppVersion == false);
742 
743 	/// It should find the app version when `--version` is the first arg
744 	assert(new CommandArgs(["--version"]).hasAppVersion == true);
745 
746 	/// It should not find the app version when `--version` is the second arg
747 	assert(new CommandArgs(["a", "--version"]).hasAppVersion == false);
748 
749 	/// It returns an empty app arg list when `--` arg is missing
750 	assert(new CommandArgs(["1", "2"]).appArgs == []);
751 
752 	/// It returns an empty app arg list when `--` arg is missing
753 	assert(new CommandArgs(["1", "2"]).appArgs == []);
754 
755 	/// It returns app args set after "--"
756 	assert(new CommandArgs(["1", "2", "--", "a"]).appArgs == ["a"]);
757 	assert(new CommandArgs(["1", "2", "--"]).appArgs == []);
758 	assert(new CommandArgs(["--"]).appArgs == []);
759 	assert(new CommandArgs(["--", "a"]).appArgs == ["a"]);
760 
761 	/// It returns the list of all args when no args are processed
762 	assert(new CommandArgs(["1", "2", "--", "a"]).extractAllRemainingArgs == ["1", "2", "--", "a"]);
763 }
764 
765 /// It removes the extracted args
766 unittest {
767 	auto args = new CommandArgs(["-a", "-b", "--", "-c"]);
768 	bool value;
769 	args.getopt("b", &value, [""]);
770 
771 	assert(args.extractAllRemainingArgs == ["-a", "--", "-c"]);
772 }
773 
774 /// It should not be able to remove app args
775 unittest {
776 	auto args = new CommandArgs(["-a", "-b", "--", "-c"]);
777 	bool value;
778 	args.getopt("-c", &value, [""]);
779 
780 	assert(!value);
781 	assert(args.extractAllRemainingArgs == ["-a", "-b", "--", "-c"]);
782 }
783 
784 /** Base class for all commands.
785 
786 	This cass contains a high-level description of the command, including brief
787 	and full descriptions and a human readable command line pattern. On top of
788 	that it defines the two main entry functions for command execution.
789 */
790 class Command {
791 	string name;
792 	string argumentsPattern;
793 	string description;
794 	string[] helpText;
795 	bool acceptsAppArgs;
796 	bool hidden = false; // used for deprecated commands
797 
798 	/** Parses all known command line options without executing any actions.
799 
800 		This function will be called prior to execute, or may be called as
801 		the only method when collecting the list of recognized command line
802 		options.
803 
804 		Only `args.getopt` should be called within this method.
805 	*/
806 	abstract void prepare(scope CommandArgs args);
807 
808 	/**
809 	 * Initialize the dub instance used by `execute`
810 	 */
811 	public Dub prepareDub(CommonOptions options) {
812 		Dub dub;
813 
814 		if (options.bare) {
815 			dub = new Dub(NativePath(options.root_path), getWorkingDirectory());
816 			dub.defaultPlacementLocation = options.placementLocation;
817 
818 			return dub;
819 		}
820 
821 		// initialize DUB
822 		auto package_suppliers = options.registry_urls
823 			.map!((url) {
824 				// Allow to specify fallback mirrors as space separated urls. Undocumented as we
825 				// should simply retry over all registries instead of using a special
826 				// FallbackPackageSupplier.
827 				auto urls = url.splitter(' ');
828 				PackageSupplier ps = getRegistryPackageSupplier(urls.front);
829 				urls.popFront;
830 				if (!urls.empty)
831 					ps = new FallbackPackageSupplier(ps ~ urls.map!getRegistryPackageSupplier.array);
832 				return ps;
833 			})
834 			.array;
835 
836 		dub = new Dub(options.root_path, package_suppliers, options.skipRegistry);
837 		dub.dryRun = options.annotate;
838 		dub.defaultPlacementLocation = options.placementLocation;
839 
840 		// make the CWD package available so that for example sub packages can reference their
841 		// parent package.
842 		try dub.packageManager.getOrLoadPackage(NativePath(options.root_path), NativePath.init, false, StrictMode.Warn);
843 		catch (Exception e) { logDiagnostic("No valid package found in current working directory: %s", e.msg); }
844 
845 		return dub;
846 	}
847 
848 	/** Executes the actual action.
849 
850 		Note that `prepare` will be called before any call to `execute`.
851 	*/
852 	abstract int execute(Dub dub, string[] free_args, string[] app_args);
853 
854 	private bool loadCwdPackage(Dub dub, bool warn_missing_package)
855 	{
856 		auto filePath = Package.findPackageFile(dub.rootPath);
857 
858 		if (filePath.empty) {
859 			if (warn_missing_package) {
860 				logInfoNoTag("");
861 				logInfoNoTag("No package manifest (dub.json or dub.sdl) was found in");
862 				logInfoNoTag(dub.rootPath.toNativeString());
863 				logInfoNoTag("Please run DUB from the root directory of an existing package, or run");
864 				logInfoNoTag("\"%s\" to get information on creating a new package.", "dub init --help".color(Mode.bold));
865 				logInfoNoTag("");
866 			}
867 			return false;
868 		}
869 
870 		dub.loadPackage();
871 
872 		return true;
873 	}
874 }
875 
876 
877 /** Encapsulates a group of commands that fit into a common category.
878 */
879 struct CommandGroup {
880 	/// Caption of the command category
881 	string caption;
882 
883 	/// List of commands contained in this group
884 	Command[] commands;
885 
886 	this(string caption, Command[] commands...) @safe pure nothrow
887 	{
888 		this.caption = caption;
889 		this.commands = commands.dup;
890 	}
891 }
892 
893 /******************************************************************************/
894 /* HELP                                                                       */
895 /******************************************************************************/
896 
897 class HelpCommand : Command {
898 
899 	this() @safe pure nothrow
900 	{
901 		this.name = "help";
902 		this.description = "Shows the help message";
903 		this.helpText = [
904 			"Shows the help message and the supported command options."
905 		];
906 	}
907 
908 	/// HelpCommand.prepare is not supposed to be called, use
909 	/// cast(HelpCommand)this to check if help was requested before execution.
910 	override void prepare(scope CommandArgs args)
911 	{
912 		assert(false, "HelpCommand.prepare is not supposed to be called, use cast(HelpCommand)this to check if help was requested before execution.");
913 	}
914 
915 	/// HelpCommand.execute is not supposed to be called, use
916 	/// cast(HelpCommand)this to check if help was requested before execution.
917 	override int execute(Dub dub, string[] free_args, string[] app_args) {
918 		assert(false, "HelpCommand.execute is not supposed to be called, use cast(HelpCommand)this to check if help was requested before execution.");
919 	}
920 }
921 
922 /******************************************************************************/
923 /* INIT                                                                       */
924 /******************************************************************************/
925 
926 class InitCommand : Command {
927 	private{
928 		string m_templateType = "minimal";
929 		PackageFormat m_format = PackageFormat.json;
930 		bool m_nonInteractive;
931 	}
932 	this() @safe pure nothrow
933 	{
934 		this.name = "init";
935 		this.argumentsPattern = "[<directory> [<dependency>...]]";
936 		this.description = "Initializes an empty package skeleton";
937 		this.helpText = [
938 			"Initializes an empty package of the specified type in the given directory.",
939 			"By default, the current working directory is used.",
940 			"",
941 			"Custom templates can be defined by packages by providing a sub-package called \"init-exec\". No default source files are added in this case.",
942 			"The \"init-exec\" sub-package is compiled and executed inside the destination folder after the base project directory has been created.",
943 			"Free arguments \"dub init -t custom -- free args\" are passed into the \"init-exec\" sub-package as app arguments."
944 		];
945 		this.acceptsAppArgs = true;
946 	}
947 
948 	override void prepare(scope CommandArgs args)
949 	{
950 		args.getopt("t|type", &m_templateType, [
951 			"Set the type of project to generate. Available types:",
952 			"",
953 			"minimal - simple \"hello world\" project (default)",
954 			"vibe.d  - minimal HTTP server based on vibe.d",
955 			"deimos  - skeleton for C header bindings",
956 			"custom  - custom project provided by dub package",
957 		]);
958 		args.getopt("f|format", &m_format, [
959 			"Sets the format to use for the package description file. Possible values:",
960 			"  " ~ [__traits(allMembers, PackageFormat)].map!(f => f == m_format.init.to!string ? f ~ " (default)" : f).join(", ")
961 		]);
962 		args.getopt("n|non-interactive", &m_nonInteractive, ["Don't enter interactive mode."]);
963 	}
964 
965 	override int execute(Dub dub, string[] free_args, string[] app_args)
966 	{
967 		string dir;
968 		if (free_args.length)
969 		{
970 			dir = free_args[0];
971 			free_args = free_args[1 .. $];
972 		}
973 
974 		static string input(string caption, string default_value)
975 		{
976 			writef("%s [%s]: ", caption, default_value);
977 			stdout.flush();
978 			auto inp = readln();
979 			return inp.length > 1 ? inp[0 .. $-1] : default_value;
980 		}
981 
982 		void depCallback(ref PackageRecipe p, ref PackageFormat fmt) {
983 			import std.datetime: Clock;
984 
985 			if (m_nonInteractive) return;
986 
987 			while (true) {
988 				string rawfmt = input("Package recipe format (sdl/json)", fmt.to!string);
989 				if (!rawfmt.length) break;
990 				try {
991 					fmt = rawfmt.to!PackageFormat;
992 					break;
993 				} catch (Exception) {
994 					logError(`Invalid format '%s', enter either 'sdl' or 'json'.`, rawfmt);
995 				}
996 			}
997 			auto author = p.authors.join(", ");
998 			while (true) {
999 				// Tries getting the name until a valid one is given.
1000 				import std.regex;
1001 				auto nameRegex = regex(`^[a-z0-9\-_]+$`);
1002 				string triedName = input("Name", p.name);
1003 				if (triedName.matchFirst(nameRegex).empty) {
1004 					logError(`Invalid name '%s', names should consist only of lowercase alphanumeric characters, dashes ('-') and underscores ('_').`, triedName);
1005 				} else {
1006 					p.name = triedName;
1007 					break;
1008 				}
1009 			}
1010 			p.description = input("Description", p.description);
1011 			p.authors = input("Author name", author).split(",").map!(a => a.strip).array;
1012 			p.license = input("License", p.license);
1013 			string copyrightString = .format("Copyright © %s, %-(%s, %)", Clock.currTime().year, p.authors);
1014 			p.copyright = input("Copyright string", copyrightString);
1015 
1016 			while (true) {
1017 				auto depspec = input("Add dependency (leave empty to skip)", null);
1018 				if (!depspec.length) break;
1019 				addDependency(dub, p, depspec);
1020 			}
1021 		}
1022 
1023 		if (!["vibe.d", "deimos", "minimal"].canFind(m_templateType))
1024 		{
1025 			free_args ~= m_templateType;
1026 		}
1027 		dub.createEmptyPackage(NativePath(dir), free_args, m_templateType, m_format, &depCallback, app_args);
1028 
1029 		logInfo("Package successfully created in %s", dir.length ? dir : ".");
1030 		return 0;
1031 	}
1032 }
1033 
1034 
1035 /******************************************************************************/
1036 /* GENERATE / BUILD / RUN / TEST / DESCRIBE                                   */
1037 /******************************************************************************/
1038 
1039 abstract class PackageBuildCommand : Command {
1040 	protected {
1041 		string m_compilerName;
1042 		string m_arch;
1043 		string[] m_debugVersions;
1044 		string[] m_overrideConfigs;
1045 		GeneratorSettings baseSettings;
1046 		string m_defaultConfig;
1047 		bool m_nodeps;
1048 		bool m_forceRemove = false;
1049 	}
1050 
1051 	override void prepare(scope CommandArgs args)
1052 	{
1053 		args.getopt("b|build", &this.baseSettings.buildType, [
1054 			"Specifies the type of build to perform. Note that setting the DFLAGS environment variable will override the build type with custom flags.",
1055 			"Possible names:",
1056 			"  "~builtinBuildTypes.join(", ")~" and custom types"
1057 		]);
1058 		args.getopt("c|config", &this.baseSettings.config, [
1059 			"Builds the specified configuration. Configurations can be defined in dub.json"
1060 		]);
1061 		args.getopt("override-config", &m_overrideConfigs, [
1062 			"Uses the specified configuration for a certain dependency. Can be specified multiple times.",
1063 			"Format: --override-config=<dependency>/<config>"
1064 		]);
1065 		args.getopt("compiler", &m_compilerName, [
1066 			"Specifies the compiler binary to use (can be a path).",
1067 			"Arbitrary pre- and suffixes to the identifiers below are recognized (e.g. ldc2 or dmd-2.063) and matched to the proper compiler type:",
1068 			"  "~["dmd", "gdc", "ldc", "gdmd", "ldmd"].join(", ")
1069 		]);
1070 		args.getopt("a|arch", &m_arch, [
1071 			"Force a different architecture (e.g. x86 or x86_64)"
1072 		]);
1073 		args.getopt("d|debug", &m_debugVersions, [
1074 			"Define the specified debug version identifier when building - can be used multiple times"
1075 		]);
1076 		args.getopt("nodeps", &m_nodeps, [
1077 			"Do not resolve missing dependencies before building"
1078 		]);
1079 		args.getopt("build-mode", &this.baseSettings.buildMode, [
1080 			"Specifies the way the compiler and linker are invoked. Valid values:",
1081 			"  separate (default), allAtOnce, singleFile"
1082 		]);
1083 		args.getopt("single", &this.baseSettings.single, [
1084 			"Treats the package name as a filename. The file must contain a package recipe comment."
1085 		]);
1086 		args.getopt("force-remove", &m_forceRemove, [
1087 			"Deprecated option that does nothing."
1088 		]);
1089 		args.getopt("filter-versions", &this.baseSettings.filterVersions, [
1090 			"[Experimental] Filter version identifiers and debug version identifiers to improve build cache efficiency."
1091 		]);
1092 	}
1093 
1094 	protected void setupVersionPackage(Dub dub, string str_package_info, string default_build_type = "debug")
1095 	{
1096 		PackageAndVersion package_info = splitPackageName(str_package_info);
1097 		setupPackage(dub, package_info.name, default_build_type, package_info.version_);
1098 	}
1099 
1100 	protected void setupPackage(Dub dub, string package_name, string default_build_type = "debug", string ver = "")
1101 	{
1102 		if (!m_compilerName.length) m_compilerName = dub.defaultCompiler;
1103 		if (!m_arch.length) m_arch = dub.defaultArchitecture;
1104 		if (dub.defaultLowMemory) this.baseSettings.buildSettings.options |= BuildOption.lowmem;
1105 		if (dub.defaultEnvironments) this.baseSettings.buildSettings.addEnvironments(dub.defaultEnvironments);
1106 		if (dub.defaultBuildEnvironments) this.baseSettings.buildSettings.addBuildEnvironments(dub.defaultBuildEnvironments);
1107 		if (dub.defaultRunEnvironments) this.baseSettings.buildSettings.addRunEnvironments(dub.defaultRunEnvironments);
1108 		if (dub.defaultPreGenerateEnvironments) this.baseSettings.buildSettings.addPreGenerateEnvironments(dub.defaultPreGenerateEnvironments);
1109 		if (dub.defaultPostGenerateEnvironments) this.baseSettings.buildSettings.addPostGenerateEnvironments(dub.defaultPostGenerateEnvironments);
1110 		if (dub.defaultPreBuildEnvironments) this.baseSettings.buildSettings.addPreBuildEnvironments(dub.defaultPreBuildEnvironments);
1111 		if (dub.defaultPostBuildEnvironments) this.baseSettings.buildSettings.addPostBuildEnvironments(dub.defaultPostBuildEnvironments);
1112 		if (dub.defaultPreRunEnvironments) this.baseSettings.buildSettings.addPreRunEnvironments(dub.defaultPreRunEnvironments);
1113 		if (dub.defaultPostRunEnvironments) this.baseSettings.buildSettings.addPostRunEnvironments(dub.defaultPostRunEnvironments);
1114 		this.baseSettings.compiler = getCompiler(m_compilerName);
1115 		this.baseSettings.platform = this.baseSettings.compiler.determinePlatform(this.baseSettings.buildSettings, m_compilerName, m_arch);
1116 		this.baseSettings.buildSettings.addDebugVersions(m_debugVersions);
1117 
1118 		m_defaultConfig = null;
1119 		enforce (loadSpecificPackage(dub, package_name, ver), "Failed to load package.");
1120 
1121 		if (this.baseSettings.config.length != 0 &&
1122 			!dub.configurations.canFind(this.baseSettings.config) &&
1123 			this.baseSettings.config != "unittest")
1124 		{
1125 			string msg = "Unknown build configuration: " ~ this.baseSettings.config;
1126 			enum distance = 3;
1127 			auto match = dub.configurations.getClosestMatch(this.baseSettings.config, distance);
1128 			if (match !is null) msg ~= ". Did you mean '" ~ match ~ "'?";
1129 			enforce(0, msg);
1130 		}
1131 
1132 		if (this.baseSettings.buildType.length == 0) {
1133 			if (environment.get("DFLAGS") !is null) this.baseSettings.buildType = "$DFLAGS";
1134 			else this.baseSettings.buildType = default_build_type;
1135 		}
1136 
1137 		if (!m_nodeps) {
1138 			// retrieve missing packages
1139 			if (!dub.project.hasAllDependencies) {
1140 				logDiagnostic("Checking for missing dependencies.");
1141 				if (this.baseSettings.single)
1142 					dub.upgrade(UpgradeOptions.select | UpgradeOptions.noSaveSelections);
1143 				else dub.upgrade(UpgradeOptions.select);
1144 			}
1145 		}
1146 
1147 		dub.project.validate();
1148 
1149 		foreach (sc; m_overrideConfigs) {
1150 			auto idx = sc.indexOf('/');
1151 			enforceUsage(idx >= 0, "Expected \"<package>/<configuration>\" as argument to --override-config.");
1152 			dub.project.overrideConfiguration(sc[0 .. idx], sc[idx+1 .. $]);
1153 		}
1154 	}
1155 
1156 	private bool loadSpecificPackage(Dub dub, string package_name, string ver)
1157 	{
1158 		if (this.baseSettings.single) {
1159 			enforce(package_name.length, "Missing file name of single-file package.");
1160 			dub.loadSingleFilePackage(package_name);
1161 			return true;
1162 		}
1163 
1164 		bool from_cwd = package_name.length == 0 || package_name.startsWith(":");
1165 		// load package in root_path to enable searching for sub packages
1166 		if (loadCwdPackage(dub, from_cwd)) {
1167 			if (package_name.startsWith(":"))
1168 			{
1169 				auto pack = dub.packageManager.getSubPackage(dub.project.rootPackage, package_name[1 .. $], false);
1170 				dub.loadPackage(pack);
1171 				return true;
1172 			}
1173 			if (from_cwd) return true;
1174 		}
1175 
1176 		enforce(package_name.length, "No valid root package found - aborting.");
1177 
1178 		const vers = ver.length ? VersionRange.fromString(ver) : VersionRange.Any;
1179 		auto pack = dub.packageManager.getBestPackage(package_name, vers);
1180 
1181 		enforce(pack, format!"Failed to find a package named '%s%s' locally."(package_name,
1182 			ver == "" ? "" : ("@" ~ ver)
1183 		));
1184 		logInfo("Building package %s in %s", pack.name, pack.path.toNativeString());
1185 		dub.loadPackage(pack);
1186 		return true;
1187 	}
1188 }
1189 
1190 class GenerateCommand : PackageBuildCommand {
1191 	protected {
1192 		string m_generator;
1193 		bool m_printPlatform, m_printBuilds, m_printConfigs;
1194 	}
1195 
1196 	this() @safe pure nothrow
1197 	{
1198 		this.name = "generate";
1199 		this.argumentsPattern = "<generator> [<package>[@<version-spec>]]";
1200 		this.description = "Generates project files using the specified generator";
1201 		this.helpText = [
1202 			"Generates project files using one of the supported generators:",
1203 			"",
1204 			"visuald - VisualD project files",
1205 			"sublimetext - SublimeText project file",
1206 			"cmake - CMake build scripts",
1207 			"build - Builds the package directly",
1208 			"",
1209 			"An optional package name can be given to generate a different package than the root/CWD package."
1210 		];
1211 	}
1212 
1213 	override void prepare(scope CommandArgs args)
1214 	{
1215 		super.prepare(args);
1216 
1217 		args.getopt("combined", &this.baseSettings.combined, [
1218 			"Tries to build the whole project in a single compiler run."
1219 		]);
1220 
1221 		args.getopt("print-builds", &m_printBuilds, [
1222 			"Prints the list of available build types"
1223 		]);
1224 		args.getopt("print-configs", &m_printConfigs, [
1225 			"Prints the list of available configurations"
1226 		]);
1227 		args.getopt("print-platform", &m_printPlatform, [
1228 			"Prints the identifiers for the current build platform as used for the build fields in dub.json"
1229 		]);
1230 		args.getopt("parallel", &this.baseSettings.parallelBuild, [
1231 			"Runs multiple compiler instances in parallel, if possible."
1232 		]);
1233 	}
1234 
1235 	override int execute(Dub dub, string[] free_args, string[] app_args)
1236 	{
1237 		string str_package_info;
1238 		if (!m_generator.length) {
1239 			enforceUsage(free_args.length >= 1 && free_args.length <= 2, "Expected one or two arguments.");
1240 			m_generator = free_args[0];
1241 			if (free_args.length >= 2) str_package_info = free_args[1];
1242 		} else {
1243 			enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1244 			if (free_args.length >= 1) str_package_info = free_args[0];
1245 		}
1246 
1247 		setupVersionPackage(dub, str_package_info, "debug");
1248 
1249 		if (m_printBuilds) {
1250 			logInfo("Available build types:");
1251 			foreach (i, tp; dub.project.builds)
1252 				logInfo("  %s%s", tp, i == 0 ? " [default]" : null);
1253 			logInfo("");
1254 		}
1255 
1256 		m_defaultConfig = dub.project.getDefaultConfiguration(this.baseSettings.platform);
1257 		if (m_printConfigs) {
1258 			logInfo("Available configurations:");
1259 			foreach (tp; dub.configurations)
1260 				logInfo("  %s%s", tp, tp == m_defaultConfig ? " [default]" : null);
1261 			logInfo("");
1262 		}
1263 
1264 		GeneratorSettings gensettings = this.baseSettings;
1265 		if (!gensettings.config.length)
1266 			gensettings.config = m_defaultConfig;
1267 		gensettings.runArgs = app_args;
1268 		// legacy compatibility, default working directory is always CWD
1269 		gensettings.overrideToolWorkingDirectory = getWorkingDirectory();
1270 
1271 		logDiagnostic("Generating using %s", m_generator);
1272 		dub.generateProject(m_generator, gensettings);
1273 		if (this.baseSettings.buildType == "ddox") dub.runDdox(gensettings.run, app_args);
1274 		return 0;
1275 	}
1276 }
1277 
1278 class BuildCommand : GenerateCommand {
1279 	protected {
1280 		bool m_yes; // automatic yes to prompts;
1281 		bool m_nonInteractive;
1282 	}
1283 	this() @safe pure nothrow
1284 	{
1285 		this.name = "build";
1286 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1287 		this.description = "Builds a package (uses the main package in the current working directory by default)";
1288 		this.helpText = [
1289 			"Builds a package (uses the main package in the current working directory by default)"
1290 		];
1291 	}
1292 
1293 	override void prepare(scope CommandArgs args)
1294 	{
1295 		args.getopt("temp-build", &this.baseSettings.tempBuild, [
1296 			"Builds the project in the temp folder if possible."
1297 		]);
1298 
1299 		args.getopt("rdmd", &this.baseSettings.rdmd, [
1300 			"Use rdmd instead of directly invoking the compiler"
1301 		]);
1302 
1303 		args.getopt("f|force", &this.baseSettings.force, [
1304 			"Forces a recompilation even if the target is up to date"
1305 		]);
1306 		args.getopt("y|yes", &m_yes, [
1307 			`Automatic yes to prompts. Assume "yes" as answer to all interactive prompts.`
1308 		]);
1309 		args.getopt("n|non-interactive", &m_nonInteractive, [
1310 			"Don't enter interactive mode."
1311 		]);
1312 		super.prepare(args);
1313 		m_generator = "build";
1314 	}
1315 
1316 	override int execute(Dub dub, string[] free_args, string[] app_args)
1317 	{
1318 		// single package files don't need to be downloaded, they are on the disk.
1319 		if (free_args.length < 1 || this.baseSettings.single)
1320 			return super.execute(dub, free_args, app_args);
1321 
1322 		if (!m_nonInteractive)
1323 		{
1324 			const packageParts = splitPackageName(free_args[0]);
1325 			if (auto rc = fetchMissingPackages(dub, packageParts))
1326 				return rc;
1327 		}
1328 		return super.execute(dub, free_args, app_args);
1329 	}
1330 
1331 	private int fetchMissingPackages(Dub dub, in PackageAndVersion packageParts)
1332 	{
1333 
1334 		static bool input(string caption, bool default_value = true) {
1335 			writef("%s [%s]: ", caption, default_value ? "Y/n" : "y/N");
1336 			auto inp = readln();
1337 			string userInput = "y";
1338 			if (inp.length > 1)
1339 				userInput = inp[0 .. $ - 1].toLower;
1340 
1341 			switch (userInput) {
1342 				case "no", "n", "0":
1343 					return false;
1344 				case "yes", "y", "1":
1345 				default:
1346 					return true;
1347 			}
1348 		}
1349 
1350 		VersionRange dep;
1351 
1352 		if (packageParts.version_.length > 0) {
1353 			// the user provided a version manually
1354 			dep = VersionRange.fromString(packageParts.version_);
1355 		} else if (packageParts.name.startsWith(":")) {
1356 			// Sub-packages are always assumed to be present
1357 			return 0;
1358 		} else if (dub.packageManager.getBestPackage(packageParts.name)) {
1359 			// found locally
1360 			return 0;
1361 		} else {
1362 			// search for the package and filter versions for exact matches
1363 			auto basePackageName = getBasePackageName(packageParts.name);
1364 			auto search = dub.searchPackages(basePackageName)
1365 				.map!(tup => tup[1].find!(p => p.name == basePackageName))
1366 				.filter!(ps => !ps.empty);
1367 			if (search.empty) {
1368 				logWarn("Package '%s' was neither found locally nor online.", packageParts.name);
1369 				return 2;
1370 			}
1371 
1372 			const p = search.front.front;
1373 			logInfo("Package '%s' was not found locally but is available online:", packageParts.name);
1374 			logInfo("---");
1375 			logInfo("Description: %s", p.description);
1376 			logInfo("Version: %s", p.version_);
1377 			logInfo("---");
1378 
1379 			const answer = m_yes ? true : input("Do you want to fetch '%s' now?".format(packageParts.name));
1380 			if (!answer)
1381 				return 0;
1382 			dep = VersionRange.fromString(p.version_);
1383 		}
1384 
1385 		dub.fetch(packageParts.name, dep, dub.defaultPlacementLocation, FetchOptions.none);
1386 		return 0;
1387 	}
1388 }
1389 
1390 class RunCommand : BuildCommand {
1391 	this() @safe pure nothrow
1392 	{
1393 		this.name = "run";
1394 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1395 		this.description = "Builds and runs a package (default command)";
1396 		this.helpText = [
1397 			"Builds and runs a package (uses the main package in the current working directory by default)"
1398 		];
1399 		this.acceptsAppArgs = true;
1400 	}
1401 
1402 	override void prepare(scope CommandArgs args)
1403 	{
1404 		super.prepare(args);
1405 		this.baseSettings.run = true;
1406 	}
1407 
1408 	override int execute(Dub dub, string[] free_args, string[] app_args)
1409 	{
1410 		return super.execute(dub, free_args, app_args);
1411 	}
1412 }
1413 
1414 class TestCommand : PackageBuildCommand {
1415 	private {
1416 		string m_mainFile;
1417 	}
1418 
1419 	this() @safe pure nothrow
1420 	{
1421 		this.name = "test";
1422 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1423 		this.description = "Executes the tests of the selected package";
1424 		this.helpText = [
1425 			`Builds the package and executes all contained unit tests.`,
1426 			``,
1427 			`If no explicit configuration is given, an existing "unittest" ` ~
1428 			`configuration will be preferred for testing. If none exists, the ` ~
1429 			`first library type configuration will be used, and if that doesn't ` ~
1430 			`exist either, the first executable configuration is chosen.`,
1431 			``,
1432 			`When a custom main file (--main-file) is specified, only library ` ~
1433 			`configurations can be used. Otherwise, depending on the type of ` ~
1434 			`the selected configuration, either an existing main file will be ` ~
1435 			`used (and needs to be properly adjusted to just run the unit ` ~
1436 			`tests for 'version(unittest)'), or DUB will generate one for ` ~
1437 			`library type configurations.`,
1438 			``,
1439 			`Finally, if the package contains a dependency to the "tested" ` ~
1440 			`package, the automatically generated main file will use it to ` ~
1441 			`run the unit tests.`
1442 		];
1443 		this.acceptsAppArgs = true;
1444 	}
1445 
1446 	override void prepare(scope CommandArgs args)
1447 	{
1448 		args.getopt("temp-build", &this.baseSettings.tempBuild, [
1449 			"Builds the project in the temp folder if possible."
1450 		]);
1451 
1452 		args.getopt("main-file", &m_mainFile, [
1453 			"Specifies a custom file containing the main() function to use for running the tests."
1454 		]);
1455 		args.getopt("combined", &this.baseSettings.combined, [
1456 			"Tries to build the whole project in a single compiler run."
1457 		]);
1458 		args.getopt("parallel", &this.baseSettings.parallelBuild, [
1459 			"Runs multiple compiler instances in parallel, if possible."
1460 		]);
1461 		args.getopt("f|force", &this.baseSettings.force, [
1462 			"Forces a recompilation even if the target is up to date"
1463 		]);
1464 
1465 		bool coverage = false;
1466 		args.getopt("coverage", &coverage, [
1467 			"Enables code coverage statistics to be generated."
1468 		]);
1469 		if (coverage) this.baseSettings.buildType = "unittest-cov";
1470 
1471 		bool coverageCTFE = false;
1472 		args.getopt("coverage-ctfe", &coverageCTFE, [
1473 			"Enables code coverage (including CTFE) statistics to be generated."
1474 		]);
1475 		if (coverageCTFE) this.baseSettings.buildType = "unittest-cov-ctfe";
1476 
1477 		super.prepare(args);
1478 	}
1479 
1480 	override int execute(Dub dub, string[] free_args, string[] app_args)
1481 	{
1482 		string str_package_info;
1483 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1484 		if (free_args.length >= 1) str_package_info = free_args[0];
1485 
1486 		setupVersionPackage(dub, str_package_info, "unittest");
1487 
1488 		GeneratorSettings settings = this.baseSettings;
1489 		settings.compiler = getCompiler(this.baseSettings.platform.compilerBinary);
1490 		settings.run = true;
1491 		settings.runArgs = app_args;
1492 
1493 		dub.testProject(settings, this.baseSettings.config, NativePath(m_mainFile));
1494 		return 0;
1495 	}
1496 }
1497 
1498 class LintCommand : PackageBuildCommand {
1499 	private {
1500 		bool m_syntaxCheck = false;
1501 		bool m_styleCheck = false;
1502 		string m_errorFormat;
1503 		bool m_report = false;
1504 		string m_reportFormat;
1505 		string m_reportFile;
1506 		string[] m_importPaths;
1507 		string m_config;
1508 	}
1509 
1510 	this() @safe pure nothrow
1511 	{
1512 		this.name = "lint";
1513 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1514 		this.description = "Executes the linter tests of the selected package";
1515 		this.helpText = [
1516 			`Builds the package and executes D-Scanner linter tests.`
1517 		];
1518 		this.acceptsAppArgs = true;
1519 	}
1520 
1521 	override void prepare(scope CommandArgs args)
1522 	{
1523 		args.getopt("syntax-check", &m_syntaxCheck, [
1524 			"Lexes and parses sourceFile, printing the line and column number of " ~
1525 			"any syntax errors to stdout."
1526 		]);
1527 
1528 		args.getopt("style-check", &m_styleCheck, [
1529 			"Lexes and parses sourceFiles, printing the line and column number of " ~
1530 			"any static analysis check failures stdout."
1531 		]);
1532 
1533 		args.getopt("error-format", &m_errorFormat, [
1534 			"Format errors produced by the style/syntax checkers."
1535 		]);
1536 
1537 		args.getopt("report", &m_report, [
1538 			"Generate a static analysis report in JSON format."
1539 		]);
1540 
1541 		args.getopt("report-format", &m_reportFormat, [
1542 			"Specifies the format of the generated report."
1543 		]);
1544 
1545 		args.getopt("report-file", &m_reportFile, [
1546 			"Write report to file."
1547 		]);
1548 
1549 		if (m_reportFormat || m_reportFile) m_report = true;
1550 
1551 		args.getopt("import-paths", &m_importPaths, [
1552 			"Import paths"
1553 		]);
1554 
1555 		args.getopt("dscanner-config", &m_config, [
1556 			"Use the given d-scanner configuration file."
1557 		]);
1558 
1559 		super.prepare(args);
1560 	}
1561 
1562 	override int execute(Dub dub, string[] free_args, string[] app_args)
1563 	{
1564 		string str_package_info;
1565 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1566 		if (free_args.length >= 1) str_package_info = free_args[0];
1567 
1568 		string[] args;
1569 		if (!m_syntaxCheck && !m_styleCheck && !m_report && app_args.length == 0) { m_styleCheck = true; }
1570 
1571 		if (m_syntaxCheck) args ~= "--syntaxCheck";
1572 		if (m_styleCheck) args ~= "--styleCheck";
1573 		if (m_errorFormat) args ~= ["--errorFormat", m_errorFormat];
1574 		if (m_report) args ~= "--report";
1575 		if (m_reportFormat) args ~= ["--reportFormat", m_reportFormat];
1576 		if (m_reportFile) args ~= ["--reportFile", m_reportFile];
1577 		foreach (import_path; m_importPaths) args ~= ["-I", import_path];
1578 		if (m_config) args ~= ["--config", m_config];
1579 
1580 		setupVersionPackage(dub, str_package_info);
1581 		dub.lintProject(args ~ app_args);
1582 		return 0;
1583 	}
1584 }
1585 
1586 class DescribeCommand : PackageBuildCommand {
1587 	private {
1588 		bool m_importPaths = false;
1589 		bool m_stringImportPaths = false;
1590 		bool m_dataList = false;
1591 		bool m_dataNullDelim = false;
1592 		string[] m_data;
1593 	}
1594 
1595 	this() @safe pure nothrow
1596 	{
1597 		this.name = "describe";
1598 		this.argumentsPattern = "[<package>[@<version-spec>]]";
1599 		this.description = "Prints a JSON description of the project and its dependencies";
1600 		this.helpText = [
1601 			"Prints a JSON build description for the root package an all of " ~
1602 			"their dependencies in a format similar to a JSON package " ~
1603 			"description file. This is useful mostly for IDEs.",
1604 			"",
1605 			"All usual options that are also used for build/run/generate apply.",
1606 			"",
1607 			"When --data=VALUE is supplied, specific build settings for a project " ~
1608 			"will be printed instead (by default, formatted for the current compiler).",
1609 			"",
1610 			"The --data=VALUE option can be specified multiple times to retrieve " ~
1611 			"several pieces of information at once. A comma-separated list is " ~
1612 			"also acceptable (ex: --data=dflags,libs). The data will be output in " ~
1613 			"the same order requested on the command line.",
1614 			"",
1615 			"The accepted values for --data=VALUE are:",
1616 			"",
1617 			"main-source-file, dflags, lflags, libs, linker-files, " ~
1618 			"source-files, versions, debug-versions, import-paths, " ~
1619 			"string-import-paths, import-files, options",
1620 			"",
1621 			"The following are also accepted by --data if --data-list is used:",
1622 			"",
1623 			"target-type, target-path, target-name, working-directory, " ~
1624 			"copy-files, string-import-files, pre-generate-commands, " ~
1625 			"post-generate-commands, pre-build-commands, post-build-commands, " ~
1626 			"pre-run-commands, post-run-commands, requirements",
1627 		];
1628 	}
1629 
1630 	override void prepare(scope CommandArgs args)
1631 	{
1632 		super.prepare(args);
1633 
1634 		args.getopt("import-paths", &m_importPaths, [
1635 			"Shortcut for --data=import-paths --data-list"
1636 		]);
1637 
1638 		args.getopt("string-import-paths", &m_stringImportPaths, [
1639 			"Shortcut for --data=string-import-paths --data-list"
1640 		]);
1641 
1642 		args.getopt("data", &m_data, [
1643 			"Just list the values of a particular build setting, either for this "~
1644 			"package alone or recursively including all dependencies. Accepts a "~
1645 			"comma-separated list. See above for more details and accepted "~
1646 			"possibilities for VALUE."
1647 		]);
1648 
1649 		args.getopt("data-list", &m_dataList, [
1650 			"Output --data information in list format (line-by-line), instead "~
1651 			"of formatting for a compiler command line.",
1652 		]);
1653 
1654 		args.getopt("data-0", &m_dataNullDelim, [
1655 			"Output --data information using null-delimiters, rather than "~
1656 			"spaces or newlines. Result is usable with, ex., xargs -0.",
1657 		]);
1658 	}
1659 
1660 	override int execute(Dub dub, string[] free_args, string[] app_args)
1661 	{
1662 		enforceUsage(
1663 			!(m_importPaths && m_stringImportPaths),
1664 			"--import-paths and --string-import-paths may not be used together."
1665 		);
1666 
1667 		enforceUsage(
1668 			!(m_data && (m_importPaths || m_stringImportPaths)),
1669 			"--data may not be used together with --import-paths or --string-import-paths."
1670 		);
1671 
1672 		// disable all log output to stdout and use "writeln" to output the JSON description
1673 		auto ll = getLogLevel();
1674 		setLogLevel(max(ll, LogLevel.warn));
1675 		scope (exit) setLogLevel(ll);
1676 
1677 		string str_package_info;
1678 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1679 		if (free_args.length >= 1) str_package_info = free_args[0];
1680 		setupVersionPackage(dub, str_package_info);
1681 
1682 		m_defaultConfig = dub.project.getDefaultConfiguration(this.baseSettings.platform);
1683 
1684 		GeneratorSettings settings = this.baseSettings;
1685 		if (!settings.config.length)
1686 			settings.config = m_defaultConfig;
1687 		settings.cache = dub.cachePathDontUse(); // See function's description
1688 		// Ignore other options
1689 		settings.buildSettings.options = this.baseSettings.buildSettings.options & BuildOption.lowmem;
1690 
1691 		// With a requested `unittest` config, switch to the special test runner
1692 		// config (which doesn't require an existing `unittest` configuration).
1693 		if (this.baseSettings.config == "unittest") {
1694 			const test_config = dub.project.addTestRunnerConfiguration(settings, !dub.dryRun);
1695 			if (test_config) settings.config = test_config;
1696 		}
1697 
1698 		if (m_importPaths) { m_data = ["import-paths"]; m_dataList = true; }
1699 		else if (m_stringImportPaths) { m_data = ["string-import-paths"]; m_dataList = true; }
1700 
1701 		if (m_data.length) {
1702 			ListBuildSettingsFormat lt;
1703 			with (ListBuildSettingsFormat)
1704 				lt = m_dataList ? (m_dataNullDelim ? listNul : list) : (m_dataNullDelim ? commandLineNul : commandLine);
1705 			dub.listProjectData(settings, m_data, lt);
1706 		} else {
1707 			auto desc = dub.project.describe(settings);
1708 			writeln(desc.serializeToPrettyJson());
1709 		}
1710 
1711 		return 0;
1712 	}
1713 }
1714 
1715 class CleanCommand : Command {
1716 	private {
1717 		bool m_allPackages;
1718 	}
1719 
1720 	this() @safe pure nothrow
1721 	{
1722 		this.name = "clean";
1723 		this.argumentsPattern = "[<package>]";
1724 		this.description = "Removes intermediate build files and cached build results";
1725 		this.helpText = [
1726 			"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.",
1727 			"Without arguments, the package in the current working directory will be cleaned."
1728 		];
1729 	}
1730 
1731 	override void prepare(scope CommandArgs args)
1732 	{
1733 		args.getopt("all-packages", &m_allPackages, [
1734 			"Cleans up *all* known packages (dub list)"
1735 		]);
1736 	}
1737 
1738 	override int execute(Dub dub, string[] free_args, string[] app_args)
1739 	{
1740 		enforceUsage(free_args.length <= 1, "Expected one or zero arguments.");
1741 		enforceUsage(app_args.length == 0, "Application arguments are not supported for the clean command.");
1742 		enforceUsage(!m_allPackages || !free_args.length, "The --all-packages flag may not be used together with an explicit package name.");
1743 
1744 		enforce(free_args.length == 0, "Cleaning a specific package isn't possible right now.");
1745 
1746 		if (m_allPackages) {
1747 			dub.clean();
1748 		} else {
1749 			dub.loadPackage();
1750 			dub.clean(dub.project.rootPackage);
1751 		}
1752 
1753 		return 0;
1754 	}
1755 }
1756 
1757 
1758 /******************************************************************************/
1759 /* FETCH / ADD / REMOVE / UPGRADE                                             */
1760 /******************************************************************************/
1761 
1762 class AddCommand : Command {
1763 	this() @safe pure nothrow
1764 	{
1765 		this.name = "add";
1766 		this.argumentsPattern = "<package>[@<version-spec>] [<packages...>]";
1767 		this.description = "Adds dependencies to the package file.";
1768 		this.helpText = [
1769 			"Adds <packages> as dependencies.",
1770 			"",
1771 			"Running \"dub add <package>\" is the same as adding <package> to the \"dependencies\" section in dub.json/dub.sdl.",
1772 			"If no version is specified for one of the packages, dub will query the registry for the latest version."
1773 		];
1774 	}
1775 
1776 	override void prepare(scope CommandArgs args) {}
1777 
1778 	override int execute(Dub dub, string[] free_args, string[] app_args)
1779 	{
1780 		import dub.recipe.io : readPackageRecipe, writePackageRecipe;
1781 		import dub.internal.vibecompat.core.file : existsFile;
1782 		enforceUsage(free_args.length != 0, "Expected one or more arguments.");
1783 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1784 
1785 		if (!loadCwdPackage(dub, true)) return 2;
1786 		auto recipe = dub.project.rootPackage.rawRecipe.clone;
1787 
1788 		foreach (depspec; free_args) {
1789 			if (!addDependency(dub, recipe, depspec))
1790 				return 2;
1791 		}
1792 		writePackageRecipe(dub.project.rootPackage.recipePath, recipe);
1793 
1794 		return 0;
1795 	}
1796 }
1797 
1798 class UpgradeCommand : Command {
1799 	private {
1800 		bool m_prerelease = false;
1801 		bool m_includeSubPackages = false;
1802 		bool m_forceRemove = false;
1803 		bool m_missingOnly = false;
1804 		bool m_verify = false;
1805 		bool m_dryRun = false;
1806 	}
1807 
1808 	this() @safe pure nothrow
1809 	{
1810 		this.name = "upgrade";
1811 		this.argumentsPattern = "[<packages...>]";
1812 		this.description = "Forces an upgrade of the dependencies";
1813 		this.helpText = [
1814 			"Upgrades all dependencies of the package by querying the package registry(ies) for new versions.",
1815 			"",
1816 			"This will update the versions stored in the selections file ("~SelectedVersions.defaultFile~") accordingly.",
1817 			"",
1818 			"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."
1819 		];
1820 	}
1821 
1822 	override void prepare(scope CommandArgs args)
1823 	{
1824 		args.getopt("prerelease", &m_prerelease, [
1825 			"Uses the latest pre-release version, even if release versions are available"
1826 		]);
1827 		args.getopt("s|sub-packages", &m_includeSubPackages, [
1828 			"Also upgrades dependencies of all directory based sub packages"
1829 		]);
1830 		args.getopt("verify", &m_verify, [
1831 			"Updates the project and performs a build. If successful, rewrites the selected versions file <to be implemented>."
1832 		]);
1833 		args.getopt("dry-run", &m_dryRun, [
1834 			"Only print what would be upgraded, but don't actually upgrade anything."
1835 		]);
1836 		args.getopt("missing-only", &m_missingOnly, [
1837 			"Performs an upgrade only for dependencies that don't yet have a version selected. This is also done automatically before each build."
1838 		]);
1839 		args.getopt("force-remove", &m_forceRemove, [
1840 			"Deprecated option that does nothing."
1841 		]);
1842 	}
1843 
1844 	override int execute(Dub dub, string[] free_args, string[] app_args)
1845 	{
1846 		enforceUsage(free_args.length <= 1, "Unexpected arguments.");
1847 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1848 		enforceUsage(!m_verify, "--verify is not yet implemented.");
1849 		enforce(loadCwdPackage(dub, true), "Failed to load package.");
1850 		logInfo("Upgrading", Color.cyan, "project in %s", dub.projectPath.toNativeString().color(Mode.bold));
1851 		auto options = UpgradeOptions.upgrade|UpgradeOptions.select;
1852 		if (m_missingOnly) options &= ~UpgradeOptions.upgrade;
1853 		if (m_prerelease) options |= UpgradeOptions.preRelease;
1854 		if (m_dryRun) options |= UpgradeOptions.dryRun;
1855 		dub.upgrade(options, free_args);
1856 
1857 		auto spacks = dub.project.rootPackage
1858 			.subPackages
1859 			.filter!(sp => sp.path.length);
1860 
1861 		if (m_includeSubPackages) {
1862 			bool any_error = false;
1863 
1864 			// Go through each path based sub package, load it as a new instance
1865 			// and perform an upgrade as if the upgrade had been run from within
1866 			// the sub package folder. Note that we have to use separate Dub
1867 			// instances, because the upgrade always works on the root package
1868 			// of a project, which in this case are the individual sub packages.
1869 			foreach (sp; spacks) {
1870 				try {
1871 					auto fullpath = (dub.projectPath ~ sp.path).toNativeString();
1872 					logInfo("Upgrading", Color.cyan, "sub package in %s", fullpath);
1873 					auto sdub = new Dub(fullpath, dub.packageSuppliers, SkipPackageSuppliers.all);
1874 					sdub.defaultPlacementLocation = dub.defaultPlacementLocation;
1875 					sdub.loadPackage();
1876 					sdub.upgrade(options, free_args);
1877 				} catch (Exception e) {
1878 					logError("Failed to update sub package at %s: %s",
1879 						sp.path, e.msg);
1880 					any_error = true;
1881 				}
1882 			}
1883 
1884 			if (any_error) return 1;
1885 		} else if (!spacks.empty) {
1886 			foreach (sp; spacks)
1887 				logInfo("Not upgrading sub package in %s", sp.path);
1888 			logInfo("\nNote: specify -s to also upgrade sub packages.");
1889 		}
1890 
1891 		return 0;
1892 	}
1893 }
1894 
1895 class FetchRemoveCommand : Command {
1896 	protected {
1897 		string m_version;
1898 		bool m_forceRemove = false;
1899 	}
1900 
1901 	override void prepare(scope CommandArgs args)
1902 	{
1903 		args.getopt("version", &m_version, [
1904 			"Use the specified version/branch instead of the latest available match",
1905 			"The remove command also accepts \"*\" here as a wildcard to remove all versions of the package from the specified location"
1906 		], true); // hide --version from help
1907 
1908 		args.getopt("force-remove", &m_forceRemove, [
1909 			"Deprecated option that does nothing"
1910 		]);
1911 	}
1912 
1913 	abstract override int execute(Dub dub, string[] free_args, string[] app_args);
1914 }
1915 
1916 class FetchCommand : FetchRemoveCommand {
1917 	this() @safe pure nothrow
1918 	{
1919 		this.name = "fetch";
1920 		this.argumentsPattern = "<package>[@<version-spec>]";
1921 		this.description = "Manually retrieves and caches a package";
1922 		this.helpText = [
1923 			"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.",
1924 			"",
1925 			"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.",
1926 			"",
1927 			"Without specified options, placement/removal will default to a user wide shared location.",
1928 			"",
1929 			"Complete applications can be retrieved and run easily by e.g.",
1930 			"$ dub fetch vibelog --cache=local",
1931 			"$ dub run vibelog --cache=local",
1932 			"",
1933 			"This will grab all needed dependencies and compile and run the application.",
1934 		];
1935 	}
1936 
1937 	override void prepare(scope CommandArgs args)
1938 	{
1939 		super.prepare(args);
1940 	}
1941 
1942 	override int execute(Dub dub, string[] free_args, string[] app_args)
1943 	{
1944 		enforceUsage(free_args.length == 1, "Expecting exactly one argument.");
1945 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
1946 
1947 		auto location = dub.defaultPlacementLocation;
1948 
1949 		auto name = free_args[0];
1950 
1951 		FetchOptions fetchOpts;
1952 		fetchOpts |= FetchOptions.forceBranchUpgrade;
1953 		if (m_version.length) { // remove then --version removed
1954 			enforceUsage(!name.canFindVersionSplitter, "Double version spec not allowed.");
1955 			logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", name, m_version);
1956 			dub.fetch(name, VersionRange.fromString(m_version), location, fetchOpts);
1957 		} else if (name.canFindVersionSplitter) {
1958 			const parts = name.splitPackageName;
1959 			dub.fetch(parts.name, VersionRange.fromString(parts.version_), location, fetchOpts);
1960 		} else {
1961 			try {
1962 				dub.fetch(name, VersionRange.Any, location, fetchOpts);
1963 				logInfo("Finished", Color.green, "%s fetched", name.color(Mode.bold));
1964 				logInfo(
1965 					"Please note that you need to use `dub run <pkgname>` " ~
1966 					"or add it to dependencies of your package to actually use/run it. "
1967 				);
1968 			}
1969 			catch(Exception e){
1970 				logInfo("Getting a release version failed: %s", e.msg);
1971 				logInfo("Retry with ~master...");
1972 				dub.fetch(name, VersionRange.fromString("~master"), location, fetchOpts);
1973 			}
1974 		}
1975 		return 0;
1976 	}
1977 }
1978 
1979 class RemoveCommand : FetchRemoveCommand {
1980 	private {
1981 		bool m_nonInteractive;
1982 	}
1983 
1984 	this() @safe pure nothrow
1985 	{
1986 		this.name = "remove";
1987 		this.argumentsPattern = "<package>[@<version-spec>]";
1988 		this.description = "Removes a cached package";
1989 		this.helpText = [
1990 			"Removes a package that is cached on the local system."
1991 		];
1992 	}
1993 
1994 	override void prepare(scope CommandArgs args)
1995 	{
1996 		super.prepare(args);
1997 		args.getopt("n|non-interactive", &m_nonInteractive, ["Don't enter interactive mode."]);
1998 	}
1999 
2000 	override int execute(Dub dub, string[] free_args, string[] app_args)
2001 	{
2002 		enforceUsage(free_args.length == 1, "Expecting exactly one argument.");
2003 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
2004 
2005 		auto package_id = free_args[0];
2006 		auto location = dub.defaultPlacementLocation;
2007 
2008 		size_t resolveVersion(in Package[] packages) {
2009 			// just remove only package version
2010 			if (packages.length == 1)
2011 				return 0;
2012 
2013 			writeln("Select version of '", package_id, "' to remove from location '", location, "':");
2014 			foreach (i, pack; packages)
2015 				writefln("%s) %s", i + 1, pack.version_);
2016 			writeln(packages.length + 1, ") ", "all versions");
2017 			while (true) {
2018 				writef("> ");
2019 				auto inp = readln();
2020 				if (!inp.length) // Ctrl+D
2021 					return size_t.max;
2022 				inp = inp.stripRight;
2023 				if (!inp.length) // newline or space
2024 					continue;
2025 				try {
2026 					immutable selection = inp.to!size_t - 1;
2027 					if (selection <= packages.length)
2028 						return selection;
2029 				} catch (ConvException e) {
2030 				}
2031 				logError("Please enter a number between 1 and %s.", packages.length + 1);
2032 			}
2033 		}
2034 
2035 		if (!m_version.empty) { // remove then --version removed
2036 			enforceUsage(!package_id.canFindVersionSplitter, "Double version spec not allowed.");
2037 			logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", package_id, m_version);
2038 			dub.remove(package_id, m_version, location);
2039 		} else {
2040 			const parts = package_id.splitPackageName;
2041 			if (m_nonInteractive || parts.version_.length) {
2042 				dub.remove(parts.name, parts.version_, location);
2043 			} else {
2044 				dub.remove(package_id, location, &resolveVersion);
2045 			}
2046 		}
2047 		return 0;
2048 	}
2049 }
2050 
2051 /******************************************************************************/
2052 /* ADD/REMOVE PATH/LOCAL                                                      */
2053 /******************************************************************************/
2054 
2055 abstract class RegistrationCommand : Command {
2056 	private {
2057 		bool m_system;
2058 	}
2059 
2060 	override void prepare(scope CommandArgs args)
2061 	{
2062 		args.getopt("system", &m_system, [
2063 			"Register system-wide instead of user-wide"
2064 		]);
2065 	}
2066 
2067 	abstract override int execute(Dub dub, string[] free_args, string[] app_args);
2068 }
2069 
2070 class AddPathCommand : RegistrationCommand {
2071 	this() @safe pure nothrow
2072 	{
2073 		this.name = "add-path";
2074 		this.argumentsPattern = "<path>";
2075 		this.description = "Adds a default package search path";
2076 		this.helpText = [
2077 			"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.",
2078 			"",
2079 			"Any packages registered using add-path will be preferred over packages downloaded from the package registry when searching for dependencies during a build operation.",
2080 			"",
2081 			"The version of the packages will be determined by one of the following:",
2082 			"  - For GIT working copies, the last tag (git describe) is used to determine the version",
2083 			"  - If the package contains a \"version\" field in the package description, this is used",
2084 			"  - If neither of those apply, \"~master\" is assumed"
2085 		];
2086 	}
2087 
2088 	override int execute(Dub dub, string[] free_args, string[] app_args)
2089 	{
2090 		enforceUsage(free_args.length == 1, "Missing search path.");
2091 		dub.addSearchPath(free_args[0], m_system);
2092 		return 0;
2093 	}
2094 }
2095 
2096 class RemovePathCommand : RegistrationCommand {
2097 	this() @safe pure nothrow
2098 	{
2099 		this.name = "remove-path";
2100 		this.argumentsPattern = "<path>";
2101 		this.description = "Removes a package search path";
2102 		this.helpText = ["Removes a package search path previously added with add-path."];
2103 	}
2104 
2105 	override int execute(Dub dub, string[] free_args, string[] app_args)
2106 	{
2107 		enforceUsage(free_args.length == 1, "Expected one argument.");
2108 		dub.removeSearchPath(free_args[0], m_system);
2109 		return 0;
2110 	}
2111 }
2112 
2113 class AddLocalCommand : RegistrationCommand {
2114 	this() @safe pure nothrow
2115 	{
2116 		this.name = "add-local";
2117 		this.argumentsPattern = "<path> [<version>]";
2118 		this.description = "Adds a local package directory (e.g. a git repository)";
2119 		this.helpText = [
2120 			"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.",
2121 			"",
2122 			"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.",
2123 			"",
2124 			"See 'dub add-path -h' for a way to register multiple local packages at once."
2125 		];
2126 	}
2127 
2128 	override int execute(Dub dub, string[] free_args, string[] app_args)
2129 	{
2130 		enforceUsage(free_args.length == 1 || free_args.length == 2, "Expecting one or two arguments.");
2131 		string ver = free_args.length == 2 ? free_args[1] : null;
2132 		dub.addLocalPackage(free_args[0], ver, m_system);
2133 		return 0;
2134 	}
2135 }
2136 
2137 class RemoveLocalCommand : RegistrationCommand {
2138 	this() @safe pure nothrow
2139 	{
2140 		this.name = "remove-local";
2141 		this.argumentsPattern = "<path>";
2142 		this.description = "Removes a local package directory";
2143 		this.helpText = ["Removes a local package directory"];
2144 	}
2145 
2146 	override int execute(Dub dub, string[] free_args, string[] app_args)
2147 	{
2148 		enforceUsage(free_args.length >= 1, "Missing package path argument.");
2149 		enforceUsage(free_args.length <= 1, "Expected the package path to be the only argument.");
2150 		dub.removeLocalPackage(free_args[0], m_system);
2151 		return 0;
2152 	}
2153 }
2154 
2155 class ListCommand : Command {
2156 	this() @safe pure nothrow
2157 	{
2158 		this.name = "list";
2159 		this.argumentsPattern = "[<package>[@<version-spec>]]";
2160 		this.description = "Prints a list of all or selected local packages dub is aware of";
2161 		this.helpText = [
2162 			"Prints a list of all or selected local packages. This includes all cached "~
2163 			"packages (user or system wide), all packages in the package search paths "~
2164 			"(\"dub add-path\") and all manually registered packages (\"dub add-local\"). "~
2165 			"If a package (and optionally a version spec) is specified, only matching packages are shown."
2166 		];
2167 	}
2168 	override void prepare(scope CommandArgs args) {}
2169 	override int execute(Dub dub, string[] free_args, string[] app_args)
2170 	{
2171 		enforceUsage(free_args.length <= 1, "Expecting zero or one extra arguments.");
2172 		const pinfo = free_args.length ? splitPackageName(free_args[0]) : PackageAndVersion("","*");
2173 		const pname = pinfo.name;
2174 		const pvlim = Dependency(pinfo.version_ == "" ? "*" : pinfo.version_);
2175 		enforceUsage(app_args.length == 0, "The list command supports no application arguments.");
2176 		logInfoNoTag("Packages present in the system and known to dub:");
2177 		foreach (p; dub.packageManager.getPackageIterator()) {
2178 			if ((pname == "" || pname == p.name) && pvlim.matches(p.version_))
2179 				logInfoNoTag("  %s %s: %s", p.name.color(Mode.bold), p.version_, p.path.toNativeString());
2180 		}
2181 		logInfo("");
2182 		return 0;
2183 	}
2184 }
2185 
2186 class SearchCommand : Command {
2187 	this() @safe pure nothrow
2188 	{
2189 		this.name = "search";
2190 		this.argumentsPattern = "<package-name>";
2191 		this.description = "Search for available packages.";
2192 		this.helpText = [
2193 			"Search all specified providers for matching packages."
2194 		];
2195 	}
2196 	override void prepare(scope CommandArgs args) {}
2197 	override int execute(Dub dub, string[] free_args, string[] app_args)
2198 	{
2199 		enforce(free_args.length == 1, "Expected one argument.");
2200 		auto res = dub.searchPackages(free_args[0]);
2201 		if (res.empty)
2202 		{
2203 			logError("No matches found.");
2204 			return 2;
2205 		}
2206 		auto justify = res
2207 			.map!((descNmatches) => descNmatches[1])
2208 			.joiner
2209 			.map!(m => m.name.length + m.version_.length)
2210 			.reduce!max + " ()".length;
2211 		justify += (~justify & 3) + 1; // round to next multiple of 4
2212 		int colorDifference = cast(int)"a".color(Mode.bold).length - 1;
2213 		justify += colorDifference;
2214 		foreach (desc, matches; res)
2215 		{
2216 			logInfoNoTag("==== %s ====", desc);
2217 			foreach (m; matches)
2218 				logInfoNoTag("  %s%s", leftJustify(m.name.color(Mode.bold)
2219 					~ " (" ~ m.version_ ~ ")", justify), m.description);
2220 		}
2221 		return 0;
2222 	}
2223 }
2224 
2225 
2226 /******************************************************************************/
2227 /* OVERRIDES                                                                  */
2228 /******************************************************************************/
2229 
2230 class AddOverrideCommand : Command {
2231 	private {
2232 		bool m_system = false;
2233 	}
2234 
2235 	static immutable string DeprecationMessage =
2236 		"This command is deprecated. Use path based dependency, custom cache path, " ~
2237 		"or edit `dub.selections.json` to achieve the same results.";
2238 
2239 
2240 	this() @safe pure nothrow
2241 	{
2242 		this.name = "add-override";
2243 		this.argumentsPattern = "<package> <version-spec> <target-path/target-version>";
2244 		this.description = "Adds a new package override.";
2245 
2246 		this.hidden = true;
2247 		this.helpText = [ DeprecationMessage ];
2248 	}
2249 
2250 	override void prepare(scope CommandArgs args)
2251 	{
2252 		args.getopt("system", &m_system, [
2253 			"Register system-wide instead of user-wide"
2254 		]);
2255 	}
2256 
2257 	override int execute(Dub dub, string[] free_args, string[] app_args)
2258 	{
2259 		logWarn(DeprecationMessage);
2260 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
2261 		enforceUsage(free_args.length == 3, "Expected three arguments, not "~free_args.length.to!string);
2262 		auto scope_ = m_system ? PlacementLocation.system : PlacementLocation.user;
2263 		auto pack = free_args[0];
2264 		auto source = VersionRange.fromString(free_args[1]);
2265 		if (existsFile(NativePath(free_args[2]))) {
2266 			auto target = NativePath(free_args[2]);
2267 			if (!target.absolute) target = getWorkingDirectory() ~ target;
2268 			dub.packageManager.addOverride_(scope_, pack, source, target);
2269 			logInfo("Added override %s %s => %s", pack, source, target);
2270 		} else {
2271 			auto target = Version(free_args[2]);
2272 			dub.packageManager.addOverride_(scope_, pack, source, target);
2273 			logInfo("Added override %s %s => %s", pack, source, target);
2274 		}
2275 		return 0;
2276 	}
2277 }
2278 
2279 class RemoveOverrideCommand : Command {
2280 	private {
2281 		bool m_system = false;
2282 	}
2283 
2284 	this() @safe pure nothrow
2285 	{
2286 		this.name = "remove-override";
2287 		this.argumentsPattern = "<package> <version-spec>";
2288 		this.description = "Removes an existing package override.";
2289 
2290 		this.hidden = true;
2291 		this.helpText = [ AddOverrideCommand.DeprecationMessage ];
2292 	}
2293 
2294 	override void prepare(scope CommandArgs args)
2295 	{
2296 		args.getopt("system", &m_system, [
2297 			"Register system-wide instead of user-wide"
2298 		]);
2299 	}
2300 
2301 	override int execute(Dub dub, string[] free_args, string[] app_args)
2302 	{
2303 		logWarn(AddOverrideCommand.DeprecationMessage);
2304 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
2305 		enforceUsage(free_args.length == 2, "Expected two arguments, not "~free_args.length.to!string);
2306 		auto scope_ = m_system ? PlacementLocation.system : PlacementLocation.user;
2307 		auto source = VersionRange.fromString(free_args[1]);
2308 		dub.packageManager.removeOverride_(scope_, free_args[0], source);
2309 		return 0;
2310 	}
2311 }
2312 
2313 class ListOverridesCommand : Command {
2314 	this() @safe pure nothrow
2315 	{
2316 		this.name = "list-overrides";
2317 		this.argumentsPattern = "";
2318 		this.description = "Prints a list of all local package overrides";
2319 
2320 		this.hidden = true;
2321 		this.helpText = [ AddOverrideCommand.DeprecationMessage ];
2322 	}
2323 	override void prepare(scope CommandArgs args) {}
2324 	override int execute(Dub dub, string[] free_args, string[] app_args)
2325 	{
2326 		logWarn(AddOverrideCommand.DeprecationMessage);
2327 
2328 		void printList(in PackageOverride_[] overrides, string caption)
2329 		{
2330 			if (overrides.length == 0) return;
2331 			logInfoNoTag("# %s", caption);
2332 			foreach (ovr; overrides)
2333 				ovr.target.match!(
2334 					t => logInfoNoTag("%s %s => %s", ovr.package_.color(Mode.bold), ovr.version_, t));
2335 		}
2336 		printList(dub.packageManager.getOverrides_(PlacementLocation.user), "User wide overrides");
2337 		printList(dub.packageManager.getOverrides_(PlacementLocation.system), "System wide overrides");
2338 		return 0;
2339 	}
2340 }
2341 
2342 /******************************************************************************/
2343 /* Cache cleanup                                                              */
2344 /******************************************************************************/
2345 
2346 class CleanCachesCommand : Command {
2347 	this() @safe pure nothrow
2348 	{
2349 		this.name = "clean-caches";
2350 		this.argumentsPattern = "";
2351 		this.description = "Removes cached metadata";
2352 		this.helpText = [
2353 			"This command removes any cached metadata like the list of available packages and their latest version."
2354 		];
2355 	}
2356 
2357 	override void prepare(scope CommandArgs args) {}
2358 
2359 	override int execute(Dub dub, string[] free_args, string[] app_args)
2360 	{
2361 		return 0;
2362 	}
2363 }
2364 
2365 /******************************************************************************/
2366 /* DUSTMITE                                                                   */
2367 /******************************************************************************/
2368 
2369 class DustmiteCommand : PackageBuildCommand {
2370 	private {
2371 		int m_compilerStatusCode = int.min;
2372 		int m_linkerStatusCode = int.min;
2373 		int m_programStatusCode = int.min;
2374 		string m_compilerRegex;
2375 		string m_linkerRegex;
2376 		string m_programRegex;
2377 		string m_testPackage;
2378 		bool m_noRedirect;
2379 		string m_strategy;
2380 		uint m_jobCount;		// zero means not specified
2381 		bool m_trace;
2382 	}
2383 
2384 	this() @safe pure nothrow
2385 	{
2386 		this.name = "dustmite";
2387 		this.argumentsPattern = "<destination-path>";
2388 		this.acceptsAppArgs = true;
2389 		this.description = "Create reduced test cases for build errors";
2390 		this.helpText = [
2391 			"This command uses the Dustmite utility to isolate the cause of build errors in a DUB project.",
2392 			"",
2393 			"It will create a copy of all involved packages and run dustmite on this copy, leaving a reduced test case.",
2394 			"",
2395 			"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."
2396 		];
2397 	}
2398 
2399 	override void prepare(scope CommandArgs args)
2400 	{
2401 		args.getopt("compiler-status", &m_compilerStatusCode, ["The expected status code of the compiler run"]);
2402 		args.getopt("compiler-regex", &m_compilerRegex, ["A regular expression used to match against the compiler output"]);
2403 		args.getopt("linker-status", &m_linkerStatusCode, ["The expected status code of the linker run"]);
2404 		args.getopt("linker-regex", &m_linkerRegex, ["A regular expression used to match against the linker output"]);
2405 		args.getopt("program-status", &m_programStatusCode, ["The expected status code of the built executable"]);
2406 		args.getopt("program-regex", &m_programRegex, ["A regular expression used to match against the program output"]);
2407 		args.getopt("test-package", &m_testPackage, ["Perform a test run - usually only used internally"]);
2408 		args.getopt("combined", &this.baseSettings.combined, ["Builds multiple packages with one compiler run"]);
2409 		args.getopt("no-redirect", &m_noRedirect, ["Don't redirect stdout/stderr streams of the test command"]);
2410 		args.getopt("strategy", &m_strategy, ["Set strategy (careful/lookback/pingpong/indepth/inbreadth)"]);
2411 		args.getopt("j", &m_jobCount, ["Set number of look-ahead processes"]);
2412 		args.getopt("trace", &m_trace, ["Save all attempted reductions to DIR.trace"]);
2413 		super.prepare(args);
2414 
2415 		// speed up loading when in test mode
2416 		if (m_testPackage.length) {
2417 			m_nodeps = true;
2418 		}
2419 	}
2420 
2421 	/// Returns: A minimally-initialized dub instance in test mode
2422 	override Dub prepareDub(CommonOptions options)
2423 	{
2424 		if (!m_testPackage.length)
2425 			return super.prepareDub(options);
2426 		return new Dub(NativePath(options.root_path), getWorkingDirectory());
2427 	}
2428 
2429 	override int execute(Dub dub, string[] free_args, string[] app_args)
2430 	{
2431 		import std.format : formattedWrite;
2432 
2433 		if (m_testPackage.length) {
2434 			setupPackage(dub, m_testPackage);
2435 			m_defaultConfig = dub.project.getDefaultConfiguration(this.baseSettings.platform);
2436 
2437 			GeneratorSettings gensettings = this.baseSettings;
2438 			if (!gensettings.config.length)
2439 				gensettings.config = m_defaultConfig;
2440 			gensettings.run = m_programStatusCode != int.min || m_programRegex.length;
2441 			gensettings.runArgs = app_args;
2442 			gensettings.force = true;
2443 			gensettings.compileCallback = check(m_compilerStatusCode, m_compilerRegex);
2444 			gensettings.linkCallback = check(m_linkerStatusCode, m_linkerRegex);
2445 			gensettings.runCallback = check(m_programStatusCode, m_programRegex);
2446 			try dub.generateProject("build", gensettings);
2447 			catch (DustmiteMismatchException) {
2448 				logInfoNoTag("Dustmite test doesn't match.");
2449 				return 3;
2450 			}
2451 			catch (DustmiteMatchException) {
2452 				logInfoNoTag("Dustmite test matches.");
2453 				return 0;
2454 			}
2455 		} else {
2456 			enforceUsage(free_args.length == 1, "Expected destination path.");
2457 			auto path = NativePath(free_args[0]);
2458 			path.normalize();
2459 			enforceUsage(!path.empty, "Destination path must not be empty.");
2460 			if (!path.absolute) path = getWorkingDirectory() ~ path;
2461 			enforceUsage(!path.startsWith(dub.rootPath), "Destination path must not be a sub directory of the tested package!");
2462 
2463 			setupPackage(dub, null);
2464 			auto prj = dub.project;
2465 			if (this.baseSettings.config.empty)
2466 				this.baseSettings.config = prj.getDefaultConfiguration(this.baseSettings.platform);
2467 
2468 			void copyFolderRec(NativePath folder, NativePath dstfolder)
2469 			{
2470 				ensureDirectory(dstfolder);
2471 				foreach (de; iterateDirectory(folder.toNativeString())) {
2472 					if (de.name.startsWith(".")) continue;
2473 					if (de.isDirectory) {
2474 						copyFolderRec(folder ~ de.name, dstfolder ~ de.name);
2475 					} else {
2476 						if (de.name.endsWith(".o") || de.name.endsWith(".obj")) continue;
2477 						if (de.name.endsWith(".exe")) continue;
2478 						try copyFile(folder ~ de.name, dstfolder ~ de.name);
2479 						catch (Exception e) {
2480 							logWarn("Failed to copy file %s: %s", (folder ~ de.name).toNativeString(), e.msg);
2481 						}
2482 					}
2483 				}
2484 			}
2485 
2486 			static void fixPathDependency(string pack, ref Dependency dep) {
2487 				dep.visit!(
2488 					(NativePath path) {
2489 						auto mainpack = getBasePackageName(pack);
2490 						dep = Dependency(NativePath("../") ~ mainpack);
2491 					},
2492 					(any) { /* Nothing to do */ },
2493 				);
2494 			}
2495 
2496 			void fixPathDependencies(ref PackageRecipe recipe, NativePath base_path)
2497 			{
2498 				foreach (name, ref dep; recipe.buildSettings.dependencies)
2499 					fixPathDependency(name, dep);
2500 
2501 				foreach (ref cfg; recipe.configurations)
2502 					foreach (name, ref dep; cfg.buildSettings.dependencies)
2503 						fixPathDependency(name, dep);
2504 
2505 				foreach (ref subp; recipe.subPackages)
2506 					if (subp.path.length) {
2507 						auto sub_path = base_path ~ NativePath(subp.path);
2508 						auto pack = prj.packageManager.getOrLoadPackage(sub_path);
2509 						fixPathDependencies(pack.recipe, sub_path);
2510 						pack.storeInfo(sub_path);
2511 					} else fixPathDependencies(subp.recipe, base_path);
2512 			}
2513 
2514 			bool[string] visited;
2515 			foreach (pack_; prj.getTopologicalPackageList()) {
2516 				auto pack = pack_.basePackage;
2517 				if (pack.name in visited) continue;
2518 				visited[pack.name] = true;
2519 				auto dst_path = path ~ pack.name;
2520 				logInfo("Prepare", Color.light_blue, "Copy package %s to destination folder...", pack.name.color(Mode.bold));
2521 				copyFolderRec(pack.path, dst_path);
2522 
2523 				// adjust all path based dependencies
2524 				fixPathDependencies(pack.recipe, dst_path);
2525 
2526 				// overwrite package description file with additional version information
2527 				pack.storeInfo(dst_path);
2528 			}
2529 
2530 			logInfo("Starting", Color.light_green, "Executing dustmite...");
2531 			auto testcmd = appender!string();
2532 			testcmd.formattedWrite("%s dustmite --test-package=%s --build=%s --config=%s",
2533 				thisExePath, prj.name, this.baseSettings.buildType, this.baseSettings.config);
2534 
2535 			if (m_compilerName.length) testcmd.formattedWrite(" \"--compiler=%s\"", m_compilerName);
2536 			if (m_arch.length) testcmd.formattedWrite(" --arch=%s", m_arch);
2537 			if (m_compilerStatusCode != int.min) testcmd.formattedWrite(" --compiler-status=%s", m_compilerStatusCode);
2538 			if (m_compilerRegex.length) testcmd.formattedWrite(" \"--compiler-regex=%s\"", m_compilerRegex);
2539 			if (m_linkerStatusCode != int.min) testcmd.formattedWrite(" --linker-status=%s", m_linkerStatusCode);
2540 			if (m_linkerRegex.length) testcmd.formattedWrite(" \"--linker-regex=%s\"", m_linkerRegex);
2541 			if (m_programStatusCode != int.min) testcmd.formattedWrite(" --program-status=%s", m_programStatusCode);
2542 			if (m_programRegex.length) testcmd.formattedWrite(" \"--program-regex=%s\"", m_programRegex);
2543 			if (this.baseSettings.combined) testcmd ~= " --combined";
2544 
2545 			// --vquiet swallows dustmite's output ...
2546 			if (!m_noRedirect) testcmd ~= " --vquiet";
2547 
2548 			// TODO: pass *all* original parameters
2549 			logDiagnostic("Running dustmite: %s", testcmd);
2550 
2551 			string[] extraArgs;
2552 			if (m_noRedirect) extraArgs ~= "--no-redirect";
2553 			if (m_strategy.length) extraArgs ~= "--strategy=" ~ m_strategy;
2554 			if (m_jobCount) extraArgs ~= "-j" ~ m_jobCount.to!string;
2555 			if (m_trace) extraArgs ~= "--trace";
2556 
2557 			const cmd = "dustmite" ~ extraArgs ~ [path.toNativeString(), testcmd.data];
2558 			auto dmpid = spawnProcess(cmd);
2559 			return dmpid.wait();
2560 		}
2561 		return 0;
2562 	}
2563 
2564 	void delegate(int, string) check(int code_match, string regex_match)
2565 	{
2566 		return (code, output) {
2567 			import std.encoding;
2568 			import std.regex;
2569 
2570 			logInfo("%s", output);
2571 
2572 			if (code_match != int.min && code != code_match) {
2573 				logInfo("Exit code %s doesn't match expected value %s", code, code_match);
2574 				throw new DustmiteMismatchException;
2575 			}
2576 
2577 			if (regex_match.length > 0 && !match(output.sanitize, regex_match)) {
2578 				logInfo("Output doesn't match regex:");
2579 				logInfo("%s", output);
2580 				throw new DustmiteMismatchException;
2581 			}
2582 
2583 			if (code != 0 && code_match != int.min || regex_match.length > 0) {
2584 				logInfo("Tool failed, but matched either exit code or output - counting as match.");
2585 				throw new DustmiteMatchException;
2586 			}
2587 		};
2588 	}
2589 
2590 	static class DustmiteMismatchException : Exception {
2591 		this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null)
2592 		{
2593 			super(message, file, line, next);
2594 		}
2595 	}
2596 
2597 	static class DustmiteMatchException : Exception {
2598 		this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null)
2599 		{
2600 			super(message, file, line, next);
2601 		}
2602 	}
2603 }
2604 
2605 
2606 /******************************************************************************/
2607 /* CONVERT command                                                               */
2608 /******************************************************************************/
2609 
2610 class ConvertCommand : Command {
2611 	private {
2612 		string m_format;
2613 		bool m_stdout;
2614 	}
2615 
2616 	this() @safe pure nothrow
2617 	{
2618 		this.name = "convert";
2619 		this.argumentsPattern = "";
2620 		this.description = "Converts the file format of the package recipe.";
2621 		this.helpText = [
2622 			"This command will convert between JSON and SDLang formatted package recipe files.",
2623 			"",
2624 			"Warning: Beware that any formatting and comments within the package recipe will get lost in the conversion process."
2625 		];
2626 	}
2627 
2628 	override void prepare(scope CommandArgs args)
2629 	{
2630 		args.getopt("f|format", &m_format, ["Specifies the target package recipe format. Possible values:", "  json, sdl"]);
2631 		args.getopt("s|stdout", &m_stdout, ["Outputs the converted package recipe to stdout instead of writing to disk."]);
2632 	}
2633 
2634 	override int execute(Dub dub, string[] free_args, string[] app_args)
2635 	{
2636 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
2637 		enforceUsage(free_args.length == 0, "Unexpected arguments: "~free_args.join(" "));
2638 		enforceUsage(m_format.length > 0, "Missing target format file extension (--format=...).");
2639 		if (!loadCwdPackage(dub, true)) return 2;
2640 		dub.convertRecipe(m_format, m_stdout);
2641 		return 0;
2642 	}
2643 }
2644 
2645 
2646 /******************************************************************************/
2647 /* HELP                                                                       */
2648 /******************************************************************************/
2649 
2650 private {
2651 	enum shortArgColumn = 2;
2652 	enum longArgColumn = 6;
2653 	enum descColumn = 24;
2654 	enum lineWidth = 80 - 1;
2655 }
2656 
2657 private void showHelp(in CommandGroup[] commands, CommandArgs common_args)
2658 {
2659 	writeln(
2660 `USAGE: dub [--version] [<command>] [<options...>] [-- [<application arguments...>]]
2661 
2662 Manages the DUB project in the current directory. If the command is omitted,
2663 DUB will default to "run". When running an application, "--" can be used to
2664 separate DUB options from options passed to the application.
2665 
2666 Run "dub <command> --help" to get help for a specific command.
2667 
2668 You can use the "http_proxy" environment variable to configure a proxy server
2669 to be used for fetching packages.
2670 
2671 
2672 Available commands
2673 ==================`);
2674 
2675 	foreach (grp; commands) {
2676 		writeln();
2677 		writeWS(shortArgColumn);
2678 		writeln(grp.caption);
2679 		writeWS(shortArgColumn);
2680 		writerep!'-'(grp.caption.length);
2681 		writeln();
2682 		foreach (cmd; grp.commands) {
2683 			if (cmd.hidden) continue;
2684 			writeWS(shortArgColumn);
2685 			writef("%s %s", cmd.name, cmd.argumentsPattern);
2686 			auto chars_output = cmd.name.length + cmd.argumentsPattern.length + shortArgColumn + 1;
2687 			if (chars_output < descColumn) {
2688 				writeWS(descColumn - chars_output);
2689 			} else {
2690 				writeln();
2691 				writeWS(descColumn);
2692 			}
2693 			writeWrapped(cmd.description, descColumn, descColumn);
2694 		}
2695 	}
2696 	writeln();
2697 	writeln();
2698 	writeln(`Common options`);
2699 	writeln(`==============`);
2700 	writeln();
2701 	writeOptions(common_args);
2702 	writeln();
2703 	showVersion();
2704 }
2705 
2706 private void showVersion()
2707 {
2708 	writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__);
2709 }
2710 
2711 private void showCommandHelp(Command cmd, CommandArgs args, CommandArgs common_args)
2712 {
2713 	writefln(`USAGE: dub %s %s [<options...>]%s`, cmd.name, cmd.argumentsPattern, cmd.acceptsAppArgs ? " [-- <application arguments...>]": null);
2714 	writeln();
2715 	foreach (ln; cmd.helpText)
2716 		ln.writeWrapped();
2717 
2718 	if (args.recognizedArgs.length) {
2719 		writeln();
2720 		writeln();
2721 		writeln("Command specific options");
2722 		writeln("========================");
2723 		writeln();
2724 		writeOptions(args);
2725 	}
2726 
2727 	writeln();
2728 	writeln();
2729 	writeln("Common options");
2730 	writeln("==============");
2731 	writeln();
2732 	writeOptions(common_args);
2733 	writeln();
2734 	writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__);
2735 }
2736 
2737 private void writeOptions(CommandArgs args)
2738 {
2739 	foreach (arg; args.recognizedArgs) {
2740 		if (arg.hidden) continue;
2741 		auto names = arg.names.split("|");
2742 		assert(names.length == 1 || names.length == 2);
2743 		string sarg = names[0].length == 1 ? names[0] : null;
2744 		string larg = names[0].length > 1 ? names[0] : names.length > 1 ? names[1] : null;
2745 		if (sarg !is null) {
2746 			writeWS(shortArgColumn);
2747 			writef("-%s", sarg);
2748 			writeWS(longArgColumn - shortArgColumn - 2);
2749 		} else writeWS(longArgColumn);
2750 		size_t col = longArgColumn;
2751 		if (larg !is null) {
2752 			arg.defaultValue.match!(
2753 				(bool b) {
2754 					writef("--%s", larg);
2755 					col += larg.length + 2;
2756 				},
2757 				(_) {
2758 					writef("--%s=VALUE", larg);
2759 					col += larg.length + 8;
2760 				}
2761 			);
2762 		}
2763 		if (col < descColumn) {
2764 			writeWS(descColumn - col);
2765 		} else {
2766 			writeln();
2767 			writeWS(descColumn);
2768 		}
2769 		foreach (i, ln; arg.helpText) {
2770 			if (i > 0) writeWS(descColumn);
2771 			ln.writeWrapped(descColumn, descColumn);
2772 		}
2773 	}
2774 }
2775 
2776 private void writeWrapped(string string, size_t indent = 0, size_t first_line_pos = 0)
2777 {
2778 	// handle pre-indented strings and bullet lists
2779 	size_t first_line_indent = 0;
2780 	while (string.startsWith(" ")) {
2781 		string = string[1 .. $];
2782 		indent++;
2783 		first_line_indent++;
2784 	}
2785 	if (string.startsWith("- ")) indent += 2;
2786 
2787 	auto wrapped = string.wrap(lineWidth, getRepString!' '(first_line_pos+first_line_indent), getRepString!' '(indent));
2788 	wrapped = wrapped[first_line_pos .. $];
2789 	foreach (ln; wrapped.splitLines())
2790 		writeln(ln);
2791 }
2792 
2793 private void writeWS(size_t num) { writerep!' '(num); }
2794 private void writerep(char ch)(size_t num) { write(getRepString!ch(num)); }
2795 
2796 private string getRepString(char ch)(size_t len)
2797 {
2798 	static string buf;
2799 	if (len > buf.length) buf ~= [ch].replicate(len-buf.length);
2800 	return buf[0 .. len];
2801 }
2802 
2803 /***
2804 */
2805 
2806 
2807 private void enforceUsage(bool cond, string text)
2808 {
2809 	if (!cond) throw new UsageException(text);
2810 }
2811 
2812 private class UsageException : Exception {
2813 	this(string message, string file = __FILE__, int line = __LINE__, Throwable next = null)
2814 	{
2815 		super(message, file, line, next);
2816 	}
2817 }
2818 
2819 private bool addDependency(Dub dub, ref PackageRecipe recipe, string depspec)
2820 {
2821 	Dependency dep;
2822 	const parts = splitPackageName(depspec);
2823 	const depname = parts.name;
2824 	if (parts.version_)
2825 		dep = Dependency(parts.version_);
2826 	else
2827 	{
2828 		try {
2829 			const ver = dub.getLatestVersion(depname);
2830 			dep = ver.isBranch ? Dependency(ver) : Dependency("~>" ~ ver.toString());
2831 		} catch (Exception e) {
2832 			logError("Could not find package '%s'.", depname);
2833 			logDebug("Full error: %s", e.toString().sanitize);
2834 			return false;
2835 		}
2836 	}
2837 	recipe.buildSettings.dependencies[depname] = dep;
2838 	logInfo("Adding dependency %s %s", depname, dep.toString());
2839 	return true;
2840 }
2841 
2842 private struct PackageAndVersion
2843 {
2844 	string name;
2845 	string version_;
2846 }
2847 
2848 /* Split <package>=<version-specifier> and <package>@<version-specifier>
2849    into `name` and `version_`. */
2850 private PackageAndVersion splitPackageName(string packageName)
2851 {
2852 	// split <package>@<version-specifier>
2853 	auto parts = packageName.findSplit("@");
2854 	if (parts[1].empty) {
2855 		// split <package>=<version-specifier>
2856 		parts = packageName.findSplit("=");
2857 	}
2858 
2859 	PackageAndVersion p;
2860 	p.name = parts[0];
2861 	if (!parts[1].empty)
2862 		p.version_ = parts[2];
2863 	return p;
2864 }
2865 
2866 unittest
2867 {
2868 	// https://github.com/dlang/dub/issues/1681
2869 	assert(splitPackageName("") == PackageAndVersion("", null));
2870 
2871 	assert(splitPackageName("foo") == PackageAndVersion("foo", null));
2872 	assert(splitPackageName("foo=1.0.1") == PackageAndVersion("foo", "1.0.1"));
2873 	assert(splitPackageName("foo@1.0.1") == PackageAndVersion("foo", "1.0.1"));
2874 	assert(splitPackageName("foo@==1.0.1") == PackageAndVersion("foo", "==1.0.1"));
2875 	assert(splitPackageName("foo@>=1.0.1") == PackageAndVersion("foo", ">=1.0.1"));
2876 	assert(splitPackageName("foo@~>1.0.1") == PackageAndVersion("foo", "~>1.0.1"));
2877 	assert(splitPackageName("foo@<1.0.1") == PackageAndVersion("foo", "<1.0.1"));
2878 }
2879 
2880 private ulong canFindVersionSplitter(string packageName)
2881 {
2882 	// see splitPackageName
2883 	return packageName.canFind("@", "=");
2884 }
2885 
2886 unittest
2887 {
2888 	assert(!canFindVersionSplitter("foo"));
2889 	assert(canFindVersionSplitter("foo=1.0.1"));
2890 	assert(canFindVersionSplitter("foo@1.0.1"));
2891 	assert(canFindVersionSplitter("foo@==1.0.1"));
2892 	assert(canFindVersionSplitter("foo@>=1.0.1"));
2893 	assert(canFindVersionSplitter("foo@~>1.0.1"));
2894 	assert(canFindVersionSplitter("foo@<1.0.1"));
2895 }