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