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