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