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.easy; 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, PackageName.init, 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.normalized()) { 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 return m_packageManager.getOrLoadPackage(path, NativePath.init, dep.name); 557 }, 558 (Repository repo) { 559 return m_packageManager.loadSCMPackage(dep.name, repo); 560 }, 561 (VersionRange range) { 562 // See `dub.recipe.selection : SelectedDependency.fromConfig` 563 assert(range.isExactVersion()); 564 return m_packageManager.getPackage(dep.name, vspec.version_); 565 }, 566 ); 567 } else if (m_dependencies.canFind!(d => PackageName(d.name).main == basename)) { 568 auto idx = m_dependencies.countUntil!(d => PackageName(d.name).main == basename); 569 auto bp = m_dependencies[idx].basePackage; 570 vspec = Dependency(bp.path); 571 p = resolveSubPackage(bp, subname, false); 572 } else { 573 logDiagnostic("%sVersion selection for dependency %s (%s) of %s is missing.", 574 indent, basename, dep.name, pack.name); 575 } 576 577 // We didn't find the package 578 if (p is null) 579 { 580 if (!vspec.repository.empty) { 581 p = m_packageManager.loadSCMPackage(dep.name, vspec.repository); 582 enforce(p !is null, 583 "Unable to fetch '%s@%s' using git - does the repository and version exist?".format( 584 dep.name, vspec.repository)); 585 } else if (!vspec.path.empty && is_desired) { 586 NativePath path = vspec.path; 587 if (!path.absolute) path = pack.path ~ path; 588 logDiagnostic("%sAdding local %s in %s", indent, dep.name, path); 589 p = m_packageManager.getOrLoadPackage(path, NativePath.init, dep.name); 590 path.endsWithSlash = true; 591 if (path != p.basePackage.path) { 592 logWarn("%sSub package %s must be referenced using the path to it's parent package.", indent, dep.name); 593 } 594 } else { 595 logDiagnostic("%sMissing dependency %s %s of %s", indent, dep.name, vspec, pack.name); 596 if (is_desired) m_missingDependencies ~= dep.name.toString(); 597 continue; 598 } 599 } 600 601 if (!m_dependencies.canFind(p)) { 602 logDiagnostic("%sFound dependency %s %s", indent, dep.name, vspec.toString()); 603 m_dependencies ~= p; 604 if (basename.toString() == m_rootPackage.basePackage.name) 605 p.warnOnSpecialCompilerFlags(); 606 collectDependenciesRec(p, depth+1); 607 } 608 609 m_dependees[p] ~= pack; 610 //enforce(p !is null, "Failed to resolve dependency "~dep.name~" "~vspec.toString()); 611 } 612 } 613 614 /// Convenience function used by `reinit` 615 private Package resolveSubPackage(Package p, string subname, bool silentFail) { 616 if (!subname.length || p is null) 617 return p; 618 return m_packageManager.getSubPackage(p, subname, silentFail); 619 } 620 621 /// Returns the name of the root package. 622 @property string name() const { return m_rootPackage ? m_rootPackage.name : "app"; } 623 624 /// Returns the names of all configurations of the root package. 625 @property string[] configurations() const { return m_rootPackage.configurations; } 626 627 /// Returns the names of all built-in and custom build types of the root package. 628 /// The default built-in build type is the first item in the list. 629 @property string[] builds() const { return builtinBuildTypes ~ m_rootPackage.customBuildTypes; } 630 631 /// Returns a map with the configuration for all packages in the dependency tree. 632 string[string] getPackageConfigs(in BuildPlatform platform, string config, bool allow_non_library = true) 633 const { 634 import std.typecons : Rebindable, rebindable; 635 import std.range : only; 636 637 // prepare by collecting information about all packages in the project 638 // qualified names and dependencies are cached, to avoid recomputing 639 // them multiple times during the algorithm 640 auto packages = collectPackageInformation(); 641 642 // graph of the project's package configuration dependencies 643 // (package, config) -> (sub-package, sub-config) 644 static struct Vertex { size_t pack = size_t.max; string config; } 645 static struct Edge { size_t from, to; } 646 Vertex[] configs; 647 void[0][Vertex] configs_set; 648 Edge[] edges; 649 650 651 size_t createConfig(size_t pack_idx, string config) { 652 foreach (i, v; configs) 653 if (v.pack == pack_idx && v.config == config) 654 return i; 655 656 auto pname = packages[pack_idx].name; 657 assert(pname !in m_overriddenConfigs || config == m_overriddenConfigs[pname]); 658 logDebug("Add config %s %s", pname, config); 659 auto cfg = Vertex(pack_idx, config); 660 configs ~= cfg; 661 configs_set[cfg] = (void[0]).init; 662 return configs.length-1; 663 } 664 665 bool haveConfig(size_t pack_idx, string config) { 666 return (Vertex(pack_idx, config) in configs_set) !is null; 667 } 668 669 void removeConfig(size_t config_index) { 670 logDebug("Eliminating config %s for %s", configs[config_index].config, configs[config_index].pack); 671 auto had_dep_to_pack = new bool[configs.length]; 672 auto still_has_dep_to_pack = new bool[configs.length]; 673 674 // eliminate all edges that connect to config 'config_index' and 675 // track all connected configs 676 edges = edges.filterInPlace!((e) { 677 if (e.to == config_index) { 678 had_dep_to_pack[e.from] = true; 679 return false; 680 } else if (configs[e.to].pack == configs[config_index].pack) { 681 still_has_dep_to_pack[e.from] = true; 682 } 683 684 return e.from != config_index; 685 }); 686 687 // mark config as removed 688 configs_set.remove(configs[config_index]); 689 configs[config_index] = Vertex.init; 690 691 // also remove any configs that cannot be satisfied anymore 692 foreach (j; 0 .. configs.length) 693 if (j != config_index && had_dep_to_pack[j] && !still_has_dep_to_pack[j]) 694 removeConfig(j); 695 } 696 697 bool[] reachable = new bool[packages.length]; // reused to avoid continuous re-allocation 698 bool isReachableByAllParentPacks(size_t cidx) { 699 foreach (p; packages[configs[cidx].pack].parents) reachable[p] = false; 700 foreach (e; edges) { 701 if (e.to != cidx) continue; 702 reachable[configs[e.from].pack] = true; 703 } 704 foreach (p; packages[configs[cidx].pack].parents) 705 if (!reachable[p]) 706 return false; 707 return true; 708 } 709 710 string[][] depconfigs = new string[][](packages.length); 711 void determineDependencyConfigs(size_t pack_idx, string c) 712 { 713 void[0][Edge] edges_set; 714 void createEdge(size_t from, size_t to) { 715 if (Edge(from, to) in edges_set) 716 return; 717 logDebug("Including %s %s -> %s %s", configs[from].pack, configs[from].config, configs[to].pack, configs[to].config); 718 edges ~= Edge(from, to); 719 edges_set[Edge(from, to)] = (void[0]).init; 720 } 721 722 auto pack = &packages[pack_idx]; 723 724 // below we call createConfig for the main package if 725 // config.length is not zero. Carry on for that case, 726 // otherwise we've handle the pair (p, c) already 727 if(haveConfig(pack_idx, c) && !(config.length && pack.name == m_rootPackage.name && config == c)) 728 return; 729 730 foreach (d; pack.dependencies) { 731 auto dp = packages.getPackageIndex(d.name.toString()); 732 if (dp == size_t.max) continue; 733 734 depconfigs[dp].length = 0; 735 depconfigs[dp].assumeSafeAppend; 736 737 void setConfigs(R)(R configs) { 738 configs 739 .filter!(c => haveConfig(dp, c)) 740 .each!((c) { depconfigs[dp] ~= c; }); 741 } 742 if (auto pc = packages[dp].name in m_overriddenConfigs) { 743 setConfigs(only(*pc)); 744 } else { 745 auto subconf = pack.package_.getSubConfiguration(c, packages[dp].package_, platform); 746 if (!subconf.empty) setConfigs(only(subconf)); 747 else { 748 setConfigs(packages[dp].package_.getPlatformConfigurations(platform)); 749 if (depconfigs[dp].length == 0) { 750 // Try with the executables too 751 setConfigs(packages[dp].package_.getPlatformConfigurations(platform, true)); 752 } 753 } 754 } 755 756 // if no valid configuration was found for a dependency, don't include the 757 // current configuration 758 if (!depconfigs[dp].length) { 759 logDebug("Skip %s %s (missing configuration for %s)", pack.name, c, packages[dp].name); 760 return; 761 } 762 } 763 764 // add this configuration to the graph 765 size_t cidx = createConfig(pack_idx, c); 766 foreach (d; pack.dependencies) { 767 if (auto pdp = d.name.toString() in packages) 768 foreach (sc; depconfigs[*pdp]) 769 createEdge(cidx, createConfig(*pdp, sc)); 770 } 771 } 772 773 string[] allconfigs_path; 774 void determineAllConfigs(size_t pack_idx) 775 { 776 auto pack = &packages[pack_idx]; 777 778 auto idx = allconfigs_path.countUntil(pack.name); 779 enforce(idx < 0, format("Detected dependency cycle: %s", (allconfigs_path[idx .. $] ~ pack.name).join("->"))); 780 allconfigs_path ~= pack.name; 781 scope (exit) { 782 allconfigs_path.length--; 783 allconfigs_path.assumeSafeAppend; 784 } 785 786 // first, add all dependency configurations 787 foreach (d; pack.dependencies) 788 if (auto pi = d.name.toString() in packages) 789 determineAllConfigs(*pi); 790 791 // for each configuration, determine the configurations usable for the dependencies 792 if (auto pc = pack.name in m_overriddenConfigs) 793 determineDependencyConfigs(pack_idx, *pc); 794 else 795 foreach (c; pack.package_.getPlatformConfigurations(platform, allow_non_library)) 796 determineDependencyConfigs(pack_idx, c); 797 } 798 799 800 // first, create a graph of all possible package configurations 801 assert(packages[0].package_ is m_rootPackage); 802 if (config.length) createConfig(0, config); 803 determineAllConfigs(0); 804 805 // then, successively remove configurations until only one configuration 806 // per package is left 807 bool changed; 808 do { 809 // remove all configs that are not reachable by all parent packages 810 changed = false; 811 foreach (i, ref c; configs) { 812 if (c == Vertex.init) continue; // ignore deleted configurations 813 if (!isReachableByAllParentPacks(i)) { 814 logDebug("%s %s NOT REACHABLE by all of (%s):", c.pack, c.config, packages[c.pack].parents); 815 removeConfig(i); 816 changed = true; 817 } 818 } 819 820 // when all edges are cleaned up, pick one package and remove all but one config 821 if (!changed) { 822 foreach (pidx; 0 .. packages.length) { 823 size_t cnt = 0; 824 foreach (i, ref c; configs) 825 if (c.pack == pidx && ++cnt > 1) { 826 logDebug("NON-PRIMARY: %s %s", c.pack, c.config); 827 removeConfig(i); 828 } 829 if (cnt > 1) { 830 changed = true; 831 break; 832 } 833 } 834 } 835 } while (changed); 836 837 // print out the resulting tree 838 foreach (e; edges) logDebug(" %s %s -> %s %s", configs[e.from].pack, configs[e.from].config, configs[e.to].pack, configs[e.to].config); 839 840 // return the resulting configuration set as an AA 841 string[string] ret; 842 foreach (c; configs) { 843 if (c == Vertex.init) continue; // ignore deleted configurations 844 auto pname = packages[c.pack].name; 845 assert(ret.get(pname, c.config) == c.config, format("Conflicting configurations for %s found: %s vs. %s", pname, c.config, ret[pname])); 846 logDebug("Using configuration '%s' for %s", c.config, pname); 847 ret[pname] = c.config; 848 } 849 850 // check for conflicts (packages missing in the final configuration graph) 851 auto visited = new bool[](packages.length); 852 void checkPacksRec(size_t pack_idx) { 853 if (visited[pack_idx]) return; 854 visited[pack_idx] = true; 855 auto pname = packages[pack_idx].name; 856 auto pc = pname in ret; 857 enforce(pc !is null, "Could not resolve configuration for package "~pname); 858 foreach (p, dep; packages[pack_idx].package_.getDependencies(*pc)) { 859 auto deppack = getDependency(p, dep.optional); 860 if (deppack) checkPacksRec(packages[].countUntil!(p => p.package_ is deppack)); 861 } 862 } 863 checkPacksRec(0); 864 865 return ret; 866 } 867 868 /** Returns an ordered list of all packages with the additional possibility 869 to look up by name. 870 */ 871 private auto collectPackageInformation() 872 const { 873 static struct PackageInfo { 874 const(Package) package_; 875 size_t[] parents; 876 string name; 877 PackageDependency[] dependencies; 878 } 879 880 static struct PackageInfoAccessor { 881 private { 882 PackageInfo[] m_packages; 883 size_t[string] m_packageMap; 884 } 885 886 private void initialize(P)(P all_packages, size_t reserve_count) 887 { 888 m_packages.reserve(reserve_count); 889 foreach (p; all_packages) { 890 auto pname = p.name; 891 m_packageMap[pname] = m_packages.length; 892 m_packages ~= PackageInfo(p, null, pname, p.getAllDependencies()); 893 } 894 foreach (pack_idx, ref pack_info; m_packages) 895 foreach (d; pack_info.dependencies) 896 if (auto pi = d.name.toString() in m_packageMap) 897 m_packages[*pi].parents ~= pack_idx; 898 } 899 900 size_t length() const { return m_packages.length; } 901 const(PackageInfo)[] opIndex() const { return m_packages; } 902 ref const(PackageInfo) opIndex(size_t package_index) const { return m_packages[package_index]; } 903 size_t getPackageIndex(string package_name) const { return m_packageMap.get(package_name, size_t.max); } 904 const(size_t)* opBinaryRight(string op = "in")(string package_name) const { return package_name in m_packageMap; } 905 } 906 907 PackageInfoAccessor ret; 908 ret.initialize(getTopologicalPackageList(), m_dependencies.length); 909 return ret; 910 } 911 912 /** 913 * Fills `dst` with values from this project. 914 * 915 * `dst` gets initialized according to the given platform and config. 916 * 917 * Params: 918 * dst = The BuildSettings struct to fill with data. 919 * gsettings = The generator settings to retrieve the values for. 920 * config = Values of the given configuration will be retrieved. 921 * root_package = If non null, use it instead of the project's real root package. 922 * shallow = If true, collects only build settings for the main package (including inherited settings) and doesn't stop on target type none and sourceLibrary. 923 */ 924 void addBuildSettings(ref BuildSettings dst, in GeneratorSettings gsettings, string config, in Package root_package = null, bool shallow = false) 925 const { 926 import dub.internal.utils : stripDlangSpecialChars; 927 928 auto configs = getPackageConfigs(gsettings.platform, config); 929 930 foreach (pkg; this.getTopologicalPackageList(false, root_package, configs)) { 931 auto pkg_path = pkg.path.toNativeString(); 932 dst.addVersions(["Have_" ~ stripDlangSpecialChars(pkg.name)]); 933 934 assert(pkg.name in configs, "Missing configuration for "~pkg.name); 935 logDebug("Gathering build settings for %s (%s)", pkg.name, configs[pkg.name]); 936 937 auto psettings = pkg.getBuildSettings(gsettings.platform, configs[pkg.name]); 938 if (psettings.targetType != TargetType.none) { 939 if (shallow && pkg !is m_rootPackage) 940 psettings.sourceFiles = null; 941 processVars(dst, this, pkg, psettings, gsettings); 942 if (!gsettings.single && psettings.importPaths.empty) 943 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]); 944 if (psettings.mainSourceFile.empty && pkg is m_rootPackage && psettings.targetType == TargetType.executable) 945 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); 946 } 947 if (pkg is m_rootPackage) { 948 if (!shallow) { 949 enforce(psettings.targetType != TargetType.none, "Main package has target type \"none\" - stopping build."); 950 enforce(psettings.targetType != TargetType.sourceLibrary, "Main package has target type \"sourceLibrary\" which generates no target - stopping build."); 951 } 952 dst.targetType = psettings.targetType; 953 dst.targetPath = psettings.targetPath; 954 dst.targetName = psettings.targetName; 955 if (!psettings.workingDirectory.empty) 956 dst.workingDirectory = processVars(psettings.workingDirectory, this, pkg, gsettings, true, [dst.environments, dst.buildEnvironments]); 957 if (psettings.mainSourceFile.length) 958 dst.mainSourceFile = processVars(psettings.mainSourceFile, this, pkg, gsettings, true, [dst.environments, dst.buildEnvironments]); 959 } 960 } 961 962 // always add all version identifiers of all packages 963 foreach (pkg; this.getTopologicalPackageList(false, null, configs)) { 964 auto psettings = pkg.getBuildSettings(gsettings.platform, configs[pkg.name]); 965 dst.addVersions(psettings.versions); 966 } 967 } 968 969 /** Fills `dst` with build settings specific to the given build type. 970 971 Params: 972 dst = The `BuildSettings` instance to add the build settings to 973 gsettings = Target generator settings 974 for_root_package = Selects if the build settings are for the root 975 package or for one of the dependencies. Unittest flags will 976 only be added to the root package. 977 */ 978 void addBuildTypeSettings(ref BuildSettings dst, in GeneratorSettings gsettings, bool for_root_package = true) 979 { 980 bool usedefflags = !(dst.requirements & BuildRequirement.noDefaultFlags); 981 if (usedefflags) { 982 BuildSettings btsettings; 983 m_rootPackage.addBuildTypeSettings(btsettings, gsettings.platform, gsettings.buildType); 984 985 if (!for_root_package) { 986 // don't propagate unittest switch to dependencies, as dependent 987 // unit tests aren't run anyway and the additional code may 988 // cause linking to fail on Windows (issue #640) 989 btsettings.removeOptions(BuildOption.unittests); 990 } 991 992 processVars(dst, this, m_rootPackage, btsettings, gsettings); 993 } 994 } 995 996 /// Outputs a build description of the project, including its dependencies. 997 ProjectDescription describe(GeneratorSettings settings) 998 { 999 import dub.generators.targetdescription; 1000 1001 // store basic build parameters 1002 ProjectDescription ret; 1003 ret.rootPackage = m_rootPackage.name; 1004 ret.configuration = settings.config; 1005 ret.buildType = settings.buildType; 1006 ret.compiler = settings.platform.compiler; 1007 ret.architecture = settings.platform.architecture; 1008 ret.platform = settings.platform.platform; 1009 1010 // collect high level information about projects (useful for IDE display) 1011 auto configs = getPackageConfigs(settings.platform, settings.config); 1012 ret.packages ~= m_rootPackage.describe(settings.platform, settings.config); 1013 foreach (dep; m_dependencies) 1014 ret.packages ~= dep.describe(settings.platform, configs[dep.name]); 1015 1016 foreach (p; getTopologicalPackageList(false, null, configs)) 1017 ret.packages[ret.packages.countUntil!(pp => pp.name == p.name)].active = true; 1018 1019 if (settings.buildType.length) { 1020 // collect build target information (useful for build tools) 1021 auto gen = new TargetDescriptionGenerator(this); 1022 try { 1023 gen.generate(settings); 1024 ret.targets = gen.targetDescriptions; 1025 ret.targetLookup = gen.targetDescriptionLookup; 1026 } catch (Exception e) { 1027 logDiagnostic("Skipping targets description: %s", e.msg); 1028 logDebug("Full error: %s", e.toString().sanitize); 1029 } 1030 } 1031 1032 return ret; 1033 } 1034 1035 private string[] listBuildSetting(string attributeName)(ref GeneratorSettings settings, 1036 string config, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping) 1037 { 1038 return listBuildSetting!attributeName(settings, getPackageConfigs(settings.platform, config), 1039 projectDescription, compiler, disableEscaping); 1040 } 1041 1042 private string[] listBuildSetting(string attributeName)(ref GeneratorSettings settings, 1043 string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping) 1044 { 1045 if (compiler) 1046 return formatBuildSettingCompiler!attributeName(settings, configs, projectDescription, compiler, disableEscaping); 1047 else 1048 return formatBuildSettingPlain!attributeName(settings, configs, projectDescription); 1049 } 1050 1051 // Output a build setting formatted for a compiler 1052 private string[] formatBuildSettingCompiler(string attributeName)(ref GeneratorSettings settings, 1053 string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping) 1054 { 1055 import std.process : escapeShellFileName; 1056 import std.path : dirSeparator; 1057 1058 assert(compiler); 1059 1060 auto targetDescription = projectDescription.lookupTarget(projectDescription.rootPackage); 1061 auto buildSettings = targetDescription.buildSettings; 1062 1063 string[] values; 1064 switch (attributeName) 1065 { 1066 case "dflags": 1067 case "linkerFiles": 1068 case "mainSourceFile": 1069 case "importFiles": 1070 case "frameworks": 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 "frameworks": return listBuildSetting!"frameworks"(args); 1317 case "linker-files": return listBuildSetting!"linkerFiles"(args); 1318 case "source-files": return listBuildSetting!"sourceFiles"(args); 1319 case "inject-source-files": return listBuildSetting!"injectSourceFiles"(args); 1320 case "copy-files": return listBuildSetting!"copyFiles"(args); 1321 case "extra-dependency-files": return listBuildSetting!"extraDependencyFiles"(args); 1322 case "versions": return listBuildSetting!"versions"(args); 1323 case "debug-versions": return listBuildSetting!"debugVersions"(args); 1324 case "import-paths": return listBuildSetting!"importPaths"(args); 1325 case "string-import-paths": return listBuildSetting!"stringImportPaths"(args); 1326 case "import-files": return listBuildSetting!"importFiles"(args); 1327 case "string-import-files": return listBuildSetting!"stringImportFiles"(args); 1328 case "pre-generate-commands": return listBuildSetting!"preGenerateCommands"(args); 1329 case "post-generate-commands": return listBuildSetting!"postGenerateCommands"(args); 1330 case "pre-build-commands": return listBuildSetting!"preBuildCommands"(args); 1331 case "post-build-commands": return listBuildSetting!"postBuildCommands"(args); 1332 case "pre-run-commands": return listBuildSetting!"preRunCommands"(args); 1333 case "post-run-commands": return listBuildSetting!"postRunCommands"(args); 1334 case "environments": return listBuildSetting!"environments"(args); 1335 case "build-environments": return listBuildSetting!"buildEnvironments"(args); 1336 case "run-environments": return listBuildSetting!"runEnvironments"(args); 1337 case "pre-generate-environments": return listBuildSetting!"preGenerateEnvironments"(args); 1338 case "post-generate-environments": return listBuildSetting!"postGenerateEnvironments"(args); 1339 case "pre-build-environments": return listBuildSetting!"preBuildEnvironments"(args); 1340 case "post-build-environments": return listBuildSetting!"postBuildEnvironments"(args); 1341 case "pre-run-environments": return listBuildSetting!"preRunEnvironments"(args); 1342 case "post-run-environments": return listBuildSetting!"postRunEnvironments"(args); 1343 case "requirements": return listBuildSetting!"requirements"(args); 1344 case "options": return listBuildSetting!"options"(args); 1345 case "default-config": return [getDefaultConfiguration(settings.platform)]; 1346 case "configs": return configurations; 1347 case "default-build": return [builds[0]]; 1348 case "builds": return builds; 1349 1350 default: 1351 enforce(false, "--data="~requestedData~ 1352 " is not a valid option. See 'dub describe --help' for accepted --data= values."); 1353 } 1354 1355 assert(0); 1356 } 1357 1358 /// Outputs requested data for the project, optionally including its dependencies. 1359 string[] listBuildSettings(GeneratorSettings settings, string[] requestedData, ListBuildSettingsFormat list_type) 1360 { 1361 import dub.compilers.utils : isLinkerFile; 1362 1363 auto projectDescription = describe(settings); 1364 auto configs = getPackageConfigs(settings.platform, settings.config); 1365 PackageDescription packageDescription; 1366 foreach (pack; projectDescription.packages) { 1367 if (pack.name == projectDescription.rootPackage) 1368 packageDescription = pack; 1369 } 1370 1371 if (projectDescription.rootPackage in projectDescription.targetLookup) { 1372 // Copy linker files from sourceFiles to linkerFiles 1373 auto target = projectDescription.lookupTarget(projectDescription.rootPackage); 1374 foreach (file; target.buildSettings.sourceFiles.filter!(f => isLinkerFile(settings.platform, f))) 1375 target.buildSettings.addLinkerFiles(file); 1376 1377 // Remove linker files from sourceFiles 1378 target.buildSettings.sourceFiles = 1379 target.buildSettings.sourceFiles 1380 .filter!(a => !isLinkerFile(settings.platform, a)) 1381 .array(); 1382 projectDescription.lookupTarget(projectDescription.rootPackage) = target; 1383 } 1384 1385 Compiler compiler; 1386 bool no_escape; 1387 final switch (list_type) with (ListBuildSettingsFormat) { 1388 case list: break; 1389 case listNul: no_escape = true; break; 1390 case commandLine: compiler = settings.compiler; break; 1391 case commandLineNul: compiler = settings.compiler; no_escape = true; break; 1392 1393 } 1394 1395 auto result = requestedData 1396 .map!(dataName => listBuildSetting(settings, configs, projectDescription, dataName, compiler, no_escape)); 1397 1398 final switch (list_type) with (ListBuildSettingsFormat) { 1399 case list: return result.map!(l => l.join("\n")).array(); 1400 case listNul: return result.map!(l => l.join("\0")).array; 1401 case commandLine: return result.map!(l => l.join(" ")).array; 1402 case commandLineNul: return result.map!(l => l.join("\0")).array; 1403 } 1404 } 1405 1406 /** Saves the currently selected dependency versions to disk. 1407 1408 The selections will be written to a file named 1409 `SelectedVersions.defaultFile` ("dub.selections.json") within the 1410 directory of the root package. Any existing file will get overwritten. 1411 */ 1412 void saveSelections() 1413 { 1414 assert(m_selections !is null, "Cannot save selections for non-disk based project (has no selections)."); 1415 const name = PackageName(m_rootPackage.basePackage.name); 1416 if (m_selections.hasSelectedVersion(name)) 1417 m_selections.deselectVersion(name); 1418 this.m_packageManager.writeSelections( 1419 this.m_rootPackage, this.m_selections.m_selections, 1420 this.m_selections.dirty); 1421 } 1422 1423 deprecated bool isUpgradeCacheUpToDate() 1424 { 1425 return false; 1426 } 1427 1428 deprecated Dependency[string] getUpgradeCache() 1429 { 1430 return null; 1431 } 1432 } 1433 1434 1435 /// Determines the output format used for `Project.listBuildSettings`. 1436 enum ListBuildSettingsFormat { 1437 list, /// Newline separated list entries 1438 listNul, /// NUL character separated list entries (unescaped) 1439 commandLine, /// Formatted for compiler command line (one data list per line) 1440 commandLineNul, /// NUL character separated list entries (unescaped, data lists separated by two NUL characters) 1441 } 1442 1443 deprecated("Use `dub.packagemanager : PlacementLocation` instead") 1444 public alias PlacementLocation = dub.packagemanager.PlacementLocation; 1445 1446 void processVars(ref BuildSettings dst, in Project project, in Package pack, 1447 BuildSettings settings, in GeneratorSettings gsettings, bool include_target_settings = false) 1448 { 1449 string[string] processVerEnvs(in string[string] targetEnvs, in string[string] defaultEnvs) 1450 { 1451 string[string] retEnv; 1452 foreach (k, v; targetEnvs) 1453 retEnv[k] = v; 1454 foreach (k, v; defaultEnvs) { 1455 if (k !in targetEnvs) 1456 retEnv[k] = v; 1457 } 1458 return processVars(project, pack, gsettings, retEnv); 1459 } 1460 dst.addEnvironments(processVerEnvs(settings.environments, gsettings.buildSettings.environments)); 1461 dst.addBuildEnvironments(processVerEnvs(settings.buildEnvironments, gsettings.buildSettings.buildEnvironments)); 1462 dst.addRunEnvironments(processVerEnvs(settings.runEnvironments, gsettings.buildSettings.runEnvironments)); 1463 dst.addPreGenerateEnvironments(processVerEnvs(settings.preGenerateEnvironments, gsettings.buildSettings.preGenerateEnvironments)); 1464 dst.addPostGenerateEnvironments(processVerEnvs(settings.postGenerateEnvironments, gsettings.buildSettings.postGenerateEnvironments)); 1465 dst.addPreBuildEnvironments(processVerEnvs(settings.preBuildEnvironments, gsettings.buildSettings.preBuildEnvironments)); 1466 dst.addPostBuildEnvironments(processVerEnvs(settings.postBuildEnvironments, gsettings.buildSettings.postBuildEnvironments)); 1467 dst.addPreRunEnvironments(processVerEnvs(settings.preRunEnvironments, gsettings.buildSettings.preRunEnvironments)); 1468 dst.addPostRunEnvironments(processVerEnvs(settings.postRunEnvironments, gsettings.buildSettings.postRunEnvironments)); 1469 1470 auto buildEnvs = [dst.environments, dst.buildEnvironments]; 1471 1472 dst.addDFlags(processVars(project, pack, gsettings, settings.dflags, false, buildEnvs)); 1473 dst.addLFlags(processVars(project, pack, gsettings, settings.lflags, false, buildEnvs)); 1474 dst.addLibs(processVars(project, pack, gsettings, settings.libs, false, buildEnvs)); 1475 dst.addFrameworks(processVars(project, pack, gsettings, settings.frameworks, false, buildEnvs)); 1476 dst.addSourceFiles(processVars!true(project, pack, gsettings, settings.sourceFiles, true, buildEnvs)); 1477 dst.addImportFiles(processVars(project, pack, gsettings, settings.importFiles, true, buildEnvs)); 1478 dst.addStringImportFiles(processVars(project, pack, gsettings, settings.stringImportFiles, true, buildEnvs)); 1479 dst.addInjectSourceFiles(processVars!true(project, pack, gsettings, settings.injectSourceFiles, true, buildEnvs)); 1480 dst.addCopyFiles(processVars(project, pack, gsettings, settings.copyFiles, true, buildEnvs)); 1481 dst.addExtraDependencyFiles(processVars(project, pack, gsettings, settings.extraDependencyFiles, true, buildEnvs)); 1482 dst.addVersions(processVars(project, pack, gsettings, settings.versions, false, buildEnvs)); 1483 dst.addDebugVersions(processVars(project, pack, gsettings, settings.debugVersions, false, buildEnvs)); 1484 dst.addVersionFilters(processVars(project, pack, gsettings, settings.versionFilters, false, buildEnvs)); 1485 dst.addDebugVersionFilters(processVars(project, pack, gsettings, settings.debugVersionFilters, false, buildEnvs)); 1486 dst.addImportPaths(processVars(project, pack, gsettings, settings.importPaths, true, buildEnvs)); 1487 dst.addCImportPaths(processVars(project, pack, gsettings, settings.cImportPaths, true, buildEnvs)); 1488 dst.addStringImportPaths(processVars(project, pack, gsettings, settings.stringImportPaths, true, buildEnvs)); 1489 dst.addRequirements(settings.requirements); 1490 dst.addOptions(settings.options); 1491 1492 // commands are substituted in dub.generators.generator : runBuildCommands 1493 dst.addPreGenerateCommands(settings.preGenerateCommands); 1494 dst.addPostGenerateCommands(settings.postGenerateCommands); 1495 dst.addPreBuildCommands(settings.preBuildCommands); 1496 dst.addPostBuildCommands(settings.postBuildCommands); 1497 dst.addPreRunCommands(settings.preRunCommands); 1498 dst.addPostRunCommands(settings.postRunCommands); 1499 1500 if (include_target_settings) { 1501 dst.targetType = settings.targetType; 1502 dst.targetPath = processVars(settings.targetPath, project, pack, gsettings, true, buildEnvs); 1503 dst.targetName = settings.targetName; 1504 if (!settings.workingDirectory.empty) 1505 dst.workingDirectory = processVars(settings.workingDirectory, project, pack, gsettings, true, buildEnvs); 1506 if (settings.mainSourceFile.length) 1507 dst.mainSourceFile = processVars(settings.mainSourceFile, project, pack, gsettings, true, buildEnvs); 1508 } 1509 } 1510 1511 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) 1512 { 1513 auto ret = appender!(string[])(); 1514 processVars!glob(ret, project, pack, gsettings, vars, are_paths, extraVers); 1515 return ret.data; 1516 } 1517 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) 1518 { 1519 static if (glob) 1520 alias process = processVarsWithGlob!(Project, Package); 1521 else 1522 alias process = processVars!(Project, Package); 1523 foreach (var; vars) 1524 dst.put(process(var, project, pack, gsettings, are_paths, extraVers)); 1525 } 1526 1527 string processVars(Project, Package)(string var, in Project project, in Package pack, in GeneratorSettings gsettings, bool is_path, in string[string][] extraVers = null) 1528 { 1529 var = var.expandVars!(varName => getVariable(varName, project, pack, gsettings, extraVers)); 1530 if (!is_path) 1531 return var; 1532 auto p = NativePath(var); 1533 if (!p.absolute) 1534 return (pack.path ~ p).toNativeString(); 1535 else 1536 return p.toNativeString(); 1537 } 1538 string[string] processVars(bool glob = false)(in Project project, in Package pack, in GeneratorSettings gsettings, in string[string] vars, in string[string][] extraVers = null) 1539 { 1540 string[string] ret; 1541 processVars!glob(ret, project, pack, gsettings, vars, extraVers); 1542 return ret; 1543 } 1544 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) 1545 { 1546 static if (glob) 1547 alias process = processVarsWithGlob!(Project, Package); 1548 else 1549 alias process = processVars!(Project, Package); 1550 foreach (k, var; vars) 1551 dst[k] = process(var, project, pack, gsettings, false, extraVers); 1552 } 1553 1554 private string[] processVarsWithGlob(Project, Package)(string var, in Project project, in Package pack, in GeneratorSettings gsettings, bool is_path, in string[string][] extraVers) 1555 { 1556 assert(is_path, "can't glob something that isn't a path"); 1557 string res = processVars(var, project, pack, gsettings, is_path, extraVers); 1558 // Find the unglobbed prefix and iterate from there. 1559 size_t i = 0; 1560 size_t sepIdx = 0; 1561 loop: while (i < res.length) { 1562 switch_: switch (res[i]) 1563 { 1564 case '*', '?', '[', '{': break loop; 1565 case '/': sepIdx = i; goto default; 1566 version (Windows) { case '\\': sepIdx = i; goto default; } 1567 default: ++i; break switch_; 1568 } 1569 } 1570 if (i == res.length) //no globbing found in the path 1571 return [res]; 1572 import std.file : dirEntries, SpanMode; 1573 import std.path : buildNormalizedPath, globMatch, isAbsolute, relativePath; 1574 auto cwd = gsettings.toolWorkingDirectory.toNativeString; 1575 auto path = res[0 .. sepIdx]; 1576 bool prependCwd = false; 1577 if (!isAbsolute(path)) 1578 { 1579 prependCwd = true; 1580 path = buildNormalizedPath(cwd, path); 1581 } 1582 1583 return dirEntries(path, SpanMode.depth) 1584 .map!(de => prependCwd 1585 ? de.name.relativePath(cwd) 1586 : de.name) 1587 .filter!(name => globMatch(name, res)) 1588 .array; 1589 } 1590 /// Expand variables using `$VAR_NAME` or `${VAR_NAME}` syntax. 1591 /// `$$` escapes itself and is expanded to a single `$`. 1592 private string expandVars(alias expandVar)(string s) 1593 { 1594 import std.functional : not; 1595 1596 auto result = appender!string; 1597 1598 static bool isVarChar(char c) 1599 { 1600 import std.ascii; 1601 return isAlphaNum(c) || c == '_'; 1602 } 1603 1604 while (true) 1605 { 1606 auto pos = s.indexOf('$'); 1607 if (pos < 0) 1608 { 1609 result.put(s); 1610 return result.data; 1611 } 1612 result.put(s[0 .. pos]); 1613 s = s[pos + 1 .. $]; 1614 enforce(s.length > 0, "Variable name expected at end of string"); 1615 switch (s[0]) 1616 { 1617 case '$': 1618 result.put("$"); 1619 s = s[1 .. $]; 1620 break; 1621 case '{': 1622 pos = s.indexOf('}'); 1623 enforce(pos >= 0, "Could not find '}' to match '${'"); 1624 result.put(expandVar(s[1 .. pos])); 1625 s = s[pos + 1 .. $]; 1626 break; 1627 default: 1628 pos = s.representation.countUntil!(not!isVarChar); 1629 if (pos < 0) 1630 pos = s.length; 1631 result.put(expandVar(s[0 .. pos])); 1632 s = s[pos .. $]; 1633 break; 1634 } 1635 } 1636 } 1637 1638 unittest 1639 { 1640 string[string] vars = 1641 [ 1642 "A" : "a", 1643 "B" : "b", 1644 ]; 1645 1646 string expandVar(string name) { auto p = name in vars; enforce(p, name); return *p; } 1647 1648 assert(expandVars!expandVar("") == ""); 1649 assert(expandVars!expandVar("x") == "x"); 1650 assert(expandVars!expandVar("$$") == "$"); 1651 assert(expandVars!expandVar("x$$") == "x$"); 1652 assert(expandVars!expandVar("$$x") == "$x"); 1653 assert(expandVars!expandVar("$$$$") == "$$"); 1654 assert(expandVars!expandVar("x$A") == "xa"); 1655 assert(expandVars!expandVar("x$$A") == "x$A"); 1656 assert(expandVars!expandVar("$A$B") == "ab"); 1657 assert(expandVars!expandVar("${A}$B") == "ab"); 1658 assert(expandVars!expandVar("$A${B}") == "ab"); 1659 assert(expandVars!expandVar("a${B}") == "ab"); 1660 assert(expandVars!expandVar("${A}b") == "ab"); 1661 1662 import std.exception : assertThrown; 1663 assertThrown(expandVars!expandVar("$")); 1664 assertThrown(expandVars!expandVar("${}")); 1665 assertThrown(expandVars!expandVar("$|")); 1666 assertThrown(expandVars!expandVar("x$")); 1667 assertThrown(expandVars!expandVar("$X")); 1668 assertThrown(expandVars!expandVar("${")); 1669 assertThrown(expandVars!expandVar("${X")); 1670 1671 // https://github.com/dlang/dmd/pull/9275 1672 assert(expandVars!expandVar("$${DUB_EXE:-dub}") == "${DUB_EXE:-dub}"); 1673 } 1674 1675 /// Expands the variables in the input string with the same rules as command 1676 /// variables inside custom dub commands. 1677 /// 1678 /// Params: 1679 /// s = the input string where environment variables in form `$VAR` should be replaced 1680 /// throwIfMissing = if true, throw an exception if the given variable is not found, 1681 /// otherwise replace unknown variables with the empty string. 1682 string expandEnvironmentVariables(string s, bool throwIfMissing = true) 1683 { 1684 import std.process : environment; 1685 1686 return expandVars!((v) { 1687 auto ret = environment.get(v); 1688 if (ret is null && throwIfMissing) 1689 throw new Exception("Specified environment variable `$" ~ v ~ "` is not set"); 1690 return ret; 1691 })(s); 1692 } 1693 1694 // Keep the following list up-to-date if adding more build settings variables. 1695 /// List of variables that can be used in build settings 1696 package(dub) immutable buildSettingsVars = [ 1697 "ARCH", "PLATFORM", "PLATFORM_POSIX", "BUILD_TYPE" 1698 ]; 1699 1700 private string getVariable(Project, Package)(string name, in Project project, in Package pack, in GeneratorSettings gsettings, in string[string][] extraVars = null) 1701 { 1702 import dub.internal.utils : getDUBExePath; 1703 import std.process : environment, escapeShellFileName; 1704 import std.uni : asUpperCase; 1705 1706 NativePath path; 1707 if (name == "PACKAGE_DIR") 1708 path = pack.path; 1709 else if (name == "ROOT_PACKAGE_DIR") 1710 path = project.rootPackage.path; 1711 1712 if (name.endsWith("_PACKAGE_DIR")) { 1713 auto pname = name[0 .. $-12]; 1714 foreach (prj; project.getTopologicalPackageList()) 1715 if (prj.name.asUpperCase.map!(a => a == '-' ? '_' : a).equal(pname)) 1716 { 1717 path = prj.path; 1718 break; 1719 } 1720 } 1721 1722 if (!path.empty) 1723 { 1724 // no trailing slash for clean path concatenation (see #1392) 1725 path.endsWithSlash = false; 1726 return path.toNativeString(); 1727 } 1728 1729 if (name == "DUB") { 1730 return getDUBExePath(gsettings.platform.compilerBinary).toNativeString(); 1731 } 1732 1733 if (name == "ARCH") { 1734 foreach (a; gsettings.platform.architecture) 1735 return a; 1736 return ""; 1737 } 1738 1739 if (name == "PLATFORM") { 1740 import std.algorithm : filter; 1741 foreach (p; gsettings.platform.platform.filter!(p => p != "posix")) 1742 return p; 1743 foreach (p; gsettings.platform.platform) 1744 return p; 1745 return ""; 1746 } 1747 1748 if (name == "PLATFORM_POSIX") { 1749 import std.algorithm : canFind; 1750 if (gsettings.platform.platform.canFind("posix")) 1751 return "posix"; 1752 foreach (p; gsettings.platform.platform) 1753 return p; 1754 return ""; 1755 } 1756 1757 if (name == "BUILD_TYPE") return gsettings.buildType; 1758 1759 if (name == "DFLAGS" || name == "LFLAGS") 1760 { 1761 auto buildSettings = pack.getBuildSettings(gsettings.platform, gsettings.config); 1762 if (name == "DFLAGS") 1763 return join(buildSettings.dflags," "); 1764 else if (name == "LFLAGS") 1765 return join(buildSettings.lflags," "); 1766 } 1767 1768 import std.range; 1769 foreach (aa; retro(extraVars)) 1770 if (auto exvar = name in aa) 1771 return *exvar; 1772 1773 auto envvar = environment.get(name); 1774 if (envvar !is null) return envvar; 1775 1776 throw new Exception("Invalid variable: "~name); 1777 } 1778 1779 1780 unittest 1781 { 1782 static struct MockPackage 1783 { 1784 this(string name) 1785 { 1786 this.name = name; 1787 version (Posix) 1788 path = NativePath("/pkgs/"~name); 1789 else version (Windows) 1790 path = NativePath(`C:\pkgs\`~name); 1791 // see 4d4017c14c, #268, and #1392 for why this all package paths end on slash internally 1792 path.endsWithSlash = true; 1793 } 1794 string name; 1795 NativePath path; 1796 BuildSettings getBuildSettings(in BuildPlatform platform, string config) const 1797 { 1798 return BuildSettings(); 1799 } 1800 } 1801 1802 static struct MockProject 1803 { 1804 MockPackage rootPackage; 1805 inout(MockPackage)[] getTopologicalPackageList() inout 1806 { 1807 return _dependencies; 1808 } 1809 private: 1810 MockPackage[] _dependencies; 1811 } 1812 1813 MockProject proj = { 1814 rootPackage: MockPackage("root"), 1815 _dependencies: [MockPackage("dep1"), MockPackage("dep2")] 1816 }; 1817 auto pack = MockPackage("test"); 1818 GeneratorSettings gsettings; 1819 enum isPath = true; 1820 1821 import std.path : dirSeparator; 1822 1823 static NativePath woSlash(NativePath p) { p.endsWithSlash = false; return p; } 1824 // basic vars 1825 assert(processVars("Hello $PACKAGE_DIR", proj, pack, gsettings, !isPath) == "Hello "~woSlash(pack.path).toNativeString); 1826 assert(processVars("Hello $ROOT_PACKAGE_DIR", proj, pack, gsettings, !isPath) == "Hello "~woSlash(proj.rootPackage.path).toNativeString.chomp(dirSeparator)); 1827 assert(processVars("Hello $DEP1_PACKAGE_DIR", proj, pack, gsettings, !isPath) == "Hello "~woSlash(proj._dependencies[0].path).toNativeString); 1828 // ${VAR} replacements 1829 assert(processVars("Hello ${PACKAGE_DIR}"~dirSeparator~"foobar", proj, pack, gsettings, !isPath) == "Hello "~(pack.path ~ "foobar").toNativeString); 1830 assert(processVars("Hello $PACKAGE_DIR"~dirSeparator~"foobar", proj, pack, gsettings, !isPath) == "Hello "~(pack.path ~ "foobar").toNativeString); 1831 // test with isPath 1832 assert(processVars("local", proj, pack, gsettings, isPath) == (pack.path ~ "local").toNativeString); 1833 assert(processVars("foo/$$ESCAPED", proj, pack, gsettings, isPath) == (pack.path ~ "foo/$ESCAPED").toNativeString); 1834 assert(processVars("$$ESCAPED", proj, pack, gsettings, !isPath) == "$ESCAPED"); 1835 // test other env variables 1836 import std.process : environment; 1837 environment["MY_ENV_VAR"] = "blablabla"; 1838 assert(processVars("$MY_ENV_VAR", proj, pack, gsettings, !isPath) == "blablabla"); 1839 assert(processVars("${MY_ENV_VAR}suffix", proj, pack, gsettings, !isPath) == "blablablasuffix"); 1840 assert(processVars("$MY_ENV_VAR-suffix", proj, pack, gsettings, !isPath) == "blablabla-suffix"); 1841 assert(processVars("$MY_ENV_VAR:suffix", proj, pack, gsettings, !isPath) == "blablabla:suffix"); 1842 assert(processVars("$MY_ENV_VAR$MY_ENV_VAR", proj, pack, gsettings, !isPath) == "blablablablablabla"); 1843 environment.remove("MY_ENV_VAR"); 1844 } 1845 1846 /** 1847 * Holds and stores a set of version selections for package dependencies. 1848 * 1849 * This is the runtime representation of the information contained in 1850 * "dub.selections.json" within a package's directory. 1851 * 1852 * Note that as subpackages share the same version as their main package, 1853 * this class will treat any subpackage reference as a reference to its 1854 * main package. 1855 */ 1856 public class SelectedVersions { 1857 protected { 1858 enum FileVersion = 1; 1859 Selections!1 m_selections; 1860 bool m_dirty = false; // has changes since last save 1861 bool m_bare = true; 1862 } 1863 1864 /// Default file name to use for storing selections. 1865 enum defaultFile = "dub.selections.json"; 1866 1867 /// Constructs a new empty version selection. 1868 public this(uint version_ = FileVersion) @safe pure 1869 { 1870 enforce(version_ == 1, "Unsupported file version"); 1871 this.m_selections = Selections!1(version_); 1872 } 1873 1874 /// Constructs a new non-empty version selection. 1875 public this(Selections!1 data) @safe pure nothrow @nogc 1876 { 1877 this.m_selections = data; 1878 this.m_bare = false; 1879 } 1880 1881 /** Constructs a new non-empty version selection, prefixing relative path 1882 selections with the specified prefix. 1883 1884 To be used in cases where the "dub.selections.json" file isn't located 1885 in the root package directory. 1886 */ 1887 public this(Selections!1 data, NativePath relPathPrefix) 1888 { 1889 this(data); 1890 if (relPathPrefix.empty) return; 1891 foreach (ref dep; m_selections.versions.byValue) { 1892 const depPath = dep.path; 1893 if (!depPath.empty && !depPath.absolute) 1894 dep = Dependency(relPathPrefix ~ depPath); 1895 } 1896 } 1897 1898 /** Constructs a new version selection from JSON data. 1899 1900 The structure of the JSON document must match the contents of the 1901 "dub.selections.json" file. 1902 */ 1903 deprecated("Pass a `dub.recipe.selection : Selected` directly") 1904 this(Json data) 1905 { 1906 deserialize(data); 1907 m_dirty = false; 1908 } 1909 1910 /** Constructs a new version selections from an existing JSON file. 1911 */ 1912 deprecated("JSON deserialization is deprecated") 1913 this(NativePath path) 1914 { 1915 auto json = jsonFromFile(path); 1916 deserialize(json); 1917 m_dirty = false; 1918 m_bare = false; 1919 } 1920 1921 /// Returns a list of names for all packages that have a version selection. 1922 @property string[] selectedPackages() const { return m_selections.versions.keys; } 1923 1924 /// Determines if any changes have been made after loading the selections from a file. 1925 @property bool dirty() const { return m_dirty; } 1926 1927 /// Determine if this set of selections is still empty (but not `clear`ed). 1928 @property bool bare() const { return m_bare && !m_dirty; } 1929 1930 /// Removes all selections. 1931 void clear() 1932 { 1933 m_selections.versions = null; 1934 m_dirty = true; 1935 } 1936 1937 /// Duplicates the set of selected versions from another instance. 1938 void set(SelectedVersions versions) 1939 { 1940 m_selections.fileVersion = versions.m_selections.fileVersion; 1941 m_selections.versions = versions.m_selections.versions.dup; 1942 m_selections.inheritable = versions.m_selections.inheritable; 1943 m_dirty = true; 1944 } 1945 1946 /// Selects a certain version for a specific package. 1947 deprecated("Use the overload that accepts a `PackageName`") 1948 void selectVersion(string package_id, Version version_) 1949 { 1950 const name = PackageName(package_id); 1951 return this.selectVersionInternal(name, Dependency(version_)); 1952 } 1953 1954 /// Ditto 1955 void selectVersion(in PackageName name, Version version_, in IntegrityTag tag = IntegrityTag.init) 1956 { 1957 auto dep = SelectedDependency(Dependency(version_), tag); 1958 if (auto pdep = name.main.toString() in this.m_selections.versions) { 1959 if (*pdep == dep) 1960 return; 1961 } 1962 this.m_selections.versions[name.main.toString()] = dep; 1963 this.m_dirty = true; 1964 } 1965 1966 /// Selects a certain path for a specific package. 1967 deprecated("Use the overload that accepts a `PackageName`") 1968 void selectVersion(string package_id, NativePath path) 1969 { 1970 const name = PackageName(package_id); 1971 return this.selectVersion(name, path); 1972 } 1973 1974 /// Ditto 1975 void selectVersion(in PackageName name, NativePath path) 1976 { 1977 const dep = Dependency(path); 1978 this.selectVersionInternal(name, dep); 1979 } 1980 1981 /// Selects a certain Git reference for a specific package. 1982 deprecated("Use the overload that accepts a `PackageName`") 1983 void selectVersion(string package_id, Repository repository) 1984 { 1985 const name = PackageName(package_id); 1986 return this.selectVersion(name, repository); 1987 } 1988 1989 /// Ditto 1990 void selectVersion(in PackageName name, Repository repository) 1991 { 1992 const dep = Dependency(repository); 1993 this.selectVersionInternal(name, dep); 1994 } 1995 1996 /// Internal implementation of selectVersion 1997 private void selectVersionInternal(in PackageName name, in Dependency dep) 1998 { 1999 if (auto pdep = name.main.toString() in m_selections.versions) { 2000 if (*pdep == dep) 2001 return; 2002 } 2003 m_selections.versions[name.main.toString()] = dep; 2004 m_dirty = true; 2005 } 2006 2007 deprecated("Move `spec` inside of the `repository` parameter and call `selectVersion`") 2008 void selectVersionWithRepository(string package_id, Repository repository, string spec) 2009 { 2010 this.selectVersion(package_id, Repository(repository.remote(), spec)); 2011 } 2012 2013 /// Removes the selection for a particular package. 2014 deprecated("Use the overload that accepts a `PackageName`") 2015 void deselectVersion(string package_id) 2016 { 2017 const n = PackageName(package_id); 2018 this.deselectVersion(n); 2019 } 2020 2021 /// Ditto 2022 void deselectVersion(in PackageName name) 2023 { 2024 m_selections.versions.remove(name.main.toString()); 2025 m_dirty = true; 2026 } 2027 2028 /// Determines if a particular package has a selection set. 2029 deprecated("Use the overload that accepts a `PackageName`") 2030 bool hasSelectedVersion(string packageId) const { 2031 const name = PackageName(packageId); 2032 return this.hasSelectedVersion(name); 2033 } 2034 2035 /// Ditto 2036 bool hasSelectedVersion(in PackageName name) const 2037 { 2038 return (name.main.toString() in m_selections.versions) !is null; 2039 } 2040 2041 /** Returns the selection for a particular package. 2042 2043 Note that the returned `Dependency` can either have the 2044 `Dependency.path` property set to a non-empty value, in which case this 2045 is a path based selection, or its `Dependency.version_` property is 2046 valid and it is a version selection. 2047 */ 2048 deprecated("Use the overload that accepts a `PackageName`") 2049 Dependency getSelectedVersion(string packageId) const 2050 { 2051 const name = PackageName(packageId); 2052 return this.getSelectedVersion(name); 2053 } 2054 2055 /// Ditto 2056 Dependency getSelectedVersion(in PackageName name) const 2057 { 2058 enforce(hasSelectedVersion(name)); 2059 return m_selections.versions[name.main.toString()]; 2060 } 2061 2062 /// Returns: The `IntegrityTag` associated to the version, or `.init` if none 2063 IntegrityTag getIntegrityTag(in PackageName name) const 2064 { 2065 if (auto ptr = name.main.toString() in this.m_selections.versions) 2066 return (*ptr).integrity; 2067 return typeof(return).init; 2068 } 2069 2070 /** Stores the selections to disk. 2071 2072 The target file will be written in JSON format. Usually, `defaultFile` 2073 should be used as the file name and the directory should be the root 2074 directory of the project's root package. 2075 */ 2076 deprecated("Use `PackageManager.writeSelections` to write a `SelectionsFile`") 2077 void save(NativePath path) 2078 { 2079 path.writeFile(PackageManager.selectionsToString(this.m_selections)); 2080 m_dirty = false; 2081 m_bare = false; 2082 } 2083 2084 deprecated("Use `dub.dependency : Dependency.toJson(true)`") 2085 static Json dependencyToJson(Dependency d) 2086 { 2087 return d.toJson(true); 2088 } 2089 2090 deprecated("JSON deserialization is deprecated") 2091 static Dependency dependencyFromJson(Json j) 2092 { 2093 if (j.type == Json.Type..string) 2094 return Dependency(Version(j.get!string)); 2095 else if (j.type == Json.Type.object && "path" in j) 2096 return Dependency(NativePath(j["path"].get!string)); 2097 else if (j.type == Json.Type.object && "repository" in j) 2098 return Dependency(Repository(j["repository"].get!string, 2099 enforce("version" in j, "Expected \"version\" field in repository version object").get!string)); 2100 else throw new Exception(format("Unexpected type for dependency: %s", j)); 2101 } 2102 2103 deprecated("JSON serialization is deprecated") 2104 Json serialize() const { 2105 return PackageManager.selectionsToJSON(this.m_selections); 2106 } 2107 2108 deprecated("JSON deserialization is deprecated") 2109 private void deserialize(Json json) 2110 { 2111 const fileVersion = json["fileVersion"].get!int; 2112 enforce(fileVersion == FileVersion, "Mismatched dub.selections.json version: " ~ to!string(fileVersion) ~ " vs. " ~ to!string(FileVersion)); 2113 clear(); 2114 m_selections.fileVersion = fileVersion; 2115 scope(failure) clear(); 2116 if (auto p = "inheritable" in json) 2117 m_selections.inheritable = p.get!bool; 2118 foreach (string p, dep; json["versions"]) 2119 m_selections.versions[p] = dependencyFromJson(dep); 2120 } 2121 } 2122 2123 /// The template code from which the test runner is generated 2124 private immutable TestRunnerTemplate = q{ 2125 deprecated // allow silently using deprecated symbols 2126 module dub_test_root; 2127 2128 import std.typetuple; 2129 2130 %-(static import %s; 2131 %); 2132 2133 alias allModules = TypeTuple!( 2134 %-(%s, %) 2135 ); 2136 2137 %s 2138 }; 2139 2140 /// The default test runner that gets used if none is provided 2141 private immutable DefaultTestRunnerCode = q{ 2142 version(D_BetterC) { 2143 extern(C) int main() { 2144 foreach (module_; allModules) { 2145 foreach (unitTest; __traits(getUnitTests, module_)) { 2146 unitTest(); 2147 } 2148 } 2149 import core.stdc.stdio : puts; 2150 puts("All unit tests have been run successfully."); 2151 return 0; 2152 } 2153 } else { 2154 void main() { 2155 version (D_Coverage) { 2156 } else { 2157 import std.stdio : writeln; 2158 writeln("All unit tests have been run successfully."); 2159 } 2160 } 2161 shared static this() { 2162 version (Have_tested) { 2163 import tested; 2164 import core.runtime; 2165 import std.exception; 2166 Runtime.moduleUnitTester = () => true; 2167 enforce(runUnitTests!allModules(new ConsoleTestResultWriter), "Unit tests failed."); 2168 } 2169 } 2170 } 2171 };