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