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