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