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