1 /** 2 A package manager. 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.dub; 9 10 import dub.compilers.compiler; 11 import dub.dependency; 12 import dub.dependencyresolver; 13 import dub.internal.utils; 14 import dub.internal.vibecompat.core.file; 15 import dub.internal.vibecompat.data.json; 16 import dub.internal.vibecompat.inet.url; 17 import dub.internal.logging; 18 import dub.package_; 19 import dub.packagemanager; 20 import dub.packagesuppliers; 21 import dub.project; 22 import dub.generators.generator; 23 import dub.init; 24 25 import std.algorithm; 26 import std.array : array, replace; 27 import std.conv : text, to; 28 import std.encoding : sanitize; 29 import std.exception : enforce; 30 import std.file; 31 import std.process : environment; 32 import std.range : assumeSorted, empty; 33 import std.string; 34 35 // Set output path and options for coverage reports 36 version (DigitalMars) version (D_Coverage) 37 { 38 shared static this() 39 { 40 import core.runtime, std.file, std.path, std.stdio; 41 dmd_coverSetMerge(true); 42 auto path = buildPath(dirName(thisExePath()), "../cov"); 43 if (!path.exists) 44 mkdir(path); 45 dmd_coverDestPath(path); 46 } 47 } 48 49 static this() 50 { 51 import dub.compilers.dmd : DMDCompiler; 52 import dub.compilers.gdc : GDCCompiler; 53 import dub.compilers.ldc : LDCCompiler; 54 registerCompiler(new DMDCompiler); 55 registerCompiler(new GDCCompiler); 56 registerCompiler(new LDCCompiler); 57 } 58 59 deprecated("use defaultRegistryURLs") enum defaultRegistryURL = defaultRegistryURLs[0]; 60 61 /// The URL to the official package registry and it's default fallback registries. 62 static immutable string[] defaultRegistryURLs = [ 63 "https://code.dlang.org/", 64 "https://codemirror.dlang.org/", 65 "https://dub.bytecraft.nl/", 66 "https://code-mirror.dlang.io/", 67 ]; 68 69 /** Returns a default list of package suppliers. 70 71 This will contain a single package supplier that points to the official 72 package registry. 73 74 See_Also: `defaultRegistryURLs` 75 */ 76 PackageSupplier[] defaultPackageSuppliers() 77 { 78 logDiagnostic("Using dub registry url '%s'", defaultRegistryURLs[0]); 79 return [new FallbackPackageSupplier(defaultRegistryURLs.map!getRegistryPackageSupplier.array)]; 80 } 81 82 /** Returns a registry package supplier according to protocol. 83 84 Allowed protocols are dub+http(s):// and maven+http(s)://. 85 */ 86 PackageSupplier getRegistryPackageSupplier(string url) 87 { 88 switch (url.startsWith("dub+", "mvn+", "file://")) 89 { 90 case 1: 91 return new RegistryPackageSupplier(URL(url[4..$])); 92 case 2: 93 return new MavenRegistryPackageSupplier(URL(url[4..$])); 94 case 3: 95 return new FileSystemPackageSupplier(NativePath(url[7..$])); 96 default: 97 return new RegistryPackageSupplier(URL(url)); 98 } 99 } 100 101 unittest 102 { 103 auto dubRegistryPackageSupplier = getRegistryPackageSupplier("dub+https://code.dlang.org"); 104 assert(dubRegistryPackageSupplier.description.canFind(" https://code.dlang.org")); 105 106 dubRegistryPackageSupplier = getRegistryPackageSupplier("https://code.dlang.org"); 107 assert(dubRegistryPackageSupplier.description.canFind(" https://code.dlang.org")); 108 109 auto mavenRegistryPackageSupplier = getRegistryPackageSupplier("mvn+http://localhost:8040/maven/libs-release/dubpackages"); 110 assert(mavenRegistryPackageSupplier.description.canFind(" http://localhost:8040/maven/libs-release/dubpackages")); 111 112 auto fileSystemPackageSupplier = getRegistryPackageSupplier("file:///etc/dubpackages"); 113 assert(fileSystemPackageSupplier.description.canFind(" " ~ NativePath("/etc/dubpackages").toNativeString)); 114 } 115 116 /** Provides a high-level entry point for DUB's functionality. 117 118 This class provides means to load a certain project (a root package with 119 all of its dependencies) and to perform high-level operations as found in 120 the command line interface. 121 */ 122 class Dub { 123 private { 124 bool m_dryRun = false; 125 PackageManager m_packageManager; 126 PackageSupplier[] m_packageSuppliers; 127 NativePath m_rootPath; 128 SpecialDirs m_dirs; 129 UserConfiguration m_config; 130 Project m_project; 131 string m_defaultCompiler; 132 } 133 134 /** The default placement location of fetched packages. 135 136 This property can be altered, so that packages which are downloaded as part 137 of the normal upgrade process are stored in a certain location. This is 138 how the "--local" and "--system" command line switches operate. 139 */ 140 PlacementLocation defaultPlacementLocation = PlacementLocation.user; 141 142 143 /** Initializes the instance for use with a specific root package. 144 145 Note that a package still has to be loaded using one of the 146 `loadPackage` overloads. 147 148 Params: 149 root_path = Path to the root package 150 additional_package_suppliers = A list of package suppliers to try 151 before the suppliers found in the configurations files and the 152 `defaultPackageSuppliers`. 153 skip_registry = Can be used to skip using the configured package 154 suppliers, as well as the default suppliers. 155 */ 156 this(string root_path = ".", PackageSupplier[] additional_package_suppliers = null, 157 SkipPackageSuppliers skip_registry = SkipPackageSuppliers.none) 158 { 159 m_rootPath = NativePath(root_path); 160 if (!m_rootPath.absolute) m_rootPath = NativePath(getcwd()) ~ m_rootPath; 161 162 init(); 163 164 m_packageSuppliers = this.computePkgSuppliers(additional_package_suppliers, 165 skip_registry, environment.get("DUB_REGISTRY", null)); 166 m_packageManager = new PackageManager(m_rootPath, m_dirs.localRepository, m_dirs.systemSettings); 167 168 auto ccps = m_config.customCachePaths; 169 if (ccps.length) 170 m_packageManager.customCachePaths = ccps; 171 172 // TODO: Move this environment read out of the ctor 173 if (auto p = environment.get("DUBPATH")) { 174 version(Windows) enum pathsep = ";"; 175 else enum pathsep = ":"; 176 NativePath[] paths = p.split(pathsep) 177 .map!(p => NativePath(p))().array(); 178 m_packageManager.searchPath = paths; 179 } 180 } 181 182 /** Initializes the instance with a single package search path, without 183 loading a package. 184 185 This constructor corresponds to the "--bare" option of the command line 186 interface. 187 188 Params: 189 root = The root path of the Dub instance itself. 190 pkg_root = The root of the location where packages are located 191 Only packages under this location will be accessible. 192 Note that packages at the top levels will be ignored. 193 */ 194 this(NativePath root, NativePath pkg_root) 195 { 196 // Note: We're doing `init()` before setting the `rootPath`, 197 // to prevent `init` from reading the project's settings. 198 init(); 199 this.m_rootPath = root; 200 m_packageManager = new PackageManager(pkg_root); 201 } 202 203 deprecated("Use the overload that takes `(NativePath pkg_root, NativePath root)`") 204 this(NativePath pkg_root) 205 { 206 this(pkg_root, pkg_root); 207 } 208 209 private void init() 210 { 211 this.m_dirs = SpecialDirs.make(); 212 this.loadConfig(); 213 this.determineDefaultCompiler(); 214 } 215 216 /** 217 * Load user configuration for this instance 218 * 219 * This can be overloaded in child classes to prevent library / unittest 220 * dub from doing any kind of file IO. 221 */ 222 protected void loadConfig() 223 { 224 import configy.Read; 225 226 void readSettingsFile (NativePath path_) 227 { 228 // TODO: Remove `StrictMode.Warn` after v1.40 release 229 // The default is to error, but as the previous parser wasn't 230 // complaining, we should first warn the user. 231 const path = path_.toNativeString(); 232 if (path.exists) { 233 auto newConf = parseConfigFileSimple!UserConfiguration(path, StrictMode.Warn); 234 if (!newConf.isNull()) 235 this.m_config = this.m_config.merge(newConf.get()); 236 } 237 } 238 239 const dubFolderPath = NativePath(thisExePath).parentPath; 240 241 // override default userSettings + localRepository if a $DPATH or 242 // $DUB_HOME environment variable is set. 243 bool overrideDubHomeFromEnv; 244 { 245 string dubHome = environment.get("DUB_HOME"); 246 if (!dubHome.length) { 247 auto dpath = environment.get("DPATH"); 248 if (dpath.length) 249 dubHome = (NativePath(dpath) ~ "dub/").toNativeString(); 250 251 } 252 if (dubHome.length) { 253 overrideDubHomeFromEnv = true; 254 255 m_dirs.userSettings = NativePath(dubHome); 256 m_dirs.localRepository = m_dirs.userSettings; 257 } 258 } 259 260 readSettingsFile(m_dirs.systemSettings ~ "settings.json"); 261 readSettingsFile(dubFolderPath ~ "../etc/dub/settings.json"); 262 version (Posix) { 263 if (dubFolderPath.absolute && dubFolderPath.startsWith(NativePath("usr"))) 264 readSettingsFile(NativePath("/etc/dub/settings.json")); 265 } 266 267 // Override user + local package path from system / binary settings 268 // Then continues loading local settings from these folders. (keeping 269 // global /etc/dub/settings.json settings intact) 270 // 271 // Don't use it if either $DPATH or $DUB_HOME are set, as environment 272 // variables usually take precedence over configuration. 273 if (!overrideDubHomeFromEnv && this.m_config.dubHome.set) { 274 m_dirs.userSettings = NativePath(this.m_config.dubHome.expandEnvironmentVariables); 275 } 276 277 // load user config: 278 readSettingsFile(m_dirs.userSettings ~ "settings.json"); 279 280 // load per-package config: 281 if (!this.m_rootPath.empty) 282 readSettingsFile(this.m_rootPath ~ "dub.settings.json"); 283 284 // same as userSettings above, but taking into account the 285 // config loaded from user settings and per-package config as well. 286 if (!overrideDubHomeFromEnv && this.m_config.dubHome.set) { 287 m_dirs.localRepository = NativePath(this.m_config.dubHome.expandEnvironmentVariables); 288 } 289 } 290 291 unittest 292 { 293 scope (exit) environment.remove("DUB_REGISTRY"); 294 auto dub = new TestDub(".", null, SkipPackageSuppliers.configured); 295 assert(dub.m_packageSuppliers.length == 0); 296 environment["DUB_REGISTRY"] = "http://example.com/"; 297 dub = new TestDub(".", null, SkipPackageSuppliers.configured); 298 assert(dub.m_packageSuppliers.length == 1); 299 environment["DUB_REGISTRY"] = "http://example.com/;http://foo.com/"; 300 dub = new TestDub(".", null, SkipPackageSuppliers.configured); 301 assert(dub.m_packageSuppliers.length == 2); 302 dub = new TestDub(".", [new RegistryPackageSupplier(URL("http://bar.com/"))], SkipPackageSuppliers.configured); 303 assert(dub.m_packageSuppliers.length == 3); 304 } 305 306 /** Get the list of package suppliers. 307 308 Params: 309 additional_package_suppliers = A list of package suppliers to try 310 before the suppliers found in the configurations files and the 311 `defaultPackageSuppliers`. 312 skip_registry = Can be used to skip using the configured package 313 suppliers, as well as the default suppliers. 314 */ 315 deprecated("This is an implementation detail. " ~ 316 "Use `packageSuppliers` to get the computed list of package " ~ 317 "suppliers once a `Dub` instance has been constructed.") 318 public PackageSupplier[] getPackageSuppliers(PackageSupplier[] additional_package_suppliers, SkipPackageSuppliers skip_registry) 319 { 320 return this.computePkgSuppliers(additional_package_suppliers, skip_registry, environment.get("DUB_REGISTRY", null)); 321 } 322 323 /// Ditto 324 private PackageSupplier[] computePkgSuppliers( 325 PackageSupplier[] additional_package_suppliers, SkipPackageSuppliers skip_registry, 326 string dub_registry_var) 327 { 328 PackageSupplier[] ps = additional_package_suppliers; 329 330 if (skip_registry < SkipPackageSuppliers.all) 331 { 332 ps ~= dub_registry_var 333 .splitter(";") 334 .map!(url => getRegistryPackageSupplier(url)) 335 .array; 336 } 337 338 if (skip_registry < SkipPackageSuppliers.configured) 339 { 340 ps ~= m_config.registryUrls 341 .map!(url => getRegistryPackageSupplier(url)) 342 .array; 343 } 344 345 if (skip_registry < SkipPackageSuppliers.standard) 346 ps ~= defaultPackageSuppliers(); 347 348 return ps; 349 } 350 351 /// ditto 352 deprecated("This is an implementation detail. " ~ 353 "Use `packageSuppliers` to get the computed list of package " ~ 354 "suppliers once a `Dub` instance has been constructed.") 355 public PackageSupplier[] getPackageSuppliers(PackageSupplier[] additional_package_suppliers) 356 { 357 return getPackageSuppliers(additional_package_suppliers, m_config.skipRegistry); 358 } 359 360 unittest 361 { 362 auto dub = new TestDub(); 363 364 assert(dub.computePkgSuppliers(null, SkipPackageSuppliers.none, null).length == 1); 365 assert(dub.computePkgSuppliers(null, SkipPackageSuppliers.configured, null).length == 0); 366 assert(dub.computePkgSuppliers(null, SkipPackageSuppliers.standard, null).length == 0); 367 368 assert(dub.computePkgSuppliers(null, SkipPackageSuppliers.standard, "http://example.com/") 369 .length == 1); 370 } 371 372 @property bool dryRun() const { return m_dryRun; } 373 @property void dryRun(bool v) { m_dryRun = v; } 374 375 /** Returns the root path (usually the current working directory). 376 */ 377 @property NativePath rootPath() const { return m_rootPath; } 378 /// ditto 379 deprecated("Changing the root path is deprecated as it has non-obvious pitfalls " ~ 380 "(e.g. settings aren't reloaded). Instantiate a new `Dub` instead") 381 @property void rootPath(NativePath root_path) 382 { 383 m_rootPath = root_path; 384 if (!m_rootPath.absolute) m_rootPath = NativePath(getcwd()) ~ m_rootPath; 385 } 386 387 /// Returns the name listed in the dub.json of the current 388 /// application. 389 @property string projectName() const { return m_project.name; } 390 391 @property NativePath projectPath() const { return this.m_project.rootPackage.path; } 392 393 @property string[] configurations() const { return m_project.configurations; } 394 395 @property inout(PackageManager) packageManager() inout { return m_packageManager; } 396 397 @property inout(Project) project() inout { return m_project; } 398 399 @property inout(PackageSupplier)[] packageSuppliers() inout { return m_packageSuppliers; } 400 401 /** Returns the default compiler binary to use for building D code. 402 403 If set, the "defaultCompiler" field of the DUB user or system 404 configuration file will be used. Otherwise the PATH environment variable 405 will be searched for files named "dmd", "gdc", "gdmd", "ldc2", "ldmd2" 406 (in that order, taking into account operating system specific file 407 extensions) and the first match is returned. If no match is found, "dmd" 408 will be used. 409 */ 410 @property string defaultCompiler() const { return m_defaultCompiler; } 411 412 /** Returns the default architecture to use for building D code. 413 414 If set, the "defaultArchitecture" field of the DUB user or system 415 configuration file will be used. Otherwise null will be returned. 416 */ 417 @property string defaultArchitecture() const { return this.m_config.defaultArchitecture; } 418 419 /** Returns the default low memory option to use for building D code. 420 421 If set, the "defaultLowMemory" field of the DUB user or system 422 configuration file will be used. Otherwise false will be returned. 423 */ 424 @property bool defaultLowMemory() const { return this.m_config.defaultLowMemory; } 425 426 @property const(string[string]) defaultEnvironments() const { return this.m_config.defaultEnvironments; } 427 @property const(string[string]) defaultBuildEnvironments() const { return this.m_config.defaultBuildEnvironments; } 428 @property const(string[string]) defaultRunEnvironments() const { return this.m_config.defaultRunEnvironments; } 429 @property const(string[string]) defaultPreGenerateEnvironments() const { return this.m_config.defaultPreGenerateEnvironments; } 430 @property const(string[string]) defaultPostGenerateEnvironments() const { return this.m_config.defaultPostGenerateEnvironments; } 431 @property const(string[string]) defaultPreBuildEnvironments() const { return this.m_config.defaultPreBuildEnvironments; } 432 @property const(string[string]) defaultPostBuildEnvironments() const { return this.m_config.defaultPostBuildEnvironments; } 433 @property const(string[string]) defaultPreRunEnvironments() const { return this.m_config.defaultPreRunEnvironments; } 434 @property const(string[string]) defaultPostRunEnvironments() const { return this.m_config.defaultPostRunEnvironments; } 435 436 /** Loads the package that resides within the configured `rootPath`. 437 */ 438 void loadPackage() 439 { 440 loadPackage(m_rootPath); 441 } 442 443 /// Loads the package from the specified path as the main project package. 444 void loadPackage(NativePath path) 445 { 446 m_project = new Project(m_packageManager, path); 447 } 448 449 /// Loads a specific package as the main project package (can be a sub package) 450 void loadPackage(Package pack) 451 { 452 m_project = new Project(m_packageManager, pack); 453 } 454 455 /** Loads a single file package. 456 457 Single-file packages are D files that contain a package receipe comment 458 at their top. A recipe comment must be a nested `/+ ... +/` style 459 comment, containing the virtual recipe file name and a colon, followed by the 460 recipe contents (what would normally be in dub.sdl/dub.json). 461 462 Example: 463 --- 464 /+ dub.sdl: 465 name "test" 466 dependency "vibe-d" version="~>0.7.29" 467 +/ 468 import vibe.http.server; 469 470 void main() 471 { 472 auto settings = new HTTPServerSettings; 473 settings.port = 8080; 474 listenHTTP(settings, &hello); 475 } 476 477 void hello(HTTPServerRequest req, HTTPServerResponse res) 478 { 479 res.writeBody("Hello, World!"); 480 } 481 --- 482 483 The script above can be invoked with "dub --single test.d". 484 */ 485 void loadSingleFilePackage(NativePath path) 486 { 487 import dub.recipe.io : parsePackageRecipe; 488 import std.file : mkdirRecurse, readText; 489 import std.path : baseName, stripExtension; 490 491 path = makeAbsolute(path); 492 493 string file_content = readText(path.toNativeString()); 494 495 if (file_content.startsWith("#!")) { 496 auto idx = file_content.indexOf('\n'); 497 enforce(idx > 0, "The source fine doesn't contain anything but a shebang line."); 498 file_content = file_content[idx+1 .. $]; 499 } 500 501 file_content = file_content.strip(); 502 503 string recipe_content; 504 505 if (file_content.startsWith("/+")) { 506 file_content = file_content[2 .. $]; 507 auto idx = file_content.indexOf("+/"); 508 enforce(idx >= 0, "Missing \"+/\" to close comment."); 509 recipe_content = file_content[0 .. idx].strip(); 510 } else throw new Exception("The source file must start with a recipe comment."); 511 512 auto nidx = recipe_content.indexOf('\n'); 513 514 auto idx = recipe_content.indexOf(':'); 515 enforce(idx > 0 && (nidx < 0 || nidx > idx), 516 "The first line of the recipe comment must list the recipe file name followed by a colon (e.g. \"/+ dub.sdl:\")."); 517 auto recipe_filename = recipe_content[0 .. idx]; 518 recipe_content = recipe_content[idx+1 .. $]; 519 auto recipe_default_package_name = path.toString.baseName.stripExtension.strip; 520 521 auto recipe = parsePackageRecipe(recipe_content, recipe_filename, null, recipe_default_package_name); 522 enforce(recipe.buildSettings.sourceFiles.length == 0, "Single-file packages are not allowed to specify source files."); 523 enforce(recipe.buildSettings.sourcePaths.length == 0, "Single-file packages are not allowed to specify source paths."); 524 enforce(recipe.buildSettings.importPaths.length == 0, "Single-file packages are not allowed to specify import paths."); 525 recipe.buildSettings.sourceFiles[""] = [path.toNativeString()]; 526 recipe.buildSettings.sourcePaths[""] = []; 527 recipe.buildSettings.importPaths[""] = []; 528 recipe.buildSettings.mainSourceFile = path.toNativeString(); 529 if (recipe.buildSettings.targetType == TargetType.autodetect) 530 recipe.buildSettings.targetType = TargetType.executable; 531 532 auto pack = new Package(recipe, path.parentPath, null, "~master"); 533 loadPackage(pack); 534 } 535 /// ditto 536 void loadSingleFilePackage(string path) 537 { 538 loadSingleFilePackage(NativePath(path)); 539 } 540 541 /** Gets the default configuration for a particular build platform. 542 543 This forwards to `Project.getDefaultConfiguration` and requires a 544 project to be loaded. 545 */ 546 string getDefaultConfiguration(in BuildPlatform platform, bool allow_non_library_configs = true) const { return m_project.getDefaultConfiguration(platform, allow_non_library_configs); } 547 548 /** Attempts to upgrade the dependency selection of the loaded project. 549 550 Params: 551 options = Flags that control how the upgrade is carried out 552 packages_to_upgrade = Optional list of packages. If this list 553 contains one or more packages, only those packages will 554 be upgraded. Otherwise, all packages will be upgraded at 555 once. 556 */ 557 void upgrade(UpgradeOptions options, string[] packages_to_upgrade = null) 558 { 559 // clear non-existent version selections 560 if (!(options & UpgradeOptions.upgrade)) { 561 next_pack: 562 foreach (p; m_project.selections.selectedPackages) { 563 auto dep = m_project.selections.getSelectedVersion(p); 564 if (!dep.path.empty) { 565 auto path = dep.path; 566 if (!path.absolute) path = this.rootPath ~ path; 567 try if (m_packageManager.getOrLoadPackage(path)) continue; 568 catch (Exception e) { logDebug("Failed to load path based selection: %s", e.toString().sanitize); } 569 } else if (!dep.repository.empty) { 570 if (m_packageManager.loadSCMPackage(getBasePackageName(p), dep.repository)) 571 continue; 572 } else { 573 if (m_packageManager.getPackage(p, dep.version_)) continue; 574 foreach (ps; m_packageSuppliers) { 575 try { 576 auto versions = ps.getVersions(p); 577 if (versions.canFind!(v => dep.matches(v, VersionMatchMode.strict))) 578 continue next_pack; 579 } catch (Exception e) { 580 logWarn("Error querying versions for %s, %s: %s", p, ps.description, e.msg); 581 logDebug("Full error: %s", e.toString().sanitize()); 582 } 583 } 584 } 585 586 logWarn("Selected package %s %s doesn't exist. Using latest matching version instead.", p, dep); 587 m_project.selections.deselectVersion(p); 588 } 589 } 590 591 auto resolver = new DependencyVersionResolver( 592 this, options, m_project.rootPackage, m_project.selections); 593 Dependency[string] versions = resolver.resolve(packages_to_upgrade); 594 595 if (options & UpgradeOptions.dryRun) { 596 bool any = false; 597 string rootbasename = getBasePackageName(m_project.rootPackage.name); 598 599 foreach (p, ver; versions) { 600 if (!ver.path.empty || !ver.repository.empty) continue; 601 602 auto basename = getBasePackageName(p); 603 if (basename == rootbasename) continue; 604 605 if (!m_project.selections.hasSelectedVersion(basename)) { 606 logInfo("Upgrade", Color.cyan, 607 "Package %s would be selected with version %s", basename, ver); 608 any = true; 609 continue; 610 } 611 auto sver = m_project.selections.getSelectedVersion(basename); 612 if (!sver.path.empty || !sver.repository.empty) continue; 613 if (ver.version_ <= sver.version_) continue; 614 logInfo("Upgrade", Color.cyan, 615 "%s would be upgraded from %s to %s.", 616 basename.color(Mode.bold), sver, ver); 617 any = true; 618 } 619 if (any) logInfo("Use \"%s\" to perform those changes", "dub upgrade".color(Mode.bold)); 620 return; 621 } 622 623 foreach (p, ver; versions) { 624 assert(!p.canFind(":"), "Resolved packages contain a sub package!?: "~p); 625 Package pack; 626 if (!ver.path.empty) { 627 try pack = m_packageManager.getOrLoadPackage(ver.path); 628 catch (Exception e) { 629 logDebug("Failed to load path based selection: %s", e.toString().sanitize); 630 continue; 631 } 632 } else if (!ver.repository.empty) { 633 pack = m_packageManager.loadSCMPackage(p, ver.repository); 634 } else { 635 assert(ver.isExactVersion, "Resolved dependency is neither path, nor repository, nor exact version based!?"); 636 pack = m_packageManager.getPackage(p, ver.version_); 637 if (pack && m_packageManager.isManagedPackage(pack) 638 && ver.version_.isBranch && (options & UpgradeOptions.upgrade) != 0) 639 { 640 // TODO: only re-install if there is actually a new commit available 641 logInfo("Re-installing branch based dependency %s %s", p, ver.toString()); 642 m_packageManager.remove(pack); 643 pack = null; 644 } 645 } 646 647 FetchOptions fetchOpts; 648 fetchOpts |= (options & UpgradeOptions.preRelease) != 0 ? FetchOptions.usePrerelease : FetchOptions.none; 649 if (!pack) fetch(p, ver.version_, defaultPlacementLocation, fetchOpts, "getting selected version"); 650 if ((options & UpgradeOptions.select) && p != m_project.rootPackage.name) { 651 if (!ver.repository.empty) { 652 m_project.selections.selectVersion(p, ver.repository); 653 } else if (ver.path.empty) { 654 m_project.selections.selectVersion(p, ver.version_); 655 } else { 656 NativePath relpath = ver.path; 657 if (relpath.absolute) relpath = relpath.relativeTo(m_project.rootPackage.path); 658 m_project.selections.selectVersion(p, relpath); 659 } 660 } 661 } 662 663 string[] missingDependenciesBeforeReinit = m_project.missingDependencies; 664 m_project.reinit(); 665 666 if (!m_project.hasAllDependencies) { 667 auto resolvedDependencies = setDifference( 668 assumeSorted(missingDependenciesBeforeReinit), 669 assumeSorted(m_project.missingDependencies) 670 ); 671 if (!resolvedDependencies.empty) 672 upgrade(options, m_project.missingDependencies); 673 } 674 675 if ((options & UpgradeOptions.select) && !(options & (UpgradeOptions.noSaveSelections | UpgradeOptions.dryRun))) 676 m_project.saveSelections(); 677 } 678 679 /** Generate project files for a specified generator. 680 681 Any existing project files will be overridden. 682 */ 683 void generateProject(string ide, GeneratorSettings settings) 684 { 685 // With a requested `unittest` config, switch to the special test runner 686 // config (which doesn't require an existing `unittest` configuration). 687 if (settings.config == "unittest") { 688 const test_config = m_project.addTestRunnerConfiguration(settings, !m_dryRun); 689 if (test_config) settings.config = test_config; 690 } 691 692 auto generator = createProjectGenerator(ide, m_project); 693 if (m_dryRun) return; // TODO: pass m_dryRun to the generator 694 generator.generate(settings); 695 } 696 697 /** Generate project files using the special test runner (`dub test`) configuration. 698 699 Any existing project files will be overridden. 700 */ 701 void testProject(GeneratorSettings settings, string config, NativePath custom_main_file) 702 { 703 if (!custom_main_file.empty && !custom_main_file.absolute) custom_main_file = getWorkingDirectory() ~ custom_main_file; 704 705 const test_config = m_project.addTestRunnerConfiguration(settings, !m_dryRun, config, custom_main_file); 706 if (!test_config) return; // target type "none" 707 708 settings.config = test_config; 709 710 auto generator = createProjectGenerator("build", m_project); 711 generator.generate(settings); 712 } 713 714 /** Executes D-Scanner tests on the current project. **/ 715 void lintProject(string[] args) 716 { 717 import std.path : buildPath, buildNormalizedPath; 718 719 if (m_dryRun) return; 720 721 auto tool = "dscanner"; 722 723 auto tool_pack = m_packageManager.getBestPackage(tool); 724 if (!tool_pack) { 725 logInfo("Hint", Color.light_blue, "%s is not present, getting and storing it user wide", tool); 726 tool_pack = fetch(tool, VersionRange.Any, defaultPlacementLocation, FetchOptions.none); 727 } 728 729 auto dscanner_dub = new Dub(null, m_packageSuppliers); 730 dscanner_dub.loadPackage(tool_pack); 731 dscanner_dub.upgrade(UpgradeOptions.select); 732 733 GeneratorSettings settings = this.makeAppSettings(); 734 foreach (dependencyPackage; m_project.dependencies) 735 { 736 auto cfgs = m_project.getPackageConfigs(settings.platform, null, true); 737 auto buildSettings = dependencyPackage.getBuildSettings(settings.platform, cfgs[dependencyPackage.name]); 738 foreach (importPath; buildSettings.importPaths) { 739 settings.runArgs ~= ["-I", buildNormalizedPath(dependencyPackage.path.toNativeString(), importPath.idup)]; 740 } 741 } 742 743 string configFilePath = buildPath(m_project.rootPackage.path.toNativeString(), "dscanner.ini"); 744 if (!args.canFind("--config") && exists(configFilePath)) { 745 settings.runArgs ~= ["--config", configFilePath]; 746 } 747 748 settings.runArgs ~= args ~ [m_project.rootPackage.path.toNativeString()]; 749 dscanner_dub.generateProject("build", settings); 750 } 751 752 /** Prints the specified build settings necessary for building the root package. 753 */ 754 void listProjectData(GeneratorSettings settings, string[] requestedData, ListBuildSettingsFormat list_type) 755 { 756 import std.stdio; 757 import std.ascii : newline; 758 759 // Split comma-separated lists 760 string[] requestedDataSplit = 761 requestedData 762 .map!(a => a.splitter(",").map!strip) 763 .joiner() 764 .array(); 765 766 auto data = m_project.listBuildSettings(settings, requestedDataSplit, list_type); 767 768 string delimiter; 769 final switch (list_type) with (ListBuildSettingsFormat) { 770 case list: delimiter = newline ~ newline; break; 771 case listNul: delimiter = "\0\0"; break; 772 case commandLine: delimiter = " "; break; 773 case commandLineNul: delimiter = "\0\0"; break; 774 } 775 776 write(data.joiner(delimiter)); 777 if (delimiter != "\0\0") writeln(); 778 } 779 780 /// Cleans intermediate/cache files of the given package 781 void cleanPackage(NativePath path) 782 { 783 logInfo("Cleaning", Color.green, "package at %s", path.toNativeString().color(Mode.bold)); 784 enforce(!Package.findPackageFile(path).empty, "No package found.", path.toNativeString()); 785 786 // TODO: clear target files and copy files 787 788 if (existsFile(path ~ ".dub/build")) rmdirRecurse((path ~ ".dub/build").toNativeString()); 789 if (existsFile(path ~ ".dub/metadata_cache.json")) std.file.remove((path ~ ".dub/metadata_cache.json").toNativeString()); 790 791 auto p = Package.load(path); 792 if (p.getBuildSettings().targetType == TargetType.none) { 793 foreach (sp; p.subPackages.filter!(sp => !sp.path.empty)) { 794 cleanPackage(path ~ sp.path); 795 } 796 } 797 } 798 799 /// Fetches the package matching the dependency and places it in the specified location. 800 deprecated("Use the overload that accepts either a `Version` or a `VersionRange` as second argument") 801 Package fetch(string packageId, const Dependency dep, PlacementLocation location, FetchOptions options, string reason = "") 802 { 803 const vrange = dep.visit!( 804 (VersionRange range) => range, 805 function VersionRange (any) { throw new Exception("Cannot call `dub.fetch` with a " ~ typeof(any).stringof ~ " dependency"); } 806 ); 807 return this.fetch(packageId, vrange, location, options, reason); 808 } 809 810 /// Ditto 811 Package fetch(string packageId, in Version vers, PlacementLocation location, FetchOptions options, string reason = "") 812 { 813 return this.fetch(packageId, VersionRange(vers, vers), location, options, reason); 814 } 815 816 /// Ditto 817 Package fetch(string packageId, in VersionRange range, PlacementLocation location, FetchOptions options, string reason = "") 818 { 819 auto basePackageName = getBasePackageName(packageId); 820 Json pinfo; 821 PackageSupplier supplier; 822 foreach(ps; m_packageSuppliers){ 823 try { 824 pinfo = ps.fetchPackageRecipe(basePackageName, Dependency(range), (options & FetchOptions.usePrerelease) != 0); 825 if (pinfo.type == Json.Type.null_) 826 continue; 827 supplier = ps; 828 break; 829 } catch(Exception e) { 830 logWarn("Package %s not found for %s: %s", packageId, ps.description, e.msg); 831 logDebug("Full error: %s", e.toString().sanitize()); 832 } 833 } 834 enforce(pinfo.type != Json.Type.undefined, "No package "~packageId~" was found matching the dependency " ~ range.toString()); 835 Version ver = Version(pinfo["version"].get!string); 836 837 // always upgrade branch based versions - TODO: actually check if there is a new commit available 838 Package existing = m_packageManager.getPackage(packageId, ver, location); 839 if (options & FetchOptions.printOnly) { 840 if (existing && existing.version_ != ver) 841 logInfo("A new version for %s is available (%s -> %s). Run \"%s\" to switch.", 842 packageId.color(Mode.bold), existing.version_, ver, 843 text("dub upgrade ", packageId).color(Mode.bold)); 844 return null; 845 } 846 847 if (existing) { 848 if (!ver.isBranch() || !(options & FetchOptions.forceBranchUpgrade) || location == PlacementLocation.local) { 849 // TODO: support git working trees by performing a "git pull" instead of this 850 logDiagnostic("Package %s %s (in %s packages) is already present with the latest version, skipping upgrade.", 851 packageId, ver, location.toString); 852 return existing; 853 } else { 854 logInfo("Removing", Color.yellow, "%s %s to prepare replacement with a new version", packageId.color(Mode.bold), ver); 855 if (!m_dryRun) m_packageManager.remove(existing); 856 } 857 } 858 859 if (reason.length) logInfo("Fetching", Color.yellow, "%s %s (%s)", packageId.color(Mode.bold), ver, reason); 860 else logInfo("Fetching", Color.yellow, "%s %s", packageId.color(Mode.bold), ver); 861 if (m_dryRun) return null; 862 863 logDebug("Acquiring package zip file"); 864 865 // repeat download on corrupted zips, see #1336 866 foreach_reverse (i; 0..3) 867 { 868 import std.zip : ZipException; 869 870 auto path = getTempFile(basePackageName, ".zip"); 871 supplier.fetchPackage(path, basePackageName, Dependency(range), (options & FetchOptions.usePrerelease) != 0); // Q: continue on fail? 872 scope(exit) std.file.remove(path.toNativeString()); 873 logDiagnostic("Placing to %s...", location.toString()); 874 875 try { 876 return m_packageManager.store(path, location, basePackageName, ver); 877 } catch (ZipException e) { 878 logInfo("Failed to extract zip archive for %s %s...", packageId, ver); 879 // rethrow the exception at the end of the loop 880 if (i == 0) 881 throw e; 882 } 883 } 884 assert(0, "Should throw a ZipException instead."); 885 } 886 887 /** Removes a specific locally cached package. 888 889 This will delete the package files from disk and removes the 890 corresponding entry from the list of known packages. 891 892 Params: 893 pack = Package instance to remove 894 */ 895 void remove(in Package pack) 896 { 897 logInfo("Removing", Color.yellow, "%s (in %s)", pack.name.color(Mode.bold), pack.path.toNativeString()); 898 if (!m_dryRun) m_packageManager.remove(pack); 899 } 900 901 /// Compatibility overload. Use the version without a `force_remove` argument instead. 902 deprecated("Use `remove(pack)` directly instead, the boolean has no effect") 903 void remove(in Package pack, bool force_remove) 904 { 905 remove(pack); 906 } 907 908 /// @see remove(string, string, RemoveLocation) 909 enum RemoveVersionWildcard = "*"; 910 911 /** Removes one or more versions of a locally cached package. 912 913 This will remove a given package with a specified version from the 914 given location. It will remove at most one package, unless `version_` 915 is set to `RemoveVersionWildcard`. 916 917 Params: 918 package_id = Name of the package to be removed 919 location_ = Specifies the location to look for the given package 920 name/version. 921 resolve_version = Callback to select package version. 922 */ 923 void remove(string package_id, PlacementLocation location, 924 scope size_t delegate(in Package[] packages) resolve_version) 925 { 926 enforce(!package_id.empty); 927 if (location == PlacementLocation.local) { 928 logInfo("To remove a locally placed package, make sure you don't have any data" 929 ~ "\nleft in it's directory and then simply remove the whole directory."); 930 throw new Exception("dub cannot remove locally installed packages."); 931 } 932 933 Package[] packages; 934 935 // Retrieve packages to be removed. 936 foreach(pack; m_packageManager.getPackageIterator(package_id)) 937 if (m_packageManager.isManagedPackage(pack)) 938 packages ~= pack; 939 940 // Check validity of packages to be removed. 941 if(packages.empty) { 942 throw new Exception("Cannot find package to remove. (" 943 ~ "id: '" ~ package_id ~ "', location: '" ~ to!string(location) ~ "'" 944 ~ ")"); 945 } 946 947 // Sort package list in ascending version order 948 packages.sort!((a, b) => a.version_ < b.version_); 949 950 immutable idx = resolve_version(packages); 951 if (idx == size_t.max) 952 return; 953 else if (idx != packages.length) 954 packages = packages[idx .. idx + 1]; 955 956 logDebug("Removing %s packages.", packages.length); 957 foreach(pack; packages) { 958 try { 959 remove(pack); 960 } catch (Exception e) { 961 logError("Failed to remove %s %s: %s", package_id, pack.version_, e.msg); 962 logInfo("Continuing with other packages (if any)."); 963 } 964 } 965 } 966 967 /// Compatibility overload. Use the version without a `force_remove` argument instead. 968 void remove(string package_id, PlacementLocation location, bool force_remove, 969 scope size_t delegate(in Package[] packages) resolve_version) 970 { 971 remove(package_id, location, resolve_version); 972 } 973 974 /** Removes a specific version of a package. 975 976 Params: 977 package_id = Name of the package to be removed 978 version_ = Identifying a version or a wild card. If an empty string 979 is passed, the package will be removed from the location, if 980 there is only one version retrieved. This will throw an 981 exception, if there are multiple versions retrieved. 982 location_ = Specifies the location to look for the given package 983 name/version. 984 */ 985 void remove(string package_id, string version_, PlacementLocation location) 986 { 987 remove(package_id, location, (in packages) { 988 if (version_ == RemoveVersionWildcard || version_.empty) 989 return packages.length; 990 991 foreach (i, p; packages) { 992 if (p.version_ == Version(version_)) 993 return i; 994 } 995 throw new Exception("Cannot find package to remove. (" 996 ~ "id: '" ~ package_id ~ "', version: '" ~ version_ ~ "', location: '" ~ to!string(location) ~ "'" 997 ~ ")"); 998 }); 999 } 1000 1001 /// Compatibility overload. Use the version without a `force_remove` argument instead. 1002 deprecated("Use the overload without force_remove instead") 1003 void remove(string package_id, string version_, PlacementLocation location, bool force_remove) 1004 { 1005 remove(package_id, version_, location); 1006 } 1007 1008 /** Adds a directory to the list of locally known packages. 1009 1010 Forwards to `PackageManager.addLocalPackage`. 1011 1012 Params: 1013 path = Path to the package 1014 ver = Optional version to associate with the package (can be left 1015 empty) 1016 system = Make the package known system wide instead of user wide 1017 (requires administrator privileges). 1018 1019 See_Also: `removeLocalPackage` 1020 */ 1021 void addLocalPackage(string path, string ver, bool system) 1022 { 1023 if (m_dryRun) return; 1024 m_packageManager.addLocalPackage(makeAbsolute(path), ver, system ? PlacementLocation.system : PlacementLocation.user); 1025 } 1026 1027 /** Removes a directory from the list of locally known packages. 1028 1029 Forwards to `PackageManager.removeLocalPackage`. 1030 1031 Params: 1032 path = Path to the package 1033 system = Make the package known system wide instead of user wide 1034 (requires administrator privileges). 1035 1036 See_Also: `addLocalPackage` 1037 */ 1038 void removeLocalPackage(string path, bool system) 1039 { 1040 if (m_dryRun) return; 1041 m_packageManager.removeLocalPackage(makeAbsolute(path), system ? PlacementLocation.system : PlacementLocation.user); 1042 } 1043 1044 /** Registers a local directory to search for packages to use for satisfying 1045 dependencies. 1046 1047 Params: 1048 path = Path to a directory containing package directories 1049 system = Make the package known system wide instead of user wide 1050 (requires administrator privileges). 1051 1052 See_Also: `removeSearchPath` 1053 */ 1054 void addSearchPath(string path, bool system) 1055 { 1056 if (m_dryRun) return; 1057 m_packageManager.addSearchPath(makeAbsolute(path), system ? PlacementLocation.system : PlacementLocation.user); 1058 } 1059 1060 /** Unregisters a local directory search path. 1061 1062 Params: 1063 path = Path to a directory containing package directories 1064 system = Make the package known system wide instead of user wide 1065 (requires administrator privileges). 1066 1067 See_Also: `addSearchPath` 1068 */ 1069 void removeSearchPath(string path, bool system) 1070 { 1071 if (m_dryRun) return; 1072 m_packageManager.removeSearchPath(makeAbsolute(path), system ? PlacementLocation.system : PlacementLocation.user); 1073 } 1074 1075 /** Queries all package suppliers with the given query string. 1076 1077 Returns a list of tuples, where the first entry is the human readable 1078 name of the package supplier and the second entry is the list of 1079 matched packages. 1080 1081 Params: 1082 query = the search term to match packages on 1083 1084 See_Also: `PackageSupplier.searchPackages` 1085 */ 1086 auto searchPackages(string query) 1087 { 1088 import std.typecons : Tuple, tuple; 1089 Tuple!(string, PackageSupplier.SearchResult[])[] results; 1090 foreach (ps; this.m_packageSuppliers) { 1091 try 1092 results ~= tuple(ps.description, ps.searchPackages(query)); 1093 catch (Exception e) { 1094 logWarn("Searching %s for '%s' failed: %s", ps.description, query, e.msg); 1095 } 1096 } 1097 return results.filter!(tup => tup[1].length); 1098 } 1099 1100 /** Returns a list of all available versions (including branches) for a 1101 particular package. 1102 1103 The list returned is based on the registered package suppliers. Local 1104 packages are not queried in the search for versions. 1105 1106 See_also: `getLatestVersion` 1107 */ 1108 Version[] listPackageVersions(string name) 1109 { 1110 Version[] versions; 1111 auto basePackageName = getBasePackageName(name); 1112 foreach (ps; this.m_packageSuppliers) { 1113 try versions ~= ps.getVersions(basePackageName); 1114 catch (Exception e) { 1115 logWarn("Failed to get versions for package %s on provider %s: %s", name, ps.description, e.msg); 1116 } 1117 } 1118 return versions.sort().uniq.array; 1119 } 1120 1121 /** Returns the latest available version for a particular package. 1122 1123 This function returns the latest numbered version of a package. If no 1124 numbered versions are available, it will return an available branch, 1125 preferring "~master". 1126 1127 Params: 1128 package_name: The name of the package in question. 1129 prefer_stable: If set to `true` (the default), returns the latest 1130 stable version, even if there are newer pre-release versions. 1131 1132 See_also: `listPackageVersions` 1133 */ 1134 Version getLatestVersion(string package_name, bool prefer_stable = true) 1135 { 1136 auto vers = listPackageVersions(package_name); 1137 enforce(!vers.empty, "Failed to find any valid versions for a package name of '"~package_name~"'."); 1138 auto final_versions = vers.filter!(v => !v.isBranch && !v.isPreRelease).array; 1139 if (prefer_stable && final_versions.length) return final_versions[$-1]; 1140 else return vers[$-1]; 1141 } 1142 1143 /** Initializes a directory with a package skeleton. 1144 1145 Params: 1146 path = Path of the directory to create the new package in. The 1147 directory will be created if it doesn't exist. 1148 deps = List of dependencies to add to the package recipe. 1149 type = Specifies the type of the application skeleton to use. 1150 format = Determines the package recipe format to use. 1151 recipe_callback = Optional callback that can be used to 1152 customize the recipe before it gets written. 1153 */ 1154 void createEmptyPackage(NativePath path, string[] deps, string type, 1155 PackageFormat format = PackageFormat.sdl, 1156 scope void delegate(ref PackageRecipe, ref PackageFormat) recipe_callback = null, 1157 string[] app_args = []) 1158 { 1159 if (!path.absolute) path = m_rootPath ~ path; 1160 path.normalize(); 1161 1162 VersionRange[string] depVers; 1163 string[] notFound; // keep track of any failed packages in here 1164 foreach (dep; deps) { 1165 try { 1166 Version ver = getLatestVersion(dep); 1167 if (ver.isBranch()) 1168 depVers[dep] = VersionRange(ver); 1169 else 1170 depVers[dep] = VersionRange.fromString("~>" ~ ver.toString()); 1171 } catch (Exception e) { 1172 notFound ~= dep; 1173 } 1174 } 1175 1176 if(notFound.length > 1){ 1177 throw new Exception(.format("Couldn't find packages: %-(%s, %).", notFound)); 1178 } 1179 else if(notFound.length == 1){ 1180 throw new Exception(.format("Couldn't find package: %-(%s, %).", notFound)); 1181 } 1182 1183 if (m_dryRun) return; 1184 1185 initPackage(path, depVers, type, format, recipe_callback); 1186 1187 if (!["vibe.d", "deimos", "minimal"].canFind(type)) { 1188 runCustomInitialization(path, type, app_args); 1189 } 1190 1191 //Act smug to the user. 1192 logInfo("Success", Color.green, "created empty project in %s", path.toNativeString().color(Mode.bold)); 1193 } 1194 1195 private void runCustomInitialization(NativePath path, string type, string[] runArgs) 1196 { 1197 string packageName = type; 1198 auto template_pack = m_packageManager.getBestPackage(packageName); 1199 if (!template_pack) { 1200 logInfo("%s is not present, getting and storing it user wide", packageName); 1201 template_pack = fetch(packageName, VersionRange.Any, defaultPlacementLocation, FetchOptions.none); 1202 } 1203 1204 Package initSubPackage = m_packageManager.getSubPackage(template_pack, "init-exec", false); 1205 auto template_dub = new Dub(null, m_packageSuppliers); 1206 template_dub.loadPackage(initSubPackage); 1207 1208 GeneratorSettings settings = this.makeAppSettings(); 1209 settings.runArgs = runArgs; 1210 1211 initSubPackage.recipe.buildSettings.workingDirectory = path.toNativeString(); 1212 template_dub.generateProject("build", settings); 1213 } 1214 1215 /** Converts the package recipe of the loaded root package to the given format. 1216 1217 Params: 1218 destination_file_ext = The file extension matching the desired 1219 format. Possible values are "json" or "sdl". 1220 print_only = Print the converted recipe instead of writing to disk 1221 */ 1222 void convertRecipe(string destination_file_ext, bool print_only = false) 1223 { 1224 import std.path : extension; 1225 import std.stdio : stdout; 1226 import dub.recipe.io : serializePackageRecipe, writePackageRecipe; 1227 1228 if (print_only) { 1229 auto dst = stdout.lockingTextWriter; 1230 serializePackageRecipe(dst, m_project.rootPackage.rawRecipe, "dub."~destination_file_ext); 1231 return; 1232 } 1233 1234 auto srcfile = m_project.rootPackage.recipePath; 1235 auto srcext = srcfile.head.name.extension; 1236 if (srcext == "."~destination_file_ext) { 1237 // no logging before this point 1238 tagWidth.push(5); 1239 logError("Package format is already %s.", destination_file_ext); 1240 return; 1241 } 1242 1243 writePackageRecipe(srcfile.parentPath ~ ("dub."~destination_file_ext), m_project.rootPackage.rawRecipe); 1244 removeFile(srcfile); 1245 } 1246 1247 /** Runs DDOX to generate or serve documentation. 1248 1249 Params: 1250 run = If set to true, serves documentation on a local web server. 1251 Otherwise generates actual HTML files. 1252 generate_args = Additional command line arguments to pass to 1253 "ddox generate-html" or "ddox serve-html". 1254 */ 1255 void runDdox(bool run, string[] generate_args = null) 1256 { 1257 import std.process : browse; 1258 1259 if (m_dryRun) return; 1260 1261 // allow to choose a custom ddox tool 1262 auto tool = m_project.rootPackage.recipe.ddoxTool; 1263 if (tool.empty) tool = "ddox"; 1264 1265 auto tool_pack = m_packageManager.getBestPackage(tool); 1266 if (!tool_pack) { 1267 logInfo("%s is not present, getting and storing it user wide", tool); 1268 tool_pack = fetch(tool, VersionRange.Any, defaultPlacementLocation, FetchOptions.none); 1269 } 1270 1271 auto ddox_dub = new Dub(null, m_packageSuppliers); 1272 ddox_dub.loadPackage(tool_pack); 1273 ddox_dub.upgrade(UpgradeOptions.select); 1274 1275 GeneratorSettings settings = this.makeAppSettings(); 1276 1277 auto filterargs = m_project.rootPackage.recipe.ddoxFilterArgs.dup; 1278 if (filterargs.empty) filterargs = ["--min-protection=Protected", "--only-documented"]; 1279 1280 settings.runArgs = "filter" ~ filterargs ~ "docs.json"; 1281 ddox_dub.generateProject("build", settings); 1282 1283 auto p = tool_pack.path; 1284 p.endsWithSlash = true; 1285 auto tool_path = p.toNativeString(); 1286 1287 if (run) { 1288 settings.runArgs = ["serve-html", "--navigation-type=ModuleTree", "docs.json", "--web-file-dir="~tool_path~"public"] ~ generate_args; 1289 browse("http://127.0.0.1:8080/"); 1290 } else { 1291 settings.runArgs = ["generate-html", "--navigation-type=ModuleTree", "docs.json", "docs"] ~ generate_args; 1292 } 1293 ddox_dub.generateProject("build", settings); 1294 1295 if (!run) { 1296 // TODO: ddox should copy those files itself 1297 version(Windows) runCommand(`xcopy /S /D "`~tool_path~`public\*" docs\`); 1298 else runCommand("rsync -ru '"~tool_path~"public/' docs/"); 1299 } 1300 } 1301 1302 /// Make a `GeneratorSettings` suitable to generate tools (DDOC, DScanner, etc...) 1303 private GeneratorSettings makeAppSettings () const 1304 { 1305 GeneratorSettings settings; 1306 auto compiler_binary = this.defaultCompiler; 1307 1308 settings.config = "application"; 1309 settings.buildType = "debug"; 1310 settings.compiler = getCompiler(compiler_binary); 1311 settings.platform = settings.compiler.determinePlatform( 1312 settings.buildSettings, compiler_binary, this.defaultArchitecture); 1313 if (this.defaultLowMemory) 1314 settings.buildSettings.options |= BuildOption.lowmem; 1315 if (this.defaultEnvironments) 1316 settings.buildSettings.addEnvironments(this.defaultEnvironments); 1317 if (this.defaultBuildEnvironments) 1318 settings.buildSettings.addBuildEnvironments(this.defaultBuildEnvironments); 1319 if (this.defaultRunEnvironments) 1320 settings.buildSettings.addRunEnvironments(this.defaultRunEnvironments); 1321 if (this.defaultPreGenerateEnvironments) 1322 settings.buildSettings.addPreGenerateEnvironments(this.defaultPreGenerateEnvironments); 1323 if (this.defaultPostGenerateEnvironments) 1324 settings.buildSettings.addPostGenerateEnvironments(this.defaultPostGenerateEnvironments); 1325 if (this.defaultPreBuildEnvironments) 1326 settings.buildSettings.addPreBuildEnvironments(this.defaultPreBuildEnvironments); 1327 if (this.defaultPostBuildEnvironments) 1328 settings.buildSettings.addPostBuildEnvironments(this.defaultPostBuildEnvironments); 1329 if (this.defaultPreRunEnvironments) 1330 settings.buildSettings.addPreRunEnvironments(this.defaultPreRunEnvironments); 1331 if (this.defaultPostRunEnvironments) 1332 settings.buildSettings.addPostRunEnvironments(this.defaultPostRunEnvironments); 1333 settings.run = true; 1334 1335 return settings; 1336 } 1337 1338 private void determineDefaultCompiler() 1339 { 1340 import std.file : thisExePath; 1341 import std.path : buildPath, dirName, expandTilde, isAbsolute, isDirSeparator; 1342 import std.range : front; 1343 1344 // Env takes precedence 1345 if (auto envCompiler = environment.get("DC")) 1346 m_defaultCompiler = envCompiler; 1347 else 1348 m_defaultCompiler = m_config.defaultCompiler.expandTilde; 1349 if (m_defaultCompiler.length && m_defaultCompiler.isAbsolute) 1350 return; 1351 1352 static immutable BinaryPrefix = `$DUB_BINARY_PATH`; 1353 if(m_defaultCompiler.startsWith(BinaryPrefix)) 1354 { 1355 m_defaultCompiler = thisExePath().dirName() ~ m_defaultCompiler[BinaryPrefix.length .. $]; 1356 return; 1357 } 1358 1359 if (!find!isDirSeparator(m_defaultCompiler).empty) 1360 throw new Exception("defaultCompiler specified in a DUB config file cannot use an unqualified relative path:\n\n" ~ m_defaultCompiler ~ 1361 "\n\nUse \"$DUB_BINARY_PATH/../path/you/want\" instead."); 1362 1363 version (Windows) enum sep = ";", exe = ".exe"; 1364 version (Posix) enum sep = ":", exe = ""; 1365 1366 auto compilers = ["dmd", "gdc", "gdmd", "ldc2", "ldmd2"]; 1367 // If a compiler name is specified, look for it next to dub. 1368 // Otherwise, look for any of the common compilers adjacent to dub. 1369 if (m_defaultCompiler.length) 1370 { 1371 string compilerPath = buildPath(thisExePath().dirName(), m_defaultCompiler ~ exe); 1372 if (existsFile(compilerPath)) 1373 { 1374 m_defaultCompiler = compilerPath; 1375 return; 1376 } 1377 } 1378 else 1379 { 1380 auto nextFound = compilers.find!(bin => existsFile(buildPath(thisExePath().dirName(), bin ~ exe))); 1381 if (!nextFound.empty) 1382 { 1383 m_defaultCompiler = buildPath(thisExePath().dirName(), nextFound.front ~ exe); 1384 return; 1385 } 1386 } 1387 1388 // If nothing found next to dub, search the user's PATH, starting 1389 // with the compiler name from their DUB config file, if specified. 1390 auto paths = environment.get("PATH", "").splitter(sep).map!NativePath; 1391 if (m_defaultCompiler.length && paths.canFind!(p => existsFile(p ~ (m_defaultCompiler~exe)))) 1392 return; 1393 foreach (p; paths) { 1394 auto res = compilers.find!(bin => existsFile(p ~ (bin~exe))); 1395 if (!res.empty) { 1396 m_defaultCompiler = res.front; 1397 return; 1398 } 1399 } 1400 m_defaultCompiler = compilers[0]; 1401 } 1402 1403 unittest 1404 { 1405 import std.path: buildPath, absolutePath; 1406 auto dub = new TestDub(".", null, SkipPackageSuppliers.configured); 1407 immutable olddc = environment.get("DC", null); 1408 immutable oldpath = environment.get("PATH", null); 1409 immutable testdir = "test-determineDefaultCompiler"; 1410 void repairenv(string name, string var) 1411 { 1412 if (var !is null) 1413 environment[name] = var; 1414 else if (name in environment) 1415 environment.remove(name); 1416 } 1417 scope (exit) repairenv("DC", olddc); 1418 scope (exit) repairenv("PATH", oldpath); 1419 scope (exit) rmdirRecurse(testdir); 1420 1421 version (Windows) enum sep = ";", exe = ".exe"; 1422 version (Posix) enum sep = ":", exe = ""; 1423 1424 immutable dmdpath = testdir.buildPath("dmd", "bin"); 1425 immutable ldcpath = testdir.buildPath("ldc", "bin"); 1426 mkdirRecurse(dmdpath); 1427 mkdirRecurse(ldcpath); 1428 immutable dmdbin = dmdpath.buildPath("dmd"~exe); 1429 immutable ldcbin = ldcpath.buildPath("ldc2"~exe); 1430 std.file.write(dmdbin, null); 1431 std.file.write(ldcbin, null); 1432 1433 environment["DC"] = dmdbin.absolutePath(); 1434 dub.determineDefaultCompiler(); 1435 assert(dub.m_defaultCompiler == dmdbin.absolutePath()); 1436 1437 environment["DC"] = "dmd"; 1438 environment["PATH"] = dmdpath ~ sep ~ ldcpath; 1439 dub.determineDefaultCompiler(); 1440 assert(dub.m_defaultCompiler == "dmd"); 1441 1442 environment["DC"] = "ldc2"; 1443 environment["PATH"] = dmdpath ~ sep ~ ldcpath; 1444 dub.determineDefaultCompiler(); 1445 assert(dub.m_defaultCompiler == "ldc2"); 1446 1447 environment.remove("DC"); 1448 environment["PATH"] = ldcpath ~ sep ~ dmdpath; 1449 dub.determineDefaultCompiler(); 1450 assert(dub.m_defaultCompiler == "ldc2"); 1451 } 1452 1453 private NativePath makeAbsolute(NativePath p) const { return p.absolute ? p : m_rootPath ~ p; } 1454 private NativePath makeAbsolute(string p) const { return makeAbsolute(NativePath(p)); } 1455 } 1456 1457 1458 /// Option flags for `Dub.fetch` 1459 enum FetchOptions 1460 { 1461 none = 0, 1462 forceBranchUpgrade = 1<<0, 1463 usePrerelease = 1<<1, 1464 forceRemove = 1<<2, /// Deprecated, does nothing. 1465 printOnly = 1<<3, 1466 } 1467 1468 /// Option flags for `Dub.upgrade` 1469 enum UpgradeOptions 1470 { 1471 none = 0, 1472 upgrade = 1<<1, /// Upgrade existing packages 1473 preRelease = 1<<2, /// inclde pre-release versions in upgrade 1474 forceRemove = 1<<3, /// Deprecated, does nothing. 1475 select = 1<<4, /// Update the dub.selections.json file with the upgraded versions 1476 dryRun = 1<<5, /// Instead of downloading new packages, just print a message to notify the user of their existence 1477 /*deprecated*/ printUpgradesOnly = dryRun, /// deprecated, use dryRun instead 1478 /*deprecated*/ useCachedResult = 1<<6, /// deprecated, has no effect 1479 noSaveSelections = 1<<7, /// Don't store updated selections on disk 1480 } 1481 1482 /// Determines which of the default package suppliers are queried for packages. 1483 enum SkipPackageSuppliers { 1484 none, /// Uses all configured package suppliers. 1485 standard, /// Does not use the default package suppliers (`defaultPackageSuppliers`). 1486 configured, /// Does not use default suppliers or suppliers configured in DUB's configuration file 1487 all /// Uses only manually specified package suppliers. 1488 } 1489 1490 private class DependencyVersionResolver : DependencyResolver!(Dependency, Dependency) { 1491 protected { 1492 Dub m_dub; 1493 UpgradeOptions m_options; 1494 Dependency[][string] m_packageVersions; 1495 Package[string] m_remotePackages; 1496 SelectedVersions m_selectedVersions; 1497 Package m_rootPackage; 1498 bool[string] m_packagesToUpgrade; 1499 Package[PackageDependency] m_packages; 1500 TreeNodes[][TreeNode] m_children; 1501 } 1502 1503 1504 this(Dub dub, UpgradeOptions options, Package root, SelectedVersions selected_versions) 1505 { 1506 assert(dub !is null); 1507 assert(root !is null); 1508 assert(selected_versions !is null); 1509 1510 if (environment.get("DUB_NO_RESOLVE_LIMIT") !is null) 1511 super(ulong.max); 1512 else 1513 super(1_000_000); 1514 1515 m_dub = dub; 1516 m_options = options; 1517 m_rootPackage = root; 1518 m_selectedVersions = selected_versions; 1519 } 1520 1521 Dependency[string] resolve(string[] filter) 1522 { 1523 foreach (name; filter) 1524 m_packagesToUpgrade[name] = true; 1525 return super.resolve(TreeNode(m_rootPackage.name, Dependency(m_rootPackage.version_)), 1526 (m_options & UpgradeOptions.dryRun) == 0); 1527 } 1528 1529 protected bool isFixedPackage(string pack) 1530 { 1531 return m_packagesToUpgrade !is null && pack !in m_packagesToUpgrade; 1532 } 1533 1534 protected override Dependency[] getAllConfigs(string pack) 1535 { 1536 if (auto pvers = pack in m_packageVersions) 1537 return *pvers; 1538 1539 if ((!(m_options & UpgradeOptions.upgrade) || isFixedPackage(pack)) && m_selectedVersions.hasSelectedVersion(pack)) { 1540 auto ret = [m_selectedVersions.getSelectedVersion(pack)]; 1541 logDiagnostic("Using fixed selection %s %s", pack, ret[0]); 1542 m_packageVersions[pack] = ret; 1543 return ret; 1544 } 1545 1546 logDiagnostic("Search for versions of %s (%s package suppliers)", pack, m_dub.m_packageSuppliers.length); 1547 Version[] versions; 1548 foreach (p; m_dub.packageManager.getPackageIterator(pack)) 1549 versions ~= p.version_; 1550 1551 foreach (ps; m_dub.m_packageSuppliers) { 1552 try { 1553 auto vers = ps.getVersions(pack); 1554 vers.reverse(); 1555 if (!vers.length) { 1556 logDiagnostic("No versions for %s for %s", pack, ps.description); 1557 continue; 1558 } 1559 1560 versions ~= vers; 1561 break; 1562 } catch (Exception e) { 1563 logWarn("Package %s not found in %s: %s", pack, ps.description, e.msg); 1564 logDebug("Full error: %s", e.toString().sanitize); 1565 } 1566 } 1567 1568 // sort by version, descending, and remove duplicates 1569 versions = versions.sort!"a>b".uniq.array; 1570 1571 // move pre-release versions to the back of the list if no preRelease flag is given 1572 if (!(m_options & UpgradeOptions.preRelease)) 1573 versions = versions.filter!(v => !v.isPreRelease).array ~ versions.filter!(v => v.isPreRelease).array; 1574 1575 // filter out invalid/unreachable dependency specs 1576 versions = versions.filter!((v) { 1577 bool valid = getPackage(pack, Dependency(v)) !is null; 1578 if (!valid) logDiagnostic("Excluding invalid dependency specification %s %s from dependency resolution process.", pack, v); 1579 return valid; 1580 }).array; 1581 1582 if (!versions.length) logDiagnostic("Nothing found for %s", pack); 1583 else logDiagnostic("Return for %s: %s", pack, versions); 1584 1585 auto ret = versions.map!(v => Dependency(v)).array; 1586 m_packageVersions[pack] = ret; 1587 return ret; 1588 } 1589 1590 protected override Dependency[] getSpecificConfigs(string pack, TreeNodes nodes) 1591 { 1592 if (!nodes.configs.path.empty || !nodes.configs.repository.empty) { 1593 if (getPackage(pack, nodes.configs)) return [nodes.configs]; 1594 else return null; 1595 } 1596 else return null; 1597 } 1598 1599 1600 protected override TreeNodes[] getChildren(TreeNode node) 1601 { 1602 if (auto pc = node in m_children) 1603 return *pc; 1604 auto ret = getChildrenRaw(node); 1605 m_children[node] = ret; 1606 return ret; 1607 } 1608 1609 private final TreeNodes[] getChildrenRaw(TreeNode node) 1610 { 1611 import std.array : appender; 1612 auto ret = appender!(TreeNodes[]); 1613 auto pack = getPackage(node.pack, node.config); 1614 if (!pack) { 1615 // this can hapen when the package description contains syntax errors 1616 logDebug("Invalid package in dependency tree: %s %s", node.pack, node.config); 1617 return null; 1618 } 1619 auto basepack = pack.basePackage; 1620 1621 foreach (d; pack.getAllDependenciesRange()) { 1622 auto dbasename = getBasePackageName(d.name); 1623 1624 // detect dependencies to the root package (or sub packages thereof) 1625 if (dbasename == basepack.name) { 1626 auto absdeppath = d.spec.mapToPath(pack.path).path; 1627 absdeppath.endsWithSlash = true; 1628 auto subpack = m_dub.m_packageManager.getSubPackage(basepack, getSubPackageName(d.name), true); 1629 if (subpack) { 1630 auto desireddeppath = basepack.path; 1631 desireddeppath.endsWithSlash = true; 1632 1633 auto altdeppath = d.name == dbasename ? basepack.path : subpack.path; 1634 altdeppath.endsWithSlash = true; 1635 1636 if (!d.spec.path.empty && absdeppath != desireddeppath) 1637 logWarn("Sub package %s, referenced by %s %s must be referenced using the path to its base package", 1638 subpack.name, pack.name, pack.version_); 1639 1640 enforce(d.spec.path.empty || absdeppath == desireddeppath || absdeppath == altdeppath, 1641 format("Dependency from %s to %s uses wrong path: %s vs. %s", 1642 node.pack, subpack.name, absdeppath.toNativeString(), desireddeppath.toNativeString())); 1643 } 1644 ret ~= TreeNodes(d.name, node.config); 1645 continue; 1646 } 1647 1648 DependencyType dt; 1649 if (d.spec.optional) { 1650 if (d.spec.default_) dt = DependencyType.optionalDefault; 1651 else dt = DependencyType.optional; 1652 } else dt = DependencyType.required; 1653 1654 Dependency dspec = d.spec.mapToPath(pack.path); 1655 1656 // if not upgrading, use the selected version 1657 if (!(m_options & UpgradeOptions.upgrade) && m_selectedVersions.hasSelectedVersion(dbasename)) 1658 dspec = m_selectedVersions.getSelectedVersion(dbasename); 1659 1660 // keep selected optional dependencies and avoid non-selected optional-default dependencies by default 1661 if (!m_selectedVersions.bare) { 1662 if (dt == DependencyType.optionalDefault && !m_selectedVersions.hasSelectedVersion(dbasename)) 1663 dt = DependencyType.optional; 1664 else if (dt == DependencyType.optional && m_selectedVersions.hasSelectedVersion(dbasename)) 1665 dt = DependencyType.optionalDefault; 1666 } 1667 1668 ret ~= TreeNodes(d.name, dspec, dt); 1669 } 1670 return ret.data; 1671 } 1672 1673 protected override bool matches(Dependency configs, Dependency config) 1674 { 1675 if (!configs.path.empty) return configs.path == config.path; 1676 return configs.merge(config).valid; 1677 } 1678 1679 private Package getPackage(string name, Dependency dep) 1680 { 1681 auto key = PackageDependency(name, dep); 1682 if (auto pp = key in m_packages) 1683 return *pp; 1684 auto p = getPackageRaw(name, dep); 1685 m_packages[key] = p; 1686 return p; 1687 } 1688 1689 private Package getPackageRaw(string name, Dependency dep) 1690 { 1691 auto basename = getBasePackageName(name); 1692 1693 // for sub packages, first try to get them from the base package 1694 if (basename != name) { 1695 auto subname = getSubPackageName(name); 1696 auto basepack = getPackage(basename, dep); 1697 if (!basepack) return null; 1698 if (auto sp = m_dub.m_packageManager.getSubPackage(basepack, subname, true)) { 1699 return sp; 1700 } else if (!basepack.subPackages.canFind!(p => p.path.length)) { 1701 // note: external sub packages are handled further below 1702 auto spr = basepack.getInternalSubPackage(subname); 1703 if (!spr.isNull) { 1704 auto sp = new Package(spr.get, basepack.path, basepack); 1705 m_remotePackages[sp.name] = sp; 1706 return sp; 1707 } else { 1708 logDiagnostic("Sub package %s doesn't exist in %s %s.", name, basename, dep.version_); 1709 return null; 1710 } 1711 } else if (auto ret = m_dub.m_packageManager.getBestPackage(name, dep)) { 1712 return ret; 1713 } else { 1714 logDiagnostic("External sub package %s %s not found.", name, dep.version_); 1715 return null; 1716 } 1717 } 1718 1719 // shortcut if the referenced package is the root package 1720 if (basename == m_rootPackage.basePackage.name) 1721 return m_rootPackage.basePackage; 1722 1723 if (!dep.repository.empty) { 1724 auto ret = m_dub.packageManager.loadSCMPackage(name, dep.repository); 1725 return ret !is null && dep.matches(ret.version_) ? ret : null; 1726 } else if (!dep.path.empty) { 1727 try { 1728 return m_dub.packageManager.getOrLoadPackage(dep.path); 1729 } catch (Exception e) { 1730 logDiagnostic("Failed to load path based dependency %s: %s", name, e.msg); 1731 logDebug("Full error: %s", e.toString().sanitize); 1732 return null; 1733 } 1734 } 1735 const vers = dep.version_; 1736 1737 if (auto ret = m_dub.m_packageManager.getBestPackage(name, dep)) 1738 return ret; 1739 1740 auto key = name ~ ":" ~ vers.toString(); 1741 if (auto ret = key in m_remotePackages) 1742 return *ret; 1743 1744 auto prerelease = (m_options & UpgradeOptions.preRelease) != 0; 1745 1746 auto rootpack = name.split(":")[0]; 1747 1748 foreach (ps; m_dub.m_packageSuppliers) { 1749 if (rootpack == name) { 1750 try { 1751 auto desc = ps.fetchPackageRecipe(name, dep, prerelease); 1752 if (desc.type == Json.Type.null_) 1753 continue; 1754 auto ret = new Package(desc); 1755 m_remotePackages[key] = ret; 1756 return ret; 1757 } catch (Exception e) { 1758 logDiagnostic("Metadata for %s %s could not be downloaded from %s: %s", name, vers, ps.description, e.msg); 1759 logDebug("Full error: %s", e.toString().sanitize); 1760 } 1761 } else { 1762 logDiagnostic("Package %s not found in base package description (%s). Downloading whole package.", name, vers.toString()); 1763 try { 1764 FetchOptions fetchOpts; 1765 fetchOpts |= prerelease ? FetchOptions.usePrerelease : FetchOptions.none; 1766 m_dub.fetch(rootpack, vers, m_dub.defaultPlacementLocation, fetchOpts, "need sub package description"); 1767 auto ret = m_dub.m_packageManager.getBestPackage(name, dep); 1768 if (!ret) { 1769 logWarn("Package %s %s doesn't have a sub package %s", rootpack, dep.version_, name); 1770 return null; 1771 } 1772 m_remotePackages[key] = ret; 1773 return ret; 1774 } catch (Exception e) { 1775 logDiagnostic("Package %s could not be downloaded from %s: %s", rootpack, ps.description, e.msg); 1776 logDebug("Full error: %s", e.toString().sanitize); 1777 } 1778 } 1779 } 1780 1781 m_remotePackages[key] = null; 1782 1783 logWarn("Package %s %s could not be loaded either locally, or from the configured package registries.", name, dep); 1784 return null; 1785 } 1786 } 1787 1788 /** 1789 * An instance of Dub that does not rely on the environment 1790 * 1791 * This instance of dub should not read any environment variables, 1792 * nor should it do any file IO, to make it usable and reliable in unittests. 1793 * Currently it reads environment variables but does not read the configuration. 1794 */ 1795 package final class TestDub : Dub 1796 { 1797 /// Forward to base constructor 1798 public this (string root = ".", PackageSupplier[] extras = null, 1799 SkipPackageSuppliers skip = SkipPackageSuppliers.none) 1800 { 1801 super(root, extras, skip); 1802 } 1803 1804 /// Avoid loading user configuration 1805 protected override void loadConfig() { /* No-op */ } 1806 } 1807 1808 private struct SpecialDirs { 1809 /// The path where to store temporary files and directory 1810 NativePath temp; 1811 /// The system-wide dub-specific folder 1812 NativePath systemSettings; 1813 /// The dub-specific folder in the user home directory 1814 NativePath userSettings; 1815 /** 1816 * Windows-only: the local, user-specific folder 1817 * 1818 * This folder, unlike `userSettings`, does not roam, IOW an account 1819 * on a company network will not save the content of this data, 1820 * unlike `userSettings`. 1821 * On Posix, this is equivalent to `userSettings`. 1822 * 1823 * See_Also: https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid 1824 */ 1825 NativePath localRepository; 1826 1827 /// Returns: An instance of `SpecialDirs` initialized from the environment 1828 public static SpecialDirs make () { 1829 import std.file : tempDir; 1830 1831 SpecialDirs result; 1832 result.temp = NativePath(tempDir); 1833 1834 version(Windows) { 1835 result.systemSettings = NativePath(environment.get("ProgramData")) ~ "dub/"; 1836 immutable appDataDir = environment.get("APPDATA"); 1837 result.userSettings = NativePath(appDataDir) ~ "dub/"; 1838 // LOCALAPPDATA is not defined before Windows Vista 1839 result.localRepository = NativePath(environment.get("LOCALAPPDATA", appDataDir)) ~ "dub"; 1840 } else version(Posix) { 1841 result.systemSettings = NativePath("/var/lib/dub/"); 1842 result.userSettings = NativePath(environment.get("HOME")) ~ ".dub/"; 1843 if (!result.userSettings.absolute) 1844 result.userSettings = NativePath(getcwd()) ~ result.userSettings; 1845 result.localRepository = result.userSettings; 1846 } 1847 return result; 1848 } 1849 } 1850 1851 /** 1852 * User-provided configuration 1853 * 1854 * All fields in this struct should be optional. 1855 * Fields that are *not* optional should be mandatory from the POV 1856 * of the application, not the POV of file parsing. 1857 * For example, git's `core.author` and `core.email` are required to commit, 1858 * but the error happens on the commit, not when the gitconfig is parsed. 1859 * 1860 * We have multiple configuration locations, and two kinds of fields: 1861 * additive and non-additive. Additive fields are fields which are the union 1862 * of all configuration files (e.g. `registryURLs`). Non-additive fields 1863 * will ignore values set in lower priorities configuration, although parsing 1864 * must still succeed. Additive fields are marked as `@Optional`, 1865 * non-additive are marked as `SetInfo`. 1866 */ 1867 private struct UserConfiguration { 1868 import configy.Attributes; 1869 1870 @Optional string[] registryUrls; 1871 @Optional NativePath[] customCachePaths; 1872 1873 SetInfo!(SkipPackageSuppliers) skipRegistry; 1874 SetInfo!(string) defaultCompiler; 1875 SetInfo!(string) defaultArchitecture; 1876 SetInfo!(bool) defaultLowMemory; 1877 1878 SetInfo!(string[string]) defaultEnvironments; 1879 SetInfo!(string[string]) defaultBuildEnvironments; 1880 SetInfo!(string[string]) defaultRunEnvironments; 1881 SetInfo!(string[string]) defaultPreGenerateEnvironments; 1882 SetInfo!(string[string]) defaultPostGenerateEnvironments; 1883 SetInfo!(string[string]) defaultPreBuildEnvironments; 1884 SetInfo!(string[string]) defaultPostBuildEnvironments; 1885 SetInfo!(string[string]) defaultPreRunEnvironments; 1886 SetInfo!(string[string]) defaultPostRunEnvironments; 1887 SetInfo!(string) dubHome; 1888 1889 /// Merge a lower priority config (`this`) with a `higher` priority config 1890 public UserConfiguration merge(UserConfiguration higher) 1891 return @safe pure nothrow 1892 { 1893 import std.traits : hasUDA; 1894 UserConfiguration result; 1895 1896 static foreach (idx, _; UserConfiguration.tupleof) { 1897 static if (hasUDA!(UserConfiguration.tupleof[idx], Optional)) 1898 result.tupleof[idx] = higher.tupleof[idx] ~ this.tupleof[idx]; 1899 else static if (IsSetInfo!(typeof(this.tupleof[idx]))) { 1900 if (higher.tupleof[idx].set) 1901 result.tupleof[idx] = higher.tupleof[idx]; 1902 else 1903 result.tupleof[idx] = this.tupleof[idx]; 1904 } else 1905 static assert(false, 1906 "Expect `@Optional` or `SetInfo` on: `" ~ 1907 __traits(identifier, this.tupleof[idx]) ~ 1908 "` of type : `" ~ 1909 typeof(this.tupleof[idx]).stringof ~ "`"); 1910 } 1911 1912 return result; 1913 } 1914 1915 /// Workaround multiple `E` declaration in `static foreach` when inline 1916 private template IsSetInfo(T) { enum bool IsSetInfo = is(T : SetInfo!E, E); } 1917 } 1918 1919 unittest { 1920 import configy.Read; 1921 1922 const str1 = `{ 1923 "registryUrls": [ "http://foo.bar\/optional\/escape" ], 1924 "customCachePaths": [ "foo/bar", "foo/foo" ], 1925 1926 "skipRegistry": "all", 1927 "defaultCompiler": "dmd", 1928 "defaultArchitecture": "fooarch", 1929 "defaultLowMemory": false, 1930 1931 "defaultEnvironments": { 1932 "VAR2": "settings.VAR2", 1933 "VAR3": "settings.VAR3", 1934 "VAR4": "settings.VAR4" 1935 } 1936 }`; 1937 1938 const str2 = `{ 1939 "registryUrls": [ "http://bar.foo" ], 1940 "customCachePaths": [ "bar/foo", "bar/bar" ], 1941 1942 "skipRegistry": "none", 1943 "defaultCompiler": "ldc", 1944 "defaultArchitecture": "bararch", 1945 "defaultLowMemory": true, 1946 1947 "defaultEnvironments": { 1948 "VAR": "Hi", 1949 } 1950 }`; 1951 1952 auto c1 = parseConfigString!UserConfiguration(str1, "/dev/null"); 1953 assert(c1.registryUrls == [ "http://foo.bar/optional/escape" ]); 1954 assert(c1.customCachePaths == [ NativePath("foo/bar"), NativePath("foo/foo") ]); 1955 assert(c1.skipRegistry == SkipPackageSuppliers.all); 1956 assert(c1.defaultCompiler == "dmd"); 1957 assert(c1.defaultArchitecture == "fooarch"); 1958 assert(c1.defaultLowMemory == false); 1959 assert(c1.defaultEnvironments.length == 3); 1960 assert(c1.defaultEnvironments["VAR2"] == "settings.VAR2"); 1961 assert(c1.defaultEnvironments["VAR3"] == "settings.VAR3"); 1962 assert(c1.defaultEnvironments["VAR4"] == "settings.VAR4"); 1963 1964 auto c2 = parseConfigString!UserConfiguration(str2, "/dev/null"); 1965 assert(c2.registryUrls == [ "http://bar.foo" ]); 1966 assert(c2.customCachePaths == [ NativePath("bar/foo"), NativePath("bar/bar") ]); 1967 assert(c2.skipRegistry == SkipPackageSuppliers.none); 1968 assert(c2.defaultCompiler == "ldc"); 1969 assert(c2.defaultArchitecture == "bararch"); 1970 assert(c2.defaultLowMemory == true); 1971 assert(c2.defaultEnvironments.length == 1); 1972 assert(c2.defaultEnvironments["VAR"] == "Hi"); 1973 1974 auto m1 = c2.merge(c1); 1975 // c1 takes priority, so its registryUrls is first 1976 assert(m1.registryUrls == [ "http://foo.bar/optional/escape", "http://bar.foo" ]); 1977 // Same with CCP 1978 assert(m1.customCachePaths == [ 1979 NativePath("foo/bar"), NativePath("foo/foo"), 1980 NativePath("bar/foo"), NativePath("bar/bar"), 1981 ]); 1982 1983 // c1 fields only 1984 assert(m1.skipRegistry == c1.skipRegistry); 1985 assert(m1.defaultCompiler == c1.defaultCompiler); 1986 assert(m1.defaultArchitecture == c1.defaultArchitecture); 1987 assert(m1.defaultLowMemory == c1.defaultLowMemory); 1988 assert(m1.defaultEnvironments == c1.defaultEnvironments); 1989 1990 auto m2 = c1.merge(c2); 1991 assert(m2.registryUrls == [ "http://bar.foo", "http://foo.bar/optional/escape" ]); 1992 assert(m2.customCachePaths == [ 1993 NativePath("bar/foo"), NativePath("bar/bar"), 1994 NativePath("foo/bar"), NativePath("foo/foo"), 1995 ]); 1996 assert(m2.skipRegistry == c2.skipRegistry); 1997 assert(m2.defaultCompiler == c2.defaultCompiler); 1998 assert(m2.defaultArchitecture == c2.defaultArchitecture); 1999 assert(m2.defaultLowMemory == c2.defaultLowMemory); 2000 assert(m2.defaultEnvironments == c2.defaultEnvironments); 2001 2002 auto m3 = UserConfiguration.init.merge(c1); 2003 assert(m3 == c1); 2004 }