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