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