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