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.log; 12 import dub.internal.vibecompat.core.file; 13 import dub.internal.vibecompat.data.json; 14 import dub.internal.vibecompat.inet.url; 15 import dub.package_; 16 import dub.semver; 17 18 import std.algorithm; 19 import std.array; 20 import std.exception; 21 import std.regex; 22 import std.string; 23 import std.typecons; 24 static import std.compiler; 25 26 27 /** Encapsulates the name of a package along with its dependency specification. 28 */ 29 struct PackageDependency { 30 /// Name of the referenced package. 31 string name; 32 33 /// Dependency specification used to select a particular version of the package. 34 Dependency spec; 35 } 36 37 38 /** 39 Represents a dependency specification. 40 41 A dependency specification either represents a specific version or version 42 range, or a path to a package. In addition to that it has `optional` and 43 `default_` flags to control how non-mandatory dependencies are handled. The 44 package name is notably not part of the dependency specification. 45 */ 46 struct Dependency { 47 @safe: 48 49 private { 50 // Shortcut to create >=0.0.0 51 enum ANY_IDENT = "*"; 52 bool m_inclusiveA = true; // A comparison > (true) or >= (false) 53 Version m_versA; 54 bool m_inclusiveB = true; // B comparison < (true) or <= (false) 55 Version m_versB; 56 NativePath m_path; 57 bool m_optional = false; 58 bool m_default = false; 59 } 60 61 /// A Dependency, which matches every valid version. 62 static @property Dependency any() { return Dependency(ANY_IDENT); } 63 64 /// An invalid dependency (with no possible version matches). 65 static @property Dependency invalid() { Dependency ret; ret.m_versA = Version.maxRelease; ret.m_versB = Version.minRelease; return ret; } 66 67 /** Constructs a new dependency specification from a string 68 69 See the `versionSpec` property for a description of the accepted 70 contents of that string. 71 */ 72 this(string spec) 73 { 74 this.versionSpec = spec; 75 } 76 77 /** Constructs a new dependency specification that matches a specific 78 version. 79 */ 80 this(const Version ver) 81 { 82 m_inclusiveA = m_inclusiveB = true; 83 m_versA = ver; 84 m_versB = ver; 85 } 86 87 /** Constructs a new dependency specification that matches a specific 88 path. 89 */ 90 this(NativePath path) 91 { 92 this(ANY_IDENT); 93 m_path = path; 94 } 95 96 /// If set, overrides any version based dependency selection. 97 @property void path(NativePath value) { m_path = value; } 98 /// ditto 99 @property NativePath path() const { return m_path; } 100 101 /// Determines if the dependency is required or optional. 102 @property bool optional() const { return m_optional; } 103 /// ditto 104 @property void optional(bool optional) { m_optional = optional; } 105 106 /// Determines if an optional dependency should be chosen by default. 107 @property bool default_() const { return m_default; } 108 /// ditto 109 @property void default_(bool value) { m_default = value; } 110 111 /// Returns true $(I iff) the version range only matches a specific version. 112 @property bool isExactVersion() const { return m_versA == m_versB; } 113 114 /// Returns the exact version matched by the version range. 115 @property Version version_() const { 116 enforce(m_versA == m_versB, "Dependency "~this.versionSpec~" is no exact version."); 117 return m_versA; 118 } 119 120 /** Sets/gets the matching version range as a specification string. 121 122 The acceptable forms for this string are as follows: 123 124 $(UL 125 $(LI `"1.0.0"` - a single version in SemVer format) 126 $(LI `"==1.0.0"` - alternative single version notation) 127 $(LI `">1.0.0"` - version range with a single bound) 128 $(LI `">1.0.0 <2.0.0"` - version range with two bounds) 129 $(LI `"~>1.0.0"` - a fuzzy version range) 130 $(LI `"~>1.0"` - a fuzzy version range with partial version) 131 $(LI `"^1.0.0"` - semver compatible version range (same version if 0.x.y, ==major >=minor.patch if x.y.z)) 132 $(LI `"^1.0"` - same as ^1.0.0) 133 $(LI `"~master"` - a branch name) 134 $(LI `"*" - match any version (see also `any`)) 135 ) 136 137 Apart from "$(LT)" and "$(GT)", "$(GT)=" and "$(LT)=" are also valid 138 comparators. 139 140 */ 141 @property void versionSpec(string ves) 142 { 143 static import std.string; 144 145 enforce(ves.length > 0); 146 147 if (ves == ANY_IDENT) { 148 // Any version is good. 149 ves = ">=0.0.0"; 150 } 151 152 if (ves.startsWith("~>")) { 153 // Shortcut: "~>x.y.z" variant. Last non-zero number will indicate 154 // the base for this so something like this: ">=x.y.z <x.(y+1).z" 155 m_inclusiveA = true; 156 m_inclusiveB = false; 157 ves = ves[2..$]; 158 m_versA = Version(expandVersion(ves)); 159 m_versB = Version(bumpVersion(ves) ~ "-0"); 160 } else if (ves.startsWith("^")) { 161 // Shortcut: "^x.y.z" variant. "Semver compatible" - no breaking changes. 162 // if 0.x.y, ==0.x.y 163 // if x.y.z, >=x.y.z <(x+1).0.0-0 164 // ^x.y is equivalent to ^x.y.0. 165 m_inclusiveA = true; 166 m_inclusiveB = false; 167 ves = ves[1..$].expandVersion; 168 m_versA = Version(ves); 169 m_versB = Version(bumpIncompatibleVersion(ves) ~ "-0"); 170 } else if (ves[0] == Version.branchPrefix) { 171 m_inclusiveA = true; 172 m_inclusiveB = true; 173 m_versA = m_versB = Version(ves); 174 } else if (std..string.indexOf("><=", ves[0]) == -1) { 175 m_inclusiveA = true; 176 m_inclusiveB = true; 177 m_versA = m_versB = Version(ves); 178 } else { 179 auto cmpa = skipComp(ves); 180 size_t idx2 = std..string.indexOf(ves, " "); 181 if (idx2 == -1) { 182 if (cmpa == "<=" || cmpa == "<") { 183 m_versA = Version.minRelease; 184 m_inclusiveA = true; 185 m_versB = Version(ves); 186 m_inclusiveB = cmpa == "<="; 187 } else if (cmpa == ">=" || cmpa == ">") { 188 m_versA = Version(ves); 189 m_inclusiveA = cmpa == ">="; 190 m_versB = Version.maxRelease; 191 m_inclusiveB = true; 192 } else { 193 // Converts "==" to ">=a&&<=a", which makes merging easier 194 m_versA = m_versB = Version(ves); 195 m_inclusiveA = m_inclusiveB = true; 196 } 197 } else { 198 enforce(cmpa == ">" || cmpa == ">=", "First comparison operator expected to be either > or >=, not "~cmpa); 199 assert(ves[idx2] == ' '); 200 m_versA = Version(ves[0..idx2]); 201 m_inclusiveA = cmpa == ">="; 202 string v2 = ves[idx2+1..$]; 203 auto cmpb = skipComp(v2); 204 enforce(cmpb == "<" || cmpb == "<=", "Second comparison operator expected to be either < or <=, not "~cmpb); 205 m_versB = Version(v2); 206 m_inclusiveB = cmpb == "<="; 207 208 enforce(!m_versA.isBranch && !m_versB.isBranch, format("Cannot compare branches: %s", ves)); 209 enforce(m_versA <= m_versB, "First version must not be greater than the second one."); 210 } 211 } 212 } 213 /// ditto 214 @property string versionSpec() 215 const { 216 static import std.string; 217 218 string r; 219 220 if (this == invalid) return "invalid"; 221 222 if (m_versA == m_versB && m_inclusiveA && m_inclusiveB) { 223 // Special "==" case 224 if (m_versA == Version.masterBranch) return "~master"; 225 else return m_versA.toString(); 226 } 227 228 // "~>", "^" case 229 if (m_inclusiveA && !m_inclusiveB && !m_versA.isBranch) { 230 auto vs = m_versA.toString(); 231 auto i1 = std..string.indexOf(vs, '-'), i2 = std..string.indexOf(vs, '+'); 232 auto i12 = i1 >= 0 ? i2 >= 0 ? i1 < i2 ? i1 : i2 : i1 : i2; 233 auto va = i12 >= 0 ? vs[0 .. i12] : vs; 234 auto parts = va.splitter('.').array; 235 assert(parts.length == 3, "Version string with a digit group count != 3: "~va); 236 237 foreach (i; 0 .. 3) { 238 auto vp = parts[0 .. i+1].join("."); 239 auto ve = Version(expandVersion(vp)); 240 auto veb = Version(bumpVersion(vp) ~ "-0"); 241 if (ve == m_versA && veb == m_versB) return "~>" ~ vp; 242 243 auto veb2 = Version(bumpIncompatibleVersion(expandVersion(vp)) ~ "-0"); 244 if (ve == m_versA && veb2 == m_versB) return "^" ~ vp; 245 } 246 } 247 248 if (m_versA != Version.minRelease) r = (m_inclusiveA ? ">=" : ">") ~ m_versA.toString(); 249 if (m_versB != Version.maxRelease) r ~= (r.length==0 ? "" : " ") ~ (m_inclusiveB ? "<=" : "<") ~ m_versB.toString(); 250 if (m_versA == Version.minRelease && m_versB == Version.maxRelease) r = ">=0.0.0"; 251 return r; 252 } 253 254 /** Returns a modified dependency that gets mapped to a given path. 255 256 This function will return an unmodified `Dependency` if it is not path 257 based. Otherwise, the given `path` will be prefixed to the existing 258 path. 259 */ 260 Dependency mapToPath(NativePath path) 261 const @trusted { // NOTE Path is @system in vibe.d 0.7.x and in the compatibility layer 262 if (m_path.empty || m_path.absolute) return this; 263 else { 264 Dependency ret = this; 265 ret.path = path ~ ret.path; 266 return ret; 267 } 268 } 269 270 /** Returns a human-readable string representation of the dependency 271 specification. 272 */ 273 string toString()() 274 const { 275 auto ret = versionSpec; 276 if (optional) { 277 if (default_) ret ~= " (optional, default)"; 278 else ret ~= " (optional)"; 279 } 280 281 // NOTE Path is @system in vibe.d 0.7.x and in the compatibility layer 282 () @trusted { 283 if (!path.empty) ret ~= " @"~path.toNativeString(); 284 } (); 285 286 return ret; 287 } 288 289 /** Returns a JSON representation of the dependency specification. 290 291 Simple specifications will be represented as a single specification 292 string (`versionSpec`), while more complex specifications will be 293 represented as a JSON object with optional "version", "path", "optional" 294 and "default" fields. 295 */ 296 Json toJson() 297 const @trusted { // NOTE Path and Json is @system in vibe.d 0.7.x and in the compatibility layer 298 Json json; 299 if( path.empty && !optional ){ 300 json = Json(this.versionSpec); 301 } else { 302 json = Json.emptyObject; 303 json["version"] = this.versionSpec; 304 if (!path.empty) json["path"] = path.toString(); 305 if (optional) json["optional"] = true; 306 if (default_) json["default"] = true; 307 } 308 return json; 309 } 310 311 @trusted unittest { 312 Dependency d = Dependency("==1.0.0"); 313 assert(d.toJson() == Json("1.0.0"), "Failed: " ~ d.toJson().toPrettyString()); 314 d = fromJson((fromJson(d.toJson())).toJson()); 315 assert(d == Dependency("1.0.0")); 316 assert(d.toJson() == Json("1.0.0"), "Failed: " ~ d.toJson().toPrettyString()); 317 } 318 319 /** Constructs a new `Dependency` from its JSON representation. 320 321 See `toJson` for a description of the JSON format. 322 */ 323 static Dependency fromJson(Json verspec) 324 @trusted { // NOTE Path and Json is @system in vibe.d 0.7.x and in the compatibility layer 325 Dependency dep; 326 if( verspec.type == Json.Type.object ){ 327 if( auto pp = "path" in verspec ) { 328 if (auto pv = "version" in verspec) 329 logDiagnostic("Ignoring version specification (%s) for path based dependency %s", pv.get!string, pp.get!string); 330 331 dep = Dependency.any; 332 dep.path = NativePath(verspec["path"].get!string); 333 } else { 334 enforce("version" in verspec, "No version field specified!"); 335 auto ver = verspec["version"].get!string; 336 // Using the string to be able to specify a range of versions. 337 dep = Dependency(ver); 338 } 339 340 if (auto po = "optional" in verspec) dep.optional = po.get!bool; 341 if (auto po = "default" in verspec) dep.default_ = po.get!bool; 342 } else { 343 // canonical "package-id": "version" 344 dep = Dependency(verspec.get!string); 345 } 346 return dep; 347 } 348 349 @trusted unittest { 350 assert(fromJson(parseJsonString("\">=1.0.0 <2.0.0\"")) == Dependency(">=1.0.0 <2.0.0")); 351 Dependency parsed = fromJson(parseJsonString(` 352 { 353 "version": "2.0.0", 354 "optional": true, 355 "default": true, 356 "path": "path/to/package" 357 } 358 `)); 359 Dependency d = Dependency.any; // supposed to ignore the version spec 360 d.optional = true; 361 d.default_ = true; 362 d.path = NativePath("path/to/package"); 363 assert(d == parsed); 364 // optional and path not checked by opEquals. 365 assert(d.optional == parsed.optional); 366 assert(d.default_ == parsed.default_); 367 assert(d.path == parsed.path); 368 } 369 370 /** Compares dependency specifications. 371 372 These methods are suitable for equality comparisons, as well as for 373 using `Dependency` as a key in hash or tree maps. 374 */ 375 bool opEquals(const Dependency o) 376 const { 377 // TODO(mdondorff): Check if not comparing the path is correct for all clients. 378 return o.m_inclusiveA == m_inclusiveA && o.m_inclusiveB == m_inclusiveB 379 && o.m_versA == m_versA && o.m_versB == m_versB 380 && o.m_optional == m_optional && o.m_default == m_default; 381 } 382 383 /// ditto 384 int opCmp(const Dependency o) 385 const { 386 if (m_inclusiveA != o.m_inclusiveA) return m_inclusiveA < o.m_inclusiveA ? -1 : 1; 387 if (m_inclusiveB != o.m_inclusiveB) return m_inclusiveB < o.m_inclusiveB ? -1 : 1; 388 if (m_versA != o.m_versA) return m_versA < o.m_versA ? -1 : 1; 389 if (m_versB != o.m_versB) return m_versB < o.m_versB ? -1 : 1; 390 if (m_optional != o.m_optional) return m_optional ? -1 : 1; 391 return 0; 392 } 393 394 /// ditto 395 size_t toHash() 396 const nothrow @trusted { 397 try { 398 size_t hash = 0; 399 hash = m_inclusiveA.hashOf(hash); 400 hash = m_versA.toString().hashOf(hash); 401 hash = m_inclusiveB.hashOf(hash); 402 hash = m_versB.toString().hashOf(hash); 403 hash = m_optional.hashOf(hash); 404 hash = m_default.hashOf(hash); 405 return hash; 406 } catch (Exception) assert(false); 407 } 408 409 /** Determines if this dependency specification is valid. 410 411 A specification is valid if it can match at least one version. 412 */ 413 bool valid() const { 414 return m_versA <= m_versB && doCmp(m_inclusiveA && m_inclusiveB, m_versA, m_versB); 415 } 416 417 /** Determines if this dependency specification matches arbitrary versions. 418 419 This is true in particular for the `any` constant. 420 */ 421 bool matchesAny() 422 const { 423 return m_inclusiveA && m_inclusiveB 424 && m_versA.toString() == "0.0.0" 425 && m_versB == Version.maxRelease; 426 } 427 428 unittest { 429 assert(Dependency("*").matchesAny); 430 assert(!Dependency(">0.0.0").matchesAny); 431 assert(!Dependency(">=1.0.0").matchesAny); 432 assert(!Dependency("<1.0.0").matchesAny); 433 } 434 435 /** Tests if the specification matches a specific version. 436 */ 437 bool matches(string vers) const { return matches(Version(vers)); } 438 /// ditto 439 bool matches(const(Version) v) const { return matches(v); } 440 /// ditto 441 bool matches(ref const(Version) v) const { 442 if (this.matchesAny) return true; 443 //logDebug(" try match: %s with: %s", v, this); 444 // Master only matches master 445 if(m_versA.isBranch) { 446 enforce(m_versA == m_versB); 447 return m_versA == v; 448 } 449 if(v.isBranch || m_versA.isBranch) 450 return m_versA == v; 451 if( !doCmp(m_inclusiveA, m_versA, v) ) 452 return false; 453 if( !doCmp(m_inclusiveB, v, m_versB) ) 454 return false; 455 return true; 456 } 457 458 /** Merges two dependency specifications. 459 460 The result is a specification that matches the intersection of the set 461 of versions matched by the individual specifications. Note that this 462 result can be invalid (i.e. not match any version). 463 */ 464 Dependency merge(ref const(Dependency) o) 465 const { 466 if (this.matchesAny) return o; 467 if (o.matchesAny) return this; 468 if (m_versA.isBranch != o.m_versA.isBranch) return invalid; 469 if (m_versB.isBranch != o.m_versB.isBranch) return invalid; 470 if (m_versA.isBranch) return m_versA == o.m_versA ? this : invalid; 471 // NOTE Path is @system in vibe.d 0.7.x and in the compatibility layer 472 if (() @trusted { return this.path != o.path; } ()) return invalid; 473 474 int acmp = m_versA.opCmp(o.m_versA); 475 int bcmp = m_versB.opCmp(o.m_versB); 476 477 Dependency d = this; 478 d.m_inclusiveA = !m_inclusiveA && acmp >= 0 ? false : o.m_inclusiveA; 479 d.m_versA = acmp > 0 ? m_versA : o.m_versA; 480 d.m_inclusiveB = !m_inclusiveB && bcmp <= 0 ? false : o.m_inclusiveB; 481 d.m_versB = bcmp < 0 ? m_versB : o.m_versB; 482 d.m_optional = m_optional && o.m_optional; 483 if (!d.valid) return invalid; 484 485 return d; 486 } 487 488 private static bool isDigit(char ch) { return ch >= '0' && ch <= '9'; } 489 private static string skipComp(ref string c) { 490 size_t idx = 0; 491 while (idx < c.length && !isDigit(c[idx]) && c[idx] != Version.branchPrefix) idx++; 492 enforce(idx < c.length, "Expected version number in version spec: "~c); 493 string cmp = idx==c.length-1||idx==0? ">=" : c[0..idx]; 494 c = c[idx..$]; 495 switch(cmp) { 496 default: enforce(false, "No/Unknown comparison specified: '"~cmp~"'"); return ">="; 497 case ">=": goto case; case ">": goto case; 498 case "<=": goto case; case "<": goto case; 499 case "==": return cmp; 500 } 501 } 502 503 private static bool doCmp(bool inclusive, ref const Version a, ref const Version b) { 504 return inclusive ? a <= b : a < b; 505 } 506 } 507 508 unittest { 509 Dependency a = Dependency(">=1.1.0"), b = Dependency(">=1.3.0"); 510 assert (a.merge(b).valid() && a.merge(b).versionSpec == ">=1.3.0", a.merge(b).toString()); 511 512 assertThrown(Dependency("<=2.0.0 >=1.0.0")); 513 assertThrown(Dependency(">=2.0.0 <=1.0.0")); 514 515 a = Dependency(">=1.0.0 <=5.0.0"); b = Dependency(">=2.0.0"); 516 assert (a.merge(b).valid() && a.merge(b).versionSpec == ">=2.0.0 <=5.0.0", a.merge(b).toString()); 517 518 assertThrown(a = Dependency(">1.0.0 ==5.0.0"), "Construction is invalid"); 519 520 a = Dependency(">1.0.0"); b = Dependency("<2.0.0"); 521 assert (a.merge(b).valid(), a.merge(b).toString()); 522 assert (a.merge(b).versionSpec == ">1.0.0 <2.0.0", a.merge(b).toString()); 523 524 a = Dependency(">2.0.0"); b = Dependency("<1.0.0"); 525 assert (!(a.merge(b)).valid(), a.merge(b).toString()); 526 527 a = Dependency(">=2.0.0"); b = Dependency("<=1.0.0"); 528 assert (!(a.merge(b)).valid(), a.merge(b).toString()); 529 530 a = Dependency("==2.0.0"); b = Dependency("==1.0.0"); 531 assert (!(a.merge(b)).valid(), a.merge(b).toString()); 532 533 a = Dependency("1.0.0"); b = Dependency("==1.0.0"); 534 assert (a == b); 535 536 a = Dependency("<=2.0.0"); b = Dependency("==1.0.0"); 537 Dependency m = a.merge(b); 538 assert (m.valid(), m.toString()); 539 assert (m.matches(Version("1.0.0"))); 540 assert (!m.matches(Version("1.1.0"))); 541 assert (!m.matches(Version("0.0.1"))); 542 543 544 // branches / head revisions 545 a = Dependency(Version.masterBranch); 546 assert(a.valid()); 547 assert(a.matches(Version.masterBranch)); 548 b = Dependency(Version.masterBranch); 549 m = a.merge(b); 550 assert(m.matches(Version.masterBranch)); 551 552 //assertThrown(a = Dependency(Version.MASTER_STRING ~ " <=1.0.0"), "Construction invalid"); 553 assertThrown(a = Dependency(">=1.0.0 " ~ Version.masterBranch.toString()), "Construction invalid"); 554 555 immutable string branch1 = Version.branchPrefix ~ "Branch1"; 556 immutable string branch2 = Version.branchPrefix ~ "Branch2"; 557 558 //assertThrown(a = Dependency(branch1 ~ " " ~ branch2), "Error: '" ~ branch1 ~ " " ~ branch2 ~ "' succeeded"); 559 //assertThrown(a = Dependency(Version.MASTER_STRING ~ " " ~ branch1), "Error: '" ~ Version.MASTER_STRING ~ " " ~ branch1 ~ "' succeeded"); 560 561 a = Dependency(branch1); 562 b = Dependency(branch2); 563 assert(!a.merge(b).valid, "Shouldn't be able to merge to different branches"); 564 b = a.merge(a); 565 assert(b.valid, "Should be able to merge the same branches. (?)"); 566 assert(a == b); 567 568 a = Dependency(branch1); 569 assert(a.matches(branch1), "Dependency(branch1) does not match 'branch1'"); 570 assert(a.matches(Version(branch1)), "Dependency(branch1) does not match Version('branch1')"); 571 assert(!a.matches(Version.masterBranch), "Dependency(branch1) matches Version.masterBranch"); 572 assert(!a.matches(branch2), "Dependency(branch1) matches 'branch2'"); 573 assert(!a.matches(Version("1.0.0")), "Dependency(branch1) matches '1.0.0'"); 574 a = Dependency(">=1.0.0"); 575 assert(!a.matches(Version(branch1)), "Dependency(1.0.0) matches 'branch1'"); 576 577 // Testing optional dependencies. 578 a = Dependency(">=1.0.0"); 579 assert(!a.optional, "Default is not optional."); 580 b = a; 581 assert(!a.merge(b).optional, "Merging two not optional dependencies wrong."); 582 a.optional = true; 583 assert(!a.merge(b).optional, "Merging optional with not optional wrong."); 584 b.optional = true; 585 assert(a.merge(b).optional, "Merging two optional dependencies wrong."); 586 587 // SemVer's sub identifiers. 588 a = Dependency(">=1.0.0-beta"); 589 assert(!a.matches(Version("1.0.0-alpha")), "Failed: match 1.0.0-alpha with >=1.0.0-beta"); 590 assert(a.matches(Version("1.0.0-beta")), "Failed: match 1.0.0-beta with >=1.0.0-beta"); 591 assert(a.matches(Version("1.0.0")), "Failed: match 1.0.0 with >=1.0.0-beta"); 592 assert(a.matches(Version("1.0.0-rc")), "Failed: match 1.0.0-rc with >=1.0.0-beta"); 593 594 // Approximate versions. 595 a = Dependency("~>3.0"); 596 b = Dependency(">=3.0.0 <4.0.0-0"); 597 assert(a == b, "Testing failed: " ~ a.toString()); 598 assert(a.matches(Version("3.1.146")), "Failed: Match 3.1.146 with ~>0.1.2"); 599 assert(!a.matches(Version("0.2.0")), "Failed: Match 0.2.0 with ~>0.1.2"); 600 assert(!a.matches(Version("4.0.0-beta.1"))); 601 a = Dependency("~>3.0.0"); 602 assert(a == Dependency(">=3.0.0 <3.1.0-0"), "Testing failed: " ~ a.toString()); 603 a = Dependency("~>3.5"); 604 assert(a == Dependency(">=3.5.0 <4.0.0-0"), "Testing failed: " ~ a.toString()); 605 a = Dependency("~>3.5.0"); 606 assert(a == Dependency(">=3.5.0 <3.6.0-0"), "Testing failed: " ~ a.toString()); 607 assert(!Dependency("~>3.0.0").matches(Version("3.1.0-beta"))); 608 609 a = Dependency("^0.1.2"); 610 assert(a == Dependency(">=0.1.2 <0.1.3-0")); 611 a = Dependency("^1.2.3"); 612 assert(a == Dependency(">=1.2.3 <2.0.0-0"), "Testing failed: " ~ a.toString()); 613 a = Dependency("^1.2"); 614 assert(a == Dependency(">=1.2.0 <2.0.0-0"), "Testing failed: " ~ a.toString()); 615 616 a = Dependency("~>0.1.1"); 617 b = Dependency("==0.1.0"); 618 assert(!a.merge(b).valid); 619 b = Dependency("==0.1.9999"); 620 assert(a.merge(b).valid); 621 b = Dependency("==0.2.0"); 622 assert(!a.merge(b).valid); 623 b = Dependency("==0.2.0-beta.1"); 624 assert(!a.merge(b).valid); 625 626 a = Dependency("~>1.0.1-beta"); 627 b = Dependency(">=1.0.1-beta <1.1.0-0"); 628 assert(a == b, "Testing failed: " ~ a.toString()); 629 assert(a.matches(Version("1.0.1-beta"))); 630 assert(a.matches(Version("1.0.1-beta.6"))); 631 632 a = Dependency("~d2test"); 633 assert(!a.optional); 634 assert(a.valid); 635 assert(a.version_ == Version("~d2test")); 636 637 a = Dependency("==~d2test"); 638 assert(!a.optional); 639 assert(a.valid); 640 assert(a.version_ == Version("~d2test")); 641 642 a = Dependency.any; 643 assert(!a.optional); 644 assert(a.valid); 645 assertThrown(a.version_); 646 assert(a.matches(Version.masterBranch)); 647 assert(a.matches(Version("1.0.0"))); 648 assert(a.matches(Version("0.0.1-pre"))); 649 b = Dependency(">=1.0.1"); 650 assert(b == a.merge(b)); 651 assert(b == b.merge(a)); 652 b = Dependency(Version.masterBranch); 653 assert(a.merge(b) == b); 654 assert(b.merge(a) == b); 655 656 a.optional = true; 657 assert(a.matches(Version.masterBranch)); 658 assert(a.matches(Version("1.0.0"))); 659 assert(a.matches(Version("0.0.1-pre"))); 660 b = Dependency(">=1.0.1"); 661 assert(b == a.merge(b)); 662 assert(b == b.merge(a)); 663 b = Dependency(Version.masterBranch); 664 assert(a.merge(b) == b); 665 assert(b.merge(a) == b); 666 667 logDebug("Dependency unittest success."); 668 } 669 670 unittest { 671 assert(Dependency("~>1.0.4").versionSpec == "~>1.0.4"); 672 assert(Dependency("~>1.4").versionSpec == "~>1.4"); 673 assert(Dependency("~>2").versionSpec == "~>2"); 674 assert(Dependency("~>1.0.4+1.2.3").versionSpec == "~>1.0.4"); 675 assert(Dependency("^0.1.2").versionSpec == "^0.1.2"); 676 assert(Dependency("^1.2.3").versionSpec == "^1.2.3"); 677 assert(Dependency("^1.2").versionSpec == "~>1.2"); // equivalent; prefer ~> 678 } 679 680 681 /** 682 Represents a version in semantic version format, or a branch identifier. 683 684 This can either have the form "~master", where "master" is a branch name, 685 or the form "major.update.bugfix-prerelease+buildmetadata" (see the 686 Semantic Versioning Specification v2.0.0 at http://semver.org/). 687 */ 688 struct Version { 689 @safe: 690 private { 691 static immutable MAX_VERS = "99999.0.0"; 692 static immutable UNKNOWN_VERS = "unknown"; 693 static immutable masterString = "~master"; 694 enum branchPrefix = '~'; 695 string m_version; 696 } 697 698 static immutable Version minRelease = Version("0.0.0"); 699 static immutable Version maxRelease = Version(MAX_VERS); 700 static immutable Version masterBranch = Version(masterString); 701 static immutable Version unknown = Version(UNKNOWN_VERS); 702 703 /** Constructs a new `Version` from its string representation. 704 */ 705 this(string vers) 706 { 707 enforce(vers.length > 1, "Version strings must not be empty."); 708 if (vers[0] != branchPrefix && vers.ptr !is UNKNOWN_VERS.ptr) 709 enforce(vers.isValidVersion(), "Invalid SemVer format: " ~ vers); 710 m_version = vers; 711 } 712 713 /** Constructs a new `Version` from its string representation. 714 715 This method is equivalent to calling the constructor and is used as an 716 endpoint for the serialization framework. 717 */ 718 static Version fromString(string vers) { return Version(vers); } 719 720 bool opEquals(const Version oth) const { return opCmp(oth) == 0; } 721 722 /// Tests if this represents a branch instead of a version. 723 @property bool isBranch() const { return m_version.length > 0 && m_version[0] == branchPrefix; } 724 725 /// Tests if this represents the master branch "~master". 726 @property bool isMaster() const { return m_version == masterString; } 727 728 /** Tests if this represents a pre-release version. 729 730 Note that branches are always considered pre-release versions. 731 */ 732 @property bool isPreRelease() const { 733 if (isBranch) return true; 734 return isPreReleaseVersion(m_version); 735 } 736 737 /// Tests if this represents the special unknown version constant. 738 @property bool isUnknown() const { return m_version == UNKNOWN_VERS; } 739 740 /** Compares two versions/branches for precedence. 741 742 Versions generally have precedence over branches and the master branch 743 has precedence over other branches. Apart from that, versions are 744 compared using SemVer semantics, while branches are compared 745 lexicographically. 746 */ 747 int opCmp(ref const Version other) 748 const { 749 if (isUnknown || other.isUnknown) { 750 throw new Exception("Can't compare unknown versions! (this: %s, other: %s)".format(this, other)); 751 } 752 if (isBranch || other.isBranch) { 753 if(m_version == other.m_version) return 0; 754 if (!isBranch) return 1; 755 else if (!other.isBranch) return -1; 756 if (isMaster) return 1; 757 else if (other.isMaster) return -1; 758 return this.m_version < other.m_version ? -1 : 1; 759 } 760 761 return compareVersions(m_version, other.m_version); 762 } 763 /// ditto 764 int opCmp(const Version other) const { return opCmp(other); } 765 766 /// Returns the string representation of the version/branch. 767 string toString() const { return m_version; } 768 } 769 770 unittest { 771 Version a, b; 772 773 assertNotThrown(a = Version("1.0.0"), "Constructing Version('1.0.0') failed"); 774 assert(!a.isBranch, "Error: '1.0.0' treated as branch"); 775 assert(a == a, "a == a failed"); 776 777 assertNotThrown(a = Version(Version.masterString), "Constructing Version("~Version.masterString~"') failed"); 778 assert(a.isBranch, "Error: '"~Version.masterString~"' treated as branch"); 779 assert(a.isMaster); 780 assert(a == Version.masterBranch, "Constructed master version != default master version."); 781 782 assertNotThrown(a = Version("~BRANCH"), "Construction of branch Version failed."); 783 assert(a.isBranch, "Error: '~BRANCH' not treated as branch'"); 784 assert(!a.isMaster); 785 assert(a == a, "a == a with branch failed"); 786 787 // opCmp 788 a = Version("1.0.0"); 789 b = Version("1.0.0"); 790 assert(a == b, "a == b with a:'1.0.0', b:'1.0.0' failed"); 791 b = Version("2.0.0"); 792 assert(a != b, "a != b with a:'1.0.0', b:'2.0.0' failed"); 793 a = Version.masterBranch; 794 b = Version("~BRANCH"); 795 assert(a != b, "a != b with a:MASTER, b:'~branch' failed"); 796 assert(a > b); 797 assert(a < Version("0.0.0")); 798 assert(b < Version("0.0.0")); 799 assert(a > Version("~Z")); 800 assert(b < Version("~Z")); 801 802 // SemVer 2.0.0-rc.2 803 a = Version("2.0.0-rc.2"); 804 b = Version("2.0.0-rc.3"); 805 assert(a < b, "Failed: 2.0.0-rc.2 < 2.0.0-rc.3"); 806 807 a = Version("2.0.0-rc.2+build-metadata"); 808 b = Version("2.0.0+build-metadata"); 809 assert(a < b, "Failed: "~a.toString()~"<"~b.toString()); 810 811 // 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 812 Version[] versions; 813 versions ~= Version("1.0.0-alpha"); 814 versions ~= Version("1.0.0-alpha.1"); 815 versions ~= Version("1.0.0-beta.2"); 816 versions ~= Version("1.0.0-beta.11"); 817 versions ~= Version("1.0.0-rc.1"); 818 versions ~= Version("1.0.0"); 819 for(int i=1; i<versions.length; ++i) 820 for(int j=i-1; j>=0; --j) 821 assert(versions[j] < versions[i], "Failed: " ~ versions[j].toString() ~ "<" ~ versions[i].toString()); 822 823 a = Version.unknown; 824 b = Version.minRelease; 825 assertThrown(a == b, "Failed: compared " ~ a.toString() ~ " with " ~ b.toString() ~ ""); 826 827 a = Version.unknown; 828 b = Version.unknown; 829 assertThrown(a == b, "Failed: UNKNOWN == UNKNOWN"); 830 831 assert(Version("1.0.0+a") == Version("1.0.0+b")); 832 }