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