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