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