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