diff --git a/changelog/semver-compatibility-operator.dd b/changelog/semver-compatibility-operator.dd new file mode 100644 index 0000000..c21b0d8 --- /dev/null +++ b/changelog/semver-compatibility-operator.dd @@ -0,0 +1,8 @@ +Added SemVer compatibility operator "^" + +Dub now supports version specifications of the form `^x.y.z`. +This corresponds to a "semver compatible version", ie. any version up from +`x.y.z` with the same major number. If the major number is 0, only the same +version matches. This corresponds to the versions listed on https://semver.org/ as +compatible with the public API of the version given. +`^x.y` is equivalent to `^x.y.0`. diff --git a/source/dub/dependency.d b/source/dub/dependency.d index e1e40b0..1d2c51f 100644 --- a/source/dub/dependency.d +++ b/source/dub/dependency.d @@ -128,6 +128,8 @@ $(LI `">1.0.0 <2.0.0"` - version range with two bounds) $(LI `"~>1.0.0"` - a fuzzy version range) $(LI `"~>1.0"` - a fuzzy version range with partial version) + $(LI `"^1.0.0"` - semver compatible version range (same version if 0.x.y, ==major >=minor.patch if x.y.z)) + $(LI `"^1.0"` - same as ^1.0.0) $(LI `"~master"` - a branch name) $(LI `"*" - match any version (see also `any`)) ) @@ -156,6 +158,16 @@ ves = ves[2..$]; m_versA = Version(expandVersion(ves)); m_versB = Version(bumpVersion(ves) ~ "-0"); + } else if (ves.startsWith("^")) { + // Shortcut: "^x.y.z" variant. "Semver compatible" - no breaking changes. + // if 0.x.y, ==0.x.y + // if x.y.z, >=x.y.z <(x+1).0.0-0 + // ^x.y is equivalent to ^x.y.0. + m_inclusiveA = true; + m_inclusiveB = false; + ves = ves[1..$].expandVersion; + m_versA = Version(ves); + m_versB = Version(bumpIncompatibleVersion(ves) ~ "-0"); } else if (ves[0] == Version.branchPrefix) { m_inclusiveA = true; m_inclusiveB = true; @@ -214,7 +226,7 @@ else return m_versA.toString(); } - // "~>" case + // "~>", "^" case if (m_inclusiveA && !m_inclusiveB && !m_versA.isBranch) { auto vs = m_versA.toString(); auto i1 = std.string.indexOf(vs, '-'), i2 = std.string.indexOf(vs, '+'); @@ -228,6 +240,9 @@ auto ve = Version(expandVersion(vp)); auto veb = Version(bumpVersion(vp) ~ "-0"); if (ve == m_versA && veb == m_versB) return "~>" ~ vp; + + auto veb2 = Version(bumpIncompatibleVersion(expandVersion(vp)) ~ "-0"); + if (ve == m_versA && veb2 == m_versB) return "^" ~ vp; } } @@ -592,6 +607,13 @@ assert(a == Dependency(">=3.5.0 <3.6.0-0"), "Testing failed: " ~ a.toString()); assert(!Dependency("~>3.0.0").matches(Version("3.1.0-beta"))); + a = Dependency("^0.1.2"); + assert(a == Dependency(">=0.1.2 <0.1.3-0")); + a = Dependency("^1.2.3"); + assert(a == Dependency(">=1.2.3 <2.0.0-0"), "Testing failed: " ~ a.toString()); + a = Dependency("^1.2"); + assert(a == Dependency(">=1.2.0 <2.0.0-0"), "Testing failed: " ~ a.toString()); + a = Dependency("~>0.1.1"); b = Dependency("==0.1.0"); assert(!a.merge(b).valid); @@ -651,6 +673,9 @@ assert(Dependency("~>1.4").versionSpec == "~>1.4"); assert(Dependency("~>2").versionSpec == "~>2"); assert(Dependency("~>1.0.4+1.2.3").versionSpec == "~>1.0.4"); + assert(Dependency("^0.1.2").versionSpec == "^0.1.2"); + assert(Dependency("^1.2.3").versionSpec == "^1.2.3"); + assert(Dependency("^1.2").versionSpec == "~>1.2"); // equivalent; prefer ~> } diff --git a/source/dub/recipe/packagerecipe.d b/source/dub/recipe/packagerecipe.d index 5a73030..5ecf3de 100644 --- a/source/dub/recipe/packagerecipe.d +++ b/source/dub/recipe/packagerecipe.d @@ -226,7 +226,11 @@ auto files = appender!(string[]); import dub.project : buildSettingsVars; - auto envVars = environment.toAA(); + import std.typecons : Nullable; + + static Nullable!(string[string]) envVarCache; + + if (envVarCache.isNull) envVarCache = environment.toAA(); foreach (suffix, paths; paths_map) { if (!platform.matchesSpecification(suffix)) @@ -238,7 +242,7 @@ if (!path.absolute) path = base_path ~ path; if (!existsFile(path) || !isDir(path.toNativeString())) { import std.algorithm : any, find; - const hasVar = chain(buildSettingsVars, envVars.byKey).any!((string var) { + const hasVar = chain(buildSettingsVars, envVarCache.get.byKey).any!((string var) { return spath.find("$"~var).length > 0 || spath.find("${"~var~"}").length > 0; }); if (!hasVar) diff --git a/source/dub/semver.d b/source/dub/semver.d index a99749c..e4c0143 100644 --- a/source/dub/semver.d +++ b/source/dub/semver.d @@ -252,6 +252,38 @@ } /** + Increments a given version number to the next incompatible version. + + Prerelease and build metadata information is removed. + + This implements the "^" comparison operator, which represents "nonbreaking semver compatibility." + With 0.x.y releases, any release can break. + With x.y.z releases, only major releases can break. +*/ +string bumpIncompatibleVersion(string ver) +pure { + // Cut off metadata and prerelease information. + auto mi = ver.indexOfAny("+-"); + if (mi > 0) ver = ver[0..mi]; + // Increment next to last version from a[.b[.c]]. + auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0 + assert(splitted.length == 3, "Version corrupt: " ~ ver); + if (splitted[0] == "0") splitted[2] = to!string(to!int(splitted[2]) + 1); + else splitted = [to!string(to!int(splitted[0]) + 1), "0", "0"]; + return splitted.join("."); +} +/// +unittest { + assert(bumpIncompatibleVersion("0.0.0") == "0.0.1"); + assert(bumpIncompatibleVersion("0.1.2") == "0.1.3"); + assert(bumpIncompatibleVersion("1.0.0") == "2.0.0"); + assert(bumpIncompatibleVersion("1.2.3") == "2.0.0"); + assert(bumpIncompatibleVersion("1.2.3+metadata") == "2.0.0"); + assert(bumpIncompatibleVersion("1.2.3-pre.release") == "2.0.0"); + assert(bumpIncompatibleVersion("1.2.3-pre.release+metadata") == "2.0.0"); +} + +/** Takes a partial version and expands it to a valid SemVer version. This function corresponds to the semantivs of the "~>" comparison operator's