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