1 /** 2 Contains high-level functionality for working with packages. 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, Martin Nowak, Nick Sabalausky 7 */ 8 module dub.package_; 9 10 public import dub.recipe.packagerecipe; 11 12 import dub.compilers.compiler; 13 import dub.dependency; 14 import dub.description; 15 import dub.recipe.json; 16 import dub.recipe.sdl; 17 18 import dub.internal.utils; 19 import dub.internal.vibecompat.core.log; 20 import dub.internal.vibecompat.core.file; 21 import dub.internal.vibecompat.data.json; 22 import dub.internal.vibecompat.inet.url; 23 24 import std.algorithm; 25 import std.array; 26 import std.conv; 27 import std.exception; 28 import std.file; 29 import std.range; 30 import std.string; 31 import std.typecons : Nullable; 32 33 34 /// Lists the supported package recipe formats. 35 enum PackageFormat { 36 json, /// JSON based, using the ".json" file extension 37 sdl /// SDLang based, using the ".sdl" file extension 38 } 39 40 struct FilenameAndFormat { 41 string filename; 42 PackageFormat format; 43 } 44 45 /// Supported package descriptions in decreasing order of preference. 46 static immutable FilenameAndFormat[] packageInfoFiles = [ 47 {"dub.json", PackageFormat.json}, 48 {"dub.sdl", PackageFormat.sdl}, 49 {"package.json", PackageFormat.json} 50 ]; 51 52 /// Returns a list of all recognized package recipe file names in descending order of precedence. 53 @property string[] packageInfoFilenames() { return packageInfoFiles.map!(f => cast(string)f.filename).array; } 54 55 /// Returns the default package recile file name. 56 @property string defaultPackageFilename() { return packageInfoFiles[0].filename; } 57 58 59 /** Represents a package, including its sub packages. 60 */ 61 class Package { 62 private { 63 Path m_path; 64 Path m_infoFile; 65 PackageRecipe m_info; 66 PackageRecipe m_rawRecipe; 67 Package m_parentPackage; 68 } 69 70 /** Constructs a `Package` using an in-memory package recipe. 71 72 Params: 73 json_recipe = The package recipe in JSON format 74 recipe = The package recipe in generic format 75 root = The directory in which the package resides (if any). 76 parent = Reference to the parent package, if the new package is a 77 sub package. 78 version_override = Optional version to associate to the package 79 instead of the one declared in the package recipe, or the one 80 determined by invoking the VCS (GIT currently). 81 */ 82 this(Json json_recipe, Path root = Path(), Package parent = null, string version_override = "") 83 { 84 import dub.recipe.json; 85 86 PackageRecipe recipe; 87 parseJson(recipe, json_recipe, parent ? parent.name : null); 88 this(recipe, root, parent, version_override); 89 } 90 /// ditto 91 this(PackageRecipe recipe, Path root = Path(), Package parent = null, string version_override = "") 92 { 93 // save the original recipe 94 m_rawRecipe = recipe.clone; 95 96 if (!version_override.empty) 97 recipe.version_ = version_override; 98 99 // try to run git to determine the version of the package if no explicit version was given 100 if (recipe.version_.length == 0 && !parent) { 101 try recipe.version_ = determineVersionFromSCM(root); 102 catch (Exception e) logDebug("Failed to determine version by SCM: %s", e.msg); 103 104 if (recipe.version_.length == 0) { 105 logDiagnostic("Note: Failed to determine version of package %s at %s. Assuming ~master.", recipe.name, this.path.toNativeString()); 106 // TODO: Assume unknown version here? 107 // recipe.version_ = Version.unknown.toString(); 108 recipe.version_ = Version.masterBranch.toString(); 109 } else logDiagnostic("Determined package version using GIT: %s %s", recipe.name, recipe.version_); 110 } 111 112 m_parentPackage = parent; 113 m_path = root; 114 m_path.endsWithSlash = true; 115 116 // use the given recipe as the basis 117 m_info = recipe; 118 119 fillWithDefaults(); 120 simpleLint(); 121 } 122 123 /** Searches the given directory for package recipe files. 124 125 Params: 126 directory = The directory to search 127 128 Returns: 129 Returns the full path to the package file, if any was found. 130 Otherwise returns an empty path. 131 */ 132 static Path findPackageFile(Path directory) 133 { 134 foreach (file; packageInfoFiles) { 135 auto filename = directory ~ file.filename; 136 if (existsFile(filename)) return filename; 137 } 138 return Path.init; 139 } 140 141 /** Constructs a `Package` using a package that is physically present on the local file system. 142 143 Params: 144 root = The directory in which the package resides. 145 recipe_file = Optional path to the package recipe file. If left 146 empty, the `root` directory will be searched for a recipe file. 147 parent = Reference to the parent package, if the new package is a 148 sub package. 149 version_override = Optional version to associate to the package 150 instead of the one declared in the package recipe, or the one 151 determined by invoking the VCS (GIT currently). 152 */ 153 static Package load(Path root, Path recipe_file = Path.init, Package parent = null, string version_override = "") 154 { 155 import dub.recipe.io; 156 157 if (recipe_file.empty) recipe_file = findPackageFile(root); 158 159 enforce(!recipe_file.empty, 160 "No package file found in %s, expected one of %s" 161 .format(root.toNativeString(), 162 packageInfoFiles.map!(f => cast(string)f.filename).join("/"))); 163 164 auto recipe = readPackageRecipe(recipe_file, parent ? parent.name : null); 165 166 auto ret = new Package(recipe, root, parent, version_override); 167 ret.m_infoFile = recipe_file; 168 return ret; 169 } 170 171 /** Returns the qualified name of the package. 172 173 The qualified name includes any possible parent package if this package 174 is a sub package. 175 */ 176 @property string name() 177 const { 178 if (m_parentPackage) return m_parentPackage.name ~ ":" ~ m_info.name; 179 else return m_info.name; 180 } 181 182 /** Returns the directory in which the package resides. 183 184 Note that this can be empty for packages that are not stored in the 185 local file system. 186 */ 187 @property Path path() const { return m_path; } 188 189 190 /** Accesses the version associated with this package. 191 192 Note that this is a shortcut to `this.recipe.version_`. 193 */ 194 @property Version version_() const { return m_parentPackage ? m_parentPackage.version_ : Version(m_info.version_); } 195 /// ditto 196 @property void version_(Version value) { assert(m_parentPackage is null); m_info.version_ = value.toString(); } 197 198 /** Accesses the recipe contents of this package. 199 200 The recipe contains any default values and configurations added by DUB. 201 To access the raw user recipe, use the `rawRecipe` property. 202 203 See_Also: `rawRecipe` 204 */ 205 @property ref inout(PackageRecipe) recipe() inout { return m_info; } 206 207 /** Accesses the original package recipe. 208 209 The returned recipe matches exactly the contents of the original package 210 recipe. For the effective package recipe, augmented with DUB generated 211 default settings and configurations, use the `recipe` property. 212 213 See_Also: `recipe` 214 */ 215 @property ref const(PackageRecipe) rawRecipe() const { return m_rawRecipe; } 216 217 /** Returns the path to the package recipe file. 218 219 Note that this can be empty for packages that are not stored in the 220 local file system. 221 */ 222 @property Path recipePath() const { return m_infoFile; } 223 224 225 /** Returns the base package of this package. 226 227 The base package is the root of the sub package hierarchy (i.e. the 228 topmost parent). This will be `null` for packages that are not sub 229 packages. 230 */ 231 @property inout(Package) basePackage() inout { return m_parentPackage ? m_parentPackage.basePackage : this; } 232 233 /** Returns the parent of this package. 234 235 The parent package is the package that contains a sub package. This will 236 be `null` for packages that are not sub packages. 237 */ 238 @property inout(Package) parentPackage() inout { return m_parentPackage; } 239 240 /** Returns the list of all sub packages. 241 242 Note that this is a shortcut for `this.recipe.subPackages`. 243 */ 244 @property inout(SubPackage)[] subPackages() inout { return m_info.subPackages; } 245 246 /** Returns the list of all build configuration names. 247 248 Configuration contents can be accessed using `this.recipe.configurations`. 249 */ 250 @property string[] configurations() 251 const { 252 auto ret = appender!(string[])(); 253 foreach (ref config; m_info.configurations) 254 ret.put(config.name); 255 return ret.data; 256 } 257 258 /** Writes the current recipe contents to a recipe file. 259 260 The parameter-less overload writes to `this.path`, which must not be 261 empty. The default recipe file name will be used in this case. 262 */ 263 void storeInfo() 264 { 265 storeInfo(m_path); 266 m_infoFile = m_path ~ defaultPackageFilename; 267 } 268 /// ditto 269 void storeInfo(Path path) 270 const { 271 enforce(!version_.isUnknown, "Trying to store a package with an 'unknown' version, this is not supported."); 272 auto filename = path ~ defaultPackageFilename; 273 auto dstFile = openFile(filename.toNativeString(), FileMode.createTrunc); 274 scope(exit) dstFile.close(); 275 dstFile.writePrettyJsonString(m_info.toJson()); 276 } 277 278 /** Returns the package recipe of a non-path-based sub package. 279 280 For sub packages that are declared within the package recipe of the 281 parent package, this function will return the corresponding recipe. Sub 282 packages declared using a path must be loaded manually (or using the 283 `PackageManager`). 284 */ 285 Nullable!PackageRecipe getInternalSubPackage(string name) 286 { 287 foreach (ref p; m_info.subPackages) 288 if (p.path.empty && p.recipe.name == name) 289 return Nullable!PackageRecipe(p.recipe); 290 return Nullable!PackageRecipe(); 291 } 292 293 /** Searches for use of compiler-specific flags that have generic 294 alternatives. 295 296 This will output a warning message for each such flag to the console. 297 */ 298 void warnOnSpecialCompilerFlags() 299 { 300 // warn about use of special flags 301 m_info.buildSettings.warnOnSpecialCompilerFlags(m_info.name, null); 302 foreach (ref config; m_info.configurations) 303 config.buildSettings.warnOnSpecialCompilerFlags(m_info.name, config.name); 304 } 305 306 /** Retrieves a build settings template. 307 308 If no `config` is given, this returns the build settings declared at the 309 root level of the package recipe. Otherwise returns the settings 310 declared within the given configuration (excluding those at the root 311 level). 312 313 Note that this is a shortcut to accessing `this.recipe.buildSettings` or 314 `this.recipe.configurations[].buildSettings`. 315 */ 316 const(BuildSettingsTemplate) getBuildSettings(string config = null) 317 const { 318 if (config.length) { 319 foreach (ref conf; m_info.configurations) 320 if (conf.name == config) 321 return conf.buildSettings; 322 assert(false, "Unknown configuration: "~config); 323 } else { 324 return m_info.buildSettings; 325 } 326 } 327 328 /** Returns all BuildSettings for the given platform and configuration. 329 330 This will gather the effective build settings declared in tha package 331 recipe for when building on a particular platform and configuration. 332 Root build settings and configuration specific settings will be 333 merged. 334 */ 335 BuildSettings getBuildSettings(in BuildPlatform platform, string config) 336 const { 337 BuildSettings ret; 338 m_info.buildSettings.getPlatformSettings(ret, platform, this.path); 339 bool found = false; 340 foreach(ref conf; m_info.configurations){ 341 if( conf.name != config ) continue; 342 conf.buildSettings.getPlatformSettings(ret, platform, this.path); 343 found = true; 344 break; 345 } 346 assert(found || config is null, "Unknown configuration for "~m_info.name~": "~config); 347 348 // construct default target name based on package name 349 if( ret.targetName.empty ) ret.targetName = this.name.replace(":", "_"); 350 351 // special support for DMD style flags 352 getCompiler("dmd").extractBuildOptions(ret); 353 354 return ret; 355 } 356 357 /** Returns the combination of all build settings for all configurations 358 and platforms. 359 360 This can be useful for IDEs to gather a list of all potentially used 361 files or settings. 362 */ 363 BuildSettings getCombinedBuildSettings() 364 const { 365 BuildSettings ret; 366 m_info.buildSettings.getPlatformSettings(ret, BuildPlatform.any, this.path); 367 foreach(ref conf; m_info.configurations) 368 conf.buildSettings.getPlatformSettings(ret, BuildPlatform.any, this.path); 369 370 // construct default target name based on package name 371 if (ret.targetName.empty) ret.targetName = this.name.replace(":", "_"); 372 373 // special support for DMD style flags 374 getCompiler("dmd").extractBuildOptions(ret); 375 376 return ret; 377 } 378 379 /** Adds build type specific settings to an existing set of build settings. 380 381 This function searches the package recipe for overridden build types. If 382 none is found, the default build settings will be applied, if 383 `build_type` matches a default build type name. An exception is thrown 384 otherwise. 385 */ 386 void addBuildTypeSettings(ref BuildSettings settings, in BuildPlatform platform, string build_type) 387 const { 388 if (build_type == "$DFLAGS") { 389 import std.process; 390 string dflags = environment.get("DFLAGS"); 391 settings.addDFlags(dflags.split()); 392 return; 393 } 394 395 if (auto pbt = build_type in m_info.buildTypes) { 396 logDiagnostic("Using custom build type '%s'.", build_type); 397 pbt.getPlatformSettings(settings, platform, this.path); 398 } else { 399 with(BuildOption) switch (build_type) { 400 default: throw new Exception(format("Unknown build type for %s: '%s'", this.name, build_type)); 401 case "plain": break; 402 case "debug": settings.addOptions(debugMode, debugInfo); break; 403 case "release": settings.addOptions(releaseMode, optimize, inline); break; 404 case "release-debug": settings.addOptions(releaseMode, optimize, inline, debugInfo); break; 405 case "release-nobounds": settings.addOptions(releaseMode, optimize, inline, noBoundsCheck); break; 406 case "unittest": settings.addOptions(unittests, debugMode, debugInfo); break; 407 case "docs": settings.addOptions(syntaxOnly, _docs); break; 408 case "ddox": settings.addOptions(syntaxOnly, _ddox); break; 409 case "profile": settings.addOptions(profile, optimize, inline, debugInfo); break; 410 case "profile-gc": settings.addOptions(profileGC, debugInfo); break; 411 case "cov": settings.addOptions(coverage, debugInfo); break; 412 case "unittest-cov": settings.addOptions(unittests, coverage, debugMode, debugInfo); break; 413 } 414 } 415 } 416 417 /** Returns the selected configuration for a certain dependency. 418 419 If no configuration is specified in the package recipe, null will be 420 returned instead. 421 422 FIXME: The `platform` parameter is currently ignored, as the 423 `"subConfigurations"` field doesn't support platform suffixes. 424 */ 425 string getSubConfiguration(string config, in Package dependency, in BuildPlatform platform) 426 const { 427 bool found = false; 428 foreach(ref c; m_info.configurations){ 429 if( c.name == config ){ 430 if( auto pv = dependency.name in c.buildSettings.subConfigurations ) return *pv; 431 found = true; 432 break; 433 } 434 } 435 assert(found || config is null, "Invalid configuration \""~config~"\" for "~this.name); 436 if( auto pv = dependency.name in m_info.buildSettings.subConfigurations ) return *pv; 437 return null; 438 } 439 440 /** Returns the default configuration to build for the given platform. 441 442 This will return the first configuration that is applicable to the given 443 platform, or `null` if none is applicable. By default, only library 444 configurations will be returned. Setting `allow_non_library` to `true` 445 will also return executable configurations. 446 447 See_Also: `getPlatformConfigurations` 448 */ 449 string getDefaultConfiguration(in BuildPlatform platform, bool allow_non_library = false) 450 const { 451 foreach (ref conf; m_info.configurations) { 452 if (!conf.matchesPlatform(platform)) continue; 453 if (!allow_non_library && conf.buildSettings.targetType == TargetType.executable) continue; 454 return conf.name; 455 } 456 return null; 457 } 458 459 /** Returns a list of configurations suitable for the given platform. 460 461 Params: 462 platform = The platform against which to match configurations 463 allow_non_library = If set to true, executable configurations will 464 also be included. 465 466 See_Also: `getDefaultConfiguration` 467 */ 468 string[] getPlatformConfigurations(in BuildPlatform platform, bool allow_non_library = false) 469 const { 470 auto ret = appender!(string[]); 471 foreach(ref conf; m_info.configurations){ 472 if (!conf.matchesPlatform(platform)) continue; 473 if (!allow_non_library && conf.buildSettings.targetType == TargetType.executable) continue; 474 ret ~= conf.name; 475 } 476 if (ret.data.length == 0) ret.put(null); 477 return ret.data; 478 } 479 480 /** Determines if the package has a dependency to a certain package. 481 482 Params: 483 dependency_name = The name of the package to search for 484 config = Name of the configuration to use when searching 485 for dependencies 486 487 See_Also: `getDependencies` 488 */ 489 bool hasDependency(string dependency_name, string config) 490 const { 491 if (dependency_name in m_info.buildSettings.dependencies) return true; 492 foreach (ref c; m_info.configurations) 493 if ((config.empty || c.name == config) && dependency_name in c.buildSettings.dependencies) 494 return true; 495 return false; 496 } 497 498 /** Retrieves all dependencies for a particular configuration. 499 500 This includes dependencies that are declared at the root level of the 501 package recipe, as well as those declared within the specified 502 configuration. If no configuration with the given name exists, only 503 dependencies declared at the root level will be retunred. 504 505 See_Also: `hasDependency` 506 */ 507 const(Dependency[string]) getDependencies(string config) 508 const { 509 Dependency[string] ret; 510 foreach (k, v; m_info.buildSettings.dependencies) 511 ret[k] = v; 512 foreach (ref conf; m_info.configurations) 513 if (conf.name == config) { 514 foreach (k, v; conf.buildSettings.dependencies) 515 ret[k] = v; 516 break; 517 } 518 return ret; 519 } 520 521 /** Returns a list of all possible dependencies of the package. 522 523 This list includes all dependencies of all configurations. The same 524 package may occur multiple times with possibly different `Dependency` 525 values. 526 */ 527 PackageDependency[] getAllDependencies() 528 const { 529 auto ret = appender!(PackageDependency[]); 530 foreach (n, d; this.recipe.buildSettings.dependencies) 531 ret ~= PackageDependency(n, d); 532 foreach (ref c; this.recipe.configurations) 533 foreach (n, d; c.buildSettings.dependencies) 534 ret ~= PackageDependency(n, d); 535 return ret.data; 536 } 537 538 539 /** Returns a description of the package for use in IDEs or build tools. 540 */ 541 PackageDescription describe(BuildPlatform platform, string config) 542 const { 543 return describe(platform, getCompiler(platform.compilerBinary), config); 544 } 545 /// ditto 546 PackageDescription describe(BuildPlatform platform, Compiler compiler, string config) 547 const { 548 PackageDescription ret; 549 ret.configuration = config; 550 ret.path = m_path.toNativeString(); 551 ret.name = this.name; 552 ret.version_ = this.version_; 553 ret.description = m_info.description; 554 ret.homepage = m_info.homepage; 555 ret.authors = m_info.authors.dup; 556 ret.copyright = m_info.copyright; 557 ret.license = m_info.license; 558 ret.dependencies = getDependencies(config).keys; 559 560 // save build settings 561 BuildSettings bs = getBuildSettings(platform, config); 562 BuildSettings allbs = getCombinedBuildSettings(); 563 564 ret.targetType = bs.targetType; 565 ret.targetPath = bs.targetPath; 566 ret.targetName = bs.targetName; 567 if (ret.targetType != TargetType.none && compiler) 568 ret.targetFileName = compiler.getTargetFileName(bs, platform); 569 ret.workingDirectory = bs.workingDirectory; 570 ret.mainSourceFile = bs.mainSourceFile; 571 ret.dflags = bs.dflags; 572 ret.lflags = bs.lflags; 573 ret.libs = bs.libs; 574 ret.copyFiles = bs.copyFiles; 575 ret.versions = bs.versions; 576 ret.debugVersions = bs.debugVersions; 577 ret.importPaths = bs.importPaths; 578 ret.stringImportPaths = bs.stringImportPaths; 579 ret.preGenerateCommands = bs.preGenerateCommands; 580 ret.postGenerateCommands = bs.postGenerateCommands; 581 ret.preBuildCommands = bs.preBuildCommands; 582 ret.postBuildCommands = bs.postBuildCommands; 583 584 // prettify build requirements output 585 for (int i = 1; i <= BuildRequirement.max; i <<= 1) 586 if (bs.requirements & cast(BuildRequirement)i) 587 ret.buildRequirements ~= cast(BuildRequirement)i; 588 589 // prettify options output 590 for (int i = 1; i <= BuildOption.max; i <<= 1) 591 if (bs.options & cast(BuildOption)i) 592 ret.options ~= cast(BuildOption)i; 593 594 // collect all possible source files and determine their types 595 SourceFileRole[string] sourceFileTypes; 596 foreach (f; allbs.stringImportFiles) sourceFileTypes[f] = SourceFileRole.unusedStringImport; 597 foreach (f; allbs.importFiles) sourceFileTypes[f] = SourceFileRole.unusedImport; 598 foreach (f; allbs.sourceFiles) sourceFileTypes[f] = SourceFileRole.unusedSource; 599 foreach (f; bs.stringImportFiles) sourceFileTypes[f] = SourceFileRole.stringImport; 600 foreach (f; bs.importFiles) sourceFileTypes[f] = SourceFileRole.import_; 601 foreach (f; bs.sourceFiles) sourceFileTypes[f] = SourceFileRole.source; 602 foreach (f; sourceFileTypes.byKey.array.sort()) { 603 SourceFileDescription sf; 604 sf.path = f; 605 sf.role = sourceFileTypes[f]; 606 ret.files ~= sf; 607 } 608 609 return ret; 610 } 611 612 private void fillWithDefaults() 613 { 614 auto bs = &m_info.buildSettings; 615 616 // check for default string import folders 617 if ("" !in bs.stringImportPaths) { 618 foreach(defvf; ["views"]){ 619 if( existsFile(m_path ~ defvf) ) 620 bs.stringImportPaths[""] ~= defvf; 621 } 622 } 623 624 // check for default source folders 625 immutable hasSP = ("" in bs.sourcePaths) !is null; 626 immutable hasIP = ("" in bs.importPaths) !is null; 627 if (!hasSP || !hasIP) { 628 foreach (defsf; ["source/", "src/"]) { 629 if (existsFile(m_path ~ defsf)) { 630 if (!hasSP) bs.sourcePaths[""] ~= defsf; 631 if (!hasIP) bs.importPaths[""] ~= defsf; 632 } 633 } 634 } 635 636 // check for default app_main 637 string app_main_file; 638 auto pkg_name = m_info.name.length ? m_info.name : "unknown"; 639 foreach(sf; bs.sourcePaths.get("", null)){ 640 auto p = m_path ~ sf; 641 if( !existsFile(p) ) continue; 642 foreach(fil; ["app.d", "main.d", pkg_name ~ "/main.d", pkg_name ~ "/" ~ "app.d"]){ 643 if( existsFile(p ~ fil) ) { 644 app_main_file = (Path(sf) ~ fil).toNativeString(); 645 break; 646 } 647 } 648 } 649 650 // generate default configurations if none are defined 651 if (m_info.configurations.length == 0) { 652 if (bs.targetType == TargetType.executable) { 653 BuildSettingsTemplate app_settings; 654 app_settings.targetType = TargetType.executable; 655 if (bs.mainSourceFile.empty) app_settings.mainSourceFile = app_main_file; 656 m_info.configurations ~= ConfigurationInfo("application", app_settings); 657 } else if (bs.targetType != TargetType.none) { 658 BuildSettingsTemplate lib_settings; 659 lib_settings.targetType = bs.targetType == TargetType.autodetect ? TargetType.library : bs.targetType; 660 661 if (bs.targetType == TargetType.autodetect) { 662 if (app_main_file.length) { 663 lib_settings.excludedSourceFiles[""] ~= app_main_file; 664 665 BuildSettingsTemplate app_settings; 666 app_settings.targetType = TargetType.executable; 667 app_settings.mainSourceFile = app_main_file; 668 m_info.configurations ~= ConfigurationInfo("application", app_settings); 669 } 670 } 671 672 m_info.configurations ~= ConfigurationInfo("library", lib_settings); 673 } 674 } 675 } 676 677 private void simpleLint() const { 678 if (m_parentPackage) { 679 if (m_parentPackage.path != path) { 680 if (this.recipe.license.length && this.recipe.license != m_parentPackage.recipe.license) 681 logWarn("License in subpackage %s is different than it's parent package, this is discouraged.", name); 682 } 683 } 684 if (name.empty) logWarn("The package in %s has no name.", path); 685 } 686 } 687 688 private string determineVersionFromSCM(Path path) 689 { 690 // On Windows, which is slow at running external processes, 691 // cache the version numbers that are determined using 692 // GIT to speed up the initialization phase. 693 version (Windows) { 694 import std.file : exists, readText; 695 696 // quickly determine head commit without invoking GIT 697 string head_commit; 698 auto hpath = (path ~ ".git/HEAD").toNativeString(); 699 if (exists(hpath)) { 700 auto head_ref = readText(hpath).strip(); 701 if (head_ref.startsWith("ref: ")) { 702 auto rpath = (path ~ (".git/"~head_ref[5 .. $])).toNativeString(); 703 if (exists(rpath)) 704 head_commit = readText(rpath).strip(); 705 } 706 } 707 708 // return the last determined version for that commit 709 // not that this is not always correct, most notably when 710 // a tag gets added/removed/changed and changes the outcome 711 // of the full version detection computation 712 auto vcachepath = path ~ ".dub/version.json"; 713 if (existsFile(vcachepath)) { 714 auto ver = jsonFromFile(vcachepath); 715 if (head_commit == ver["commit"].opt!string) 716 return ver["version"].get!string; 717 } 718 } 719 720 // if no cache file or the HEAD commit changed, perform full detection 721 auto ret = determineVersionWithGIT(path); 722 723 version (Windows) { 724 // update version cache file 725 if (head_commit.length) { 726 if (!existsFile(path ~".dub")) createDirectory(path ~ ".dub"); 727 atomicWriteJsonFile(vcachepath, Json(["commit": Json(head_commit), "version": Json(ret)])); 728 } 729 } 730 731 return ret; 732 } 733 734 // determines the version of a package that is stored in a GIT working copy 735 // by invoking the "git" executable 736 private string determineVersionWithGIT(Path path) 737 { 738 import std.process; 739 import dub.semver; 740 741 auto git_dir = path ~ ".git"; 742 if (!existsFile(git_dir) || !isDir(git_dir.toNativeString)) return null; 743 auto git_dir_param = "--git-dir=" ~ git_dir.toNativeString(); 744 745 static string exec(scope string[] params...) { 746 auto ret = executeShell(escapeShellCommand(params)); 747 if (ret.status == 0) return ret.output.strip; 748 logDebug("'%s' failed with exit code %s: %s", params.join(" "), ret.status, ret.output.strip); 749 return null; 750 } 751 752 auto tag = exec("git", git_dir_param, "describe", "--long", "--tags"); 753 if (tag !is null) { 754 auto parts = tag.split("-"); 755 auto commit = parts[$-1]; 756 auto num = parts[$-2].to!int; 757 tag = parts[0 .. $-2].join("-"); 758 if (tag.startsWith("v") && isValidVersion(tag[1 .. $])) { 759 if (num == 0) return tag[1 .. $]; 760 else if (tag.canFind("+")) return format("%s.commit.%s.%s", tag[1 .. $], num, commit); 761 else return format("%s+commit.%s.%s", tag[1 .. $], num, commit); 762 } 763 } 764 765 auto branch = exec("git", git_dir_param, "rev-parse", "--abbrev-ref", "HEAD"); 766 if (branch !is null) { 767 if (branch != "HEAD") return "~" ~ branch; 768 } 769 770 return null; 771 }