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