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