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