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