diff --git a/src/_manifest.lua b/src/_manifest.lua index e5bc68c3..07bcfab9 100644 --- a/src/_manifest.lua +++ b/src/_manifest.lua @@ -19,6 +19,7 @@ "base/tools.lua", "base/tree.lua", "base/globals.lua", + "base/semver.lua", -- configuration data "base/field.lua", diff --git a/src/base/_foundation.lua b/src/base/_foundation.lua index 49e4f8ed..250aff7b 100644 --- a/src/base/_foundation.lua +++ b/src/base/_foundation.lua @@ -10,6 +10,7 @@ premake.modules = {} premake.extensions = premake.modules + local semver = dofile('semver.lua') local p = premake @@ -113,7 +114,7 @@ for i = 1, n do local fn = namespace[array[i]] if not fn then - error(string.format("Unable to find function '%s'", array[i])) + error(string.format("Unable to find function '%s'", array[i])) end fn(...) end @@ -123,7 +124,7 @@ --- --- Compare a version string of the form "major.minor.patch.dev" against a +-- Compare a version string that uses semver semantics against a -- version comparision string. Comparisions take the form of ">=5.0" (5.0 or -- later), "5.0" (5.0 or later), ">=5.0 <6.0" (5.0 or later but not 6.0 or -- later). @@ -141,31 +142,14 @@ return false end - local function parse(str) - local major, minor, patch, dev = str:match("^(%d+)%.?(%d*)%.?(%d*)(.-)$") - major = tonumber(major) or 0 - minor = tonumber(minor) or 0 - patch = tonumber(patch) or 0 - dev = dev or "" - return { major, minor, patch, dev } - end - - local function compare(a, b) - for i=1,4 do - if a[i] > b[i] then return 1 end - if a[i] < b[i] then return -1 end - end - return 0 - end - - local function eq(r) return r == 0 end - local function le(r) return r <= 0 end - local function lt(r) return r < 0 end - local function ge(r) return r >= 0 end - local function gt(r) return r > 0 end - - version = parse(version) + local function eq(a, b) return a == b end + local function le(a, b) return a <= b end + local function lt(a, b) return a < b end + local function ge(a, b) return a >= b end + local function gt(a, b) return a > b end + local function compat(a, b) return a ^ b end + version = semver(version) checks = string.explode(checks, " ", true) for i = 1, #checks do local check = checks[i] @@ -185,12 +169,15 @@ elseif check:startswith("=") then func = eq check = check:sub(2) + elseif check:startswith("^") then + func = compat + check = check:sub(2) else func = ge end - check = parse(check) - if not func(compare(version, check)) then + check = semver(check) + if not func(version, check) then return false end end diff --git a/src/base/semver.lua b/src/base/semver.lua new file mode 100644 index 00000000..1b848820 --- /dev/null +++ b/src/base/semver.lua @@ -0,0 +1,206 @@ +local semver = { + _VERSION = '1.2.0', + _DESCRIPTION = 'semver for Lua', + _URL = 'https://github.com/kikito/semver.lua', + _LICENSE = [[ + MIT LICENSE + + Copyright (c) 2015 Enrique GarcĂ­a Cota + + Permission is hereby granted, free of charge, to any person obtaining a + copy of tother software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and tother permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ]] +} + +local function checkPositiveInteger(number, name) + assert(number >= 0, name .. ' must be a valid positive number') + assert(math.floor(number) == number, name .. ' must be an integer') +end + +local function present(value) + return value and value ~= '' +end + +-- splitByDot("a.bbc.d") == {"a", "bbc", "d"} +local function splitByDot(str) + str = str or "" + local t, count = {}, 0 + str:gsub("([^%.]+)", function(c) + count = count + 1 + t[count] = c + end) + return t +end + +local function parsePrereleaseAndBuildWithSign(str) + local prereleaseWithSign, buildWithSign = str:match("^(-[^+]+)(+.+)$") + if not (prereleaseWithSign and buildWithSign) then + prereleaseWithSign = str:match("^(-.+)$") + buildWithSign = str:match("^(+.+)$") + end + assert(prereleaseWithSign or buildWithSign, ("The parameter %q must begin with + or - to denote a prerelease or a build"):format(str)) + return prereleaseWithSign, buildWithSign +end + +local function parsePrerelease(prereleaseWithSign) + if prereleaseWithSign then + local prerelease = prereleaseWithSign:match("^-(%w[%.%w-]*)$") + assert(prerelease, ("The prerelease %q is not a slash followed by alphanumerics, dots and slashes"):format(prereleaseWithSign)) + return prerelease + end +end + +local function parseBuild(buildWithSign) + if buildWithSign then + local build = buildWithSign:match("^%+(%w[%.%w-]*)$") + assert(build, ("The build %q is not a + sign followed by alphanumerics, dots and slashes"):format(buildWithSign)) + return build + end +end + +local function parsePrereleaseAndBuild(str) + if not present(str) then return nil, nil end + + local prereleaseWithSign, buildWithSign = parsePrereleaseAndBuildWithSign(str) + + local prerelease = parsePrerelease(prereleaseWithSign) + local build = parseBuild(buildWithSign) + + return prerelease, build +end + +local function parseVersion(str) + local sMajor, sMinor, sPatch, sPrereleaseAndBuild = str:match("^(%d+)%.?(%d*)%.?(%d*)(.-)$") + assert(type(sMajor) == 'string', ("Could not extract version number(s) from %q"):format(str)) + local major, minor, patch = tonumber(sMajor), tonumber(sMinor), tonumber(sPatch) + local prerelease, build = parsePrereleaseAndBuild(sPrereleaseAndBuild) + return major, minor, patch, prerelease, build +end + + +-- return 0 if a == b, -1 if a < b, and 1 if a > b +local function compare(a,b) + return a == b and 0 or a < b and -1 or 1 +end + +local function compareIds(myId, otherId) + if myId == otherId then return 0 + elseif not myId then return -1 + elseif not otherId then return 1 + end + + local selfNumber, otherNumber = tonumber(myId), tonumber(otherId) + + if selfNumber and otherNumber then -- numerical comparison + return compare(selfNumber, otherNumber) + -- numericals are always smaller than alphanums + elseif selfNumber then + return -1 + elseif otherNumber then + return 1 + else + return compare(myId, otherId) -- alphanumerical comparison + end +end + +local function smallerIdList(myIds, otherIds) + local myLength = #myIds + local comparison + + for i=1, myLength do + comparison = compareIds(myIds[i], otherIds[i]) + if comparison ~= 0 then + return comparison == -1 + end + -- if comparison == 0, continue loop + end + + return myLength < #otherIds +end + +local function smallerPrerelease(mine, other) + if mine == other or not mine then return false + elseif not other then return true + end + + return smallerIdList(splitByDot(mine), splitByDot(other)) +end + +local methods = {} + +function methods:nextMajor() + return semver(self.major + 1, 0, 0) +end +function methods:nextMinor() + return semver(self.major, self.minor + 1, 0) +end +function methods:nextPatch() + return semver(self.major, self.minor, self.patch + 1) +end + +local mt = { __index = methods } +function mt:__eq(other) + return self.major == other.major and + self.minor == other.minor and + self.patch == other.patch and + self.prerelease == other.prerelease + -- notice that build is ignored for precedence in semver 2.0.0 +end +function mt:__lt(other) + if self.major ~= other.major then return self.major < other.major end + if self.minor ~= other.minor then return self.minor < other.minor end + if self.patch ~= other.patch then return self.patch < other.patch end + return smallerPrerelease(self.prerelease, other.prerelease) + -- notice that build is ignored for precedence in semver 2.0.0 +end +-- This works like the "pessimisstic operator" in Rubygems. +-- if a and b are versions, a ^ b means "b is backwards-compatible with a" +-- in other words, "it's safe to upgrade from a to b" +function mt:__pow(other) + return self.major == other.major and + self.minor <= other.minor +end +function mt:__tostring() + local buffer = { ("%d.%d.%d"):format(self.major, self.minor, self.patch) } + if self.prerelease then table.insert(buffer, "-" .. self.prerelease) end + if self.build then table.insert(buffer, "+" .. self.build) end + return table.concat(buffer) +end + +local function new(major, minor, patch, prerelease, build) + assert(major, "At least one parameter is needed") + + if type(major) == 'string' then + major,minor,patch,prerelease,build = parseVersion(major) + end + patch = patch or 0 + minor = minor or 0 + + checkPositiveInteger(major, "major") + checkPositiveInteger(minor, "minor") + checkPositiveInteger(patch, "patch") + + local result = {major=major, minor=minor, patch=patch, prerelease=prerelease, build=build} + return setmetatable(result, mt) +end + +setmetatable(semver, { __call = function(_, ...) return new(...) end }) +semver._VERSION= semver(semver._VERSION) + +return semver diff --git a/tests/base/test_versions.lua b/tests/base/test_versions.lua index c7475bc9..1e9481ea 100644 --- a/tests/base/test_versions.lua +++ b/tests/base/test_versions.lua @@ -29,14 +29,22 @@ test.istrue(p.checkVersion("1.0.1", "1")) end - function suite.pass_majorOnly_alpha() - test.istrue(p.checkVersion("1.0.0.alpha1", "1")) +-- +-- prereleases should be fail against true release. +-- + + function suite.fail_majorOnly_alpha() + test.isfalse(p.checkVersion("1.0.0-alpha1", "1")) end - function suite.pass_majorOnly_dev() - test.istrue(p.checkVersion("1.0.0.dev", "1")) + function suite.fail_majorOnly_dev() + test.isfalse(p.checkVersion("1.0.0-dev", "1")) end +-- +-- ealier versions should be fail against major release. +-- + function suite.fail_earlierMajor() test.isfalse(p.checkVersion("0.9.0", "1")) end @@ -83,19 +91,19 @@ -- function suite.pass_alphaBeforeBeta() - test.istrue(p.checkVersion("1.0.0.beta1", "1.0.0.alpha1")) + test.istrue(p.checkVersion("1.0.0-beta1", "1.0.0-alpha1")) end function suite.fail_alphaBeforeBeta() - test.isfalse(p.checkVersion("1.0.0.alpha1", "1.0.0.beta1")) + test.isfalse(p.checkVersion("1.0.0-alpha1", "1.0.0-beta1")) end function suite.pass_betaBeforeDev() - test.istrue(p.checkVersion("1.0.0.dev", "1.0.0.beta1")) + test.istrue(p.checkVersion("1.0.0-dev", "1.0.0-beta1")) end function suite.fail_betaBeforeDev() - test.isfalse(p.checkVersion("1.0.0.beta1", "1.0.0.dev")) + test.isfalse(p.checkVersion("1.0.0-beta1", "1.0.0-dev")) end @@ -162,11 +170,11 @@ -- function suite.pass_onMultipleConditions() - test.istrue(p.checkVersion("1.2.0.0", ">=1.0 <2.0")) + test.istrue(p.checkVersion("1.2.0", ">=1.0 <2.0")) end function suite.fail_onMultipleConditions() - test.isfalse(p.checkVersion("2.2.0.0", ">=1.0 <2.0")) + test.isfalse(p.checkVersion("2.2.0", ">=1.0 <2.0")) end