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