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.internal.utils;
  14. import dub.internal.vibecompat.core.file;
  15. import dub.internal.vibecompat.core.log;
  16. import dub.internal.vibecompat.data.json;
  17. import dub.internal.vibecompat.inet.url;
  18. import dub.package_;
  19. import dub.packagemanager;
  20. import dub.packagesupplier;
  21. import dub.generators.generator;
  22.  
  23. import std.algorithm;
  24. import std.array;
  25. import std.conv : to;
  26. import std.datetime;
  27. import std.exception : enforce;
  28. import std.string;
  29. import std.encoding : sanitize;
  30.  
  31. /**
  32. Represents a full project, a root package with its dependencies and package
  33. selection.
  34.  
  35. All dependencies must be available locally so that the package dependency
  36. graph can be built. Use `Project.reinit` if necessary for reloading
  37. dependencies after more packages are available.
  38. */
  39. class Project {
  40. private {
  41. PackageManager m_packageManager;
  42. Json m_packageSettings;
  43. Package m_rootPackage;
  44. Package[] m_dependencies;
  45. Package[][Package] m_dependees;
  46. SelectedVersions m_selections;
  47. bool m_hasAllDependencies;
  48. string[string] m_overriddenConfigs;
  49. }
  50.  
  51. /** Loads a project.
  52.  
  53. Params:
  54. package_manager = Package manager instance to use for loading
  55. dependencies
  56. project_path = Path of the root package to load
  57. pack = An existing `Package` instance to use as the root package
  58. */
  59. this(PackageManager package_manager, Path project_path)
  60. {
  61. Package pack;
  62. auto packageFile = Package.findPackageFile(project_path);
  63. if (packageFile.empty) {
  64. logWarn("There was no package description found for the application in '%s'.", project_path.toNativeString());
  65. pack = new Package(PackageRecipe.init, project_path);
  66. } else {
  67. pack = package_manager.getOrLoadPackage(project_path, packageFile);
  68. }
  69.  
  70. this(package_manager, pack);
  71. }
  72.  
  73. /// ditto
  74. this(PackageManager package_manager, Package pack)
  75. {
  76. m_packageManager = package_manager;
  77. m_rootPackage = pack;
  78. m_packageSettings = Json.emptyObject;
  79.  
  80. try m_packageSettings = jsonFromFile(m_rootPackage.path ~ ".dub/dub.json", true);
  81. catch(Exception t) logDiagnostic("Failed to read .dub/dub.json: %s", t.msg);
  82.  
  83. auto selverfile = m_rootPackage.path ~ SelectedVersions.defaultFile;
  84. if (existsFile(selverfile)) {
  85. try m_selections = new SelectedVersions(selverfile);
  86. catch(Exception e) {
  87. logWarn("Failed to load %s: %s", SelectedVersions.defaultFile, e.msg);
  88. logDiagnostic("Full error: %s", e.toString().sanitize);
  89. m_selections = new SelectedVersions;
  90. }
  91. } else m_selections = new SelectedVersions;
  92.  
  93. reinit();
  94. }
  95.  
  96. /** List of all resolved dependencies.
  97.  
  98. This includes all direct and indirect dependencies of all configurations
  99. combined. Optional dependencies that were not chosen are not included.
  100. */
  101. @property const(Package[]) dependencies() const { return m_dependencies; }
  102.  
  103. /// The root package of the project.
  104. @property inout(Package) rootPackage() inout { return m_rootPackage; }
  105.  
  106. /// The versions to use for all dependencies. Call reinit() after changing these.
  107. @property inout(SelectedVersions) selections() inout { return m_selections; }
  108.  
  109. /// Package manager instance used by the project.
  110. @property inout(PackageManager) packageManager() inout { return m_packageManager; }
  111.  
  112. /** Determines if all dependencies necessary to build have been collected.
  113.  
  114. If this function returns `false`, it may be necessary to add more entries
  115. to `selections`, or to use `Dub.upgrade` to automatically select all
  116. missing dependencies.
  117. */
  118. bool hasAllDependencies() const { return m_hasAllDependencies; }
  119.  
  120. /** Allows iteration of the dependency tree in topological order
  121. */
  122. int delegate(int delegate(ref Package)) getTopologicalPackageList(bool children_first = false, Package root_package = null, string[string] configs = null)
  123. {
  124. // ugly way to avoid code duplication since inout isn't compatible with foreach type inference
  125. return cast(int delegate(int delegate(ref Package)))(cast(const)this).getTopologicalPackageList(children_first, root_package, configs);
  126. }
  127. /// ditto
  128. int delegate(int delegate(ref const Package)) getTopologicalPackageList(bool children_first = false, in Package root_package = null, string[string] configs = null)
  129. const {
  130. const(Package) rootpack = root_package ? root_package : m_rootPackage;
  131.  
  132. int iterator(int delegate(ref const Package) del)
  133. {
  134. int ret = 0;
  135. bool[const(Package)] visited;
  136. void perform_rec(in Package p){
  137. if( p in visited ) return;
  138. visited[p] = true;
  139.  
  140. if( !children_first ){
  141. ret = del(p);
  142. if( ret ) return;
  143. }
  144.  
  145. auto cfg = configs.get(p.name, null);
  146.  
  147. PackageDependency[] deps;
  148. if (!cfg.length) deps = p.getAllDependencies();
  149. else {
  150. auto depmap = p.getDependencies(cfg);
  151. deps = depmap.byKey.map!(k => PackageDependency(k, depmap[k])).array;
  152. }
  153. deps.sort!((a, b) => a.name < b.name);
  154.  
  155. foreach (d; deps) {
  156. auto dependency = getDependency(d.name, true);
  157. assert(dependency || d.spec.optional,
  158. format("Non-optional dependency %s of %s not found in dependency tree!?.", d.name, p.name));
  159. if(dependency) perform_rec(dependency);
  160. if( ret ) return;
  161. }
  162.  
  163. if( children_first ){
  164. ret = del(p);
  165. if( ret ) return;
  166. }
  167. }
  168. perform_rec(rootpack);
  169. return ret;
  170. }
  171.  
  172. return &iterator;
  173. }
  174.  
  175. /** Retrieves a particular dependency by name.
  176.  
  177. Params:
  178. name = (Qualified) package name of the dependency
  179. is_optional = If set to true, will return `null` for unsatisfiable
  180. dependencies instead of throwing an exception.
  181. */
  182. inout(Package) getDependency(string name, bool is_optional)
  183. inout {
  184. foreach(dp; m_dependencies)
  185. if( dp.name == name )
  186. return dp;
  187. if (!is_optional) throw new Exception("Unknown dependency: "~name);
  188. else return null;
  189. }
  190.  
  191. /** Returns the name of the default build configuration for the specified
  192. target platform.
  193.  
  194. Params:
  195. platform = The target build platform
  196. allow_non_library_configs = If set to true, will use the first
  197. possible configuration instead of the first "executable"
  198. configuration.
  199. */
  200. string getDefaultConfiguration(BuildPlatform platform, bool allow_non_library_configs = true)
  201. const {
  202. auto cfgs = getPackageConfigs(platform, null, allow_non_library_configs);
  203. return cfgs[m_rootPackage.name];
  204. }
  205.  
  206. /** Overrides the configuration chosen for a particular package in the
  207. dependency graph.
  208.  
  209. Setting a certain configuration here is equivalent to removing all
  210. but one configuration from the package.
  211.  
  212. Params:
  213. package_ = The package for which to force selecting a certain
  214. dependency
  215. config = Name of the configuration to force
  216. */
  217. void overrideConfiguration(string package_, string config)
  218. {
  219. auto p = getDependency(package_, true);
  220. enforce(p !is null,
  221. format("Package '%s', marked for configuration override, is not present in dependency graph.", package_));
  222. enforce(p.configurations.canFind(config),
  223. format("Package '%s' does not have a configuration named '%s'.", package_, config));
  224. m_overriddenConfigs[package_] = config;
  225. }
  226.  
  227. /** Performs basic validation of various aspects of the package.
  228.  
  229. This will emit warnings to `stderr` if any discouraged names or
  230. dependency patterns are found.
  231. */
  232. void validate()
  233. {
  234. // some basic package lint
  235. m_rootPackage.warnOnSpecialCompilerFlags();
  236. string nameSuggestion() {
  237. string ret;
  238. ret ~= `Please modify the "name" field in %s accordingly.`.format(m_rootPackage.recipePath.toNativeString());
  239. if (!m_rootPackage.recipe.buildSettings.targetName.length) {
  240. if (m_rootPackage.recipePath.head.toString().endsWith(".sdl")) {
  241. ret ~= ` You can then add 'targetName "%s"' to keep the current executable name.`.format(m_rootPackage.name);
  242. } else {
  243. ret ~= ` You can then add '"targetName": "%s"' to keep the current executable name.`.format(m_rootPackage.name);
  244. }
  245. }
  246. return ret;
  247. }
  248. if (m_rootPackage.name != m_rootPackage.name.toLower()) {
  249. logWarn(`WARNING: DUB package names should always be lower case. %s`, nameSuggestion());
  250. } else if (!m_rootPackage.recipe.name.all!(ch => ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' || ch == '-' || ch == '_')) {
  251. logWarn(`WARNING: DUB package names may only contain alphanumeric characters, `
  252. ~ `as well as '-' and '_'. %s`, nameSuggestion());
  253. }
  254. enforce(!m_rootPackage.name.canFind(' '), "Aborting due to the package name containing spaces.");
  255.  
  256. foreach (d; m_rootPackage.getAllDependencies())
  257. if (d.spec.isExactVersion && d.spec.version_.isBranch) {
  258. logWarn("WARNING: A deprecated branch based version specification is used "
  259. ~ "for the dependency %s. Please use numbered versions instead. Also "
  260. ~ "note that you can still use the %s file to override a certain "
  261. ~ "dependency to use a branch instead.",
  262. d.name, SelectedVersions.defaultFile);
  263. }
  264.  
  265. // search for orphan sub configurations
  266. void warnSubConfig(string pack, string config) {
  267. logWarn("The sub configuration directive \"%s\" -> \"%s\" "
  268. ~ "references a package that is not specified as a dependency "
  269. ~ "and will have no effect.", pack, config);
  270. }
  271. void checkSubConfig(string pack, string config) {
  272. auto p = getDependency(pack, true);
  273. if (p && !p.configurations.canFind(config)) {
  274. logWarn("The sub configuration directive \"%s\" -> \"%s\" "
  275. ~ "references a configuration that does not exist.",
  276. pack, config);
  277. }
  278. }
  279. auto globalbs = m_rootPackage.getBuildSettings();
  280. foreach (p, c; globalbs.subConfigurations) {
  281. if (p !in globalbs.dependencies) warnSubConfig(p, c);
  282. else checkSubConfig(p, c);
  283. }
  284. foreach (c; m_rootPackage.configurations) {
  285. auto bs = m_rootPackage.getBuildSettings(c);
  286. foreach (p, c; bs.subConfigurations) {
  287. if (p !in bs.dependencies && p !in globalbs.dependencies)
  288. warnSubConfig(p, c);
  289. else checkSubConfig(p, c);
  290. }
  291. }
  292.  
  293. // check for version specification mismatches
  294. bool[Package] visited;
  295. void validateDependenciesRec(Package pack) {
  296. // perform basic package linting
  297. pack.simpleLint();
  298.  
  299. foreach (d; pack.getAllDependencies()) {
  300. auto basename = getBasePackageName(d.name);
  301. if (m_selections.hasSelectedVersion(basename)) {
  302. auto selver = m_selections.getSelectedVersion(basename);
  303. if (d.spec.merge(selver) == Dependency.invalid) {
  304. logWarn("Selected package %s %s does not match the dependency specification %s in package %s. Need to \"dub upgrade\"?",
  305. basename, selver, d.spec, pack.name);
  306. }
  307. }
  308.  
  309. auto deppack = getDependency(d.name, true);
  310. if (deppack in visited) continue;
  311. visited[deppack] = true;
  312. if (deppack) validateDependenciesRec(deppack);
  313. }
  314. }
  315. validateDependenciesRec(m_rootPackage);
  316. }
  317.  
  318. /// Reloads dependencies.
  319. void reinit()
  320. {
  321. m_dependencies = null;
  322. m_hasAllDependencies = true;
  323. m_packageManager.refresh(false);
  324.  
  325. void collectDependenciesRec(Package pack, int depth = 0)
  326. {
  327. auto indent = replicate(" ", depth);
  328. logDebug("%sCollecting dependencies for %s", indent, pack.name);
  329. indent ~= " ";
  330.  
  331. foreach (dep; pack.getAllDependencies()) {
  332. Dependency vspec = dep.spec;
  333. Package p;
  334.  
  335. auto basename = getBasePackageName(dep.name);
  336. auto subname = getSubPackageName(dep.name);
  337.  
  338. // non-optional and optional-default dependencies (if no selections file exists)
  339. // need to be satisfied
  340. bool is_desired = !vspec.optional || m_selections.hasSelectedVersion(basename) || (vspec.default_ && m_selections.bare);
  341.  
  342. if (dep.name == m_rootPackage.basePackage.name) {
  343. vspec = Dependency(m_rootPackage.version_);
  344. p = m_rootPackage.basePackage;
  345. } else if (basename == m_rootPackage.basePackage.name) {
  346. vspec = Dependency(m_rootPackage.version_);
  347. try p = m_packageManager.getSubPackage(m_rootPackage.basePackage, subname, false);
  348. catch (Exception e) {
  349. logDiagnostic("%sError getting sub package %s: %s", indent, dep.name, e.msg);
  350. if (is_desired) m_hasAllDependencies = false;
  351. continue;
  352. }
  353. } else if (m_selections.hasSelectedVersion(basename)) {
  354. vspec = m_selections.getSelectedVersion(basename);
  355. if (vspec.path.empty) p = m_packageManager.getBestPackage(dep.name, vspec);
  356. else {
  357. auto path = vspec.path;
  358. if (!path.absolute) path = m_rootPackage.path ~ path;
  359. p = m_packageManager.getOrLoadPackage(path, Path.init, true);
  360. if (subname.length) p = m_packageManager.getSubPackage(p, subname, true);
  361. }
  362. } else if (m_dependencies.canFind!(d => getBasePackageName(d.name) == basename)) {
  363. auto idx = m_dependencies.countUntil!(d => getBasePackageName(d.name) == basename);
  364. auto bp = m_dependencies[idx].basePackage;
  365. vspec = Dependency(bp.path);
  366. if (subname.length) p = m_packageManager.getSubPackage(bp, subname, false);
  367. else p = bp;
  368. } else {
  369. logDiagnostic("%sVersion selection for dependency %s (%s) of %s is missing.",
  370. indent, basename, dep.name, pack.name);
  371. }
  372.  
  373. if (!p && !vspec.path.empty) {
  374. Path path = vspec.path;
  375. if (!path.absolute) path = pack.path ~ path;
  376. logDiagnostic("%sAdding local %s in %s", indent, dep.name, path);
  377. p = m_packageManager.getOrLoadPackage(path, Path.init, true);
  378. if (p.parentPackage !is null) {
  379. logWarn("%sSub package %s must be referenced using the path to it's parent package.", indent, dep.name);
  380. p = p.parentPackage;
  381. }
  382. if (subname.length) p = m_packageManager.getSubPackage(p, subname, false);
  383. enforce(p.name == dep.name,
  384. format("Path based dependency %s is referenced with a wrong name: %s vs. %s",
  385. path.toNativeString(), dep.name, p.name));
  386. }
  387.  
  388. if (!p) {
  389. logDiagnostic("%sMissing dependency %s %s of %s", indent, dep.name, vspec, pack.name);
  390. if (is_desired) m_hasAllDependencies = false;
  391. continue;
  392. }
  393.  
  394. if (!m_dependencies.canFind(p)) {
  395. logDiagnostic("%sFound dependency %s %s", indent, dep.name, vspec.toString());
  396. m_dependencies ~= p;
  397. if (basename == m_rootPackage.basePackage.name)
  398. p.warnOnSpecialCompilerFlags();
  399. collectDependenciesRec(p, depth+1);
  400. }
  401.  
  402. m_dependees[p] ~= pack;
  403. //enforce(p !is null, "Failed to resolve dependency "~dep.name~" "~vspec.toString());
  404. }
  405. }
  406. collectDependenciesRec(m_rootPackage);
  407. }
  408.  
  409. /// Returns the name of the root package.
  410. @property string name() const { return m_rootPackage ? m_rootPackage.name : "app"; }
  411.  
  412. /// Returns the names of all configurations of the root package.
  413. @property string[] configurations() const { return m_rootPackage.configurations; }
  414.  
  415. /// Returns a map with the configuration for all packages in the dependency tree.
  416. string[string] getPackageConfigs(in BuildPlatform platform, string config, bool allow_non_library = true)
  417. const {
  418. struct Vertex { string pack, config; }
  419. struct Edge { size_t from, to; }
  420.  
  421. Vertex[] configs;
  422. Edge[] edges;
  423. string[][string] parents;
  424. parents[m_rootPackage.name] = null;
  425. foreach (p; getTopologicalPackageList())
  426. foreach (d; p.getAllDependencies())
  427. parents[d.name] ~= p.name;
  428.  
  429. size_t createConfig(string pack, string config) {
  430. foreach (i, v; configs)
  431. if (v.pack == pack && v.config == config)
  432. return i;
  433. assert(pack !in m_overriddenConfigs || config == m_overriddenConfigs[pack]);
  434. logDebug("Add config %s %s", pack, config);
  435. configs ~= Vertex(pack, config);
  436. return configs.length-1;
  437. }
  438.  
  439. bool haveConfig(string pack, string config) {
  440. return configs.any!(c => c.pack == pack && c.config == config);
  441. }
  442.  
  443. size_t createEdge(size_t from, size_t to) {
  444. auto idx = edges.countUntil(Edge(from, to));
  445. if (idx >= 0) return idx;
  446. logDebug("Including %s %s -> %s %s", configs[from].pack, configs[from].config, configs[to].pack, configs[to].config);
  447. edges ~= Edge(from, to);
  448. return edges.length-1;
  449. }
  450.  
  451. void removeConfig(size_t i) {
  452. logDebug("Eliminating config %s for %s", configs[i].config, configs[i].pack);
  453. auto had_dep_to_pack = new bool[configs.length];
  454. auto still_has_dep_to_pack = new bool[configs.length];
  455.  
  456. edges = edges.filter!((e) {
  457. if (e.to == i) {
  458. had_dep_to_pack[e.from] = true;
  459. return false;
  460. } else if (configs[e.to].pack == configs[i].pack) {
  461. still_has_dep_to_pack[e.from] = true;
  462. }
  463. if (e.from == i) return false;
  464. return true;
  465. }).array;
  466.  
  467. configs[i] = Vertex.init; // mark config as removed
  468.  
  469. // also remove any configs that cannot be satisfied anymore
  470. foreach (j; 0 .. configs.length)
  471. if (j != i && had_dep_to_pack[j] && !still_has_dep_to_pack[j])
  472. removeConfig(j);
  473. }
  474.  
  475. bool isReachable(string pack, string conf) {
  476. if (pack == configs[0].pack && configs[0].config == conf) return true;
  477. foreach (e; edges)
  478. if (configs[e.to].pack == pack && configs[e.to].config == conf)
  479. return true;
  480. return false;
  481. //return (pack == configs[0].pack && conf == configs[0].config) || edges.canFind!(e => configs[e.to].pack == pack && configs[e.to].config == config);
  482. }
  483.  
  484. bool isReachableByAllParentPacks(size_t cidx) {
  485. bool[string] r;
  486. foreach (p; parents[configs[cidx].pack]) r[p] = false;
  487. foreach (e; edges) {
  488. if (e.to != cidx) continue;
  489. if (auto pp = configs[e.from].pack in r) *pp = true;
  490. }
  491. foreach (bool v; r) if (!v) return false;
  492. return true;
  493. }
  494.  
  495. string[] allconfigs_path;
  496.  
  497. void determineDependencyConfigs(in Package p, string c)
  498. {
  499. string[][string] depconfigs;
  500. foreach (d; p.getAllDependencies()) {
  501. auto dp = getDependency(d.name, true);
  502. if (!dp) continue;
  503.  
  504. string[] cfgs;
  505. if (auto pc = dp.name in m_overriddenConfigs) cfgs = [*pc];
  506. else {
  507. auto subconf = p.getSubConfiguration(c, dp, platform);
  508. if (!subconf.empty) cfgs = [subconf];
  509. else cfgs = dp.getPlatformConfigurations(platform);
  510. }
  511. cfgs = cfgs.filter!(c => haveConfig(d.name, c)).array;
  512.  
  513. // if no valid configuration was found for a dependency, don't include the
  514. // current configuration
  515. if (!cfgs.length) {
  516. logDebug("Skip %s %s (missing configuration for %s)", p.name, c, dp.name);
  517. return;
  518. }
  519. depconfigs[d.name] = cfgs;
  520. }
  521.  
  522. // add this configuration to the graph
  523. size_t cidx = createConfig(p.name, c);
  524. foreach (d; p.getAllDependencies())
  525. foreach (sc; depconfigs.get(d.name, null))
  526. createEdge(cidx, createConfig(d.name, sc));
  527. }
  528.  
  529. // create a graph of all possible package configurations (package, config) -> (subpackage, subconfig)
  530. void determineAllConfigs(in Package p)
  531. {
  532. auto idx = allconfigs_path.countUntil(p.name);
  533. enforce(idx < 0, format("Detected dependency cycle: %s", (allconfigs_path[idx .. $] ~ p.name).join("->")));
  534. allconfigs_path ~= p.name;
  535. scope (exit) allconfigs_path.length--;
  536.  
  537. // first, add all dependency configurations
  538. foreach (d; p.getAllDependencies) {
  539. auto dp = getDependency(d.name, true);
  540. if (!dp) continue;
  541. determineAllConfigs(dp);
  542. }
  543.  
  544. // for each configuration, determine the configurations usable for the dependencies
  545. if (auto pc = p.name in m_overriddenConfigs)
  546. determineDependencyConfigs(p, *pc);
  547. else
  548. foreach (c; p.getPlatformConfigurations(platform, p is m_rootPackage && allow_non_library))
  549. determineDependencyConfigs(p, c);
  550. }
  551. if (config.length) createConfig(m_rootPackage.name, config);
  552. determineAllConfigs(m_rootPackage);
  553.  
  554. // successively remove configurations until only one configuration per package is left
  555. bool changed;
  556. do {
  557. // remove all configs that are not reachable by all parent packages
  558. changed = false;
  559. foreach (i, ref c; configs) {
  560. if (c == Vertex.init) continue; // ignore deleted configurations
  561. if (!isReachableByAllParentPacks(i)) {
  562. logDebug("%s %s NOT REACHABLE by all of (%s):", c.pack, c.config, parents[c.pack]);
  563. removeConfig(i);
  564. changed = true;
  565. }
  566. }
  567.  
  568. // when all edges are cleaned up, pick one package and remove all but one config
  569. if (!changed) {
  570. foreach (p; getTopologicalPackageList()) {
  571. size_t cnt = 0;
  572. foreach (i, ref c; configs)
  573. if (c.pack == p.name && ++cnt > 1) {
  574. logDebug("NON-PRIMARY: %s %s", c.pack, c.config);
  575. removeConfig(i);
  576. }
  577. if (cnt > 1) {
  578. changed = true;
  579. break;
  580. }
  581. }
  582. }
  583. } while (changed);
  584.  
  585. // print out the resulting tree
  586. foreach (e; edges) logDebug(" %s %s -> %s %s", configs[e.from].pack, configs[e.from].config, configs[e.to].pack, configs[e.to].config);
  587.  
  588. // return the resulting configuration set as an AA
  589. string[string] ret;
  590. foreach (c; configs) {
  591. if (c == Vertex.init) continue; // ignore deleted configurations
  592. assert(ret.get(c.pack, c.config) == c.config, format("Conflicting configurations for %s found: %s vs. %s", c.pack, c.config, ret[c.pack]));
  593. logDebug("Using configuration '%s' for %s", c.config, c.pack);
  594. ret[c.pack] = c.config;
  595. }
  596.  
  597. // check for conflicts (packages missing in the final configuration graph)
  598. void checkPacksRec(in Package pack) {
  599. auto pc = pack.name in ret;
  600. enforce(pc !is null, "Could not resolve configuration for package "~pack.name);
  601. foreach (p, dep; pack.getDependencies(*pc)) {
  602. auto deppack = getDependency(p, dep.optional);
  603. if (deppack) checkPacksRec(deppack);
  604. }
  605. }
  606. checkPacksRec(m_rootPackage);
  607.  
  608. return ret;
  609. }
  610.  
  611. /**
  612. * Fills `dst` with values from this project.
  613. *
  614. * `dst` gets initialized according to the given platform and config.
  615. *
  616. * Params:
  617. * dst = The BuildSettings struct to fill with data.
  618. * platform = The platform to retrieve the values for.
  619. * config = Values of the given configuration will be retrieved.
  620. * root_package = If non null, use it instead of the project's real root package.
  621. * shallow = If true, collects only build settings for the main package (including inherited settings) and doesn't stop on target type none and sourceLibrary.
  622. */
  623. void addBuildSettings(ref BuildSettings dst, in BuildPlatform platform, string config, in Package root_package = null, bool shallow = false)
  624. const {
  625. import dub.internal.utils : stripDlangSpecialChars;
  626.  
  627. auto configs = getPackageConfigs(platform, config);
  628.  
  629. foreach (pkg; this.getTopologicalPackageList(false, root_package, configs)) {
  630. auto pkg_path = pkg.path.toNativeString();
  631. dst.addVersions(["Have_" ~ stripDlangSpecialChars(pkg.name)]);
  632.  
  633. assert(pkg.name in configs, "Missing configuration for "~pkg.name);
  634. logDebug("Gathering build settings for %s (%s)", pkg.name, configs[pkg.name]);
  635.  
  636. auto psettings = pkg.getBuildSettings(platform, configs[pkg.name]);
  637. if (psettings.targetType != TargetType.none) {
  638. if (shallow && pkg !is m_rootPackage)
  639. psettings.sourceFiles = null;
  640. processVars(dst, this, pkg, psettings);
  641. if (psettings.importPaths.empty)
  642. 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]);
  643. if (psettings.mainSourceFile.empty && pkg is m_rootPackage && psettings.targetType == TargetType.executable)
  644. 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);
  645. }
  646. if (pkg is m_rootPackage) {
  647. if (!shallow) {
  648. enforce(psettings.targetType != TargetType.none, "Main package has target type \"none\" - stopping build.");
  649. enforce(psettings.targetType != TargetType.sourceLibrary, "Main package has target type \"sourceLibrary\" which generates no target - stopping build.");
  650. }
  651. dst.targetType = psettings.targetType;
  652. dst.targetPath = psettings.targetPath;
  653. dst.targetName = psettings.targetName;
  654. if (!psettings.workingDirectory.empty)
  655. dst.workingDirectory = processVars(psettings.workingDirectory, this, pkg, true);
  656. if (psettings.mainSourceFile.length)
  657. dst.mainSourceFile = processVars(psettings.mainSourceFile, this, pkg, true);
  658. }
  659. }
  660.  
  661. // always add all version identifiers of all packages
  662. foreach (pkg; this.getTopologicalPackageList(false, null, configs)) {
  663. auto psettings = pkg.getBuildSettings(platform, configs[pkg.name]);
  664. dst.addVersions(psettings.versions);
  665. }
  666. }
  667.  
  668. /** Fills `dst` with build settings specific to the given build type.
  669.  
  670. Params:
  671. dst = The `BuildSettings` instance to add the build settings to
  672. platform = Target build platform
  673. build_type = Name of the build type
  674. for_root_package = Selects if the build settings are for the root
  675. package or for one of the dependencies. Unittest flags will
  676. only be added to the root package.
  677. */
  678. void addBuildTypeSettings(ref BuildSettings dst, in BuildPlatform platform, string build_type, bool for_root_package = true)
  679. {
  680. bool usedefflags = !(dst.requirements & BuildRequirement.noDefaultFlags);
  681. if (usedefflags) {
  682. BuildSettings btsettings;
  683. m_rootPackage.addBuildTypeSettings(btsettings, platform, build_type);
  684.  
  685. if (!for_root_package) {
  686. // don't propagate unittest switch to dependencies, as dependent
  687. // unit tests aren't run anyway and the additional code may
  688. // cause linking to fail on Windows (issue #640)
  689. btsettings.removeOptions(BuildOption.unittests);
  690. }
  691.  
  692. processVars(dst, this, m_rootPackage, btsettings);
  693. }
  694. }
  695.  
  696. /// Outputs a build description of the project, including its dependencies.
  697. ProjectDescription describe(GeneratorSettings settings)
  698. {
  699. import dub.generators.targetdescription;
  700.  
  701. // store basic build parameters
  702. ProjectDescription ret;
  703. ret.rootPackage = m_rootPackage.name;
  704. ret.configuration = settings.config;
  705. ret.buildType = settings.buildType;
  706. ret.compiler = settings.platform.compiler;
  707. ret.architecture = settings.platform.architecture;
  708. ret.platform = settings.platform.platform;
  709.  
  710. // collect high level information about projects (useful for IDE display)
  711. auto configs = getPackageConfigs(settings.platform, settings.config);
  712. ret.packages ~= m_rootPackage.describe(settings.platform, settings.config);
  713. foreach (dep; m_dependencies)
  714. ret.packages ~= dep.describe(settings.platform, configs[dep.name]);
  715.  
  716. foreach (p; getTopologicalPackageList(false, null, configs))
  717. ret.packages[ret.packages.countUntil!(pp => pp.name == p.name)].active = true;
  718.  
  719. if (settings.buildType.length) {
  720. // collect build target information (useful for build tools)
  721. auto gen = new TargetDescriptionGenerator(this);
  722. try {
  723. gen.generate(settings);
  724. ret.targets = gen.targetDescriptions;
  725. ret.targetLookup = gen.targetDescriptionLookup;
  726. } catch (Exception e) {
  727. logDiagnostic("Skipping targets description: %s", e.msg);
  728. logDebug("Full error: %s", e.toString().sanitize);
  729. }
  730. }
  731.  
  732. return ret;
  733. }
  734.  
  735. private string[] listBuildSetting(string attributeName)(BuildPlatform platform,
  736. string config, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping)
  737. {
  738. return listBuildSetting!attributeName(platform, getPackageConfigs(platform, config),
  739. projectDescription, compiler, disableEscaping);
  740. }
  741.  
  742. private string[] listBuildSetting(string attributeName)(BuildPlatform platform,
  743. string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping)
  744. {
  745. if (compiler)
  746. return formatBuildSettingCompiler!attributeName(platform, configs, projectDescription, compiler, disableEscaping);
  747. else
  748. return formatBuildSettingPlain!attributeName(platform, configs, projectDescription);
  749. }
  750.  
  751. // Output a build setting formatted for a compiler
  752. private string[] formatBuildSettingCompiler(string attributeName)(BuildPlatform platform,
  753. string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping)
  754. {
  755. import std.process : escapeShellFileName;
  756. import std.path : dirSeparator;
  757.  
  758. assert(compiler);
  759.  
  760. auto targetDescription = projectDescription.lookupTarget(projectDescription.rootPackage);
  761. auto buildSettings = targetDescription.buildSettings;
  762.  
  763. string[] values;
  764. switch (attributeName)
  765. {
  766. case "dflags":
  767. case "linkerFiles":
  768. case "mainSourceFile":
  769. case "importFiles":
  770. values = formatBuildSettingPlain!attributeName(platform, configs, projectDescription);
  771. break;
  772.  
  773. case "lflags":
  774. case "sourceFiles":
  775. case "versions":
  776. case "debugVersions":
  777. case "importPaths":
  778. case "stringImportPaths":
  779. case "options":
  780. auto bs = buildSettings.dup;
  781. bs.dflags = null;
  782.  
  783. // Ensure trailing slash on directory paths
  784. auto ensureTrailingSlash = (string path) => path.endsWith(dirSeparator) ? path : path ~ dirSeparator;
  785. static if (attributeName == "importPaths")
  786. bs.importPaths = bs.importPaths.map!(ensureTrailingSlash).array();
  787. else static if (attributeName == "stringImportPaths")
  788. bs.stringImportPaths = bs.stringImportPaths.map!(ensureTrailingSlash).array();
  789.  
  790. compiler.prepareBuildSettings(bs, BuildSetting.all & ~to!BuildSetting(attributeName));
  791. values = bs.dflags;
  792. break;
  793.  
  794. case "libs":
  795. auto bs = buildSettings.dup;
  796. bs.dflags = null;
  797. bs.lflags = null;
  798. bs.sourceFiles = null;
  799. bs.targetType = TargetType.none; // Force Compiler to NOT omit dependency libs when package is a library.
  800.  
  801. compiler.prepareBuildSettings(bs, BuildSetting.all & ~to!BuildSetting(attributeName));
  802.  
  803. if (bs.lflags)
  804. values = compiler.lflagsToDFlags( bs.lflags );
  805. else if (bs.sourceFiles)
  806. values = compiler.lflagsToDFlags( bs.sourceFiles );
  807. else
  808. values = bs.dflags;
  809.  
  810. break;
  811.  
  812. default: assert(0);
  813. }
  814.  
  815. // Escape filenames and paths
  816. if(!disableEscaping)
  817. {
  818. switch (attributeName)
  819. {
  820. case "mainSourceFile":
  821. case "linkerFiles":
  822. case "copyFiles":
  823. case "importFiles":
  824. case "stringImportFiles":
  825. case "sourceFiles":
  826. case "importPaths":
  827. case "stringImportPaths":
  828. return values.map!(escapeShellFileName).array();
  829.  
  830. default:
  831. return values;
  832. }
  833. }
  834.  
  835. return values;
  836. }
  837.  
  838. // Output a build setting without formatting for any particular compiler
  839. private string[] formatBuildSettingPlain(string attributeName)(BuildPlatform platform, string[string] configs, ProjectDescription projectDescription)
  840. {
  841. import std.path : buildNormalizedPath, dirSeparator;
  842. import std.range : only;
  843.  
  844. string[] list;
  845.  
  846. enforce(attributeName == "targetType" || projectDescription.lookupRootPackage().targetType != TargetType.none,
  847. "Target type is 'none'. Cannot list build settings.");
  848.  
  849. static if (attributeName == "targetType")
  850. if (projectDescription.rootPackage !in projectDescription.targetLookup)
  851. return ["none"];
  852.  
  853. auto targetDescription = projectDescription.lookupTarget(projectDescription.rootPackage);
  854. auto buildSettings = targetDescription.buildSettings;
  855.  
  856. // Return any BuildSetting member attributeName as a range of strings. Don't attempt to fixup values.
  857. // allowEmptyString: When the value is a string (as opposed to string[]),
  858. // is empty string an actual permitted value instead of
  859. // a missing value?
  860. auto getRawBuildSetting(Package pack, bool allowEmptyString) {
  861. auto value = __traits(getMember, buildSettings, attributeName);
  862.  
  863. static if( is(typeof(value) == string[]) )
  864. return value;
  865. else static if( is(typeof(value) == string) )
  866. {
  867. auto ret = only(value);
  868.  
  869. // only() has a different return type from only(value), so we
  870. // have to empty the range rather than just returning only().
  871. if(value.empty && !allowEmptyString) {
  872. ret.popFront();
  873. assert(ret.empty);
  874. }
  875.  
  876. return ret;
  877. }
  878. else static if( is(typeof(value) == enum) )
  879. return only(value);
  880. else static if( is(typeof(value) == BuildRequirements) )
  881. return only(cast(BuildRequirement) cast(int) value.values);
  882. else static if( is(typeof(value) == BuildOptions) )
  883. return only(cast(BuildOption) cast(int) value.values);
  884. else
  885. static assert(false, "Type of BuildSettings."~attributeName~" is unsupported.");
  886. }
  887.  
  888. // Adjust BuildSetting member attributeName as needed.
  889. // Returns a range of strings.
  890. auto getFixedBuildSetting(Package pack) {
  891. // Is relative path(s) to a directory?
  892. enum isRelativeDirectory =
  893. attributeName == "importPaths" || attributeName == "stringImportPaths" ||
  894. attributeName == "targetPath" || attributeName == "workingDirectory";
  895.  
  896. // Is relative path(s) to a file?
  897. enum isRelativeFile =
  898. attributeName == "sourceFiles" || attributeName == "linkerFiles" ||
  899. attributeName == "importFiles" || attributeName == "stringImportFiles" ||
  900. attributeName == "copyFiles" || attributeName == "mainSourceFile";
  901.  
  902. // For these, empty string means "main project directory", not "missing value"
  903. enum allowEmptyString =
  904. attributeName == "targetPath" || attributeName == "workingDirectory";
  905.  
  906. enum isEnumBitfield =
  907. attributeName == "requirements" || attributeName == "options";
  908.  
  909. enum isEnum = attributeName == "targetType";
  910.  
  911. auto values = getRawBuildSetting(pack, allowEmptyString);
  912. string fixRelativePath(string importPath) { return buildNormalizedPath(pack.path.toString(), importPath); }
  913. static string ensureTrailingSlash(string path) { return path.endsWith(dirSeparator) ? path : path ~ dirSeparator; }
  914.  
  915. static if(isRelativeDirectory) {
  916. // Return full paths for the paths, making sure a
  917. // directory separator is on the end of each path.
  918. return values.map!(fixRelativePath).map!(ensureTrailingSlash);
  919. }
  920. else static if(isRelativeFile) {
  921. // Return full paths.
  922. return values.map!(fixRelativePath);
  923. }
  924. else static if(isEnumBitfield)
  925. return bitFieldNames(values.front);
  926. else static if (isEnum)
  927. return [values.front.to!string];
  928. else
  929. return values;
  930. }
  931.  
  932. foreach(value; getFixedBuildSetting(m_rootPackage)) {
  933. list ~= value;
  934. }
  935.  
  936. return list;
  937. }
  938.  
  939. // The "compiler" arg is for choosing which compiler the output should be formatted for,
  940. // or null to imply "list" format.
  941. private string[] listBuildSetting(BuildPlatform platform, string[string] configs,
  942. ProjectDescription projectDescription, string requestedData, Compiler compiler, bool disableEscaping)
  943. {
  944. // Certain data cannot be formatter for a compiler
  945. if (compiler)
  946. {
  947. switch (requestedData)
  948. {
  949. case "target-type":
  950. case "target-path":
  951. case "target-name":
  952. case "working-directory":
  953. case "string-import-files":
  954. case "copy-files":
  955. case "pre-generate-commands":
  956. case "post-generate-commands":
  957. case "pre-build-commands":
  958. case "post-build-commands":
  959. enforce(false, "--data="~requestedData~" can only be used with --data-list or --data-0.");
  960. break;
  961.  
  962. case "requirements":
  963. enforce(false, "--data=requirements can only be used with --data-list or --data-0. Use --data=options instead.");
  964. break;
  965.  
  966. default: break;
  967. }
  968. }
  969.  
  970. import std.typetuple : TypeTuple;
  971. auto args = TypeTuple!(platform, configs, projectDescription, compiler, disableEscaping);
  972. switch (requestedData)
  973. {
  974. case "target-type": return listBuildSetting!"targetType"(args);
  975. case "target-path": return listBuildSetting!"targetPath"(args);
  976. case "target-name": return listBuildSetting!"targetName"(args);
  977. case "working-directory": return listBuildSetting!"workingDirectory"(args);
  978. case "main-source-file": return listBuildSetting!"mainSourceFile"(args);
  979. case "dflags": return listBuildSetting!"dflags"(args);
  980. case "lflags": return listBuildSetting!"lflags"(args);
  981. case "libs": return listBuildSetting!"libs"(args);
  982. case "linker-files": return listBuildSetting!"linkerFiles"(args);
  983. case "source-files": return listBuildSetting!"sourceFiles"(args);
  984. case "copy-files": return listBuildSetting!"copyFiles"(args);
  985. case "versions": return listBuildSetting!"versions"(args);
  986. case "debug-versions": return listBuildSetting!"debugVersions"(args);
  987. case "import-paths": return listBuildSetting!"importPaths"(args);
  988. case "string-import-paths": return listBuildSetting!"stringImportPaths"(args);
  989. case "import-files": return listBuildSetting!"importFiles"(args);
  990. case "string-import-files": return listBuildSetting!"stringImportFiles"(args);
  991. case "pre-generate-commands": return listBuildSetting!"preGenerateCommands"(args);
  992. case "post-generate-commands": return listBuildSetting!"postGenerateCommands"(args);
  993. case "pre-build-commands": return listBuildSetting!"preBuildCommands"(args);
  994. case "post-build-commands": return listBuildSetting!"postBuildCommands"(args);
  995. case "requirements": return listBuildSetting!"requirements"(args);
  996. case "options": return listBuildSetting!"options"(args);
  997.  
  998. default:
  999. enforce(false, "--data="~requestedData~
  1000. " is not a valid option. See 'dub describe --help' for accepted --data= values.");
  1001. }
  1002.  
  1003. assert(0);
  1004. }
  1005.  
  1006. /// Outputs requested data for the project, optionally including its dependencies.
  1007. string[] listBuildSettings(GeneratorSettings settings, string[] requestedData, ListBuildSettingsFormat list_type)
  1008. {
  1009. import dub.compilers.utils : isLinkerFile;
  1010.  
  1011. auto projectDescription = describe(settings);
  1012. auto configs = getPackageConfigs(settings.platform, settings.config);
  1013. PackageDescription packageDescription;
  1014. foreach (pack; projectDescription.packages) {
  1015. if (pack.name == projectDescription.rootPackage)
  1016. packageDescription = pack;
  1017. }
  1018.  
  1019. if (projectDescription.rootPackage in projectDescription.targetLookup) {
  1020. // Copy linker files from sourceFiles to linkerFiles
  1021. auto target = projectDescription.lookupTarget(projectDescription.rootPackage);
  1022. foreach (file; target.buildSettings.sourceFiles.filter!(isLinkerFile))
  1023. target.buildSettings.addLinkerFiles(file);
  1024.  
  1025. // Remove linker files from sourceFiles
  1026. target.buildSettings.sourceFiles =
  1027. target.buildSettings.sourceFiles
  1028. .filter!(a => !isLinkerFile(a))
  1029. .array();
  1030. projectDescription.lookupTarget(projectDescription.rootPackage) = target;
  1031. }
  1032.  
  1033. Compiler compiler;
  1034. bool no_escape;
  1035. final switch (list_type) with (ListBuildSettingsFormat) {
  1036. case list: break;
  1037. case listNul: no_escape = true; break;
  1038. case commandLine: compiler = settings.compiler; break;
  1039. case commandLineNul: compiler = settings.compiler; no_escape = true; break;
  1040.  
  1041. }
  1042.  
  1043. auto result = requestedData
  1044. .map!(dataName => listBuildSetting(settings.platform, configs, projectDescription, dataName, compiler, no_escape));
  1045.  
  1046. final switch (list_type) with (ListBuildSettingsFormat) {
  1047. case list: return result.map!(l => l.join("\n")).array();
  1048. case listNul: return result.map!(l => l.join("\0")).array;
  1049. case commandLine: return result.map!(l => l.join(" ")).array;
  1050. case commandLineNul: return result.map!(l => l.join("\0")).array;
  1051. }
  1052. }
  1053.  
  1054. /** Saves the currently selected dependency versions to disk.
  1055.  
  1056. The selections will be written to a file named
  1057. `SelectedVersions.defaultFile` ("dub.selections.json") within the
  1058. directory of the root package. Any existing file will get overwritten.
  1059. */
  1060. void saveSelections()
  1061. {
  1062. assert(m_selections !is null, "Cannot save selections for non-disk based project (has no selections).");
  1063. if (m_selections.hasSelectedVersion(m_rootPackage.basePackage.name))
  1064. m_selections.deselectVersion(m_rootPackage.basePackage.name);
  1065.  
  1066. auto path = m_rootPackage.path ~ SelectedVersions.defaultFile;
  1067. if (m_selections.dirty || !existsFile(path))
  1068. m_selections.save(path);
  1069. }
  1070.  
  1071. /** Checks if the cached upgrade information is still considered up to date.
  1072.  
  1073. The cache will be considered out of date after 24 hours after the last
  1074. online check.
  1075. */
  1076. bool isUpgradeCacheUpToDate()
  1077. {
  1078. try {
  1079. auto datestr = m_packageSettings["dub"].opt!(Json[string]).get("lastUpgrade", Json("")).get!string;
  1080. if (!datestr.length) return false;
  1081. auto date = SysTime.fromISOExtString(datestr);
  1082. if ((Clock.currTime() - date) > 1.days) return false;
  1083. return true;
  1084. } catch (Exception t) {
  1085. logDebug("Failed to get the last upgrade time: %s", t.msg);
  1086. return false;
  1087. }
  1088. }
  1089.  
  1090. /** Returns the currently cached upgrade information.
  1091.  
  1092. The returned dictionary maps from dependency package name to the latest
  1093. available version that matches the dependency specifications.
  1094. */
  1095. Dependency[string] getUpgradeCache()
  1096. {
  1097. try {
  1098. Dependency[string] ret;
  1099. foreach (string p, d; m_packageSettings["dub"].opt!(Json[string]).get("cachedUpgrades", Json.emptyObject))
  1100. ret[p] = SelectedVersions.dependencyFromJson(d);
  1101. return ret;
  1102. } catch (Exception t) {
  1103. logDebug("Failed to get cached upgrades: %s", t.msg);
  1104. return null;
  1105. }
  1106. }
  1107.  
  1108. /** Sets a new set of versions for the upgrade cache.
  1109. */
  1110. void setUpgradeCache(Dependency[string] versions)
  1111. {
  1112. logDebug("markUpToDate");
  1113. Json create(ref Json json, string object) {
  1114. if (json[object].type == Json.Type.undefined) json[object] = Json.emptyObject;
  1115. return json[object];
  1116. }
  1117. create(m_packageSettings, "dub");
  1118. m_packageSettings["dub"]["lastUpgrade"] = Clock.currTime().toISOExtString();
  1119.  
  1120. create(m_packageSettings["dub"], "cachedUpgrades");
  1121. foreach (p, d; versions)
  1122. m_packageSettings["dub"]["cachedUpgrades"][p] = SelectedVersions.dependencyToJson(d);
  1123.  
  1124. writeDubJson();
  1125. }
  1126.  
  1127. private void writeDubJson() {
  1128. import std.file : exists, mkdir;
  1129. // don't bother to write an empty file
  1130. if( m_packageSettings.length == 0 ) return;
  1131.  
  1132. try {
  1133. logDebug("writeDubJson");
  1134. auto dubpath = m_rootPackage.path~".dub";
  1135. if( !exists(dubpath.toNativeString()) ) mkdir(dubpath.toNativeString());
  1136. auto dstFile = openFile((dubpath~"dub.json").toString(), FileMode.createTrunc);
  1137. scope(exit) dstFile.close();
  1138. dstFile.writePrettyJsonString(m_packageSettings);
  1139. } catch( Exception e ){
  1140. logWarn("Could not write .dub/dub.json.");
  1141. }
  1142. }
  1143. }
  1144.  
  1145.  
  1146. /// Determines the output format used for `Project.listBuildSettings`.
  1147. enum ListBuildSettingsFormat {
  1148. list, /// Newline separated list entries
  1149. listNul, /// NUL character separated list entries (unescaped)
  1150. commandLine, /// Formatted for compiler command line (one data list per line)
  1151. commandLineNul, /// NUL character separated list entries (unescaped, data lists separated by two NUL characters)
  1152. }
  1153.  
  1154.  
  1155. /// Indicates where a package has been or should be placed to.
  1156. enum PlacementLocation {
  1157. /// Packages retrieved with 'local' will be placed in the current folder
  1158. /// using the package name as destination.
  1159. local,
  1160. /// Packages with 'userWide' will be placed in a folder accessible by
  1161. /// all of the applications from the current user.
  1162. user,
  1163. /// Packages retrieved with 'systemWide' will be placed in a shared folder,
  1164. /// which can be accessed by all users of the system.
  1165. system
  1166. }
  1167.  
  1168. void processVars(ref BuildSettings dst, in Project project, in Package pack,
  1169. BuildSettings settings, bool include_target_settings = false)
  1170. {
  1171. dst.addDFlags(processVars(project, pack, settings.dflags));
  1172. dst.addLFlags(processVars(project, pack, settings.lflags));
  1173. dst.addLibs(processVars(project, pack, settings.libs));
  1174. dst.addSourceFiles(processVars(project, pack, settings.sourceFiles, true));
  1175. dst.addImportFiles(processVars(project, pack, settings.importFiles, true));
  1176. dst.addStringImportFiles(processVars(project, pack, settings.stringImportFiles, true));
  1177. dst.addCopyFiles(processVars(project, pack, settings.copyFiles, true));
  1178. dst.addVersions(processVars(project, pack, settings.versions));
  1179. dst.addDebugVersions(processVars(project, pack, settings.debugVersions));
  1180. dst.addImportPaths(processVars(project, pack, settings.importPaths, true));
  1181. dst.addStringImportPaths(processVars(project, pack, settings.stringImportPaths, true));
  1182. dst.addPreGenerateCommands(processVars(project, pack, settings.preGenerateCommands));
  1183. dst.addPostGenerateCommands(processVars(project, pack, settings.postGenerateCommands));
  1184. dst.addPreBuildCommands(processVars(project, pack, settings.preBuildCommands));
  1185. dst.addPostBuildCommands(processVars(project, pack, settings.postBuildCommands));
  1186. dst.addRequirements(settings.requirements);
  1187. dst.addOptions(settings.options);
  1188.  
  1189. if (include_target_settings) {
  1190. dst.targetType = settings.targetType;
  1191. dst.targetPath = processVars(settings.targetPath, project, pack, true);
  1192. dst.targetName = settings.targetName;
  1193. if (!settings.workingDirectory.empty)
  1194. dst.workingDirectory = processVars(settings.workingDirectory, project, pack, true);
  1195. if (settings.mainSourceFile.length)
  1196. dst.mainSourceFile = processVars(settings.mainSourceFile, project, pack, true);
  1197. }
  1198. }
  1199.  
  1200. private string[] processVars(in Project project, in Package pack, string[] vars, bool are_paths = false)
  1201. {
  1202. auto ret = appender!(string[])();
  1203. processVars(ret, project, pack, vars, are_paths);
  1204. return ret.data;
  1205.  
  1206. }
  1207. private void processVars(ref Appender!(string[]) dst, in Project project, in Package pack, string[] vars, bool are_paths = false)
  1208. {
  1209. foreach (var; vars) dst.put(processVars(var, project, pack, are_paths));
  1210. }
  1211.  
  1212. private string processVars(string var, in Project project, in Package pack, bool is_path)
  1213. {
  1214. auto idx = std.string.indexOf(var, '$');
  1215. if (idx >= 0) {
  1216. auto vres = appender!string();
  1217. while (idx >= 0) {
  1218. if (idx+1 >= var.length) break;
  1219. if (var[idx+1] == '$') {
  1220. vres.put(var[0 .. idx+1]);
  1221. var = var[idx+2 .. $];
  1222. } else {
  1223. vres.put(var[0 .. idx]);
  1224. var = var[idx+1 .. $];
  1225.  
  1226. size_t idx2 = 0;
  1227. while( idx2 < var.length && isIdentChar(var[idx2]) ) idx2++;
  1228. auto varname = var[0 .. idx2];
  1229. var = var[idx2 .. $];
  1230.  
  1231. vres.put(getVariable(varname, project, pack));
  1232. }
  1233. idx = std.string.indexOf(var, '$');
  1234. }
  1235. vres.put(var);
  1236. var = vres.data;
  1237. }
  1238. if (is_path) {
  1239. auto p = Path(var);
  1240. if (!p.absolute) {
  1241. return (pack.path ~ p).toNativeString();
  1242. } else return p.toNativeString();
  1243. } else return var;
  1244. }
  1245.  
  1246. private string getVariable(string name, in Project project, in Package pack)
  1247. {
  1248. import std.process : environment;
  1249. if (name == "PACKAGE_DIR") return pack.path.toNativeString();
  1250. if (name == "ROOT_PACKAGE_DIR") return project.rootPackage.path.toNativeString();
  1251.  
  1252. if (name.endsWith("_PACKAGE_DIR")) {
  1253. auto pname = name[0 .. $-12];
  1254. foreach (prj; project.getTopologicalPackageList())
  1255. if (prj.name.toUpper().replace("-", "_") == pname)
  1256. return prj.path.toNativeString();
  1257. }
  1258.  
  1259. auto envvar = environment.get(name);
  1260. if (envvar !is null) return envvar;
  1261.  
  1262. throw new Exception("Invalid variable: "~name);
  1263. }
  1264.  
  1265.  
  1266. /** Holds and stores a set of version selections for package dependencies.
  1267.  
  1268. This is the runtime representation of the information contained in
  1269. "dub.selections.json" within a package's directory.
  1270. */
  1271. final class SelectedVersions {
  1272. private struct Selected {
  1273. Dependency dep;
  1274. //Dependency[string] packages;
  1275. }
  1276. private {
  1277. enum FileVersion = 1;
  1278. Selected[string] m_selections;
  1279. bool m_dirty = false; // has changes since last save
  1280. bool m_bare = true;
  1281. }
  1282.  
  1283. /// Default file name to use for storing selections.
  1284. enum defaultFile = "dub.selections.json";
  1285.  
  1286. /// Constructs a new empty version selection.
  1287. this() {}
  1288.  
  1289. /** Constructs a new version selection from JSON data.
  1290.  
  1291. The structure of the JSON document must match the contents of the
  1292. "dub.selections.json" file.
  1293. */
  1294. this(Json data)
  1295. {
  1296. deserialize(data);
  1297. m_dirty = false;
  1298. }
  1299.  
  1300. /** Constructs a new version selections from an existing JSON file.
  1301. */
  1302. this(Path path)
  1303. {
  1304. auto json = jsonFromFile(path);
  1305. deserialize(json);
  1306. m_dirty = false;
  1307. m_bare = false;
  1308. }
  1309.  
  1310. /// Returns a list of names for all packages that have a version selection.
  1311. @property string[] selectedPackages() const { return m_selections.keys; }
  1312.  
  1313. /// Determines if any changes have been made after loading the selections from a file.
  1314. @property bool dirty() const { return m_dirty; }
  1315.  
  1316. /// Determine if this set of selections is still empty (but not `clear`ed).
  1317. @property bool bare() const { return m_bare && !m_dirty; }
  1318.  
  1319. /// Removes all selections.
  1320. void clear()
  1321. {
  1322. m_selections = null;
  1323. m_dirty = true;
  1324. }
  1325.  
  1326. /// Duplicates the set of selected versions from another instance.
  1327. void set(SelectedVersions versions)
  1328. {
  1329. m_selections = versions.m_selections.dup;
  1330. m_dirty = true;
  1331. }
  1332.  
  1333. /// Selects a certain version for a specific package.
  1334. void selectVersion(string package_id, Version version_)
  1335. {
  1336. if (auto ps = package_id in m_selections) {
  1337. if (ps.dep == Dependency(version_))
  1338. return;
  1339. }
  1340. m_selections[package_id] = Selected(Dependency(version_)/*, issuer*/);
  1341. m_dirty = true;
  1342. }
  1343.  
  1344. /// Selects a certain path for a specific package.
  1345. void selectVersion(string package_id, Path path)
  1346. {
  1347. if (auto ps = package_id in m_selections) {
  1348. if (ps.dep == Dependency(path))
  1349. return;
  1350. }
  1351. m_selections[package_id] = Selected(Dependency(path));
  1352. m_dirty = true;
  1353. }
  1354.  
  1355. /// Removes the selection for a particular package.
  1356. void deselectVersion(string package_id)
  1357. {
  1358. m_selections.remove(package_id);
  1359. m_dirty = true;
  1360. }
  1361.  
  1362. /// Determines if a particular package has a selection set.
  1363. bool hasSelectedVersion(string packageId)
  1364. const {
  1365. return (packageId in m_selections) !is null;
  1366. }
  1367.  
  1368. /** Returns the selection for a particular package.
  1369.  
  1370. Note that the returned `Dependency` can either have the
  1371. `Dependency.path` property set to a non-empty value, in which case this
  1372. is a path based selection, or its `Dependency.version_` property is
  1373. valid and it is a version selection.
  1374. */
  1375. Dependency getSelectedVersion(string packageId)
  1376. const {
  1377. enforce(hasSelectedVersion(packageId));
  1378. return m_selections[packageId].dep;
  1379. }
  1380.  
  1381. /** Stores the selections to disk.
  1382.  
  1383. The target file will be written in JSON format. Usually, `defaultFile`
  1384. should be used as the file name and the directory should be the root
  1385. directory of the project's root package.
  1386. */
  1387. void save(Path path)
  1388. {
  1389. Json json = serialize();
  1390. auto file = openFile(path, FileMode.createTrunc);
  1391. scope(exit) file.close();
  1392.  
  1393. assert(json.type == Json.Type.object);
  1394. assert(json.length == 2);
  1395. assert(json["versions"].type != Json.Type.undefined);
  1396.  
  1397. file.write("{\n\t\"fileVersion\": ");
  1398. file.writeJsonString(json["fileVersion"]);
  1399. file.write(",\n\t\"versions\": {");
  1400. auto vers = json["versions"].get!(Json[string]);
  1401. bool first = true;
  1402. foreach (k; vers.byKey.array.sort()) {
  1403. if (!first) file.write(",");
  1404. else first = false;
  1405. file.write("\n\t\t");
  1406. file.writeJsonString(Json(k));
  1407. file.write(": ");
  1408. file.writeJsonString(vers[k]);
  1409. }
  1410. file.write("\n\t}\n}\n");
  1411. m_dirty = false;
  1412. m_bare = false;
  1413. }
  1414.  
  1415. static Json dependencyToJson(Dependency d)
  1416. {
  1417. if (d.path.empty) return Json(d.version_.toString());
  1418. else return serializeToJson(["path": d.path.toString()]);
  1419. }
  1420.  
  1421. static Dependency dependencyFromJson(Json j)
  1422. {
  1423. if (j.type == Json.Type.string)
  1424. return Dependency(Version(j.get!string));
  1425. else if (j.type == Json.Type.object)
  1426. return Dependency(Path(j["path"].get!string));
  1427. else throw new Exception(format("Unexpected type for dependency: %s", j.type));
  1428. }
  1429.  
  1430. Json serialize()
  1431. const {
  1432. Json json = serializeToJson(m_selections);
  1433. Json serialized = Json.emptyObject;
  1434. serialized["fileVersion"] = FileVersion;
  1435. serialized["versions"] = Json.emptyObject;
  1436. foreach (p, v; m_selections)
  1437. serialized["versions"][p] = dependencyToJson(v.dep);
  1438. return serialized;
  1439. }
  1440.  
  1441. private void deserialize(Json json)
  1442. {
  1443. enforce(cast(int)json["fileVersion"] == FileVersion, "Mismatched dub.select.json version: " ~ to!string(cast(int)json["fileVersion"]) ~ "vs. " ~to!string(FileVersion));
  1444. clear();
  1445. scope(failure) clear();
  1446. foreach (string p, v; json["versions"])
  1447. m_selections[p] = Selected(dependencyFromJson(v));
  1448. }
  1449. }
  1450.