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