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