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 : 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.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 " debug (default), plain, release, release-debug, release-nobounds, unittest, profile, profile-gc, docs, ddox, cov, unittest-cov, syntax 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 m_compiler = getCompiler(m_compilerName); 1003 m_buildPlatform = m_compiler.determinePlatform(m_buildSettings, m_compilerName, m_arch); 1004 m_buildSettings.addDebugVersions(m_debugVersions); 1005 1006 m_defaultConfig = null; 1007 enforce (loadSpecificPackage(dub, package_name, ver), "Failed to load package."); 1008 1009 if (m_buildConfig.length != 0 && !dub.configurations.canFind(m_buildConfig)) 1010 { 1011 string msg = "Unknown build configuration: "~m_buildConfig; 1012 enum distance = 3; 1013 auto match = dub.configurations.getClosestMatch(m_buildConfig, distance); 1014 if (match !is null) msg ~= ". Did you mean '" ~ match ~ "'?"; 1015 enforce(0, msg); 1016 } 1017 1018 if (m_buildType.length == 0) { 1019 if (environment.get("DFLAGS") !is null) m_buildType = "$DFLAGS"; 1020 else m_buildType = default_build_type; 1021 } 1022 1023 if (!m_nodeps) { 1024 // retrieve missing packages 1025 dub.project.reinit(); 1026 if (!dub.project.hasAllDependencies) { 1027 logDiagnostic("Checking for missing dependencies."); 1028 if (m_single) dub.upgrade(UpgradeOptions.select | UpgradeOptions.noSaveSelections); 1029 else dub.upgrade(UpgradeOptions.select); 1030 } 1031 } 1032 1033 dub.project.validate(); 1034 1035 foreach (sc; m_overrideConfigs) { 1036 auto idx = sc.indexOf('/'); 1037 enforceUsage(idx >= 0, "Expected \"<package>/<configuration>\" as argument to --override-config."); 1038 dub.project.overrideConfiguration(sc[0 .. idx], sc[idx+1 .. $]); 1039 } 1040 } 1041 1042 private bool loadSpecificPackage(Dub dub, string package_name, string ver) 1043 { 1044 if (m_single) { 1045 enforce(package_name.length, "Missing file name of single-file package."); 1046 dub.loadSingleFilePackage(package_name); 1047 return true; 1048 } 1049 1050 bool from_cwd = package_name.length == 0 || package_name.startsWith(":"); 1051 // load package in root_path to enable searching for sub packages 1052 if (loadCwdPackage(dub, from_cwd)) { 1053 if (package_name.startsWith(":")) 1054 { 1055 auto pack = dub.packageManager.getSubPackage(dub.project.rootPackage, package_name[1 .. $], false); 1056 dub.loadPackage(pack); 1057 return true; 1058 } 1059 if (from_cwd) return true; 1060 } 1061 1062 enforce(package_name.length, "No valid root package found - aborting."); 1063 1064 auto pack = ver == "" 1065 ? dub.packageManager.getLatestPackage(package_name) 1066 : dub.packageManager.getBestPackage(package_name, ver); 1067 1068 enforce(pack, format!"Failed to find a package named '%s%s' locally."(package_name, 1069 ver == "" ? "" : ("@" ~ ver) 1070 )); 1071 logInfo("Building package %s in %s", pack.name, pack.path.toNativeString()); 1072 dub.loadPackage(pack); 1073 return true; 1074 } 1075 } 1076 1077 class GenerateCommand : PackageBuildCommand { 1078 protected { 1079 string m_generator; 1080 bool m_rdmd = false; 1081 bool m_tempBuild = false; 1082 bool m_run = false; 1083 bool m_force = false; 1084 bool m_combined = false; 1085 bool m_parallel = false; 1086 bool m_printPlatform, m_printBuilds, m_printConfigs; 1087 } 1088 1089 this() @safe pure nothrow 1090 { 1091 this.name = "generate"; 1092 this.argumentsPattern = "<generator> [<package>[@<version-spec>]]"; 1093 this.description = "Generates project files using the specified generator"; 1094 this.helpText = [ 1095 "Generates project files using one of the supported generators:", 1096 "", 1097 "visuald - VisualD project files", 1098 "sublimetext - SublimeText project file", 1099 "cmake - CMake build scripts", 1100 "build - Builds the package directly", 1101 "", 1102 "An optional package name can be given to generate a different package than the root/CWD package." 1103 ]; 1104 } 1105 1106 override void prepare(scope CommandArgs args) 1107 { 1108 super.prepare(args); 1109 1110 args.getopt("combined", &m_combined, [ 1111 "Tries to build the whole project in a single compiler run." 1112 ]); 1113 1114 args.getopt("print-builds", &m_printBuilds, [ 1115 "Prints the list of available build types" 1116 ]); 1117 args.getopt("print-configs", &m_printConfigs, [ 1118 "Prints the list of available configurations" 1119 ]); 1120 args.getopt("print-platform", &m_printPlatform, [ 1121 "Prints the identifiers for the current build platform as used for the build fields in dub.json" 1122 ]); 1123 args.getopt("parallel", &m_parallel, [ 1124 "Runs multiple compiler instances in parallel, if possible." 1125 ]); 1126 } 1127 1128 override int execute(Dub dub, string[] free_args, string[] app_args) 1129 { 1130 string str_package_info; 1131 if (!m_generator.length) { 1132 enforceUsage(free_args.length >= 1 && free_args.length <= 2, "Expected one or two arguments."); 1133 m_generator = free_args[0]; 1134 if (free_args.length >= 2) str_package_info = free_args[1]; 1135 } else { 1136 enforceUsage(free_args.length <= 1, "Expected one or zero arguments."); 1137 if (free_args.length >= 1) str_package_info = free_args[0]; 1138 } 1139 1140 setupVersionPackage(dub, str_package_info, "debug"); 1141 1142 if (m_printBuilds) { // FIXME: use actual package data 1143 logInfo("Available build types:"); 1144 foreach (tp; ["debug", "release", "unittest", "profile"]) 1145 logInfo(" %s", tp); 1146 logInfo(""); 1147 } 1148 1149 m_defaultConfig = dub.project.getDefaultConfiguration(m_buildPlatform); 1150 if (m_printConfigs) { 1151 logInfo("Available configurations:"); 1152 foreach (tp; dub.configurations) 1153 logInfo(" %s%s", tp, tp == m_defaultConfig ? " [default]" : null); 1154 logInfo(""); 1155 } 1156 1157 GeneratorSettings gensettings; 1158 gensettings.platform = m_buildPlatform; 1159 gensettings.config = m_buildConfig.length ? m_buildConfig : m_defaultConfig; 1160 gensettings.buildType = m_buildType; 1161 gensettings.buildMode = m_buildMode; 1162 gensettings.compiler = m_compiler; 1163 gensettings.buildSettings = m_buildSettings; 1164 gensettings.combined = m_combined; 1165 gensettings.filterVersions = m_filterVersions; 1166 gensettings.run = m_run; 1167 gensettings.runArgs = app_args; 1168 gensettings.force = m_force; 1169 gensettings.rdmd = m_rdmd; 1170 gensettings.tempBuild = m_tempBuild; 1171 gensettings.parallelBuild = m_parallel; 1172 gensettings.single = m_single; 1173 1174 logDiagnostic("Generating using %s", m_generator); 1175 dub.generateProject(m_generator, gensettings); 1176 if (m_buildType == "ddox") dub.runDdox(gensettings.run, app_args); 1177 return 0; 1178 } 1179 } 1180 1181 class BuildCommand : GenerateCommand { 1182 protected { 1183 bool m_yes; // automatic yes to prompts; 1184 bool m_nonInteractive; 1185 } 1186 this() @safe pure nothrow 1187 { 1188 this.name = "build"; 1189 this.argumentsPattern = "[<package>[@<version-spec>]]"; 1190 this.description = "Builds a package (uses the main package in the current working directory by default)"; 1191 this.helpText = [ 1192 "Builds a package (uses the main package in the current working directory by default)" 1193 ]; 1194 } 1195 1196 override void prepare(scope CommandArgs args) 1197 { 1198 args.getopt("rdmd", &m_rdmd, [ 1199 "Use rdmd instead of directly invoking the compiler" 1200 ]); 1201 1202 args.getopt("f|force", &m_force, [ 1203 "Forces a recompilation even if the target is up to date" 1204 ]); 1205 args.getopt("y|yes", &m_yes, [ 1206 `Automatic yes to prompts. Assume "yes" as answer to all interactive prompts.` 1207 ]); 1208 args.getopt("n|non-interactive", &m_nonInteractive, [ 1209 "Don't enter interactive mode." 1210 ]); 1211 super.prepare(args); 1212 m_generator = "build"; 1213 } 1214 1215 override int execute(Dub dub, string[] free_args, string[] app_args) 1216 { 1217 // single package files don't need to be downloaded, they are on the disk. 1218 if (free_args.length < 1 || m_single) 1219 return super.execute(dub, free_args, app_args); 1220 1221 if (!m_nonInteractive) 1222 { 1223 const packageParts = splitPackageName(free_args[0]); 1224 if (auto rc = fetchMissingPackages(dub, packageParts)) 1225 return rc; 1226 } 1227 return super.execute(dub, free_args, app_args); 1228 } 1229 1230 private int fetchMissingPackages(Dub dub, in PackageAndVersion packageParts) 1231 { 1232 1233 static bool input(string caption, bool default_value = true) { 1234 writef("%s [%s]: ", caption, default_value ? "Y/n" : "y/N"); 1235 auto inp = readln(); 1236 string userInput = "y"; 1237 if (inp.length > 1) 1238 userInput = inp[0 .. $ - 1].toLower; 1239 1240 switch (userInput) { 1241 case "no", "n", "0": 1242 return false; 1243 case "yes", "y", "1": 1244 default: 1245 return true; 1246 } 1247 } 1248 1249 Dependency dep; 1250 1251 if (packageParts.version_.length > 0) { 1252 // the user provided a version manually 1253 dep = Dependency(packageParts.version_); 1254 } else { 1255 if (packageParts.name.startsWith(":") || 1256 dub.packageManager.getFirstPackage(packageParts.name)) 1257 // found locally 1258 return 0; 1259 1260 // search for the package and filter versions for exact matches 1261 auto basePackageName = getBasePackageName(packageParts.name); 1262 auto search = dub.searchPackages(basePackageName) 1263 .map!(tup => tup[1].find!(p => p.name == basePackageName)) 1264 .filter!(ps => !ps.empty); 1265 if (search.empty) { 1266 logWarn("Package '%s' was neither found locally nor online.", packageParts.name); 1267 return 2; 1268 } 1269 1270 const p = search.front.front; 1271 logInfo("Package '%s' was not found locally but is available online:", packageParts.name); 1272 logInfo("---"); 1273 logInfo("Description: %s", p.description); 1274 logInfo("Version: %s", p.version_); 1275 logInfo("---"); 1276 1277 const answer = m_yes ? true : input("Do you want to fetch '%s' now?".format(packageParts.name)); 1278 if (!answer) 1279 return 0; 1280 dep = Dependency(p.version_); 1281 } 1282 1283 dub.fetch(packageParts.name, dep, dub.defaultPlacementLocation, FetchOptions.none); 1284 return 0; 1285 } 1286 } 1287 1288 class RunCommand : BuildCommand { 1289 this() @safe pure nothrow 1290 { 1291 this.name = "run"; 1292 this.argumentsPattern = "[<package>[@<version-spec>]]"; 1293 this.description = "Builds and runs a package (default command)"; 1294 this.helpText = [ 1295 "Builds and runs a package (uses the main package in the current working directory by default)" 1296 ]; 1297 this.acceptsAppArgs = true; 1298 } 1299 1300 override void prepare(scope CommandArgs args) 1301 { 1302 args.getopt("temp-build", &m_tempBuild, [ 1303 "Builds the project in the temp folder if possible." 1304 ]); 1305 1306 super.prepare(args); 1307 m_run = true; 1308 } 1309 1310 override int execute(Dub dub, string[] free_args, string[] app_args) 1311 { 1312 return super.execute(dub, free_args, app_args); 1313 } 1314 } 1315 1316 class TestCommand : PackageBuildCommand { 1317 private { 1318 string m_mainFile; 1319 bool m_combined = false; 1320 bool m_parallel = false; 1321 bool m_force = false; 1322 } 1323 1324 this() @safe pure nothrow 1325 { 1326 this.name = "test"; 1327 this.argumentsPattern = "[<package>[@<version-spec>]]"; 1328 this.description = "Executes the tests of the selected package"; 1329 this.helpText = [ 1330 `Builds the package and executes all contained unit tests.`, 1331 ``, 1332 `If no explicit configuration is given, an existing "unittest" ` ~ 1333 `configuration will be preferred for testing. If none exists, the ` ~ 1334 `first library type configuration will be used, and if that doesn't ` ~ 1335 `exist either, the first executable configuration is chosen.`, 1336 ``, 1337 `When a custom main file (--main-file) is specified, only library ` ~ 1338 `configurations can be used. Otherwise, depending on the type of ` ~ 1339 `the selected configuration, either an existing main file will be ` ~ 1340 `used (and needs to be properly adjusted to just run the unit ` ~ 1341 `tests for 'version(unittest)'), or DUB will generate one for ` ~ 1342 `library type configurations.`, 1343 ``, 1344 `Finally, if the package contains a dependency to the "tested" ` ~ 1345 `package, the automatically generated main file will use it to ` ~ 1346 `run the unit tests.` 1347 ]; 1348 this.acceptsAppArgs = true; 1349 } 1350 1351 override void prepare(scope CommandArgs args) 1352 { 1353 args.getopt("main-file", &m_mainFile, [ 1354 "Specifies a custom file containing the main() function to use for running the tests." 1355 ]); 1356 args.getopt("combined", &m_combined, [ 1357 "Tries to build the whole project in a single compiler run." 1358 ]); 1359 args.getopt("parallel", &m_parallel, [ 1360 "Runs multiple compiler instances in parallel, if possible." 1361 ]); 1362 args.getopt("f|force", &m_force, [ 1363 "Forces a recompilation even if the target is up to date" 1364 ]); 1365 bool coverage = false; 1366 args.getopt("coverage", &coverage, [ 1367 "Enables code coverage statistics to be generated." 1368 ]); 1369 if (coverage) m_buildType = "unittest-cov"; 1370 1371 super.prepare(args); 1372 } 1373 1374 override int execute(Dub dub, string[] free_args, string[] app_args) 1375 { 1376 string str_package_info; 1377 enforceUsage(free_args.length <= 1, "Expected one or zero arguments."); 1378 if (free_args.length >= 1) str_package_info = free_args[0]; 1379 1380 setupVersionPackage(dub, str_package_info, "unittest"); 1381 1382 GeneratorSettings settings; 1383 settings.platform = m_buildPlatform; 1384 settings.compiler = getCompiler(m_buildPlatform.compilerBinary); 1385 settings.buildType = m_buildType; 1386 settings.buildMode = m_buildMode; 1387 settings.buildSettings = m_buildSettings; 1388 settings.combined = m_combined; 1389 settings.filterVersions = m_filterVersions; 1390 settings.parallelBuild = m_parallel; 1391 settings.force = m_force; 1392 settings.tempBuild = m_single; 1393 settings.run = true; 1394 settings.runArgs = app_args; 1395 settings.single = m_single; 1396 1397 dub.testProject(settings, m_buildConfig, NativePath(m_mainFile)); 1398 return 0; 1399 } 1400 } 1401 1402 class LintCommand : PackageBuildCommand { 1403 private { 1404 bool m_syntaxCheck = false; 1405 bool m_styleCheck = false; 1406 string m_errorFormat; 1407 bool m_report = false; 1408 string m_reportFormat; 1409 string m_reportFile; 1410 string[] m_importPaths; 1411 string m_config; 1412 } 1413 1414 this() @safe pure nothrow 1415 { 1416 this.name = "lint"; 1417 this.argumentsPattern = "[<package>[@<version-spec>]]"; 1418 this.description = "Executes the linter tests of the selected package"; 1419 this.helpText = [ 1420 `Builds the package and executes D-Scanner linter tests.` 1421 ]; 1422 this.acceptsAppArgs = true; 1423 } 1424 1425 override void prepare(scope CommandArgs args) 1426 { 1427 args.getopt("syntax-check", &m_syntaxCheck, [ 1428 "Lexes and parses sourceFile, printing the line and column number of " ~ 1429 "any syntax errors to stdout." 1430 ]); 1431 1432 args.getopt("style-check", &m_styleCheck, [ 1433 "Lexes and parses sourceFiles, printing the line and column number of " ~ 1434 "any static analysis check failures stdout." 1435 ]); 1436 1437 args.getopt("error-format", &m_errorFormat, [ 1438 "Format errors produced by the style/syntax checkers." 1439 ]); 1440 1441 args.getopt("report", &m_report, [ 1442 "Generate a static analysis report in JSON format." 1443 ]); 1444 1445 args.getopt("report-format", &m_reportFormat, [ 1446 "Specifies the format of the generated report." 1447 ]); 1448 1449 args.getopt("report-file", &m_reportFile, [ 1450 "Write report to file." 1451 ]); 1452 1453 if (m_reportFormat || m_reportFile) m_report = true; 1454 1455 args.getopt("import-paths", &m_importPaths, [ 1456 "Import paths" 1457 ]); 1458 1459 args.getopt("config", &m_config, [ 1460 "Use the given configuration file." 1461 ]); 1462 1463 super.prepare(args); 1464 } 1465 1466 override int execute(Dub dub, string[] free_args, string[] app_args) 1467 { 1468 string str_package_info; 1469 enforceUsage(free_args.length <= 1, "Expected one or zero arguments."); 1470 if (free_args.length >= 1) str_package_info = free_args[0]; 1471 1472 string[] args; 1473 if (!m_syntaxCheck && !m_styleCheck && !m_report && app_args.length == 0) { m_styleCheck = true; } 1474 1475 if (m_syntaxCheck) args ~= "--syntaxCheck"; 1476 if (m_styleCheck) args ~= "--styleCheck"; 1477 if (m_errorFormat) args ~= ["--errorFormat", m_errorFormat]; 1478 if (m_report) args ~= "--report"; 1479 if (m_reportFormat) args ~= ["--reportFormat", m_reportFormat]; 1480 if (m_reportFile) args ~= ["--reportFile", m_reportFile]; 1481 foreach (import_path; m_importPaths) args ~= ["-I", import_path]; 1482 if (m_config) args ~= ["--config", m_config]; 1483 1484 setupVersionPackage(dub, str_package_info); 1485 dub.lintProject(args ~ app_args); 1486 return 0; 1487 } 1488 } 1489 1490 class DescribeCommand : PackageBuildCommand { 1491 private { 1492 bool m_importPaths = false; 1493 bool m_stringImportPaths = false; 1494 bool m_dataList = false; 1495 bool m_dataNullDelim = false; 1496 string[] m_data; 1497 } 1498 1499 this() @safe pure nothrow 1500 { 1501 this.name = "describe"; 1502 this.argumentsPattern = "[<package>[@<version-spec>]]"; 1503 this.description = "Prints a JSON description of the project and its dependencies"; 1504 this.helpText = [ 1505 "Prints a JSON build description for the root package an all of " ~ 1506 "their dependencies in a format similar to a JSON package " ~ 1507 "description file. This is useful mostly for IDEs.", 1508 "", 1509 "All usual options that are also used for build/run/generate apply.", 1510 "", 1511 "When --data=VALUE is supplied, specific build settings for a project " ~ 1512 "will be printed instead (by default, formatted for the current compiler).", 1513 "", 1514 "The --data=VALUE option can be specified multiple times to retrieve " ~ 1515 "several pieces of information at once. A comma-separated list is " ~ 1516 "also acceptable (ex: --data=dflags,libs). The data will be output in " ~ 1517 "the same order requested on the command line.", 1518 "", 1519 "The accepted values for --data=VALUE are:", 1520 "", 1521 "main-source-file, dflags, lflags, libs, linker-files, " ~ 1522 "source-files, versions, debug-versions, import-paths, " ~ 1523 "string-import-paths, import-files, options", 1524 "", 1525 "The following are also accepted by --data if --data-list is used:", 1526 "", 1527 "target-type, target-path, target-name, working-directory, " ~ 1528 "copy-files, string-import-files, pre-generate-commands, " ~ 1529 "post-generate-commands, pre-build-commands, post-build-commands, " ~ 1530 "requirements", 1531 ]; 1532 } 1533 1534 override void prepare(scope CommandArgs args) 1535 { 1536 super.prepare(args); 1537 1538 args.getopt("import-paths", &m_importPaths, [ 1539 "Shortcut for --data=import-paths --data-list" 1540 ]); 1541 1542 args.getopt("string-import-paths", &m_stringImportPaths, [ 1543 "Shortcut for --data=string-import-paths --data-list" 1544 ]); 1545 1546 args.getopt("data", &m_data, [ 1547 "Just list the values of a particular build setting, either for this "~ 1548 "package alone or recursively including all dependencies. Accepts a "~ 1549 "comma-separated list. See above for more details and accepted "~ 1550 "possibilities for VALUE." 1551 ]); 1552 1553 args.getopt("data-list", &m_dataList, [ 1554 "Output --data information in list format (line-by-line), instead "~ 1555 "of formatting for a compiler command line.", 1556 ]); 1557 1558 args.getopt("data-0", &m_dataNullDelim, [ 1559 "Output --data information using null-delimiters, rather than "~ 1560 "spaces or newlines. Result is usable with, ex., xargs -0.", 1561 ]); 1562 } 1563 1564 override int execute(Dub dub, string[] free_args, string[] app_args) 1565 { 1566 enforceUsage( 1567 !(m_importPaths && m_stringImportPaths), 1568 "--import-paths and --string-import-paths may not be used together." 1569 ); 1570 1571 enforceUsage( 1572 !(m_data && (m_importPaths || m_stringImportPaths)), 1573 "--data may not be used together with --import-paths or --string-import-paths." 1574 ); 1575 1576 // disable all log output to stdout and use "writeln" to output the JSON description 1577 auto ll = getLogLevel(); 1578 setLogLevel(max(ll, LogLevel.warn)); 1579 scope (exit) setLogLevel(ll); 1580 1581 string str_package_info; 1582 enforceUsage(free_args.length <= 1, "Expected one or zero arguments."); 1583 if (free_args.length >= 1) str_package_info = free_args[0]; 1584 setupVersionPackage(dub, str_package_info); 1585 1586 m_defaultConfig = dub.project.getDefaultConfiguration(m_buildPlatform); 1587 1588 auto config = m_buildConfig.length ? m_buildConfig : m_defaultConfig; 1589 1590 GeneratorSettings settings; 1591 settings.platform = m_buildPlatform; 1592 settings.config = config; 1593 settings.buildType = m_buildType; 1594 settings.compiler = m_compiler; 1595 settings.filterVersions = m_filterVersions; 1596 settings.buildSettings.options |= m_buildSettings.options & BuildOption.lowmem; 1597 1598 if (m_importPaths) { m_data = ["import-paths"]; m_dataList = true; } 1599 else if (m_stringImportPaths) { m_data = ["string-import-paths"]; m_dataList = true; } 1600 1601 if (m_data.length) { 1602 ListBuildSettingsFormat lt; 1603 with (ListBuildSettingsFormat) 1604 lt = m_dataList ? (m_dataNullDelim ? listNul : list) : (m_dataNullDelim ? commandLineNul : commandLine); 1605 dub.listProjectData(settings, m_data, lt); 1606 } else { 1607 auto desc = dub.project.describe(settings); 1608 writeln(desc.serializeToPrettyJson()); 1609 } 1610 1611 return 0; 1612 } 1613 } 1614 1615 class CleanCommand : Command { 1616 private { 1617 bool m_allPackages; 1618 } 1619 1620 this() @safe pure nothrow 1621 { 1622 this.name = "clean"; 1623 this.argumentsPattern = "[<package>]"; 1624 this.description = "Removes intermediate build files and cached build results"; 1625 this.helpText = [ 1626 "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.", 1627 "Without arguments, the package in the current working directory will be cleaned." 1628 ]; 1629 } 1630 1631 override void prepare(scope CommandArgs args) 1632 { 1633 args.getopt("all-packages", &m_allPackages, [ 1634 "Cleans up *all* known packages (dub list)" 1635 ]); 1636 } 1637 1638 override int execute(Dub dub, string[] free_args, string[] app_args) 1639 { 1640 enforceUsage(free_args.length <= 1, "Expected one or zero arguments."); 1641 enforceUsage(app_args.length == 0, "Application arguments are not supported for the clean command."); 1642 enforceUsage(!m_allPackages || !free_args.length, "The --all-packages flag may not be used together with an explicit package name."); 1643 1644 enforce(free_args.length == 0, "Cleaning a specific package isn't possible right now."); 1645 1646 if (m_allPackages) { 1647 bool any_error = false; 1648 1649 foreach (p; dub.packageManager.getPackageIterator()) { 1650 try dub.cleanPackage(p.path); 1651 catch (Exception e) { 1652 logWarn("Failed to clean package %s at %s: %s", p.name, p.path, e.msg); 1653 any_error = true; 1654 } 1655 1656 foreach (sp; p.subPackages.filter!(sp => !sp.path.empty)) { 1657 try dub.cleanPackage(p.path ~ sp.path); 1658 catch (Exception e) { 1659 logWarn("Failed to clean sub package of %s at %s: %s", p.name, p.path ~ sp.path, e.msg); 1660 any_error = true; 1661 } 1662 } 1663 } 1664 1665 if (any_error) return 1; 1666 } else { 1667 dub.cleanPackage(dub.rootPath); 1668 } 1669 1670 return 0; 1671 } 1672 } 1673 1674 1675 /******************************************************************************/ 1676 /* FETCH / ADD / REMOVE / UPGRADE */ 1677 /******************************************************************************/ 1678 1679 class AddCommand : Command { 1680 this() @safe pure nothrow 1681 { 1682 this.name = "add"; 1683 this.argumentsPattern = "<package>[@<version-spec>] [<packages...>]"; 1684 this.description = "Adds dependencies to the package file."; 1685 this.helpText = [ 1686 "Adds <packages> as dependencies.", 1687 "", 1688 "Running \"dub add <package>\" is the same as adding <package> to the \"dependencies\" section in dub.json/dub.sdl.", 1689 "If no version is specified for one of the packages, dub will query the registry for the latest version." 1690 ]; 1691 } 1692 1693 override void prepare(scope CommandArgs args) {} 1694 1695 override int execute(Dub dub, string[] free_args, string[] app_args) 1696 { 1697 import dub.recipe.io : readPackageRecipe, writePackageRecipe; 1698 import dub.internal.vibecompat.core.file : existsFile; 1699 enforceUsage(free_args.length != 0, "Expected one or more arguments."); 1700 enforceUsage(app_args.length == 0, "Unexpected application arguments."); 1701 1702 if (!loadCwdPackage(dub, true)) return 1; 1703 auto recipe = dub.project.rootPackage.rawRecipe.clone; 1704 1705 foreach (depspec; free_args) { 1706 if (!addDependency(dub, recipe, depspec)) 1707 return 1; 1708 } 1709 writePackageRecipe(dub.project.rootPackage.recipePath, recipe); 1710 1711 return 0; 1712 } 1713 } 1714 1715 class UpgradeCommand : Command { 1716 private { 1717 bool m_prerelease = false; 1718 bool m_forceRemove = false; 1719 bool m_missingOnly = false; 1720 bool m_verify = false; 1721 bool m_dryRun = false; 1722 } 1723 1724 this() @safe pure nothrow 1725 { 1726 this.name = "upgrade"; 1727 this.argumentsPattern = "[<packages...>]"; 1728 this.description = "Forces an upgrade of the dependencies"; 1729 this.helpText = [ 1730 "Upgrades all dependencies of the package by querying the package registry(ies) for new versions.", 1731 "", 1732 "This will update the versions stored in the selections file ("~SelectedVersions.defaultFile~") accordingly.", 1733 "", 1734 "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." 1735 ]; 1736 } 1737 1738 override void prepare(scope CommandArgs args) 1739 { 1740 args.getopt("prerelease", &m_prerelease, [ 1741 "Uses the latest pre-release version, even if release versions are available" 1742 ]); 1743 args.getopt("verify", &m_verify, [ 1744 "Updates the project and performs a build. If successful, rewrites the selected versions file <to be implemented>." 1745 ]); 1746 args.getopt("dry-run", &m_dryRun, [ 1747 "Only print what would be upgraded, but don't actually upgrade anything." 1748 ]); 1749 args.getopt("missing-only", &m_missingOnly, [ 1750 "Performs an upgrade only for dependencies that don't yet have a version selected. This is also done automatically before each build." 1751 ]); 1752 args.getopt("force-remove", &m_forceRemove, [ 1753 "Deprecated option that does nothing." 1754 ]); 1755 } 1756 1757 override int execute(Dub dub, string[] free_args, string[] app_args) 1758 { 1759 enforceUsage(free_args.length <= 1, "Unexpected arguments."); 1760 enforceUsage(app_args.length == 0, "Unexpected application arguments."); 1761 enforceUsage(!m_verify, "--verify is not yet implemented."); 1762 enforce(loadCwdPackage(dub, true), "Failed to load package."); 1763 logInfo("Upgrading project in %s", dub.projectPath.toNativeString()); 1764 auto options = UpgradeOptions.upgrade|UpgradeOptions.select; 1765 if (m_missingOnly) options &= ~UpgradeOptions.upgrade; 1766 if (m_prerelease) options |= UpgradeOptions.preRelease; 1767 if (m_dryRun) options |= UpgradeOptions.dryRun; 1768 dub.upgrade(options, free_args); 1769 return 0; 1770 } 1771 } 1772 1773 class FetchRemoveCommand : Command { 1774 protected { 1775 string m_version; 1776 bool m_forceRemove = false; 1777 } 1778 1779 override void prepare(scope CommandArgs args) 1780 { 1781 args.getopt("version", &m_version, [ 1782 "Use the specified version/branch instead of the latest available match", 1783 "The remove command also accepts \"*\" here as a wildcard to remove all versions of the package from the specified location" 1784 ], true); // hide --version from help 1785 1786 args.getopt("force-remove", &m_forceRemove, [ 1787 "Deprecated option that does nothing" 1788 ]); 1789 } 1790 1791 abstract override int execute(Dub dub, string[] free_args, string[] app_args); 1792 } 1793 1794 class FetchCommand : FetchRemoveCommand { 1795 this() @safe pure nothrow 1796 { 1797 this.name = "fetch"; 1798 this.argumentsPattern = "<package>[@<version-spec>]"; 1799 this.description = "Manually retrieves and caches a package"; 1800 this.helpText = [ 1801 "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.", 1802 "", 1803 "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.", 1804 "", 1805 "Without specified options, placement/removal will default to a user wide shared location.", 1806 "", 1807 "Complete applications can be retrieved and run easily by e.g.", 1808 "$ dub fetch vibelog --cache=local", 1809 "$ dub run vibelog --cache=local", 1810 "", 1811 "This will grab all needed dependencies and compile and run the application.", 1812 "", 1813 "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." 1814 ]; 1815 } 1816 1817 override void prepare(scope CommandArgs args) 1818 { 1819 super.prepare(args); 1820 } 1821 1822 override int execute(Dub dub, string[] free_args, string[] app_args) 1823 { 1824 enforceUsage(free_args.length == 1, "Expecting exactly one argument."); 1825 enforceUsage(app_args.length == 0, "Unexpected application arguments."); 1826 1827 auto location = dub.defaultPlacementLocation; 1828 1829 auto name = free_args[0]; 1830 1831 FetchOptions fetchOpts; 1832 fetchOpts |= FetchOptions.forceBranchUpgrade; 1833 if (m_version.length) { // remove then --version removed 1834 enforceUsage(!name.canFindVersionSplitter, "Double version spec not allowed."); 1835 logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", name, m_version); 1836 dub.fetch(name, Dependency(m_version), location, fetchOpts); 1837 } else if (name.canFindVersionSplitter) { 1838 const parts = name.splitPackageName; 1839 dub.fetch(parts.name, Dependency(parts.version_), location, fetchOpts); 1840 } else { 1841 try { 1842 dub.fetch(name, Dependency(">=0.0.0"), location, fetchOpts); 1843 logInfo( 1844 "Please note that you need to use `dub run <pkgname>` " ~ 1845 "or add it to dependencies of your package to actually use/run it. " ~ 1846 "dub does not do actual installation of packages outside of its own ecosystem."); 1847 } 1848 catch(Exception e){ 1849 logInfo("Getting a release version failed: %s", e.msg); 1850 logInfo("Retry with ~master..."); 1851 dub.fetch(name, Dependency("~master"), location, fetchOpts); 1852 } 1853 } 1854 return 0; 1855 } 1856 } 1857 1858 class InstallCommand : FetchCommand { 1859 this() @safe pure nothrow 1860 { 1861 this.name = "install"; 1862 this.hidden = true; 1863 } 1864 override void prepare(scope CommandArgs args) { super.prepare(args); } 1865 override int execute(Dub dub, string[] free_args, string[] app_args) 1866 { 1867 warnRenamed("install", "fetch"); 1868 return super.execute(dub, free_args, app_args); 1869 } 1870 } 1871 1872 class RemoveCommand : FetchRemoveCommand { 1873 private { 1874 bool m_nonInteractive; 1875 } 1876 1877 this() @safe pure nothrow 1878 { 1879 this.name = "remove"; 1880 this.argumentsPattern = "<package>[@<version-spec>]"; 1881 this.description = "Removes a cached package"; 1882 this.helpText = [ 1883 "Removes a package that is cached on the local system." 1884 ]; 1885 } 1886 1887 override void prepare(scope CommandArgs args) 1888 { 1889 super.prepare(args); 1890 args.getopt("n|non-interactive", &m_nonInteractive, ["Don't enter interactive mode."]); 1891 } 1892 1893 override int execute(Dub dub, string[] free_args, string[] app_args) 1894 { 1895 enforceUsage(free_args.length == 1, "Expecting exactly one argument."); 1896 enforceUsage(app_args.length == 0, "Unexpected application arguments."); 1897 1898 auto package_id = free_args[0]; 1899 auto location = dub.defaultPlacementLocation; 1900 1901 size_t resolveVersion(in Package[] packages) { 1902 // just remove only package version 1903 if (packages.length == 1) 1904 return 0; 1905 1906 writeln("Select version of '", package_id, "' to remove from location '", location, "':"); 1907 foreach (i, pack; packages) 1908 writefln("%s) %s", i + 1, pack.version_); 1909 writeln(packages.length + 1, ") ", "all versions"); 1910 while (true) { 1911 writef("> "); 1912 auto inp = readln(); 1913 if (!inp.length) // Ctrl+D 1914 return size_t.max; 1915 inp = inp.stripRight; 1916 if (!inp.length) // newline or space 1917 continue; 1918 try { 1919 immutable selection = inp.to!size_t - 1; 1920 if (selection <= packages.length) 1921 return selection; 1922 } catch (ConvException e) { 1923 } 1924 logError("Please enter a number between 1 and %s.", packages.length + 1); 1925 } 1926 } 1927 1928 if (!m_version.empty) { // remove then --version removed 1929 enforceUsage(!package_id.canFindVersionSplitter, "Double version spec not allowed."); 1930 logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", package_id, m_version); 1931 dub.remove(package_id, m_version, location); 1932 } else { 1933 const parts = package_id.splitPackageName; 1934 if (m_nonInteractive || parts.version_.length) { 1935 dub.remove(parts.name, parts.version_, location); 1936 } else { 1937 dub.remove(package_id, location, &resolveVersion); 1938 } 1939 } 1940 return 0; 1941 } 1942 } 1943 1944 class UninstallCommand : RemoveCommand { 1945 this() @safe pure nothrow 1946 { 1947 this.name = "uninstall"; 1948 this.hidden = true; 1949 } 1950 override void prepare(scope CommandArgs args) { super.prepare(args); } 1951 override int execute(Dub dub, string[] free_args, string[] app_args) 1952 { 1953 warnRenamed("uninstall", "remove"); 1954 return super.execute(dub, free_args, app_args); 1955 } 1956 } 1957 1958 1959 /******************************************************************************/ 1960 /* ADD/REMOVE PATH/LOCAL */ 1961 /******************************************************************************/ 1962 1963 abstract class RegistrationCommand : Command { 1964 private { 1965 bool m_system; 1966 } 1967 1968 override void prepare(scope CommandArgs args) 1969 { 1970 args.getopt("system", &m_system, [ 1971 "Register system-wide instead of user-wide" 1972 ]); 1973 } 1974 1975 abstract override int execute(Dub dub, string[] free_args, string[] app_args); 1976 } 1977 1978 class AddPathCommand : RegistrationCommand { 1979 this() @safe pure nothrow 1980 { 1981 this.name = "add-path"; 1982 this.argumentsPattern = "<path>"; 1983 this.description = "Adds a default package search path"; 1984 this.helpText = [ 1985 "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.", 1986 "", 1987 "Any packages registered using add-path will be preferred over packages downloaded from the package registry when searching for dependencies during a build operation.", 1988 "", 1989 "The version of the packages will be determined by one of the following:", 1990 " - For GIT working copies, the last tag (git describe) is used to determine the version", 1991 " - If the package contains a \"version\" field in the package description, this is used", 1992 " - If neither of those apply, \"~master\" is assumed" 1993 ]; 1994 } 1995 1996 override int execute(Dub dub, string[] free_args, string[] app_args) 1997 { 1998 enforceUsage(free_args.length == 1, "Missing search path."); 1999 dub.addSearchPath(free_args[0], m_system); 2000 return 0; 2001 } 2002 } 2003 2004 class RemovePathCommand : RegistrationCommand { 2005 this() @safe pure nothrow 2006 { 2007 this.name = "remove-path"; 2008 this.argumentsPattern = "<path>"; 2009 this.description = "Removes a package search path"; 2010 this.helpText = ["Removes a package search path previously added with add-path."]; 2011 } 2012 2013 override int execute(Dub dub, string[] free_args, string[] app_args) 2014 { 2015 enforceUsage(free_args.length == 1, "Expected one argument."); 2016 dub.removeSearchPath(free_args[0], m_system); 2017 return 0; 2018 } 2019 } 2020 2021 class AddLocalCommand : RegistrationCommand { 2022 this() @safe pure nothrow 2023 { 2024 this.name = "add-local"; 2025 this.argumentsPattern = "<path> [<version>]"; 2026 this.description = "Adds a local package directory (e.g. a git repository)"; 2027 this.helpText = [ 2028 "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.", 2029 "", 2030 "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.", 2031 "", 2032 "See 'dub add-path -h' for a way to register multiple local packages at once." 2033 ]; 2034 } 2035 2036 override int execute(Dub dub, string[] free_args, string[] app_args) 2037 { 2038 enforceUsage(free_args.length == 1 || free_args.length == 2, "Expecting one or two arguments."); 2039 string ver = free_args.length == 2 ? free_args[1] : null; 2040 dub.addLocalPackage(free_args[0], ver, m_system); 2041 return 0; 2042 } 2043 } 2044 2045 class RemoveLocalCommand : RegistrationCommand { 2046 this() @safe pure nothrow 2047 { 2048 this.name = "remove-local"; 2049 this.argumentsPattern = "<path>"; 2050 this.description = "Removes a local package directory"; 2051 this.helpText = ["Removes a local package directory"]; 2052 } 2053 2054 override int execute(Dub dub, string[] free_args, string[] app_args) 2055 { 2056 enforceUsage(free_args.length >= 1, "Missing package path argument."); 2057 enforceUsage(free_args.length <= 1, "Expected the package path to be the only argument."); 2058 dub.removeLocalPackage(free_args[0], m_system); 2059 return 0; 2060 } 2061 } 2062 2063 class ListCommand : Command { 2064 this() @safe pure nothrow 2065 { 2066 this.name = "list"; 2067 this.argumentsPattern = "[<package>[@<version-spec>]]"; 2068 this.description = "Prints a list of all or selected local packages dub is aware of"; 2069 this.helpText = [ 2070 "Prints a list of all or selected local packages. This includes all cached "~ 2071 "packages (user or system wide), all packages in the package search paths "~ 2072 "(\"dub add-path\") and all manually registered packages (\"dub add-local\"). "~ 2073 "If package specified output filtered by package spec." 2074 ]; 2075 } 2076 override void prepare(scope CommandArgs args) {} 2077 override int execute(Dub dub, string[] free_args, string[] app_args) 2078 { 2079 enforceUsage(free_args.length <= 1, "Expecting zero or one extra arguments."); 2080 const pinfo = free_args.length ? splitPackageName(free_args[0]) : PackageAndVersion("","*"); 2081 const pname = pinfo.name; 2082 const pvlim = Dependency(pinfo.version_ == "" ? "*" : pinfo.version_); 2083 enforceUsage(app_args.length == 0, "The list command supports no application arguments."); 2084 logInfo("Packages present in the system and known to dub:"); 2085 foreach (p; dub.packageManager.getPackageIterator()) { 2086 if ((pname == "" || pname == p.name) && pvlim.matches(p.version_)) 2087 logInfo(" %s %s: %s", p.name, p.version_, p.path.toNativeString()); 2088 } 2089 logInfo(""); 2090 return 0; 2091 } 2092 } 2093 2094 class SearchCommand : Command { 2095 this() @safe pure nothrow 2096 { 2097 this.name = "search"; 2098 this.argumentsPattern = "<query>"; 2099 this.description = "Search for available packages."; 2100 this.helpText = [ 2101 "Search all specified DUB registries for packages matching query." 2102 ]; 2103 } 2104 override void prepare(scope CommandArgs args) {} 2105 override int execute(Dub dub, string[] free_args, string[] app_args) 2106 { 2107 enforce(free_args.length == 1, "Expected one argument."); 2108 auto res = dub.searchPackages(free_args[0]); 2109 if (res.empty) 2110 { 2111 logError("No matches found."); 2112 return 1; 2113 } 2114 auto justify = res 2115 .map!((descNmatches) => descNmatches[1]) 2116 .joiner 2117 .map!(m => m.name.length + m.version_.length) 2118 .reduce!max + " ()".length; 2119 justify += (~justify & 3) + 1; // round to next multiple of 4 2120 foreach (desc, matches; res) 2121 { 2122 logInfo("==== %s ====", desc); 2123 foreach (m; matches) 2124 logInfo("%s%s", leftJustify(m.name ~ " (" ~ m.version_ ~ ")", justify), m.description); 2125 } 2126 return 0; 2127 } 2128 } 2129 2130 2131 /******************************************************************************/ 2132 /* OVERRIDES */ 2133 /******************************************************************************/ 2134 2135 class AddOverrideCommand : Command { 2136 private { 2137 bool m_system = false; 2138 } 2139 2140 this() @safe pure nothrow 2141 { 2142 this.name = "add-override"; 2143 this.argumentsPattern = "<package> <version-spec> <target-path/target-version>"; 2144 this.description = "Adds a new package override."; 2145 this.helpText = [ 2146 ]; 2147 } 2148 2149 override void prepare(scope CommandArgs args) 2150 { 2151 args.getopt("system", &m_system, [ 2152 "Register system-wide instead of user-wide" 2153 ]); 2154 } 2155 2156 override int execute(Dub dub, string[] free_args, string[] app_args) 2157 { 2158 enforceUsage(app_args.length == 0, "Unexpected application arguments."); 2159 enforceUsage(free_args.length == 3, "Expected three arguments, not "~free_args.length.to!string); 2160 auto scope_ = m_system ? LocalPackageType.system : LocalPackageType.user; 2161 auto pack = free_args[0]; 2162 auto ver = Dependency(free_args[1]); 2163 if (existsFile(NativePath(free_args[2]))) { 2164 auto target = NativePath(free_args[2]); 2165 if (!target.absolute) target = NativePath(getcwd()) ~ target; 2166 dub.packageManager.addOverride(scope_, pack, ver, target); 2167 logInfo("Added override %s %s => %s", pack, ver, target); 2168 } else { 2169 auto target = Version(free_args[2]); 2170 dub.packageManager.addOverride(scope_, pack, ver, target); 2171 logInfo("Added override %s %s => %s", pack, ver, target); 2172 } 2173 return 0; 2174 } 2175 } 2176 2177 class RemoveOverrideCommand : Command { 2178 private { 2179 bool m_system = false; 2180 } 2181 2182 this() @safe pure nothrow 2183 { 2184 this.name = "remove-override"; 2185 this.argumentsPattern = "<package> <version-spec>"; 2186 this.description = "Removes an existing package override."; 2187 this.helpText = [ 2188 ]; 2189 } 2190 2191 override void prepare(scope CommandArgs args) 2192 { 2193 args.getopt("system", &m_system, [ 2194 "Register system-wide instead of user-wide" 2195 ]); 2196 } 2197 2198 override int execute(Dub dub, string[] free_args, string[] app_args) 2199 { 2200 enforceUsage(app_args.length == 0, "Unexpected application arguments."); 2201 enforceUsage(free_args.length == 2, "Expected two arguments, not "~free_args.length.to!string); 2202 auto scope_ = m_system ? LocalPackageType.system : LocalPackageType.user; 2203 dub.packageManager.removeOverride(scope_, free_args[0], Dependency(free_args[1])); 2204 return 0; 2205 } 2206 } 2207 2208 class ListOverridesCommand : Command { 2209 this() @safe pure nothrow 2210 { 2211 this.name = "list-overrides"; 2212 this.argumentsPattern = ""; 2213 this.description = "Prints a list of all local package overrides"; 2214 this.helpText = [ 2215 "Prints a list of all overridden packages added via \"dub add-override\"." 2216 ]; 2217 } 2218 override void prepare(scope CommandArgs args) {} 2219 override int execute(Dub dub, string[] free_args, string[] app_args) 2220 { 2221 void printList(in PackageOverride[] overrides, string caption) 2222 { 2223 if (overrides.length == 0) return; 2224 logInfo("# %s", caption); 2225 foreach (ovr; overrides) { 2226 if (!ovr.targetPath.empty) logInfo("%s %s => %s", ovr.package_, ovr.version_, ovr.targetPath); 2227 else logInfo("%s %s => %s", ovr.package_, ovr.version_, ovr.targetVersion); 2228 } 2229 } 2230 printList(dub.packageManager.getOverrides(LocalPackageType.user), "User wide overrides"); 2231 printList(dub.packageManager.getOverrides(LocalPackageType.system), "System wide overrides"); 2232 return 0; 2233 } 2234 } 2235 2236 /******************************************************************************/ 2237 /* Cache cleanup */ 2238 /******************************************************************************/ 2239 2240 class CleanCachesCommand : Command { 2241 this() @safe pure nothrow 2242 { 2243 this.name = "clean-caches"; 2244 this.argumentsPattern = ""; 2245 this.description = "Removes cached metadata"; 2246 this.helpText = [ 2247 "This command removes any cached metadata like the list of available packages and their latest version." 2248 ]; 2249 } 2250 2251 override void prepare(scope CommandArgs args) {} 2252 2253 override int execute(Dub dub, string[] free_args, string[] app_args) 2254 { 2255 return 0; 2256 } 2257 } 2258 2259 /******************************************************************************/ 2260 /* DUSTMITE */ 2261 /******************************************************************************/ 2262 2263 class DustmiteCommand : PackageBuildCommand { 2264 private { 2265 int m_compilerStatusCode = int.min; 2266 int m_linkerStatusCode = int.min; 2267 int m_programStatusCode = int.min; 2268 string m_compilerRegex; 2269 string m_linkerRegex; 2270 string m_programRegex; 2271 string m_testPackage; 2272 bool m_combined; 2273 bool m_noRedirect; 2274 } 2275 2276 this() @safe pure nothrow 2277 { 2278 this.name = "dustmite"; 2279 this.argumentsPattern = "<destination-path>"; 2280 this.acceptsAppArgs = true; 2281 this.description = "Create reduced test cases for build errors"; 2282 this.helpText = [ 2283 "This command uses the Dustmite utility to isolate the cause of build errors in a DUB project.", 2284 "", 2285 "It will create a copy of all involved packages and run dustmite on this copy, leaving a reduced test case.", 2286 "", 2287 "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." 2288 ]; 2289 } 2290 2291 override void prepare(scope CommandArgs args) 2292 { 2293 args.getopt("compiler-status", &m_compilerStatusCode, ["The expected status code of the compiler run"]); 2294 args.getopt("compiler-regex", &m_compilerRegex, ["A regular expression used to match against the compiler output"]); 2295 args.getopt("linker-status", &m_linkerStatusCode, ["The expected status code of the linker run"]); 2296 args.getopt("linker-regex", &m_linkerRegex, ["A regular expression used to match against the linker output"]); 2297 args.getopt("program-status", &m_programStatusCode, ["The expected status code of the built executable"]); 2298 args.getopt("program-regex", &m_programRegex, ["A regular expression used to match against the program output"]); 2299 args.getopt("test-package", &m_testPackage, ["Perform a test run - usually only used internally"]); 2300 args.getopt("combined", &m_combined, ["Builds multiple packages with one compiler run"]); 2301 args.getopt("no-redirect", &m_noRedirect, ["Don't redirect stdout/stderr streams of the test command"]); 2302 super.prepare(args); 2303 2304 // speed up loading when in test mode 2305 if (m_testPackage.length) { 2306 skipDubInitialization = true; 2307 m_nodeps = true; 2308 } 2309 } 2310 2311 override int execute(Dub dub, string[] free_args, string[] app_args) 2312 { 2313 import std.format : formattedWrite; 2314 2315 if (m_testPackage.length) { 2316 dub = new Dub(NativePath(getcwd())); 2317 2318 setupPackage(dub, m_testPackage); 2319 m_defaultConfig = dub.project.getDefaultConfiguration(m_buildPlatform); 2320 2321 GeneratorSettings gensettings; 2322 gensettings.platform = m_buildPlatform; 2323 gensettings.config = m_buildConfig.length ? m_buildConfig : m_defaultConfig; 2324 gensettings.buildType = m_buildType; 2325 gensettings.compiler = m_compiler; 2326 gensettings.buildSettings = m_buildSettings; 2327 gensettings.combined = m_combined; 2328 gensettings.filterVersions = m_filterVersions; 2329 gensettings.run = m_programStatusCode != int.min || m_programRegex.length; 2330 gensettings.runArgs = app_args; 2331 gensettings.force = true; 2332 gensettings.compileCallback = check(m_compilerStatusCode, m_compilerRegex); 2333 gensettings.linkCallback = check(m_linkerStatusCode, m_linkerRegex); 2334 gensettings.runCallback = check(m_programStatusCode, m_programRegex); 2335 try dub.generateProject("build", gensettings); 2336 catch (DustmiteMismatchException) { 2337 logInfo("Dustmite test doesn't match."); 2338 return 3; 2339 } 2340 catch (DustmiteMatchException) { 2341 logInfo("Dustmite test matches."); 2342 return 0; 2343 } 2344 } else { 2345 enforceUsage(free_args.length == 1, "Expected destination path."); 2346 auto path = NativePath(free_args[0]); 2347 path.normalize(); 2348 enforceUsage(!path.empty, "Destination path must not be empty."); 2349 if (!path.absolute) path = NativePath(getcwd()) ~ path; 2350 enforceUsage(!path.startsWith(dub.rootPath), "Destination path must not be a sub directory of the tested package!"); 2351 2352 setupPackage(dub, null); 2353 auto prj = dub.project; 2354 if (m_buildConfig.empty) 2355 m_buildConfig = prj.getDefaultConfiguration(m_buildPlatform); 2356 2357 void copyFolderRec(NativePath folder, NativePath dstfolder) 2358 { 2359 mkdirRecurse(dstfolder.toNativeString()); 2360 foreach (de; iterateDirectory(folder.toNativeString())) { 2361 if (de.name.startsWith(".")) continue; 2362 if (de.isDirectory) { 2363 copyFolderRec(folder ~ de.name, dstfolder ~ de.name); 2364 } else { 2365 if (de.name.endsWith(".o") || de.name.endsWith(".obj")) continue; 2366 if (de.name.endsWith(".exe")) continue; 2367 try copyFile(folder ~ de.name, dstfolder ~ de.name); 2368 catch (Exception e) { 2369 logWarn("Failed to copy file %s: %s", (folder ~ de.name).toNativeString(), e.msg); 2370 } 2371 } 2372 } 2373 } 2374 2375 static void fixPathDependency(string pack, ref Dependency dep) { 2376 if (!dep.path.empty) { 2377 auto mainpack = getBasePackageName(pack); 2378 dep.path = NativePath("../") ~ mainpack; 2379 } 2380 } 2381 2382 void fixPathDependencies(ref PackageRecipe recipe, NativePath base_path) 2383 { 2384 foreach (name, ref dep; recipe.buildSettings.dependencies) 2385 fixPathDependency(name, dep); 2386 2387 foreach (ref cfg; recipe.configurations) 2388 foreach (name, ref dep; cfg.buildSettings.dependencies) 2389 fixPathDependency(name, dep); 2390 2391 foreach (ref subp; recipe.subPackages) 2392 if (subp.path.length) { 2393 auto sub_path = base_path ~ NativePath(subp.path); 2394 auto pack = prj.packageManager.getOrLoadPackage(sub_path); 2395 fixPathDependencies(pack.recipe, sub_path); 2396 pack.storeInfo(sub_path); 2397 } else fixPathDependencies(subp.recipe, base_path); 2398 } 2399 2400 bool[string] visited; 2401 foreach (pack_; prj.getTopologicalPackageList()) { 2402 auto pack = pack_.basePackage; 2403 if (pack.name in visited) continue; 2404 visited[pack.name] = true; 2405 auto dst_path = path ~ pack.name; 2406 logInfo("Copy package '%s' to destination folder...", pack.name); 2407 copyFolderRec(pack.path, dst_path); 2408 2409 // adjust all path based dependencies 2410 fixPathDependencies(pack.recipe, dst_path); 2411 2412 // overwrite package description file with additional version information 2413 pack.storeInfo(dst_path); 2414 } 2415 2416 logInfo("Executing dustmite..."); 2417 auto testcmd = appender!string(); 2418 testcmd.formattedWrite("%s dustmite --test-package=%s --build=%s --config=%s", 2419 thisExePath, prj.name, m_buildType, m_buildConfig); 2420 2421 if (m_compilerName.length) testcmd.formattedWrite(" \"--compiler=%s\"", m_compilerName); 2422 if (m_arch.length) testcmd.formattedWrite(" --arch=%s", m_arch); 2423 if (m_compilerStatusCode != int.min) testcmd.formattedWrite(" --compiler-status=%s", m_compilerStatusCode); 2424 if (m_compilerRegex.length) testcmd.formattedWrite(" \"--compiler-regex=%s\"", m_compilerRegex); 2425 if (m_linkerStatusCode != int.min) testcmd.formattedWrite(" --linker-status=%s", m_linkerStatusCode); 2426 if (m_linkerRegex.length) testcmd.formattedWrite(" \"--linker-regex=%s\"", m_linkerRegex); 2427 if (m_programStatusCode != int.min) testcmd.formattedWrite(" --program-status=%s", m_programStatusCode); 2428 if (m_programRegex.length) testcmd.formattedWrite(" \"--program-regex=%s\"", m_programRegex); 2429 if (m_combined) testcmd ~= " --combined"; 2430 2431 // --vquiet swallows dustmite's output ... 2432 if (!m_noRedirect) testcmd ~= " --vquiet"; 2433 2434 // TODO: pass *all* original parameters 2435 logDiagnostic("Running dustmite: %s", testcmd); 2436 2437 string[] extraArgs; 2438 if (m_noRedirect) extraArgs ~= "--no-redirect"; 2439 const cmd = "dustmite" ~ extraArgs ~ [path.toNativeString(), testcmd.data]; 2440 auto dmpid = spawnProcess(cmd); 2441 return dmpid.wait(); 2442 } 2443 return 0; 2444 } 2445 2446 void delegate(int, string) check(int code_match, string regex_match) 2447 { 2448 return (code, output) { 2449 import std.encoding; 2450 import std.regex; 2451 2452 logInfo("%s", output); 2453 2454 if (code_match != int.min && code != code_match) { 2455 logInfo("Exit code %s doesn't match expected value %s", code, code_match); 2456 throw new DustmiteMismatchException; 2457 } 2458 2459 if (regex_match.length > 0 && !match(output.sanitize, regex_match)) { 2460 logInfo("Output doesn't match regex:"); 2461 logInfo("%s", output); 2462 throw new DustmiteMismatchException; 2463 } 2464 2465 if (code != 0 && code_match != int.min || regex_match.length > 0) { 2466 logInfo("Tool failed, but matched either exit code or output - counting as match."); 2467 throw new DustmiteMatchException; 2468 } 2469 }; 2470 } 2471 2472 static class DustmiteMismatchException : Exception { 2473 this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null) 2474 { 2475 super(message, file, line, next); 2476 } 2477 } 2478 2479 static class DustmiteMatchException : Exception { 2480 this(string message = "", string file = __FILE__, int line = __LINE__, Throwable next = null) 2481 { 2482 super(message, file, line, next); 2483 } 2484 } 2485 } 2486 2487 2488 /******************************************************************************/ 2489 /* CONVERT command */ 2490 /******************************************************************************/ 2491 2492 class ConvertCommand : Command { 2493 private { 2494 string m_format; 2495 bool m_stdout; 2496 } 2497 2498 this() @safe pure nothrow 2499 { 2500 this.name = "convert"; 2501 this.argumentsPattern = ""; 2502 this.description = "Converts the file format of the package recipe."; 2503 this.helpText = [ 2504 "This command will convert between JSON and SDLang formatted package recipe files.", 2505 "", 2506 "Warning: Beware that any formatting and comments within the package recipe will get lost in the conversion process." 2507 ]; 2508 } 2509 2510 override void prepare(scope CommandArgs args) 2511 { 2512 args.getopt("f|format", &m_format, ["Specifies the target package recipe format. Possible values:", " json, sdl"]); 2513 args.getopt("s|stdout", &m_stdout, ["Outputs the converted package recipe to stdout instead of writing to disk."]); 2514 } 2515 2516 override int execute(Dub dub, string[] free_args, string[] app_args) 2517 { 2518 enforceUsage(app_args.length == 0, "Unexpected application arguments."); 2519 enforceUsage(free_args.length == 0, "Unexpected arguments: "~free_args.join(" ")); 2520 enforceUsage(m_format.length > 0, "Missing target format file extension (--format=...)."); 2521 if (!loadCwdPackage(dub, true)) return 1; 2522 dub.convertRecipe(m_format, m_stdout); 2523 return 0; 2524 } 2525 } 2526 2527 2528 /******************************************************************************/ 2529 /* HELP */ 2530 /******************************************************************************/ 2531 2532 private { 2533 enum shortArgColumn = 2; 2534 enum longArgColumn = 6; 2535 enum descColumn = 24; 2536 enum lineWidth = 80 - 1; 2537 } 2538 2539 private void showHelp(in CommandGroup[] commands, CommandArgs common_args) 2540 { 2541 writeln( 2542 `USAGE: dub [--version] [<command>] [<options...>] [-- [<application arguments...>]] 2543 2544 Manages the DUB project in the current directory. If the command is omitted, 2545 DUB will default to "run". When running an application, "--" can be used to 2546 separate DUB options from options passed to the application. 2547 2548 Run "dub <command> --help" to get help for a specific command. 2549 2550 You can use the "http_proxy" environment variable to configure a proxy server 2551 to be used for fetching packages. 2552 2553 2554 Available commands 2555 ==================`); 2556 2557 foreach (grp; commands) { 2558 writeln(); 2559 writeWS(shortArgColumn); 2560 writeln(grp.caption); 2561 writeWS(shortArgColumn); 2562 writerep!'-'(grp.caption.length); 2563 writeln(); 2564 foreach (cmd; grp.commands) { 2565 if (cmd.hidden) continue; 2566 writeWS(shortArgColumn); 2567 writef("%s %s", cmd.name, cmd.argumentsPattern); 2568 auto chars_output = cmd.name.length + cmd.argumentsPattern.length + shortArgColumn + 1; 2569 if (chars_output < descColumn) { 2570 writeWS(descColumn - chars_output); 2571 } else { 2572 writeln(); 2573 writeWS(descColumn); 2574 } 2575 writeWrapped(cmd.description, descColumn, descColumn); 2576 } 2577 } 2578 writeln(); 2579 writeln(); 2580 writeln(`Common options`); 2581 writeln(`==============`); 2582 writeln(); 2583 writeOptions(common_args); 2584 writeln(); 2585 showVersion(); 2586 } 2587 2588 private void showVersion() 2589 { 2590 writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__); 2591 } 2592 2593 private void showCommandHelp(Command cmd, CommandArgs args, CommandArgs common_args) 2594 { 2595 writefln(`USAGE: dub %s %s [<options...>]%s`, cmd.name, cmd.argumentsPattern, cmd.acceptsAppArgs ? " [-- <application arguments...>]": null); 2596 writeln(); 2597 foreach (ln; cmd.helpText) 2598 ln.writeWrapped(); 2599 2600 if (args.recognizedArgs.length) { 2601 writeln(); 2602 writeln(); 2603 writeln("Command specific options"); 2604 writeln("========================"); 2605 writeln(); 2606 writeOptions(args); 2607 } 2608 2609 writeln(); 2610 writeln(); 2611 writeln("Common options"); 2612 writeln("=============="); 2613 writeln(); 2614 writeOptions(common_args); 2615 writeln(); 2616 writefln("DUB version %s, built on %s", getDUBVersion(), __DATE__); 2617 } 2618 2619 private void writeOptions(CommandArgs args) 2620 { 2621 foreach (arg; args.recognizedArgs) { 2622 if (arg.hidden) continue; 2623 auto names = arg.names.split("|"); 2624 assert(names.length == 1 || names.length == 2); 2625 string sarg = names[0].length == 1 ? names[0] : null; 2626 string larg = names[0].length > 1 ? names[0] : names.length > 1 ? names[1] : null; 2627 if (sarg !is null) { 2628 writeWS(shortArgColumn); 2629 writef("-%s", sarg); 2630 writeWS(longArgColumn - shortArgColumn - 2); 2631 } else writeWS(longArgColumn); 2632 size_t col = longArgColumn; 2633 if (larg !is null) { 2634 if (arg.defaultValue.peek!bool) { 2635 writef("--%s", larg); 2636 col += larg.length + 2; 2637 } else { 2638 writef("--%s=VALUE", larg); 2639 col += larg.length + 8; 2640 } 2641 } 2642 if (col < descColumn) { 2643 writeWS(descColumn - col); 2644 } else { 2645 writeln(); 2646 writeWS(descColumn); 2647 } 2648 foreach (i, ln; arg.helpText) { 2649 if (i > 0) writeWS(descColumn); 2650 ln.writeWrapped(descColumn, descColumn); 2651 } 2652 } 2653 } 2654 2655 private void writeWrapped(string string, size_t indent = 0, size_t first_line_pos = 0) 2656 { 2657 // handle pre-indented strings and bullet lists 2658 size_t first_line_indent = 0; 2659 while (string.startsWith(" ")) { 2660 string = string[1 .. $]; 2661 indent++; 2662 first_line_indent++; 2663 } 2664 if (string.startsWith("- ")) indent += 2; 2665 2666 auto wrapped = string.wrap(lineWidth, getRepString!' '(first_line_pos+first_line_indent), getRepString!' '(indent)); 2667 wrapped = wrapped[first_line_pos .. $]; 2668 foreach (ln; wrapped.splitLines()) 2669 writeln(ln); 2670 } 2671 2672 private void writeWS(size_t num) { writerep!' '(num); } 2673 private void writerep(char ch)(size_t num) { write(getRepString!ch(num)); } 2674 2675 private string getRepString(char ch)(size_t len) 2676 { 2677 static string buf; 2678 if (len > buf.length) buf ~= [ch].replicate(len-buf.length); 2679 return buf[0 .. len]; 2680 } 2681 2682 /*** 2683 */ 2684 2685 2686 private void enforceUsage(bool cond, string text) 2687 { 2688 if (!cond) throw new UsageException(text); 2689 } 2690 2691 private class UsageException : Exception { 2692 this(string message, string file = __FILE__, int line = __LINE__, Throwable next = null) 2693 { 2694 super(message, file, line, next); 2695 } 2696 } 2697 2698 private void warnRenamed(string prev, string curr) 2699 { 2700 logWarn("The '%s' Command was renamed to '%s'. Please update your scripts.", prev, curr); 2701 } 2702 2703 private bool addDependency(Dub dub, ref PackageRecipe recipe, string depspec) 2704 { 2705 Dependency dep; 2706 const parts = splitPackageName(depspec); 2707 const depname = parts.name; 2708 if (parts.version_) 2709 dep = Dependency(parts.version_); 2710 else 2711 { 2712 try { 2713 const ver = dub.getLatestVersion(depname); 2714 dep = ver.isBranch ? Dependency(ver) : Dependency("~>" ~ ver.toString()); 2715 } catch (Exception e) { 2716 logError("Could not find package '%s'.", depname); 2717 logDebug("Full error: %s", e.toString().sanitize); 2718 return false; 2719 } 2720 } 2721 recipe.buildSettings.dependencies[depname] = dep; 2722 logInfo("Adding dependency %s %s", depname, dep.versionSpec); 2723 return true; 2724 } 2725 2726 private struct PackageAndVersion 2727 { 2728 string name; 2729 string version_; 2730 } 2731 2732 /* Split <package>=<version-specifier> and <package>@<version-specifier> 2733 into `name` and `version_`. */ 2734 private PackageAndVersion splitPackageName(string packageName) 2735 { 2736 // split <package>@<version-specifier> 2737 auto parts = packageName.findSplit("@"); 2738 if (parts[1].empty) { 2739 // split <package>=<version-specifier> 2740 parts = packageName.findSplit("="); 2741 } 2742 2743 PackageAndVersion p; 2744 p.name = parts[0]; 2745 if (!parts[1].empty) 2746 p.version_ = parts[2]; 2747 return p; 2748 } 2749 2750 unittest 2751 { 2752 // https://github.com/dlang/dub/issues/1681 2753 assert(splitPackageName("") == PackageAndVersion("", null)); 2754 2755 assert(splitPackageName("foo") == PackageAndVersion("foo", null)); 2756 assert(splitPackageName("foo=1.0.1") == PackageAndVersion("foo", "1.0.1")); 2757 assert(splitPackageName("foo@1.0.1") == PackageAndVersion("foo", "1.0.1")); 2758 assert(splitPackageName("foo@==1.0.1") == PackageAndVersion("foo", "==1.0.1")); 2759 assert(splitPackageName("foo@>=1.0.1") == PackageAndVersion("foo", ">=1.0.1")); 2760 assert(splitPackageName("foo@~>1.0.1") == PackageAndVersion("foo", "~>1.0.1")); 2761 assert(splitPackageName("foo@<1.0.1") == PackageAndVersion("foo", "<1.0.1")); 2762 } 2763 2764 private ulong canFindVersionSplitter(string packageName) 2765 { 2766 // see splitPackageName 2767 return packageName.canFind("@", "="); 2768 } 2769 2770 unittest 2771 { 2772 assert(!canFindVersionSplitter("foo")); 2773 assert(canFindVersionSplitter("foo=1.0.1")); 2774 assert(canFindVersionSplitter("foo@1.0.1")); 2775 assert(canFindVersionSplitter("foo@==1.0.1")); 2776 assert(canFindVersionSplitter("foo@>=1.0.1")); 2777 assert(canFindVersionSplitter("foo@~>1.0.1")); 2778 assert(canFindVersionSplitter("foo@<1.0.1")); 2779 }