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