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