Newer
Older
dub_jkp / source / dub / project.d
  1. /**
  2. Representing a full project, with a root Package and several dependencies.
  3.  
  4. Copyright: © 2012-2013 Matthias Dondorff, 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.project;
  9.  
  10. import dub.compilers.compiler;
  11. import dub.dependency;
  12. import dub.description;
  13. import dub.generators.generator;
  14. import dub.internal.utils;
  15. import dub.internal.vibecompat.core.file;
  16. import dub.internal.vibecompat.data.json;
  17. import dub.internal.vibecompat.inet.path;
  18. import dub.internal.logging;
  19. import dub.package_;
  20. import dub.packagemanager;
  21. import dub.recipe.selection;
  22.  
  23. import dub.internal.configy.Read;
  24.  
  25. import std.algorithm;
  26. import std.array;
  27. import std.conv : to;
  28. import std.datetime;
  29. import std.encoding : sanitize;
  30. import std.exception : enforce;
  31. import std.string;
  32.  
  33. /**
  34. Represents a full project, a root package with its dependencies and package
  35. selection.
  36.  
  37. All dependencies must be available locally so that the package dependency
  38. graph can be built. Use `Project.reinit` if necessary for reloading
  39. dependencies after more packages are available.
  40. */
  41. class Project {
  42. private {
  43. PackageManager m_packageManager;
  44. Package m_rootPackage;
  45. Package[] m_dependencies;
  46. Package[string] m_dependenciesByName;
  47. Package[][Package] m_dependees;
  48. SelectedVersions m_selections;
  49. string[] m_missingDependencies;
  50. string[string] m_overriddenConfigs;
  51. }
  52.  
  53. /** Loads a project.
  54.  
  55. Params:
  56. package_manager = Package manager instance to use for loading
  57. dependencies
  58. project_path = Path of the root package to load
  59. pack = An existing `Package` instance to use as the root package
  60. */
  61. deprecated("Load the package using `PackageManager.getOrLoadPackage` then call the `(PackageManager, Package)` overload")
  62. this(PackageManager package_manager, NativePath project_path)
  63. {
  64. Package pack;
  65. auto packageFile = Package.findPackageFile(project_path);
  66. if (packageFile.empty) {
  67. logWarn("There was no package description found for the application in '%s'.", project_path.toNativeString());
  68. pack = new Package(PackageRecipe.init, project_path);
  69. } else {
  70. pack = package_manager.getOrLoadPackage(project_path, packageFile, false, StrictMode.Warn);
  71. }
  72.  
  73. this(package_manager, pack);
  74. }
  75.  
  76. /// Ditto
  77. this(PackageManager package_manager, Package pack)
  78. {
  79. auto selections = Project.loadSelections(pack.path, package_manager);
  80. this(package_manager, pack, selections);
  81. }
  82.  
  83. /// ditto
  84. this(PackageManager package_manager, Package pack, SelectedVersions selections)
  85. {
  86. m_packageManager = package_manager;
  87. m_rootPackage = pack;
  88. m_selections = selections;
  89. reinit();
  90. }
  91.  
  92. /**
  93. * Loads a project's `dub.selections.json` and returns it
  94. *
  95. * This function will load `dub.selections.json` from the path at which
  96. * `pack` is located, and returned the resulting `SelectedVersions`.
  97. * If no `dub.selections.json` is found, an empty `SelectedVersions`
  98. * is returned.
  99. *
  100. * Params:
  101. * packPath = Absolute path of the Package to load the selection file from.
  102. *
  103. * Returns:
  104. * Always a non-null instance.
  105. */
  106. static package SelectedVersions loadSelections(in NativePath packPath, PackageManager mgr)
  107. {
  108. import dub.version_;
  109. import dub.internal.dyaml.stdsumtype;
  110.  
  111. auto lookupResult = mgr.readSelections(packPath);
  112. if (lookupResult.isNull()) // no file, or parsing error (displayed to the user)
  113. return new SelectedVersions();
  114.  
  115. auto r = lookupResult.get();
  116. return r.selectionsFile.content.match!(
  117. (Selections!0 s) {
  118. logWarnTag("Unsupported version",
  119. "File %s has fileVersion %s, which is not yet supported by DUB %s.",
  120. r.absolutePath, s.fileVersion, dubVersion);
  121. logWarn("Ignoring selections file. Use a newer DUB version " ~
  122. "and set the appropriate toolchainRequirements in your recipe file");
  123. return new SelectedVersions();
  124. },
  125. (Selections!1 s) {
  126. auto selectionsDir = r.absolutePath.parentPath;
  127. return new SelectedVersions(s, selectionsDir.relativeTo(packPath));
  128. },
  129. );
  130. }
  131.  
  132. /** List of all resolved dependencies.
  133.  
  134. This includes all direct and indirect dependencies of all configurations
  135. combined. Optional dependencies that were not chosen are not included.
  136. */
  137. @property const(Package[]) dependencies() const { return m_dependencies; }
  138.  
  139. /// The root package of the project.
  140. @property inout(Package) rootPackage() inout { return m_rootPackage; }
  141.  
  142. /// The versions to use for all dependencies. Call reinit() after changing these.
  143. @property inout(SelectedVersions) selections() inout { return m_selections; }
  144.  
  145. /// Package manager instance used by the project.
  146. deprecated("Use `Dub.packageManager` instead")
  147. @property inout(PackageManager) packageManager() inout { return m_packageManager; }
  148.  
  149. /** Determines if all dependencies necessary to build have been collected.
  150.  
  151. If this function returns `false`, it may be necessary to add more entries
  152. to `selections`, or to use `Dub.upgrade` to automatically select all
  153. missing dependencies.
  154. */
  155. bool hasAllDependencies() const { return m_missingDependencies.length == 0; }
  156.  
  157. /// Sorted list of missing dependencies.
  158. string[] missingDependencies() { return m_missingDependencies; }
  159.  
  160. /** Allows iteration of the dependency tree in topological order
  161. */
  162. int delegate(int delegate(ref Package)) getTopologicalPackageList(bool children_first = false, Package root_package = null, string[string] configs = null)
  163. {
  164. // ugly way to avoid code duplication since inout isn't compatible with foreach type inference
  165. return cast(int delegate(int delegate(ref Package)))(cast(const)this).getTopologicalPackageList(children_first, root_package, configs);
  166. }
  167. /// ditto
  168. int delegate(int delegate(ref const Package)) getTopologicalPackageList(bool children_first = false, in Package root_package = null, string[string] configs = null)
  169. const {
  170. const(Package) rootpack = root_package ? root_package : m_rootPackage;
  171.  
  172. int iterator(int delegate(ref const Package) del)
  173. {
  174. int ret = 0;
  175. bool[const(Package)] visited;
  176. void perform_rec(in Package p){
  177. if( p in visited ) return;
  178. visited[p] = true;
  179.  
  180. if( !children_first ){
  181. ret = del(p);
  182. if( ret ) return;
  183. }
  184.  
  185. auto cfg = configs.get(p.name, null);
  186.  
  187. PackageDependency[] deps;
  188. if (!cfg.length) deps = p.getAllDependencies();
  189. else {
  190. auto depmap = p.getDependencies(cfg);
  191. deps = depmap.byKey.map!(k => PackageDependency(PackageName(k), depmap[k])).array;
  192. }
  193. deps.sort!((a, b) => a.name.toString() < b.name.toString());
  194.  
  195. foreach (d; deps) {
  196. auto dependency = getDependency(d.name.toString(), true);
  197. assert(dependency || d.spec.optional,
  198. format("Non-optional dependency '%s' of '%s' not found in dependency tree!?.", d.name, p.name));
  199. if(dependency) perform_rec(dependency);
  200. if( ret ) return;
  201. }
  202.  
  203. if( children_first ){
  204. ret = del(p);
  205. if( ret ) return;
  206. }
  207. }
  208. perform_rec(rootpack);
  209. return ret;
  210. }
  211.  
  212. return &iterator;
  213. }
  214.  
  215. /** Retrieves a particular dependency by name.
  216.  
  217. Params:
  218. name = (Qualified) package name of the dependency
  219. is_optional = If set to true, will return `null` for unsatisfiable
  220. dependencies instead of throwing an exception.
  221. */
  222. inout(Package) getDependency(string name, bool is_optional)
  223. inout {
  224. if (auto pp = name in m_dependenciesByName)
  225. return *pp;
  226. if (!is_optional) throw new Exception("Unknown dependency: "~name);
  227. else return null;
  228. }
  229.  
  230. /** Returns the name of the default build configuration for the specified
  231. target platform.
  232.  
  233. Params:
  234. platform = The target build platform
  235. allow_non_library_configs = If set to true, will use the first
  236. possible configuration instead of the first "executable"
  237. configuration.
  238. */
  239. string getDefaultConfiguration(in BuildPlatform platform, bool allow_non_library_configs = true)
  240. const {
  241. auto cfgs = getPackageConfigs(platform, null, allow_non_library_configs);
  242. return cfgs[m_rootPackage.name];
  243. }
  244.  
  245. /** Overrides the configuration chosen for a particular package in the
  246. dependency graph.
  247.  
  248. Setting a certain configuration here is equivalent to removing all
  249. but one configuration from the package.
  250.  
  251. Params:
  252. package_ = The package for which to force selecting a certain
  253. dependency
  254. config = Name of the configuration to force
  255. */
  256. void overrideConfiguration(string package_, string config)
  257. {
  258. auto p = getDependency(package_, true);
  259. enforce(p !is null,
  260. format("Package '%s', marked for configuration override, is not present in dependency graph.", package_));
  261. enforce(p.configurations.canFind(config),
  262. format("Package '%s' does not have a configuration named '%s'.", package_, config));
  263. m_overriddenConfigs[package_] = config;
  264. }
  265.  
  266. /** Adds a test runner configuration for the root package.
  267.  
  268. Params:
  269. settings = The generator settings to use
  270. generate_main = Whether to generate the main.d file
  271. base_config = Optional base configuration
  272. custom_main_file = Optional path to file with custom main entry point
  273.  
  274. Returns:
  275. Name of the added test runner configuration, or null for base configurations with target type `none`
  276. */
  277. string addTestRunnerConfiguration(in GeneratorSettings settings, bool generate_main = true, string base_config = "", NativePath custom_main_file = NativePath())
  278. {
  279. if (base_config.length == 0) {
  280. // if a custom main file was given, favor the first library configuration, so that it can be applied
  281. if (!custom_main_file.empty) base_config = getDefaultConfiguration(settings.platform, false);
  282. // else look for a "unittest" configuration
  283. if (!base_config.length && rootPackage.configurations.canFind("unittest")) base_config = "unittest";
  284. // if not found, fall back to the first "library" configuration
  285. if (!base_config.length) base_config = getDefaultConfiguration(settings.platform, false);
  286. // if still nothing found, use the first executable configuration
  287. if (!base_config.length) base_config = getDefaultConfiguration(settings.platform, true);
  288. }
  289.  
  290. BuildSettings lbuildsettings = settings.buildSettings.dup;
  291. addBuildSettings(lbuildsettings, settings, base_config, null, true);
  292.  
  293. if (lbuildsettings.targetType == TargetType.none) {
  294. logInfo(`Configuration '%s' has target type "none". Skipping test runner configuration.`, base_config);
  295. return null;
  296. }
  297.  
  298. if (lbuildsettings.targetType == TargetType.executable && base_config == "unittest") {
  299. if (!custom_main_file.empty) logWarn("Ignoring custom main file.");
  300. return base_config;
  301. }
  302.  
  303. if (lbuildsettings.sourceFiles.empty) {
  304. logInfo(`No source files found in configuration '%s'. Falling back to default configuration for test runner.`, base_config);
  305. if (!custom_main_file.empty) logWarn("Ignoring custom main file.");
  306. return getDefaultConfiguration(settings.platform);
  307. }
  308.  
  309. const config = format("%s-test-%s", rootPackage.name.replace(".", "-").replace(":", "-"), base_config);
  310. logInfo(`Generating test runner configuration '%s' for '%s' (%s).`, config, base_config, lbuildsettings.targetType);
  311.  
  312. BuildSettingsTemplate tcinfo = rootPackage.recipe.getConfiguration(base_config).buildSettings.dup;
  313. tcinfo.targetType = TargetType.executable;
  314.  
  315. // set targetName unless specified explicitly in unittest base configuration
  316. if (tcinfo.targetName.empty || base_config != "unittest")
  317. tcinfo.targetName = config;
  318.  
  319. auto mainfil = tcinfo.mainSourceFile;
  320. if (!mainfil.length) mainfil = rootPackage.recipe.buildSettings.mainSourceFile;
  321.  
  322. string custommodname;
  323. if (!custom_main_file.empty) {
  324. import std.path;
  325. tcinfo.sourceFiles[""] ~= custom_main_file.relativeTo(rootPackage.path).toNativeString();
  326. tcinfo.importPaths[""] ~= custom_main_file.parentPath.toNativeString();
  327. custommodname = custom_main_file.head.name.baseName(".d");
  328. }
  329.  
  330. // prepare the list of tested modules
  331.  
  332. string[] import_modules;
  333. if (settings.single)
  334. lbuildsettings.importPaths ~= NativePath(mainfil).parentPath.toNativeString;
  335. bool firstTimePackage = true;
  336. foreach (file; lbuildsettings.sourceFiles) {
  337. if (file.endsWith(".d")) {
  338. auto fname = NativePath(file).head.name;
  339. NativePath msf = NativePath(mainfil);
  340. if (msf.absolute)
  341. msf = msf.relativeTo(rootPackage.path);
  342. if (!settings.single && NativePath(file).relativeTo(rootPackage.path) == msf) {
  343. logWarn("Excluding main source file %s from test.", mainfil);
  344. tcinfo.excludedSourceFiles[""] ~= mainfil;
  345. continue;
  346. }
  347. if (fname == "package.d") {
  348. if (firstTimePackage) {
  349. firstTimePackage = false;
  350. logDiagnostic("Excluding package.d file from test due to https://issues.dlang.org/show_bug.cgi?id=11847");
  351. }
  352. continue;
  353. }
  354. import_modules ~= dub.internal.utils.determineModuleName(lbuildsettings, NativePath(file), rootPackage.path);
  355. }
  356. }
  357.  
  358. NativePath mainfile;
  359. if (settings.tempBuild)
  360. mainfile = getTempFile("dub_test_root", ".d");
  361. else {
  362. import dub.generators.build : computeBuildName;
  363. mainfile = packageCache(settings.cache, this.rootPackage) ~
  364. format("code/%s/dub_test_root.d",
  365. computeBuildName(config, settings, import_modules));
  366. }
  367.  
  368. auto escapedMainFile = mainfile.toNativeString().replace("$", "$$");
  369. tcinfo.sourceFiles[""] ~= escapedMainFile;
  370. tcinfo.mainSourceFile = escapedMainFile;
  371. if (!settings.tempBuild) {
  372. // add the directory containing dub_test_root.d to the import paths
  373. tcinfo.importPaths[""] ~= NativePath(escapedMainFile).parentPath.toNativeString();
  374. }
  375.  
  376. if (generate_main && (settings.force || !existsFile(mainfile))) {
  377. ensureDirectory(mainfile.parentPath);
  378.  
  379. const runnerCode = custommodname.length ?
  380. format("import %s;", custommodname) : DefaultTestRunnerCode;
  381. const content = TestRunnerTemplate.format(
  382. import_modules, import_modules, runnerCode);
  383. writeFile(mainfile, content);
  384. }
  385.  
  386. rootPackage.recipe.configurations ~= ConfigurationInfo(config, tcinfo);
  387.  
  388. return config;
  389. }
  390.  
  391. /** Performs basic validation of various aspects of the package.
  392.  
  393. This will emit warnings to `stderr` if any discouraged names or
  394. dependency patterns are found.
  395. */
  396. void validate()
  397. {
  398. bool isSDL = !m_rootPackage.recipePath.empty
  399. && m_rootPackage.recipePath.head.name.endsWith(".sdl");
  400.  
  401. // some basic package lint
  402. m_rootPackage.warnOnSpecialCompilerFlags();
  403. string nameSuggestion() {
  404. string ret;
  405. ret ~= `Please modify the "name" field in %s accordingly.`.format(m_rootPackage.recipePath.toNativeString());
  406. if (!m_rootPackage.recipe.buildSettings.targetName.length) {
  407. if (isSDL) {
  408. ret ~= ` You can then add 'targetName "%s"' to keep the current executable name.`.format(m_rootPackage.name);
  409. } else {
  410. ret ~= ` You can then add '"targetName": "%s"' to keep the current executable name.`.format(m_rootPackage.name);
  411. }
  412. }
  413. return ret;
  414. }
  415. if (m_rootPackage.name != m_rootPackage.name.toLower()) {
  416. logWarn(`DUB package names should always be lower case. %s`, nameSuggestion());
  417. } else if (!m_rootPackage.recipe.name.all!(ch => ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' || ch == '-' || ch == '_')) {
  418. logWarn(`DUB package names may only contain alphanumeric characters, `
  419. ~ `as well as '-' and '_'. %s`, nameSuggestion());
  420. }
  421. enforce(!m_rootPackage.name.canFind(' '), "Aborting due to the package name containing spaces.");
  422.  
  423. foreach (d; m_rootPackage.getAllDependencies())
  424. if (d.spec.isExactVersion && d.spec.version_.isBranch) {
  425. string suggestion = isSDL
  426. ? format(`dependency "%s" repository="git+<git url>" version="<commit>"`, d.name)
  427. : format(`"%s": {"repository": "git+<git url>", "version": "<commit>"}`, d.name);
  428. logWarn("Dependency '%s' depends on git branch '%s', which is deprecated.",
  429. d.name.toString().color(Mode.bold),
  430. d.spec.version_.toString.color(Mode.bold));
  431. logWarnTag("", "Specify the git repository and commit hash in your %s:",
  432. (isSDL ? "dub.sdl" : "dub.json").color(Mode.bold));
  433. logWarnTag("", "%s", suggestion.color(Mode.bold));
  434. }
  435.  
  436. // search for orphan sub configurations
  437. void warnSubConfig(string pack, string config) {
  438. logWarn("The sub configuration directive \"%s\" -> [%s] "
  439. ~ "references a package that is not specified as a dependency "
  440. ~ "and will have no effect.", pack.color(Mode.bold), config.color(Color.blue));
  441. }
  442.  
  443. void checkSubConfig(in PackageName name, string config) {
  444. auto p = getDependency(name.toString(), true);
  445. if (p && !p.configurations.canFind(config)) {
  446. logWarn("The sub configuration directive \"%s\" -> [%s] "
  447. ~ "references a configuration that does not exist.",
  448. name.toString().color(Mode.bold), config.color(Color.red));
  449. }
  450. }
  451. auto globalbs = m_rootPackage.getBuildSettings();
  452. foreach (p, c; globalbs.subConfigurations) {
  453. if (p !in globalbs.dependencies) warnSubConfig(p, c);
  454. else checkSubConfig(PackageName(p), c);
  455. }
  456. foreach (c; m_rootPackage.configurations) {
  457. auto bs = m_rootPackage.getBuildSettings(c);
  458. foreach (p, subConf; bs.subConfigurations) {
  459. if (p !in bs.dependencies && p !in globalbs.dependencies)
  460. warnSubConfig(p, subConf);
  461. else checkSubConfig(PackageName(p), subConf);
  462. }
  463. }
  464.  
  465. // check for version specification mismatches
  466. bool[Package] visited;
  467. void validateDependenciesRec(Package pack) {
  468. // perform basic package linting
  469. pack.simpleLint();
  470.  
  471. foreach (d; pack.getAllDependencies()) {
  472. auto basename = d.name.main;
  473. d.spec.visit!(
  474. (NativePath path) { /* Valid */ },
  475. (Repository repo) { /* Valid */ },
  476. (VersionRange vers) {
  477. if (m_selections.hasSelectedVersion(basename)) {
  478. auto selver = m_selections.getSelectedVersion(basename);
  479. if (d.spec.merge(selver) == Dependency.Invalid) {
  480. logWarn(`Selected package %s@%s does not match ` ~
  481. `the dependency specification %s in ` ~
  482. `package %s. Need to "%s"?`,
  483. basename.toString().color(Mode.bold), selver,
  484. vers, pack.name.color(Mode.bold),
  485. "dub upgrade".color(Mode.bold));
  486. }
  487. }
  488. },
  489. );
  490.  
  491. auto deppack = getDependency(d.name.toString(), true);
  492. if (deppack in visited) continue;
  493. visited[deppack] = true;
  494. if (deppack) validateDependenciesRec(deppack);
  495. }
  496. }
  497. validateDependenciesRec(m_rootPackage);
  498. }
  499.  
  500. /**
  501. * Reloads dependencies
  502. *
  503. * This function goes through the project and make sure that all
  504. * required packages are loaded. To do so, it uses information
  505. * both from the recipe file (`dub.json`) and from the selections
  506. * file (`dub.selections.json`).
  507. *
  508. * In the process, it populates the `dependencies`, `missingDependencies`,
  509. * and `hasAllDependencies` properties, which can only be relied on
  510. * once this has run once (the constructor always calls this).
  511. */
  512. void reinit()
  513. {
  514. m_dependencies = null;
  515. m_dependenciesByName = null;
  516. m_missingDependencies = [];
  517. collectDependenciesRec(m_rootPackage);
  518. foreach (p; m_dependencies) m_dependenciesByName[p.name] = p;
  519. m_missingDependencies.sort();
  520. }
  521.  
  522. /// Implementation of `reinit`
  523. private void collectDependenciesRec(Package pack, int depth = 0)
  524. {
  525. auto indent = replicate(" ", depth);
  526. logDebug("%sCollecting dependencies for %s", indent, pack.name);
  527. indent ~= " ";
  528.  
  529. foreach (dep; pack.getAllDependencies()) {
  530. Dependency vspec = dep.spec;
  531. Package p;
  532.  
  533. auto basename = dep.name.main;
  534. auto subname = dep.name.sub;
  535.  
  536. // non-optional and optional-default dependencies (if no selections file exists)
  537. // need to be satisfied
  538. bool is_desired = !vspec.optional || m_selections.hasSelectedVersion(basename) || (vspec.default_ && m_selections.bare);
  539.  
  540. if (dep.name.toString() == m_rootPackage.basePackage.name) {
  541. vspec = Dependency(m_rootPackage.version_);
  542. p = m_rootPackage.basePackage;
  543. } else if (basename.toString() == m_rootPackage.basePackage.name) {
  544. vspec = Dependency(m_rootPackage.version_);
  545. try p = m_packageManager.getSubPackage(m_rootPackage.basePackage, subname, false);
  546. catch (Exception e) {
  547. logDiagnostic("%sError getting sub package %s: %s", indent, dep.name, e.msg);
  548. if (is_desired) m_missingDependencies ~= dep.name.toString();
  549. continue;
  550. }
  551. } else if (m_selections.hasSelectedVersion(basename)) {
  552. vspec = m_selections.getSelectedVersion(basename);
  553. p = vspec.visit!(
  554. (NativePath path_) {
  555. auto path = path_.absolute ? path_ : m_rootPackage.path ~ path_;
  556. auto tmp = m_packageManager.getOrLoadPackage(path, NativePath.init, true);
  557. return resolveSubPackage(tmp, subname, true);
  558. },
  559. (Repository repo) {
  560. auto tmp = m_packageManager.loadSCMPackage(basename, repo);
  561. return resolveSubPackage(tmp, subname, true);
  562. },
  563. (VersionRange range) {
  564. // See `dub.recipe.selection : SelectedDependency.fromYAML`
  565. assert(range.isExactVersion());
  566. return m_packageManager.getPackage(dep.name, vspec.version_);
  567. },
  568. );
  569. } else if (m_dependencies.canFind!(d => PackageName(d.name).main == basename)) {
  570. auto idx = m_dependencies.countUntil!(d => PackageName(d.name).main == basename);
  571. auto bp = m_dependencies[idx].basePackage;
  572. vspec = Dependency(bp.path);
  573. p = resolveSubPackage(bp, subname, false);
  574. } else {
  575. logDiagnostic("%sVersion selection for dependency %s (%s) of %s is missing.",
  576. indent, basename, dep.name, pack.name);
  577. }
  578.  
  579. // We didn't find the package
  580. if (p is null)
  581. {
  582. if (!vspec.repository.empty) {
  583. p = m_packageManager.loadSCMPackage(basename, vspec.repository);
  584. resolveSubPackage(p, subname, false);
  585. enforce(p !is null,
  586. "Unable to fetch '%s@%s' using git - does the repository and version exists?".format(
  587. dep.name, vspec.repository));
  588. } else if (!vspec.path.empty && is_desired) {
  589. NativePath path = vspec.path;
  590. if (!path.absolute) path = pack.path ~ path;
  591. logDiagnostic("%sAdding local %s in %s", indent, dep.name, path);
  592. p = m_packageManager.getOrLoadPackage(path, NativePath.init, true);
  593. if (p.parentPackage !is null) {
  594. logWarn("%sSub package %s must be referenced using the path to it's parent package.", indent, dep.name);
  595. p = p.parentPackage;
  596. }
  597. p = resolveSubPackage(p, subname, false);
  598. enforce(p.name == dep.name.toString(),
  599. format("Path based dependency %s is referenced with a wrong name: %s vs. %s",
  600. path.toNativeString(), dep.name, p.name));
  601. } else {
  602. logDiagnostic("%sMissing dependency %s %s of %s", indent, dep.name, vspec, pack.name);
  603. if (is_desired) m_missingDependencies ~= dep.name.toString();
  604. continue;
  605. }
  606. }
  607.  
  608. if (!m_dependencies.canFind(p)) {
  609. logDiagnostic("%sFound dependency %s %s", indent, dep.name, vspec.toString());
  610. m_dependencies ~= p;
  611. if (basename.toString() == m_rootPackage.basePackage.name)
  612. p.warnOnSpecialCompilerFlags();
  613. collectDependenciesRec(p, depth+1);
  614. }
  615.  
  616. m_dependees[p] ~= pack;
  617. //enforce(p !is null, "Failed to resolve dependency "~dep.name~" "~vspec.toString());
  618. }
  619. }
  620.  
  621. /// Convenience function used by `reinit`
  622. private Package resolveSubPackage(Package p, string subname, bool silentFail) {
  623. if (!subname.length || p is null)
  624. return p;
  625. return m_packageManager.getSubPackage(p, subname, silentFail);
  626. }
  627.  
  628. /// Returns the name of the root package.
  629. @property string name() const { return m_rootPackage ? m_rootPackage.name : "app"; }
  630.  
  631. /// Returns the names of all configurations of the root package.
  632. @property string[] configurations() const { return m_rootPackage.configurations; }
  633.  
  634. /// Returns the names of all built-in and custom build types of the root package.
  635. /// The default built-in build type is the first item in the list.
  636. @property string[] builds() const { return builtinBuildTypes ~ m_rootPackage.customBuildTypes; }
  637.  
  638. /// Returns a map with the configuration for all packages in the dependency tree.
  639. string[string] getPackageConfigs(in BuildPlatform platform, string config, bool allow_non_library = true)
  640. const {
  641. import std.typecons : Rebindable, rebindable;
  642. import std.range : only;
  643.  
  644. // prepare by collecting information about all packages in the project
  645. // qualified names and dependencies are cached, to avoid recomputing
  646. // them multiple times during the algorithm
  647. auto packages = collectPackageInformation();
  648.  
  649. // graph of the project's package configuration dependencies
  650. // (package, config) -> (sub-package, sub-config)
  651. static struct Vertex { size_t pack = size_t.max; string config; }
  652. static struct Edge { size_t from, to; }
  653. Vertex[] configs;
  654. void[0][Vertex] configs_set;
  655. Edge[] edges;
  656.  
  657.  
  658. size_t createConfig(size_t pack_idx, string config) {
  659. foreach (i, v; configs)
  660. if (v.pack == pack_idx && v.config == config)
  661. return i;
  662.  
  663. auto pname = packages[pack_idx].name;
  664. assert(pname !in m_overriddenConfigs || config == m_overriddenConfigs[pname]);
  665. logDebug("Add config %s %s", pname, config);
  666. auto cfg = Vertex(pack_idx, config);
  667. configs ~= cfg;
  668. configs_set[cfg] = (void[0]).init;
  669. return configs.length-1;
  670. }
  671.  
  672. bool haveConfig(size_t pack_idx, string config) {
  673. return (Vertex(pack_idx, config) in configs_set) !is null;
  674. }
  675.  
  676. void removeConfig(size_t config_index) {
  677. logDebug("Eliminating config %s for %s", configs[config_index].config, configs[config_index].pack);
  678. auto had_dep_to_pack = new bool[configs.length];
  679. auto still_has_dep_to_pack = new bool[configs.length];
  680.  
  681. // eliminate all edges that connect to config 'config_index' and
  682. // track all connected configs
  683. edges = edges.filterInPlace!((e) {
  684. if (e.to == config_index) {
  685. had_dep_to_pack[e.from] = true;
  686. return false;
  687. } else if (configs[e.to].pack == configs[config_index].pack) {
  688. still_has_dep_to_pack[e.from] = true;
  689. }
  690.  
  691. return e.from != config_index;
  692. });
  693.  
  694. // mark config as removed
  695. configs_set.remove(configs[config_index]);
  696. configs[config_index] = Vertex.init;
  697.  
  698. // also remove any configs that cannot be satisfied anymore
  699. foreach (j; 0 .. configs.length)
  700. if (j != config_index && had_dep_to_pack[j] && !still_has_dep_to_pack[j])
  701. removeConfig(j);
  702. }
  703.  
  704. bool[] reachable = new bool[packages.length]; // reused to avoid continuous re-allocation
  705. bool isReachableByAllParentPacks(size_t cidx) {
  706. foreach (p; packages[configs[cidx].pack].parents) reachable[p] = false;
  707. foreach (e; edges) {
  708. if (e.to != cidx) continue;
  709. reachable[configs[e.from].pack] = true;
  710. }
  711. foreach (p; packages[configs[cidx].pack].parents)
  712. if (!reachable[p])
  713. return false;
  714. return true;
  715. }
  716.  
  717. string[][] depconfigs = new string[][](packages.length);
  718. void determineDependencyConfigs(size_t pack_idx, string c)
  719. {
  720. void[0][Edge] edges_set;
  721. void createEdge(size_t from, size_t to) {
  722. if (Edge(from, to) in edges_set)
  723. return;
  724. logDebug("Including %s %s -> %s %s", configs[from].pack, configs[from].config, configs[to].pack, configs[to].config);
  725. edges ~= Edge(from, to);
  726. edges_set[Edge(from, to)] = (void[0]).init;
  727. }
  728.  
  729. auto pack = &packages[pack_idx];
  730.  
  731. // below we call createConfig for the main package if
  732. // config.length is not zero. Carry on for that case,
  733. // otherwise we've handle the pair (p, c) already
  734. if(haveConfig(pack_idx, c) && !(config.length && pack.name == m_rootPackage.name && config == c))
  735. return;
  736.  
  737. foreach (d; pack.dependencies) {
  738. auto dp = packages.getPackageIndex(d.name.toString());
  739. if (dp == size_t.max) continue;
  740.  
  741. depconfigs[dp].length = 0;
  742. depconfigs[dp].assumeSafeAppend;
  743.  
  744. void setConfigs(R)(R configs) {
  745. configs
  746. .filter!(c => haveConfig(dp, c))
  747. .each!((c) { depconfigs[dp] ~= c; });
  748. }
  749. if (auto pc = packages[dp].name in m_overriddenConfigs) {
  750. setConfigs(only(*pc));
  751. } else {
  752. auto subconf = pack.package_.getSubConfiguration(c, packages[dp].package_, platform);
  753. if (!subconf.empty) setConfigs(only(subconf));
  754. else setConfigs(packages[dp].package_.getPlatformConfigurations(platform));
  755. }
  756.  
  757. // if no valid configuration was found for a dependency, don't include the
  758. // current configuration
  759. if (!depconfigs[dp].length) {
  760. logDebug("Skip %s %s (missing configuration for %s)", pack.name, c, packages[dp].name);
  761. return;
  762. }
  763. }
  764.  
  765. // add this configuration to the graph
  766. size_t cidx = createConfig(pack_idx, c);
  767. foreach (d; pack.dependencies) {
  768. if (auto pdp = d.name.toString() in packages)
  769. foreach (sc; depconfigs[*pdp])
  770. createEdge(cidx, createConfig(*pdp, sc));
  771. }
  772. }
  773.  
  774. string[] allconfigs_path;
  775. void determineAllConfigs(size_t pack_idx)
  776. {
  777. auto pack = &packages[pack_idx];
  778.  
  779. auto idx = allconfigs_path.countUntil(pack.name);
  780. enforce(idx < 0, format("Detected dependency cycle: %s", (allconfigs_path[idx .. $] ~ pack.name).join("->")));
  781. allconfigs_path ~= pack.name;
  782. scope (exit) {
  783. allconfigs_path.length--;
  784. allconfigs_path.assumeSafeAppend;
  785. }
  786.  
  787. // first, add all dependency configurations
  788. foreach (d; pack.dependencies)
  789. if (auto pi = d.name.toString() in packages)
  790. determineAllConfigs(*pi);
  791.  
  792. // for each configuration, determine the configurations usable for the dependencies
  793. if (auto pc = pack.name in m_overriddenConfigs)
  794. determineDependencyConfigs(pack_idx, *pc);
  795. else
  796. foreach (c; pack.package_.getPlatformConfigurations(platform, pack.package_ is m_rootPackage && allow_non_library))
  797. determineDependencyConfigs(pack_idx, c);
  798. }
  799.  
  800.  
  801. // first, create a graph of all possible package configurations
  802. assert(packages[0].package_ is m_rootPackage);
  803. if (config.length) createConfig(0, config);
  804. determineAllConfigs(0);
  805.  
  806. // then, successively remove configurations until only one configuration
  807. // per package is left
  808. bool changed;
  809. do {
  810. // remove all configs that are not reachable by all parent packages
  811. changed = false;
  812. foreach (i, ref c; configs) {
  813. if (c == Vertex.init) continue; // ignore deleted configurations
  814. if (!isReachableByAllParentPacks(i)) {
  815. logDebug("%s %s NOT REACHABLE by all of (%s):", c.pack, c.config, packages[c.pack].parents);
  816. removeConfig(i);
  817. changed = true;
  818. }
  819. }
  820.  
  821. // when all edges are cleaned up, pick one package and remove all but one config
  822. if (!changed) {
  823. foreach (pidx; 0 .. packages.length) {
  824. size_t cnt = 0;
  825. foreach (i, ref c; configs)
  826. if (c.pack == pidx && ++cnt > 1) {
  827. logDebug("NON-PRIMARY: %s %s", c.pack, c.config);
  828. removeConfig(i);
  829. }
  830. if (cnt > 1) {
  831. changed = true;
  832. break;
  833. }
  834. }
  835. }
  836. } while (changed);
  837.  
  838. // print out the resulting tree
  839. foreach (e; edges) logDebug(" %s %s -> %s %s", configs[e.from].pack, configs[e.from].config, configs[e.to].pack, configs[e.to].config);
  840.  
  841. // return the resulting configuration set as an AA
  842. string[string] ret;
  843. foreach (c; configs) {
  844. if (c == Vertex.init) continue; // ignore deleted configurations
  845. auto pname = packages[c.pack].name;
  846. assert(ret.get(pname, c.config) == c.config, format("Conflicting configurations for %s found: %s vs. %s", pname, c.config, ret[pname]));
  847. logDebug("Using configuration '%s' for %s", c.config, pname);
  848. ret[pname] = c.config;
  849. }
  850.  
  851. // check for conflicts (packages missing in the final configuration graph)
  852. auto visited = new bool[](packages.length);
  853. void checkPacksRec(size_t pack_idx) {
  854. if (visited[pack_idx]) return;
  855. visited[pack_idx] = true;
  856. auto pname = packages[pack_idx].name;
  857. auto pc = pname in ret;
  858. enforce(pc !is null, "Could not resolve configuration for package "~pname);
  859. foreach (p, dep; packages[pack_idx].package_.getDependencies(*pc)) {
  860. auto deppack = getDependency(p, dep.optional);
  861. if (deppack) checkPacksRec(packages[].countUntil!(p => p.package_ is deppack));
  862. }
  863. }
  864. checkPacksRec(0);
  865.  
  866. return ret;
  867. }
  868.  
  869. /** Returns an ordered list of all packages with the additional possibility
  870. to look up by name.
  871. */
  872. private auto collectPackageInformation()
  873. const {
  874. static struct PackageInfo {
  875. const(Package) package_;
  876. size_t[] parents;
  877. string name;
  878. PackageDependency[] dependencies;
  879. }
  880.  
  881. static struct PackageInfoAccessor {
  882. private {
  883. PackageInfo[] m_packages;
  884. size_t[string] m_packageMap;
  885. }
  886.  
  887. private void initialize(P)(P all_packages, size_t reserve_count)
  888. {
  889. m_packages.reserve(reserve_count);
  890. foreach (p; all_packages) {
  891. auto pname = p.name;
  892. m_packageMap[pname] = m_packages.length;
  893. m_packages ~= PackageInfo(p, null, pname, p.getAllDependencies());
  894. }
  895. foreach (pack_idx, ref pack_info; m_packages)
  896. foreach (d; pack_info.dependencies)
  897. if (auto pi = d.name.toString() in m_packageMap)
  898. m_packages[*pi].parents ~= pack_idx;
  899. }
  900.  
  901. size_t length() const { return m_packages.length; }
  902. const(PackageInfo)[] opIndex() const { return m_packages; }
  903. ref const(PackageInfo) opIndex(size_t package_index) const { return m_packages[package_index]; }
  904. size_t getPackageIndex(string package_name) const { return m_packageMap.get(package_name, size_t.max); }
  905. const(size_t)* opBinaryRight(string op = "in")(string package_name) const { return package_name in m_packageMap; }
  906. }
  907.  
  908. PackageInfoAccessor ret;
  909. ret.initialize(getTopologicalPackageList(), m_dependencies.length);
  910. return ret;
  911. }
  912.  
  913. /**
  914. * Fills `dst` with values from this project.
  915. *
  916. * `dst` gets initialized according to the given platform and config.
  917. *
  918. * Params:
  919. * dst = The BuildSettings struct to fill with data.
  920. * gsettings = The generator settings to retrieve the values for.
  921. * config = Values of the given configuration will be retrieved.
  922. * root_package = If non null, use it instead of the project's real root package.
  923. * shallow = If true, collects only build settings for the main package (including inherited settings) and doesn't stop on target type none and sourceLibrary.
  924. */
  925. void addBuildSettings(ref BuildSettings dst, in GeneratorSettings gsettings, string config, in Package root_package = null, bool shallow = false)
  926. const {
  927. import dub.internal.utils : stripDlangSpecialChars;
  928.  
  929. auto configs = getPackageConfigs(gsettings.platform, config);
  930.  
  931. foreach (pkg; this.getTopologicalPackageList(false, root_package, configs)) {
  932. auto pkg_path = pkg.path.toNativeString();
  933. dst.addVersions(["Have_" ~ stripDlangSpecialChars(pkg.name)]);
  934.  
  935. assert(pkg.name in configs, "Missing configuration for "~pkg.name);
  936. logDebug("Gathering build settings for %s (%s)", pkg.name, configs[pkg.name]);
  937.  
  938. auto psettings = pkg.getBuildSettings(gsettings.platform, configs[pkg.name]);
  939. if (psettings.targetType != TargetType.none) {
  940. if (shallow && pkg !is m_rootPackage)
  941. psettings.sourceFiles = null;
  942. processVars(dst, this, pkg, psettings, gsettings);
  943. if (!gsettings.single && psettings.importPaths.empty)
  944. logWarn(`Package %s (configuration "%s") defines no import paths, use {"importPaths": [...]} or the default package directory structure to fix this.`, pkg.name, configs[pkg.name]);
  945. if (psettings.mainSourceFile.empty && pkg is m_rootPackage && psettings.targetType == TargetType.executable)
  946. logWarn(`Executable configuration "%s" of package %s defines no main source file, this may cause certain build modes to fail. Add an explicit "mainSourceFile" to the package description to fix this.`, configs[pkg.name], pkg.name);
  947. }
  948. if (pkg is m_rootPackage) {
  949. if (!shallow) {
  950. enforce(psettings.targetType != TargetType.none, "Main package has target type \"none\" - stopping build.");
  951. enforce(psettings.targetType != TargetType.sourceLibrary, "Main package has target type \"sourceLibrary\" which generates no target - stopping build.");
  952. }
  953. dst.targetType = psettings.targetType;
  954. dst.targetPath = psettings.targetPath;
  955. dst.targetName = psettings.targetName;
  956. if (!psettings.workingDirectory.empty)
  957. dst.workingDirectory = processVars(psettings.workingDirectory, this, pkg, gsettings, true, [dst.environments, dst.buildEnvironments]);
  958. if (psettings.mainSourceFile.length)
  959. dst.mainSourceFile = processVars(psettings.mainSourceFile, this, pkg, gsettings, true, [dst.environments, dst.buildEnvironments]);
  960. }
  961. }
  962.  
  963. // always add all version identifiers of all packages
  964. foreach (pkg; this.getTopologicalPackageList(false, null, configs)) {
  965. auto psettings = pkg.getBuildSettings(gsettings.platform, configs[pkg.name]);
  966. dst.addVersions(psettings.versions);
  967. }
  968. }
  969.  
  970. /** Fills `dst` with build settings specific to the given build type.
  971.  
  972. Params:
  973. dst = The `BuildSettings` instance to add the build settings to
  974. gsettings = Target generator settings
  975. for_root_package = Selects if the build settings are for the root
  976. package or for one of the dependencies. Unittest flags will
  977. only be added to the root package.
  978. */
  979. void addBuildTypeSettings(ref BuildSettings dst, in GeneratorSettings gsettings, bool for_root_package = true)
  980. {
  981. bool usedefflags = !(dst.requirements & BuildRequirement.noDefaultFlags);
  982. if (usedefflags) {
  983. BuildSettings btsettings;
  984. m_rootPackage.addBuildTypeSettings(btsettings, gsettings.platform, gsettings.buildType);
  985.  
  986. if (!for_root_package) {
  987. // don't propagate unittest switch to dependencies, as dependent
  988. // unit tests aren't run anyway and the additional code may
  989. // cause linking to fail on Windows (issue #640)
  990. btsettings.removeOptions(BuildOption.unittests);
  991. }
  992.  
  993. processVars(dst, this, m_rootPackage, btsettings, gsettings);
  994. }
  995. }
  996.  
  997. /// Outputs a build description of the project, including its dependencies.
  998. ProjectDescription describe(GeneratorSettings settings)
  999. {
  1000. import dub.generators.targetdescription;
  1001.  
  1002. // store basic build parameters
  1003. ProjectDescription ret;
  1004. ret.rootPackage = m_rootPackage.name;
  1005. ret.configuration = settings.config;
  1006. ret.buildType = settings.buildType;
  1007. ret.compiler = settings.platform.compiler;
  1008. ret.architecture = settings.platform.architecture;
  1009. ret.platform = settings.platform.platform;
  1010.  
  1011. // collect high level information about projects (useful for IDE display)
  1012. auto configs = getPackageConfigs(settings.platform, settings.config);
  1013. ret.packages ~= m_rootPackage.describe(settings.platform, settings.config);
  1014. foreach (dep; m_dependencies)
  1015. ret.packages ~= dep.describe(settings.platform, configs[dep.name]);
  1016.  
  1017. foreach (p; getTopologicalPackageList(false, null, configs))
  1018. ret.packages[ret.packages.countUntil!(pp => pp.name == p.name)].active = true;
  1019.  
  1020. if (settings.buildType.length) {
  1021. // collect build target information (useful for build tools)
  1022. auto gen = new TargetDescriptionGenerator(this);
  1023. try {
  1024. gen.generate(settings);
  1025. ret.targets = gen.targetDescriptions;
  1026. ret.targetLookup = gen.targetDescriptionLookup;
  1027. } catch (Exception e) {
  1028. logDiagnostic("Skipping targets description: %s", e.msg);
  1029. logDebug("Full error: %s", e.toString().sanitize);
  1030. }
  1031. }
  1032.  
  1033. return ret;
  1034. }
  1035.  
  1036. private string[] listBuildSetting(string attributeName)(ref GeneratorSettings settings,
  1037. string config, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping)
  1038. {
  1039. return listBuildSetting!attributeName(settings, getPackageConfigs(settings.platform, config),
  1040. projectDescription, compiler, disableEscaping);
  1041. }
  1042.  
  1043. private string[] listBuildSetting(string attributeName)(ref GeneratorSettings settings,
  1044. string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping)
  1045. {
  1046. if (compiler)
  1047. return formatBuildSettingCompiler!attributeName(settings, configs, projectDescription, compiler, disableEscaping);
  1048. else
  1049. return formatBuildSettingPlain!attributeName(settings, configs, projectDescription);
  1050. }
  1051.  
  1052. // Output a build setting formatted for a compiler
  1053. private string[] formatBuildSettingCompiler(string attributeName)(ref GeneratorSettings settings,
  1054. string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping)
  1055. {
  1056. import std.process : escapeShellFileName;
  1057. import std.path : dirSeparator;
  1058.  
  1059. assert(compiler);
  1060.  
  1061. auto targetDescription = projectDescription.lookupTarget(projectDescription.rootPackage);
  1062. auto buildSettings = targetDescription.buildSettings;
  1063.  
  1064. string[] values;
  1065. switch (attributeName)
  1066. {
  1067. case "dflags":
  1068. case "linkerFiles":
  1069. case "mainSourceFile":
  1070. case "importFiles":
  1071. values = formatBuildSettingPlain!attributeName(settings, configs, projectDescription);
  1072. break;
  1073.  
  1074. case "lflags":
  1075. case "sourceFiles":
  1076. case "injectSourceFiles":
  1077. case "versions":
  1078. case "debugVersions":
  1079. case "importPaths":
  1080. case "cImportPaths":
  1081. case "stringImportPaths":
  1082. case "options":
  1083. auto bs = buildSettings.dup;
  1084. bs.dflags = null;
  1085.  
  1086. // Ensure trailing slash on directory paths
  1087. auto ensureTrailingSlash = (string path) => path.endsWith(dirSeparator) ? path : path ~ dirSeparator;
  1088. static if (attributeName == "importPaths")
  1089. bs.importPaths = bs.importPaths.map!(ensureTrailingSlash).array();
  1090. else static if (attributeName == "cImportPaths")
  1091. bs.cImportPaths = bs.cImportPaths.map!(ensureTrailingSlash).array();
  1092. else static if (attributeName == "stringImportPaths")
  1093. bs.stringImportPaths = bs.stringImportPaths.map!(ensureTrailingSlash).array();
  1094.  
  1095. compiler.prepareBuildSettings(bs, settings.platform, BuildSetting.all & ~to!BuildSetting(attributeName));
  1096. values = bs.dflags;
  1097. break;
  1098.  
  1099. case "libs":
  1100. auto bs = buildSettings.dup;
  1101. bs.dflags = null;
  1102. bs.lflags = null;
  1103. bs.sourceFiles = null;
  1104. bs.targetType = TargetType.none; // Force Compiler to NOT omit dependency libs when package is a library.
  1105.  
  1106. compiler.prepareBuildSettings(bs, settings.platform, BuildSetting.all & ~to!BuildSetting(attributeName));
  1107.  
  1108. if (bs.lflags)
  1109. values = compiler.lflagsToDFlags( bs.lflags );
  1110. else if (bs.sourceFiles)
  1111. values = compiler.lflagsToDFlags( bs.sourceFiles );
  1112. else
  1113. values = bs.dflags;
  1114.  
  1115. break;
  1116.  
  1117. default: assert(0);
  1118. }
  1119.  
  1120. // Escape filenames and paths
  1121. if(!disableEscaping)
  1122. {
  1123. switch (attributeName)
  1124. {
  1125. case "mainSourceFile":
  1126. case "linkerFiles":
  1127. case "injectSourceFiles":
  1128. case "copyFiles":
  1129. case "importFiles":
  1130. case "stringImportFiles":
  1131. case "sourceFiles":
  1132. case "importPaths":
  1133. case "cImportPaths":
  1134. case "stringImportPaths":
  1135. return values.map!(escapeShellFileName).array();
  1136.  
  1137. default:
  1138. return values;
  1139. }
  1140. }
  1141.  
  1142. return values;
  1143. }
  1144.  
  1145. // Output a build setting without formatting for any particular compiler
  1146. private string[] formatBuildSettingPlain(string attributeName)(ref GeneratorSettings settings, string[string] configs, ProjectDescription projectDescription)
  1147. {
  1148. import std.path : buildNormalizedPath, dirSeparator;
  1149. import std.range : only;
  1150.  
  1151. string[] list;
  1152.  
  1153. enforce(attributeName == "targetType" || projectDescription.lookupRootPackage().targetType != TargetType.none,
  1154. "Target type is 'none'. Cannot list build settings.");
  1155.  
  1156. static if (attributeName == "targetType")
  1157. if (projectDescription.rootPackage !in projectDescription.targetLookup)
  1158. return ["none"];
  1159.  
  1160. auto targetDescription = projectDescription.lookupTarget(projectDescription.rootPackage);
  1161. auto buildSettings = targetDescription.buildSettings;
  1162.  
  1163. string[] substituteCommands(Package pack, string[] commands, CommandType type)
  1164. {
  1165. auto env = makeCommandEnvironmentVariables(type, pack, this, settings, buildSettings);
  1166. return processVars(this, pack, settings, commands, false, env);
  1167. }
  1168.  
  1169. // Return any BuildSetting member attributeName as a range of strings. Don't attempt to fixup values.
  1170. // allowEmptyString: When the value is a string (as opposed to string[]),
  1171. // is empty string an actual permitted value instead of
  1172. // a missing value?
  1173. auto getRawBuildSetting(Package pack, bool allowEmptyString) {
  1174. auto value = __traits(getMember, buildSettings, attributeName);
  1175.  
  1176. static if( attributeName.endsWith("Commands") )
  1177. return substituteCommands(pack, value, mixin("CommandType.", attributeName[0 .. $ - "Commands".length]));
  1178. else static if( is(typeof(value) == string[]) )
  1179. return value;
  1180. else static if( is(typeof(value) == string) )
  1181. {
  1182. auto ret = only(value);
  1183.  
  1184. // only() has a different return type from only(value), so we
  1185. // have to empty the range rather than just returning only().
  1186. if(value.empty && !allowEmptyString) {
  1187. ret.popFront();
  1188. assert(ret.empty);
  1189. }
  1190.  
  1191. return ret;
  1192. }
  1193. else static if( is(typeof(value) == string[string]) )
  1194. return value.byKeyValue.map!(a => a.key ~ "=" ~ a.value);
  1195. else static if( is(typeof(value) == enum) )
  1196. return only(value);
  1197. else static if( is(typeof(value) == Flags!BuildRequirement) )
  1198. return only(cast(BuildRequirement) cast(int) value.values);
  1199. else static if( is(typeof(value) == Flags!BuildOption) )
  1200. return only(cast(BuildOption) cast(int) value.values);
  1201. else
  1202. static assert(false, "Type of BuildSettings."~attributeName~" is unsupported.");
  1203. }
  1204.  
  1205. // Adjust BuildSetting member attributeName as needed.
  1206. // Returns a range of strings.
  1207. auto getFixedBuildSetting(Package pack) {
  1208. // Is relative path(s) to a directory?
  1209. enum isRelativeDirectory =
  1210. attributeName == "importPaths" || attributeName == "cImportPaths" || attributeName == "stringImportPaths" ||
  1211. attributeName == "targetPath" || attributeName == "workingDirectory";
  1212.  
  1213. // Is relative path(s) to a file?
  1214. enum isRelativeFile =
  1215. attributeName == "sourceFiles" || attributeName == "linkerFiles" ||
  1216. attributeName == "importFiles" || attributeName == "stringImportFiles" ||
  1217. attributeName == "copyFiles" || attributeName == "mainSourceFile" ||
  1218. attributeName == "injectSourceFiles";
  1219.  
  1220. // For these, empty string means "main project directory", not "missing value"
  1221. enum allowEmptyString =
  1222. attributeName == "targetPath" || attributeName == "workingDirectory";
  1223.  
  1224. enum isEnumBitfield =
  1225. attributeName == "requirements" || attributeName == "options";
  1226.  
  1227. enum isEnum = attributeName == "targetType";
  1228.  
  1229. auto values = getRawBuildSetting(pack, allowEmptyString);
  1230. string fixRelativePath(string importPath) { return buildNormalizedPath(pack.path.toString(), importPath); }
  1231. static string ensureTrailingSlash(string path) { return path.endsWith(dirSeparator) ? path : path ~ dirSeparator; }
  1232.  
  1233. static if(isRelativeDirectory) {
  1234. // Return full paths for the paths, making sure a
  1235. // directory separator is on the end of each path.
  1236. return values.map!(fixRelativePath).map!(ensureTrailingSlash);
  1237. }
  1238. else static if(isRelativeFile) {
  1239. // Return full paths.
  1240. return values.map!(fixRelativePath);
  1241. }
  1242. else static if(isEnumBitfield)
  1243. return bitFieldNames(values.front);
  1244. else static if (isEnum)
  1245. return [values.front.to!string];
  1246. else
  1247. return values;
  1248. }
  1249.  
  1250. foreach(value; getFixedBuildSetting(m_rootPackage)) {
  1251. list ~= value;
  1252. }
  1253.  
  1254. return list;
  1255. }
  1256.  
  1257. // The "compiler" arg is for choosing which compiler the output should be formatted for,
  1258. // or null to imply "list" format.
  1259. private string[] listBuildSetting(ref GeneratorSettings settings, string[string] configs,
  1260. ProjectDescription projectDescription, string requestedData, Compiler compiler, bool disableEscaping)
  1261. {
  1262. // Certain data cannot be formatter for a compiler
  1263. if (compiler)
  1264. {
  1265. switch (requestedData)
  1266. {
  1267. case "target-type":
  1268. case "target-path":
  1269. case "target-name":
  1270. case "working-directory":
  1271. case "string-import-files":
  1272. case "copy-files":
  1273. case "extra-dependency-files":
  1274. case "pre-generate-commands":
  1275. case "post-generate-commands":
  1276. case "pre-build-commands":
  1277. case "post-build-commands":
  1278. case "pre-run-commands":
  1279. case "post-run-commands":
  1280. case "environments":
  1281. case "build-environments":
  1282. case "run-environments":
  1283. case "pre-generate-environments":
  1284. case "post-generate-environments":
  1285. case "pre-build-environments":
  1286. case "post-build-environments":
  1287. case "pre-run-environments":
  1288. case "post-run-environments":
  1289. case "default-config":
  1290. case "configs":
  1291. case "default-build":
  1292. case "builds":
  1293. enforce(false, "--data="~requestedData~" can only be used with `--data-list` or `--data-list --data-0`.");
  1294. break;
  1295.  
  1296. case "requirements":
  1297. enforce(false, "--data=requirements can only be used with `--data-list` or `--data-list --data-0`. Use --data=options instead.");
  1298. break;
  1299.  
  1300. default: break;
  1301. }
  1302. }
  1303.  
  1304. import std.typetuple : TypeTuple;
  1305. auto args = TypeTuple!(settings, configs, projectDescription, compiler, disableEscaping);
  1306. switch (requestedData)
  1307. {
  1308. case "target-type": return listBuildSetting!"targetType"(args);
  1309. case "target-path": return listBuildSetting!"targetPath"(args);
  1310. case "target-name": return listBuildSetting!"targetName"(args);
  1311. case "working-directory": return listBuildSetting!"workingDirectory"(args);
  1312. case "main-source-file": return listBuildSetting!"mainSourceFile"(args);
  1313. case "dflags": return listBuildSetting!"dflags"(args);
  1314. case "lflags": return listBuildSetting!"lflags"(args);
  1315. case "libs": return listBuildSetting!"libs"(args);
  1316. case "linker-files": return listBuildSetting!"linkerFiles"(args);
  1317. case "source-files": return listBuildSetting!"sourceFiles"(args);
  1318. case "inject-source-files": return listBuildSetting!"injectSourceFiles"(args);
  1319. case "copy-files": return listBuildSetting!"copyFiles"(args);
  1320. case "extra-dependency-files": return listBuildSetting!"extraDependencyFiles"(args);
  1321. case "versions": return listBuildSetting!"versions"(args);
  1322. case "debug-versions": return listBuildSetting!"debugVersions"(args);
  1323. case "import-paths": return listBuildSetting!"importPaths"(args);
  1324. case "string-import-paths": return listBuildSetting!"stringImportPaths"(args);
  1325. case "import-files": return listBuildSetting!"importFiles"(args);
  1326. case "string-import-files": return listBuildSetting!"stringImportFiles"(args);
  1327. case "pre-generate-commands": return listBuildSetting!"preGenerateCommands"(args);
  1328. case "post-generate-commands": return listBuildSetting!"postGenerateCommands"(args);
  1329. case "pre-build-commands": return listBuildSetting!"preBuildCommands"(args);
  1330. case "post-build-commands": return listBuildSetting!"postBuildCommands"(args);
  1331. case "pre-run-commands": return listBuildSetting!"preRunCommands"(args);
  1332. case "post-run-commands": return listBuildSetting!"postRunCommands"(args);
  1333. case "environments": return listBuildSetting!"environments"(args);
  1334. case "build-environments": return listBuildSetting!"buildEnvironments"(args);
  1335. case "run-environments": return listBuildSetting!"runEnvironments"(args);
  1336. case "pre-generate-environments": return listBuildSetting!"preGenerateEnvironments"(args);
  1337. case "post-generate-environments": return listBuildSetting!"postGenerateEnvironments"(args);
  1338. case "pre-build-environments": return listBuildSetting!"preBuildEnvironments"(args);
  1339. case "post-build-environments": return listBuildSetting!"postBuildEnvironments"(args);
  1340. case "pre-run-environments": return listBuildSetting!"preRunEnvironments"(args);
  1341. case "post-run-environments": return listBuildSetting!"postRunEnvironments"(args);
  1342. case "requirements": return listBuildSetting!"requirements"(args);
  1343. case "options": return listBuildSetting!"options"(args);
  1344. case "default-config": return [getDefaultConfiguration(settings.platform)];
  1345. case "configs": return configurations;
  1346. case "default-build": return [builds[0]];
  1347. case "builds": return builds;
  1348.  
  1349. default:
  1350. enforce(false, "--data="~requestedData~
  1351. " is not a valid option. See 'dub describe --help' for accepted --data= values.");
  1352. }
  1353.  
  1354. assert(0);
  1355. }
  1356.  
  1357. /// Outputs requested data for the project, optionally including its dependencies.
  1358. string[] listBuildSettings(GeneratorSettings settings, string[] requestedData, ListBuildSettingsFormat list_type)
  1359. {
  1360. import dub.compilers.utils : isLinkerFile;
  1361.  
  1362. auto projectDescription = describe(settings);
  1363. auto configs = getPackageConfigs(settings.platform, settings.config);
  1364. PackageDescription packageDescription;
  1365. foreach (pack; projectDescription.packages) {
  1366. if (pack.name == projectDescription.rootPackage)
  1367. packageDescription = pack;
  1368. }
  1369.  
  1370. if (projectDescription.rootPackage in projectDescription.targetLookup) {
  1371. // Copy linker files from sourceFiles to linkerFiles
  1372. auto target = projectDescription.lookupTarget(projectDescription.rootPackage);
  1373. foreach (file; target.buildSettings.sourceFiles.filter!(f => isLinkerFile(settings.platform, f)))
  1374. target.buildSettings.addLinkerFiles(file);
  1375.  
  1376. // Remove linker files from sourceFiles
  1377. target.buildSettings.sourceFiles =
  1378. target.buildSettings.sourceFiles
  1379. .filter!(a => !isLinkerFile(settings.platform, a))
  1380. .array();
  1381. projectDescription.lookupTarget(projectDescription.rootPackage) = target;
  1382. }
  1383.  
  1384. Compiler compiler;
  1385. bool no_escape;
  1386. final switch (list_type) with (ListBuildSettingsFormat) {
  1387. case list: break;
  1388. case listNul: no_escape = true; break;
  1389. case commandLine: compiler = settings.compiler; break;
  1390. case commandLineNul: compiler = settings.compiler; no_escape = true; break;
  1391.  
  1392. }
  1393.  
  1394. auto result = requestedData
  1395. .map!(dataName => listBuildSetting(settings, configs, projectDescription, dataName, compiler, no_escape));
  1396.  
  1397. final switch (list_type) with (ListBuildSettingsFormat) {
  1398. case list: return result.map!(l => l.join("\n")).array();
  1399. case listNul: return result.map!(l => l.join("\0")).array;
  1400. case commandLine: return result.map!(l => l.join(" ")).array;
  1401. case commandLineNul: return result.map!(l => l.join("\0")).array;
  1402. }
  1403. }
  1404.  
  1405. /** Saves the currently selected dependency versions to disk.
  1406.  
  1407. The selections will be written to a file named
  1408. `SelectedVersions.defaultFile` ("dub.selections.json") within the
  1409. directory of the root package. Any existing file will get overwritten.
  1410. */
  1411. void saveSelections()
  1412. {
  1413. assert(m_selections !is null, "Cannot save selections for non-disk based project (has no selections).");
  1414. const name = PackageName(m_rootPackage.basePackage.name);
  1415. if (m_selections.hasSelectedVersion(name))
  1416. m_selections.deselectVersion(name);
  1417. this.m_packageManager.writeSelections(
  1418. this.m_rootPackage, this.m_selections.m_selections,
  1419. this.m_selections.dirty);
  1420. }
  1421.  
  1422. deprecated bool isUpgradeCacheUpToDate()
  1423. {
  1424. return false;
  1425. }
  1426.  
  1427. deprecated Dependency[string] getUpgradeCache()
  1428. {
  1429. return null;
  1430. }
  1431. }
  1432.  
  1433.  
  1434. /// Determines the output format used for `Project.listBuildSettings`.
  1435. enum ListBuildSettingsFormat {
  1436. list, /// Newline separated list entries
  1437. listNul, /// NUL character separated list entries (unescaped)
  1438. commandLine, /// Formatted for compiler command line (one data list per line)
  1439. commandLineNul, /// NUL character separated list entries (unescaped, data lists separated by two NUL characters)
  1440. }
  1441.  
  1442. deprecated("Use `dub.packagemanager : PlacementLocation` instead")
  1443. public alias PlacementLocation = dub.packagemanager.PlacementLocation;
  1444.  
  1445. void processVars(ref BuildSettings dst, in Project project, in Package pack,
  1446. BuildSettings settings, in GeneratorSettings gsettings, bool include_target_settings = false)
  1447. {
  1448. string[string] processVerEnvs(in string[string] targetEnvs, in string[string] defaultEnvs)
  1449. {
  1450. string[string] retEnv;
  1451. foreach (k, v; targetEnvs)
  1452. retEnv[k] = v;
  1453. foreach (k, v; defaultEnvs) {
  1454. if (k !in targetEnvs)
  1455. retEnv[k] = v;
  1456. }
  1457. return processVars(project, pack, gsettings, retEnv);
  1458. }
  1459. dst.addEnvironments(processVerEnvs(settings.environments, gsettings.buildSettings.environments));
  1460. dst.addBuildEnvironments(processVerEnvs(settings.buildEnvironments, gsettings.buildSettings.buildEnvironments));
  1461. dst.addRunEnvironments(processVerEnvs(settings.runEnvironments, gsettings.buildSettings.runEnvironments));
  1462. dst.addPreGenerateEnvironments(processVerEnvs(settings.preGenerateEnvironments, gsettings.buildSettings.preGenerateEnvironments));
  1463. dst.addPostGenerateEnvironments(processVerEnvs(settings.postGenerateEnvironments, gsettings.buildSettings.postGenerateEnvironments));
  1464. dst.addPreBuildEnvironments(processVerEnvs(settings.preBuildEnvironments, gsettings.buildSettings.preBuildEnvironments));
  1465. dst.addPostBuildEnvironments(processVerEnvs(settings.postBuildEnvironments, gsettings.buildSettings.postBuildEnvironments));
  1466. dst.addPreRunEnvironments(processVerEnvs(settings.preRunEnvironments, gsettings.buildSettings.preRunEnvironments));
  1467. dst.addPostRunEnvironments(processVerEnvs(settings.postRunEnvironments, gsettings.buildSettings.postRunEnvironments));
  1468.  
  1469. auto buildEnvs = [dst.environments, dst.buildEnvironments];
  1470.  
  1471. dst.addDFlags(processVars(project, pack, gsettings, settings.dflags, false, buildEnvs));
  1472. dst.addLFlags(processVars(project, pack, gsettings, settings.lflags, false, buildEnvs));
  1473. dst.addLibs(processVars(project, pack, gsettings, settings.libs, false, buildEnvs));
  1474. dst.addSourceFiles(processVars!true(project, pack, gsettings, settings.sourceFiles, true, buildEnvs));
  1475. dst.addImportFiles(processVars(project, pack, gsettings, settings.importFiles, true, buildEnvs));
  1476. dst.addStringImportFiles(processVars(project, pack, gsettings, settings.stringImportFiles, true, buildEnvs));
  1477. dst.addInjectSourceFiles(processVars!true(project, pack, gsettings, settings.injectSourceFiles, true, buildEnvs));
  1478. dst.addCopyFiles(processVars(project, pack, gsettings, settings.copyFiles, true, buildEnvs));
  1479. dst.addExtraDependencyFiles(processVars(project, pack, gsettings, settings.extraDependencyFiles, true, buildEnvs));
  1480. dst.addVersions(processVars(project, pack, gsettings, settings.versions, false, buildEnvs));
  1481. dst.addDebugVersions(processVars(project, pack, gsettings, settings.debugVersions, false, buildEnvs));
  1482. dst.addVersionFilters(processVars(project, pack, gsettings, settings.versionFilters, false, buildEnvs));
  1483. dst.addDebugVersionFilters(processVars(project, pack, gsettings, settings.debugVersionFilters, false, buildEnvs));
  1484. dst.addImportPaths(processVars(project, pack, gsettings, settings.importPaths, true, buildEnvs));
  1485. dst.addCImportPaths(processVars(project, pack, gsettings, settings.cImportPaths, true, buildEnvs));
  1486. dst.addStringImportPaths(processVars(project, pack, gsettings, settings.stringImportPaths, true, buildEnvs));
  1487. dst.addRequirements(settings.requirements);
  1488. dst.addOptions(settings.options);
  1489.  
  1490. // commands are substituted in dub.generators.generator : runBuildCommands
  1491. dst.addPreGenerateCommands(settings.preGenerateCommands);
  1492. dst.addPostGenerateCommands(settings.postGenerateCommands);
  1493. dst.addPreBuildCommands(settings.preBuildCommands);
  1494. dst.addPostBuildCommands(settings.postBuildCommands);
  1495. dst.addPreRunCommands(settings.preRunCommands);
  1496. dst.addPostRunCommands(settings.postRunCommands);
  1497.  
  1498. if (include_target_settings) {
  1499. dst.targetType = settings.targetType;
  1500. dst.targetPath = processVars(settings.targetPath, project, pack, gsettings, true, buildEnvs);
  1501. dst.targetName = settings.targetName;
  1502. if (!settings.workingDirectory.empty)
  1503. dst.workingDirectory = processVars(settings.workingDirectory, project, pack, gsettings, true, buildEnvs);
  1504. if (settings.mainSourceFile.length)
  1505. dst.mainSourceFile = processVars(settings.mainSourceFile, project, pack, gsettings, true, buildEnvs);
  1506. }
  1507. }
  1508.  
  1509. string[] processVars(bool glob = false)(in Project project, in Package pack, in GeneratorSettings gsettings, in string[] vars, bool are_paths = false, in string[string][] extraVers = null)
  1510. {
  1511. auto ret = appender!(string[])();
  1512. processVars!glob(ret, project, pack, gsettings, vars, are_paths, extraVers);
  1513. return ret.data;
  1514. }
  1515. void processVars(bool glob = false)(ref Appender!(string[]) dst, in Project project, in Package pack, in GeneratorSettings gsettings, in string[] vars, bool are_paths = false, in string[string][] extraVers = null)
  1516. {
  1517. static if (glob)
  1518. alias process = processVarsWithGlob!(Project, Package);
  1519. else
  1520. alias process = processVars!(Project, Package);
  1521. foreach (var; vars)
  1522. dst.put(process(var, project, pack, gsettings, are_paths, extraVers));
  1523. }
  1524.  
  1525. string processVars(Project, Package)(string var, in Project project, in Package pack, in GeneratorSettings gsettings, bool is_path, in string[string][] extraVers = null)
  1526. {
  1527. var = var.expandVars!(varName => getVariable(varName, project, pack, gsettings, extraVers));
  1528. if (!is_path)
  1529. return var;
  1530. auto p = NativePath(var);
  1531. if (!p.absolute)
  1532. return (pack.path ~ p).toNativeString();
  1533. else
  1534. return p.toNativeString();
  1535. }
  1536. string[string] processVars(bool glob = false)(in Project project, in Package pack, in GeneratorSettings gsettings, in string[string] vars, in string[string][] extraVers = null)
  1537. {
  1538. string[string] ret;
  1539. processVars!glob(ret, project, pack, gsettings, vars, extraVers);
  1540. return ret;
  1541. }
  1542. void processVars(bool glob = false)(ref string[string] dst, in Project project, in Package pack, in GeneratorSettings gsettings, in string[string] vars, in string[string][] extraVers)
  1543. {
  1544. static if (glob)
  1545. alias process = processVarsWithGlob!(Project, Package);
  1546. else
  1547. alias process = processVars!(Project, Package);
  1548. foreach (k, var; vars)
  1549. dst[k] = process(var, project, pack, gsettings, false, extraVers);
  1550. }
  1551.  
  1552. private string[] processVarsWithGlob(Project, Package)(string var, in Project project, in Package pack, in GeneratorSettings gsettings, bool is_path, in string[string][] extraVers)
  1553. {
  1554. assert(is_path, "can't glob something that isn't a path");
  1555. string res = processVars(var, project, pack, gsettings, is_path, extraVers);
  1556. // Find the unglobbed prefix and iterate from there.
  1557. size_t i = 0;
  1558. size_t sepIdx = 0;
  1559. loop: while (i < res.length) {
  1560. switch_: switch (res[i])
  1561. {
  1562. case '*', '?', '[', '{': break loop;
  1563. case '/': sepIdx = i; goto default;
  1564. version (Windows) { case '\\': sepIdx = i; goto default; }
  1565. default: ++i; break switch_;
  1566. }
  1567. }
  1568. if (i == res.length) //no globbing found in the path
  1569. return [res];
  1570. import std.file : dirEntries, SpanMode;
  1571. import std.path : buildNormalizedPath, globMatch, isAbsolute, relativePath;
  1572. auto cwd = gsettings.toolWorkingDirectory.toNativeString;
  1573. auto path = res[0 .. sepIdx];
  1574. bool prependCwd = false;
  1575. if (!isAbsolute(path))
  1576. {
  1577. prependCwd = true;
  1578. path = buildNormalizedPath(cwd, path);
  1579. }
  1580.  
  1581. return dirEntries(path, SpanMode.depth)
  1582. .map!(de => prependCwd
  1583. ? de.name.relativePath(cwd)
  1584. : de.name)
  1585. .filter!(name => globMatch(name, res))
  1586. .array;
  1587. }
  1588. /// Expand variables using `$VAR_NAME` or `${VAR_NAME}` syntax.
  1589. /// `$$` escapes itself and is expanded to a single `$`.
  1590. private string expandVars(alias expandVar)(string s)
  1591. {
  1592. import std.functional : not;
  1593.  
  1594. auto result = appender!string;
  1595.  
  1596. static bool isVarChar(char c)
  1597. {
  1598. import std.ascii;
  1599. return isAlphaNum(c) || c == '_';
  1600. }
  1601.  
  1602. while (true)
  1603. {
  1604. auto pos = s.indexOf('$');
  1605. if (pos < 0)
  1606. {
  1607. result.put(s);
  1608. return result.data;
  1609. }
  1610. result.put(s[0 .. pos]);
  1611. s = s[pos + 1 .. $];
  1612. enforce(s.length > 0, "Variable name expected at end of string");
  1613. switch (s[0])
  1614. {
  1615. case '$':
  1616. result.put("$");
  1617. s = s[1 .. $];
  1618. break;
  1619. case '{':
  1620. pos = s.indexOf('}');
  1621. enforce(pos >= 0, "Could not find '}' to match '${'");
  1622. result.put(expandVar(s[1 .. pos]));
  1623. s = s[pos + 1 .. $];
  1624. break;
  1625. default:
  1626. pos = s.representation.countUntil!(not!isVarChar);
  1627. if (pos < 0)
  1628. pos = s.length;
  1629. result.put(expandVar(s[0 .. pos]));
  1630. s = s[pos .. $];
  1631. break;
  1632. }
  1633. }
  1634. }
  1635.  
  1636. unittest
  1637. {
  1638. string[string] vars =
  1639. [
  1640. "A" : "a",
  1641. "B" : "b",
  1642. ];
  1643.  
  1644. string expandVar(string name) { auto p = name in vars; enforce(p, name); return *p; }
  1645.  
  1646. assert(expandVars!expandVar("") == "");
  1647. assert(expandVars!expandVar("x") == "x");
  1648. assert(expandVars!expandVar("$$") == "$");
  1649. assert(expandVars!expandVar("x$$") == "x$");
  1650. assert(expandVars!expandVar("$$x") == "$x");
  1651. assert(expandVars!expandVar("$$$$") == "$$");
  1652. assert(expandVars!expandVar("x$A") == "xa");
  1653. assert(expandVars!expandVar("x$$A") == "x$A");
  1654. assert(expandVars!expandVar("$A$B") == "ab");
  1655. assert(expandVars!expandVar("${A}$B") == "ab");
  1656. assert(expandVars!expandVar("$A${B}") == "ab");
  1657. assert(expandVars!expandVar("a${B}") == "ab");
  1658. assert(expandVars!expandVar("${A}b") == "ab");
  1659.  
  1660. import std.exception : assertThrown;
  1661. assertThrown(expandVars!expandVar("$"));
  1662. assertThrown(expandVars!expandVar("${}"));
  1663. assertThrown(expandVars!expandVar("$|"));
  1664. assertThrown(expandVars!expandVar("x$"));
  1665. assertThrown(expandVars!expandVar("$X"));
  1666. assertThrown(expandVars!expandVar("${"));
  1667. assertThrown(expandVars!expandVar("${X"));
  1668.  
  1669. // https://github.com/dlang/dmd/pull/9275
  1670. assert(expandVars!expandVar("$${DUB_EXE:-dub}") == "${DUB_EXE:-dub}");
  1671. }
  1672.  
  1673. /// Expands the variables in the input string with the same rules as command
  1674. /// variables inside custom dub commands.
  1675. ///
  1676. /// Params:
  1677. /// s = the input string where environment variables in form `$VAR` should be replaced
  1678. /// throwIfMissing = if true, throw an exception if the given variable is not found,
  1679. /// otherwise replace unknown variables with the empty string.
  1680. string expandEnvironmentVariables(string s, bool throwIfMissing = true)
  1681. {
  1682. import std.process : environment;
  1683.  
  1684. return expandVars!((v) {
  1685. auto ret = environment.get(v);
  1686. if (ret is null && throwIfMissing)
  1687. throw new Exception("Specified environment variable `$" ~ v ~ "` is not set");
  1688. return ret;
  1689. })(s);
  1690. }
  1691.  
  1692. // Keep the following list up-to-date if adding more build settings variables.
  1693. /// List of variables that can be used in build settings
  1694. package(dub) immutable buildSettingsVars = [
  1695. "ARCH", "PLATFORM", "PLATFORM_POSIX", "BUILD_TYPE"
  1696. ];
  1697.  
  1698. private string getVariable(Project, Package)(string name, in Project project, in Package pack, in GeneratorSettings gsettings, in string[string][] extraVars = null)
  1699. {
  1700. import dub.internal.utils : getDUBExePath;
  1701. import std.process : environment, escapeShellFileName;
  1702. import std.uni : asUpperCase;
  1703.  
  1704. NativePath path;
  1705. if (name == "PACKAGE_DIR")
  1706. path = pack.path;
  1707. else if (name == "ROOT_PACKAGE_DIR")
  1708. path = project.rootPackage.path;
  1709.  
  1710. if (name.endsWith("_PACKAGE_DIR")) {
  1711. auto pname = name[0 .. $-12];
  1712. foreach (prj; project.getTopologicalPackageList())
  1713. if (prj.name.asUpperCase.map!(a => a == '-' ? '_' : a).equal(pname))
  1714. {
  1715. path = prj.path;
  1716. break;
  1717. }
  1718. }
  1719.  
  1720. if (!path.empty)
  1721. {
  1722. // no trailing slash for clean path concatenation (see #1392)
  1723. path.endsWithSlash = false;
  1724. return path.toNativeString();
  1725. }
  1726.  
  1727. if (name == "DUB") {
  1728. return getDUBExePath(gsettings.platform.compilerBinary).toNativeString();
  1729. }
  1730.  
  1731. if (name == "ARCH") {
  1732. foreach (a; gsettings.platform.architecture)
  1733. return a;
  1734. return "";
  1735. }
  1736.  
  1737. if (name == "PLATFORM") {
  1738. import std.algorithm : filter;
  1739. foreach (p; gsettings.platform.platform.filter!(p => p != "posix"))
  1740. return p;
  1741. foreach (p; gsettings.platform.platform)
  1742. return p;
  1743. return "";
  1744. }
  1745.  
  1746. if (name == "PLATFORM_POSIX") {
  1747. import std.algorithm : canFind;
  1748. if (gsettings.platform.platform.canFind("posix"))
  1749. return "posix";
  1750. foreach (p; gsettings.platform.platform)
  1751. return p;
  1752. return "";
  1753. }
  1754.  
  1755. if (name == "BUILD_TYPE") return gsettings.buildType;
  1756.  
  1757. if (name == "DFLAGS" || name == "LFLAGS")
  1758. {
  1759. auto buildSettings = pack.getBuildSettings(gsettings.platform, gsettings.config);
  1760. if (name == "DFLAGS")
  1761. return join(buildSettings.dflags," ");
  1762. else if (name == "LFLAGS")
  1763. return join(buildSettings.lflags," ");
  1764. }
  1765.  
  1766. import std.range;
  1767. foreach (aa; retro(extraVars))
  1768. if (auto exvar = name in aa)
  1769. return *exvar;
  1770.  
  1771. auto envvar = environment.get(name);
  1772. if (envvar !is null) return envvar;
  1773.  
  1774. throw new Exception("Invalid variable: "~name);
  1775. }
  1776.  
  1777.  
  1778. unittest
  1779. {
  1780. static struct MockPackage
  1781. {
  1782. this(string name)
  1783. {
  1784. this.name = name;
  1785. version (Posix)
  1786. path = NativePath("/pkgs/"~name);
  1787. else version (Windows)
  1788. path = NativePath(`C:\pkgs\`~name);
  1789. // see 4d4017c14c, #268, and #1392 for why this all package paths end on slash internally
  1790. path.endsWithSlash = true;
  1791. }
  1792. string name;
  1793. NativePath path;
  1794. BuildSettings getBuildSettings(in BuildPlatform platform, string config) const
  1795. {
  1796. return BuildSettings();
  1797. }
  1798. }
  1799.  
  1800. static struct MockProject
  1801. {
  1802. MockPackage rootPackage;
  1803. inout(MockPackage)[] getTopologicalPackageList() inout
  1804. {
  1805. return _dependencies;
  1806. }
  1807. private:
  1808. MockPackage[] _dependencies;
  1809. }
  1810.  
  1811. MockProject proj = {
  1812. rootPackage: MockPackage("root"),
  1813. _dependencies: [MockPackage("dep1"), MockPackage("dep2")]
  1814. };
  1815. auto pack = MockPackage("test");
  1816. GeneratorSettings gsettings;
  1817. enum isPath = true;
  1818.  
  1819. import std.path : dirSeparator;
  1820.  
  1821. static NativePath woSlash(NativePath p) { p.endsWithSlash = false; return p; }
  1822. // basic vars
  1823. assert(processVars("Hello $PACKAGE_DIR", proj, pack, gsettings, !isPath) == "Hello "~woSlash(pack.path).toNativeString);
  1824. assert(processVars("Hello $ROOT_PACKAGE_DIR", proj, pack, gsettings, !isPath) == "Hello "~woSlash(proj.rootPackage.path).toNativeString.chomp(dirSeparator));
  1825. assert(processVars("Hello $DEP1_PACKAGE_DIR", proj, pack, gsettings, !isPath) == "Hello "~woSlash(proj._dependencies[0].path).toNativeString);
  1826. // ${VAR} replacements
  1827. assert(processVars("Hello ${PACKAGE_DIR}"~dirSeparator~"foobar", proj, pack, gsettings, !isPath) == "Hello "~(pack.path ~ "foobar").toNativeString);
  1828. assert(processVars("Hello $PACKAGE_DIR"~dirSeparator~"foobar", proj, pack, gsettings, !isPath) == "Hello "~(pack.path ~ "foobar").toNativeString);
  1829. // test with isPath
  1830. assert(processVars("local", proj, pack, gsettings, isPath) == (pack.path ~ "local").toNativeString);
  1831. assert(processVars("foo/$$ESCAPED", proj, pack, gsettings, isPath) == (pack.path ~ "foo/$ESCAPED").toNativeString);
  1832. assert(processVars("$$ESCAPED", proj, pack, gsettings, !isPath) == "$ESCAPED");
  1833. // test other env variables
  1834. import std.process : environment;
  1835. environment["MY_ENV_VAR"] = "blablabla";
  1836. assert(processVars("$MY_ENV_VAR", proj, pack, gsettings, !isPath) == "blablabla");
  1837. assert(processVars("${MY_ENV_VAR}suffix", proj, pack, gsettings, !isPath) == "blablablasuffix");
  1838. assert(processVars("$MY_ENV_VAR-suffix", proj, pack, gsettings, !isPath) == "blablabla-suffix");
  1839. assert(processVars("$MY_ENV_VAR:suffix", proj, pack, gsettings, !isPath) == "blablabla:suffix");
  1840. assert(processVars("$MY_ENV_VAR$MY_ENV_VAR", proj, pack, gsettings, !isPath) == "blablablablablabla");
  1841. environment.remove("MY_ENV_VAR");
  1842. }
  1843.  
  1844. /**
  1845. * Holds and stores a set of version selections for package dependencies.
  1846. *
  1847. * This is the runtime representation of the information contained in
  1848. * "dub.selections.json" within a package's directory.
  1849. *
  1850. * Note that as subpackages share the same version as their main package,
  1851. * this class will treat any subpackage reference as a reference to its
  1852. * main package.
  1853. */
  1854. public class SelectedVersions {
  1855. protected {
  1856. enum FileVersion = 1;
  1857. Selections!1 m_selections;
  1858. bool m_dirty = false; // has changes since last save
  1859. bool m_bare = true;
  1860. }
  1861.  
  1862. /// Default file name to use for storing selections.
  1863. enum defaultFile = "dub.selections.json";
  1864.  
  1865. /// Constructs a new empty version selection.
  1866. public this(uint version_ = FileVersion) @safe pure
  1867. {
  1868. enforce(version_ == 1, "Unsupported file version");
  1869. this.m_selections = Selections!1(version_);
  1870. }
  1871.  
  1872. /// Constructs a new non-empty version selection.
  1873. public this(Selections!1 data) @safe pure nothrow @nogc
  1874. {
  1875. this.m_selections = data;
  1876. this.m_bare = false;
  1877. }
  1878.  
  1879. /** Constructs a new non-empty version selection, prefixing relative path
  1880. selections with the specified prefix.
  1881.  
  1882. To be used in cases where the "dub.selections.json" file isn't located
  1883. in the root package directory.
  1884. */
  1885. public this(Selections!1 data, NativePath relPathPrefix)
  1886. {
  1887. this(data);
  1888. if (relPathPrefix.empty) return;
  1889. foreach (ref dep; m_selections.versions.byValue) {
  1890. const depPath = dep.path;
  1891. if (!depPath.empty && !depPath.absolute)
  1892. dep = Dependency(relPathPrefix ~ depPath);
  1893. }
  1894. }
  1895.  
  1896. /** Constructs a new version selection from JSON data.
  1897.  
  1898. The structure of the JSON document must match the contents of the
  1899. "dub.selections.json" file.
  1900. */
  1901. deprecated("Pass a `dub.recipe.selection : Selected` directly")
  1902. this(Json data)
  1903. {
  1904. deserialize(data);
  1905. m_dirty = false;
  1906. }
  1907.  
  1908. /** Constructs a new version selections from an existing JSON file.
  1909. */
  1910. deprecated("JSON deserialization is deprecated")
  1911. this(NativePath path)
  1912. {
  1913. auto json = jsonFromFile(path);
  1914. deserialize(json);
  1915. m_dirty = false;
  1916. m_bare = false;
  1917. }
  1918.  
  1919. /// Returns a list of names for all packages that have a version selection.
  1920. @property string[] selectedPackages() const { return m_selections.versions.keys; }
  1921.  
  1922. /// Determines if any changes have been made after loading the selections from a file.
  1923. @property bool dirty() const { return m_dirty; }
  1924.  
  1925. /// Determine if this set of selections is still empty (but not `clear`ed).
  1926. @property bool bare() const { return m_bare && !m_dirty; }
  1927.  
  1928. /// Removes all selections.
  1929. void clear()
  1930. {
  1931. m_selections.versions = null;
  1932. m_dirty = true;
  1933. }
  1934.  
  1935. /// Duplicates the set of selected versions from another instance.
  1936. void set(SelectedVersions versions)
  1937. {
  1938. m_selections.fileVersion = versions.m_selections.fileVersion;
  1939. m_selections.versions = versions.m_selections.versions.dup;
  1940. m_selections.inheritable = versions.m_selections.inheritable;
  1941. m_dirty = true;
  1942. }
  1943.  
  1944. /// Selects a certain version for a specific package.
  1945. deprecated("Use the overload that accepts a `PackageName`")
  1946. void selectVersion(string package_id, Version version_)
  1947. {
  1948. const name = PackageName(package_id);
  1949. return this.selectVersion(name, version_);
  1950. }
  1951.  
  1952. /// Ditto
  1953. void selectVersion(in PackageName name, Version version_)
  1954. {
  1955. const dep = Dependency(version_);
  1956. this.selectVersionInternal(name, dep);
  1957. }
  1958.  
  1959. /// Selects a certain path for a specific package.
  1960. deprecated("Use the overload that accepts a `PackageName`")
  1961. void selectVersion(string package_id, NativePath path)
  1962. {
  1963. const name = PackageName(package_id);
  1964. return this.selectVersion(name, path);
  1965. }
  1966.  
  1967. /// Ditto
  1968. void selectVersion(in PackageName name, NativePath path)
  1969. {
  1970. const dep = Dependency(path);
  1971. this.selectVersionInternal(name, dep);
  1972. }
  1973.  
  1974. /// Selects a certain Git reference for a specific package.
  1975. deprecated("Use the overload that accepts a `PackageName`")
  1976. void selectVersion(string package_id, Repository repository)
  1977. {
  1978. const name = PackageName(package_id);
  1979. return this.selectVersion(name, repository);
  1980. }
  1981.  
  1982. /// Ditto
  1983. void selectVersion(in PackageName name, Repository repository)
  1984. {
  1985. const dep = Dependency(repository);
  1986. this.selectVersionInternal(name, dep);
  1987. }
  1988.  
  1989. /// Internal implementation of selectVersion
  1990. private void selectVersionInternal(in PackageName name, in Dependency dep)
  1991. {
  1992. if (auto pdep = name.main.toString() in m_selections.versions) {
  1993. if (*pdep == dep)
  1994. return;
  1995. }
  1996. m_selections.versions[name.main.toString()] = dep;
  1997. m_dirty = true;
  1998. }
  1999.  
  2000. deprecated("Move `spec` inside of the `repository` parameter and call `selectVersion`")
  2001. void selectVersionWithRepository(string package_id, Repository repository, string spec)
  2002. {
  2003. this.selectVersion(package_id, Repository(repository.remote(), spec));
  2004. }
  2005.  
  2006. /// Removes the selection for a particular package.
  2007. deprecated("Use the overload that accepts a `PackageName`")
  2008. void deselectVersion(string package_id)
  2009. {
  2010. const n = PackageName(package_id);
  2011. this.deselectVersion(n);
  2012. }
  2013.  
  2014. /// Ditto
  2015. void deselectVersion(in PackageName name)
  2016. {
  2017. m_selections.versions.remove(name.main.toString());
  2018. m_dirty = true;
  2019. }
  2020.  
  2021. /// Determines if a particular package has a selection set.
  2022. deprecated("Use the overload that accepts a `PackageName`")
  2023. bool hasSelectedVersion(string packageId) const {
  2024. const name = PackageName(packageId);
  2025. return this.hasSelectedVersion(name);
  2026. }
  2027.  
  2028. /// Ditto
  2029. bool hasSelectedVersion(in PackageName name) const
  2030. {
  2031. return (name.main.toString() in m_selections.versions) !is null;
  2032. }
  2033.  
  2034. /** Returns the selection for a particular package.
  2035.  
  2036. Note that the returned `Dependency` can either have the
  2037. `Dependency.path` property set to a non-empty value, in which case this
  2038. is a path based selection, or its `Dependency.version_` property is
  2039. valid and it is a version selection.
  2040. */
  2041. deprecated("Use the overload that accepts a `PackageName`")
  2042. Dependency getSelectedVersion(string packageId) const
  2043. {
  2044. const name = PackageName(packageId);
  2045. return this.getSelectedVersion(name);
  2046. }
  2047.  
  2048. /// Ditto
  2049. Dependency getSelectedVersion(in PackageName name) const
  2050. {
  2051. enforce(hasSelectedVersion(name));
  2052. return m_selections.versions[name.main.toString()];
  2053. }
  2054.  
  2055. /** Stores the selections to disk.
  2056.  
  2057. The target file will be written in JSON format. Usually, `defaultFile`
  2058. should be used as the file name and the directory should be the root
  2059. directory of the project's root package.
  2060. */
  2061. deprecated("Use `PackageManager.writeSelections` to write a `SelectionsFile`")
  2062. void save(NativePath path)
  2063. {
  2064. path.writeFile(PackageManager.selectionsToString(this.m_selections));
  2065. m_dirty = false;
  2066. m_bare = false;
  2067. }
  2068.  
  2069. deprecated("Use `dub.dependency : Dependency.toJson(true)`")
  2070. static Json dependencyToJson(Dependency d)
  2071. {
  2072. return d.toJson(true);
  2073. }
  2074.  
  2075. deprecated("JSON deserialization is deprecated")
  2076. static Dependency dependencyFromJson(Json j)
  2077. {
  2078. if (j.type == Json.Type.string)
  2079. return Dependency(Version(j.get!string));
  2080. else if (j.type == Json.Type.object && "path" in j)
  2081. return Dependency(NativePath(j["path"].get!string));
  2082. else if (j.type == Json.Type.object && "repository" in j)
  2083. return Dependency(Repository(j["repository"].get!string,
  2084. enforce("version" in j, "Expected \"version\" field in repository version object").get!string));
  2085. else throw new Exception(format("Unexpected type for dependency: %s", j));
  2086. }
  2087.  
  2088. deprecated("JSON serialization is deprecated")
  2089. Json serialize() const {
  2090. return PackageManager.selectionsToJSON(this.m_selections);
  2091. }
  2092.  
  2093. deprecated("JSON deserialization is deprecated")
  2094. private void deserialize(Json json)
  2095. {
  2096. const fileVersion = json["fileVersion"].get!int;
  2097. enforce(fileVersion == FileVersion, "Mismatched dub.selections.json version: " ~ to!string(fileVersion) ~ " vs. " ~ to!string(FileVersion));
  2098. clear();
  2099. m_selections.fileVersion = fileVersion;
  2100. scope(failure) clear();
  2101. if (auto p = "inheritable" in json)
  2102. m_selections.inheritable = p.get!bool;
  2103. foreach (string p, dep; json["versions"])
  2104. m_selections.versions[p] = dependencyFromJson(dep);
  2105. }
  2106. }
  2107.  
  2108. /// The template code from which the test runner is generated
  2109. private immutable TestRunnerTemplate = q{
  2110. deprecated // allow silently using deprecated symbols
  2111. module dub_test_root;
  2112.  
  2113. import std.typetuple;
  2114.  
  2115. %-(static import %s;
  2116. %);
  2117.  
  2118. alias allModules = TypeTuple!(
  2119. %-(%s, %)
  2120. );
  2121.  
  2122. %s
  2123. };
  2124.  
  2125. /// The default test runner that gets used if none is provided
  2126. private immutable DefaultTestRunnerCode = q{
  2127. version(D_BetterC) {
  2128. extern(C) int main() {
  2129. foreach (module_; allModules) {
  2130. foreach (unitTest; __traits(getUnitTests, module_)) {
  2131. unitTest();
  2132. }
  2133. }
  2134. import core.stdc.stdio : puts;
  2135. puts("All unit tests have been run successfully.");
  2136. return 0;
  2137. }
  2138. } else {
  2139. void main() {
  2140. version (D_Coverage) {
  2141. } else {
  2142. import std.stdio : writeln;
  2143. writeln("All unit tests have been run successfully.");
  2144. }
  2145. }
  2146. shared static this() {
  2147. version (Have_tested) {
  2148. import tested;
  2149. import core.runtime;
  2150. import std.exception;
  2151. Runtime.moduleUnitTester = () => true;
  2152. enforce(runUnitTests!allModules(new ConsoleTestResultWriter), "Unit tests failed.");
  2153. }
  2154. }
  2155. }
  2156. };