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.data.json; 16 import dub.internal.vibecompat.inet.path; 17 import dub.internal.logging; 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 dub.internal.dyaml.stdsumtype; 25 26 import std.algorithm; 27 import std.array; 28 import std.conv; 29 import std.encoding; 30 import std.exception; 31 static import std.file; 32 import std.getopt; 33 import std.path : absolutePath, buildNormalizedPath, expandTilde, setExtension; 34 import std.process : environment, spawnProcess, wait; 35 import std.stdio; 36 import std.string; 37 import std.traits : EnumMembers; 38 import std.typecons : Nullable, nullable, Tuple, tuple; 39 40 /** Retrieves a list of all available commands. 41 42 Commands are grouped by category. 43 */ 44 CommandGroup[] getCommands() @safe pure nothrow 45 { 46 return [ 47 CommandGroup("Package creation", 48 new InitCommand 49 ), 50 CommandGroup("Build, test and run", 51 new RunCommand, 52 new BuildCommand, 53 new TestCommand, 54 new LintCommand, 55 new GenerateCommand, 56 new DescribeCommand, 57 new CleanCommand, 58 new DustmiteCommand 59 ), 60 CommandGroup("Package management", 61 new FetchCommand, 62 new AddCommand, 63 new RemoveCommand, 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 /** Extract the command name from the argument list 81 82 Params: 83 args = a list of string arguments that will be processed 84 85 Returns: 86 The command name that was found (may be null). 87 */ 88 string commandNameArgument(ref string[] args) 89 { 90 if (args.length >= 1 && !args[0].startsWith("-") && !args[0].canFind(":")) { 91 const result = args[0]; 92 args = args[1 .. $]; 93 return result; 94 } 95 return null; 96 } 97 98 /// test extractCommandNameArgument usage 99 unittest { 100 { 101 string[] args; 102 /// It returns an empty string on when there are no args 103 assert(commandNameArgument(args) is null); 104 assert(!args.length); 105 } 106 107 { 108 string[] args = [ "test" ]; 109 /// It returns the first argument when it does not start with `-` 110 assert(commandNameArgument(args) == "test"); 111 /// There is nothing to extract when the arguments only contain the `test` cmd 112 assert(!args.length); 113 } 114 115 { 116 string[] args = [ "-a", "-b" ]; 117 /// It extracts two arguments when they are not a command 118 assert(commandNameArgument(args) is null); 119 assert(args == ["-a", "-b"]); 120 } 121 122 { 123 string[] args = [ "-test" ]; 124 /// It returns the an empty string when it starts with `-` 125 assert(commandNameArgument(args) is null); 126 assert(args.length == 1); 127 } 128 129 { 130 string[] args = [ "foo:bar" ]; 131 // Sub package names are ignored as command names 132 assert(commandNameArgument(args) is null); 133 assert(args.length == 1); 134 args[0] = ":foo"; 135 assert(commandNameArgument(args) is null); 136 assert(args.length == 1); 137 } 138 } 139 140 /** Handles the Command Line options and commands. 141 */ 142 struct CommandLineHandler 143 { 144 /// The list of commands that can be handled 145 CommandGroup[] commandGroups; 146 147 /// General options parser 148 CommonOptions options; 149 150 /** Create the list of all supported commands 151 152 Returns: 153 Returns the list of the supported command names 154 */ 155 string[] commandNames() 156 { 157 return commandGroups.map!(g => g.commands).joiner.map!(c => c.name).array; 158 } 159 160 /** Parses the general options and sets up the log level 161 and the root_path 162 */ 163 string[] prepareOptions(CommandArgs args) { 164 LogLevel loglevel = LogLevel.info; 165 166 options.prepare(args); 167 168 if (options.vverbose) loglevel = LogLevel.debug_; 169 else if (options.verbose) loglevel = LogLevel.diagnostic; 170 else if (options.vquiet) loglevel = LogLevel.none; 171 else if (options.quiet) loglevel = LogLevel.warn; 172 else if (options.verror) loglevel = LogLevel.error; 173 setLogLevel(loglevel); 174 175 if (options.root_path.empty) 176 { 177 options.root_path = std.file.getcwd(); 178 } 179 else 180 { 181 options.root_path = options.root_path.expandTilde.absolutePath.buildNormalizedPath; 182 } 183 184 final switch (options.colorMode) with (options.Color) 185 { 186 case automatic: 187 // Use default determined in internal.logging.initLogging(). 188 break; 189 case on: 190 foreach (ref grp; commandGroups) 191 foreach (ref cmd; grp.commands) 192 if (auto pc = cast(PackageBuildCommand)cmd) 193 pc.baseSettings.buildSettings.options |= BuildOption.color; 194 setLoggingColorsEnabled(true); // enable colors, no matter what 195 break; 196 case off: 197 foreach (ref grp; commandGroups) 198 foreach (ref cmd; grp.commands) 199 if (auto pc = cast(PackageBuildCommand)cmd) 200 pc.baseSettings.buildSettings.options &= ~BuildOption.color; 201 setLoggingColorsEnabled(false); // disable colors, no matter what 202 break; 203 } 204 return args.extractAllRemainingArgs(); 205 } 206 207 /** Get an instance of the requested command. 208 209 If there is no command in the argument list, the `run` command is returned 210 by default. 211 212 If the `--help` argument previously handled by `prepareOptions`, 213 `this.options.help` is already `true`, with this returning the requested 214 command. If no command was requested (just dub --help) this returns the 215 help command. 216 217 Params: 218 name = the command name 219 220 Returns: 221 Returns the command instance if it exists, null otherwise 222 */ 223 Command getCommand(string name) { 224 if (name == "help" || (name == "" && options.help)) 225 { 226 return new HelpCommand(); 227 } 228 229 if (name == "") 230 { 231 name = "run"; 232 } 233 234 foreach (grp; commandGroups) 235 foreach (c; grp.commands) 236 if (c.name == name) { 237 return c; 238 } 239 240 return null; 241 } 242 243 /** Get an instance of the requested command after the args are sent. 244 245 It uses getCommand to get the command instance and then calls prepare. 246 247 Params: 248 name = the command name 249 args = the command arguments 250 251 Returns: 252 Returns the command instance if it exists, null otherwise 253 */ 254 Command prepareCommand(string name, CommandArgs args) { 255 auto cmd = getCommand(name); 256 257 if (cmd !is null && !(cast(HelpCommand)cmd)) 258 { 259 // process command line options for the selected command 260 cmd.prepare(args); 261 enforceUsage(cmd.acceptsAppArgs || !args.hasAppArgs, name ~ " doesn't accept application arguments."); 262 } 263 264 return cmd; 265 } 266 } 267 268 /// Can get the command names 269 unittest { 270 CommandLineHandler handler; 271 handler.commandGroups = getCommands(); 272 273 assert(handler.commandNames == ["init", "run", "build", "test", "lint", "generate", 274 "describe", "clean", "dustmite", "fetch", "add", "remove", 275 "upgrade", "add-path", "remove-path", "add-local", "remove-local", "list", "search", 276 "add-override", "remove-override", "list-overrides", "clean-caches", "convert"]); 277 } 278 279 /// It sets the cwd as root_path by default 280 unittest { 281 CommandLineHandler handler; 282 283 auto args = new CommandArgs([]); 284 handler.prepareOptions(args); 285 assert(handler.options.root_path == std.file.getcwd()); 286 } 287 288 /// It can set a custom root_path 289 unittest { 290 CommandLineHandler handler; 291 292 auto args = new CommandArgs(["--root=/tmp/test"]); 293 handler.prepareOptions(args); 294 assert(handler.options.root_path == "/tmp/test".absolutePath.buildNormalizedPath); 295 296 args = new CommandArgs(["--root=./test"]); 297 handler.prepareOptions(args); 298 assert(handler.options.root_path == "./test".absolutePath.buildNormalizedPath); 299 } 300 301 /// It sets the info log level by default 302 unittest { 303 scope(exit) setLogLevel(LogLevel.info); 304 CommandLineHandler handler; 305 306 auto args = new CommandArgs([]); 307 handler.prepareOptions(args); 308 assert(getLogLevel() == LogLevel.info); 309 } 310 311 /// It can set a custom error level 312 unittest { 313 scope(exit) setLogLevel(LogLevel.info); 314 CommandLineHandler handler; 315 316 auto args = new CommandArgs(["--vverbose"]); 317 handler.prepareOptions(args); 318 assert(getLogLevel() == LogLevel.debug_); 319 320 handler = CommandLineHandler(); 321 args = new CommandArgs(["--verbose"]); 322 handler.prepareOptions(args); 323 assert(getLogLevel() == LogLevel.diagnostic); 324 325 handler = CommandLineHandler(); 326 args = new CommandArgs(["--vquiet"]); 327 handler.prepareOptions(args); 328 assert(getLogLevel() == LogLevel.none); 329 330 handler = CommandLineHandler(); 331 args = new CommandArgs(["--quiet"]); 332 handler.prepareOptions(args); 333 assert(getLogLevel() == LogLevel.warn); 334 335 handler = CommandLineHandler(); 336 args = new CommandArgs(["--verror"]); 337 handler.prepareOptions(args); 338 assert(getLogLevel() == LogLevel.error); 339 } 340 341 /// It returns the `run` command by default 342 unittest { 343 CommandLineHandler handler; 344 handler.commandGroups = getCommands(); 345 assert(handler.getCommand("").name == "run"); 346 } 347 348 /// It returns the `help` command when there is none set and the --help arg 349 /// was set 350 unittest { 351 CommandLineHandler handler; 352 auto args = new CommandArgs(["--help"]); 353 handler.prepareOptions(args); 354 handler.commandGroups = getCommands(); 355 assert(cast(HelpCommand)handler.getCommand("") !is null); 356 } 357 358 /// It returns the `help` command when the `help` command is sent 359 unittest { 360 CommandLineHandler handler; 361 handler.commandGroups = getCommands(); 362 assert(cast(HelpCommand) handler.getCommand("help") !is null); 363 } 364 365 /// It returns the `init` command when the `init` command is sent 366 unittest { 367 CommandLineHandler handler; 368 handler.commandGroups = getCommands(); 369 assert(handler.getCommand("init").name == "init"); 370 } 371 372 /// It returns null when a missing command is sent 373 unittest { 374 CommandLineHandler handler; 375 handler.commandGroups = getCommands(); 376 assert(handler.getCommand("missing") is null); 377 } 378 379 /** Processes the given command line and executes the appropriate actions. 380 381 Params: 382 args = This command line argument array as received in `main`. The first 383 entry is considered to be the name of the binary invoked. 384 385 Returns: 386 Returns the exit code that is supposed to be returned to the system. 387 */ 388 int runDubCommandLine(string[] args) 389 { 390 static string[] toSinglePackageArgs (string args0, string file, string[] trailing) 391 { 392 return [args0, "run", "-q", "--temp-build", "--single", file, "--"] ~ trailing; 393 } 394 395 // Initialize the logging module, ensure that whether stdout/stderr are a TTY 396 // or not is detected in order to disable colors if the output isn't a console 397 initLogging(); 398 399 logDiagnostic("DUB version %s", getDUBVersion()); 400 401 { 402 version(Windows) { 403 // Guarantee that this environment variable is set 404 // this is specifically needed because of the Windows fix that follows this statement. 405 // While it probably isn't needed for all targets, it does simplify things a bit. 406 // Question is can it be more generic? Probably not due to $TMP 407 if ("TEMP" !in environment) 408 environment["TEMP"] = std.file.tempDir(); 409 410 // rdmd uses $TEMP to compute a temporary path. since cygwin substitutes backslashes 411 // with slashes, this causes OPTLINK to fail (it thinks path segments are options) 412 // we substitute the other way around here to fix this. 413 414 // In case the environment variable TEMP is empty (it should never be), we'll swap out 415 // opIndex in favor of get with the fallback. 416 417 environment["TEMP"] = environment.get("TEMP", null).replace("/", "\\"); 418 } 419 } 420 421 auto handler = CommandLineHandler(getCommands()); 422 423 // Special syntaxes need to be handled before regular argument parsing 424 if (args.length >= 2) 425 { 426 // Read input source code from stdin 427 if (args[1] == "-") 428 { 429 auto path = getTempFile("app", ".d"); 430 stdin.byChunk(4096).joiner.toFile(path.toNativeString()); 431 args = toSinglePackageArgs(args[0], path.toNativeString(), args[2 .. $]); 432 } 433 434 // Dub has a shebang syntax to be able to use it as script, e.g. 435 // #/usr/bin/env dub 436 // With this approach, we need to support the file having 437 // both the `.d` extension, or having none at all. 438 // We also need to make sure arguments passed to the script 439 // are passed to the program, not `dub`, e.g.: 440 // ./my_dub_script foo bar 441 // Gives us `args = [ "dub", "./my_dub_script" "foo", "bar" ]`, 442 // which we need to interpret as: 443 // `args = [ "dub", "./my_dub_script", "--", "foo", "bar" ]` 444 else if (args[1].endsWith(".d")) 445 args = toSinglePackageArgs(args[0], args[1], args[2 .. $]); 446 447 // Here we have a problem: What if the script name is a command name ? 448 // We have to assume it isn't, and to reduce the risk of false positive 449 // we only consider the case where the file name is the first argument, 450 // as the shell invocation cannot be controlled. 451 else if (handler.getCommand(args[1]) is null && !args[1].startsWith("-")) { 452 if (std.file.exists(args[1])) { 453 auto path = getTempFile("app", ".d"); 454 std.file.copy(args[1], path.toNativeString()); 455 args = toSinglePackageArgs(args[0], path.toNativeString(), args[2 .. $]); 456 } else if (std.file.exists(args[1].setExtension(".d"))) { 457 args = toSinglePackageArgs(args[0], args[1].setExtension(".d"), args[2 .. $]); 458 } 459 } 460 } 461 462 auto common_args = new CommandArgs(args[1..$]); 463 464 try 465 args = handler.prepareOptions(common_args); 466 catch (Exception e) { 467 logError("Error processing arguments: %s", e.msg); 468 logDiagnostic("Full exception: %s", e.toString().sanitize); 469 logInfo("Run 'dub help' for usage information."); 470 return 1; 471 } 472 473 if (handler.options.version_) 474 { 475 showVersion(); 476 return 0; 477 } 478 479 const command_name = commandNameArgument(args); 480 auto command_args = new CommandArgs(args); 481 Command cmd; 482 483 try { 484 cmd = handler.prepareCommand(command_name, command_args); 485 } catch (Exception e) { 486 logError("Error processing arguments: %s", e.msg); 487 logDiagnostic("Full exception: %s", e.toString().sanitize); 488 logInfo("Run 'dub help' for usage information."); 489 return 1; 490 } 491 492 if (cmd is null) { 493 logInfoNoTag("USAGE: dub [--version] [<command>] [<options...>] [-- [<application arguments...>]]"); 494 logInfoNoTag(""); 495 logError("Unknown command: %s", command_name); 496 import std.algorithm.iteration : filter; 497 import std.uni : toUpper; 498 foreach (CommandGroup key; handler.commandGroups) 499 { 500 auto similarCommands = key.commands.filter!(cmd => levenshteinDistance(command_name, cmd.name) < 4).array(); 501 if (similarCommands) { 502 sort!((a, b) => levenshteinDistance(command_name, a.name) < levenshteinDistance( 503 command_name, b.name))(similarCommands); 504 foreach (command; similarCommands) { 505 logInfo("Did you mean '%s'?", command.name); 506 } 507 } 508 } 509 510 logInfoNoTag(""); 511 return 1; 512 } 513 514 if (cast(HelpCommand)cmd !is null) { 515 showHelp(handler.commandGroups, common_args); 516 return 0; 517 } 518 519 if (handler.options.help) { 520 showCommandHelp(cmd, command_args, common_args); 521 return 0; 522 } 523 524 auto remaining_args = command_args.extractRemainingArgs(); 525 if (remaining_args.any!(a => a.startsWith("-"))) { 526 logError("Unknown command line flags: %s", remaining_args.filter!(a => a.startsWith("-")).array.join(" ").color(Mode.bold)); 527 logInfo(`Type "%s" to get a list of all supported flags.`, text("dub ", cmd.name, " -h").color(Mode.bold)); 528 return 1; 529 } 530 531 import dub.internal.configy.exceptions : ConfigException; 532 533 try { 534 // initialize the root package 535 Dub dub = cmd.prepareDub(handler.options); 536 537 // execute the command 538 return cmd.execute(dub, remaining_args, command_args.appArgs); 539 } 540 catch (UsageException e) { 541 // usage exceptions get thrown before any logging, so we are 542 // making the errors more narrow to better fit on small screens. 543 tagWidth.push(5); 544 logError("%s", e.message); 545 logDebug("Full exception: %s", e.toString().sanitize); 546 logInfo(`Run "%s" for more information about the "%s" command.`, 547 text("dub ", cmd.name, " -h").color(Mode.bold), cmd.name.color(Mode.bold)); 548 return 1; 549 } 550 catch (ConfigException exc) { 551 logError("%S", exc); 552 logDiagnostic("%s", exc.stackTraceToString()); 553 return 1; 554 } 555 catch (Exception e) { 556 // most exceptions get thrown before logging, so same thing here as 557 // above. However this might be subject to change if it results in 558 // weird behavior anywhere. 559 tagWidth.push(5); 560 logError("%s", e.message); 561 logDebug("Full exception: %s", e.toString().sanitize); 562 return 2; 563 } 564 } 565 566 567 /** Contains and parses options common to all commands. 568 */ 569 struct CommonOptions { 570 bool verbose, vverbose, quiet, vquiet, verror, version_; 571 bool help, annotate, bare; 572 string[] registry_urls; 573 string root_path, recipeFile; 574 enum Color { automatic, on, off } 575 Color colorMode = Color.automatic; 576 Nullable!SkipPackageSuppliers skipRegistry; 577 PlacementLocation placementLocation = PlacementLocation.user; 578 579 private void parseColor(string option, string value) @safe 580 { 581 // `automatic`, `on`, `off` are there for backwards compatibility 582 // `auto`, `always`, `never` is being used for compatibility with most 583 // other development and linux tools, after evaluating what other tools 584 // are doing, to help users intuitively pick correct values. 585 // See https://github.com/dlang/dub/issues/2410 for discussion 586 if (!value.length || value == "auto" || value == "automatic") 587 colorMode = Color.automatic; 588 else if (value == "always" || value == "on") 589 colorMode = Color.on; 590 else if (value == "never" || value == "off") 591 colorMode = Color.off; 592 else 593 throw new ConvException("Unable to parse argument '--" ~ option ~ "=" ~ value 594 ~ "', supported values: --color[=auto], --color=always, --color=never"); 595 } 596 597 private void parseSkipRegistry(string option, string value) @safe 598 { 599 // We only want to support `none`, `standard`, `configured`, and `all`. 600 // We use a separate function to prevent getopt from parsing SkipPackageSuppliers.default_. 601 assert(option == "skip-registry", 602 "parseSkipRegistry called with unknown option '" ~ option ~ "'"); 603 switch (value) with (SkipPackageSuppliers) { 604 case "none": 605 skipRegistry = none; 606 break; 607 case "standard": 608 skipRegistry = standard; 609 break; 610 case "configured": 611 skipRegistry = configured; 612 break; 613 case "all": 614 skipRegistry = all; 615 break; 616 default: 617 throw new GetOptException("skip-registry only accepts 'none', 'standard', 'configured', and 'all', not '" ~ value ~ "'"); 618 } 619 } 620 621 /// Parses all common options and stores the result in the struct instance. 622 void prepare(CommandArgs args) 623 { 624 args.getopt("h|help", &help, ["Display general or command specific help"]); 625 args.getopt("root", &root_path, ["Path to operate in instead of the current working dir"]); 626 args.getopt("recipe", &recipeFile, ["Loads a custom recipe path instead of dub.json/dub.sdl"]); 627 args.getopt("registry", ®istry_urls, [ 628 "Search the given registry URL first when resolving dependencies. Can be specified multiple times. Available registry types:", 629 " DUB: URL to DUB registry (default)", 630 " Maven: URL to Maven repository + group id containing dub packages as artifacts. E.g. mvn+http://localhost:8040/maven/libs-release/dubpackages", 631 ]); 632 string skipRegistryValue; 633 args.getopt("skip-registry", &skipRegistryValue, &parseSkipRegistry, [ 634 "Sets a mode for skipping the search on certain package registry types:", 635 " none: Search all configured or default registries (default)", 636 " standard: Don't search the main registry (e.g. "~defaultRegistryURLs[0]~")", 637 " configured: Skip all default and user configured registries", 638 " all: Only search registries specified with --registry", 639 ]); 640 args.getopt("annotate", &annotate, ["Do not perform any action, just print what would be done"]); 641 args.getopt("bare", &bare, ["Read only packages contained in the current directory"]); 642 args.getopt("v|verbose", &verbose, ["Print diagnostic output"]); 643 args.getopt("vverbose", &vverbose, ["Print debug output"]); 644 args.getopt("q|quiet", &quiet, ["Only print warnings and errors"]); 645 args.getopt("verror", &verror, ["Only print errors"]); 646 args.getopt("vquiet", &vquiet, ["Print no messages"]); 647 args.getopt("color", &colorMode, &parseColor, [ 648 "Configure colored output. Accepted values:", 649 " auto: Colored output on console/terminal,", 650 " unless NO_COLOR is set and non-empty (default)", 651 " always: Force colors enabled", 652 " never: Force colors disabled" 653 ]); 654 args.getopt("cache", &placementLocation, ["Puts any fetched packages in the specified location [local|system|user]."]); 655 656 version_ = args.hasAppVersion; 657 } 658 } 659 660 /** Encapsulates a set of application arguments. 661 662 This class serves two purposes. The first is to provide an API for parsing 663 command line arguments (`getopt`). At the same time it records all calls 664 to `getopt` and provides a list of all possible options using the 665 `recognizedArgs` property. 666 */ 667 class CommandArgs { 668 struct Arg { 669 alias Value = SumType!(string[], string, bool, int, uint); 670 671 Value defaultValue; 672 Value value; 673 string names; 674 string[] helpText; 675 bool hidden; 676 } 677 private { 678 string[] m_args; 679 Arg[] m_recognizedArgs; 680 string[] m_appArgs; 681 } 682 683 /** Initializes the list of source arguments. 684 685 Note that all array entries are considered application arguments (i.e. 686 no application name entry is present as the first entry) 687 */ 688 this(string[] args) @safe pure nothrow 689 { 690 auto app_args_idx = args.countUntil("--"); 691 692 m_appArgs = app_args_idx >= 0 ? args[app_args_idx+1 .. $] : []; 693 m_args = "dummy" ~ (app_args_idx >= 0 ? args[0..app_args_idx] : args); 694 } 695 696 /** Checks if the app arguments are present. 697 698 Returns: 699 true if an -- argument is given with arguments after it, otherwise false 700 */ 701 @property bool hasAppArgs() { return m_appArgs.length > 0; } 702 703 704 /** Checks if the `--version` argument is present on the first position in 705 the list. 706 707 Returns: 708 true if the application version argument was found on the first position 709 */ 710 @property bool hasAppVersion() { return m_args.length > 1 && m_args[1] == "--version"; } 711 712 /** Returns the list of app args. 713 714 The app args are provided after the `--` argument. 715 */ 716 @property string[] appArgs() { return m_appArgs; } 717 718 /** Returns the list of all options recognized. 719 720 This list is created by recording all calls to `getopt`. 721 */ 722 @property const(Arg)[] recognizedArgs() { return m_recognizedArgs; } 723 724 void getopt(T)(string names, T* var, string[] help_text = null, bool hidden=false) 725 { 726 getopt!T(names, var, null, help_text, hidden); 727 } 728 729 void getopt(T)(string names, T* var, void delegate(string, string) @safe parseValue, string[] help_text = null, bool hidden=false) 730 { 731 import std.traits : OriginalType; 732 733 foreach (ref arg; m_recognizedArgs) 734 if (names == arg.names) { 735 assert(help_text is null, format!("Duplicated argument '%s' must not change helptext, consider to remove the duplication")(names)); 736 *var = arg.value.match!( 737 (OriginalType!T v) => cast(T)v, 738 (_) { 739 if (false) 740 return T.init; 741 assert(false, "value from previous getopt has different type than the current getopt call"); 742 } 743 ); 744 return; 745 } 746 assert(help_text.length > 0); 747 Arg arg; 748 arg.defaultValue = cast(OriginalType!T)*var; 749 arg.names = names; 750 arg.helpText = help_text; 751 arg.hidden = hidden; 752 if (parseValue is null) 753 m_args.getopt(config.passThrough, names, var); 754 else 755 m_args.getopt(config.passThrough, names, parseValue); 756 arg.value = cast(OriginalType!T)*var; 757 m_recognizedArgs ~= arg; 758 } 759 760 /** Resets the list of available source arguments. 761 */ 762 void dropAllArgs() 763 { 764 m_args = null; 765 } 766 767 /** Returns the list of unprocessed arguments, ignoring the app arguments, 768 and resets the list of available source arguments. 769 */ 770 string[] extractRemainingArgs() 771 { 772 assert(m_args !is null, "extractRemainingArgs must be called only once."); 773 774 auto ret = m_args[1 .. $]; 775 m_args = null; 776 return ret; 777 } 778 779 /** Returns the list of unprocessed arguments, including the app arguments 780 and resets the list of available source arguments. 781 */ 782 string[] extractAllRemainingArgs() 783 { 784 auto ret = extractRemainingArgs(); 785 786 if (this.hasAppArgs) 787 { 788 ret ~= "--" ~ m_appArgs; 789 } 790 791 return ret; 792 } 793 } 794 795 /// Using CommandArgs 796 unittest { 797 /// It should not find the app version for an empty arg list 798 assert(new CommandArgs([]).hasAppVersion == false); 799 800 /// It should find the app version when `--version` is the first arg 801 assert(new CommandArgs(["--version"]).hasAppVersion == true); 802 803 /// It should not find the app version when `--version` is the second arg 804 assert(new CommandArgs(["a", "--version"]).hasAppVersion == false); 805 806 /// It returns an empty app arg list when `--` arg is missing 807 assert(new CommandArgs(["1", "2"]).appArgs == []); 808 809 /// It returns an empty app arg list when `--` arg is missing 810 assert(new CommandArgs(["1", "2"]).appArgs == []); 811 812 /// It returns app args set after "--" 813 assert(new CommandArgs(["1", "2", "--", "a"]).appArgs == ["a"]); 814 assert(new CommandArgs(["1", "2", "--"]).appArgs == []); 815 assert(new CommandArgs(["--"]).appArgs == []); 816 assert(new CommandArgs(["--", "a"]).appArgs == ["a"]); 817 818 /// It returns the list of all args when no args are processed 819 assert(new CommandArgs(["1", "2", "--", "a"]).extractAllRemainingArgs == ["1", "2", "--", "a"]); 820 } 821 822 /// It removes the extracted args 823 unittest { 824 auto args = new CommandArgs(["-a", "-b", "--", "-c"]); 825 bool value; 826 args.getopt("b", &value, [""]); 827 828 assert(args.extractAllRemainingArgs == ["-a", "--", "-c"]); 829 } 830 831 /// It should not be able to remove app args 832 unittest { 833 auto args = new CommandArgs(["-a", "-b", "--", "-c"]); 834 bool value; 835 args.getopt("-c", &value, [""]); 836 837 assert(!value); 838 assert(args.extractAllRemainingArgs == ["-a", "-b", "--", "-c"]); 839 } 840 841 /** Base class for all commands. 842 843 This cass contains a high-level description of the command, including brief 844 and full descriptions and a human readable command line pattern. On top of 845 that it defines the two main entry functions for command execution. 846 */ 847 class Command { 848 string name; 849 string argumentsPattern; 850 string description; 851 string[] helpText; 852 bool acceptsAppArgs; 853 bool hidden = false; // used for deprecated commands 854 855 /** Parses all known command line options without executing any actions. 856 857 This function will be called prior to execute, or may be called as 858 the only method when collecting the list of recognized command line 859 options. 860 861 Only `args.getopt` should be called within this method. 862 */ 863 abstract void prepare(scope CommandArgs args); 864 865 /** 866 * Initialize the dub instance used by `execute` 867 */ 868 public Dub prepareDub(CommonOptions options) { 869 Dub dub; 870 871 if (options.bare) { 872 dub = new Dub(NativePath(options.root_path), getWorkingDirectory()); 873 dub.defaultPlacementLocation = options.placementLocation; 874 875 return dub; 876 } 877 878 // initialize DUB 879 auto package_suppliers = options.registry_urls 880 .map!((url) { 881 // Allow to specify fallback mirrors as space separated urls. Undocumented as we 882 // should simply retry over all registries instead of using a special 883 // FallbackPackageSupplier. 884 auto urls = url.splitter(' '); 885 PackageSupplier ps = _getRegistryPackageSupplier(urls.front); 886 urls.popFront; 887 if (!urls.empty) 888 ps = new FallbackPackageSupplier(ps ~ urls.map!_getRegistryPackageSupplier.array); 889 return ps; 890 }) 891 .array; 892 893 dub = new Dub(options.root_path, package_suppliers, options.skipRegistry); 894 dub.dryRun = options.annotate; 895 dub.defaultPlacementLocation = options.placementLocation; 896 dub.mainRecipePath = options.recipeFile; 897 // make the CWD package available so that for example sub packages can reference their 898 // parent package. 899 try dub.packageManager.getOrLoadPackage(NativePath(options.root_path), NativePath(options.recipeFile), PackageName.init, StrictMode.Warn); 900 catch (Exception e) { 901 // by default we ignore CWD package load fails in prepareDUB, since 902 // they will fail again later when they are actually requested. This 903 // is done to provide custom options to the loading logic and should 904 // ideally be moved elsewhere. (This catch has been around since 10 905 // years when it was first introduced in _app.d_) 906 logDiagnostic("No valid package found in current working directory: %s", e.msg); 907 908 // for now, we work around not knowing if the package is needed or 909 // not, simply by trusting the user to only use `--recipe` when the 910 // recipe file actually exists, otherwise we throw the error. 911 bool loadMustSucceed = options.recipeFile.length > 0; 912 if (loadMustSucceed) 913 throw e; 914 } 915 916 return dub; 917 } 918 919 /** Executes the actual action. 920 921 Note that `prepare` will be called before any call to `execute`. 922 */ 923 abstract int execute(Dub dub, string[] free_args, string[] app_args); 924 925 private bool loadCwdPackage(Dub dub, bool warn_missing_package) 926 { 927 auto filePath = dub.packageManager.findPackageFile(dub.rootPath); 928 929 if (filePath.empty) { 930 if (warn_missing_package) { 931 logInfoNoTag(""); 932 logInfoNoTag("No package manifest (dub.json or dub.sdl) was found in"); 933 logInfoNoTag(dub.rootPath.toNativeString()); 934 logInfoNoTag("Please run DUB from the root directory of an existing package, or run"); 935 logInfoNoTag("\"%s\" to get information on creating a new package.", "dub init --help".color(Mode.bold)); 936 logInfoNoTag(""); 937 } 938 return false; 939 } 940 941 dub.loadPackage(); 942 943 return true; 944 } 945 } 946 947 948 /** Encapsulates a group of commands that fit into a common category. 949 */ 950 struct CommandGroup { 951 /// Caption of the command category 952 string caption; 953 954 /// List of commands contained in this group 955 Command[] commands; 956 957 this(string caption, Command[] commands...) @safe pure nothrow 958 { 959 this.caption = caption; 960 this.commands = commands.dup; 961 } 962 } 963 964 /******************************************************************************/ 965 /* HELP */ 966 /******************************************************************************/ 967 968 class HelpCommand : Command { 969 970 this() @safe pure nothrow 971 { 972 this.name = "help"; 973 this.description = "Shows the help message"; 974 this.helpText = [ 975 "Shows the help message and the supported command options." 976 ]; 977 } 978 979 /// HelpCommand.prepare is not supposed to be called, use 980 /// cast(HelpCommand)this to check if help was requested before execution. 981 override void prepare(scope CommandArgs args) 982 { 983 assert(false, "HelpCommand.prepare is not supposed to be called, use cast(HelpCommand)this to check if help was requested before execution."); 984 } 985 986 /// HelpCommand.execute is not supposed to be called, use 987 /// cast(HelpCommand)this to check if help was requested before execution. 988 override int execute(Dub dub, string[] free_args, string[] app_args) { 989 assert(false, "HelpCommand.execute is not supposed to be called, use cast(HelpCommand)this to check if help was requested before execution."); 990 } 991 } 992 993 /******************************************************************************/ 994 /* INIT */ 995 /******************************************************************************/ 996 997 class InitCommand : Command { 998 private{ 999 string m_templateType = "minimal"; 1000 PackageFormat m_format = PackageFormat.json; 1001 bool m_nonInteractive; 1002 } 1003 this() @safe pure nothrow 1004 { 1005 this.name = "init"; 1006 this.argumentsPattern = "[<directory> [<dependency>...]]"; 1007 this.description = "Initializes an empty package skeleton"; 1008 this.helpText = [ 1009 "Initializes an empty package of the specified type in the given directory.", 1010 "By default, the current working directory is used.", 1011 "", 1012 "Custom templates can be defined by packages by providing a sub-package called \"init-exec\". No default source files are added in this case.", 1013 "The \"init-exec\" sub-package is compiled and executed inside the destination folder after the base project directory has been created.", 1014 "Free arguments \"dub init -t custom -- free args\" are passed into the \"init-exec\" sub-package as app arguments." 1015 ]; 1016 this.acceptsAppArgs = true; 1017 } 1018 1019 override void prepare(scope CommandArgs args) 1020 { 1021 args.getopt("t|type", &m_templateType, [ 1022 "Set the type of project to generate. Available types:", 1023 "", 1024 "minimal - simple \"hello world\" project (default)", 1025 "vibe.d - minimal HTTP server based on vibe.d", 1026 "deimos - skeleton for C header bindings", 1027 "custom - custom project provided by dub package", 1028 ]); 1029 args.getopt("f|format", &m_format, [ 1030 "Sets the format to use for the package description file. Possible values:", 1031 " " ~ [__traits(allMembers, PackageFormat)].map!(f => f == m_format.init.to!string ? f ~ " (default)" : f).join(", ") 1032 ]); 1033 args.getopt("n|non-interactive", &m_nonInteractive, ["Don't enter interactive mode."]); 1034 } 1035 1036 override int execute(Dub dub, string[] free_args, string[] app_args) 1037 { 1038 string dir; 1039 if (free_args.length) 1040 { 1041 dir = free_args[0]; 1042 free_args = free_args[1 .. $]; 1043 } 1044 1045 static string input(string caption, string default_value) 1046 { 1047 import dub.internal.colorize; 1048 cwritef("%s [%s]: ", caption.color(Mode.bold), default_value); 1049 auto inp = readln(); 1050 return inp.length > 1 ? inp[0 .. $-1] : default_value; 1051 } 1052 1053 static string select(string caption, bool free_choice, string default_value, const string[] options...) 1054 { 1055 import dub.internal.colorize.cwrite; 1056 assert(options.length); 1057 import std.math : floor, log10; 1058 auto ndigits = (size_t val) => log10(cast(double) val).floor.to!uint + 1; 1059 1060 immutable default_idx = options.countUntil(default_value); 1061 immutable max_width = options.map!(s => s.length).reduce!max + ndigits(options.length) + " ".length; 1062 immutable num_columns = max(1, 82 / max_width); 1063 immutable num_rows = (options.length + num_columns - 1) / num_columns; 1064 1065 string[] options_matrix; 1066 options_matrix.length = num_rows * num_columns; 1067 foreach (i, option; options) 1068 { 1069 size_t y = i % num_rows; 1070 size_t x = i / num_rows; 1071 options_matrix[x + y * num_columns] = option; 1072 } 1073 1074 auto idx_to_user = (string option) => cast(uint)options.countUntil(option) + 1; 1075 auto user_to_idx = (size_t i) => cast(uint)i - 1; 1076 1077 assert(default_idx >= 0); 1078 cwriteln((free_choice ? "Select or enter " : "Select ").color(Mode.bold), caption.color(Mode.bold), ":".color(Mode.bold)); 1079 foreach (i, option; options_matrix) 1080 { 1081 if (i != 0 && (i % num_columns) == 0) cwriteln(); 1082 if (!option.length) 1083 continue; 1084 auto user_id = idx_to_user(option); 1085 cwritef("%*u)".color(Color.cyan, Mode.bold) ~ " %s", ndigits(options.length), user_id, 1086 leftJustifier(option, max_width)); 1087 } 1088 cwriteln(); 1089 immutable default_choice = (default_idx + 1).to!string; 1090 while (true) 1091 { 1092 auto choice = input(free_choice ? "?" : "#?", default_choice); 1093 if (choice is default_choice) 1094 return default_value; 1095 choice = choice.strip; 1096 uint option_idx = uint.max; 1097 try 1098 option_idx = cast(uint)user_to_idx(to!uint(choice)); 1099 catch (ConvException) 1100 {} 1101 if (option_idx != uint.max) 1102 { 1103 if (option_idx < options.length) 1104 return options[option_idx]; 1105 } 1106 else if (free_choice || options.canFind(choice)) 1107 return choice; 1108 logError("Select an option between 1 and %u%s.", options.length, 1109 free_choice ? " or enter a custom value" : null); 1110 } 1111 } 1112 1113 static string license_select(string def) 1114 { 1115 static immutable licenses = [ 1116 "BSL-1.0 (Boost)", 1117 "MIT", 1118 "Unlicense (public domain)", 1119 "Apache-", 1120 "-1.0", 1121 "-1.1", 1122 "-2.0", 1123 "AGPL-", 1124 "-1.0-only", 1125 "-1.0-or-later", 1126 "-3.0-only", 1127 "-3.0-or-later", 1128 "GPL-", 1129 "-2.0-only", 1130 "-2.0-or-later", 1131 "-3.0-only", 1132 "-3.0-or-later", 1133 "LGPL-", 1134 "-2.0-only", 1135 "-2.0-or-later", 1136 "-2.1-only", 1137 "-2.1-or-later", 1138 "-3.0-only", 1139 "-3.0-or-later", 1140 "BSD-", 1141 "-1-Clause", 1142 "-2-Clause", 1143 "-3-Clause", 1144 "-4-Clause", 1145 "MPL- (Mozilla)", 1146 "-1.0", 1147 "-1.1", 1148 "-2.0", 1149 "-2.0-no-copyleft-exception", 1150 "EUPL-", 1151 "-1.0", 1152 "-1.1", 1153 "-2.0", 1154 "CC- (Creative Commons)", 1155 "-BY-4.0 (Attribution 4.0 International)", 1156 "-BY-SA-4.0 (Attribution Share Alike 4.0 International)", 1157 "Zlib", 1158 "ISC", 1159 "proprietary", 1160 ]; 1161 1162 static string sanitize(string license) 1163 { 1164 auto desc = license.countUntil(" ("); 1165 if (desc != -1) 1166 license = license[0 .. desc]; 1167 return license; 1168 } 1169 1170 string[] root; 1171 foreach (l; licenses) 1172 if (!l.startsWith("-")) 1173 root ~= l; 1174 1175 string result; 1176 while (true) 1177 { 1178 string picked; 1179 if (result.length) 1180 { 1181 auto start = licenses.countUntil!(a => a == result || a.startsWith(result ~ " (")) + 1; 1182 auto end = start; 1183 while (end < licenses.length && licenses[end].startsWith("-")) 1184 end++; 1185 picked = select( 1186 "variant of " ~ result[0 .. $ - 1], 1187 false, 1188 "(back)", 1189 // https://dub.pm/package-format-json.html#licenses 1190 licenses[start .. end].map!"a[1..$]".array ~ "(back)" 1191 ); 1192 if (picked == "(back)") 1193 { 1194 result = null; 1195 continue; 1196 } 1197 picked = sanitize(picked); 1198 } 1199 else 1200 { 1201 picked = select( 1202 "an SPDX license-identifier (" 1203 ~ "https://spdx.org/licenses/".color(Color.light_blue, Mode.underline) 1204 ~ ")".color(Mode.bold), 1205 true, 1206 def, 1207 // https://dub.pm/package-format-json.html#licenses 1208 root 1209 ); 1210 picked = sanitize(picked); 1211 } 1212 if (picked == def) 1213 return def; 1214 1215 if (result.length) 1216 result ~= picked; 1217 else 1218 result = picked; 1219 1220 if (!result.endsWith("-")) 1221 return result; 1222 } 1223 } 1224 1225 void depCallback(ref PackageRecipe p, ref PackageFormat fmt) { 1226 import std.datetime: Clock; 1227 1228 if (m_nonInteractive) return; 1229 1230 enum free_choice = true; 1231 fmt = select("a package recipe format", !free_choice, fmt.to!string, "sdl", "json").to!PackageFormat; 1232 auto author = p.authors.join(", "); 1233 while (true) { 1234 // Tries getting the name until a valid one is given. 1235 import std.regex; 1236 auto nameRegex = regex(`^[a-z0-9\-_]+$`); 1237 string triedName = input("Name", p.name); 1238 if (triedName.matchFirst(nameRegex).empty) { 1239 logError(`Invalid name '%s', names should consist only of lowercase alphanumeric characters, dashes ('-') and underscores ('_').`, triedName); 1240 } else { 1241 p.name = triedName; 1242 break; 1243 } 1244 } 1245 p.description = input("Description", p.description); 1246 p.authors = input("Author name", author).split(",").map!(a => a.strip).array; 1247 p.license = license_select(p.license); 1248 string copyrightString = .format("Copyright © %s, %-(%s, %)", Clock.currTime().year, p.authors); 1249 p.copyright = input("Copyright string", copyrightString); 1250 1251 while (true) { 1252 auto depspec = input("Add dependency (leave empty to skip)", null); 1253 if (!depspec.length) break; 1254 addDependency(dub, p, depspec); 1255 } 1256 } 1257 1258 if (!["vibe.d", "deimos", "minimal"].canFind(m_templateType)) 1259 { 1260 free_args ~= m_templateType; 1261 } 1262 dub.createEmptyPackage(NativePath(dir), free_args, m_templateType, m_format, &depCallback, app_args); 1263 1264 logInfo("Package successfully created in %s", dir.length ? dir : "."); 1265 return 0; 1266 } 1267 } 1268 1269 1270 /******************************************************************************/ 1271 /* GENERATE / BUILD / RUN / TEST / DESCRIBE */ 1272 /******************************************************************************/ 1273 1274 abstract class PackageBuildCommand : Command { 1275 protected { 1276 string m_compilerName; 1277 string m_arch; 1278 string[] m_debugVersions; 1279 string[] m_dVersions; 1280 string[] m_overrideConfigs; 1281 GeneratorSettings baseSettings; 1282 string m_destPath; 1283 string m_defaultConfig; 1284 bool m_nodeps; 1285 bool m_forceRemove = false; 1286 } 1287 1288 override void prepare(scope CommandArgs args) 1289 { 1290 args.getopt("dest", &m_destPath, ["Base directory in which output atifacts will be placed"]); 1291 args.getopt("b|build", &this.baseSettings.buildType, [ 1292 "Specifies the type of build to perform. Note that setting the DFLAGS environment variable will override the build type with custom flags.", 1293 "Possible names:", 1294 " "~builtinBuildTypes.join(", ")~" and custom types" 1295 ]); 1296 args.getopt("c|config", &this.baseSettings.config, [ 1297 "Builds the specified configuration. Configurations can be defined in dub.json" 1298 ]); 1299 args.getopt("override-config", &m_overrideConfigs, [ 1300 "Uses the specified configuration for a certain dependency. Can be specified multiple times.", 1301 "Format: --override-config=<dependency>/<config>" 1302 ]); 1303 args.getopt("compiler", &m_compilerName, [ 1304 "Specifies the compiler binary to use (can be a path).", 1305 "Arbitrary pre- and suffixes to the identifiers below are recognized (e.g. ldc2 or dmd-2.063) and matched to the proper compiler type:", 1306 " "~["dmd", "gdc", "ldc", "gdmd", "ldmd"].join(", ") 1307 ]); 1308 args.getopt("a|arch", &m_arch, [ 1309 "Force a different architecture (e.g. x86 or x86_64)" 1310 ]); 1311 args.getopt("d|debug", &m_debugVersions, [ 1312 "Define the specified `debug` version identifier when building - can be used multiple times" 1313 ]); 1314 args.getopt("d-version", &m_dVersions, [ 1315 "Define the specified `version` identifier when building - can be used multiple times.", 1316 "Use sparingly, with great power comes great responsibility! For commonly used or combined versions " 1317 ~ "and versions that dependees should be able to use, create configurations in your package." 1318 ]); 1319 args.getopt("nodeps", &m_nodeps, [ 1320 "Do not resolve missing dependencies before building" 1321 ]); 1322 args.getopt("build-mode", &this.baseSettings.buildMode, [ 1323 "Specifies the way the compiler and linker are invoked. Valid values:", 1324 " separate (default), allAtOnce, singleFile" 1325 ]); 1326 args.getopt("single", &this.baseSettings.single, [ 1327 "Treats the package name as a filename. The file must contain a package recipe comment." 1328 ]); 1329 args.getopt("force-remove", &m_forceRemove, [ 1330 "Deprecated option that does nothing." 1331 ]); 1332 args.getopt("filter-versions", &this.baseSettings.filterVersions, [ 1333 "[Experimental] Filter version identifiers and debug version identifiers to improve build cache efficiency." 1334 ]); 1335 } 1336 1337 protected void setupVersionPackage(Dub dub, string str_package_info, string default_build_type = "debug") 1338 { 1339 UserPackageDesc udesc = UserPackageDesc.fromString(str_package_info); 1340 setupPackage(dub, udesc, default_build_type); 1341 } 1342 1343 protected void setupPackage(Dub dub, UserPackageDesc udesc, string default_build_type = "debug") 1344 { 1345 if (!m_compilerName.length) m_compilerName = dub.defaultCompiler; 1346 if (!m_arch.length) m_arch = dub.defaultArchitecture; 1347 if (dub.defaultLowMemory) this.baseSettings.buildSettings.options |= BuildOption.lowmem; 1348 if (dub.defaultEnvironments) this.baseSettings.buildSettings.addEnvironments(dub.defaultEnvironments); 1349 if (dub.defaultBuildEnvironments) this.baseSettings.buildSettings.addBuildEnvironments(dub.defaultBuildEnvironments); 1350 if (dub.defaultRunEnvironments) this.baseSettings.buildSettings.addRunEnvironments(dub.defaultRunEnvironments); 1351 if (dub.defaultPreGenerateEnvironments) this.baseSettings.buildSettings.addPreGenerateEnvironments(dub.defaultPreGenerateEnvironments); 1352 if (dub.defaultPostGenerateEnvironments) this.baseSettings.buildSettings.addPostGenerateEnvironments(dub.defaultPostGenerateEnvironments); 1353 if (dub.defaultPreBuildEnvironments) this.baseSettings.buildSettings.addPreBuildEnvironments(dub.defaultPreBuildEnvironments); 1354 if (dub.defaultPostBuildEnvironments) this.baseSettings.buildSettings.addPostBuildEnvironments(dub.defaultPostBuildEnvironments); 1355 if (dub.defaultPreRunEnvironments) this.baseSettings.buildSettings.addPreRunEnvironments(dub.defaultPreRunEnvironments); 1356 if (dub.defaultPostRunEnvironments) this.baseSettings.buildSettings.addPostRunEnvironments(dub.defaultPostRunEnvironments); 1357 this.baseSettings.compiler = getCompiler(m_compilerName); 1358 this.baseSettings.platform = this.baseSettings.compiler.determinePlatform(this.baseSettings.buildSettings, m_compilerName, m_arch); 1359 this.baseSettings.buildSettings.addDebugVersions(m_debugVersions); 1360 this.baseSettings.buildSettings.addVersions(m_dVersions); 1361 1362 this.baseSettings.destinationDirectory = m_destPath ? NativePath(m_destPath) : NativePath.init; 1363 1364 m_defaultConfig = null; 1365 enforce(loadSpecificPackage(dub, udesc), "Failed to load package."); 1366 1367 if (this.baseSettings.config.length != 0 && 1368 !dub.configurations.canFind(this.baseSettings.config) && 1369 this.baseSettings.config != "unittest") 1370 { 1371 string msg = "Unknown build configuration: " ~ this.baseSettings.config; 1372 enum distance = 3; 1373 auto match = dub.configurations.getClosestMatch(this.baseSettings.config, distance); 1374 if (match !is null) msg ~= ". Did you mean '" ~ match ~ "'?"; 1375 enforce(0, msg); 1376 } 1377 1378 if (this.baseSettings.buildType.length == 0) { 1379 if (environment.get("DFLAGS") !is null) this.baseSettings.buildType = "$DFLAGS"; 1380 else this.baseSettings.buildType = default_build_type; 1381 } 1382 1383 if (!m_nodeps) { 1384 // retrieve missing packages 1385 if (!dub.project.hasAllDependencies) { 1386 logDiagnostic("Checking for missing dependencies."); 1387 if (this.baseSettings.single) 1388 dub.upgrade(UpgradeOptions.select | UpgradeOptions.noSaveSelections); 1389 else dub.upgrade(UpgradeOptions.select); 1390 } 1391 } 1392 1393 dub.project.validate(); 1394 1395 foreach (sc; m_overrideConfigs) { 1396 auto idx = sc.indexOf('/'); 1397 enforceUsage(idx >= 0, "Expected \"<package>/<configuration>\" as argument to --override-config."); 1398 dub.project.overrideConfiguration(sc[0 .. idx], sc[idx+1 .. $]); 1399 } 1400 } 1401 1402 private bool loadSpecificPackage(Dub dub, UserPackageDesc udesc) 1403 { 1404 if (this.baseSettings.single) { 1405 enforce(udesc.name.length, "Missing file name of single-file package."); 1406 dub.loadSingleFilePackage(udesc.name); 1407 return true; 1408 } 1409 1410 1411 bool from_cwd = udesc.name.length == 0 || udesc.name.startsWith(":"); 1412 // load package in root_path to enable searching for sub packages 1413 if (loadCwdPackage(dub, from_cwd)) { 1414 if (udesc.name.startsWith(":")) 1415 { 1416 auto pack = dub.packageManager.getSubPackage( 1417 dub.project.rootPackage, udesc.name[1 .. $], false); 1418 dub.loadPackage(pack); 1419 return true; 1420 } 1421 if (from_cwd) return true; 1422 } 1423 1424 enforce(udesc.name.length, "No valid root package found - aborting."); 1425 1426 auto pack = dub.packageManager.getBestPackage( 1427 PackageName(udesc.name), udesc.range); 1428 1429 enforce(pack, format!"Failed to find package '%s' locally."(udesc)); 1430 logInfo("Building package %s in %s", pack.name, pack.path.toNativeString()); 1431 dub.loadPackage(pack); 1432 return true; 1433 } 1434 } 1435 1436 class GenerateCommand : PackageBuildCommand { 1437 protected { 1438 string m_generator; 1439 bool m_printPlatform, m_printBuilds, m_printConfigs; 1440 bool m_deep; // only set in BuildCommand 1441 } 1442 1443 this() @safe pure nothrow 1444 { 1445 this.name = "generate"; 1446 this.argumentsPattern = "<generator> [<package>[@<version-spec>]]"; 1447 this.description = "Generates project files using the specified generator"; 1448 this.helpText = [ 1449 "Generates project files using one of the supported generators:", 1450 "", 1451 "visuald - VisualD project files", 1452 "sublimetext - SublimeText project file", 1453 "cmake - CMake build scripts", 1454 "build - Builds the package directly", 1455 "", 1456 "An optional package name can be given to generate a different package than the root/CWD package." 1457 ]; 1458 } 1459 1460 override void prepare(scope CommandArgs args) 1461 { 1462 super.prepare(args); 1463 1464 args.getopt("combined", &this.baseSettings.combined, [ 1465 "Tries to build the whole project in a single compiler run." 1466 ]); 1467 1468 args.getopt("print-builds", &m_printBuilds, [ 1469 "Prints the list of available build types" 1470 ]); 1471 args.getopt("print-configs", &m_printConfigs, [ 1472 "Prints the list of available configurations" 1473 ]); 1474 args.getopt("print-platform", &m_printPlatform, [ 1475 "Prints the identifiers for the current build platform as used for the build fields in dub.json" 1476 ]); 1477 args.getopt("parallel", &this.baseSettings.parallelBuild, [ 1478 "Runs multiple compiler instances in parallel, if possible." 1479 ]); 1480 } 1481 1482 override int execute(Dub dub, string[] free_args, string[] app_args) 1483 { 1484 string str_package_info; 1485 if (!m_generator.length) { 1486 enforceUsage(free_args.length >= 1 && free_args.length <= 2, "Expected one or two arguments."); 1487 m_generator = free_args[0]; 1488 if (free_args.length >= 2) str_package_info = free_args[1]; 1489 } else { 1490 enforceUsage(free_args.length <= 1, "Expected one or zero arguments."); 1491 if (free_args.length >= 1) str_package_info = free_args[0]; 1492 } 1493 1494 setupVersionPackage(dub, str_package_info, "debug"); 1495 1496 if (m_printBuilds) { 1497 logInfo("Available build types:"); 1498 foreach (i, tp; dub.project.builds) 1499 logInfo(" %s%s", tp, i == 0 ? " [default]" : null); 1500 logInfo(""); 1501 } 1502 1503 m_defaultConfig = dub.project.getDefaultConfiguration(this.baseSettings.platform); 1504 if (m_printConfigs) { 1505 logInfo("Available configurations:"); 1506 foreach (tp; dub.configurations) 1507 logInfo(" %s%s", tp, tp == m_defaultConfig ? " [default]" : null); 1508 logInfo(""); 1509 } 1510 1511 GeneratorSettings gensettings = this.baseSettings; 1512 if (!gensettings.config.length) 1513 gensettings.config = m_defaultConfig; 1514 gensettings.runArgs = app_args; 1515 gensettings.recipeName = dub.mainRecipePath; 1516 // legacy compatibility, default working directory is always CWD 1517 gensettings.overrideToolWorkingDirectory = getWorkingDirectory(); 1518 gensettings.buildDeep = m_deep; 1519 1520 logDiagnostic("Generating using %s", m_generator); 1521 dub.generateProject(m_generator, gensettings); 1522 if (this.baseSettings.buildType == "ddox") dub.runDdox(gensettings.run, app_args); 1523 return 0; 1524 } 1525 } 1526 1527 class BuildCommand : GenerateCommand { 1528 protected { 1529 bool m_yes; // automatic yes to prompts; 1530 bool m_nonInteractive; 1531 } 1532 this() @safe pure nothrow 1533 { 1534 this.name = "build"; 1535 this.argumentsPattern = "[<package>[@<version-spec>]]"; 1536 this.description = "Builds a package (uses the main package in the current working directory by default)"; 1537 this.helpText = [ 1538 "Builds a package (uses the main package in the current working directory by default)" 1539 ]; 1540 } 1541 1542 override void prepare(scope CommandArgs args) 1543 { 1544 args.getopt("temp-build", &this.baseSettings.tempBuild, [ 1545 "Builds the project in the temp folder if possible." 1546 ]); 1547 1548 args.getopt("rdmd", &this.baseSettings.rdmd, [ 1549 "Use rdmd instead of directly invoking the compiler" 1550 ]); 1551 1552 args.getopt("f|force", &this.baseSettings.force, [ 1553 "Forces a recompilation even if the target is up to date" 1554 ]); 1555 args.getopt("y|yes", &m_yes, [ 1556 `Automatic yes to prompts. Assume "yes" as answer to all interactive prompts.` 1557 ]); 1558 args.getopt("n|non-interactive", &m_nonInteractive, [ 1559 "Don't enter interactive mode." 1560 ]); 1561 args.getopt("deep", &m_deep, [ 1562 "Build all dependencies, even when main target is a static library." 1563 ]); 1564 super.prepare(args); 1565 m_generator = "build"; 1566 } 1567 1568 override int execute(Dub dub, string[] free_args, string[] app_args) 1569 { 1570 // single package files don't need to be downloaded, they are on the disk. 1571 if (free_args.length < 1 || this.baseSettings.single) 1572 return super.execute(dub, free_args, app_args); 1573 1574 if (!m_nonInteractive) 1575 { 1576 const packageParts = UserPackageDesc.fromString(free_args[0]); 1577 if (auto rc = fetchMissingPackages(dub, packageParts)) 1578 return rc; 1579 } 1580 return super.execute(dub, free_args, app_args); 1581 } 1582 1583 private int fetchMissingPackages(Dub dub, in UserPackageDesc packageParts) 1584 { 1585 static bool input(string caption, bool default_value = true) { 1586 writef("%s [%s]: ", caption, default_value ? "Y/n" : "y/N"); 1587 auto inp = readln(); 1588 string userInput = "y"; 1589 if (inp.length > 1) 1590 userInput = inp[0 .. $ - 1].toLower; 1591 1592 switch (userInput) { 1593 case "no", "n", "0": 1594 return false; 1595 case "yes", "y", "1": 1596 default: 1597 return true; 1598 } 1599 } 1600 1601 // Local subpackages are always assumed to be present 1602 if (packageParts.name.startsWith(":")) 1603 return 0; 1604 1605 const baseName = PackageName(packageParts.name).main; 1606 // Found locally 1607 if (dub.packageManager.getBestPackage(baseName, packageParts.range)) 1608 return 0; 1609 1610 // Non-interactive, either via flag, or because a version was provided 1611 if (m_yes || !packageParts.range.matchesAny()) { 1612 dub.fetch(baseName, packageParts.range); 1613 return 0; 1614 } 1615 // Otherwise we go the long way of asking the user. 1616 // search for the package and filter versions for exact matches 1617 auto search = dub.searchPackages(baseName.toString()) 1618 .map!(tup => tup[1].find!(p => p.name == baseName.toString())) 1619 .filter!(ps => !ps.empty); 1620 if (search.empty) { 1621 logWarn("Package '%s' was neither found locally nor online.", packageParts); 1622 return 2; 1623 } 1624 1625 const p = search.front.front; 1626 logInfo("Package '%s' was not found locally but is available online:", packageParts); 1627 logInfo("---"); 1628 logInfo("Description: %s", p.description); 1629 logInfo("Version: %s", p.version_); 1630 logInfo("---"); 1631 1632 if (input("Do you want to fetch '%s@%s' now?".format(packageParts, p.version_))) 1633 dub.fetch(baseName, VersionRange.fromString(p.version_)); 1634 return 0; 1635 } 1636 } 1637 1638 class RunCommand : BuildCommand { 1639 this() @safe pure nothrow 1640 { 1641 this.name = "run"; 1642 this.argumentsPattern = "[<package>[@<version-spec>]]"; 1643 this.description = "Builds and runs a package (default command)"; 1644 this.helpText = [ 1645 "Builds and runs a package (uses the main package in the current working directory by default)" 1646 ]; 1647 this.acceptsAppArgs = true; 1648 } 1649 1650 override void prepare(scope CommandArgs args) 1651 { 1652 super.prepare(args); 1653 this.baseSettings.run = true; 1654 } 1655 1656 override int execute(Dub dub, string[] free_args, string[] app_args) 1657 { 1658 return super.execute(dub, free_args, app_args); 1659 } 1660 } 1661 1662 class TestCommand : PackageBuildCommand { 1663 private { 1664 string m_mainFile; 1665 } 1666 1667 this() @safe pure nothrow 1668 { 1669 this.name = "test"; 1670 this.argumentsPattern = "[<package>[@<version-spec>]]"; 1671 this.description = "Executes the tests of the selected package"; 1672 this.helpText = [ 1673 `Builds the package and executes all contained unit tests.`, 1674 ``, 1675 `If no explicit configuration is given, an existing "unittest" ` ~ 1676 `configuration will be preferred for testing. If none exists, the ` ~ 1677 `first library type configuration will be used, and if that doesn't ` ~ 1678 `exist either, the first executable configuration is chosen.`, 1679 ``, 1680 `When a custom main file (--main-file) is specified, only library ` ~ 1681 `configurations can be used. Otherwise, depending on the type of ` ~ 1682 `the selected configuration, either an existing main file will be ` ~ 1683 `used (and needs to be properly adjusted to just run the unit ` ~ 1684 `tests for 'version(unittest)'), or DUB will generate one for ` ~ 1685 `library type configurations.`, 1686 ``, 1687 `Finally, if the package contains a dependency to the "tested" ` ~ 1688 `package, the automatically generated main file will use it to ` ~ 1689 `run the unit tests.` 1690 ]; 1691 this.acceptsAppArgs = true; 1692 } 1693 1694 override void prepare(scope CommandArgs args) 1695 { 1696 args.getopt("temp-build", &this.baseSettings.tempBuild, [ 1697 "Builds the project in the temp folder if possible." 1698 ]); 1699 1700 args.getopt("main-file", &m_mainFile, [ 1701 "Specifies a custom file containing the main() function to use for running the tests." 1702 ]); 1703 args.getopt("combined", &this.baseSettings.combined, [ 1704 "Tries to build the whole project in a single compiler run." 1705 ]); 1706 args.getopt("parallel", &this.baseSettings.parallelBuild, [ 1707 "Runs multiple compiler instances in parallel, if possible." 1708 ]); 1709 args.getopt("f|force", &this.baseSettings.force, [ 1710 "Forces a recompilation even if the target is up to date" 1711 ]); 1712 1713 bool coverage = false; 1714 args.getopt("coverage", &coverage, [ 1715 "Enables code coverage statistics to be generated." 1716 ]); 1717 if (coverage) this.baseSettings.buildType = "unittest-cov"; 1718 1719 bool coverageCTFE = false; 1720 args.getopt("coverage-ctfe", &coverageCTFE, [ 1721 "Enables code coverage (including CTFE) statistics to be generated." 1722 ]); 1723 if (coverageCTFE) this.baseSettings.buildType = "unittest-cov-ctfe"; 1724 1725 super.prepare(args); 1726 } 1727 1728 override int execute(Dub dub, string[] free_args, string[] app_args) 1729 { 1730 string str_package_info; 1731 enforceUsage(free_args.length <= 1, "Expected one or zero arguments."); 1732 if (free_args.length >= 1) str_package_info = free_args[0]; 1733 1734 setupVersionPackage(dub, str_package_info, "unittest"); 1735 1736 GeneratorSettings settings = this.baseSettings; 1737 settings.compiler = getCompiler(this.baseSettings.platform.compilerBinary); 1738 settings.run = true; 1739 settings.runArgs = app_args; 1740 1741 dub.testProject(settings, this.baseSettings.config, NativePath(m_mainFile)); 1742 return 0; 1743 } 1744 } 1745 1746 class LintCommand : PackageBuildCommand { 1747 private { 1748 bool m_syntaxCheck = false; 1749 bool m_styleCheck = false; 1750 string m_errorFormat; 1751 bool m_report = false; 1752 string m_reportFormat; 1753 string m_reportFile; 1754 string[] m_importPaths; 1755 string m_config; 1756 } 1757 1758 this() @safe pure nothrow 1759 { 1760 this.name = "lint"; 1761 this.argumentsPattern = "[<package>[@<version-spec>]]"; 1762 this.description = "Executes the linter tests of the selected package"; 1763 this.helpText = [ 1764 `Builds the package and executes D-Scanner linter tests.` 1765 ]; 1766 this.acceptsAppArgs = true; 1767 } 1768 1769 override void prepare(scope CommandArgs args) 1770 { 1771 args.getopt("syntax-check", &m_syntaxCheck, [ 1772 "Lexes and parses sourceFile, printing the line and column number of " ~ 1773 "any syntax errors to stdout." 1774 ]); 1775 1776 args.getopt("style-check", &m_styleCheck, [ 1777 "Lexes and parses sourceFiles, printing the line and column number of " ~ 1778 "any static analysis check failures stdout." 1779 ]); 1780 1781 args.getopt("error-format", &m_errorFormat, [ 1782 "Format errors produced by the style/syntax checkers." 1783 ]); 1784 1785 args.getopt("report", &m_report, [ 1786 "Generate a static analysis report in JSON format." 1787 ]); 1788 1789 args.getopt("report-format", &m_reportFormat, [ 1790 "Specifies the format of the generated report." 1791 ]); 1792 1793 args.getopt("report-file", &m_reportFile, [ 1794 "Write report to file." 1795 ]); 1796 1797 if (m_reportFormat || m_reportFile) m_report = true; 1798 1799 args.getopt("import-paths", &m_importPaths, [ 1800 "Import paths" 1801 ]); 1802 1803 args.getopt("dscanner-config", &m_config, [ 1804 "Use the given d-scanner configuration file." 1805 ]); 1806 1807 super.prepare(args); 1808 } 1809 1810 override int execute(Dub dub, string[] free_args, string[] app_args) 1811 { 1812 string str_package_info; 1813 enforceUsage(free_args.length <= 1, "Expected one or zero arguments."); 1814 if (free_args.length >= 1) str_package_info = free_args[0]; 1815 1816 string[] args; 1817 if (!m_syntaxCheck && !m_styleCheck && !m_report && app_args.length == 0) { m_styleCheck = true; } 1818 1819 if (m_syntaxCheck) args ~= "--syntaxCheck"; 1820 if (m_styleCheck) args ~= "--styleCheck"; 1821 if (m_errorFormat) args ~= ["--errorFormat", m_errorFormat]; 1822 if (m_report) args ~= "--report"; 1823 if (m_reportFormat) args ~= ["--reportFormat", m_reportFormat]; 1824 if (m_reportFile) args ~= ["--reportFile", m_reportFile]; 1825 foreach (import_path; m_importPaths) args ~= ["-I", import_path]; 1826 if (m_config) args ~= ["--config", m_config]; 1827 1828 setupVersionPackage(dub, str_package_info); 1829 dub.lintProject(args ~ app_args); 1830 return 0; 1831 } 1832 } 1833 1834 class DescribeCommand : PackageBuildCommand { 1835 private { 1836 bool m_importPaths = false; 1837 bool m_stringImportPaths = false; 1838 bool m_dataList = false; 1839 bool m_dataNullDelim = false; 1840 string[] m_data; 1841 } 1842 1843 this() @safe pure nothrow 1844 { 1845 this.name = "describe"; 1846 this.argumentsPattern = "[<package>[@<version-spec>]]"; 1847 this.description = "Prints a JSON description of the project and its dependencies"; 1848 this.helpText = [ 1849 "Prints a JSON build description for the root package an all of " ~ 1850 "their dependencies in a format similar to a JSON package " ~ 1851 "description file. This is useful mostly for IDEs.", 1852 "", 1853 "All usual options that are also used for build/run/generate apply.", 1854 "", 1855 "When --data=VALUE is supplied, specific build settings for a project " ~ 1856 "will be printed instead (by default, formatted for the current compiler).", 1857 "", 1858 "The --data=VALUE option can be specified multiple times to retrieve " ~ 1859 "several pieces of information at once. A comma-separated list is " ~ 1860 "also acceptable (ex: --data=dflags,libs). The data will be output in " ~ 1861 "the same order requested on the command line.", 1862 "", 1863 "The accepted values for --data=VALUE are:", 1864 "", 1865 "main-source-file, dflags, lflags, libs, linker-files, " ~ 1866 "source-files, versions, debug-versions, import-paths, " ~ 1867 "string-import-paths, import-files, options", 1868 "", 1869 "The following are also accepted by --data if --data-list is used:", 1870 "", 1871 "target-type, target-path, target-name, working-directory, " ~ 1872 "copy-files, string-import-files, pre-generate-commands, " ~ 1873 "post-generate-commands, pre-build-commands, post-build-commands, " ~ 1874 "pre-run-commands, post-run-commands, requirements", 1875 ]; 1876 } 1877 1878 override void prepare(scope CommandArgs args) 1879 { 1880 super.prepare(args); 1881 1882 args.getopt("import-paths", &m_importPaths, [ 1883 "Shortcut for --data=import-paths --data-list" 1884 ]); 1885 1886 args.getopt("string-import-paths", &m_stringImportPaths, [ 1887 "Shortcut for --data=string-import-paths --data-list" 1888 ]); 1889 1890 args.getopt("data", &m_data, [ 1891 "Just list the values of a particular build setting, either for this "~ 1892 "package alone or recursively including all dependencies. Accepts a "~ 1893 "comma-separated list. See above for more details and accepted "~ 1894 "possibilities for VALUE." 1895 ]); 1896 1897 args.getopt("data-list", &m_dataList, [ 1898 "Output --data information in list format (line-by-line), instead "~ 1899 "of formatting for a compiler command line.", 1900 ]); 1901 1902 args.getopt("data-0", &m_dataNullDelim, [ 1903 "Output --data information using null-delimiters, rather than "~ 1904 "spaces or newlines. Result is usable with, ex., xargs -0.", 1905 ]); 1906 } 1907 1908 override int execute(Dub dub, string[] free_args, string[] app_args) 1909 { 1910 enforceUsage( 1911 !(m_importPaths && m_stringImportPaths), 1912 "--import-paths and --string-import-paths may not be used together." 1913 ); 1914 1915 enforceUsage( 1916 !(m_data && (m_importPaths || m_stringImportPaths)), 1917 "--data may not be used together with --import-paths or --string-import-paths." 1918 ); 1919 1920 // disable all log output to stdout and use "writeln" to output the JSON description 1921 auto ll = getLogLevel(); 1922 setLogLevel(max(ll, LogLevel.warn)); 1923 scope (exit) setLogLevel(ll); 1924 1925 string str_package_info; 1926 enforceUsage(free_args.length <= 1, "Expected one or zero arguments."); 1927 if (free_args.length >= 1) str_package_info = free_args[0]; 1928 setupVersionPackage(dub, str_package_info); 1929 1930 m_defaultConfig = dub.project.getDefaultConfiguration(this.baseSettings.platform); 1931 1932 GeneratorSettings settings = this.baseSettings; 1933 if (!settings.config.length) 1934 settings.config = m_defaultConfig; 1935 settings.cache = dub.cachePathDontUse(); // See function's description 1936 // Ignore other options 1937 settings.buildSettings.options = this.baseSettings.buildSettings.options & BuildOption.lowmem; 1938 1939 // With a requested `unittest` config, switch to the special test runner 1940 // config (which doesn't require an existing `unittest` configuration). 1941 if (this.baseSettings.config == "unittest") { 1942 const test_config = dub.project.addTestRunnerConfiguration(settings, !dub.dryRun); 1943 if (test_config) settings.config = test_config; 1944 } 1945 1946 if (m_importPaths) { m_data = ["import-paths"]; m_dataList = true; } 1947 else if (m_stringImportPaths) { m_data = ["string-import-paths"]; m_dataList = true; } 1948 1949 if (m_data.length) { 1950 ListBuildSettingsFormat lt; 1951 with (ListBuildSettingsFormat) 1952 lt = m_dataList ? (m_dataNullDelim ? listNul : list) : (m_dataNullDelim ? commandLineNul : commandLine); 1953 dub.listProjectData(settings, m_data, lt); 1954 } else { 1955 auto desc = dub.project.describe(settings); 1956 writeln(desc.serializeToPrettyJson()); 1957 } 1958 1959 return 0; 1960 } 1961 } 1962 1963 class CleanCommand : Command { 1964 private { 1965 bool m_allPackages; 1966 } 1967 1968 this() @safe pure nothrow 1969 { 1970 this.name = "clean"; 1971 this.argumentsPattern = "[<package>]"; 1972 this.description = "Removes intermediate build files and cached build results"; 1973 this.helpText = [ 1974 "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.", 1975 "Without arguments, the package in the current working directory will be cleaned." 1976 ]; 1977 } 1978 1979 override void prepare(scope CommandArgs args) 1980 { 1981 args.getopt("all-packages", &m_allPackages, [ 1982 "Cleans up *all* known packages (dub list)" 1983 ]); 1984 } 1985 1986 override int execute(Dub dub, string[] free_args, string[] app_args) 1987 { 1988 enforceUsage(free_args.length <= 1, "Expected one or zero arguments."); 1989 enforceUsage(app_args.length == 0, "Application arguments are not supported for the clean command."); 1990 enforceUsage(!m_allPackages || !free_args.length, "The --all-packages flag may not be used together with an explicit package name."); 1991 1992 enforce(free_args.length == 0, "Cleaning a specific package isn't possible right now."); 1993 1994 if (m_allPackages) { 1995 dub.clean(); 1996 } else { 1997 dub.loadPackage(); 1998 dub.clean(dub.project.rootPackage); 1999 } 2000 2001 return 0; 2002 } 2003 } 2004 2005 2006 /******************************************************************************/ 2007 /* FETCH / ADD / REMOVE / UPGRADE */ 2008 /******************************************************************************/ 2009 2010 class AddCommand : Command { 2011 this() @safe pure nothrow 2012 { 2013 this.name = "add"; 2014 this.argumentsPattern = "<package>[@<version-spec>] [<packages...>]"; 2015 this.description = "Adds dependencies to the package file."; 2016 this.helpText = [ 2017 "Adds <packages> as dependencies.", 2018 "", 2019 "Running \"dub add <package>\" is the same as adding <package> to the \"dependencies\" section in dub.json/dub.sdl.", 2020 "If no version is specified for one of the packages, dub will query the registry for the latest version." 2021 ]; 2022 } 2023 2024 override void prepare(scope CommandArgs args) {} 2025 2026 override int execute(Dub dub, string[] free_args, string[] app_args) 2027 { 2028 import dub.recipe.io : readPackageRecipe, writePackageRecipe; 2029 enforceUsage(free_args.length != 0, "Expected one or more arguments."); 2030 enforceUsage(app_args.length == 0, "Unexpected application arguments."); 2031 2032 if (!loadCwdPackage(dub, true)) return 2; 2033 auto recipe = dub.project.rootPackage.rawRecipe.clone; 2034 2035 foreach (depspec; free_args) { 2036 if (!addDependency(dub, recipe, depspec)) 2037 return 2; 2038 } 2039 writePackageRecipe(dub.project.rootPackage.recipePath, recipe); 2040 2041 return 0; 2042 } 2043 } 2044 2045 class UpgradeCommand : Command { 2046 private { 2047 bool m_prerelease = false; 2048 bool m_includeSubPackages = false; 2049 bool m_forceRemove = false; 2050 bool m_missingOnly = false; 2051 bool m_verify = false; 2052 bool m_dryRun = false; 2053 } 2054 2055 this() @safe pure nothrow 2056 { 2057 this.name = "upgrade"; 2058 this.argumentsPattern = "[<packages...>]"; 2059 this.description = "Forces an upgrade of the dependencies"; 2060 this.helpText = [ 2061 "Upgrades all dependencies of the package by querying the package registry(ies) for new versions.", 2062 "", 2063 "This will update the versions stored in the selections file ("~SelectedVersions.defaultFile~") accordingly.", 2064 "", 2065 "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." 2066 ]; 2067 } 2068 2069 override void prepare(scope CommandArgs args) 2070 { 2071 args.getopt("prerelease", &m_prerelease, [ 2072 "Uses the latest pre-release version, even if release versions are available" 2073 ]); 2074 args.getopt("s|sub-packages", &m_includeSubPackages, [ 2075 "Also upgrades dependencies of all directory based sub packages" 2076 ]); 2077 args.getopt("verify", &m_verify, [ 2078 "Updates the project and performs a build. If successful, rewrites the selected versions file <to be implemented>." 2079 ]); 2080 args.getopt("dry-run", &m_dryRun, [ 2081 "Only print what would be upgraded, but don't actually upgrade anything." 2082 ]); 2083 args.getopt("missing-only", &m_missingOnly, [ 2084 "Performs an upgrade only for dependencies that don't yet have a version selected. This is also done automatically before each build." 2085 ]); 2086 args.getopt("force-remove", &m_forceRemove, [ 2087 "Deprecated option that does nothing." 2088 ]); 2089 } 2090 2091 override int execute(Dub dub, string[] free_args, string[] app_args) 2092 { 2093 enforceUsage(free_args.length <= 1, "Unexpected arguments."); 2094 enforceUsage(app_args.length == 0, "Unexpected application arguments."); 2095 enforceUsage(!m_verify, "--verify is not yet implemented."); 2096 enforce(loadCwdPackage(dub, true), "Failed to load package."); 2097 logInfo("Upgrading", Color.cyan, "project in %s", dub.projectPath.toNativeString().color(Mode.bold)); 2098 auto options = UpgradeOptions.upgrade|UpgradeOptions.select; 2099 if (m_missingOnly) options &= ~UpgradeOptions.upgrade; 2100 if (m_prerelease) options |= UpgradeOptions.preRelease; 2101 if (m_dryRun) options |= UpgradeOptions.dryRun; 2102 dub.upgrade(options, free_args); 2103 2104 auto spacks = dub.project.rootPackage 2105 .subPackages 2106 .filter!(sp => sp.path.length); 2107 2108 if (m_includeSubPackages) { 2109 bool any_error = false; 2110 2111 // Go through each path based sub package, load it as a new instance 2112 // and perform an upgrade as if the upgrade had been run from within 2113 // the sub package folder. Note that we have to use separate Dub 2114 // instances, because the upgrade always works on the root package 2115 // of a project, which in this case are the individual sub packages. 2116 foreach (sp; spacks) { 2117 try { 2118 auto fullpath = (dub.projectPath ~ sp.path).toNativeString(); 2119 logInfo("Upgrading", Color.cyan, "sub package in %s", fullpath); 2120 auto sdub = new Dub(fullpath, dub.packageSuppliers, SkipPackageSuppliers.all); 2121 sdub.defaultPlacementLocation = dub.defaultPlacementLocation; 2122 sdub.loadPackage(); 2123 sdub.upgrade(options, free_args); 2124 } catch (Exception e) { 2125 logError("Failed to update sub package at %s: %s", 2126 sp.path, e.msg); 2127 any_error = true; 2128 } 2129 } 2130 2131 if (any_error) return 1; 2132 } else if (!spacks.empty) { 2133 foreach (sp; spacks) 2134 logInfo("Not upgrading sub package in %s", sp.path); 2135 logInfo("\nNote: specify -s to also upgrade sub packages."); 2136 } 2137 2138 return 0; 2139 } 2140 } 2141 2142 class FetchRemoveCommand : Command { 2143 protected { 2144 string m_version; 2145 bool m_forceRemove = false; 2146 } 2147 2148 override void prepare(scope CommandArgs args) 2149 { 2150 args.getopt("version", &m_version, [ 2151 "Use the specified version/branch instead of the latest available match", 2152 "The remove command also accepts \"*\" here as a wildcard to remove all versions of the package from the specified location" 2153 ], true); // hide --version from help 2154 2155 args.getopt("force-remove", &m_forceRemove, [ 2156 "Deprecated option that does nothing" 2157 ]); 2158 } 2159 2160 abstract override int execute(Dub dub, string[] free_args, string[] app_args); 2161 } 2162 2163 class FetchCommand : FetchRemoveCommand { 2164 private enum FetchStatus 2165 { 2166 /// Package is already present and on the right version 2167 Present = 0, 2168 /// Package was fetched from the registry 2169 Fetched = 1, 2170 /// Attempts at fetching the package failed 2171 Failed = 2, 2172 } 2173 2174 protected bool recursive; 2175 protected size_t[FetchStatus.max + 1] result; 2176 2177 this() @safe pure nothrow 2178 { 2179 this.name = "fetch"; 2180 this.argumentsPattern = "<package>[@<version-spec>]"; 2181 this.description = "Explicitly retrieves and caches packages"; 2182 this.helpText = [ 2183 "When run with one or more arguments, regardless of the location it is run in,", 2184 "it will fetch the packages matching the argument(s).", 2185 "Examples:", 2186 "$ dub fetch vibe-d", 2187 "$ dub fetch vibe-d@v0.9.0 --cache=local --recursive", 2188 "", 2189 "When run in a project with no arguments, it will fetch all dependencies for that project.", 2190 "If the project doesn't have set dependencies (no 'dub.selections.json'), it will also perform dependency resolution.", 2191 "Example:", 2192 "$ cd myProject && dub fetch", 2193 "", 2194 "Note that the 'build', 'run', and any other command that need packages will automatically perform fetch,", 2195 "hence it is not generally necessary to run this command before any other." 2196 ]; 2197 } 2198 2199 override void prepare(scope CommandArgs args) 2200 { 2201 args.getopt("r|recursive", &this.recursive, [ 2202 "Also fetches dependencies of specified packages", 2203 ]); 2204 super.prepare(args); 2205 } 2206 2207 override int execute(Dub dub, string[] free_args, string[] app_args) 2208 { 2209 enforceUsage(app_args.length == 0, "Unexpected application arguments."); 2210 2211 // remove then --version removed 2212 if (m_version.length) { 2213 enforceUsage(free_args.length == 1, "Expecting exactly one argument when using --version."); 2214 const name = free_args[0]; 2215 logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", name, m_version); 2216 enforceUsage(!name.canFindVersionSplitter, "Double version spec not allowed."); 2217 this.fetchPackage(dub, UserPackageDesc(name, VersionRange.fromString(m_version))); 2218 return this.result[FetchStatus.Failed] ? 1 : 0; 2219 } 2220 2221 // Fetches dependencies of the project 2222 // This is obviously mutually exclusive with the below foreach 2223 if (!free_args.length) { 2224 if (!this.loadCwdPackage(dub, true)) 2225 return 1; 2226 // retrieve missing packages 2227 if (!dub.project.hasAllDependencies) { 2228 logInfo("Resolving", Color.green, "missing dependencies for project"); 2229 dub.upgrade(UpgradeOptions.select); 2230 } 2231 else 2232 logInfo("All %s dependencies are already present locally", 2233 dub.project.dependencies.length); 2234 return 0; 2235 } 2236 2237 // Fetches packages named explicitly 2238 foreach (name; free_args) { 2239 const udesc = UserPackageDesc.fromString(name); 2240 this.fetchPackage(dub, udesc); 2241 } 2242 // Note that this does not include packages indirectly fetched. 2243 // Hence it is not currently displayed in the no-argument version, 2244 // and will only include directly mentioned packages in the arg version. 2245 logInfoNoTag("%s packages fetched, %s already present, %s failed", 2246 this.result[FetchStatus.Fetched], this.result[FetchStatus.Present], 2247 this.result[FetchStatus.Failed]); 2248 return this.result[FetchStatus.Failed] ? 1 : 0; 2249 } 2250 2251 /// Shell around `fetchSinglePackage` with logs and recursion support 2252 private void fetchPackage(Dub dub, UserPackageDesc udesc) 2253 { 2254 auto r = this.fetchSinglePackage(dub, udesc); 2255 this.result[r] += 1; 2256 final switch (r) { 2257 case FetchStatus.Failed: 2258 // Error displayed in `fetchPackage` as it has more information 2259 // However we need to return here as we can't recurse. 2260 return; 2261 case FetchStatus.Present: 2262 logInfo("Existing", Color.green, "package %s found locally", udesc); 2263 break; 2264 case FetchStatus.Fetched: 2265 logInfo("Fetched", Color.green, "package %s successfully", udesc); 2266 break; 2267 } 2268 if (this.recursive) { 2269 auto pack = dub.packageManager.getBestPackage( 2270 PackageName(udesc.name), udesc.range); 2271 auto proj = new Project(dub.packageManager, pack); 2272 if (!proj.hasAllDependencies) { 2273 logInfo("Resolving", Color.green, "missing dependencies for project"); 2274 dub.loadPackage(pack); 2275 dub.upgrade(UpgradeOptions.select); 2276 } 2277 } 2278 } 2279 2280 /// Implementation for argument version 2281 private FetchStatus fetchSinglePackage(Dub dub, UserPackageDesc udesc) 2282 { 2283 auto fspkg = dub.packageManager.getBestPackage( 2284 PackageName(udesc.name), udesc.range); 2285 // Avoid dub fetch if the package is present on the filesystem. 2286 if (fspkg !is null && udesc.range.isExactVersion()) 2287 return FetchStatus.Present; 2288 2289 try { 2290 auto pkg = dub.fetch(PackageName(udesc.name), udesc.range, 2291 FetchOptions.forceBranchUpgrade); 2292 assert(pkg !is null, "dub.fetch returned a null Package"); 2293 return pkg is fspkg ? FetchStatus.Present : FetchStatus.Fetched; 2294 } catch (Exception e) { 2295 logError("Fetching %s failed: %s", udesc, e.msg); 2296 return FetchStatus.Failed; 2297 } 2298 } 2299 } 2300 2301 class RemoveCommand : FetchRemoveCommand { 2302 private { 2303 bool m_nonInteractive; 2304 } 2305 2306 this() @safe pure nothrow 2307 { 2308 this.name = "remove"; 2309 this.argumentsPattern = "<package>[@<version-spec>]"; 2310 this.description = "Removes a cached package"; 2311 this.helpText = [ 2312 "Removes a package that is cached on the local system." 2313 ]; 2314 } 2315 2316 override void prepare(scope CommandArgs args) 2317 { 2318 super.prepare(args); 2319 args.getopt("n|non-interactive", &m_nonInteractive, ["Don't enter interactive mode."]); 2320 } 2321 2322 override int execute(Dub dub, string[] free_args, string[] app_args) 2323 { 2324 enforceUsage(free_args.length == 1, "Expecting exactly one argument."); 2325 enforceUsage(app_args.length == 0, "Unexpected application arguments."); 2326 2327 auto package_id = free_args[0]; 2328 auto location = dub.defaultPlacementLocation; 2329 2330 size_t resolveVersion(in Package[] packages) { 2331 // just remove only package version 2332 if (packages.length == 1) 2333 return 0; 2334 2335 writeln("Select version of '", package_id, "' to remove from location '", location, "':"); 2336 foreach (i, pack; packages) 2337 writefln("%s) %s", i + 1, pack.version_); 2338 writeln(packages.length + 1, ") ", "all versions"); 2339 while (true) { 2340 writef("> "); 2341 auto inp = readln(); 2342 if (!inp.length) // Ctrl+D 2343 return size_t.max; 2344 inp = inp.stripRight; 2345 if (!inp.length) // newline or space 2346 continue; 2347 try { 2348 immutable selection = inp.to!size_t - 1; 2349 if (selection <= packages.length) 2350 return selection; 2351 } catch (ConvException e) { 2352 } 2353 logError("Please enter a number between 1 and %s.", packages.length + 1); 2354 } 2355 } 2356 2357 if (!m_version.empty) { // remove then --version removed 2358 enforceUsage(!package_id.canFindVersionSplitter, "Double version spec not allowed."); 2359 logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", package_id, m_version); 2360 dub.remove(PackageName(package_id), m_version, location); 2361 } else { 2362 const parts = UserPackageDesc.fromString(package_id); 2363 const explicit = package_id.canFindVersionSplitter; 2364 if (m_nonInteractive || explicit || parts.range != VersionRange.Any) { 2365 const str = parts.range.matchesAny() ? "*" : parts.range.toString(); 2366 dub.remove(PackageName(parts.name), str, location); 2367 } else { 2368 dub.remove(PackageName(package_id), location, &resolveVersion); 2369 } 2370 } 2371 return 0; 2372 } 2373 } 2374 2375 /******************************************************************************/ 2376 /* ADD/REMOVE PATH/LOCAL */ 2377 /******************************************************************************/ 2378 2379 abstract class RegistrationCommand : Command { 2380 private { 2381 bool m_system; 2382 } 2383 2384 override void prepare(scope CommandArgs args) 2385 { 2386 args.getopt("system", &m_system, [ 2387 "DEPRECATED: Use --cache=system instead" 2388 ], true); 2389 } 2390 2391 abstract override int execute(Dub dub, string[] free_args, string[] app_args); 2392 } 2393 2394 class AddPathCommand : RegistrationCommand { 2395 this() @safe pure nothrow 2396 { 2397 this.name = "add-path"; 2398 this.argumentsPattern = "<path>"; 2399 this.description = "Adds a default package search path"; 2400 this.helpText = [ 2401 "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.", 2402 "", 2403 "Any packages registered using add-path will be preferred over packages downloaded from the package registry when searching for dependencies during a build operation.", 2404 "", 2405 "The version of the packages will be determined by one of the following:", 2406 " - For GIT working copies, the last tag (git describe) is used to determine the version", 2407 " - If the package contains a \"version\" field in the package description, this is used", 2408 " - If neither of those apply, \"~master\" is assumed" 2409 ]; 2410 } 2411 2412 override int execute(Dub dub, string[] free_args, string[] app_args) 2413 { 2414 enforceUsage(free_args.length == 1, "Missing search path."); 2415 enforceUsage(!this.m_system || dub.defaultPlacementLocation == PlacementLocation.user, 2416 "Cannot use both --system and --cache, prefer --cache"); 2417 if (this.m_system) 2418 dub.addSearchPath(free_args[0], PlacementLocation.system); 2419 else 2420 dub.addSearchPath(free_args[0], dub.defaultPlacementLocation); 2421 return 0; 2422 } 2423 } 2424 2425 class RemovePathCommand : RegistrationCommand { 2426 this() @safe pure nothrow 2427 { 2428 this.name = "remove-path"; 2429 this.argumentsPattern = "<path>"; 2430 this.description = "Removes a package search path"; 2431 this.helpText = ["Removes a package search path previously added with add-path."]; 2432 } 2433 2434 override int execute(Dub dub, string[] free_args, string[] app_args) 2435 { 2436 enforceUsage(free_args.length == 1, "Expected one argument."); 2437 enforceUsage(!this.m_system || dub.defaultPlacementLocation == PlacementLocation.user, 2438 "Cannot use both --system and --cache, prefer --cache"); 2439 if (this.m_system) 2440 dub.removeSearchPath(free_args[0], PlacementLocation.system); 2441 else 2442 dub.removeSearchPath(free_args[0], dub.defaultPlacementLocation); 2443 return 0; 2444 } 2445 } 2446 2447 class AddLocalCommand : RegistrationCommand { 2448 this() @safe pure nothrow 2449 { 2450 this.name = "add-local"; 2451 this.argumentsPattern = "<path> [<version>]"; 2452 this.description = "Adds a local package directory (e.g. a git repository)"; 2453 this.helpText = [ 2454 "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.", 2455 "", 2456 "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.", 2457 "", 2458 "See 'dub add-path -h' for a way to register multiple local packages at once." 2459 ]; 2460 } 2461 2462 override int execute(Dub dub, string[] free_args, string[] app_args) 2463 { 2464 enforceUsage(free_args.length == 1 || free_args.length == 2, 2465 "Expecting one or two arguments."); 2466 enforceUsage(!this.m_system || dub.defaultPlacementLocation == PlacementLocation.user, 2467 "Cannot use both --system and --cache, prefer --cache"); 2468 2469 string ver = free_args.length == 2 ? free_args[1] : null; 2470 if (this.m_system) 2471 dub.addLocalPackage(free_args[0], ver, PlacementLocation.system); 2472 else 2473 dub.addLocalPackage(free_args[0], ver, dub.defaultPlacementLocation); 2474 return 0; 2475 } 2476 } 2477 2478 class RemoveLocalCommand : RegistrationCommand { 2479 this() @safe pure nothrow 2480 { 2481 this.name = "remove-local"; 2482 this.argumentsPattern = "<path>"; 2483 this.description = "Removes a local package directory"; 2484 this.helpText = ["Removes a local package directory"]; 2485 } 2486 2487 override int execute(Dub dub, string[] free_args, string[] app_args) 2488 { 2489 enforceUsage(free_args.length >= 1, "Missing package path argument."); 2490 enforceUsage(free_args.length <= 1, 2491 "Expected the package path to be the only argument."); 2492 enforceUsage(!this.m_system || dub.defaultPlacementLocation == PlacementLocation.user, 2493 "Cannot use both --system and --cache, prefer --cache"); 2494 2495 if (this.m_system) 2496 dub.removeLocalPackage(free_args[0], PlacementLocation.system); 2497 else 2498 dub.removeLocalPackage(free_args[0], dub.defaultPlacementLocation); 2499 return 0; 2500 } 2501 } 2502 2503 class ListCommand : Command { 2504 this() @safe pure nothrow 2505 { 2506 this.name = "list"; 2507 this.argumentsPattern = "[<package>[@<version-spec>]]"; 2508 this.description = "Prints a list of all or selected local packages dub is aware of"; 2509 this.helpText = [ 2510 "Prints a list of all or selected local packages. This includes all cached "~ 2511 "packages (user or system wide), all packages in the package search paths "~ 2512 "(\"dub add-path\") and all manually registered packages (\"dub add-local\"). "~ 2513 "If a package (and optionally a version spec) is specified, only matching packages are shown." 2514 ]; 2515 } 2516 override void prepare(scope CommandArgs args) {} 2517 override int execute(Dub dub, string[] free_args, string[] app_args) 2518 { 2519 enforceUsage(free_args.length <= 1, "Expecting zero or one extra arguments."); 2520 const pinfo = free_args.length ? UserPackageDesc.fromString(free_args[0]) : UserPackageDesc("",VersionRange.Any); 2521 const pname = pinfo.name; 2522 enforceUsage(app_args.length == 0, "The list command supports no application arguments."); 2523 logInfoNoTag("Packages present in the system and known to dub:"); 2524 foreach (p; dub.packageManager.getPackageIterator()) { 2525 if ((pname == "" || pname == p.name) && pinfo.range.matches(p.version_)) 2526 logInfoNoTag(" %s %s: %s", p.name.color(Mode.bold), p.version_, p.path.toNativeString()); 2527 } 2528 logInfo(""); 2529 return 0; 2530 } 2531 } 2532 2533 class SearchCommand : Command { 2534 this() @safe pure nothrow 2535 { 2536 this.name = "search"; 2537 this.argumentsPattern = "<package-name>"; 2538 this.description = "Search for available packages."; 2539 this.helpText = [ 2540 "Search all specified providers for matching packages." 2541 ]; 2542 } 2543 override void prepare(scope CommandArgs args) {} 2544 override int execute(Dub dub, string[] free_args, string[] app_args) 2545 { 2546 enforce(free_args.length == 1, "Expected one argument."); 2547 auto res = dub.searchPackages(free_args[0]); 2548 if (res.empty) 2549 { 2550 logError("No matches found."); 2551 return 2; 2552 } 2553 auto justify = res 2554 .map!((descNmatches) => descNmatches[1]) 2555 .joiner 2556 .map!(m => m.name.length + m.version_.length) 2557 .reduce!max + " ()".length; 2558 justify += (~justify & 3) + 1; // round to next multiple of 4 2559 int colorDifference = cast(int)"a".color(Mode.bold).length - 1; 2560 justify += colorDifference; 2561 foreach (desc, matches; res) 2562 { 2563 logInfoNoTag("==== %s ====", desc); 2564 foreach (m; matches) 2565 logInfoNoTag(" %s%s", leftJustify(m.name.color(Mode.bold) 2566 ~ " (" ~ m.version_ ~ ")", justify), m.description); 2567 } 2568 return 0; 2569 } 2570 } 2571 2572 2573 /******************************************************************************/ 2574 /* OVERRIDES */ 2575 /******************************************************************************/ 2576 2577 class AddOverrideCommand : Command { 2578 private { 2579 bool m_system = false; 2580 } 2581 2582 static immutable string DeprecationMessage = 2583 "This command is deprecated. Use path based dependency, custom cache path, " ~ 2584 "or edit `dub.selections.json` to achieve the same results."; 2585 2586 2587 this() @safe pure nothrow 2588 { 2589 this.name = "add-override"; 2590 this.argumentsPattern = "<package> <version-spec> <target-path/target-version>"; 2591 this.description = "Adds a new package override."; 2592 2593 this.hidden = true; 2594 this.helpText = [ DeprecationMessage ]; 2595 } 2596 2597 override void prepare(scope CommandArgs args) 2598 { 2599 args.getopt("system", &m_system, [ 2600 "Register system-wide instead of user-wide" 2601 ]); 2602 } 2603 2604 override int execute(Dub dub, string[] free_args, string[] app_args) 2605 { 2606 logWarn(DeprecationMessage); 2607 enforceUsage(app_args.length == 0, "Unexpected application arguments."); 2608 enforceUsage(free_args.length == 3, "Expected three arguments, not "~free_args.length.to!string); 2609 auto scope_ = m_system ? PlacementLocation.system : PlacementLocation.user; 2610 auto pack = free_args[0]; 2611 auto source = VersionRange.fromString(free_args[1]); 2612 if (existsFile(NativePath(free_args[2]))) { 2613 auto target = NativePath(free_args[2]); 2614 if (!target.absolute) target = getWorkingDirectory() ~ target; 2615 dub.packageManager.addOverride_(scope_, pack, source, target); 2616 logInfo("Added override %s %s => %s", pack, source, target); 2617 } else { 2618 auto target = Version(free_args[2]); 2619 dub.packageManager.addOverride_(scope_, pack, source, target); 2620 logInfo("Added override %s %s => %s", pack, source, target); 2621 } 2622 return 0; 2623 } 2624 } 2625 2626 class RemoveOverrideCommand : Command { 2627 private { 2628 bool m_system = false; 2629 } 2630 2631 this() @safe pure nothrow 2632 { 2633 this.name = "remove-override"; 2634 this.argumentsPattern = "<package> <version-spec>"; 2635 this.description = "Removes an existing package override."; 2636 2637 this.hidden = true; 2638 this.helpText = [ AddOverrideCommand.DeprecationMessage ]; 2639 } 2640 2641 override void prepare(scope CommandArgs args) 2642 { 2643 args.getopt("system", &m_system, [ 2644 "Register system-wide instead of user-wide" 2645 ]); 2646 } 2647 2648 override int execute(Dub dub, string[] free_args, string[] app_args) 2649 { 2650 logWarn(AddOverrideCommand.DeprecationMessage); 2651 enforceUsage(app_args.length == 0, "Unexpected application arguments."); 2652 enforceUsage(free_args.length == 2, "Expected two arguments, not "~free_args.length.to!string); 2653 auto scope_ = m_system ? PlacementLocation.system : PlacementLocation.user; 2654 auto source = VersionRange.fromString(free_args[1]); 2655 dub.packageManager.removeOverride_(scope_, free_args[0], source); 2656 return 0; 2657 } 2658 } 2659 2660 class ListOverridesCommand : Command { 2661 this() @safe pure nothrow 2662 { 2663 this.name = "list-overrides"; 2664 this.argumentsPattern = ""; 2665 this.description = "Prints a list of all local package overrides"; 2666 2667 this.hidden = true; 2668 this.helpText = [ AddOverrideCommand.DeprecationMessage ]; 2669 } 2670 override void prepare(scope CommandArgs args) {} 2671 override int execute(Dub dub, string[] free_args, string[] app_args) 2672 { 2673 logWarn(AddOverrideCommand.DeprecationMessage); 2674 2675 void printList(in PackageOverride_[] overrides, string caption) 2676 { 2677 if (overrides.length == 0) return; 2678 logInfoNoTag("# %s", caption); 2679 foreach (ovr; overrides) 2680 ovr.target.match!( 2681 t => logInfoNoTag("%s %s => %s", ovr.package_.color(Mode.bold), ovr.source, t)); 2682 } 2683 printList(dub.packageManager.getOverrides_(PlacementLocation.user), "User wide overrides"); 2684 printList(dub.packageManager.getOverrides_(PlacementLocation.system), "System wide overrides"); 2685 return 0; 2686 } 2687 } 2688 2689 /******************************************************************************/ 2690 /* Cache cleanup */ 2691 /******************************************************************************/ 2692 2693 class CleanCachesCommand : Command { 2694 this() @safe pure nothrow 2695 { 2696 this.name = "clean-caches"; 2697 this.argumentsPattern = ""; 2698 this.description = "Removes cached metadata"; 2699 this.helpText = [ 2700 "This command removes any cached metadata like the list of available packages and their latest version." 2701 ]; 2702 } 2703 2704 override void prepare(scope CommandArgs args) {} 2705 2706 override int execute(Dub dub, string[] free_args, string[] app_args) 2707 { 2708 return 0; 2709 } 2710 } 2711 2712 /******************************************************************************/ 2713 /* DUSTMITE */ 2714 /******************************************************************************/ 2715 2716 class DustmiteCommand : PackageBuildCommand { 2717 private { 2718 int m_compilerStatusCode = int.min; 2719 int m_linkerStatusCode = int.min; 2720 int m_programStatusCode = int.min; 2721 string m_compilerRegex; 2722 string m_linkerRegex; 2723 string m_programRegex; 2724 string m_testPackage; 2725 bool m_noRedirect; 2726 string m_strategy; 2727 uint m_jobCount; // zero means not specified 2728 bool m_trace; 2729 uint m_timeout; // in seconds, zero means not specified 2730 } 2731 2732 this() @safe pure nothrow 2733 { 2734 this.name = "dustmite"; 2735 this.argumentsPattern = "<destination-path>"; 2736 this.acceptsAppArgs = true; 2737 this.description = "Create reduced test cases for build errors"; 2738 this.helpText = [ 2739 "This command uses the Dustmite utility to isolate the cause of build errors in a DUB project.", 2740 "", 2741 "It will create a copy of all involved packages and run dustmite on this copy, leaving a reduced test case.", 2742 "", 2743 "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." 2744 ]; 2745 } 2746 2747 override void prepare(scope CommandArgs args) 2748 { 2749 args.getopt("compiler-status", &m_compilerStatusCode, ["The expected status code of the compiler run"]); 2750 args.getopt("compiler-regex", &m_compilerRegex, ["A regular expression used to match against the compiler output"]); 2751 args.getopt("linker-status", &m_linkerStatusCode, ["The expected status code of the linker run"]); 2752 args.getopt("linker-regex", &m_linkerRegex, ["A regular expression used to match against the linker output"]); 2753 args.getopt("program-status", &m_programStatusCode, ["The expected status code of the built executable"]); 2754 args.getopt("program-regex", &m_programRegex, ["A regular expression used to match against the program output"]); 2755 args.getopt("test-package", &m_testPackage, ["Perform a test run - usually only used internally"]); 2756 args.getopt("combined", &this.baseSettings.combined, ["Builds multiple packages with one compiler run"]); 2757 args.getopt("no-redirect", &m_noRedirect, ["Don't redirect stdout/stderr streams of the test command"]); 2758 args.getopt("strategy", &m_strategy, ["Set strategy (careful/lookback/pingpong/indepth/inbreadth)"]); 2759 args.getopt("j", &m_jobCount, ["Set number of look-ahead processes"]); 2760 args.getopt("trace", &m_trace, ["Save all attempted reductions to DIR.trace"]); 2761 args.getopt("timeout", &m_timeout, ["Timeout for each oracle invocation in seconds"]); 2762 super.prepare(args); 2763 2764 // speed up loading when in test mode 2765 if (m_testPackage.length) { 2766 m_nodeps = true; 2767 } 2768 } 2769 2770 /// Returns: A minimally-initialized dub instance in test mode 2771 override Dub prepareDub(CommonOptions options) 2772 { 2773 if (!m_testPackage.length) 2774 return super.prepareDub(options); 2775 return new Dub(NativePath(options.root_path), getWorkingDirectory()); 2776 } 2777 2778 override int execute(Dub dub, string[] free_args, string[] app_args) 2779 { 2780 import std.format : formattedWrite; 2781 2782 if (m_testPackage.length) { 2783 setupPackage(dub, UserPackageDesc(m_testPackage)); 2784 m_defaultConfig = dub.project.getDefaultConfiguration(this.baseSettings.platform); 2785 2786 GeneratorSettings gensettings = this.baseSettings; 2787 if (!gensettings.config.length) 2788 gensettings.config = m_defaultConfig; 2789 gensettings.run = m_programStatusCode != int.min || m_programRegex.length; 2790 gensettings.runArgs = app_args; 2791 gensettings.force = true; 2792 gensettings.compileCallback = check(m_compilerStatusCode, m_compilerRegex); 2793 gensettings.linkCallback = check(m_linkerStatusCode, m_linkerRegex); 2794 gensettings.runCallback = check(m_programStatusCode, m_programRegex); 2795 try dub.generateProject("build", gensettings); 2796 catch (DustmiteMismatchException) { 2797 logInfoNoTag("Dustmite test doesn't match."); 2798 return 3; 2799 } 2800 catch (DustmiteMatchException) { 2801 logInfoNoTag("Dustmite test matches."); 2802 return 0; 2803 } 2804 } else { 2805 enforceUsage(free_args.length == 1, "Expected destination path."); 2806 auto path = NativePath(free_args[0]); 2807 path.normalize(); 2808 enforceUsage(!path.empty, "Destination path must not be empty."); 2809 if (!path.absolute) path = getWorkingDirectory() ~ path; 2810 enforceUsage(!path.startsWith(dub.rootPath), "Destination path must not be a sub directory of the tested package!"); 2811 2812 setupPackage(dub, UserPackageDesc.init); 2813 auto prj = dub.project; 2814 if (this.baseSettings.config.empty) 2815 this.baseSettings.config = prj.getDefaultConfiguration(this.baseSettings.platform); 2816 2817 void copyFolderRec(NativePath folder, NativePath dstfolder) 2818 { 2819 ensureDirectory(dstfolder); 2820 foreach (de; iterateDirectory(folder)) { 2821 if (de.name.startsWith(".")) continue; 2822 if (de.isDirectory) { 2823 copyFolderRec(folder ~ de.name, dstfolder ~ de.name); 2824 } else { 2825 if (de.name.endsWith(".o") || de.name.endsWith(".obj")) continue; 2826 if (de.name.endsWith(".exe")) continue; 2827 try copyFile(folder ~ de.name, dstfolder ~ de.name); 2828 catch (Exception e) { 2829 logWarn("Failed to copy file %s: %s", (folder ~ de.name).toNativeString(), e.msg); 2830 } 2831 } 2832 } 2833 } 2834 2835 static void fixPathDependency(in PackageName name, ref Dependency dep) { 2836 dep.visit!( 2837 (NativePath path) { 2838 dep = Dependency(NativePath("../") ~ name.main.toString()); 2839 }, 2840 (any) { /* Nothing to do */ }, 2841 ); 2842 } 2843 2844 void fixPathDependencies(ref PackageRecipe recipe, NativePath base_path) 2845 { 2846 foreach (name, ref dep; recipe.buildSettings.dependencies) 2847 fixPathDependency(PackageName(name), dep); 2848 2849 foreach (ref cfg; recipe.configurations) 2850 foreach (name, ref dep; cfg.buildSettings.dependencies) 2851 fixPathDependency(PackageName(name), dep); 2852 2853 foreach (ref subp; recipe.subPackages) 2854 if (subp.path.length) { 2855 auto sub_path = base_path ~ NativePath(subp.path); 2856 auto pack = dub.packageManager.getOrLoadPackage(sub_path); 2857 fixPathDependencies(pack.recipe, sub_path); 2858 pack.storeInfo(sub_path); 2859 } else fixPathDependencies(subp.recipe, base_path); 2860 } 2861 2862 bool[string] visited; 2863 foreach (pack_; prj.getTopologicalPackageList()) { 2864 auto pack = pack_.basePackage; 2865 if (pack.name in visited) continue; 2866 visited[pack.name] = true; 2867 auto dst_path = path ~ pack.name; 2868 logInfo("Prepare", Color.light_blue, "Copy package %s to destination folder...", pack.name.color(Mode.bold)); 2869 copyFolderRec(pack.path, dst_path); 2870 2871 // adjust all path based dependencies 2872 fixPathDependencies(pack.recipe, dst_path); 2873 2874 // overwrite package description file with additional version information 2875 pack.storeInfo(dst_path); 2876 } 2877 2878 logInfo("Starting", Color.light_green, "Executing dustmite..."); 2879 auto testcmd = appender!string(); 2880 if (m_timeout) 2881 testcmd.formattedWrite("timeout %s ", m_timeout); 2882 testcmd.formattedWrite("%s dustmite --test-package=%s --build=%s --config=%s", 2883 std.file.thisExePath, prj.name, this.baseSettings.buildType, this.baseSettings.config); 2884 2885 if (m_compilerName.length) testcmd.formattedWrite(" \"--compiler=%s\"", m_compilerName); 2886 if (m_arch.length) testcmd.formattedWrite(" --arch=%s", m_arch); 2887 if (m_compilerStatusCode != int.min) testcmd.formattedWrite(" --compiler-status=%s", m_compilerStatusCode); 2888 if (m_compilerRegex.length) testcmd.formattedWrite(" \"--compiler-regex=%s\"", m_compilerRegex); 2889 if (m_linkerStatusCode != int.min) testcmd.formattedWrite(" --linker-status=%s", m_linkerStatusCode); 2890 if (m_linkerRegex.length) testcmd.formattedWrite(" \"--linker-regex=%s\"", m_linkerRegex); 2891 if (m_programStatusCode != int.min) testcmd.formattedWrite(" --program-status=%s", m_programStatusCode); 2892 if (m_programRegex.length) testcmd.formattedWrite(" \"--program-regex=%s\"", m_programRegex); 2893 if (this.baseSettings.combined) testcmd ~= " --combined"; 2894 2895 // --vquiet swallows dustmite's output ... 2896 if (!m_noRedirect) testcmd ~= " --vquiet"; 2897 2898 // TODO: pass *all* original parameters 2899 logDiagnostic("Running dustmite: %s", testcmd); 2900 2901 string[] extraArgs; 2902 if (m_noRedirect) extraArgs ~= "--no-redirect"; 2903 if (m_strategy.length) extraArgs ~= "--strategy=" ~ m_strategy; 2904 if (m_jobCount) extraArgs ~= "-j" ~ m_jobCount.to!string; 2905 if (m_trace) extraArgs ~= "--trace"; 2906 2907 const cmd = "dustmite" ~ extraArgs ~ [path.toNativeString(), testcmd.data]; 2908 auto dmpid = spawnProcess(cmd); 2909 return dmpid.wait(); 2910 } 2911 return 0; 2912 } 2913 2914 void delegate(int, string) check(int code_match, string regex_match) 2915 { 2916 return (code, output) { 2917 import std.encoding; 2918 import std.regex; 2919 2920 logInfo("%s", output); 2921 2922 if (code_match != int.min && code != code_match) { 2923 logInfo("Exit code %s doesn't match expected value %s", code, code_match); 2924 throw new DustmiteMismatchException; 2925 } 2926 2927 if (regex_match.length > 0 && !match(output.sanitize, regex_match)) { 2928 logInfo("Output doesn't match regex:"); 2929 logInfo("%s", output); 2930 throw new DustmiteMismatchException; 2931 } 2932 2933 if (code != 0 && code_match != int.min || regex_match.length > 0) { 2934 logInfo("Tool failed, but matched either exit code or output - counting as match."); 2935 throw new DustmiteMatchException; 2936 } 2937 }; 2938 } 2939 2940 static class DustmiteMismatchException : Exception { 2941 this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null) 2942 { 2943 super(message, file, line, next); 2944 } 2945 } 2946 2947 static class DustmiteMatchException : Exception { 2948 this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null) 2949 { 2950 super(message, file, line, next); 2951 } 2952 } 2953 } 2954 2955 2956 /******************************************************************************/ 2957 /* CONVERT command */ 2958 /******************************************************************************/ 2959 2960 class ConvertCommand : Command { 2961 private { 2962 string m_format; 2963 bool m_stdout; 2964 } 2965 2966 this() @safe pure nothrow 2967 { 2968 this.name = "convert"; 2969 this.argumentsPattern = ""; 2970 this.description = "Converts the file format of the package recipe."; 2971 this.helpText = [ 2972 "This command will convert between JSON and SDLang formatted package recipe files.", 2973 "", 2974 "Warning: Beware that any formatting and comments within the package recipe will get lost in the conversion process." 2975 ]; 2976 } 2977 2978 override void prepare(scope CommandArgs args) 2979 { 2980 args.getopt("f|format", &m_format, ["Specifies the target package recipe format. Possible values:", " json, sdl"]); 2981 args.getopt("s|stdout", &m_stdout, ["Outputs the converted package recipe to stdout instead of writing to disk."]); 2982 } 2983 2984 override int execute(Dub dub, string[] free_args, string[] app_args) 2985 { 2986 enforceUsage(app_args.length == 0, "Unexpected application arguments."); 2987 enforceUsage(free_args.length == 0, "Unexpected arguments: "~free_args.join(" ")); 2988 enforceUsage(m_format.length > 0, "Missing target format file extension (--format=...)."); 2989 try this.m_format.to!PackageFormat; 2990 catch (Exception exc) 2991 enforceUsage(false, 2992 "Unsupported format type '%s', supported values are: %(%s, %)" 2993 .format(m_format, [ EnumMembers!PackageFormat ])); 2994 2995 if (!loadCwdPackage(dub, true)) return 2; 2996 dub.convertRecipe(m_format, m_stdout); 2997 return 0; 2998 } 2999 } 3000 3001 3002 /******************************************************************************/ 3003 /* HELP */ 3004 /******************************************************************************/ 3005 3006 private { 3007 enum shortArgColumn = 2; 3008 enum longArgColumn = 6; 3009 enum descColumn = 24; 3010 enum lineWidth = 80 - 1; 3011 } 3012 3013 private void showHelp(in CommandGroup[] commands, CommandArgs common_args) 3014 { 3015 writeln( 3016 `USAGE: dub [--version] [<command>] [<options...>] [-- [<application arguments...>]] 3017 3018 Manages the DUB project in the current directory. If the command is omitted, 3019 DUB will default to "run". When running an application, "--" can be used to 3020 separate DUB options from options passed to the application. 3021 3022 Run "dub <command> --help" to get help for a specific command. 3023 3024 You can use the "http_proxy" environment variable to configure a proxy server 3025 to be used for fetching packages. 3026 3027 3028 Available commands 3029 ==================`); 3030 3031 foreach (grp; commands) { 3032 writeln(); 3033 writeWS(shortArgColumn); 3034 writeln(grp.caption); 3035 writeWS(shortArgColumn); 3036 writerep!'-'(grp.caption.length); 3037 writeln(); 3038 foreach (cmd; grp.commands) { 3039 if (cmd.hidden) continue; 3040 writeWS(shortArgColumn); 3041 writef("%s %s", cmd.name, cmd.argumentsPattern); 3042 auto chars_output = cmd.name.length + cmd.argumentsPattern.length + shortArgColumn + 1; 3043 if (chars_output < descColumn) { 3044 writeWS(descColumn - chars_output); 3045 } else { 3046 writeln(); 3047 writeWS(descColumn); 3048 } 3049 writeWrapped(cmd.description, descColumn, descColumn); 3050 } 3051 } 3052 writeln(); 3053 writeln(); 3054 writeln(`Common options`); 3055 writeln(`==============`); 3056 writeln(); 3057 writeOptions(common_args); 3058 writeln(); 3059 showVersion(); 3060 } 3061 3062 private void showVersion() 3063 { 3064 writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__); 3065 } 3066 3067 private void showCommandHelp(Command cmd, CommandArgs args, CommandArgs common_args) 3068 { 3069 writefln(`USAGE: dub %s %s [<options...>]%s`, cmd.name, cmd.argumentsPattern, cmd.acceptsAppArgs ? " [-- <application arguments...>]": null); 3070 writeln(); 3071 foreach (ln; cmd.helpText) 3072 ln.writeWrapped(); 3073 3074 if (args.recognizedArgs.length) { 3075 writeln(); 3076 writeln(); 3077 writeln("Command specific options"); 3078 writeln("========================"); 3079 writeln(); 3080 writeOptions(args); 3081 } 3082 3083 writeln(); 3084 writeln(); 3085 writeln("Common options"); 3086 writeln("=============="); 3087 writeln(); 3088 writeOptions(common_args); 3089 writeln(); 3090 writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__); 3091 } 3092 3093 private void writeOptions(CommandArgs args) 3094 { 3095 foreach (arg; args.recognizedArgs) { 3096 if (arg.hidden) continue; 3097 auto names = arg.names.split("|"); 3098 assert(names.length == 1 || names.length == 2); 3099 string sarg = names[0].length == 1 ? names[0] : null; 3100 string larg = names[0].length > 1 ? names[0] : names.length > 1 ? names[1] : null; 3101 if (sarg !is null) { 3102 writeWS(shortArgColumn); 3103 writef("-%s", sarg); 3104 writeWS(longArgColumn - shortArgColumn - 2); 3105 } else writeWS(longArgColumn); 3106 size_t col = longArgColumn; 3107 if (larg !is null) { 3108 arg.defaultValue.match!( 3109 (bool b) { 3110 writef("--%s", larg); 3111 col += larg.length + 2; 3112 }, 3113 (_) { 3114 writef("--%s=VALUE", larg); 3115 col += larg.length + 8; 3116 } 3117 ); 3118 } 3119 if (col < descColumn) { 3120 writeWS(descColumn - col); 3121 } else { 3122 writeln(); 3123 writeWS(descColumn); 3124 } 3125 foreach (i, ln; arg.helpText) { 3126 if (i > 0) writeWS(descColumn); 3127 ln.writeWrapped(descColumn, descColumn); 3128 } 3129 } 3130 } 3131 3132 private void writeWrapped(string string, size_t indent = 0, size_t first_line_pos = 0) 3133 { 3134 // handle pre-indented strings and bullet lists 3135 size_t first_line_indent = 0; 3136 while (string.startsWith(" ")) { 3137 string = string[1 .. $]; 3138 indent++; 3139 first_line_indent++; 3140 } 3141 if (string.startsWith("- ")) indent += 2; 3142 3143 auto wrapped = string.wrap(lineWidth, getRepString!' '(first_line_pos+first_line_indent), getRepString!' '(indent)); 3144 wrapped = wrapped[first_line_pos .. $]; 3145 foreach (ln; wrapped.splitLines()) 3146 writeln(ln); 3147 } 3148 3149 private void writeWS(size_t num) { writerep!' '(num); } 3150 private void writerep(char ch)(size_t num) { write(getRepString!ch(num)); } 3151 3152 private string getRepString(char ch)(size_t len) 3153 { 3154 static string buf; 3155 if (len > buf.length) buf ~= [ch].replicate(len-buf.length); 3156 return buf[0 .. len]; 3157 } 3158 3159 /*** 3160 */ 3161 3162 3163 private void enforceUsage(bool cond, string text) 3164 { 3165 if (!cond) throw new UsageException(text); 3166 } 3167 3168 private class UsageException : Exception { 3169 this(string message, string file = __FILE__, int line = __LINE__, Throwable next = null) 3170 { 3171 super(message, file, line, next); 3172 } 3173 } 3174 3175 private bool addDependency(Dub dub, ref PackageRecipe recipe, string depspec) 3176 { 3177 Dependency dep; 3178 const parts = UserPackageDesc.fromString(depspec); 3179 const depname = PackageName(parts.name); 3180 if (parts.range == VersionRange.Any) 3181 { 3182 try { 3183 const ver = dub.getLatestVersion(depname); 3184 dep = ver.isBranch ? Dependency(ver) : Dependency("~>" ~ ver.toString()); 3185 } catch (Exception e) { 3186 logError("Could not find package '%s'.", depname); 3187 logDebug("Full error: %s", e.toString().sanitize); 3188 return false; 3189 } 3190 } 3191 else 3192 dep = Dependency(parts.range); 3193 recipe.buildSettings.dependencies[depname.toString()] = dep; 3194 logInfo("Adding dependency %s %s", depname, dep.toString()); 3195 return true; 3196 } 3197 3198 /** 3199 * A user-provided package description 3200 * 3201 * User provided package description currently only covers packages 3202 * referenced by their name with an associated version. 3203 * Hence there is an implicit assumption that they are in the registry. 3204 * Future improvements could support `Dependency` instead of `VersionRange`. 3205 */ 3206 private struct UserPackageDesc 3207 { 3208 string name; 3209 VersionRange range = VersionRange.Any; 3210 3211 /// Provides a string representation for the user 3212 public string toString() const 3213 { 3214 if (this.range.matchesAny()) 3215 return this.name; 3216 return format("%s@%s", this.name, range); 3217 } 3218 3219 /** 3220 * Breaks down a user-provided string into its name and version range 3221 * 3222 * User-provided strings (via the command line) are either in the form 3223 * `<package>=<version-specifier>` or `<package>@<version-specifier>`. 3224 * As it is more explicit, we recommend the latter (the `@` version 3225 * is not used by names or `VersionRange`, but `=` is). 3226 * 3227 * If no version range is provided, the returned struct has its `range` 3228 * property set to `VersionRange.Any` as this is the most usual usage 3229 * in the command line. Some cakkers may want to distinguish between 3230 * user-provided version and implicit version, but this is discouraged. 3231 * 3232 * Params: 3233 * str = User-provided string 3234 * 3235 * Returns: 3236 * A populated struct. 3237 */ 3238 static UserPackageDesc fromString(string packageName) 3239 { 3240 // split <package>@<version-specifier> 3241 auto parts = packageName.findSplit("@"); 3242 if (parts[1].empty) { 3243 // split <package>=<version-specifier> 3244 parts = packageName.findSplit("="); 3245 } 3246 3247 UserPackageDesc p; 3248 p.name = parts[0]; 3249 p.range = !parts[1].empty 3250 ? VersionRange.fromString(parts[2]) 3251 : VersionRange.Any; 3252 return p; 3253 } 3254 } 3255 3256 unittest 3257 { 3258 // https://github.com/dlang/dub/issues/1681 3259 assert(UserPackageDesc.fromString("") == UserPackageDesc("", VersionRange.Any)); 3260 3261 assert(UserPackageDesc.fromString("foo") == UserPackageDesc("foo", VersionRange.Any)); 3262 assert(UserPackageDesc.fromString("foo=1.0.1") == UserPackageDesc("foo", VersionRange.fromString("1.0.1"))); 3263 assert(UserPackageDesc.fromString("foo@1.0.1") == UserPackageDesc("foo", VersionRange.fromString("1.0.1"))); 3264 assert(UserPackageDesc.fromString("foo@==1.0.1") == UserPackageDesc("foo", VersionRange.fromString("==1.0.1"))); 3265 assert(UserPackageDesc.fromString("foo@>=1.0.1") == UserPackageDesc("foo", VersionRange.fromString(">=1.0.1"))); 3266 assert(UserPackageDesc.fromString("foo@~>1.0.1") == UserPackageDesc("foo", VersionRange.fromString("~>1.0.1"))); 3267 assert(UserPackageDesc.fromString("foo@<1.0.1") == UserPackageDesc("foo", VersionRange.fromString("<1.0.1"))); 3268 } 3269 3270 private ulong canFindVersionSplitter(string packageName) 3271 { 3272 // see UserPackageDesc.fromString 3273 return packageName.canFind("@", "="); 3274 } 3275 3276 unittest 3277 { 3278 assert(!canFindVersionSplitter("foo")); 3279 assert(canFindVersionSplitter("foo=1.0.1")); 3280 assert(canFindVersionSplitter("foo@1.0.1")); 3281 assert(canFindVersionSplitter("foo@==1.0.1")); 3282 assert(canFindVersionSplitter("foo@>=1.0.1")); 3283 assert(canFindVersionSplitter("foo@~>1.0.1")); 3284 assert(canFindVersionSplitter("foo@<1.0.1")); 3285 }