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