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