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