1 /** 2 Abstract representation of a package description file. 3 4 Copyright: © 2012-2014 rejectedsoftware e.K. 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig, Matthias Dondorff 7 */ 8 module dub.recipe.packagerecipe; 9 10 import dub.compilers.compiler; 11 import dub.compilers.utils : warnOnSpecialCompilerFlags; 12 import dub.dependency; 13 import dub.internal.logging; 14 15 import dub.internal.vibecompat.core.file; 16 import dub.internal.vibecompat.inet.path; 17 18 import dub.internal.configy.Attributes; 19 20 import std.algorithm : findSplit, sort; 21 import std.array : join, split; 22 import std.exception : enforce; 23 import std.file; 24 import std.range; 25 import std.process : environment; 26 27 28 /** 29 Returns the individual parts of a qualified package name. 30 31 Sub qualified package names are lists of package names separated by ":". For 32 example, "packa:packb:packc" references a package named "packc" that is a 33 sub package of "packb", which in turn is a sub package of "packa". 34 */ 35 deprecated("This function is not supported as subpackages cannot be nested") 36 string[] getSubPackagePath(string package_name) @safe pure 37 { 38 return package_name.split(":"); 39 } 40 41 deprecated @safe unittest 42 { 43 assert(getSubPackagePath("packa:packb:packc") == ["packa", "packb", "packc"]); 44 assert(getSubPackagePath("pack") == ["pack"]); 45 } 46 47 /** 48 Returns the name of the top level package for a given (sub) package name of 49 format `"basePackageName"` or `"basePackageName:subPackageName"`. 50 51 In case of a top level package, the qualified name is returned unmodified. 52 */ 53 deprecated("Use `dub.dependency : PackageName(arg).main` instead") 54 string getBasePackageName(string package_name) @safe pure 55 { 56 return package_name.findSplit(":")[0]; 57 } 58 59 /** 60 Returns the qualified sub package part of the given package name of format 61 `"basePackageName:subPackageName"`, or empty string if none. 62 63 This is the part of the package name excluding the base package 64 name. See also $(D getBasePackageName). 65 */ 66 deprecated("Use `dub.dependency : PackageName(arg).sub` instead") 67 string getSubPackageName(string package_name) @safe pure 68 { 69 return package_name.findSplit(":")[2]; 70 } 71 72 deprecated @safe unittest 73 { 74 assert(getBasePackageName("packa:packb:packc") == "packa"); 75 assert(getBasePackageName("pack") == "pack"); 76 assert(getSubPackageName("packa:packb:packc") == "packb:packc"); 77 assert(getSubPackageName("pack") == ""); 78 } 79 80 /** 81 Represents the contents of a package recipe file (dub.json/dub.sdl) in an abstract way. 82 83 This structure is used to reason about package descriptions in isolation. 84 For higher level package handling, see the $(D Package) class. 85 */ 86 struct PackageRecipe { 87 /** 88 * Name of the package, used to uniquely identify the package. 89 * 90 * This field is the only mandatory one. 91 * Must be comprised of only lower case ASCII alpha-numeric characters, 92 * "-" or "_". 93 */ 94 string name; 95 96 /// Brief description of the package. 97 @Optional string description; 98 99 /// URL of the project website 100 @Optional string homepage; 101 102 /** 103 * List of project authors 104 * 105 * the suggested format is either: 106 * "Peter Parker" 107 * or 108 * "Peter Parker <pparker@example.com>" 109 */ 110 @Optional string[] authors; 111 112 /// Copyright declaration string 113 @Optional string copyright; 114 115 /// License(s) under which the project can be used 116 @Optional string license; 117 118 /// Set of version requirements for DUB, compilers and/or language frontend. 119 @Optional ToolchainRequirements toolchainRequirements; 120 121 /** 122 * Specifies an optional list of build configurations 123 * 124 * By default, the first configuration present in the package recipe 125 * will be used, except for special configurations (e.g. "unittest"). 126 * A specific configuration can be chosen from the command line using 127 * `--config=name` or `-c name`. A package can select a specific 128 * configuration in one of its dependency by using the `subConfigurations` 129 * build setting. 130 * Build settings defined at the top level affect all configurations. 131 */ 132 @Optional @Key("name") ConfigurationInfo[] configurations; 133 134 /** 135 * Defines additional custom build types or overrides the default ones 136 * 137 * Build types can be selected from the command line using `--build=name` 138 * or `-b name`. The default build type is `debug`. 139 */ 140 @Optional BuildSettingsTemplate[string] buildTypes; 141 142 /** 143 * Build settings influence the command line arguments and options passed 144 * to the compiler and linker. 145 * 146 * All build settings can be present at the top level, and are optional. 147 * Build settings can also be found in `configurations`. 148 */ 149 @Optional BuildSettingsTemplate buildSettings; 150 alias buildSettings this; 151 152 /** 153 * Specifies a list of command line flags usable for controlling 154 * filter behavior for `--build=ddox` [experimental] 155 */ 156 @Optional @Name("-ddoxFilterArgs") string[] ddoxFilterArgs; 157 158 /// Specify which tool to use with `--build=ddox` (experimental) 159 @Optional @Name("-ddoxTool") string ddoxTool; 160 161 /** 162 * Sub-packages path or definitions 163 * 164 * Sub-packages allow to break component of a large framework into smaller 165 * packages. In the recipe file, sub-packages entry can take one of two forms: 166 * either the path to a sub-folder where a recipe file exists, 167 * or an object of the same format as a recipe file (or `PackageRecipe`). 168 */ 169 @Optional SubPackage[] subPackages; 170 171 /// Usually unused by users, this is set by dub automatically 172 @Optional @Name("version") string version_; 173 174 inout(ConfigurationInfo) getConfiguration(string name) 175 inout { 176 foreach (c; configurations) 177 if (c.name == name) 178 return c; 179 throw new Exception("Unknown configuration: "~name); 180 } 181 182 /** Clones the package recipe recursively. 183 */ 184 PackageRecipe clone() const { return .clone(this); } 185 } 186 187 struct SubPackage 188 { 189 string path; 190 PackageRecipe recipe; 191 192 /** 193 * Given a YAML parser, recurses into `recipe` or use `path` 194 * depending on the node type. 195 * 196 * Two formats are supported for sub-packages: a string format, 197 * which is just the path to the sub-package, and embedding the 198 * full sub-package recipe into the parent package recipe. 199 * 200 * To support such a dual syntax, Configy requires the use 201 * of a `fromYAML` method, as it exposes the underlying format. 202 */ 203 static SubPackage fromYAML (scope ConfigParser!SubPackage p) 204 { 205 import dub.internal.dyaml.node; 206 207 if (p.node.nodeID == NodeID.mapping) 208 return SubPackage(null, p.parseAs!PackageRecipe); 209 else 210 return SubPackage(p.parseAs!string); 211 } 212 } 213 214 /// Describes minimal toolchain requirements 215 struct ToolchainRequirements 216 { 217 import std.typecons : Tuple, tuple; 218 219 // TODO: We can remove `@Optional` once bosagora/configy#30 is resolved, 220 // currently it fails because `Dependency.opCmp` is not CTFE-able. 221 222 /// DUB version requirement 223 @Optional @converter((scope ConfigParser!VersionRange p) => p.node.as!string.parseVersionRange) 224 VersionRange dub = VersionRange.Any; 225 /// D front-end version requirement 226 @Optional @converter((scope ConfigParser!VersionRange p) => p.node.as!string.parseDMDDependency) 227 VersionRange frontend = VersionRange.Any; 228 /// DMD version requirement 229 @Optional @converter((scope ConfigParser!VersionRange p) => p.node.as!string.parseDMDDependency) 230 VersionRange dmd = VersionRange.Any; 231 /// LDC version requirement 232 @Optional @converter((scope ConfigParser!VersionRange p) => p.node.as!string.parseVersionRange) 233 VersionRange ldc = VersionRange.Any; 234 /// GDC version requirement 235 @Optional @converter((scope ConfigParser!VersionRange p) => p.node.as!string.parseVersionRange) 236 VersionRange gdc = VersionRange.Any; 237 238 /** Get the list of supported compilers. 239 240 Returns: 241 An array of couples of compiler name and compiler requirement 242 */ 243 @property Tuple!(string, VersionRange)[] supportedCompilers() const 244 { 245 Tuple!(string, VersionRange)[] res; 246 if (dmd != VersionRange.Invalid) res ~= Tuple!(string, VersionRange)("dmd", dmd); 247 if (ldc != VersionRange.Invalid) res ~= Tuple!(string, VersionRange)("ldc", ldc); 248 if (gdc != VersionRange.Invalid) res ~= Tuple!(string, VersionRange)("gdc", gdc); 249 return res; 250 } 251 252 bool empty() 253 const { 254 import std.algorithm.searching : all; 255 return only(dub, frontend, dmd, ldc, gdc) 256 .all!(r => r == VersionRange.Any); 257 } 258 } 259 260 261 /// Bundles information about a build configuration. 262 struct ConfigurationInfo { 263 string name; 264 @Optional string[] platforms; 265 @Optional BuildSettingsTemplate buildSettings; 266 alias buildSettings this; 267 268 /** 269 * Equivalent to the default constructor, used by Configy 270 */ 271 this(string name, string[] p, BuildSettingsTemplate build_settings) 272 @safe pure nothrow @nogc 273 { 274 this.name = name; 275 this.platforms = p; 276 this.buildSettings = build_settings; 277 } 278 279 this(string name, BuildSettingsTemplate build_settings) 280 { 281 enforce(!name.empty, "Configuration name is empty."); 282 this.name = name; 283 this.buildSettings = build_settings; 284 } 285 286 bool matchesPlatform(in BuildPlatform platform) 287 const { 288 if( platforms.empty ) return true; 289 foreach(p; platforms) 290 if (platform.matchesSpecification(p)) 291 return true; 292 return false; 293 } 294 } 295 296 /** 297 * A dependency with possible `BuildSettingsTemplate` 298 * 299 * Currently only `dflags` is taken into account, but the parser accepts any 300 * value that is in `BuildSettingsTemplate`. 301 * This feature was originally introduced to support `-preview`, as setting 302 * a `-preview` in `dflags` does not propagate down to dependencies. 303 */ 304 public struct RecipeDependency 305 { 306 /// The dependency itself 307 public Dependency dependency; 308 309 /// Additional dflags, if any 310 public BuildSettingsTemplate settings; 311 312 /// Convenience alias as most uses just want to deal with the `Dependency` 313 public alias dependency this; 314 315 /** 316 * Read a `Dependency` and `BuildSettingsTemplate` from the config file 317 * 318 * Required to support both short and long form 319 */ 320 static RecipeDependency fromYAML (scope ConfigParser!RecipeDependency p) 321 { 322 import dub.internal.dyaml.node; 323 324 if (p.node.nodeID == NodeID.scalar) { 325 auto d = YAMLFormat(p.node.as!string); 326 return RecipeDependency(d.toDependency()); 327 } 328 auto d = p.parseAs!YAMLFormat; 329 return RecipeDependency(d.toDependency(), d.settings); 330 } 331 332 /// In-file representation of a dependency as specified by the user 333 private struct YAMLFormat 334 { 335 @Name("version") @Optional string version_; 336 @Optional string path; 337 @Optional string repository; 338 bool optional; 339 @Name("default") bool default_; 340 341 @Optional BuildSettingsTemplate settings; 342 alias settings this; 343 344 /** 345 * Used by Configy to provide rich error message when parsing. 346 * 347 * Exceptions thrown from `validate` methods will be wrapped with field/file 348 * information and rethrown from Configy, providing the user 349 * with the location of the configuration that triggered the error. 350 */ 351 public void validate () const 352 { 353 enforce(this.optional || !this.default_, 354 "Setting default to 'true' has no effect if 'optional' is not set"); 355 enforce(this.version_.length || this.path.length || this.repository.length, 356 "Need to provide one of the following fields: 'version', 'path', or 'repository'"); 357 358 enforce(!this.path.length || !this.repository.length, 359 "Cannot provide a 'path' dependency if a repository dependency is used"); 360 enforce(!this.repository.length || this.version_.length, 361 "Need to provide a commit hash in 'version' field with 'repository' dependency"); 362 363 // Need to deprecate this as it's fairly common 364 version (none) { 365 enforce(!this.path.length || !this.version_.length, 366 "Cannot provide a 'path' dependency if a 'version' dependency is used"); 367 } 368 } 369 370 /// Turns this struct into a `Dependency` 371 public Dependency toDependency () const 372 { 373 auto result = () { 374 if (this.path.length) 375 return Dependency(NativePath(this.path)); 376 if (this.repository.length) 377 return Dependency(Repository(this.repository, this.version_)); 378 return Dependency(VersionRange.fromString(this.version_)); 379 }(); 380 result.optional = this.optional; 381 result.default_ = this.default_; 382 return result; 383 } 384 } 385 } 386 387 /// Type used to avoid a breaking change when `Dependency[string]` 388 /// was changed to `RecipeDependency[string]` 389 package struct RecipeDependencyAA 390 { 391 /// The underlying data, `public` as `alias this` to `private` field doesn't 392 /// always work. 393 public RecipeDependency[string] data; 394 395 /// Expose base function, e.g. `clear` 396 alias data this; 397 398 /// Supports assignment from a `RecipeDependency` (used in the parser) 399 public void opIndexAssign(RecipeDependency dep, string key) 400 pure nothrow 401 { 402 this.data[key] = dep; 403 } 404 405 /// Supports assignment from a `Dependency`, used in user code mostly 406 public void opIndexAssign(Dependency dep, string key) 407 pure nothrow 408 { 409 this.data[key] = RecipeDependency(dep); 410 } 411 412 /// Configy doesn't like `alias this` to an AA 413 static RecipeDependencyAA fromYAML (scope ConfigParser!RecipeDependencyAA p) 414 { 415 return RecipeDependencyAA(p.parseAs!(typeof(this.data))); 416 } 417 } 418 419 /// This keeps general information about how to build a package. 420 /// It contains functions to create a specific BuildSetting, targeted at 421 /// a certain BuildPlatform. 422 struct BuildSettingsTemplate { 423 @Optional RecipeDependencyAA dependencies; 424 @Optional string systemDependencies; 425 @Optional TargetType targetType = TargetType.autodetect; 426 @Optional string targetPath; 427 @Optional string targetName; 428 @Optional string workingDirectory; 429 @Optional string mainSourceFile; 430 @Optional string[string] subConfigurations; 431 @StartsWith("dflags") string[][string] dflags; 432 @StartsWith("lflags") string[][string] lflags; 433 @StartsWith("libs") string[][string] libs; 434 @StartsWith("sourceFiles") string[][string] sourceFiles; 435 @StartsWith("sourcePaths") string[][string] sourcePaths; 436 @StartsWith("cSourcePaths") string[][string] cSourcePaths; 437 @StartsWith("excludedSourceFiles") string[][string] excludedSourceFiles; 438 @StartsWith("injectSourceFiles") string[][string] injectSourceFiles; 439 @StartsWith("copyFiles") string[][string] copyFiles; 440 @StartsWith("extraDependencyFiles") string[][string] extraDependencyFiles; 441 @StartsWith("versions") string[][string] versions; 442 @StartsWith("debugVersions") string[][string] debugVersions; 443 @StartsWith("versionFilters") string[][string] versionFilters; 444 @StartsWith("debugVersionFilters") string[][string] debugVersionFilters; 445 @StartsWith("importPaths") string[][string] importPaths; 446 @StartsWith("cImportPaths") string[][string] cImportPaths; 447 @StartsWith("stringImportPaths") string[][string] stringImportPaths; 448 @StartsWith("preGenerateCommands") string[][string] preGenerateCommands; 449 @StartsWith("postGenerateCommands") string[][string] postGenerateCommands; 450 @StartsWith("preBuildCommands") string[][string] preBuildCommands; 451 @StartsWith("postBuildCommands") string[][string] postBuildCommands; 452 @StartsWith("preRunCommands") string[][string] preRunCommands; 453 @StartsWith("postRunCommands") string[][string] postRunCommands; 454 @StartsWith("environments") string[string][string] environments; 455 @StartsWith("buildEnvironments")string[string][string] buildEnvironments; 456 @StartsWith("runEnvironments") string[string][string] runEnvironments; 457 @StartsWith("preGenerateEnvironments") string[string][string] preGenerateEnvironments; 458 @StartsWith("postGenerateEnvironments") string[string][string] postGenerateEnvironments; 459 @StartsWith("preBuildEnvironments") string[string][string] preBuildEnvironments; 460 @StartsWith("postBuildEnvironments") string[string][string] postBuildEnvironments; 461 @StartsWith("preRunEnvironments") string[string][string] preRunEnvironments; 462 @StartsWith("postRunEnvironments") string[string][string] postRunEnvironments; 463 464 @StartsWith("buildRequirements") @Optional 465 Flags!BuildRequirement[string] buildRequirements; 466 @StartsWith("buildOptions") @Optional 467 Flags!BuildOption[string] buildOptions; 468 469 470 BuildSettingsTemplate dup() const { 471 return clone(this); 472 } 473 474 /// Constructs a BuildSettings object from this template. 475 void getPlatformSettings(ref BuildSettings dst, in BuildPlatform platform, NativePath base_path) 476 const { 477 dst.targetType = this.targetType; 478 if (!this.targetPath.empty) dst.targetPath = this.targetPath; 479 if (!this.targetName.empty) dst.targetName = this.targetName; 480 if (!this.workingDirectory.empty) dst.workingDirectory = this.workingDirectory; 481 if (!this.mainSourceFile.empty) { 482 auto p = NativePath(this.mainSourceFile); 483 p.normalize(); 484 dst.mainSourceFile = p.toNativeString(); 485 dst.addSourceFiles(dst.mainSourceFile); 486 } 487 488 string[] collectFiles(in string[][string] paths_map, string pattern) 489 { 490 auto files = appender!(string[]); 491 492 import dub.project : buildSettingsVars; 493 import std.typecons : Nullable; 494 495 static Nullable!(string[string]) envVarCache; 496 497 if (envVarCache.isNull) envVarCache = environment.toAA(); 498 499 foreach (suffix, paths; paths_map) { 500 if (!platform.matchesSpecification(suffix)) 501 continue; 502 503 foreach (spath; paths) { 504 enforce(!spath.empty, "Paths must not be empty strings."); 505 auto path = NativePath(spath); 506 if (!path.absolute) path = base_path ~ path; 507 if (!existsDirectory(path)) { 508 import std.algorithm : any, find; 509 const hasVar = chain(buildSettingsVars, envVarCache.get.byKey).any!((string var) { 510 return spath.find("$"~var).length > 0 || spath.find("${"~var~"}").length > 0; 511 }); 512 if (!hasVar) 513 logWarn("Invalid source/import path: %s", path.toNativeString()); 514 continue; 515 } 516 517 auto pstr = path.toNativeString(); 518 foreach (d; dirEntries(pstr, pattern, SpanMode.depth)) { 519 import std.path : baseName, pathSplitter; 520 import std.algorithm.searching : canFind; 521 // eliminate any hidden files, or files in hidden directories. But always include 522 // files that are listed inside hidden directories that are specifically added to 523 // the project. 524 if (d.isDir || pathSplitter(d.name[pstr.length .. $]) 525 .canFind!(name => name.length && name[0] == '.')) 526 continue; 527 auto src = NativePath(d.name).relativeTo(base_path); 528 files ~= src.toNativeString(); 529 } 530 } 531 } 532 533 return files.data; 534 } 535 536 // collect source files. Note: D source from 'sourcePaths' and C sources from 'cSourcePaths' are joint into 'sourceFiles' 537 dst.addSourceFiles(collectFiles(sourcePaths, "*.d")); 538 dst.addSourceFiles(collectFiles(cSourcePaths, "*.{c,i}")); 539 auto sourceFiles = dst.sourceFiles.sort(); 540 541 // collect import files and remove sources 542 import std.algorithm : copy, setDifference; 543 544 auto importFiles = 545 chain(collectFiles(importPaths, "*.{d,di}"), collectFiles(cImportPaths, "*.h")) 546 .array() 547 .sort(); 548 immutable nremoved = importFiles.setDifference(sourceFiles).copy(importFiles.release).length; 549 importFiles = importFiles[0 .. $ - nremoved]; 550 dst.addImportFiles(importFiles.release); 551 552 dst.addStringImportFiles(collectFiles(stringImportPaths, "*")); 553 554 getPlatformSetting!("dflags", "addDFlags")(dst, platform); 555 getPlatformSetting!("lflags", "addLFlags")(dst, platform); 556 getPlatformSetting!("libs", "addLibs")(dst, platform); 557 getPlatformSetting!("sourceFiles", "addSourceFiles")(dst, platform); 558 getPlatformSetting!("excludedSourceFiles", "removeSourceFiles")(dst, platform); 559 getPlatformSetting!("injectSourceFiles", "addInjectSourceFiles")(dst, platform); 560 getPlatformSetting!("copyFiles", "addCopyFiles")(dst, platform); 561 getPlatformSetting!("extraDependencyFiles", "addExtraDependencyFiles")(dst, platform); 562 getPlatformSetting!("versions", "addVersions")(dst, platform); 563 getPlatformSetting!("debugVersions", "addDebugVersions")(dst, platform); 564 getPlatformSetting!("versionFilters", "addVersionFilters")(dst, platform); 565 getPlatformSetting!("debugVersionFilters", "addDebugVersionFilters")(dst, platform); 566 getPlatformSetting!("importPaths", "addImportPaths")(dst, platform); 567 getPlatformSetting!("cImportPaths", "addCImportPaths")(dst, platform); 568 getPlatformSetting!("stringImportPaths", "addStringImportPaths")(dst, platform); 569 getPlatformSetting!("preGenerateCommands", "addPreGenerateCommands")(dst, platform); 570 getPlatformSetting!("postGenerateCommands", "addPostGenerateCommands")(dst, platform); 571 getPlatformSetting!("preBuildCommands", "addPreBuildCommands")(dst, platform); 572 getPlatformSetting!("postBuildCommands", "addPostBuildCommands")(dst, platform); 573 getPlatformSetting!("preRunCommands", "addPreRunCommands")(dst, platform); 574 getPlatformSetting!("postRunCommands", "addPostRunCommands")(dst, platform); 575 getPlatformSetting!("environments", "addEnvironments")(dst, platform); 576 getPlatformSetting!("buildEnvironments", "addBuildEnvironments")(dst, platform); 577 getPlatformSetting!("runEnvironments", "addRunEnvironments")(dst, platform); 578 getPlatformSetting!("preGenerateEnvironments", "addPreGenerateEnvironments")(dst, platform); 579 getPlatformSetting!("postGenerateEnvironments", "addPostGenerateEnvironments")(dst, platform); 580 getPlatformSetting!("preBuildEnvironments", "addPreBuildEnvironments")(dst, platform); 581 getPlatformSetting!("postBuildEnvironments", "addPostBuildEnvironments")(dst, platform); 582 getPlatformSetting!("preRunEnvironments", "addPreRunEnvironments")(dst, platform); 583 getPlatformSetting!("postRunEnvironments", "addPostRunEnvironments")(dst, platform); 584 getPlatformSetting!("buildRequirements", "addRequirements")(dst, platform); 585 getPlatformSetting!("buildOptions", "addOptions")(dst, platform); 586 } 587 588 void getPlatformSetting(string name, string addname)(ref BuildSettings dst, in BuildPlatform platform) 589 const { 590 foreach(suffix, values; __traits(getMember, this, name)){ 591 if( platform.matchesSpecification(suffix) ) 592 __traits(getMember, dst, addname)(values); 593 } 594 } 595 596 void warnOnSpecialCompilerFlags(string package_name, string config_name) 597 { 598 auto nodef = false; 599 auto noprop = false; 600 foreach (req; this.buildRequirements) { 601 if (req & BuildRequirement.noDefaultFlags) nodef = true; 602 if (req & BuildRequirement.relaxProperties) noprop = true; 603 } 604 605 if (noprop) { 606 logWarn(`Warning: "buildRequirements": ["relaxProperties"] is deprecated and is now the default behavior. Note that the -property switch will probably be removed in future versions of DMD.`); 607 logWarn(""); 608 } 609 610 if (nodef) { 611 logWarn("Warning: This package uses the \"noDefaultFlags\" build requirement. Please use only for development purposes and not for released packages."); 612 logWarn(""); 613 } else { 614 string[] all_dflags; 615 Flags!BuildOption all_options; 616 foreach (flags; this.dflags) all_dflags ~= flags; 617 foreach (options; this.buildOptions) all_options |= options; 618 .warnOnSpecialCompilerFlags(all_dflags, all_options, package_name, config_name); 619 } 620 } 621 } 622 623 package(dub) void checkPlatform(const scope ref ToolchainRequirements tr, BuildPlatform platform, string package_name) 624 { 625 import std.algorithm.iteration : map; 626 import std.format : format; 627 628 Version compilerver; 629 VersionRange compilerspec; 630 631 switch (platform.compiler) { 632 default: 633 compilerspec = VersionRange.Any; 634 compilerver = Version.minRelease; 635 break; 636 case "dmd": 637 compilerspec = tr.dmd; 638 compilerver = platform.compilerVersion.length 639 ? Version(dmdLikeVersionToSemverLike(platform.compilerVersion)) 640 : Version.minRelease; 641 break; 642 case "ldc": 643 compilerspec = tr.ldc; 644 compilerver = platform.compilerVersion.length 645 ? Version(platform.compilerVersion) 646 : Version.minRelease; 647 break; 648 case "gdc": 649 compilerspec = tr.gdc; 650 compilerver = platform.compilerVersion.length 651 ? Version(platform.compilerVersion) 652 : Version.minRelease; 653 break; 654 } 655 656 enforce(compilerspec != VersionRange.Invalid, 657 format( 658 "Installed %s %s is not supported by %s. Supported compiler(s):\n%s", 659 platform.compiler, platform.compilerVersion, package_name, 660 tr.supportedCompilers.map!((cs) { 661 auto str = " - " ~ cs[0]; 662 if (cs[1] != VersionRange.Any) str ~= ": " ~ cs[1].toString(); 663 return str; 664 }).join("\n") 665 ) 666 ); 667 668 enforce(compilerspec.matches(compilerver), 669 format( 670 "Installed %s-%s does not comply with %s compiler requirement: %s %s\n" ~ 671 "Please consider upgrading your installation.", 672 platform.compiler, platform.compilerVersion, 673 package_name, platform.compiler, compilerspec 674 ) 675 ); 676 677 enforce(tr.frontend.matches(Version(dmdLikeVersionToSemverLike(platform.frontendVersionString))), 678 format( 679 "Installed %s-%s with frontend %s does not comply with %s frontend requirement: %s\n" ~ 680 "Please consider upgrading your installation.", 681 platform.compiler, platform.compilerVersion, 682 platform.frontendVersionString, package_name, tr.frontend 683 ) 684 ); 685 } 686 687 package bool addRequirement(ref ToolchainRequirements req, string name, string value) 688 { 689 switch (name) { 690 default: return false; 691 case "dub": req.dub = parseVersionRange(value); break; 692 case "frontend": req.frontend = parseDMDDependency(value); break; 693 case "ldc": req.ldc = parseVersionRange(value); break; 694 case "gdc": req.gdc = parseVersionRange(value); break; 695 case "dmd": req.dmd = parseDMDDependency(value); break; 696 } 697 return true; 698 } 699 700 private VersionRange parseVersionRange(string dep) 701 { 702 if (dep == "no") return VersionRange.Invalid; 703 return VersionRange.fromString(dep); 704 } 705 706 private VersionRange parseDMDDependency(string dep) 707 { 708 import std.algorithm : map, splitter; 709 import std.array : join; 710 711 if (dep == "no") return VersionRange.Invalid; 712 return VersionRange.fromString(dep 713 .splitter(' ') 714 .map!(r => dmdLikeVersionToSemverLike(r)) 715 .join(' ')); 716 } 717 718 private T clone(T)(ref const(T) val) 719 { 720 import dub.internal.dyaml.stdsumtype; 721 import std.traits : isSomeString, isDynamicArray, isAssociativeArray, isBasicType, ValueType; 722 723 static if (is(T == immutable)) return val; 724 else static if (isBasicType!T || is(T Base == enum) && isBasicType!Base) { 725 return val; 726 } else static if (isDynamicArray!T) { 727 alias V = typeof(T.init[0]); 728 static if (is(V == immutable)) return val; 729 else { 730 T ret = new V[val.length]; 731 foreach (i, ref f; val) 732 ret[i] = clone!V(f); 733 return ret; 734 } 735 } else static if (isAssociativeArray!T) { 736 alias V = ValueType!T; 737 T ret; 738 foreach (k, ref f; val) 739 ret[k] = clone!V(f); 740 return ret; 741 } else static if (is(T == SumType!A, A...)) { 742 return val.match!((any) => T(clone(any))); 743 } else static if (is(T == struct)) { 744 T ret; 745 foreach (i, M; typeof(T.tupleof)) 746 ret.tupleof[i] = clone!M(val.tupleof[i]); 747 return ret; 748 } else static assert(false, "Unsupported type: "~T.stringof); 749 } 750 751 unittest { // issue #1407 - duplicate main source file 752 { 753 BuildSettingsTemplate t; 754 t.mainSourceFile = "./foo.d"; 755 t.sourceFiles[""] = ["foo.d"]; 756 BuildSettings bs; 757 t.getPlatformSettings(bs, BuildPlatform.init, NativePath("/")); 758 assert(bs.sourceFiles == ["foo.d"]); 759 } 760 761 version (Windows) {{ 762 BuildSettingsTemplate t; 763 t.mainSourceFile = "src/foo.d"; 764 t.sourceFiles[""] = ["src\\foo.d"]; 765 BuildSettings bs; 766 t.getPlatformSettings(bs, BuildPlatform.init, NativePath("/")); 767 assert(bs.sourceFiles == ["src\\foo.d"]); 768 }} 769 } 770 771 /** 772 * Edit all dependency names from `:foo` to `name:foo`. 773 * 774 * TODO: Remove the special case in the parser and remove this hack. 775 */ 776 package void fixDependenciesNames (T) (string root, ref T aggr) nothrow 777 { 778 static foreach (idx, FieldRef; T.tupleof) { 779 static if (is(immutable typeof(FieldRef) == immutable RecipeDependencyAA)) { 780 string[] toReplace; 781 foreach (key; aggr.tupleof[idx].byKey) 782 if (key.length && key[0] == ':') 783 toReplace ~= key; 784 foreach (k; toReplace) { 785 aggr.tupleof[idx][root ~ k] = aggr.tupleof[idx][k]; 786 aggr.tupleof[idx].data.remove(k); 787 } 788 } 789 else static if (is(typeof(FieldRef) == struct)) 790 fixDependenciesNames(root, aggr.tupleof[idx]); 791 } 792 } 793 794 /** 795 Turn a DMD-like version (e.g. 2.082.1) into a SemVer-like version (e.g. 2.82.1). 796 The function accepts a dependency operator prefix and some text postfix. 797 Prefix and postfix are returned verbatim. 798 Params: 799 ver = version string, possibly with a dependency operator prefix and some 800 test postfix. 801 Returns: 802 A Semver compliant string 803 */ 804 private string dmdLikeVersionToSemverLike(string ver) 805 { 806 import std.algorithm : countUntil, joiner, map, skipOver, splitter; 807 import std.array : join, split; 808 import std.ascii : isDigit; 809 import std.conv : text; 810 import std.exception : enforce; 811 import std.functional : not; 812 import std.range : padRight; 813 814 const start = ver.countUntil!isDigit; 815 enforce(start != -1, "Invalid semver: "~ver); 816 const prefix = ver[0 .. start]; 817 ver = ver[start .. $]; 818 819 const end = ver.countUntil!(c => !c.isDigit && c != '.'); 820 const postfix = end == -1 ? null : ver[end .. $]; 821 auto verStr = ver[0 .. $-postfix.length]; 822 823 auto comps = verStr 824 .splitter(".") 825 .map!((a) { if (a.length > 1) a.skipOver("0"); return a;}) 826 .padRight("0", 3); 827 828 return text(prefix, comps.joiner("."), postfix); 829 } 830 831 /// 832 unittest { 833 assert(dmdLikeVersionToSemverLike("2.082.1") == "2.82.1"); 834 assert(dmdLikeVersionToSemverLike("2.082.0") == "2.82.0"); 835 assert(dmdLikeVersionToSemverLike("2.082") == "2.82.0"); 836 assert(dmdLikeVersionToSemverLike("~>2.082") == "~>2.82.0"); 837 assert(dmdLikeVersionToSemverLike("~>2.082-beta1") == "~>2.82.0-beta1"); 838 assert(dmdLikeVersionToSemverLike("2.4.6") == "2.4.6"); 839 assert(dmdLikeVersionToSemverLike("2.4.6-alpha12") == "2.4.6-alpha12"); 840 }