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