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.generators.generator; 14 import dub.internal.utils; 15 import dub.internal.vibecompat.core.file; 16 import dub.internal.vibecompat.data.json; 17 import dub.internal.vibecompat.inet.path; 18 import dub.internal.logging; 19 import dub.package_; 20 import dub.packagemanager; 21 import dub.recipe.selection; 22 23 import dub.internal.configy.Read; 24 25 import std.algorithm; 26 import std.array; 27 import std.conv : to; 28 import std.datetime; 29 import std.encoding : sanitize; 30 import std.exception : enforce; 31 import std.string; 32 33 /** 34 Represents a full project, a root package with its dependencies and package 35 selection. 36 37 All dependencies must be available locally so that the package dependency 38 graph can be built. Use `Project.reinit` if necessary for reloading 39 dependencies after more packages are available. 40 */ 41 class Project { 42 private { 43 PackageManager m_packageManager; 44 Package m_rootPackage; 45 Package[] m_dependencies; 46 Package[string] m_dependenciesByName; 47 Package[][Package] m_dependees; 48 SelectedVersions m_selections; 49 string[] m_missingDependencies; 50 string[string] m_overriddenConfigs; 51 } 52 53 /** Loads a project. 54 55 Params: 56 package_manager = Package manager instance to use for loading 57 dependencies 58 project_path = Path of the root package to load 59 pack = An existing `Package` instance to use as the root package 60 */ 61 deprecated("Load the package using `PackageManager.getOrLoadPackage` then call the `(PackageManager, Package)` overload") 62 this(PackageManager package_manager, NativePath project_path) 63 { 64 Package pack; 65 auto packageFile = Package.findPackageFile(project_path); 66 if (packageFile.empty) { 67 logWarn("There was no package description found for the application in '%s'.", project_path.toNativeString()); 68 pack = new Package(PackageRecipe.init, project_path); 69 } else { 70 pack = package_manager.getOrLoadPackage(project_path, packageFile, false, StrictMode.Warn); 71 } 72 73 this(package_manager, pack); 74 } 75 76 /// Ditto 77 this(PackageManager package_manager, Package pack) 78 { 79 auto selections = Project.loadSelections(pack.path, package_manager); 80 this(package_manager, pack, selections); 81 } 82 83 /// ditto 84 this(PackageManager package_manager, Package pack, SelectedVersions selections) 85 { 86 m_packageManager = package_manager; 87 m_rootPackage = pack; 88 m_selections = selections; 89 reinit(); 90 } 91 92 /** 93 * Loads a project's `dub.selections.json` and returns it 94 * 95 * This function will load `dub.selections.json` from the path at which 96 * `pack` is located, and returned the resulting `SelectedVersions`. 97 * If no `dub.selections.json` is found, an empty `SelectedVersions` 98 * is returned. 99 * 100 * Params: 101 * packPath = Absolute path of the Package to load the selection file from. 102 * 103 * Returns: 104 * Always a non-null instance. 105 */ 106 static package SelectedVersions loadSelections(in NativePath packPath, PackageManager mgr) 107 { 108 import dub.version_; 109 import dub.internal.dyaml.stdsumtype; 110 111 auto lookupResult = mgr.readSelections(packPath); 112 if (lookupResult.isNull()) // no file, or parsing error (displayed to the user) 113 return new SelectedVersions(); 114 115 auto r = lookupResult.get(); 116 return r.selectionsFile.content.match!( 117 (Selections!0 s) { 118 logWarnTag("Unsupported version", 119 "File %s has fileVersion %s, which is not yet supported by DUB %s.", 120 r.absolutePath, s.fileVersion, dubVersion); 121 logWarn("Ignoring selections file. Use a newer DUB version " ~ 122 "and set the appropriate toolchainRequirements in your recipe file"); 123 return new SelectedVersions(); 124 }, 125 (Selections!1 s) { 126 auto selectionsDir = r.absolutePath.parentPath; 127 return new SelectedVersions(s, selectionsDir.relativeTo(packPath)); 128 }, 129 ); 130 } 131 132 /** List of all resolved dependencies. 133 134 This includes all direct and indirect dependencies of all configurations 135 combined. Optional dependencies that were not chosen are not included. 136 */ 137 @property const(Package[]) dependencies() const { return m_dependencies; } 138 139 /// The root package of the project. 140 @property inout(Package) rootPackage() inout { return m_rootPackage; } 141 142 /// The versions to use for all dependencies. Call reinit() after changing these. 143 @property inout(SelectedVersions) selections() inout { return m_selections; } 144 145 /// Package manager instance used by the project. 146 deprecated("Use `Dub.packageManager` instead") 147 @property inout(PackageManager) packageManager() inout { return m_packageManager; } 148 149 /** Determines if all dependencies necessary to build have been collected. 150 151 If this function returns `false`, it may be necessary to add more entries 152 to `selections`, or to use `Dub.upgrade` to automatically select all 153 missing dependencies. 154 */ 155 bool hasAllDependencies() const { return m_missingDependencies.length == 0; } 156 157 /// Sorted list of missing dependencies. 158 string[] missingDependencies() { return m_missingDependencies; } 159 160 /** Allows iteration of the dependency tree in topological order 161 */ 162 int delegate(int delegate(ref Package)) getTopologicalPackageList(bool children_first = false, Package root_package = null, string[string] configs = null) 163 { 164 // ugly way to avoid code duplication since inout isn't compatible with foreach type inference 165 return cast(int delegate(int delegate(ref Package)))(cast(const)this).getTopologicalPackageList(children_first, root_package, configs); 166 } 167 /// ditto 168 int delegate(int delegate(ref const Package)) getTopologicalPackageList(bool children_first = false, in Package root_package = null, string[string] configs = null) 169 const { 170 const(Package) rootpack = root_package ? root_package : m_rootPackage; 171 172 int iterator(int delegate(ref const Package) del) 173 { 174 int ret = 0; 175 bool[const(Package)] visited; 176 void perform_rec(in Package p){ 177 if( p in visited ) return; 178 visited[p] = true; 179 180 if( !children_first ){ 181 ret = del(p); 182 if( ret ) return; 183 } 184 185 auto cfg = configs.get(p.name, null); 186 187 PackageDependency[] deps; 188 if (!cfg.length) deps = p.getAllDependencies(); 189 else { 190 auto depmap = p.getDependencies(cfg); 191 deps = depmap.byKey.map!(k => PackageDependency(PackageName(k), depmap[k])).array; 192 } 193 deps.sort!((a, b) => a.name.toString() < b.name.toString()); 194 195 foreach (d; deps) { 196 auto dependency = getDependency(d.name.toString(), true); 197 assert(dependency || d.spec.optional, 198 format("Non-optional dependency '%s' of '%s' not found in dependency tree!?.", d.name, p.name)); 199 if(dependency) perform_rec(dependency); 200 if( ret ) return; 201 } 202 203 if( children_first ){ 204 ret = del(p); 205 if( ret ) return; 206 } 207 } 208 perform_rec(rootpack); 209 return ret; 210 } 211 212 return &iterator; 213 } 214 215 /** Retrieves a particular dependency by name. 216 217 Params: 218 name = (Qualified) package name of the dependency 219 is_optional = If set to true, will return `null` for unsatisfiable 220 dependencies instead of throwing an exception. 221 */ 222 inout(Package) getDependency(string name, bool is_optional) 223 inout { 224 if (auto pp = name in m_dependenciesByName) 225 return *pp; 226 if (!is_optional) throw new Exception("Unknown dependency: "~name); 227 else return null; 228 } 229 230 /** Returns the name of the default build configuration for the specified 231 target platform. 232 233 Params: 234 platform = The target build platform 235 allow_non_library_configs = If set to true, will use the first 236 possible configuration instead of the first "executable" 237 configuration. 238 */ 239 string getDefaultConfiguration(in BuildPlatform platform, bool allow_non_library_configs = true) 240 const { 241 auto cfgs = getPackageConfigs(platform, null, allow_non_library_configs); 242 return cfgs[m_rootPackage.name]; 243 } 244 245 /** Overrides the configuration chosen for a particular package in the 246 dependency graph. 247 248 Setting a certain configuration here is equivalent to removing all 249 but one configuration from the package. 250 251 Params: 252 package_ = The package for which to force selecting a certain 253 dependency 254 config = Name of the configuration to force 255 */ 256 void overrideConfiguration(string package_, string config) 257 { 258 auto p = getDependency(package_, true); 259 enforce(p !is null, 260 format("Package '%s', marked for configuration override, is not present in dependency graph.", package_)); 261 enforce(p.configurations.canFind(config), 262 format("Package '%s' does not have a configuration named '%s'.", package_, config)); 263 m_overriddenConfigs[package_] = config; 264 } 265 266 /** Adds a test runner configuration for the root package. 267 268 Params: 269 settings = The generator settings to use 270 generate_main = Whether to generate the main.d file 271 base_config = Optional base configuration 272 custom_main_file = Optional path to file with custom main entry point 273 274 Returns: 275 Name of the added test runner configuration, or null for base configurations with target type `none` 276 */ 277 string addTestRunnerConfiguration(in GeneratorSettings settings, bool generate_main = true, string base_config = "", NativePath custom_main_file = NativePath()) 278 { 279 if (base_config.length == 0) { 280 // if a custom main file was given, favor the first library configuration, so that it can be applied 281 if (!custom_main_file.empty) base_config = getDefaultConfiguration(settings.platform, false); 282 // else look for a "unittest" configuration 283 if (!base_config.length && rootPackage.configurations.canFind("unittest")) base_config = "unittest"; 284 // if not found, fall back to the first "library" configuration 285 if (!base_config.length) base_config = getDefaultConfiguration(settings.platform, false); 286 // if still nothing found, use the first executable configuration 287 if (!base_config.length) base_config = getDefaultConfiguration(settings.platform, true); 288 } 289 290 BuildSettings lbuildsettings = settings.buildSettings.dup; 291 addBuildSettings(lbuildsettings, settings, base_config, null, true); 292 293 if (lbuildsettings.targetType == TargetType.none) { 294 logInfo(`Configuration '%s' has target type "none". Skipping test runner configuration.`, base_config); 295 return null; 296 } 297 298 if (lbuildsettings.targetType == TargetType.executable && base_config == "unittest") { 299 if (!custom_main_file.empty) logWarn("Ignoring custom main file."); 300 return base_config; 301 } 302 303 if (lbuildsettings.sourceFiles.empty) { 304 logInfo(`No source files found in configuration '%s'. Falling back to default configuration for test runner.`, base_config); 305 if (!custom_main_file.empty) logWarn("Ignoring custom main file."); 306 return getDefaultConfiguration(settings.platform); 307 } 308 309 const config = format("%s-test-%s", rootPackage.name.replace(".", "-").replace(":", "-"), base_config); 310 logInfo(`Generating test runner configuration '%s' for '%s' (%s).`, config, base_config, lbuildsettings.targetType); 311 312 BuildSettingsTemplate tcinfo = rootPackage.recipe.getConfiguration(base_config).buildSettings.dup; 313 tcinfo.targetType = TargetType.executable; 314 315 // set targetName unless specified explicitly in unittest base configuration 316 if (tcinfo.targetName.empty || base_config != "unittest") 317 tcinfo.targetName = config; 318 319 auto mainfil = tcinfo.mainSourceFile; 320 if (!mainfil.length) mainfil = rootPackage.recipe.buildSettings.mainSourceFile; 321 322 string custommodname; 323 if (!custom_main_file.empty) { 324 import std.path; 325 tcinfo.sourceFiles[""] ~= custom_main_file.relativeTo(rootPackage.path).toNativeString(); 326 tcinfo.importPaths[""] ~= custom_main_file.parentPath.toNativeString(); 327 custommodname = custom_main_file.head.name.baseName(".d"); 328 } 329 330 // prepare the list of tested modules 331 332 string[] import_modules; 333 if (settings.single) 334 lbuildsettings.importPaths ~= NativePath(mainfil).parentPath.toNativeString; 335 bool firstTimePackage = true; 336 foreach (file; lbuildsettings.sourceFiles) { 337 if (file.endsWith(".d")) { 338 auto fname = NativePath(file).head.name; 339 NativePath msf = NativePath(mainfil); 340 if (msf.absolute) 341 msf = msf.relativeTo(rootPackage.path); 342 if (!settings.single && NativePath(file).relativeTo(rootPackage.path) == msf) { 343 logWarn("Excluding main source file %s from test.", mainfil); 344 tcinfo.excludedSourceFiles[""] ~= mainfil; 345 continue; 346 } 347 if (fname == "package.d") { 348 if (firstTimePackage) { 349 firstTimePackage = false; 350 logDiagnostic("Excluding package.d file from test due to https://issues.dlang.org/show_bug.cgi?id=11847"); 351 } 352 continue; 353 } 354 import_modules ~= dub.internal.utils.determineModuleName(lbuildsettings, NativePath(file), rootPackage.path); 355 } 356 } 357 358 NativePath mainfile; 359 if (settings.tempBuild) 360 mainfile = getTempFile("dub_test_root", ".d"); 361 else { 362 import dub.generators.build : computeBuildName; 363 mainfile = packageCache(settings.cache, this.rootPackage) ~ 364 format("code/%s/dub_test_root.d", 365 computeBuildName(config, settings, import_modules)); 366 } 367 368 auto escapedMainFile = mainfile.toNativeString().replace("$", "$$"); 369 tcinfo.sourceFiles[""] ~= escapedMainFile; 370 tcinfo.mainSourceFile = escapedMainFile; 371 if (!settings.tempBuild) { 372 // add the directory containing dub_test_root.d to the import paths 373 tcinfo.importPaths[""] ~= NativePath(escapedMainFile).parentPath.toNativeString(); 374 } 375 376 if (generate_main && (settings.force || !existsFile(mainfile))) { 377 ensureDirectory(mainfile.parentPath); 378 379 const runnerCode = custommodname.length ? 380 format("import %s;", custommodname) : DefaultTestRunnerCode; 381 const content = TestRunnerTemplate.format( 382 import_modules, import_modules, runnerCode); 383 writeFile(mainfile, content); 384 } 385 386 rootPackage.recipe.configurations ~= ConfigurationInfo(config, tcinfo); 387 388 return config; 389 } 390 391 /** Performs basic validation of various aspects of the package. 392 393 This will emit warnings to `stderr` if any discouraged names or 394 dependency patterns are found. 395 */ 396 void validate() 397 { 398 bool isSDL = !m_rootPackage.recipePath.empty 399 && m_rootPackage.recipePath.head.name.endsWith(".sdl"); 400 401 // some basic package lint 402 m_rootPackage.warnOnSpecialCompilerFlags(); 403 string nameSuggestion() { 404 string ret; 405 ret ~= `Please modify the "name" field in %s accordingly.`.format(m_rootPackage.recipePath.toNativeString()); 406 if (!m_rootPackage.recipe.buildSettings.targetName.length) { 407 if (isSDL) { 408 ret ~= ` You can then add 'targetName "%s"' to keep the current executable name.`.format(m_rootPackage.name); 409 } else { 410 ret ~= ` You can then add '"targetName": "%s"' to keep the current executable name.`.format(m_rootPackage.name); 411 } 412 } 413 return ret; 414 } 415 if (m_rootPackage.name != m_rootPackage.name.toLower()) { 416 logWarn(`DUB package names should always be lower case. %s`, nameSuggestion()); 417 } else if (!m_rootPackage.recipe.name.all!(ch => ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' || ch == '-' || ch == '_')) { 418 logWarn(`DUB package names may only contain alphanumeric characters, ` 419 ~ `as well as '-' and '_'. %s`, nameSuggestion()); 420 } 421 enforce(!m_rootPackage.name.canFind(' '), "Aborting due to the package name containing spaces."); 422 423 foreach (d; m_rootPackage.getAllDependencies()) 424 if (d.spec.isExactVersion && d.spec.version_.isBranch) { 425 string suggestion = isSDL 426 ? format(`dependency "%s" repository="git+<git url>" version="<commit>"`, d.name) 427 : format(`"%s": {"repository": "git+<git url>", "version": "<commit>"}`, d.name); 428 logWarn("Dependency '%s' depends on git branch '%s', which is deprecated.", 429 d.name.toString().color(Mode.bold), 430 d.spec.version_.toString.color(Mode.bold)); 431 logWarnTag("", "Specify the git repository and commit hash in your %s:", 432 (isSDL ? "dub.sdl" : "dub.json").color(Mode.bold)); 433 logWarnTag("", "%s", suggestion.color(Mode.bold)); 434 } 435 436 // search for orphan sub configurations 437 void warnSubConfig(string pack, string config) { 438 logWarn("The sub configuration directive \"%s\" -> [%s] " 439 ~ "references a package that is not specified as a dependency " 440 ~ "and will have no effect.", pack.color(Mode.bold), config.color(Color.blue)); 441 } 442 443 void checkSubConfig(in PackageName name, string config) { 444 auto p = getDependency(name.toString(), true); 445 if (p && !p.configurations.canFind(config)) { 446 logWarn("The sub configuration directive \"%s\" -> [%s] " 447 ~ "references a configuration that does not exist.", 448 name.toString().color(Mode.bold), config.color(Color.red)); 449 } 450 } 451 auto globalbs = m_rootPackage.getBuildSettings(); 452 foreach (p, c; globalbs.subConfigurations) { 453 if (p !in globalbs.dependencies) warnSubConfig(p, c); 454 else checkSubConfig(PackageName(p), c); 455 } 456 foreach (c; m_rootPackage.configurations) { 457 auto bs = m_rootPackage.getBuildSettings(c); 458 foreach (p, subConf; bs.subConfigurations) { 459 if (p !in bs.dependencies && p !in globalbs.dependencies) 460 warnSubConfig(p, subConf); 461 else checkSubConfig(PackageName(p), subConf); 462 } 463 } 464 465 // check for version specification mismatches 466 bool[Package] visited; 467 void validateDependenciesRec(Package pack) { 468 // perform basic package linting 469 pack.simpleLint(); 470 471 foreach (d; pack.getAllDependencies()) { 472 auto basename = d.name.main; 473 d.spec.visit!( 474 (NativePath path) { /* Valid */ }, 475 (Repository repo) { /* Valid */ }, 476 (VersionRange vers) { 477 if (m_selections.hasSelectedVersion(basename)) { 478 auto selver = m_selections.getSelectedVersion(basename); 479 if (d.spec.merge(selver) == Dependency.Invalid) { 480 logWarn(`Selected package %s@%s does not match ` ~ 481 `the dependency specification %s in ` ~ 482 `package %s. Need to "%s"?`, 483 basename.toString().color(Mode.bold), selver, 484 vers, pack.name.color(Mode.bold), 485 "dub upgrade".color(Mode.bold)); 486 } 487 } 488 }, 489 ); 490 491 auto deppack = getDependency(d.name.toString(), true); 492 if (deppack in visited) continue; 493 visited[deppack] = true; 494 if (deppack) validateDependenciesRec(deppack); 495 } 496 } 497 validateDependenciesRec(m_rootPackage); 498 } 499 500 /** 501 * Reloads dependencies 502 * 503 * This function goes through the project and make sure that all 504 * required packages are loaded. To do so, it uses information 505 * both from the recipe file (`dub.json`) and from the selections 506 * file (`dub.selections.json`). 507 * 508 * In the process, it populates the `dependencies`, `missingDependencies`, 509 * and `hasAllDependencies` properties, which can only be relied on 510 * once this has run once (the constructor always calls this). 511 */ 512 void reinit() 513 { 514 m_dependencies = null; 515 m_dependenciesByName = null; 516 m_missingDependencies = []; 517 collectDependenciesRec(m_rootPackage); 518 foreach (p; m_dependencies) m_dependenciesByName[p.name] = p; 519 m_missingDependencies.sort(); 520 } 521 522 /// Implementation of `reinit` 523 private void collectDependenciesRec(Package pack, int depth = 0) 524 { 525 auto indent = replicate(" ", depth); 526 logDebug("%sCollecting dependencies for %s", indent, pack.name); 527 indent ~= " "; 528 529 foreach (dep; pack.getAllDependencies()) { 530 Dependency vspec = dep.spec; 531 Package p; 532 533 auto basename = dep.name.main; 534 auto subname = dep.name.sub; 535 536 // non-optional and optional-default dependencies (if no selections file exists) 537 // need to be satisfied 538 bool is_desired = !vspec.optional || m_selections.hasSelectedVersion(basename) || (vspec.default_ && m_selections.bare); 539 540 if (dep.name.toString() == m_rootPackage.basePackage.name) { 541 vspec = Dependency(m_rootPackage.version_); 542 p = m_rootPackage.basePackage; 543 } else if (basename.toString() == m_rootPackage.basePackage.name) { 544 vspec = Dependency(m_rootPackage.version_); 545 try p = m_packageManager.getSubPackage(m_rootPackage.basePackage, subname, false); 546 catch (Exception e) { 547 logDiagnostic("%sError getting sub package %s: %s", indent, dep.name, e.msg); 548 if (is_desired) m_missingDependencies ~= dep.name.toString(); 549 continue; 550 } 551 } else if (m_selections.hasSelectedVersion(basename)) { 552 vspec = m_selections.getSelectedVersion(basename); 553 p = vspec.visit!( 554 (NativePath path_) { 555 auto path = path_.absolute ? path_ : m_rootPackage.path ~ path_; 556 auto tmp = m_packageManager.getOrLoadPackage(path, NativePath.init, true); 557 return resolveSubPackage(tmp, subname, true); 558 }, 559 (Repository repo) { 560 auto tmp = m_packageManager.loadSCMPackage(basename, repo); 561 return resolveSubPackage(tmp, subname, true); 562 }, 563 (VersionRange range) { 564 // See `dub.recipe.selection : SelectedDependency.fromYAML` 565 assert(range.isExactVersion()); 566 return m_packageManager.getPackage(dep.name, vspec.version_); 567 }, 568 ); 569 } else if (m_dependencies.canFind!(d => PackageName(d.name).main == basename)) { 570 auto idx = m_dependencies.countUntil!(d => PackageName(d.name).main == basename); 571 auto bp = m_dependencies[idx].basePackage; 572 vspec = Dependency(bp.path); 573 p = resolveSubPackage(bp, subname, false); 574 } else { 575 logDiagnostic("%sVersion selection for dependency %s (%s) of %s is missing.", 576 indent, basename, dep.name, pack.name); 577 } 578 579 // We didn't find the package 580 if (p is null) 581 { 582 if (!vspec.repository.empty) { 583 p = m_packageManager.loadSCMPackage(basename, vspec.repository); 584 resolveSubPackage(p, subname, false); 585 enforce(p !is null, 586 "Unable to fetch '%s@%s' using git - does the repository and version exists?".format( 587 dep.name, vspec.repository)); 588 } else if (!vspec.path.empty && is_desired) { 589 NativePath path = vspec.path; 590 if (!path.absolute) path = pack.path ~ path; 591 logDiagnostic("%sAdding local %s in %s", indent, dep.name, path); 592 p = m_packageManager.getOrLoadPackage(path, NativePath.init, true); 593 if (p.parentPackage !is null) { 594 logWarn("%sSub package %s must be referenced using the path to it's parent package.", indent, dep.name); 595 p = p.parentPackage; 596 } 597 p = resolveSubPackage(p, subname, false); 598 enforce(p.name == dep.name.toString(), 599 format("Path based dependency %s is referenced with a wrong name: %s vs. %s", 600 path.toNativeString(), dep.name, p.name)); 601 } else { 602 logDiagnostic("%sMissing dependency %s %s of %s", indent, dep.name, vspec, pack.name); 603 if (is_desired) m_missingDependencies ~= dep.name.toString(); 604 continue; 605 } 606 } 607 608 if (!m_dependencies.canFind(p)) { 609 logDiagnostic("%sFound dependency %s %s", indent, dep.name, vspec.toString()); 610 m_dependencies ~= p; 611 if (basename.toString() == m_rootPackage.basePackage.name) 612 p.warnOnSpecialCompilerFlags(); 613 collectDependenciesRec(p, depth+1); 614 } 615 616 m_dependees[p] ~= pack; 617 //enforce(p !is null, "Failed to resolve dependency "~dep.name~" "~vspec.toString()); 618 } 619 } 620 621 /// Convenience function used by `reinit` 622 private Package resolveSubPackage(Package p, string subname, bool silentFail) { 623 if (!subname.length || p is null) 624 return p; 625 return m_packageManager.getSubPackage(p, subname, silentFail); 626 } 627 628 /// Returns the name of the root package. 629 @property string name() const { return m_rootPackage ? m_rootPackage.name : "app"; } 630 631 /// Returns the names of all configurations of the root package. 632 @property string[] configurations() const { return m_rootPackage.configurations; } 633 634 /// Returns the names of all built-in and custom build types of the root package. 635 /// The default built-in build type is the first item in the list. 636 @property string[] builds() const { return builtinBuildTypes ~ m_rootPackage.customBuildTypes; } 637 638 /// Returns a map with the configuration for all packages in the dependency tree. 639 string[string] getPackageConfigs(in BuildPlatform platform, string config, bool allow_non_library = true) 640 const { 641 import std.typecons : Rebindable, rebindable; 642 import std.range : only; 643 644 // prepare by collecting information about all packages in the project 645 // qualified names and dependencies are cached, to avoid recomputing 646 // them multiple times during the algorithm 647 auto packages = collectPackageInformation(); 648 649 // graph of the project's package configuration dependencies 650 // (package, config) -> (sub-package, sub-config) 651 static struct Vertex { size_t pack = size_t.max; string config; } 652 static struct Edge { size_t from, to; } 653 Vertex[] configs; 654 void[0][Vertex] configs_set; 655 Edge[] edges; 656 657 658 size_t createConfig(size_t pack_idx, string config) { 659 foreach (i, v; configs) 660 if (v.pack == pack_idx && v.config == config) 661 return i; 662 663 auto pname = packages[pack_idx].name; 664 assert(pname !in m_overriddenConfigs || config == m_overriddenConfigs[pname]); 665 logDebug("Add config %s %s", pname, config); 666 auto cfg = Vertex(pack_idx, config); 667 configs ~= cfg; 668 configs_set[cfg] = (void[0]).init; 669 return configs.length-1; 670 } 671 672 bool haveConfig(size_t pack_idx, string config) { 673 return (Vertex(pack_idx, config) in configs_set) !is null; 674 } 675 676 void removeConfig(size_t config_index) { 677 logDebug("Eliminating config %s for %s", configs[config_index].config, configs[config_index].pack); 678 auto had_dep_to_pack = new bool[configs.length]; 679 auto still_has_dep_to_pack = new bool[configs.length]; 680 681 // eliminate all edges that connect to config 'config_index' and 682 // track all connected configs 683 edges = edges.filterInPlace!((e) { 684 if (e.to == config_index) { 685 had_dep_to_pack[e.from] = true; 686 return false; 687 } else if (configs[e.to].pack == configs[config_index].pack) { 688 still_has_dep_to_pack[e.from] = true; 689 } 690 691 return e.from != config_index; 692 }); 693 694 // mark config as removed 695 configs_set.remove(configs[config_index]); 696 configs[config_index] = Vertex.init; 697 698 // also remove any configs that cannot be satisfied anymore 699 foreach (j; 0 .. configs.length) 700 if (j != config_index && had_dep_to_pack[j] && !still_has_dep_to_pack[j]) 701 removeConfig(j); 702 } 703 704 bool[] reachable = new bool[packages.length]; // reused to avoid continuous re-allocation 705 bool isReachableByAllParentPacks(size_t cidx) { 706 foreach (p; packages[configs[cidx].pack].parents) reachable[p] = false; 707 foreach (e; edges) { 708 if (e.to != cidx) continue; 709 reachable[configs[e.from].pack] = true; 710 } 711 foreach (p; packages[configs[cidx].pack].parents) 712 if (!reachable[p]) 713 return false; 714 return true; 715 } 716 717 string[][] depconfigs = new string[][](packages.length); 718 void determineDependencyConfigs(size_t pack_idx, string c) 719 { 720 void[0][Edge] edges_set; 721 void createEdge(size_t from, size_t to) { 722 if (Edge(from, to) in edges_set) 723 return; 724 logDebug("Including %s %s -> %s %s", configs[from].pack, configs[from].config, configs[to].pack, configs[to].config); 725 edges ~= Edge(from, to); 726 edges_set[Edge(from, to)] = (void[0]).init; 727 } 728 729 auto pack = &packages[pack_idx]; 730 731 // below we call createConfig for the main package if 732 // config.length is not zero. Carry on for that case, 733 // otherwise we've handle the pair (p, c) already 734 if(haveConfig(pack_idx, c) && !(config.length && pack.name == m_rootPackage.name && config == c)) 735 return; 736 737 foreach (d; pack.dependencies) { 738 auto dp = packages.getPackageIndex(d.name.toString()); 739 if (dp == size_t.max) continue; 740 741 depconfigs[dp].length = 0; 742 depconfigs[dp].assumeSafeAppend; 743 744 void setConfigs(R)(R configs) { 745 configs 746 .filter!(c => haveConfig(dp, c)) 747 .each!((c) { depconfigs[dp] ~= c; }); 748 } 749 if (auto pc = packages[dp].name in m_overriddenConfigs) { 750 setConfigs(only(*pc)); 751 } else { 752 auto subconf = pack.package_.getSubConfiguration(c, packages[dp].package_, platform); 753 if (!subconf.empty) setConfigs(only(subconf)); 754 else setConfigs(packages[dp].package_.getPlatformConfigurations(platform)); 755 } 756 757 // if no valid configuration was found for a dependency, don't include the 758 // current configuration 759 if (!depconfigs[dp].length) { 760 logDebug("Skip %s %s (missing configuration for %s)", pack.name, c, packages[dp].name); 761 return; 762 } 763 } 764 765 // add this configuration to the graph 766 size_t cidx = createConfig(pack_idx, c); 767 foreach (d; pack.dependencies) { 768 if (auto pdp = d.name.toString() in packages) 769 foreach (sc; depconfigs[*pdp]) 770 createEdge(cidx, createConfig(*pdp, sc)); 771 } 772 } 773 774 string[] allconfigs_path; 775 void determineAllConfigs(size_t pack_idx) 776 { 777 auto pack = &packages[pack_idx]; 778 779 auto idx = allconfigs_path.countUntil(pack.name); 780 enforce(idx < 0, format("Detected dependency cycle: %s", (allconfigs_path[idx .. $] ~ pack.name).join("->"))); 781 allconfigs_path ~= pack.name; 782 scope (exit) { 783 allconfigs_path.length--; 784 allconfigs_path.assumeSafeAppend; 785 } 786 787 // first, add all dependency configurations 788 foreach (d; pack.dependencies) 789 if (auto pi = d.name.toString() in packages) 790 determineAllConfigs(*pi); 791 792 // for each configuration, determine the configurations usable for the dependencies 793 if (auto pc = pack.name in m_overriddenConfigs) 794 determineDependencyConfigs(pack_idx, *pc); 795 else 796 foreach (c; pack.package_.getPlatformConfigurations(platform, pack.package_ is m_rootPackage && allow_non_library)) 797 determineDependencyConfigs(pack_idx, c); 798 } 799 800 801 // first, create a graph of all possible package configurations 802 assert(packages[0].package_ is m_rootPackage); 803 if (config.length) createConfig(0, config); 804 determineAllConfigs(0); 805 806 // then, successively remove configurations until only one configuration 807 // per package is left 808 bool changed; 809 do { 810 // remove all configs that are not reachable by all parent packages 811 changed = false; 812 foreach (i, ref c; configs) { 813 if (c == Vertex.init) continue; // ignore deleted configurations 814 if (!isReachableByAllParentPacks(i)) { 815 logDebug("%s %s NOT REACHABLE by all of (%s):", c.pack, c.config, packages[c.pack].parents); 816 removeConfig(i); 817 changed = true; 818 } 819 } 820 821 // when all edges are cleaned up, pick one package and remove all but one config 822 if (!changed) { 823 foreach (pidx; 0 .. packages.length) { 824 size_t cnt = 0; 825 foreach (i, ref c; configs) 826 if (c.pack == pidx && ++cnt > 1) { 827 logDebug("NON-PRIMARY: %s %s", c.pack, c.config); 828 removeConfig(i); 829 } 830 if (cnt > 1) { 831 changed = true; 832 break; 833 } 834 } 835 } 836 } while (changed); 837 838 // print out the resulting tree 839 foreach (e; edges) logDebug(" %s %s -> %s %s", configs[e.from].pack, configs[e.from].config, configs[e.to].pack, configs[e.to].config); 840 841 // return the resulting configuration set as an AA 842 string[string] ret; 843 foreach (c; configs) { 844 if (c == Vertex.init) continue; // ignore deleted configurations 845 auto pname = packages[c.pack].name; 846 assert(ret.get(pname, c.config) == c.config, format("Conflicting configurations for %s found: %s vs. %s", pname, c.config, ret[pname])); 847 logDebug("Using configuration '%s' for %s", c.config, pname); 848 ret[pname] = c.config; 849 } 850 851 // check for conflicts (packages missing in the final configuration graph) 852 auto visited = new bool[](packages.length); 853 void checkPacksRec(size_t pack_idx) { 854 if (visited[pack_idx]) return; 855 visited[pack_idx] = true; 856 auto pname = packages[pack_idx].name; 857 auto pc = pname in ret; 858 enforce(pc !is null, "Could not resolve configuration for package "~pname); 859 foreach (p, dep; packages[pack_idx].package_.getDependencies(*pc)) { 860 auto deppack = getDependency(p, dep.optional); 861 if (deppack) checkPacksRec(packages[].countUntil!(p => p.package_ is deppack)); 862 } 863 } 864 checkPacksRec(0); 865 866 return ret; 867 } 868 869 /** Returns an ordered list of all packages with the additional possibility 870 to look up by name. 871 */ 872 private auto collectPackageInformation() 873 const { 874 static struct PackageInfo { 875 const(Package) package_; 876 size_t[] parents; 877 string name; 878 PackageDependency[] dependencies; 879 } 880 881 static struct PackageInfoAccessor { 882 private { 883 PackageInfo[] m_packages; 884 size_t[string] m_packageMap; 885 } 886 887 private void initialize(P)(P all_packages, size_t reserve_count) 888 { 889 m_packages.reserve(reserve_count); 890 foreach (p; all_packages) { 891 auto pname = p.name; 892 m_packageMap[pname] = m_packages.length; 893 m_packages ~= PackageInfo(p, null, pname, p.getAllDependencies()); 894 } 895 foreach (pack_idx, ref pack_info; m_packages) 896 foreach (d; pack_info.dependencies) 897 if (auto pi = d.name.toString() in m_packageMap) 898 m_packages[*pi].parents ~= pack_idx; 899 } 900 901 size_t length() const { return m_packages.length; } 902 const(PackageInfo)[] opIndex() const { return m_packages; } 903 ref const(PackageInfo) opIndex(size_t package_index) const { return m_packages[package_index]; } 904 size_t getPackageIndex(string package_name) const { return m_packageMap.get(package_name, size_t.max); } 905 const(size_t)* opBinaryRight(string op = "in")(string package_name) const { return package_name in m_packageMap; } 906 } 907 908 PackageInfoAccessor ret; 909 ret.initialize(getTopologicalPackageList(), m_dependencies.length); 910 return ret; 911 } 912 913 /** 914 * Fills `dst` with values from this project. 915 * 916 * `dst` gets initialized according to the given platform and config. 917 * 918 * Params: 919 * dst = The BuildSettings struct to fill with data. 920 * gsettings = The generator settings to retrieve the values for. 921 * config = Values of the given configuration will be retrieved. 922 * root_package = If non null, use it instead of the project's real root package. 923 * shallow = If true, collects only build settings for the main package (including inherited settings) and doesn't stop on target type none and sourceLibrary. 924 */ 925 void addBuildSettings(ref BuildSettings dst, in GeneratorSettings gsettings, string config, in Package root_package = null, bool shallow = false) 926 const { 927 import dub.internal.utils : stripDlangSpecialChars; 928 929 auto configs = getPackageConfigs(gsettings.platform, config); 930 931 foreach (pkg; this.getTopologicalPackageList(false, root_package, configs)) { 932 auto pkg_path = pkg.path.toNativeString(); 933 dst.addVersions(["Have_" ~ stripDlangSpecialChars(pkg.name)]); 934 935 assert(pkg.name in configs, "Missing configuration for "~pkg.name); 936 logDebug("Gathering build settings for %s (%s)", pkg.name, configs[pkg.name]); 937 938 auto psettings = pkg.getBuildSettings(gsettings.platform, configs[pkg.name]); 939 if (psettings.targetType != TargetType.none) { 940 if (shallow && pkg !is m_rootPackage) 941 psettings.sourceFiles = null; 942 processVars(dst, this, pkg, psettings, gsettings); 943 if (!gsettings.single && psettings.importPaths.empty) 944 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]); 945 if (psettings.mainSourceFile.empty && pkg is m_rootPackage && psettings.targetType == TargetType.executable) 946 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); 947 } 948 if (pkg is m_rootPackage) { 949 if (!shallow) { 950 enforce(psettings.targetType != TargetType.none, "Main package has target type \"none\" - stopping build."); 951 enforce(psettings.targetType != TargetType.sourceLibrary, "Main package has target type \"sourceLibrary\" which generates no target - stopping build."); 952 } 953 dst.targetType = psettings.targetType; 954 dst.targetPath = psettings.targetPath; 955 dst.targetName = psettings.targetName; 956 if (!psettings.workingDirectory.empty) 957 dst.workingDirectory = processVars(psettings.workingDirectory, this, pkg, gsettings, true, [dst.environments, dst.buildEnvironments]); 958 if (psettings.mainSourceFile.length) 959 dst.mainSourceFile = processVars(psettings.mainSourceFile, this, pkg, gsettings, true, [dst.environments, dst.buildEnvironments]); 960 } 961 } 962 963 // always add all version identifiers of all packages 964 foreach (pkg; this.getTopologicalPackageList(false, null, configs)) { 965 auto psettings = pkg.getBuildSettings(gsettings.platform, configs[pkg.name]); 966 dst.addVersions(psettings.versions); 967 } 968 } 969 970 /** Fills `dst` with build settings specific to the given build type. 971 972 Params: 973 dst = The `BuildSettings` instance to add the build settings to 974 gsettings = Target generator settings 975 for_root_package = Selects if the build settings are for the root 976 package or for one of the dependencies. Unittest flags will 977 only be added to the root package. 978 */ 979 void addBuildTypeSettings(ref BuildSettings dst, in GeneratorSettings gsettings, bool for_root_package = true) 980 { 981 bool usedefflags = !(dst.requirements & BuildRequirement.noDefaultFlags); 982 if (usedefflags) { 983 BuildSettings btsettings; 984 m_rootPackage.addBuildTypeSettings(btsettings, gsettings.platform, gsettings.buildType); 985 986 if (!for_root_package) { 987 // don't propagate unittest switch to dependencies, as dependent 988 // unit tests aren't run anyway and the additional code may 989 // cause linking to fail on Windows (issue #640) 990 btsettings.removeOptions(BuildOption.unittests); 991 } 992 993 processVars(dst, this, m_rootPackage, btsettings, gsettings); 994 } 995 } 996 997 /// Outputs a build description of the project, including its dependencies. 998 ProjectDescription describe(GeneratorSettings settings) 999 { 1000 import dub.generators.targetdescription; 1001 1002 // store basic build parameters 1003 ProjectDescription ret; 1004 ret.rootPackage = m_rootPackage.name; 1005 ret.configuration = settings.config; 1006 ret.buildType = settings.buildType; 1007 ret.compiler = settings.platform.compiler; 1008 ret.architecture = settings.platform.architecture; 1009 ret.platform = settings.platform.platform; 1010 1011 // collect high level information about projects (useful for IDE display) 1012 auto configs = getPackageConfigs(settings.platform, settings.config); 1013 ret.packages ~= m_rootPackage.describe(settings.platform, settings.config); 1014 foreach (dep; m_dependencies) 1015 ret.packages ~= dep.describe(settings.platform, configs[dep.name]); 1016 1017 foreach (p; getTopologicalPackageList(false, null, configs)) 1018 ret.packages[ret.packages.countUntil!(pp => pp.name == p.name)].active = true; 1019 1020 if (settings.buildType.length) { 1021 // collect build target information (useful for build tools) 1022 auto gen = new TargetDescriptionGenerator(this); 1023 try { 1024 gen.generate(settings); 1025 ret.targets = gen.targetDescriptions; 1026 ret.targetLookup = gen.targetDescriptionLookup; 1027 } catch (Exception e) { 1028 logDiagnostic("Skipping targets description: %s", e.msg); 1029 logDebug("Full error: %s", e.toString().sanitize); 1030 } 1031 } 1032 1033 return ret; 1034 } 1035 1036 private string[] listBuildSetting(string attributeName)(ref GeneratorSettings settings, 1037 string config, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping) 1038 { 1039 return listBuildSetting!attributeName(settings, getPackageConfigs(settings.platform, config), 1040 projectDescription, compiler, disableEscaping); 1041 } 1042 1043 private string[] listBuildSetting(string attributeName)(ref GeneratorSettings settings, 1044 string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping) 1045 { 1046 if (compiler) 1047 return formatBuildSettingCompiler!attributeName(settings, configs, projectDescription, compiler, disableEscaping); 1048 else 1049 return formatBuildSettingPlain!attributeName(settings, configs, projectDescription); 1050 } 1051 1052 // Output a build setting formatted for a compiler 1053 private string[] formatBuildSettingCompiler(string attributeName)(ref GeneratorSettings settings, 1054 string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping) 1055 { 1056 import std.process : escapeShellFileName; 1057 import std.path : dirSeparator; 1058 1059 assert(compiler); 1060 1061 auto targetDescription = projectDescription.lookupTarget(projectDescription.rootPackage); 1062 auto buildSettings = targetDescription.buildSettings; 1063 1064 string[] values; 1065 switch (attributeName) 1066 { 1067 case "dflags": 1068 case "linkerFiles": 1069 case "mainSourceFile": 1070 case "importFiles": 1071 values = formatBuildSettingPlain!attributeName(settings, configs, projectDescription); 1072 break; 1073 1074 case "lflags": 1075 case "sourceFiles": 1076 case "injectSourceFiles": 1077 case "versions": 1078 case "debugVersions": 1079 case "importPaths": 1080 case "cImportPaths": 1081 case "stringImportPaths": 1082 case "options": 1083 auto bs = buildSettings.dup; 1084 bs.dflags = null; 1085 1086 // Ensure trailing slash on directory paths 1087 auto ensureTrailingSlash = (string path) => path.endsWith(dirSeparator) ? path : path ~ dirSeparator; 1088 static if (attributeName == "importPaths") 1089 bs.importPaths = bs.importPaths.map!(ensureTrailingSlash).array(); 1090 else static if (attributeName == "cImportPaths") 1091 bs.cImportPaths = bs.cImportPaths.map!(ensureTrailingSlash).array(); 1092 else static if (attributeName == "stringImportPaths") 1093 bs.stringImportPaths = bs.stringImportPaths.map!(ensureTrailingSlash).array(); 1094 1095 compiler.prepareBuildSettings(bs, settings.platform, BuildSetting.all & ~to!BuildSetting(attributeName)); 1096 values = bs.dflags; 1097 break; 1098 1099 case "libs": 1100 auto bs = buildSettings.dup; 1101 bs.dflags = null; 1102 bs.lflags = null; 1103 bs.sourceFiles = null; 1104 bs.targetType = TargetType.none; // Force Compiler to NOT omit dependency libs when package is a library. 1105 1106 compiler.prepareBuildSettings(bs, settings.platform, BuildSetting.all & ~to!BuildSetting(attributeName)); 1107 1108 if (bs.lflags) 1109 values = compiler.lflagsToDFlags( bs.lflags ); 1110 else if (bs.sourceFiles) 1111 values = compiler.lflagsToDFlags( bs.sourceFiles ); 1112 else 1113 values = bs.dflags; 1114 1115 break; 1116 1117 default: assert(0); 1118 } 1119 1120 // Escape filenames and paths 1121 if(!disableEscaping) 1122 { 1123 switch (attributeName) 1124 { 1125 case "mainSourceFile": 1126 case "linkerFiles": 1127 case "injectSourceFiles": 1128 case "copyFiles": 1129 case "importFiles": 1130 case "stringImportFiles": 1131 case "sourceFiles": 1132 case "importPaths": 1133 case "cImportPaths": 1134 case "stringImportPaths": 1135 return values.map!(escapeShellFileName).array(); 1136 1137 default: 1138 return values; 1139 } 1140 } 1141 1142 return values; 1143 } 1144 1145 // Output a build setting without formatting for any particular compiler 1146 private string[] formatBuildSettingPlain(string attributeName)(ref GeneratorSettings settings, string[string] configs, ProjectDescription projectDescription) 1147 { 1148 import std.path : buildNormalizedPath, dirSeparator; 1149 import std.range : only; 1150 1151 string[] list; 1152 1153 enforce(attributeName == "targetType" || projectDescription.lookupRootPackage().targetType != TargetType.none, 1154 "Target type is 'none'. Cannot list build settings."); 1155 1156 static if (attributeName == "targetType") 1157 if (projectDescription.rootPackage !in projectDescription.targetLookup) 1158 return ["none"]; 1159 1160 auto targetDescription = projectDescription.lookupTarget(projectDescription.rootPackage); 1161 auto buildSettings = targetDescription.buildSettings; 1162 1163 string[] substituteCommands(Package pack, string[] commands, CommandType type) 1164 { 1165 auto env = makeCommandEnvironmentVariables(type, pack, this, settings, buildSettings); 1166 return processVars(this, pack, settings, commands, false, env); 1167 } 1168 1169 // Return any BuildSetting member attributeName as a range of strings. Don't attempt to fixup values. 1170 // allowEmptyString: When the value is a string (as opposed to string[]), 1171 // is empty string an actual permitted value instead of 1172 // a missing value? 1173 auto getRawBuildSetting(Package pack, bool allowEmptyString) { 1174 auto value = __traits(getMember, buildSettings, attributeName); 1175 1176 static if( attributeName.endsWith("Commands") ) 1177 return substituteCommands(pack, value, mixin("CommandType.", attributeName[0 .. $ - "Commands".length])); 1178 else static if( is(typeof(value) == string[]) ) 1179 return value; 1180 else static if( is(typeof(value) == string) ) 1181 { 1182 auto ret = only(value); 1183 1184 // only() has a different return type from only(value), so we 1185 // have to empty the range rather than just returning only(). 1186 if(value.empty && !allowEmptyString) { 1187 ret.popFront(); 1188 assert(ret.empty); 1189 } 1190 1191 return ret; 1192 } 1193 else static if( is(typeof(value) == string[string]) ) 1194 return value.byKeyValue.map!(a => a.key ~ "=" ~ a.value); 1195 else static if( is(typeof(value) == enum) ) 1196 return only(value); 1197 else static if( is(typeof(value) == Flags!BuildRequirement) ) 1198 return only(cast(BuildRequirement) cast(int) value.values); 1199 else static if( is(typeof(value) == Flags!BuildOption) ) 1200 return only(cast(BuildOption) cast(int) value.values); 1201 else 1202 static assert(false, "Type of BuildSettings."~attributeName~" is unsupported."); 1203 } 1204 1205 // Adjust BuildSetting member attributeName as needed. 1206 // Returns a range of strings. 1207 auto getFixedBuildSetting(Package pack) { 1208 // Is relative path(s) to a directory? 1209 enum isRelativeDirectory = 1210 attributeName == "importPaths" || attributeName == "cImportPaths" || attributeName == "stringImportPaths" || 1211 attributeName == "targetPath" || attributeName == "workingDirectory"; 1212 1213 // Is relative path(s) to a file? 1214 enum isRelativeFile = 1215 attributeName == "sourceFiles" || attributeName == "linkerFiles" || 1216 attributeName == "importFiles" || attributeName == "stringImportFiles" || 1217 attributeName == "copyFiles" || attributeName == "mainSourceFile" || 1218 attributeName == "injectSourceFiles"; 1219 1220 // For these, empty string means "main project directory", not "missing value" 1221 enum allowEmptyString = 1222 attributeName == "targetPath" || attributeName == "workingDirectory"; 1223 1224 enum isEnumBitfield = 1225 attributeName == "requirements" || attributeName == "options"; 1226 1227 enum isEnum = attributeName == "targetType"; 1228 1229 auto values = getRawBuildSetting(pack, allowEmptyString); 1230 string fixRelativePath(string importPath) { return buildNormalizedPath(pack.path.toString(), importPath); } 1231 static string ensureTrailingSlash(string path) { return path.endsWith(dirSeparator) ? path : path ~ dirSeparator; } 1232 1233 static if(isRelativeDirectory) { 1234 // Return full paths for the paths, making sure a 1235 // directory separator is on the end of each path. 1236 return values.map!(fixRelativePath).map!(ensureTrailingSlash); 1237 } 1238 else static if(isRelativeFile) { 1239 // Return full paths. 1240 return values.map!(fixRelativePath); 1241 } 1242 else static if(isEnumBitfield) 1243 return bitFieldNames(values.front); 1244 else static if (isEnum) 1245 return [values.front.to!string]; 1246 else 1247 return values; 1248 } 1249 1250 foreach(value; getFixedBuildSetting(m_rootPackage)) { 1251 list ~= value; 1252 } 1253 1254 return list; 1255 } 1256 1257 // The "compiler" arg is for choosing which compiler the output should be formatted for, 1258 // or null to imply "list" format. 1259 private string[] listBuildSetting(ref GeneratorSettings settings, string[string] configs, 1260 ProjectDescription projectDescription, string requestedData, Compiler compiler, bool disableEscaping) 1261 { 1262 // Certain data cannot be formatter for a compiler 1263 if (compiler) 1264 { 1265 switch (requestedData) 1266 { 1267 case "target-type": 1268 case "target-path": 1269 case "target-name": 1270 case "working-directory": 1271 case "string-import-files": 1272 case "copy-files": 1273 case "extra-dependency-files": 1274 case "pre-generate-commands": 1275 case "post-generate-commands": 1276 case "pre-build-commands": 1277 case "post-build-commands": 1278 case "pre-run-commands": 1279 case "post-run-commands": 1280 case "environments": 1281 case "build-environments": 1282 case "run-environments": 1283 case "pre-generate-environments": 1284 case "post-generate-environments": 1285 case "pre-build-environments": 1286 case "post-build-environments": 1287 case "pre-run-environments": 1288 case "post-run-environments": 1289 case "default-config": 1290 case "configs": 1291 case "default-build": 1292 case "builds": 1293 enforce(false, "--data="~requestedData~" can only be used with `--data-list` or `--data-list --data-0`."); 1294 break; 1295 1296 case "requirements": 1297 enforce(false, "--data=requirements can only be used with `--data-list` or `--data-list --data-0`. Use --data=options instead."); 1298 break; 1299 1300 default: break; 1301 } 1302 } 1303 1304 import std.typetuple : TypeTuple; 1305 auto args = TypeTuple!(settings, configs, projectDescription, compiler, disableEscaping); 1306 switch (requestedData) 1307 { 1308 case "target-type": return listBuildSetting!"targetType"(args); 1309 case "target-path": return listBuildSetting!"targetPath"(args); 1310 case "target-name": return listBuildSetting!"targetName"(args); 1311 case "working-directory": return listBuildSetting!"workingDirectory"(args); 1312 case "main-source-file": return listBuildSetting!"mainSourceFile"(args); 1313 case "dflags": return listBuildSetting!"dflags"(args); 1314 case "lflags": return listBuildSetting!"lflags"(args); 1315 case "libs": return listBuildSetting!"libs"(args); 1316 case "linker-files": return listBuildSetting!"linkerFiles"(args); 1317 case "source-files": return listBuildSetting!"sourceFiles"(args); 1318 case "inject-source-files": return listBuildSetting!"injectSourceFiles"(args); 1319 case "copy-files": return listBuildSetting!"copyFiles"(args); 1320 case "extra-dependency-files": return listBuildSetting!"extraDependencyFiles"(args); 1321 case "versions": return listBuildSetting!"versions"(args); 1322 case "debug-versions": return listBuildSetting!"debugVersions"(args); 1323 case "import-paths": return listBuildSetting!"importPaths"(args); 1324 case "string-import-paths": return listBuildSetting!"stringImportPaths"(args); 1325 case "import-files": return listBuildSetting!"importFiles"(args); 1326 case "string-import-files": return listBuildSetting!"stringImportFiles"(args); 1327 case "pre-generate-commands": return listBuildSetting!"preGenerateCommands"(args); 1328 case "post-generate-commands": return listBuildSetting!"postGenerateCommands"(args); 1329 case "pre-build-commands": return listBuildSetting!"preBuildCommands"(args); 1330 case "post-build-commands": return listBuildSetting!"postBuildCommands"(args); 1331 case "pre-run-commands": return listBuildSetting!"preRunCommands"(args); 1332 case "post-run-commands": return listBuildSetting!"postRunCommands"(args); 1333 case "environments": return listBuildSetting!"environments"(args); 1334 case "build-environments": return listBuildSetting!"buildEnvironments"(args); 1335 case "run-environments": return listBuildSetting!"runEnvironments"(args); 1336 case "pre-generate-environments": return listBuildSetting!"preGenerateEnvironments"(args); 1337 case "post-generate-environments": return listBuildSetting!"postGenerateEnvironments"(args); 1338 case "pre-build-environments": return listBuildSetting!"preBuildEnvironments"(args); 1339 case "post-build-environments": return listBuildSetting!"postBuildEnvironments"(args); 1340 case "pre-run-environments": return listBuildSetting!"preRunEnvironments"(args); 1341 case "post-run-environments": return listBuildSetting!"postRunEnvironments"(args); 1342 case "requirements": return listBuildSetting!"requirements"(args); 1343 case "options": return listBuildSetting!"options"(args); 1344 case "default-config": return [getDefaultConfiguration(settings.platform)]; 1345 case "configs": return configurations; 1346 case "default-build": return [builds[0]]; 1347 case "builds": return builds; 1348 1349 default: 1350 enforce(false, "--data="~requestedData~ 1351 " is not a valid option. See 'dub describe --help' for accepted --data= values."); 1352 } 1353 1354 assert(0); 1355 } 1356 1357 /// Outputs requested data for the project, optionally including its dependencies. 1358 string[] listBuildSettings(GeneratorSettings settings, string[] requestedData, ListBuildSettingsFormat list_type) 1359 { 1360 import dub.compilers.utils : isLinkerFile; 1361 1362 auto projectDescription = describe(settings); 1363 auto configs = getPackageConfigs(settings.platform, settings.config); 1364 PackageDescription packageDescription; 1365 foreach (pack; projectDescription.packages) { 1366 if (pack.name == projectDescription.rootPackage) 1367 packageDescription = pack; 1368 } 1369 1370 if (projectDescription.rootPackage in projectDescription.targetLookup) { 1371 // Copy linker files from sourceFiles to linkerFiles 1372 auto target = projectDescription.lookupTarget(projectDescription.rootPackage); 1373 foreach (file; target.buildSettings.sourceFiles.filter!(f => isLinkerFile(settings.platform, f))) 1374 target.buildSettings.addLinkerFiles(file); 1375 1376 // Remove linker files from sourceFiles 1377 target.buildSettings.sourceFiles = 1378 target.buildSettings.sourceFiles 1379 .filter!(a => !isLinkerFile(settings.platform, a)) 1380 .array(); 1381 projectDescription.lookupTarget(projectDescription.rootPackage) = target; 1382 } 1383 1384 Compiler compiler; 1385 bool no_escape; 1386 final switch (list_type) with (ListBuildSettingsFormat) { 1387 case list: break; 1388 case listNul: no_escape = true; break; 1389 case commandLine: compiler = settings.compiler; break; 1390 case commandLineNul: compiler = settings.compiler; no_escape = true; break; 1391 1392 } 1393 1394 auto result = requestedData 1395 .map!(dataName => listBuildSetting(settings, configs, projectDescription, dataName, compiler, no_escape)); 1396 1397 final switch (list_type) with (ListBuildSettingsFormat) { 1398 case list: return result.map!(l => l.join("\n")).array(); 1399 case listNul: return result.map!(l => l.join("\0")).array; 1400 case commandLine: return result.map!(l => l.join(" ")).array; 1401 case commandLineNul: return result.map!(l => l.join("\0")).array; 1402 } 1403 } 1404 1405 /** Saves the currently selected dependency versions to disk. 1406 1407 The selections will be written to a file named 1408 `SelectedVersions.defaultFile` ("dub.selections.json") within the 1409 directory of the root package. Any existing file will get overwritten. 1410 */ 1411 void saveSelections() 1412 { 1413 assert(m_selections !is null, "Cannot save selections for non-disk based project (has no selections)."); 1414 const name = PackageName(m_rootPackage.basePackage.name); 1415 if (m_selections.hasSelectedVersion(name)) 1416 m_selections.deselectVersion(name); 1417 this.m_packageManager.writeSelections( 1418 this.m_rootPackage, this.m_selections.m_selections, 1419 this.m_selections.dirty); 1420 } 1421 1422 deprecated bool isUpgradeCacheUpToDate() 1423 { 1424 return false; 1425 } 1426 1427 deprecated Dependency[string] getUpgradeCache() 1428 { 1429 return null; 1430 } 1431 } 1432 1433 1434 /// Determines the output format used for `Project.listBuildSettings`. 1435 enum ListBuildSettingsFormat { 1436 list, /// Newline separated list entries 1437 listNul, /// NUL character separated list entries (unescaped) 1438 commandLine, /// Formatted for compiler command line (one data list per line) 1439 commandLineNul, /// NUL character separated list entries (unescaped, data lists separated by two NUL characters) 1440 } 1441 1442 deprecated("Use `dub.packagemanager : PlacementLocation` instead") 1443 public alias PlacementLocation = dub.packagemanager.PlacementLocation; 1444 1445 void processVars(ref BuildSettings dst, in Project project, in Package pack, 1446 BuildSettings settings, in GeneratorSettings gsettings, bool include_target_settings = false) 1447 { 1448 string[string] processVerEnvs(in string[string] targetEnvs, in string[string] defaultEnvs) 1449 { 1450 string[string] retEnv; 1451 foreach (k, v; targetEnvs) 1452 retEnv[k] = v; 1453 foreach (k, v; defaultEnvs) { 1454 if (k !in targetEnvs) 1455 retEnv[k] = v; 1456 } 1457 return processVars(project, pack, gsettings, retEnv); 1458 } 1459 dst.addEnvironments(processVerEnvs(settings.environments, gsettings.buildSettings.environments)); 1460 dst.addBuildEnvironments(processVerEnvs(settings.buildEnvironments, gsettings.buildSettings.buildEnvironments)); 1461 dst.addRunEnvironments(processVerEnvs(settings.runEnvironments, gsettings.buildSettings.runEnvironments)); 1462 dst.addPreGenerateEnvironments(processVerEnvs(settings.preGenerateEnvironments, gsettings.buildSettings.preGenerateEnvironments)); 1463 dst.addPostGenerateEnvironments(processVerEnvs(settings.postGenerateEnvironments, gsettings.buildSettings.postGenerateEnvironments)); 1464 dst.addPreBuildEnvironments(processVerEnvs(settings.preBuildEnvironments, gsettings.buildSettings.preBuildEnvironments)); 1465 dst.addPostBuildEnvironments(processVerEnvs(settings.postBuildEnvironments, gsettings.buildSettings.postBuildEnvironments)); 1466 dst.addPreRunEnvironments(processVerEnvs(settings.preRunEnvironments, gsettings.buildSettings.preRunEnvironments)); 1467 dst.addPostRunEnvironments(processVerEnvs(settings.postRunEnvironments, gsettings.buildSettings.postRunEnvironments)); 1468 1469 auto buildEnvs = [dst.environments, dst.buildEnvironments]; 1470 1471 dst.addDFlags(processVars(project, pack, gsettings, settings.dflags, false, buildEnvs)); 1472 dst.addLFlags(processVars(project, pack, gsettings, settings.lflags, false, buildEnvs)); 1473 dst.addLibs(processVars(project, pack, gsettings, settings.libs, false, buildEnvs)); 1474 dst.addSourceFiles(processVars!true(project, pack, gsettings, settings.sourceFiles, true, buildEnvs)); 1475 dst.addImportFiles(processVars(project, pack, gsettings, settings.importFiles, true, buildEnvs)); 1476 dst.addStringImportFiles(processVars(project, pack, gsettings, settings.stringImportFiles, true, buildEnvs)); 1477 dst.addInjectSourceFiles(processVars!true(project, pack, gsettings, settings.injectSourceFiles, true, buildEnvs)); 1478 dst.addCopyFiles(processVars(project, pack, gsettings, settings.copyFiles, true, buildEnvs)); 1479 dst.addExtraDependencyFiles(processVars(project, pack, gsettings, settings.extraDependencyFiles, true, buildEnvs)); 1480 dst.addVersions(processVars(project, pack, gsettings, settings.versions, false, buildEnvs)); 1481 dst.addDebugVersions(processVars(project, pack, gsettings, settings.debugVersions, false, buildEnvs)); 1482 dst.addVersionFilters(processVars(project, pack, gsettings, settings.versionFilters, false, buildEnvs)); 1483 dst.addDebugVersionFilters(processVars(project, pack, gsettings, settings.debugVersionFilters, false, buildEnvs)); 1484 dst.addImportPaths(processVars(project, pack, gsettings, settings.importPaths, true, buildEnvs)); 1485 dst.addCImportPaths(processVars(project, pack, gsettings, settings.cImportPaths, true, buildEnvs)); 1486 dst.addStringImportPaths(processVars(project, pack, gsettings, settings.stringImportPaths, true, buildEnvs)); 1487 dst.addRequirements(settings.requirements); 1488 dst.addOptions(settings.options); 1489 1490 // commands are substituted in dub.generators.generator : runBuildCommands 1491 dst.addPreGenerateCommands(settings.preGenerateCommands); 1492 dst.addPostGenerateCommands(settings.postGenerateCommands); 1493 dst.addPreBuildCommands(settings.preBuildCommands); 1494 dst.addPostBuildCommands(settings.postBuildCommands); 1495 dst.addPreRunCommands(settings.preRunCommands); 1496 dst.addPostRunCommands(settings.postRunCommands); 1497 1498 if (include_target_settings) { 1499 dst.targetType = settings.targetType; 1500 dst.targetPath = processVars(settings.targetPath, project, pack, gsettings, true, buildEnvs); 1501 dst.targetName = settings.targetName; 1502 if (!settings.workingDirectory.empty) 1503 dst.workingDirectory = processVars(settings.workingDirectory, project, pack, gsettings, true, buildEnvs); 1504 if (settings.mainSourceFile.length) 1505 dst.mainSourceFile = processVars(settings.mainSourceFile, project, pack, gsettings, true, buildEnvs); 1506 } 1507 } 1508 1509 string[] processVars(bool glob = false)(in Project project, in Package pack, in GeneratorSettings gsettings, in string[] vars, bool are_paths = false, in string[string][] extraVers = null) 1510 { 1511 auto ret = appender!(string[])(); 1512 processVars!glob(ret, project, pack, gsettings, vars, are_paths, extraVers); 1513 return ret.data; 1514 } 1515 void processVars(bool glob = false)(ref Appender!(string[]) dst, in Project project, in Package pack, in GeneratorSettings gsettings, in string[] vars, bool are_paths = false, in string[string][] extraVers = null) 1516 { 1517 static if (glob) 1518 alias process = processVarsWithGlob!(Project, Package); 1519 else 1520 alias process = processVars!(Project, Package); 1521 foreach (var; vars) 1522 dst.put(process(var, project, pack, gsettings, are_paths, extraVers)); 1523 } 1524 1525 string processVars(Project, Package)(string var, in Project project, in Package pack, in GeneratorSettings gsettings, bool is_path, in string[string][] extraVers = null) 1526 { 1527 var = var.expandVars!(varName => getVariable(varName, project, pack, gsettings, extraVers)); 1528 if (!is_path) 1529 return var; 1530 auto p = NativePath(var); 1531 if (!p.absolute) 1532 return (pack.path ~ p).toNativeString(); 1533 else 1534 return p.toNativeString(); 1535 } 1536 string[string] processVars(bool glob = false)(in Project project, in Package pack, in GeneratorSettings gsettings, in string[string] vars, in string[string][] extraVers = null) 1537 { 1538 string[string] ret; 1539 processVars!glob(ret, project, pack, gsettings, vars, extraVers); 1540 return ret; 1541 } 1542 void processVars(bool glob = false)(ref string[string] dst, in Project project, in Package pack, in GeneratorSettings gsettings, in string[string] vars, in string[string][] extraVers) 1543 { 1544 static if (glob) 1545 alias process = processVarsWithGlob!(Project, Package); 1546 else 1547 alias process = processVars!(Project, Package); 1548 foreach (k, var; vars) 1549 dst[k] = process(var, project, pack, gsettings, false, extraVers); 1550 } 1551 1552 private string[] processVarsWithGlob(Project, Package)(string var, in Project project, in Package pack, in GeneratorSettings gsettings, bool is_path, in string[string][] extraVers) 1553 { 1554 assert(is_path, "can't glob something that isn't a path"); 1555 string res = processVars(var, project, pack, gsettings, is_path, extraVers); 1556 // Find the unglobbed prefix and iterate from there. 1557 size_t i = 0; 1558 size_t sepIdx = 0; 1559 loop: while (i < res.length) { 1560 switch_: switch (res[i]) 1561 { 1562 case '*', '?', '[', '{': break loop; 1563 case '/': sepIdx = i; goto default; 1564 version (Windows) { case '\\': sepIdx = i; goto default; } 1565 default: ++i; break switch_; 1566 } 1567 } 1568 if (i == res.length) //no globbing found in the path 1569 return [res]; 1570 import std.file : dirEntries, SpanMode; 1571 import std.path : buildNormalizedPath, globMatch, isAbsolute, relativePath; 1572 auto cwd = gsettings.toolWorkingDirectory.toNativeString; 1573 auto path = res[0 .. sepIdx]; 1574 bool prependCwd = false; 1575 if (!isAbsolute(path)) 1576 { 1577 prependCwd = true; 1578 path = buildNormalizedPath(cwd, path); 1579 } 1580 1581 return dirEntries(path, SpanMode.depth) 1582 .map!(de => prependCwd 1583 ? de.name.relativePath(cwd) 1584 : de.name) 1585 .filter!(name => globMatch(name, res)) 1586 .array; 1587 } 1588 /// Expand variables using `$VAR_NAME` or `${VAR_NAME}` syntax. 1589 /// `$$` escapes itself and is expanded to a single `$`. 1590 private string expandVars(alias expandVar)(string s) 1591 { 1592 import std.functional : not; 1593 1594 auto result = appender!string; 1595 1596 static bool isVarChar(char c) 1597 { 1598 import std.ascii; 1599 return isAlphaNum(c) || c == '_'; 1600 } 1601 1602 while (true) 1603 { 1604 auto pos = s.indexOf('$'); 1605 if (pos < 0) 1606 { 1607 result.put(s); 1608 return result.data; 1609 } 1610 result.put(s[0 .. pos]); 1611 s = s[pos + 1 .. $]; 1612 enforce(s.length > 0, "Variable name expected at end of string"); 1613 switch (s[0]) 1614 { 1615 case '$': 1616 result.put("$"); 1617 s = s[1 .. $]; 1618 break; 1619 case '{': 1620 pos = s.indexOf('}'); 1621 enforce(pos >= 0, "Could not find '}' to match '${'"); 1622 result.put(expandVar(s[1 .. pos])); 1623 s = s[pos + 1 .. $]; 1624 break; 1625 default: 1626 pos = s.representation.countUntil!(not!isVarChar); 1627 if (pos < 0) 1628 pos = s.length; 1629 result.put(expandVar(s[0 .. pos])); 1630 s = s[pos .. $]; 1631 break; 1632 } 1633 } 1634 } 1635 1636 unittest 1637 { 1638 string[string] vars = 1639 [ 1640 "A" : "a", 1641 "B" : "b", 1642 ]; 1643 1644 string expandVar(string name) { auto p = name in vars; enforce(p, name); return *p; } 1645 1646 assert(expandVars!expandVar("") == ""); 1647 assert(expandVars!expandVar("x") == "x"); 1648 assert(expandVars!expandVar("$$") == "$"); 1649 assert(expandVars!expandVar("x$$") == "x$"); 1650 assert(expandVars!expandVar("$$x") == "$x"); 1651 assert(expandVars!expandVar("$$$$") == "$$"); 1652 assert(expandVars!expandVar("x$A") == "xa"); 1653 assert(expandVars!expandVar("x$$A") == "x$A"); 1654 assert(expandVars!expandVar("$A$B") == "ab"); 1655 assert(expandVars!expandVar("${A}$B") == "ab"); 1656 assert(expandVars!expandVar("$A${B}") == "ab"); 1657 assert(expandVars!expandVar("a${B}") == "ab"); 1658 assert(expandVars!expandVar("${A}b") == "ab"); 1659 1660 import std.exception : assertThrown; 1661 assertThrown(expandVars!expandVar("$")); 1662 assertThrown(expandVars!expandVar("${}")); 1663 assertThrown(expandVars!expandVar("$|")); 1664 assertThrown(expandVars!expandVar("x$")); 1665 assertThrown(expandVars!expandVar("$X")); 1666 assertThrown(expandVars!expandVar("${")); 1667 assertThrown(expandVars!expandVar("${X")); 1668 1669 // https://github.com/dlang/dmd/pull/9275 1670 assert(expandVars!expandVar("$${DUB_EXE:-dub}") == "${DUB_EXE:-dub}"); 1671 } 1672 1673 /// Expands the variables in the input string with the same rules as command 1674 /// variables inside custom dub commands. 1675 /// 1676 /// Params: 1677 /// s = the input string where environment variables in form `$VAR` should be replaced 1678 /// throwIfMissing = if true, throw an exception if the given variable is not found, 1679 /// otherwise replace unknown variables with the empty string. 1680 string expandEnvironmentVariables(string s, bool throwIfMissing = true) 1681 { 1682 import std.process : environment; 1683 1684 return expandVars!((v) { 1685 auto ret = environment.get(v); 1686 if (ret is null && throwIfMissing) 1687 throw new Exception("Specified environment variable `$" ~ v ~ "` is not set"); 1688 return ret; 1689 })(s); 1690 } 1691 1692 // Keep the following list up-to-date if adding more build settings variables. 1693 /// List of variables that can be used in build settings 1694 package(dub) immutable buildSettingsVars = [ 1695 "ARCH", "PLATFORM", "PLATFORM_POSIX", "BUILD_TYPE" 1696 ]; 1697 1698 private string getVariable(Project, Package)(string name, in Project project, in Package pack, in GeneratorSettings gsettings, in string[string][] extraVars = null) 1699 { 1700 import dub.internal.utils : getDUBExePath; 1701 import std.process : environment, escapeShellFileName; 1702 import std.uni : asUpperCase; 1703 1704 NativePath path; 1705 if (name == "PACKAGE_DIR") 1706 path = pack.path; 1707 else if (name == "ROOT_PACKAGE_DIR") 1708 path = project.rootPackage.path; 1709 1710 if (name.endsWith("_PACKAGE_DIR")) { 1711 auto pname = name[0 .. $-12]; 1712 foreach (prj; project.getTopologicalPackageList()) 1713 if (prj.name.asUpperCase.map!(a => a == '-' ? '_' : a).equal(pname)) 1714 { 1715 path = prj.path; 1716 break; 1717 } 1718 } 1719 1720 if (!path.empty) 1721 { 1722 // no trailing slash for clean path concatenation (see #1392) 1723 path.endsWithSlash = false; 1724 return path.toNativeString(); 1725 } 1726 1727 if (name == "DUB") { 1728 return getDUBExePath(gsettings.platform.compilerBinary).toNativeString(); 1729 } 1730 1731 if (name == "ARCH") { 1732 foreach (a; gsettings.platform.architecture) 1733 return a; 1734 return ""; 1735 } 1736 1737 if (name == "PLATFORM") { 1738 import std.algorithm : filter; 1739 foreach (p; gsettings.platform.platform.filter!(p => p != "posix")) 1740 return p; 1741 foreach (p; gsettings.platform.platform) 1742 return p; 1743 return ""; 1744 } 1745 1746 if (name == "PLATFORM_POSIX") { 1747 import std.algorithm : canFind; 1748 if (gsettings.platform.platform.canFind("posix")) 1749 return "posix"; 1750 foreach (p; gsettings.platform.platform) 1751 return p; 1752 return ""; 1753 } 1754 1755 if (name == "BUILD_TYPE") return gsettings.buildType; 1756 1757 if (name == "DFLAGS" || name == "LFLAGS") 1758 { 1759 auto buildSettings = pack.getBuildSettings(gsettings.platform, gsettings.config); 1760 if (name == "DFLAGS") 1761 return join(buildSettings.dflags," "); 1762 else if (name == "LFLAGS") 1763 return join(buildSettings.lflags," "); 1764 } 1765 1766 import std.range; 1767 foreach (aa; retro(extraVars)) 1768 if (auto exvar = name in aa) 1769 return *exvar; 1770 1771 auto envvar = environment.get(name); 1772 if (envvar !is null) return envvar; 1773 1774 throw new Exception("Invalid variable: "~name); 1775 } 1776 1777 1778 unittest 1779 { 1780 static struct MockPackage 1781 { 1782 this(string name) 1783 { 1784 this.name = name; 1785 version (Posix) 1786 path = NativePath("/pkgs/"~name); 1787 else version (Windows) 1788 path = NativePath(`C:\pkgs\`~name); 1789 // see 4d4017c14c, #268, and #1392 for why this all package paths end on slash internally 1790 path.endsWithSlash = true; 1791 } 1792 string name; 1793 NativePath path; 1794 BuildSettings getBuildSettings(in BuildPlatform platform, string config) const 1795 { 1796 return BuildSettings(); 1797 } 1798 } 1799 1800 static struct MockProject 1801 { 1802 MockPackage rootPackage; 1803 inout(MockPackage)[] getTopologicalPackageList() inout 1804 { 1805 return _dependencies; 1806 } 1807 private: 1808 MockPackage[] _dependencies; 1809 } 1810 1811 MockProject proj = { 1812 rootPackage: MockPackage("root"), 1813 _dependencies: [MockPackage("dep1"), MockPackage("dep2")] 1814 }; 1815 auto pack = MockPackage("test"); 1816 GeneratorSettings gsettings; 1817 enum isPath = true; 1818 1819 import std.path : dirSeparator; 1820 1821 static NativePath woSlash(NativePath p) { p.endsWithSlash = false; return p; } 1822 // basic vars 1823 assert(processVars("Hello $PACKAGE_DIR", proj, pack, gsettings, !isPath) == "Hello "~woSlash(pack.path).toNativeString); 1824 assert(processVars("Hello $ROOT_PACKAGE_DIR", proj, pack, gsettings, !isPath) == "Hello "~woSlash(proj.rootPackage.path).toNativeString.chomp(dirSeparator)); 1825 assert(processVars("Hello $DEP1_PACKAGE_DIR", proj, pack, gsettings, !isPath) == "Hello "~woSlash(proj._dependencies[0].path).toNativeString); 1826 // ${VAR} replacements 1827 assert(processVars("Hello ${PACKAGE_DIR}"~dirSeparator~"foobar", proj, pack, gsettings, !isPath) == "Hello "~(pack.path ~ "foobar").toNativeString); 1828 assert(processVars("Hello $PACKAGE_DIR"~dirSeparator~"foobar", proj, pack, gsettings, !isPath) == "Hello "~(pack.path ~ "foobar").toNativeString); 1829 // test with isPath 1830 assert(processVars("local", proj, pack, gsettings, isPath) == (pack.path ~ "local").toNativeString); 1831 assert(processVars("foo/$$ESCAPED", proj, pack, gsettings, isPath) == (pack.path ~ "foo/$ESCAPED").toNativeString); 1832 assert(processVars("$$ESCAPED", proj, pack, gsettings, !isPath) == "$ESCAPED"); 1833 // test other env variables 1834 import std.process : environment; 1835 environment["MY_ENV_VAR"] = "blablabla"; 1836 assert(processVars("$MY_ENV_VAR", proj, pack, gsettings, !isPath) == "blablabla"); 1837 assert(processVars("${MY_ENV_VAR}suffix", proj, pack, gsettings, !isPath) == "blablablasuffix"); 1838 assert(processVars("$MY_ENV_VAR-suffix", proj, pack, gsettings, !isPath) == "blablabla-suffix"); 1839 assert(processVars("$MY_ENV_VAR:suffix", proj, pack, gsettings, !isPath) == "blablabla:suffix"); 1840 assert(processVars("$MY_ENV_VAR$MY_ENV_VAR", proj, pack, gsettings, !isPath) == "blablablablablabla"); 1841 environment.remove("MY_ENV_VAR"); 1842 } 1843 1844 /** 1845 * Holds and stores a set of version selections for package dependencies. 1846 * 1847 * This is the runtime representation of the information contained in 1848 * "dub.selections.json" within a package's directory. 1849 * 1850 * Note that as subpackages share the same version as their main package, 1851 * this class will treat any subpackage reference as a reference to its 1852 * main package. 1853 */ 1854 public class SelectedVersions { 1855 protected { 1856 enum FileVersion = 1; 1857 Selections!1 m_selections; 1858 bool m_dirty = false; // has changes since last save 1859 bool m_bare = true; 1860 } 1861 1862 /// Default file name to use for storing selections. 1863 enum defaultFile = "dub.selections.json"; 1864 1865 /// Constructs a new empty version selection. 1866 public this(uint version_ = FileVersion) @safe pure 1867 { 1868 enforce(version_ == 1, "Unsupported file version"); 1869 this.m_selections = Selections!1(version_); 1870 } 1871 1872 /// Constructs a new non-empty version selection. 1873 public this(Selections!1 data) @safe pure nothrow @nogc 1874 { 1875 this.m_selections = data; 1876 this.m_bare = false; 1877 } 1878 1879 /** Constructs a new non-empty version selection, prefixing relative path 1880 selections with the specified prefix. 1881 1882 To be used in cases where the "dub.selections.json" file isn't located 1883 in the root package directory. 1884 */ 1885 public this(Selections!1 data, NativePath relPathPrefix) 1886 { 1887 this(data); 1888 if (relPathPrefix.empty) return; 1889 foreach (ref dep; m_selections.versions.byValue) { 1890 const depPath = dep.path; 1891 if (!depPath.empty && !depPath.absolute) 1892 dep = Dependency(relPathPrefix ~ depPath); 1893 } 1894 } 1895 1896 /** Constructs a new version selection from JSON data. 1897 1898 The structure of the JSON document must match the contents of the 1899 "dub.selections.json" file. 1900 */ 1901 deprecated("Pass a `dub.recipe.selection : Selected` directly") 1902 this(Json data) 1903 { 1904 deserialize(data); 1905 m_dirty = false; 1906 } 1907 1908 /** Constructs a new version selections from an existing JSON file. 1909 */ 1910 deprecated("JSON deserialization is deprecated") 1911 this(NativePath path) 1912 { 1913 auto json = jsonFromFile(path); 1914 deserialize(json); 1915 m_dirty = false; 1916 m_bare = false; 1917 } 1918 1919 /// Returns a list of names for all packages that have a version selection. 1920 @property string[] selectedPackages() const { return m_selections.versions.keys; } 1921 1922 /// Determines if any changes have been made after loading the selections from a file. 1923 @property bool dirty() const { return m_dirty; } 1924 1925 /// Determine if this set of selections is still empty (but not `clear`ed). 1926 @property bool bare() const { return m_bare && !m_dirty; } 1927 1928 /// Removes all selections. 1929 void clear() 1930 { 1931 m_selections.versions = null; 1932 m_dirty = true; 1933 } 1934 1935 /// Duplicates the set of selected versions from another instance. 1936 void set(SelectedVersions versions) 1937 { 1938 m_selections.fileVersion = versions.m_selections.fileVersion; 1939 m_selections.versions = versions.m_selections.versions.dup; 1940 m_selections.inheritable = versions.m_selections.inheritable; 1941 m_dirty = true; 1942 } 1943 1944 /// Selects a certain version for a specific package. 1945 deprecated("Use the overload that accepts a `PackageName`") 1946 void selectVersion(string package_id, Version version_) 1947 { 1948 const name = PackageName(package_id); 1949 return this.selectVersion(name, version_); 1950 } 1951 1952 /// Ditto 1953 void selectVersion(in PackageName name, Version version_) 1954 { 1955 const dep = Dependency(version_); 1956 this.selectVersionInternal(name, dep); 1957 } 1958 1959 /// Selects a certain path for a specific package. 1960 deprecated("Use the overload that accepts a `PackageName`") 1961 void selectVersion(string package_id, NativePath path) 1962 { 1963 const name = PackageName(package_id); 1964 return this.selectVersion(name, path); 1965 } 1966 1967 /// Ditto 1968 void selectVersion(in PackageName name, NativePath path) 1969 { 1970 const dep = Dependency(path); 1971 this.selectVersionInternal(name, dep); 1972 } 1973 1974 /// Selects a certain Git reference for a specific package. 1975 deprecated("Use the overload that accepts a `PackageName`") 1976 void selectVersion(string package_id, Repository repository) 1977 { 1978 const name = PackageName(package_id); 1979 return this.selectVersion(name, repository); 1980 } 1981 1982 /// Ditto 1983 void selectVersion(in PackageName name, Repository repository) 1984 { 1985 const dep = Dependency(repository); 1986 this.selectVersionInternal(name, dep); 1987 } 1988 1989 /// Internal implementation of selectVersion 1990 private void selectVersionInternal(in PackageName name, in Dependency dep) 1991 { 1992 if (auto pdep = name.main.toString() in m_selections.versions) { 1993 if (*pdep == dep) 1994 return; 1995 } 1996 m_selections.versions[name.main.toString()] = dep; 1997 m_dirty = true; 1998 } 1999 2000 deprecated("Move `spec` inside of the `repository` parameter and call `selectVersion`") 2001 void selectVersionWithRepository(string package_id, Repository repository, string spec) 2002 { 2003 this.selectVersion(package_id, Repository(repository.remote(), spec)); 2004 } 2005 2006 /// Removes the selection for a particular package. 2007 deprecated("Use the overload that accepts a `PackageName`") 2008 void deselectVersion(string package_id) 2009 { 2010 const n = PackageName(package_id); 2011 this.deselectVersion(n); 2012 } 2013 2014 /// Ditto 2015 void deselectVersion(in PackageName name) 2016 { 2017 m_selections.versions.remove(name.main.toString()); 2018 m_dirty = true; 2019 } 2020 2021 /// Determines if a particular package has a selection set. 2022 deprecated("Use the overload that accepts a `PackageName`") 2023 bool hasSelectedVersion(string packageId) const { 2024 const name = PackageName(packageId); 2025 return this.hasSelectedVersion(name); 2026 } 2027 2028 /// Ditto 2029 bool hasSelectedVersion(in PackageName name) const 2030 { 2031 return (name.main.toString() in m_selections.versions) !is null; 2032 } 2033 2034 /** Returns the selection for a particular package. 2035 2036 Note that the returned `Dependency` can either have the 2037 `Dependency.path` property set to a non-empty value, in which case this 2038 is a path based selection, or its `Dependency.version_` property is 2039 valid and it is a version selection. 2040 */ 2041 deprecated("Use the overload that accepts a `PackageName`") 2042 Dependency getSelectedVersion(string packageId) const 2043 { 2044 const name = PackageName(packageId); 2045 return this.getSelectedVersion(name); 2046 } 2047 2048 /// Ditto 2049 Dependency getSelectedVersion(in PackageName name) const 2050 { 2051 enforce(hasSelectedVersion(name)); 2052 return m_selections.versions[name.main.toString()]; 2053 } 2054 2055 /** Stores the selections to disk. 2056 2057 The target file will be written in JSON format. Usually, `defaultFile` 2058 should be used as the file name and the directory should be the root 2059 directory of the project's root package. 2060 */ 2061 deprecated("Use `PackageManager.writeSelections` to write a `SelectionsFile`") 2062 void save(NativePath path) 2063 { 2064 path.writeFile(PackageManager.selectionsToString(this.m_selections)); 2065 m_dirty = false; 2066 m_bare = false; 2067 } 2068 2069 deprecated("Use `dub.dependency : Dependency.toJson(true)`") 2070 static Json dependencyToJson(Dependency d) 2071 { 2072 return d.toJson(true); 2073 } 2074 2075 deprecated("JSON deserialization is deprecated") 2076 static Dependency dependencyFromJson(Json j) 2077 { 2078 if (j.type == Json.Type..string) 2079 return Dependency(Version(j.get!string)); 2080 else if (j.type == Json.Type.object && "path" in j) 2081 return Dependency(NativePath(j["path"].get!string)); 2082 else if (j.type == Json.Type.object && "repository" in j) 2083 return Dependency(Repository(j["repository"].get!string, 2084 enforce("version" in j, "Expected \"version\" field in repository version object").get!string)); 2085 else throw new Exception(format("Unexpected type for dependency: %s", j)); 2086 } 2087 2088 deprecated("JSON serialization is deprecated") 2089 Json serialize() const { 2090 return PackageManager.selectionsToJSON(this.m_selections); 2091 } 2092 2093 deprecated("JSON deserialization is deprecated") 2094 private void deserialize(Json json) 2095 { 2096 const fileVersion = json["fileVersion"].get!int; 2097 enforce(fileVersion == FileVersion, "Mismatched dub.selections.json version: " ~ to!string(fileVersion) ~ " vs. " ~ to!string(FileVersion)); 2098 clear(); 2099 m_selections.fileVersion = fileVersion; 2100 scope(failure) clear(); 2101 if (auto p = "inheritable" in json) 2102 m_selections.inheritable = p.get!bool; 2103 foreach (string p, dep; json["versions"]) 2104 m_selections.versions[p] = dependencyFromJson(dep); 2105 } 2106 } 2107 2108 /// The template code from which the test runner is generated 2109 private immutable TestRunnerTemplate = q{ 2110 deprecated // allow silently using deprecated symbols 2111 module dub_test_root; 2112 2113 import std.typetuple; 2114 2115 %-(static import %s; 2116 %); 2117 2118 alias allModules = TypeTuple!( 2119 %-(%s, %) 2120 ); 2121 2122 %s 2123 }; 2124 2125 /// The default test runner that gets used if none is provided 2126 private immutable DefaultTestRunnerCode = q{ 2127 version(D_BetterC) { 2128 extern(C) int main() { 2129 foreach (module_; allModules) { 2130 foreach (unitTest; __traits(getUnitTests, module_)) { 2131 unitTest(); 2132 } 2133 } 2134 import core.stdc.stdio : puts; 2135 puts("All unit tests have been run successfully."); 2136 return 0; 2137 } 2138 } else { 2139 void main() { 2140 version (D_Coverage) { 2141 } else { 2142 import std.stdio : writeln; 2143 writeln("All unit tests have been run successfully."); 2144 } 2145 } 2146 shared static this() { 2147 version (Have_tested) { 2148 import tested; 2149 import core.runtime; 2150 import std.exception; 2151 Runtime.moduleUnitTester = () => true; 2152 enforce(runUnitTests!allModules(new ConsoleTestResultWriter), "Unit tests failed."); 2153 } 2154 } 2155 } 2156 };