
These are needed due to the raw string and fixnum comparisons which exist for legacy reasons, for instance compiler version and build comparisons.
431 lines
9.5 KiB
Ruby
431 lines
9.5 KiB
Ruby
require "version/null"
|
|
|
|
class Version
|
|
include Comparable
|
|
|
|
class Token
|
|
include Comparable
|
|
|
|
attr_reader :value
|
|
|
|
def initialize(value)
|
|
@value = value
|
|
end
|
|
|
|
def inspect
|
|
"#<#{self.class.name} #{value.inspect}>"
|
|
end
|
|
|
|
def to_s
|
|
value.to_s
|
|
end
|
|
|
|
def numeric?
|
|
false
|
|
end
|
|
end
|
|
|
|
class NullToken < Token
|
|
def initialize(value = nil)
|
|
super
|
|
end
|
|
|
|
def <=>(other)
|
|
case other
|
|
when NullToken
|
|
0
|
|
when NumericToken
|
|
other.value.zero? ? 0 : -1
|
|
when AlphaToken, BetaToken, RCToken
|
|
1
|
|
else
|
|
-1
|
|
end
|
|
end
|
|
|
|
def inspect
|
|
"#<#{self.class.name}>"
|
|
end
|
|
end
|
|
|
|
NULL_TOKEN = NullToken.new
|
|
|
|
class StringToken < Token
|
|
PATTERN = /[a-z]+[0-9]*/i
|
|
|
|
def initialize(value)
|
|
@value = value.to_s
|
|
end
|
|
|
|
def <=>(other)
|
|
case other
|
|
when StringToken
|
|
value <=> other.value
|
|
when NumericToken, NullToken
|
|
-Integer(other <=> self)
|
|
end
|
|
end
|
|
end
|
|
|
|
class NumericToken < Token
|
|
PATTERN = /[0-9]+/i
|
|
|
|
def initialize(value)
|
|
@value = value.to_i
|
|
end
|
|
|
|
def <=>(other)
|
|
case other
|
|
when NumericToken
|
|
value <=> other.value
|
|
when StringToken
|
|
1
|
|
when NullToken
|
|
-Integer(other <=> self)
|
|
end
|
|
end
|
|
|
|
def numeric?
|
|
true
|
|
end
|
|
end
|
|
|
|
class CompositeToken < StringToken
|
|
def rev
|
|
value[/[0-9]+/].to_i
|
|
end
|
|
end
|
|
|
|
class AlphaToken < CompositeToken
|
|
PATTERN = /alpha[0-9]*|a[0-9]+/i
|
|
|
|
def <=>(other)
|
|
case other
|
|
when AlphaToken
|
|
rev <=> other.rev
|
|
else
|
|
super
|
|
end
|
|
end
|
|
end
|
|
|
|
class BetaToken < CompositeToken
|
|
PATTERN = /beta[0-9]*|b[0-9]+/i
|
|
|
|
def <=>(other)
|
|
case other
|
|
when BetaToken
|
|
rev <=> other.rev
|
|
when AlphaToken
|
|
1
|
|
when RCToken, PatchToken
|
|
-1
|
|
else
|
|
super
|
|
end
|
|
end
|
|
end
|
|
|
|
class RCToken < CompositeToken
|
|
PATTERN = /rc[0-9]*/i
|
|
|
|
def <=>(other)
|
|
case other
|
|
when RCToken
|
|
rev <=> other.rev
|
|
when AlphaToken, BetaToken
|
|
1
|
|
when PatchToken
|
|
-1
|
|
else
|
|
super
|
|
end
|
|
end
|
|
end
|
|
|
|
class PatchToken < CompositeToken
|
|
PATTERN = /p[0-9]*/i
|
|
|
|
def <=>(other)
|
|
case other
|
|
when PatchToken
|
|
rev <=> other.rev
|
|
when AlphaToken, BetaToken, RCToken
|
|
1
|
|
else
|
|
super
|
|
end
|
|
end
|
|
end
|
|
|
|
SCAN_PATTERN = Regexp.union(
|
|
AlphaToken::PATTERN,
|
|
BetaToken::PATTERN,
|
|
RCToken::PATTERN,
|
|
PatchToken::PATTERN,
|
|
NumericToken::PATTERN,
|
|
StringToken::PATTERN
|
|
)
|
|
|
|
class FromURL < Version
|
|
def detected_from_url?
|
|
true
|
|
end
|
|
end
|
|
|
|
def self.detect(url, specs)
|
|
if specs.key?(:tag)
|
|
FromURL.new(specs[:tag][/((?:\d+\.)*\d+)/, 1])
|
|
else
|
|
FromURL.parse(url)
|
|
end
|
|
end
|
|
|
|
def self.create(val)
|
|
unless val.respond_to?(:to_str)
|
|
raise TypeError, "Version value must be a string; got a #{val.class} (#{val})"
|
|
end
|
|
|
|
if val.to_str.start_with?("HEAD")
|
|
HeadVersion.new(val)
|
|
else
|
|
Version.new(val)
|
|
end
|
|
end
|
|
|
|
def initialize(val)
|
|
unless val.respond_to?(:to_str)
|
|
raise TypeError, "Version value must be a string; got a #{val.class} (#{val})"
|
|
end
|
|
@version = val.to_str
|
|
end
|
|
|
|
def detected_from_url?
|
|
false
|
|
end
|
|
|
|
def head?
|
|
false
|
|
end
|
|
|
|
def null?
|
|
false
|
|
end
|
|
|
|
def <=>(other)
|
|
# Needed to retain API compatibility with older string comparisons
|
|
# for compiler versions, etc.
|
|
other = Version.new(other) if other.is_a? String
|
|
# Used by the *_build_version comparisons, which formerly returned Fixnum
|
|
other = Version.new(other.to_s) if other.is_a? Integer
|
|
return 1 if other.nil?
|
|
|
|
return unless other.is_a?(Version)
|
|
return 0 if version == other.version
|
|
return 1 if head? && !other.head?
|
|
return -1 if !head? && other.head?
|
|
return 0 if head? && other.head?
|
|
|
|
ltokens = tokens
|
|
rtokens = other.tokens
|
|
max = max(ltokens.length, rtokens.length)
|
|
l = r = 0
|
|
|
|
while l < max
|
|
a = ltokens[l] || NULL_TOKEN
|
|
b = rtokens[r] || NULL_TOKEN
|
|
|
|
if a == b
|
|
l += 1
|
|
r += 1
|
|
next
|
|
elsif a.numeric? && b.numeric?
|
|
return a <=> b
|
|
elsif a.numeric?
|
|
return 1 if a > NULL_TOKEN
|
|
l += 1
|
|
elsif b.numeric?
|
|
return -1 if b > NULL_TOKEN
|
|
r += 1
|
|
else
|
|
return a <=> b
|
|
end
|
|
end
|
|
|
|
0
|
|
end
|
|
alias eql? ==
|
|
|
|
def hash
|
|
version.hash
|
|
end
|
|
|
|
def to_s
|
|
version.dup
|
|
end
|
|
alias to_str to_s
|
|
|
|
protected
|
|
|
|
attr_reader :version
|
|
|
|
def tokens
|
|
@tokens ||= tokenize
|
|
end
|
|
|
|
private
|
|
|
|
def max(a, b)
|
|
a > b ? a : b
|
|
end
|
|
|
|
def tokenize
|
|
version.scan(SCAN_PATTERN).map! do |token|
|
|
case token
|
|
when /\A#{AlphaToken::PATTERN}\z/o then AlphaToken
|
|
when /\A#{BetaToken::PATTERN}\z/o then BetaToken
|
|
when /\A#{RCToken::PATTERN}\z/o then RCToken
|
|
when /\A#{PatchToken::PATTERN}\z/o then PatchToken
|
|
when /\A#{NumericToken::PATTERN}\z/o then NumericToken
|
|
when /\A#{StringToken::PATTERN}\z/o then StringToken
|
|
end.new(token)
|
|
end
|
|
end
|
|
|
|
def self.parse(spec)
|
|
version = _parse(spec)
|
|
version.nil? ? NULL : new(version)
|
|
end
|
|
|
|
def self._parse(spec)
|
|
spec = Pathname.new(spec) unless spec.is_a? Pathname
|
|
|
|
spec_s = spec.to_s
|
|
|
|
stem = if spec.directory?
|
|
spec.basename.to_s
|
|
elsif %r{((?:sourceforge\.net|sf\.net)/.*)/download$} =~ spec_s
|
|
Pathname.new(spec.dirname).stem
|
|
elsif /\.[^a-zA-Z]+$/ =~ spec_s
|
|
Pathname.new(spec_s).basename
|
|
else
|
|
spec.stem
|
|
end
|
|
|
|
# GitHub tarballs
|
|
# e.g. https://github.com/foo/bar/tarball/v1.2.3
|
|
# e.g. https://github.com/sam-github/libnet/tarball/libnet-1.1.4
|
|
# e.g. https://github.com/isaacs/npm/tarball/v0.2.5-1
|
|
# e.g. https://github.com/petdance/ack/tarball/1.93_02
|
|
m = %r{github\.com/.+/(?:zip|tar)ball/(?:v|\w+-)?((?:\d+[-._])+\d*)$}.match(spec_s)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# e.g. https://github.com/erlang/otp/tarball/OTP_R15B01 (erlang style)
|
|
m = /[-_]([Rr]\d+[AaBb]\d*(?:-\d+)?)/.match(spec_s)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# e.g. boost_1_39_0
|
|
m = /((?:\d+_)+\d+)$/.match(stem)
|
|
return m.captures.first.tr("_", ".") unless m.nil?
|
|
|
|
# e.g. foobar-4.5.1-1
|
|
# e.g. unrtf_0.20.4-1
|
|
# e.g. ruby-1.9.1-p243
|
|
m = /[-_]((?:\d+\.)*\d\.\d+-(?:p|rc|RC)?\d+)(?:[-._](?:bin|dist|stable|src|sources))?$/.match(stem)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# URL with no extension
|
|
# e.g. https://waf.io/waf-1.8.12
|
|
# e.g. https://codeload.github.com/gsamokovarov/jump/tar.gz/v0.7.1
|
|
m = /[-v]((?:\d+\.)*\d+)$/.match(spec_s)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# e.g. lame-398-1
|
|
m = /-((?:\d)+-\d+)/.match(stem)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# e.g. foobar-4.5.1
|
|
m = /-((?:\d+\.)*\d+)$/.match(stem)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# e.g. foobar-4.5.1b
|
|
m = /-((?:\d+\.)*\d+(?:[abc]|rc|RC)\d*)$/.match(stem)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# e.g. foobar-4.5.0-alpha5, foobar-4.5.0-beta1, or foobar-4.50-beta
|
|
m = /-((?:\d+\.)*\d+-(?:alpha|beta|rc)\d*)$/.match(stem)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# e.g. http://ftpmirror.gnu.org/libidn/libidn-1.29-win64.zip
|
|
# e.g. http://ftpmirror.gnu.org/libmicrohttpd/libmicrohttpd-0.9.17-w32.zip
|
|
m = /-(\d+\.\d+(?:\.\d+)?)-w(?:in)?(?:32|64)$/.match(stem)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# Opam packages
|
|
# e.g. https://opam.ocaml.org/archives/sha.1.9+opam.tar.gz
|
|
# e.g. https://opam.ocaml.org/archives/lablgtk.2.18.3+opam.tar.gz
|
|
# e.g. https://opam.ocaml.org/archives/easy-format.1.0.2+opam.tar.gz
|
|
m = /\.(\d+\.\d+(?:\.\d+)?)\+opam$/.match(stem)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# e.g. http://ftpmirror.gnu.org/mtools/mtools-4.0.18-1.i686.rpm
|
|
# e.g. http://ftpmirror.gnu.org/autogen/autogen-5.5.7-5.i386.rpm
|
|
# e.g. http://ftpmirror.gnu.org/libtasn1/libtasn1-2.8-x86.zip
|
|
# e.g. http://ftpmirror.gnu.org/libtasn1/libtasn1-2.8-x64.zip
|
|
# e.g. http://ftpmirror.gnu.org/mtools/mtools_4.0.18_i386.deb
|
|
m = /[-_](\d+\.\d+(?:\.\d+)?(?:-\d+)?)[-_.](?:i[36]86|x86|x64(?:[-_](?:32|64))?)$/.match(stem)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# e.g. foobar4.5.1
|
|
m = /((?:\d+\.)*\d+)$/.match(stem)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# e.g. foobar-4.5.0-bin
|
|
m = /-((?:\d+\.)+\d+[abc]?)[-._](?:bin|dist|stable|src|sources?)$/.match(stem)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# e.g. dash_0.5.5.1.orig.tar.gz (Debian style)
|
|
m = /_((?:\d+\.)+\d+[abc]?)[.]orig$/.match(stem)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# e.g. https://www.openssl.org/source/openssl-0.9.8s.tar.gz
|
|
m = /-v?([^-]+)/.match(stem)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# e.g. astyle_1.23_macosx.tar.gz
|
|
m = /_([^_]+)/.match(stem)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# e.g. http://mirrors.jenkins-ci.org/war/1.486/jenkins.war
|
|
# e.g. https://github.com/foo/bar/releases/download/0.10.11/bar.phar
|
|
m = %r{/(\d\.\d+(\.\d+)?)}.match(spec_s)
|
|
return m.captures.first unless m.nil?
|
|
|
|
# e.g. http://www.ijg.org/files/jpegsrc.v8d.tar.gz
|
|
m = /\.v(\d+[a-z]?)/.match(stem)
|
|
return m.captures.first unless m.nil?
|
|
end
|
|
end
|
|
|
|
class HeadVersion < Version
|
|
attr_reader :commit
|
|
|
|
def initialize(val)
|
|
super
|
|
@commit = @version[/^HEAD-(.+)$/, 1]
|
|
end
|
|
|
|
def update_commit(commit)
|
|
@commit = commit
|
|
@version = if commit
|
|
"HEAD-#{commit}"
|
|
else
|
|
"HEAD"
|
|
end
|
|
end
|
|
|
|
def head?
|
|
true
|
|
end
|
|
end
|