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