1 /** 2 Dependency specification functionality. 3 4 Copyright: © 2012-2013 Matthias Dondorff, © 2012-2016 Sönke Ludwig 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Matthias Dondorff, Sönke Ludwig 7 */ 8 module dub.dependency; 9 10 import dub.internal.vibecompat.data.json; 11 import dub.internal.vibecompat.inet.path; 12 import dub.semver; 13 14 import dub.internal.dyaml.stdsumtype; 15 16 import std.algorithm; 17 import std.array; 18 import std.exception; 19 import std.string; 20 21 /// Represents a fully-qualified package name 22 public struct PackageName 23 { 24 /// The underlying full name of the package 25 private string fullName; 26 /// Where the separator lies, if any 27 private size_t separator; 28 29 /// Creates a new instance of this struct 30 public this(string fn) @safe pure 31 { 32 this.fullName = fn; 33 if (auto idx = fn.indexOf(':')) 34 this.separator = idx > 0 ? idx : fn.length; 35 else // We were given `:foo` 36 assert(0, "Argument to PackageName constructor needs to be " ~ 37 "a fully qualified string"); 38 } 39 40 /// Private constructor to have nothrow / @nogc 41 private this(string fn, size_t sep) @safe pure nothrow @nogc 42 { 43 this.fullName = fn; 44 this.separator = sep; 45 } 46 47 /// The base package name in which the subpackages may live 48 public PackageName main () const return @safe pure nothrow @nogc 49 { 50 return PackageName(this.fullName[0 .. this.separator], this.separator); 51 } 52 53 /// The subpackage name, or an empty string if there isn't 54 public string sub () const return @safe pure nothrow @nogc 55 { 56 // Return `null` instead of an empty string so that 57 // it can be used in a boolean context, e.g. 58 // `if (name.sub)` would be true with empty string 59 return this.separator < this.fullName.length 60 ? this.fullName[this.separator + 1 .. $] 61 : null; 62 } 63 64 /// Human readable representation 65 public string toString () const return scope @safe pure nothrow @nogc 66 { 67 return this.fullName; 68 } 69 70 /// 71 public int opCmp (in PackageName other) const scope @safe pure nothrow @nogc 72 { 73 import core.internal.string : dstrcmp; 74 return dstrcmp(this.toString(), other.toString()); 75 } 76 77 /// 78 public bool opEquals (in PackageName other) const scope @safe pure nothrow @nogc 79 { 80 return this.toString() == other.toString(); 81 } 82 } 83 84 /** Encapsulates the name of a package along with its dependency specification. 85 */ 86 struct PackageDependency { 87 /// Backward compatibility 88 deprecated("Use the constructor that accepts a `PackageName` as first argument") 89 this(string n, Dependency s = Dependency.init) @safe pure 90 { 91 this.name = PackageName(n); 92 this.spec = s; 93 } 94 95 // Remove once deprecated overload is gone 96 this(PackageName n, Dependency s = Dependency.init) @safe pure nothrow @nogc 97 { 98 this.name = n; 99 this.spec = s; 100 } 101 102 /// Name of the referenced package. 103 PackageName name; 104 105 /// Dependency specification used to select a particular version of the package. 106 Dependency spec; 107 } 108 109 /** 110 Represents a dependency specification. 111 112 A dependency specification either represents a specific version or version 113 range, or a path to a package. In addition to that it has `optional` and 114 `default_` flags to control how non-mandatory dependencies are handled. The 115 package name is notably not part of the dependency specification. 116 */ 117 struct Dependency { 118 /// We currently support 3 'types' 119 private alias Value = SumType!(VersionRange, NativePath, Repository); 120 121 /// Used by `toString` 122 private static immutable string[] BooleanOptions = [ "optional", "default" ]; 123 124 // Shortcut to create >=0.0.0 125 private enum ANY_IDENT = "*"; 126 127 private Value m_value = Value(VersionRange.Invalid); 128 private bool m_optional; 129 private bool m_default; 130 131 /// A Dependency, which matches every valid version. 132 public static immutable Dependency Any = Dependency(VersionRange.Any); 133 134 /// An invalid dependency (with no possible version matches). 135 public static immutable Dependency Invalid = Dependency(VersionRange.Invalid); 136 137 deprecated("Use `Dependency.Any` instead") 138 static @property Dependency any() @safe { return Dependency(VersionRange.Any); } 139 deprecated("Use `Dependency.Invalid` instead") 140 static @property Dependency invalid() @safe 141 { 142 return Dependency(VersionRange.Invalid); 143 } 144 145 /** Constructs a new dependency specification that matches a specific 146 path. 147 */ 148 this(NativePath path) @safe 149 { 150 this.m_value = path; 151 } 152 153 /** Constructs a new dependency specification that matches a specific 154 Git reference. 155 */ 156 this(Repository repository) @safe 157 { 158 this.m_value = repository; 159 } 160 161 /** Constructs a new dependency specification from a string 162 163 See the `versionSpec` property for a description of the accepted 164 contents of that string. 165 */ 166 this(string spec) @safe 167 { 168 this(VersionRange.fromString(spec)); 169 } 170 171 /** Constructs a new dependency specification that matches a specific 172 version. 173 */ 174 this(const Version ver) @safe 175 { 176 this(VersionRange(ver, ver)); 177 } 178 179 /// Construct a version from a range of possible values 180 this (VersionRange rng) @safe 181 { 182 this.m_value = rng; 183 } 184 185 deprecated("Instantiate the `Repository` struct with the string directly") 186 this(Repository repository, string spec) @safe 187 { 188 assert(repository.m_ref is null); 189 repository.m_ref = spec; 190 this(repository); 191 } 192 193 /// If set, overrides any version based dependency selection. 194 deprecated("Construct a new `Dependency` object instead") 195 @property void path(NativePath value) @trusted 196 { 197 this.m_value = value; 198 } 199 /// ditto 200 @property NativePath path() const @safe 201 { 202 return this.m_value.match!( 203 (const NativePath p) => p, 204 ( any ) => NativePath.init, 205 ); 206 } 207 208 /// If set, overrides any version based dependency selection. 209 deprecated("Construct a new `Dependency` object instead") 210 @property void repository(Repository value) @trusted 211 { 212 this.m_value = value; 213 } 214 /// ditto 215 @property Repository repository() const @safe 216 { 217 return this.m_value.match!( 218 (const Repository p) => p, 219 ( any ) => Repository.init, 220 ); 221 } 222 223 /// Determines if the dependency is required or optional. 224 @property bool optional() const scope @safe pure nothrow @nogc 225 { 226 return m_optional; 227 } 228 /// ditto 229 @property void optional(bool optional) scope @safe pure nothrow @nogc 230 { 231 m_optional = optional; 232 } 233 234 /// Determines if an optional dependency should be chosen by default. 235 @property bool default_() const scope @safe pure nothrow @nogc 236 { 237 return m_default; 238 } 239 /// ditto 240 @property void default_(bool value) scope @safe pure nothrow @nogc 241 { 242 m_default = value; 243 } 244 245 /// Returns true $(I iff) the version range only matches a specific version. 246 @property bool isExactVersion() const scope @safe 247 { 248 return this.m_value.match!( 249 (NativePath v) => false, 250 (Repository v) => false, 251 (VersionRange v) => v.isExactVersion(), 252 ); 253 } 254 255 /// Returns the exact version matched by the version range. 256 @property Version version_() const @safe { 257 auto range = this.m_value.match!( 258 // Can be simplified to `=> assert(0)` once we drop support for v2.096 259 (NativePath p) { int dummy; if (dummy) return VersionRange.init; assert(0); }, 260 (Repository r) { int dummy; if (dummy) return VersionRange.init; assert(0); }, 261 (VersionRange v) => v, 262 ); 263 enforce(range.isExactVersion(), 264 "Dependency "~range.toString()~" is no exact version."); 265 return range.m_versA; 266 } 267 268 /// Sets/gets the matching version range as a specification string. 269 deprecated("Create a new `Dependency` instead and provide a `VersionRange`") 270 @property void versionSpec(string ves) @trusted 271 { 272 this.m_value = VersionRange.fromString(ves); 273 } 274 275 /// ditto 276 deprecated("Use `Dependency.visit` and match `VersionRange`instead") 277 @property string versionSpec() const @safe { 278 return this.m_value.match!( 279 (const NativePath p) => ANY_IDENT, 280 (const Repository r) => r.m_ref, 281 (const VersionRange p) => p.toString(), 282 ); 283 } 284 285 /** Returns a modified dependency that gets mapped to a given path. 286 287 This function will return an unmodified `Dependency` if it is not path 288 based. Otherwise, the given `path` will be prefixed to the existing 289 path. 290 */ 291 Dependency mapToPath(NativePath path) const @trusted { 292 // NOTE Path is @system in vibe.d 0.7.x and in the compatibility layer 293 return this.m_value.match!( 294 (NativePath v) { 295 if (v.empty || v.absolute) return this; 296 auto ret = Dependency(path ~ v); 297 ret.m_default = m_default; 298 ret.m_optional = m_optional; 299 return ret; 300 }, 301 (Repository v) => this, 302 (VersionRange v) => this, 303 ); 304 } 305 306 /** Returns a human-readable string representation of the dependency 307 specification. 308 */ 309 string toString() const scope @trusted { 310 // Trusted because `SumType.match` doesn't seem to support `scope` 311 312 string Stringifier (T, string pre = null) (const T v) 313 { 314 const bool extra = this.optional || this.default_; 315 return format("%s%s%s%-(%s, %)%s", 316 pre, v, 317 extra ? " (" : "", 318 BooleanOptions[!this.optional .. 1 + this.default_], 319 extra ? ")" : ""); 320 } 321 322 return this.m_value.match!( 323 Stringifier!Repository, 324 Stringifier!(NativePath, "@"), 325 Stringifier!VersionRange 326 ); 327 } 328 329 /** Returns a JSON representation of the dependency specification. 330 331 Simple specifications will be represented as a single specification 332 string (`versionSpec`), while more complex specifications will be 333 represented as a JSON object with optional "version", "path", "optional" 334 and "default" fields. 335 336 Params: 337 selections = We are serializing `dub.selections.json`, don't write out 338 `optional` and `default`. 339 */ 340 Json toJson(bool selections = false) const @safe 341 { 342 // NOTE Path and Json is @system in vibe.d 0.7.x and in the compatibility layer 343 static void initJson(ref Json j, bool opt, bool def, bool s = selections) 344 { 345 j = Json.emptyObject; 346 if (!s && opt) j["optional"] = true; 347 if (!s && def) j["default"] = true; 348 } 349 350 Json json; 351 this.m_value.match!( 352 (const NativePath v) @trusted { 353 initJson(json, optional, default_); 354 json["path"] = v.toString(); 355 }, 356 357 (const Repository v) @trusted { 358 initJson(json, optional, default_); 359 json["repository"] = v.toString(); 360 json["version"] = v.m_ref; 361 }, 362 363 (const VersionRange v) @trusted { 364 if (!selections && (optional || default_)) 365 { 366 initJson(json, optional, default_); 367 json["version"] = v.toString(); 368 } 369 else 370 json = Json(v.toString()); 371 }, 372 ); 373 return json; 374 } 375 376 @trusted unittest { 377 Dependency d = Dependency("==1.0.0"); 378 assert(d.toJson() == Json("1.0.0"), "Failed: " ~ d.toJson().toPrettyString()); 379 d = fromJson((fromJson(d.toJson())).toJson()); 380 assert(d == Dependency("1.0.0")); 381 assert(d.toJson() == Json("1.0.0"), "Failed: " ~ d.toJson().toPrettyString()); 382 } 383 384 @trusted unittest { 385 Dependency dependency = Dependency(Repository("git+http://localhost", "1.0.0")); 386 Json expected = Json([ 387 "repository": Json("git+http://localhost"), 388 "version": Json("1.0.0") 389 ]); 390 assert(dependency.toJson() == expected, "Failed: " ~ dependency.toJson().toPrettyString()); 391 } 392 393 @trusted unittest { 394 Dependency d = Dependency(NativePath("dir")); 395 Json expected = Json([ "path": Json("dir") ]); 396 assert(d.toJson() == expected, "Failed: " ~ d.toJson().toPrettyString()); 397 } 398 399 /** Constructs a new `Dependency` from its JSON representation. 400 401 See `toJson` for a description of the JSON format. 402 */ 403 static Dependency fromJson(Json verspec) 404 @trusted { // NOTE Path and Json is @system in vibe.d 0.7.x and in the compatibility layer 405 Dependency dep; 406 if( verspec.type == Json.Type.object ){ 407 if( auto pp = "path" in verspec ) { 408 dep = Dependency(NativePath(verspec["path"].get!string)); 409 } else if (auto repository = "repository" in verspec) { 410 enforce("version" in verspec, "No version field specified!"); 411 enforce(repository.length > 0, "No repository field specified!"); 412 413 dep = Dependency(Repository( 414 repository.get!string, verspec["version"].get!string)); 415 } else { 416 enforce("version" in verspec, "No version field specified!"); 417 auto ver = verspec["version"].get!string; 418 // Using the string to be able to specify a range of versions. 419 dep = Dependency(ver); 420 } 421 422 if (auto po = "optional" in verspec) dep.optional = po.get!bool; 423 if (auto po = "default" in verspec) dep.default_ = po.get!bool; 424 } else { 425 // canonical "package-id": "version" 426 dep = Dependency(verspec.get!string); 427 } 428 return dep; 429 } 430 431 @trusted unittest { 432 assert(fromJson(parseJsonString("\">=1.0.0 <2.0.0\"")) == Dependency(">=1.0.0 <2.0.0")); 433 Dependency parsed = fromJson(parseJsonString(` 434 { 435 "version": "2.0.0", 436 "optional": true, 437 "default": true, 438 "path": "path/to/package" 439 } 440 `)); 441 Dependency d = NativePath("path/to/package"); // supposed to ignore the version spec 442 d.optional = true; 443 d.default_ = true; 444 assert(d == parsed); 445 } 446 447 /** Compares dependency specifications. 448 449 These methods are suitable for equality comparisons, as well as for 450 using `Dependency` as a key in hash or tree maps. 451 */ 452 bool opEquals(in Dependency o) const scope @safe { 453 if (o.m_optional != this.m_optional) return false; 454 if (o.m_default != this.m_default) return false; 455 return this.m_value == o.m_value; 456 } 457 458 /// ditto 459 int opCmp(in Dependency o) const @safe { 460 alias ResultMatch = match!( 461 (VersionRange r1, VersionRange r2) => r1.opCmp(r2), 462 (_1, _2) => 0, 463 ); 464 if (auto result = ResultMatch(this.m_value, o.m_value)) 465 return result; 466 if (m_optional != o.m_optional) return m_optional ? -1 : 1; 467 return 0; 468 } 469 470 /** Determines if this dependency specification is valid. 471 472 A specification is valid if it can match at least one version. 473 */ 474 bool valid() const @safe { 475 return this.m_value.match!( 476 (NativePath v) => true, 477 (Repository v) => true, 478 (VersionRange v) => v.isValid(), 479 ); 480 } 481 482 /** Determines if this dependency specification matches arbitrary versions. 483 484 This is true in particular for the `any` constant. 485 */ 486 deprecated("Use `VersionRange.matchesAny` directly") 487 bool matchesAny() const scope @safe { 488 return this.m_value.match!( 489 (NativePath v) => true, 490 (Repository v) => true, 491 (VersionRange v) => v.matchesAny(), 492 ); 493 } 494 495 /** Tests if the specification matches a specific version. 496 */ 497 bool matches(string vers, VersionMatchMode mode = VersionMatchMode.standard) const @safe 498 { 499 return matches(Version(vers), mode); 500 } 501 /// ditto 502 bool matches(in Version v, VersionMatchMode mode = VersionMatchMode.standard) const @safe { 503 return this.m_value.match!( 504 (NativePath i) => true, 505 (Repository i) => true, 506 (VersionRange i) => i.matchesAny() || i.matches(v, mode), 507 ); 508 } 509 510 /** Merges two dependency specifications. 511 512 The result is a specification that matches the intersection of the set 513 of versions matched by the individual specifications. Note that this 514 result can be invalid (i.e. not match any version). 515 */ 516 Dependency merge(ref const(Dependency) o) const @trusted { 517 alias Merger = match!( 518 (const NativePath a, const NativePath b) => a == b ? this : Invalid, 519 (const NativePath a, any ) => o, 520 ( any , const NativePath b) => this, 521 522 (const Repository a, const Repository b) => a.m_ref == b.m_ref ? this : Invalid, 523 (const Repository a, any ) => this, 524 ( any , const Repository b) => o, 525 526 (const VersionRange a, const VersionRange b) { 527 if (a.matchesAny()) return o; 528 if (b.matchesAny()) return this; 529 530 VersionRange copy = a; 531 copy.merge(b); 532 if (!copy.isValid()) return Invalid; 533 return Dependency(copy); 534 } 535 ); 536 537 Dependency ret = Merger(this.m_value, o.m_value); 538 ret.m_optional = m_optional && o.m_optional; 539 return ret; 540 } 541 } 542 543 /// Allow direct access to the underlying dependency 544 public auto visit (Handlers...) (const auto ref Dependency dep) 545 { 546 return dep.m_value.match!(Handlers); 547 } 548 549 //// Ditto 550 public auto visit (Handlers...) (auto ref Dependency dep) 551 { 552 return dep.m_value.match!(Handlers); 553 } 554 555 556 unittest { 557 Dependency a = Dependency(">=1.1.0"), b = Dependency(">=1.3.0"); 558 assert (a.merge(b).valid() && a.merge(b).toString() == ">=1.3.0", a.merge(b).toString()); 559 560 assertThrown(Dependency("<=2.0.0 >=1.0.0")); 561 assertThrown(Dependency(">=2.0.0 <=1.0.0")); 562 563 a = Dependency(">=1.0.0 <=5.0.0"); b = Dependency(">=2.0.0"); 564 assert (a.merge(b).valid() && a.merge(b).toString() == ">=2.0.0 <=5.0.0", a.merge(b).toString()); 565 566 assertThrown(a = Dependency(">1.0.0 ==5.0.0"), "Construction is invalid"); 567 568 a = Dependency(">1.0.0"); b = Dependency("<2.0.0"); 569 assert (a.merge(b).valid(), a.merge(b).toString()); 570 assert (a.merge(b).toString() == ">1.0.0 <2.0.0", a.merge(b).toString()); 571 572 a = Dependency(">2.0.0"); b = Dependency("<1.0.0"); 573 assert (!(a.merge(b)).valid(), a.merge(b).toString()); 574 575 a = Dependency(">=2.0.0"); b = Dependency("<=1.0.0"); 576 assert (!(a.merge(b)).valid(), a.merge(b).toString()); 577 578 a = Dependency("==2.0.0"); b = Dependency("==1.0.0"); 579 assert (!(a.merge(b)).valid(), a.merge(b).toString()); 580 581 a = Dependency("1.0.0"); b = Dependency("==1.0.0"); 582 assert (a == b); 583 584 a = Dependency("<=2.0.0"); b = Dependency("==1.0.0"); 585 Dependency m = a.merge(b); 586 assert (m.valid(), m.toString()); 587 assert (m.matches(Version("1.0.0"))); 588 assert (!m.matches(Version("1.1.0"))); 589 assert (!m.matches(Version("0.0.1"))); 590 591 592 // branches / head revisions 593 a = Dependency(Version.masterBranch); 594 assert(a.valid()); 595 assert(a.matches(Version.masterBranch)); 596 b = Dependency(Version.masterBranch); 597 m = a.merge(b); 598 assert(m.matches(Version.masterBranch)); 599 600 //assertThrown(a = Dependency(Version.MASTER_STRING ~ " <=1.0.0"), "Construction invalid"); 601 assertThrown(a = Dependency(">=1.0.0 " ~ Version.masterBranch.toString()), "Construction invalid"); 602 603 immutable string branch1 = Version.branchPrefix ~ "Branch1"; 604 immutable string branch2 = Version.branchPrefix ~ "Branch2"; 605 606 //assertThrown(a = Dependency(branch1 ~ " " ~ branch2), "Error: '" ~ branch1 ~ " " ~ branch2 ~ "' succeeded"); 607 //assertThrown(a = Dependency(Version.MASTER_STRING ~ " " ~ branch1), "Error: '" ~ Version.MASTER_STRING ~ " " ~ branch1 ~ "' succeeded"); 608 609 a = Dependency(branch1); 610 b = Dependency(branch2); 611 assert(!a.merge(b).valid, "Shouldn't be able to merge to different branches"); 612 b = a.merge(a); 613 assert(b.valid, "Should be able to merge the same branches. (?)"); 614 assert(a == b); 615 616 a = Dependency(branch1); 617 assert(a.matches(branch1), "Dependency(branch1) does not match 'branch1'"); 618 assert(a.matches(Version(branch1)), "Dependency(branch1) does not match Version('branch1')"); 619 assert(!a.matches(Version.masterBranch), "Dependency(branch1) matches Version.masterBranch"); 620 assert(!a.matches(branch2), "Dependency(branch1) matches 'branch2'"); 621 assert(!a.matches(Version("1.0.0")), "Dependency(branch1) matches '1.0.0'"); 622 a = Dependency(">=1.0.0"); 623 assert(!a.matches(Version(branch1)), "Dependency(1.0.0) matches 'branch1'"); 624 625 // Testing optional dependencies. 626 a = Dependency(">=1.0.0"); 627 assert(!a.optional, "Default is not optional."); 628 b = a; 629 assert(!a.merge(b).optional, "Merging two not optional dependencies wrong."); 630 a.optional = true; 631 assert(!a.merge(b).optional, "Merging optional with not optional wrong."); 632 b.optional = true; 633 assert(a.merge(b).optional, "Merging two optional dependencies wrong."); 634 635 // SemVer's sub identifiers. 636 a = Dependency(">=1.0.0-beta"); 637 assert(!a.matches(Version("1.0.0-alpha")), "Failed: match 1.0.0-alpha with >=1.0.0-beta"); 638 assert(a.matches(Version("1.0.0-beta")), "Failed: match 1.0.0-beta with >=1.0.0-beta"); 639 assert(a.matches(Version("1.0.0")), "Failed: match 1.0.0 with >=1.0.0-beta"); 640 assert(a.matches(Version("1.0.0-rc")), "Failed: match 1.0.0-rc with >=1.0.0-beta"); 641 642 // Approximate versions. 643 a = Dependency("~>3.0"); 644 b = Dependency(">=3.0.0 <4.0.0-0"); 645 assert(a == b, "Testing failed: " ~ a.toString()); 646 assert(a.matches(Version("3.1.146")), "Failed: Match 3.1.146 with ~>0.1.2"); 647 assert(!a.matches(Version("0.2.0")), "Failed: Match 0.2.0 with ~>0.1.2"); 648 assert(!a.matches(Version("4.0.0-beta.1"))); 649 a = Dependency("~>3.0.0"); 650 assert(a == Dependency(">=3.0.0 <3.1.0-0"), "Testing failed: " ~ a.toString()); 651 a = Dependency("~>3.5"); 652 assert(a == Dependency(">=3.5.0 <4.0.0-0"), "Testing failed: " ~ a.toString()); 653 a = Dependency("~>3.5.0"); 654 assert(a == Dependency(">=3.5.0 <3.6.0-0"), "Testing failed: " ~ a.toString()); 655 assert(!Dependency("~>3.0.0").matches(Version("3.1.0-beta"))); 656 657 a = Dependency("^0.1.2"); 658 assert(a == Dependency(">=0.1.2 <0.1.3-0")); 659 a = Dependency("^1.2.3"); 660 assert(a == Dependency(">=1.2.3 <2.0.0-0"), "Testing failed: " ~ a.toString()); 661 a = Dependency("^1.2"); 662 assert(a == Dependency(">=1.2.0 <2.0.0-0"), "Testing failed: " ~ a.toString()); 663 664 a = Dependency("~>0.1.1"); 665 b = Dependency("==0.1.0"); 666 assert(!a.merge(b).valid); 667 b = Dependency("==0.1.9999"); 668 assert(a.merge(b).valid); 669 b = Dependency("==0.2.0"); 670 assert(!a.merge(b).valid); 671 b = Dependency("==0.2.0-beta.1"); 672 assert(!a.merge(b).valid); 673 674 a = Dependency("~>1.0.1-beta"); 675 b = Dependency(">=1.0.1-beta <1.1.0-0"); 676 assert(a == b, "Testing failed: " ~ a.toString()); 677 assert(a.matches(Version("1.0.1-beta"))); 678 assert(a.matches(Version("1.0.1-beta.6"))); 679 680 a = Dependency("~d2test"); 681 assert(!a.optional); 682 assert(a.valid); 683 assert(a.version_ == Version("~d2test")); 684 685 a = Dependency("==~d2test"); 686 assert(!a.optional); 687 assert(a.valid); 688 assert(a.version_ == Version("~d2test")); 689 690 a = Dependency.Any; 691 assert(!a.optional); 692 assert(a.valid); 693 assertThrown(a.version_); 694 assert(a.matches(Version.masterBranch)); 695 assert(a.matches(Version("1.0.0"))); 696 assert(a.matches(Version("0.0.1-pre"))); 697 b = Dependency(">=1.0.1"); 698 assert(b == a.merge(b)); 699 assert(b == b.merge(a)); 700 b = Dependency(Version.masterBranch); 701 assert(a.merge(b) == b); 702 assert(b.merge(a) == b); 703 704 a.optional = true; 705 assert(a.matches(Version.masterBranch)); 706 assert(a.matches(Version("1.0.0"))); 707 assert(a.matches(Version("0.0.1-pre"))); 708 b = Dependency(">=1.0.1"); 709 assert(b == a.merge(b)); 710 assert(b == b.merge(a)); 711 b = Dependency(Version.masterBranch); 712 assert(a.merge(b) == b); 713 assert(b.merge(a) == b); 714 715 assert(Dependency("1.0.0").matches(Version("1.0.0+foo"))); 716 assert(Dependency("1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.standard)); 717 assert(!Dependency("1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.strict)); 718 assert(Dependency("1.0.0+foo").matches(Version("1.0.0+foo"), VersionMatchMode.strict)); 719 assert(Dependency("~>1.0.0+foo").matches(Version("1.0.0+foo"), VersionMatchMode.strict)); 720 assert(Dependency("~>1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.strict)); 721 } 722 723 unittest { 724 assert(VersionRange.fromString("~>1.0.4").toString() == "~>1.0.4"); 725 assert(VersionRange.fromString("~>1.4").toString() == "~>1.4"); 726 assert(VersionRange.fromString("~>2").toString() == "~>2"); 727 assert(VersionRange.fromString("~>1.0.4+1.2.3").toString() == "~>1.0.4"); 728 assert(VersionRange.fromString("^0.1.2").toString() == "^0.1.2"); 729 assert(VersionRange.fromString("^1.2.3").toString() == "^1.2.3"); 730 assert(VersionRange.fromString("^1.2").toString() == "~>1.2"); // equivalent; prefer ~> 731 } 732 733 /** 734 Represents an SCM repository. 735 */ 736 struct Repository 737 { 738 private string m_remote; 739 private string m_ref; 740 741 private Kind m_kind; 742 743 enum Kind 744 { 745 git, 746 } 747 748 /** 749 Params: 750 remote = Repository remote. 751 ref_ = Reference to use (SHA1, tag, branch name...) 752 */ 753 this(string remote, string ref_) 754 { 755 enforce(remote.startsWith("git+"), "Unsupported repository type (supports: git+URL)"); 756 757 m_remote = remote["git+".length .. $]; 758 m_kind = Kind.git; 759 m_ref = ref_; 760 assert(m_remote.length); 761 assert(m_ref.length); 762 } 763 764 /// Ditto 765 deprecated("Use the constructor accepting a second parameter named `ref_`") 766 this(string remote) 767 { 768 enforce(remote.startsWith("git+"), "Unsupported repository type (supports: git+URL)"); 769 770 m_remote = remote["git+".length .. $]; 771 m_kind = Kind.git; 772 assert(m_remote.length); 773 } 774 775 string toString() const nothrow pure @safe 776 { 777 if (empty) return null; 778 string kindRepresentation; 779 780 final switch (kind) 781 { 782 case Kind.git: 783 kindRepresentation = "git"; 784 } 785 return kindRepresentation~"+"~remote; 786 } 787 788 /** 789 Returns: 790 Repository URL or path. 791 */ 792 @property string remote() const @nogc nothrow pure @safe 793 in { assert(m_remote !is null); } 794 do 795 { 796 return m_remote; 797 } 798 799 /** 800 Returns: 801 The reference (commit hash, branch name, tag) we are targeting 802 */ 803 @property string ref_() const @nogc nothrow pure @safe 804 in { assert(m_remote !is null); } 805 in { assert(m_ref !is null); } 806 do 807 { 808 return m_ref; 809 } 810 811 /** 812 Returns: 813 Repository type. 814 */ 815 @property Kind kind() const @nogc nothrow pure @safe 816 { 817 return m_kind; 818 } 819 820 /** 821 Returns: 822 Whether the repository was initialized with an URL or path. 823 */ 824 @property bool empty() const @nogc nothrow pure @safe 825 { 826 return m_remote.empty; 827 } 828 } 829 830 831 /** 832 Represents a version in semantic version format, or a branch identifier. 833 834 This can either have the form "~master", where "master" is a branch name, 835 or the form "major.update.bugfix-prerelease+buildmetadata" (see the 836 Semantic Versioning Specification v2.0.0 at http://semver.org/). 837 */ 838 struct Version { 839 private { 840 static immutable MAX_VERS = "99999.0.0"; 841 static immutable masterString = "~master"; 842 enum branchPrefix = '~'; 843 string m_version; 844 } 845 846 static immutable Version minRelease = Version("0.0.0"); 847 static immutable Version maxRelease = Version(MAX_VERS); 848 static immutable Version masterBranch = Version(masterString); 849 850 /** Constructs a new `Version` from its string representation. 851 */ 852 this(string vers) @safe pure 853 { 854 enforce(vers.length > 1, "Version strings must not be empty."); 855 if (vers[0] != branchPrefix) 856 enforce(vers.isValidVersion(), "Invalid SemVer format: " ~ vers); 857 m_version = vers; 858 } 859 860 /** Constructs a new `Version` from its string representation. 861 862 This method is equivalent to calling the constructor and is used as an 863 endpoint for the serialization framework. 864 */ 865 static Version fromString(string vers) @safe pure { return Version(vers); } 866 867 bool opEquals(in Version oth) const scope @safe pure 868 { 869 return opCmp(oth) == 0; 870 } 871 872 /// Tests if this represents a branch instead of a version. 873 @property bool isBranch() const scope @safe pure nothrow @nogc 874 { 875 return m_version.length > 0 && m_version[0] == branchPrefix; 876 } 877 878 /// Tests if this represents the master branch "~master". 879 @property bool isMaster() const scope @safe pure nothrow @nogc 880 { 881 return m_version == masterString; 882 } 883 884 /** Tests if this represents a pre-release version. 885 886 Note that branches are always considered pre-release versions. 887 */ 888 @property bool isPreRelease() const scope @safe pure nothrow @nogc 889 { 890 if (isBranch) return true; 891 return isPreReleaseVersion(m_version); 892 } 893 894 /** Tests two versions for equality, according to the selected match mode. 895 */ 896 bool matches(in Version other, VersionMatchMode mode = VersionMatchMode.standard) 897 const scope @safe pure 898 { 899 if (mode == VersionMatchMode.strict) 900 return this.toString() == other.toString(); 901 return this == other; 902 } 903 904 /** Compares two versions/branches for precedence. 905 906 Versions generally have precedence over branches and the master branch 907 has precedence over other branches. Apart from that, versions are 908 compared using SemVer semantics, while branches are compared 909 lexicographically. 910 */ 911 int opCmp(in Version other) const scope @safe pure 912 { 913 if (isBranch || other.isBranch) { 914 if(m_version == other.m_version) return 0; 915 if (!isBranch) return 1; 916 else if (!other.isBranch) return -1; 917 if (isMaster) return 1; 918 else if (other.isMaster) return -1; 919 return this.m_version < other.m_version ? -1 : 1; 920 } 921 922 return compareVersions(m_version, other.m_version); 923 } 924 925 /// Returns the string representation of the version/branch. 926 string toString() const return scope @safe pure nothrow @nogc 927 { 928 return m_version; 929 } 930 } 931 932 /** 933 * A range of versions that are acceptable 934 * 935 * While not directly described in SemVer v2.0.0, a common set 936 * of range operators have appeared among package managers. 937 * We mostly NPM's: https://semver.npmjs.com/ 938 * 939 * Hence the acceptable forms for this string are as follows: 940 * 941 * $(UL 942 * $(LI `"1.0.0"` - a single version in SemVer format) 943 * $(LI `"==1.0.0"` - alternative single version notation) 944 * $(LI `">1.0.0"` - version range with a single bound) 945 * $(LI `">1.0.0 <2.0.0"` - version range with two bounds) 946 * $(LI `"~>1.0.0"` - a fuzzy version range) 947 * $(LI `"~>1.0"` - a fuzzy version range with partial version) 948 * $(LI `"^1.0.0"` - semver compatible version range (same version if 0.x.y, ==major >=minor.patch if x.y.z)) 949 * $(LI `"^1.0"` - same as ^1.0.0) 950 * $(LI `"~master"` - a branch name) 951 * $(LI `"*"` - match any version (see also `VersionRange.Any`)) 952 * ) 953 * 954 * Apart from "$(LT)" and "$(GT)", "$(GT)=" and "$(LT)=" are also valid 955 * comparators. 956 */ 957 public struct VersionRange 958 { 959 private Version m_versA; 960 private Version m_versB; 961 private bool m_inclusiveA = true; // A comparison > (true) or >= (false) 962 private bool m_inclusiveB = true; // B comparison < (true) or <= (false) 963 964 /// Matches any version 965 public static immutable Any = VersionRange(Version.minRelease, Version.maxRelease); 966 /// Doesn't match any version 967 public static immutable Invalid = VersionRange(Version.maxRelease, Version.minRelease); 968 969 /// 970 public int opCmp (in VersionRange o) const scope @safe 971 { 972 if (m_inclusiveA != o.m_inclusiveA) return m_inclusiveA < o.m_inclusiveA ? -1 : 1; 973 if (m_inclusiveB != o.m_inclusiveB) return m_inclusiveB < o.m_inclusiveB ? -1 : 1; 974 if (m_versA != o.m_versA) return m_versA < o.m_versA ? -1 : 1; 975 if (m_versB != o.m_versB) return m_versB < o.m_versB ? -1 : 1; 976 return 0; 977 } 978 979 public bool matches (in Version v, VersionMatchMode mode = VersionMatchMode.standard) 980 const scope @safe 981 { 982 if (m_versA.isBranch) { 983 enforce(this.isExactVersion()); 984 return m_versA == v; 985 } 986 987 if (v.isBranch) 988 return m_versA == v; 989 990 if (m_versA == m_versB) 991 return this.m_versA.matches(v, mode); 992 993 return doCmp(m_inclusiveA, m_versA, v) && 994 doCmp(m_inclusiveB, v, m_versB); 995 } 996 997 /// Modify in place 998 public void merge (const VersionRange o) @safe 999 { 1000 int acmp = m_versA.opCmp(o.m_versA); 1001 int bcmp = m_versB.opCmp(o.m_versB); 1002 1003 this.m_inclusiveA = !m_inclusiveA && acmp >= 0 ? false : o.m_inclusiveA; 1004 this.m_versA = acmp > 0 ? m_versA : o.m_versA; 1005 this.m_inclusiveB = !m_inclusiveB && bcmp <= 0 ? false : o.m_inclusiveB; 1006 this.m_versB = bcmp < 0 ? m_versB : o.m_versB; 1007 } 1008 1009 /// Returns true $(I iff) the version range only matches a specific version. 1010 @property bool isExactVersion() const scope @safe 1011 { 1012 return this.m_versA == this.m_versB; 1013 } 1014 1015 /// Determines if this dependency specification matches arbitrary versions. 1016 /// This is true in particular for the `any` constant. 1017 public bool matchesAny() const scope @safe 1018 { 1019 return this.m_inclusiveA && this.m_inclusiveB 1020 && this.m_versA == Version.minRelease 1021 && this.m_versB == Version.maxRelease; 1022 } 1023 1024 unittest { 1025 assert(VersionRange.fromString("*").matchesAny); 1026 assert(!VersionRange.fromString(">0.0.0").matchesAny); 1027 assert(!VersionRange.fromString(">=1.0.0").matchesAny); 1028 assert(!VersionRange.fromString("<1.0.0").matchesAny); 1029 } 1030 1031 public static VersionRange fromString (string ves) @safe 1032 { 1033 static import std.string; 1034 1035 enforce(ves.length > 0); 1036 1037 if (ves == Dependency.ANY_IDENT) { 1038 // Any version is good. 1039 ves = ">=0.0.0"; 1040 } 1041 1042 if (ves.startsWith("~>")) { 1043 // Shortcut: "~>x.y.z" variant. Last non-zero number will indicate 1044 // the base for this so something like this: ">=x.y.z <x.(y+1).z" 1045 ves = ves[2..$]; 1046 return VersionRange( 1047 Version(expandVersion(ves)), Version(bumpVersion(ves) ~ "-0"), 1048 true, false); 1049 } 1050 1051 if (ves.startsWith("^")) { 1052 // Shortcut: "^x.y.z" variant. "Semver compatible" - no breaking changes. 1053 // if 0.x.y, ==0.x.y 1054 // if x.y.z, >=x.y.z <(x+1).0.0-0 1055 // ^x.y is equivalent to ^x.y.0. 1056 ves = ves[1..$].expandVersion; 1057 return VersionRange( 1058 Version(ves), Version(bumpIncompatibleVersion(ves) ~ "-0"), 1059 true, false); 1060 } 1061 1062 if (ves[0] == Version.branchPrefix) { 1063 auto ver = Version(ves); 1064 return VersionRange(ver, ver, true, true); 1065 } 1066 1067 if (std..string.indexOf("><=", ves[0]) == -1) { 1068 auto ver = Version(ves); 1069 return VersionRange(ver, ver, true, true); 1070 } 1071 1072 auto cmpa = skipComp(ves); 1073 size_t idx2 = std..string.indexOf(ves, " "); 1074 if (idx2 == -1) { 1075 if (cmpa == "<=" || cmpa == "<") 1076 return VersionRange(Version.minRelease, Version(ves), true, (cmpa == "<=")); 1077 1078 if (cmpa == ">=" || cmpa == ">") 1079 return VersionRange(Version(ves), Version.maxRelease, (cmpa == ">="), true); 1080 1081 // Converts "==" to ">=a&&<=a", which makes merging easier 1082 return VersionRange(Version(ves), Version(ves), true, true); 1083 } 1084 1085 enforce(cmpa == ">" || cmpa == ">=", 1086 "First comparison operator expected to be either > or >=, not " ~ cmpa); 1087 assert(ves[idx2] == ' '); 1088 VersionRange ret; 1089 ret.m_versA = Version(ves[0..idx2]); 1090 ret.m_inclusiveA = cmpa == ">="; 1091 string v2 = ves[idx2+1..$]; 1092 auto cmpb = skipComp(v2); 1093 enforce(cmpb == "<" || cmpb == "<=", 1094 "Second comparison operator expected to be either < or <=, not " ~ cmpb); 1095 ret.m_versB = Version(v2); 1096 ret.m_inclusiveB = cmpb == "<="; 1097 1098 enforce(!ret.m_versA.isBranch && !ret.m_versB.isBranch, 1099 format("Cannot compare branches: %s", ves)); 1100 enforce(ret.m_versA <= ret.m_versB, 1101 "First version must not be greater than the second one."); 1102 1103 return ret; 1104 } 1105 1106 /// Returns a string representation of this range 1107 string toString() const @safe { 1108 static import std.string; 1109 1110 string r; 1111 1112 if (this == Invalid) return "no"; 1113 if (this.isExactVersion() && m_inclusiveA && m_inclusiveB) { 1114 // Special "==" case 1115 if (m_versA == Version.masterBranch) return "~master"; 1116 else return m_versA.toString(); 1117 } 1118 1119 // "~>", "^" case 1120 if (m_inclusiveA && !m_inclusiveB && !m_versA.isBranch) { 1121 auto vs = m_versA.toString(); 1122 auto i1 = std..string.indexOf(vs, '-'), i2 = std..string.indexOf(vs, '+'); 1123 auto i12 = i1 >= 0 ? i2 >= 0 ? i1 < i2 ? i1 : i2 : i1 : i2; 1124 auto va = i12 >= 0 ? vs[0 .. i12] : vs; 1125 auto parts = va.splitter('.').array; 1126 assert(parts.length == 3, "Version string with a digit group count != 3: "~va); 1127 1128 foreach (i; 0 .. 3) { 1129 auto vp = parts[0 .. i+1].join("."); 1130 auto ve = Version(expandVersion(vp)); 1131 auto veb = Version(bumpVersion(vp) ~ "-0"); 1132 if (ve == m_versA && veb == m_versB) return "~>" ~ vp; 1133 1134 auto veb2 = Version(bumpIncompatibleVersion(expandVersion(vp)) ~ "-0"); 1135 if (ve == m_versA && veb2 == m_versB) return "^" ~ vp; 1136 } 1137 } 1138 1139 if (m_versA != Version.minRelease) r = (m_inclusiveA ? ">=" : ">") ~ m_versA.toString(); 1140 if (m_versB != Version.maxRelease) r ~= (r.length==0 ? "" : " ") ~ (m_inclusiveB ? "<=" : "<") ~ m_versB.toString(); 1141 if (this.matchesAny()) r = ">=0.0.0"; 1142 return r; 1143 } 1144 1145 public bool isValid() const @safe { 1146 return m_versA <= m_versB && doCmp(m_inclusiveA && m_inclusiveB, m_versA, m_versB); 1147 } 1148 1149 private static bool doCmp(bool inclusive, in Version a, in Version b) 1150 @safe 1151 { 1152 return inclusive ? a <= b : a < b; 1153 } 1154 1155 private static bool isDigit(char ch) @safe { return ch >= '0' && ch <= '9'; } 1156 private static string skipComp(ref string c) @safe { 1157 size_t idx = 0; 1158 while (idx < c.length && !isDigit(c[idx]) && c[idx] != Version.branchPrefix) idx++; 1159 enforce(idx < c.length, "Expected version number in version spec: "~c); 1160 string cmp = idx==c.length-1||idx==0? ">=" : c[0..idx]; 1161 c = c[idx..$]; 1162 switch(cmp) { 1163 default: enforce(false, "No/Unknown comparison specified: '"~cmp~"'"); return ">="; 1164 case ">=": goto case; case ">": goto case; 1165 case "<=": goto case; case "<": goto case; 1166 case "==": return cmp; 1167 } 1168 } 1169 } 1170 1171 enum VersionMatchMode { 1172 standard, /// Match according to SemVer rules 1173 strict /// Also include build metadata suffix in the comparison 1174 } 1175 1176 unittest { 1177 Version a, b; 1178 1179 assertNotThrown(a = Version("1.0.0"), "Constructing Version('1.0.0') failed"); 1180 assert(!a.isBranch, "Error: '1.0.0' treated as branch"); 1181 assert(a == a, "a == a failed"); 1182 1183 assertNotThrown(a = Version(Version.masterString), "Constructing Version("~Version.masterString~"') failed"); 1184 assert(a.isBranch, "Error: '"~Version.masterString~"' treated as branch"); 1185 assert(a.isMaster); 1186 assert(a == Version.masterBranch, "Constructed master version != default master version."); 1187 1188 assertNotThrown(a = Version("~BRANCH"), "Construction of branch Version failed."); 1189 assert(a.isBranch, "Error: '~BRANCH' not treated as branch'"); 1190 assert(!a.isMaster); 1191 assert(a == a, "a == a with branch failed"); 1192 1193 // opCmp 1194 a = Version("1.0.0"); 1195 b = Version("1.0.0"); 1196 assert(a == b, "a == b with a:'1.0.0', b:'1.0.0' failed"); 1197 b = Version("2.0.0"); 1198 assert(a != b, "a != b with a:'1.0.0', b:'2.0.0' failed"); 1199 1200 a = Version.masterBranch; 1201 b = Version("~BRANCH"); 1202 assert(a != b, "a != b with a:MASTER, b:'~branch' failed"); 1203 assert(a > b); 1204 assert(a < Version("0.0.0")); 1205 assert(b < Version("0.0.0")); 1206 assert(a > Version("~Z")); 1207 assert(b < Version("~Z")); 1208 1209 // SemVer 2.0.0-rc.2 1210 a = Version("2.0.0-rc.2"); 1211 b = Version("2.0.0-rc.3"); 1212 assert(a < b, "Failed: 2.0.0-rc.2 < 2.0.0-rc.3"); 1213 1214 a = Version("2.0.0-rc.2+build-metadata"); 1215 b = Version("2.0.0+build-metadata"); 1216 assert(a < b, "Failed: "~a.toString()~"<"~b.toString()); 1217 1218 // 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0 1219 Version[] versions; 1220 versions ~= Version("1.0.0-alpha"); 1221 versions ~= Version("1.0.0-alpha.1"); 1222 versions ~= Version("1.0.0-beta.2"); 1223 versions ~= Version("1.0.0-beta.11"); 1224 versions ~= Version("1.0.0-rc.1"); 1225 versions ~= Version("1.0.0"); 1226 for(int i=1; i<versions.length; ++i) 1227 for(int j=i-1; j>=0; --j) 1228 assert(versions[j] < versions[i], "Failed: " ~ versions[j].toString() ~ "<" ~ versions[i].toString()); 1229 1230 assert(Version("1.0.0+a") == Version("1.0.0+b")); 1231 1232 assert(Version("1.0.0").matches(Version("1.0.0+foo"))); 1233 assert(Version("1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.standard)); 1234 assert(!Version("1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.strict)); 1235 assert(Version("1.0.0+foo").matches(Version("1.0.0+foo"), VersionMatchMode.strict)); 1236 } 1237 1238 /// Determines whether the given string is a Git hash. 1239 bool isGitHash(string hash) @nogc nothrow pure @safe 1240 { 1241 import std.ascii : isHexDigit; 1242 import std.utf : byCodeUnit; 1243 1244 return hash.length >= 7 && hash.length <= 40 && hash.byCodeUnit.all!isHexDigit; 1245 } 1246 1247 @nogc nothrow pure @safe unittest { 1248 assert(isGitHash("73535568b79a0b124bc1653002637a830ce0fcb8")); 1249 assert(!isGitHash("735")); 1250 assert(!isGitHash("73535568b79a0b124bc1-53002637a830ce0fcb8")); 1251 assert(!isGitHash("73535568b79a0b124bg1")); 1252 }