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