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