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