Initial implementation of package specs, including parser.
spec.py can parse full dependence specs like this: openmpi@1.4.3:1.4.5%intel+debug ^hwloc@1.2 These will be used to specify how to install packages and their dependencies, as well as to specify restrictions (e.g., on particular versions) for dependencies. e.g.: class SomePackage(Package): depends_on('boost@1.46,1.49') This would require either of those two versions of boost. This also moves depends_on out to relations.py and adds "provides", which will allow packages to provide virtual dependences. This is just initial implementation of the parsing and objects to represent specs. They're not integrated with packages yet.
This commit is contained in:
parent
5dd2c53e38
commit
c3f52f0200
10 changed files with 821 additions and 76 deletions
|
@ -2,5 +2,6 @@
|
||||||
from utils import *
|
from utils import *
|
||||||
from error import *
|
from error import *
|
||||||
|
|
||||||
from package import Package, depends_on
|
from package import Package
|
||||||
|
from relations import depends_on, provides
|
||||||
from multi_function import platform
|
from multi_function import platform
|
||||||
|
|
20
lib/spack/spack/dependency.py
Normal file
20
lib/spack/spack/dependency.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
"""
|
||||||
|
This file defines the dependence relation in spack.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import packages
|
||||||
|
|
||||||
|
|
||||||
|
class Dependency(object):
|
||||||
|
"""Represents a dependency from one package to another.
|
||||||
|
"""
|
||||||
|
def __init__(self, name, version):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def package(self):
|
||||||
|
return packages.get(self.name)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "<dep: %s>" % self.name
|
|
@ -27,6 +27,7 @@
|
||||||
|
|
||||||
from multi_function import platform
|
from multi_function import platform
|
||||||
from stage import Stage
|
from stage import Stage
|
||||||
|
from dependency import *
|
||||||
|
|
||||||
|
|
||||||
class Package(object):
|
class Package(object):
|
||||||
|
@ -228,14 +229,24 @@ class SomePackage(Package):
|
||||||
clean() (some of them do this), and others to provide custom behavior.
|
clean() (some of them do this), and others to provide custom behavior.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, sys_type=arch.sys_type()):
|
"""By default a package has no dependencies."""
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
"""By default we build in parallel. Subclasses can override this."""
|
||||||
|
parallel = True
|
||||||
|
|
||||||
|
"""Remove tarball and build by default. If this is true, leave them."""
|
||||||
|
dirty = False
|
||||||
|
|
||||||
|
"""Controls whether install and uninstall check deps before running."""
|
||||||
|
ignore_dependencies = False
|
||||||
|
|
||||||
|
def __init__(self, sys_type = arch.sys_type()):
|
||||||
|
# Check for attributes that derived classes must set.
|
||||||
attr.required(self, 'homepage')
|
attr.required(self, 'homepage')
|
||||||
attr.required(self, 'url')
|
attr.required(self, 'url')
|
||||||
attr.required(self, 'md5')
|
attr.required(self, 'md5')
|
||||||
|
|
||||||
attr.setdefault(self, 'dependencies', [])
|
|
||||||
attr.setdefault(self, 'parallel', True)
|
|
||||||
|
|
||||||
# Architecture for this package.
|
# Architecture for this package.
|
||||||
self.sys_type = sys_type
|
self.sys_type = sys_type
|
||||||
|
|
||||||
|
@ -258,15 +269,9 @@ def __init__(self, sys_type=arch.sys_type()):
|
||||||
# This adds a bunch of convenience commands to the package's module scope.
|
# This adds a bunch of convenience commands to the package's module scope.
|
||||||
self.add_commands_to_module()
|
self.add_commands_to_module()
|
||||||
|
|
||||||
# Controls whether install and uninstall check deps before acting.
|
|
||||||
self.ignore_dependencies = False
|
|
||||||
|
|
||||||
# Empty at first; only compute dependents if necessary
|
# Empty at first; only compute dependents if necessary
|
||||||
self._dependents = None
|
self._dependents = None
|
||||||
|
|
||||||
# Whether to remove intermediate build/install when things go wrong.
|
|
||||||
self.dirty = False
|
|
||||||
|
|
||||||
# stage used to build this package.
|
# stage used to build this package.
|
||||||
self.stage = Stage(self.stage_name, self.url)
|
self.stage = Stage(self.stage_name, self.url)
|
||||||
|
|
||||||
|
@ -569,37 +574,6 @@ def do_clean_dist(self):
|
||||||
tty.msg("Successfully cleaned %s" % self.name)
|
tty.msg("Successfully cleaned %s" % self.name)
|
||||||
|
|
||||||
|
|
||||||
class Dependency(object):
|
|
||||||
"""Represents a dependency from one package to another."""
|
|
||||||
def __init__(self, name, **kwargs):
|
|
||||||
self.name = name
|
|
||||||
for key in kwargs:
|
|
||||||
setattr(self, key, kwargs[key])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def package(self):
|
|
||||||
return packages.get(self.name)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<dep: %s>" % self.name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.__repr__()
|
|
||||||
|
|
||||||
|
|
||||||
def depends_on(*args, **kwargs):
|
|
||||||
"""Adds a dependencies local variable in the locals of
|
|
||||||
the calling class, based on args.
|
|
||||||
"""
|
|
||||||
# This gets the calling frame so we can pop variables into it
|
|
||||||
locals = sys._getframe(1).f_locals
|
|
||||||
|
|
||||||
# Put deps into the dependencies variable
|
|
||||||
dependencies = locals.setdefault("dependencies", [])
|
|
||||||
for name in args:
|
|
||||||
dependencies.append(Dependency(name))
|
|
||||||
|
|
||||||
|
|
||||||
class MakeExecutable(Executable):
|
class MakeExecutable(Executable):
|
||||||
"""Special Executable for make so the user can specify parallel or
|
"""Special Executable for make so the user can specify parallel or
|
||||||
not on a per-invocation basis. Using 'parallel' as a kwarg will
|
not on a per-invocation basis. Using 'parallel' as a kwarg will
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
import spack.attr as attr
|
import spack.attr as attr
|
||||||
import spack.error as serr
|
import spack.error as serr
|
||||||
|
|
||||||
# Valid package names
|
# Valid package names -- can contain - but can't start with it.
|
||||||
valid_package = r'^[a-zA-Z0-9_-]*$'
|
valid_package = r'^\w[\w-]*$'
|
||||||
|
|
||||||
# Don't allow consecutive [_-] in package names
|
# Don't allow consecutive [_-] in package names
|
||||||
invalid_package = r'[_-][_-]+'
|
invalid_package = r'[_-][_-]+'
|
||||||
|
|
95
lib/spack/spack/parse.py
Normal file
95
lib/spack/spack/parse.py
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import re
|
||||||
|
import spack.error as err
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
class UnlexableInputError(err.SpackError):
|
||||||
|
"""Raised when we don't know how to lex something."""
|
||||||
|
def __init__(self, message):
|
||||||
|
super(UnlexableInputError, self).__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class ParseError(err.SpackError):
|
||||||
|
"""Raised when we don't hit an error while parsing."""
|
||||||
|
def __init__(self, message):
|
||||||
|
super(ParseError, self).__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class Token:
|
||||||
|
"""Represents tokens; generated from input by lexer and fed to parse()."""
|
||||||
|
def __init__(self, type, value=''):
|
||||||
|
self.type = type
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "'%s'" % self.value
|
||||||
|
|
||||||
|
def is_a(self, type):
|
||||||
|
return self.type == type
|
||||||
|
|
||||||
|
def __cmp__(self, other):
|
||||||
|
return cmp((self.type, self.value),
|
||||||
|
(other.type, other.value))
|
||||||
|
|
||||||
|
class Lexer(object):
|
||||||
|
"""Base class for Lexers that keep track of line numbers."""
|
||||||
|
def __init__(self, lexicon):
|
||||||
|
self.scanner = re.Scanner(lexicon)
|
||||||
|
|
||||||
|
def token(self, type, value=''):
|
||||||
|
return Token(type, value)
|
||||||
|
|
||||||
|
def lex(self, text):
|
||||||
|
tokens, remainder = self.scanner.scan(text)
|
||||||
|
if remainder:
|
||||||
|
raise UnlexableInputError("Unlexable input:\n%s\n" % remainder)
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
class Parser(object):
|
||||||
|
"""Base class for simple recursive descent parsers."""
|
||||||
|
def __init__(self, lexer):
|
||||||
|
self.tokens = iter([]) # iterators over tokens, handled in order. Starts empty.
|
||||||
|
self.token = None # last accepted token
|
||||||
|
self.next = None # next token
|
||||||
|
self.lexer = lexer
|
||||||
|
|
||||||
|
def gettok(self):
|
||||||
|
"""Puts the next token in the input stream into self.next."""
|
||||||
|
try:
|
||||||
|
self.next = self.tokens.next()
|
||||||
|
except StopIteration:
|
||||||
|
self.next = None
|
||||||
|
|
||||||
|
def push_tokens(self, iterable):
|
||||||
|
"""Adds all tokens in some iterable to the token stream."""
|
||||||
|
self.tokens = itertools.chain(iter(iterable), iter([self.next]), self.tokens)
|
||||||
|
self.gettok()
|
||||||
|
|
||||||
|
def accept(self, id):
|
||||||
|
"""Puts the next symbol in self.token if we like it. Then calls gettok()"""
|
||||||
|
if self.next and self.next.is_a(id):
|
||||||
|
self.token = self.next
|
||||||
|
self.gettok()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def unexpected_token(self):
|
||||||
|
raise ParseError("Unexpected token: %s." % self.next)
|
||||||
|
|
||||||
|
def expect(self, id):
|
||||||
|
"""Like accept(), but fails if we don't like the next token."""
|
||||||
|
if self.accept(id):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
if self.next:
|
||||||
|
self.unexpected_token()
|
||||||
|
else:
|
||||||
|
raise ParseError("Unexpected end of file.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def parse(self, text):
|
||||||
|
self.push_tokens(self.lexer.lex(text))
|
||||||
|
return self.do_parse()
|
70
lib/spack/spack/relations.py
Normal file
70
lib/spack/spack/relations.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
"""
|
||||||
|
This package contains relationships that can be defined among packages.
|
||||||
|
Relations are functions that can be called inside a package definition,
|
||||||
|
for example:
|
||||||
|
|
||||||
|
class OpenMPI(Package):
|
||||||
|
depends_on("hwloc")
|
||||||
|
provides("mpi")
|
||||||
|
...
|
||||||
|
|
||||||
|
The available relations are:
|
||||||
|
|
||||||
|
depends_on
|
||||||
|
Above, the OpenMPI package declares that it "depends on" hwloc. This means
|
||||||
|
that the hwloc package needs to be installed before OpenMPI can be
|
||||||
|
installed. When a user runs 'spack install openmpi', spack will fetch
|
||||||
|
hwloc and install it first.
|
||||||
|
|
||||||
|
provides
|
||||||
|
This is useful when more than one package can satisfy a dependence. Above,
|
||||||
|
OpenMPI declares that it "provides" mpi. Other implementations of the MPI
|
||||||
|
interface, like mvapich and mpich, also provide mpi, e.g.:
|
||||||
|
|
||||||
|
class Mvapich(Package):
|
||||||
|
provides("mpi")
|
||||||
|
...
|
||||||
|
|
||||||
|
class Mpich(Package):
|
||||||
|
provides("mpi")
|
||||||
|
...
|
||||||
|
|
||||||
|
Instead of depending on openmpi, mvapich, or mpich, another package can
|
||||||
|
declare that it depends on "mpi":
|
||||||
|
|
||||||
|
class Mpileaks(Package):
|
||||||
|
depends_on("mpi")
|
||||||
|
...
|
||||||
|
|
||||||
|
Now the user can pick which MPI they would like to build with when they
|
||||||
|
install mpileaks. For example, the user could install 3 instances of
|
||||||
|
mpileaks, one for each MPI version, by issuing these three commands:
|
||||||
|
|
||||||
|
spack install mpileaks ^openmpi
|
||||||
|
spack install mpileaks ^mvapich
|
||||||
|
spack install mpileaks ^mpich
|
||||||
|
"""
|
||||||
|
from dependency import Dependency
|
||||||
|
|
||||||
|
|
||||||
|
def depends_on(*args):
|
||||||
|
"""Adds a dependencies local variable in the locals of
|
||||||
|
the calling class, based on args.
|
||||||
|
"""
|
||||||
|
# Get the enclosing package's scope and add deps to it.
|
||||||
|
locals = sys._getframe(1).f_locals
|
||||||
|
dependencies = locals.setdefault("dependencies", [])
|
||||||
|
for name in args:
|
||||||
|
dependencies.append(Dependency(name))
|
||||||
|
|
||||||
|
|
||||||
|
def provides(*args):
|
||||||
|
"""Allows packages to provide a virtual dependency. If a package provides
|
||||||
|
"mpi", other packages can declare that they depend on "mpi", and spack
|
||||||
|
can use the providing package to satisfy the dependency.
|
||||||
|
"""
|
||||||
|
# Get the enclosing package's scope and add deps to it.
|
||||||
|
locals = sys._getframe(1).f_locals
|
||||||
|
provides = locals.setdefault("provides", [])
|
||||||
|
for name in args:
|
||||||
|
provides.append(name)
|
235
lib/spack/spack/spec.py
Normal file
235
lib/spack/spack/spec.py
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
"""
|
||||||
|
Spack allows very fine-grained control over how packages are installed and
|
||||||
|
over how they are built and configured. To make this easy, it has its own
|
||||||
|
syntax for declaring a dependence. We call a descriptor of a particular
|
||||||
|
package configuration a "spec".
|
||||||
|
|
||||||
|
The syntax looks like this:
|
||||||
|
|
||||||
|
spack install mpileaks ^openmpi @1.2:1.4 +debug %intel @12.1
|
||||||
|
0 1 2 3 4 5
|
||||||
|
|
||||||
|
The first part of this is the command, 'spack install'. The rest of the
|
||||||
|
line is a spec for a particular installation of the mpileaks package.
|
||||||
|
|
||||||
|
0. The package to install
|
||||||
|
|
||||||
|
1. A dependency of the package, prefixed by ^
|
||||||
|
|
||||||
|
2. A version descriptor for the package. This can either be a specific
|
||||||
|
version, like "1.2", or it can be a range of versions, e.g. "1.2:1.4".
|
||||||
|
If multiple specific versions or multiple ranges are acceptable, they
|
||||||
|
can be separated by commas, e.g. if a package will only build with
|
||||||
|
versions 1.0, 1.2-1.4, and 1.6-1.8 of mavpich, you could say:
|
||||||
|
|
||||||
|
depends_on("mvapich@1.0,1.2:1.4,1.6:1.8")
|
||||||
|
|
||||||
|
3. A compile-time variant of the package. If you need openmpi to be
|
||||||
|
built in debug mode for your package to work, you can require it by
|
||||||
|
adding +debug to the openmpi spec when you depend on it. If you do
|
||||||
|
NOT want the debug option to be enabled, then replace this with -debug.
|
||||||
|
|
||||||
|
4. The name of the compiler to build with.
|
||||||
|
|
||||||
|
5. The versions of the compiler to build with. Note that the identifier
|
||||||
|
for a compiler version is the same '@' that is used for a package version.
|
||||||
|
A version list denoted by '@' is associated with the compiler only if
|
||||||
|
if it comes immediately after the compiler name. Otherwise it will be
|
||||||
|
associated with the current package spec.
|
||||||
|
"""
|
||||||
|
from functools import total_ordering
|
||||||
|
|
||||||
|
import spack.parse
|
||||||
|
from spack.version import Version, VersionRange
|
||||||
|
import spack.error
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateDependenceError(spack.error.SpackError):
|
||||||
|
"""Raised when the same dependence occurs in a spec twice."""
|
||||||
|
def __init__(self, message):
|
||||||
|
super(DuplicateDependenceError, self).__init__(message)
|
||||||
|
|
||||||
|
class DuplicateVariantError(spack.error.SpackError):
|
||||||
|
"""Raised when the same variant occurs in a spec twice."""
|
||||||
|
def __init__(self, message):
|
||||||
|
super(VariantVariantError, self).__init__(message)
|
||||||
|
|
||||||
|
class DuplicateCompilerError(spack.error.SpackError):
|
||||||
|
"""Raised when the same compiler occurs in a spec twice."""
|
||||||
|
def __init__(self, message):
|
||||||
|
super(DuplicateCompilerError, self).__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class Compiler(object):
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
self.versions = []
|
||||||
|
|
||||||
|
def add_version(self, version):
|
||||||
|
self.versions.append(version)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
out = "%%%s" % self.name
|
||||||
|
if self.versions:
|
||||||
|
vlist = ",".join(str(v) for v in sorted(self.versions))
|
||||||
|
out += "@%s" % vlist
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
class Spec(object):
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
self.versions = []
|
||||||
|
self.variants = {}
|
||||||
|
self.compiler = None
|
||||||
|
self.dependencies = {}
|
||||||
|
|
||||||
|
def add_version(self, version):
|
||||||
|
self.versions.append(version)
|
||||||
|
|
||||||
|
def add_variant(self, name, enabled):
|
||||||
|
self.variants[name] = enabled
|
||||||
|
|
||||||
|
def add_compiler(self, compiler):
|
||||||
|
self.compiler = compiler
|
||||||
|
|
||||||
|
def add_dependency(self, dep):
|
||||||
|
if dep.name in self.dependencies:
|
||||||
|
raise ValueError("Cannot depend on %s twice" % dep)
|
||||||
|
self.dependencies[dep.name] = dep
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
out = self.name
|
||||||
|
|
||||||
|
if self.versions:
|
||||||
|
vlist = ",".join(str(v) for v in sorted(self.versions))
|
||||||
|
out += "@%s" % vlist
|
||||||
|
|
||||||
|
if self.compiler:
|
||||||
|
out += str(self.compiler)
|
||||||
|
|
||||||
|
for name in sorted(self.variants.keys()):
|
||||||
|
enabled = self.variants[name]
|
||||||
|
if enabled:
|
||||||
|
out += '+%s' % name
|
||||||
|
else:
|
||||||
|
out += '~%s' % name
|
||||||
|
|
||||||
|
for name in sorted(self.dependencies.keys()):
|
||||||
|
out += " ^%s" % str(self.dependencies[name])
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
#
|
||||||
|
# These are possible token types in the spec grammar.
|
||||||
|
#
|
||||||
|
DEP, AT, COLON, COMMA, ON, OFF, PCT, ID = range(8)
|
||||||
|
|
||||||
|
class SpecLexer(spack.parse.Lexer):
|
||||||
|
"""Parses tokens that make up spack specs."""
|
||||||
|
def __init__(self):
|
||||||
|
super(SpecLexer, self).__init__([
|
||||||
|
(r'\^', lambda scanner, val: self.token(DEP, val)),
|
||||||
|
(r'\@', lambda scanner, val: self.token(AT, val)),
|
||||||
|
(r'\:', lambda scanner, val: self.token(COLON, val)),
|
||||||
|
(r'\,', lambda scanner, val: self.token(COMMA, val)),
|
||||||
|
(r'\+', lambda scanner, val: self.token(ON, val)),
|
||||||
|
(r'\-', lambda scanner, val: self.token(OFF, val)),
|
||||||
|
(r'\~', lambda scanner, val: self.token(OFF, val)),
|
||||||
|
(r'\%', lambda scanner, val: self.token(PCT, val)),
|
||||||
|
(r'\w[\w.-]*', lambda scanner, val: self.token(ID, val)),
|
||||||
|
(r'\s+', lambda scanner, val: None)])
|
||||||
|
|
||||||
|
class SpecParser(spack.parse.Parser):
|
||||||
|
def __init__(self):
|
||||||
|
super(SpecParser, self).__init__(SpecLexer())
|
||||||
|
|
||||||
|
def spec(self):
|
||||||
|
self.expect(ID)
|
||||||
|
self.check_identifier()
|
||||||
|
|
||||||
|
spec = Spec(self.token.value)
|
||||||
|
while self.next:
|
||||||
|
if self.accept(DEP):
|
||||||
|
dep = self.spec()
|
||||||
|
spec.add_dependency(dep)
|
||||||
|
|
||||||
|
elif self.accept(AT):
|
||||||
|
vlist = self.version_list()
|
||||||
|
for version in vlist:
|
||||||
|
spec.add_version(version)
|
||||||
|
|
||||||
|
elif self.accept(ON):
|
||||||
|
self.expect(ID)
|
||||||
|
self.check_identifier()
|
||||||
|
spec.add_variant(self.token.value, True)
|
||||||
|
|
||||||
|
elif self.accept(OFF):
|
||||||
|
self.expect(ID)
|
||||||
|
self.check_identifier()
|
||||||
|
spec.add_variant(self.token.value, False)
|
||||||
|
|
||||||
|
elif self.accept(PCT):
|
||||||
|
spec.add_compiler(self.compiler())
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.unexpected_token()
|
||||||
|
|
||||||
|
return spec
|
||||||
|
|
||||||
|
|
||||||
|
def version(self):
|
||||||
|
start = None
|
||||||
|
end = None
|
||||||
|
if self.accept(ID):
|
||||||
|
start = self.token.value
|
||||||
|
|
||||||
|
if self.accept(COLON):
|
||||||
|
if self.accept(ID):
|
||||||
|
end = self.token.value
|
||||||
|
else:
|
||||||
|
return Version(start)
|
||||||
|
|
||||||
|
if not start and not end:
|
||||||
|
raise ParseError("Lone colon: version range needs at least one version.")
|
||||||
|
else:
|
||||||
|
if start: start = Version(start)
|
||||||
|
if end: end = Version(end)
|
||||||
|
return VersionRange(start, end)
|
||||||
|
|
||||||
|
|
||||||
|
def version_list(self):
|
||||||
|
vlist = []
|
||||||
|
while True:
|
||||||
|
vlist.append(self.version())
|
||||||
|
if not self.accept(COMMA):
|
||||||
|
break
|
||||||
|
return vlist
|
||||||
|
|
||||||
|
|
||||||
|
def compiler(self):
|
||||||
|
self.expect(ID)
|
||||||
|
self.check_identifier()
|
||||||
|
compiler = Compiler(self.token.value)
|
||||||
|
if self.accept(AT):
|
||||||
|
vlist = self.version_list()
|
||||||
|
for version in vlist:
|
||||||
|
compiler.add_version(version)
|
||||||
|
return compiler
|
||||||
|
|
||||||
|
|
||||||
|
def check_identifier(self):
|
||||||
|
"""The only identifiers that can contain '.' are versions, but version
|
||||||
|
ids are context-sensitive so we have to check on a case-by-case
|
||||||
|
basis. Call this if we detect a version id where it shouldn't be.
|
||||||
|
"""
|
||||||
|
if '.' in self.token.value:
|
||||||
|
raise spack.parse.ParseError(
|
||||||
|
"Non-version identifier cannot contain '.'")
|
||||||
|
|
||||||
|
|
||||||
|
def do_parse(self):
|
||||||
|
specs = []
|
||||||
|
while self.next:
|
||||||
|
specs.append(self.spec())
|
||||||
|
return specs
|
140
lib/spack/spack/test/compare_versions.py
Normal file
140
lib/spack/spack/test/compare_versions.py
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
"""
|
||||||
|
These version tests were taken from the RPM source code.
|
||||||
|
We try to maintain compatibility with RPM's version semantics
|
||||||
|
where it makes sense.
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
from spack.version import *
|
||||||
|
|
||||||
|
|
||||||
|
class CompareVersionsTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def assert_ver_lt(self, a, b):
|
||||||
|
a, b = ver(a), ver(b)
|
||||||
|
self.assertTrue(a < b)
|
||||||
|
self.assertTrue(a <= b)
|
||||||
|
self.assertTrue(a != b)
|
||||||
|
self.assertFalse(a == b)
|
||||||
|
self.assertFalse(a > b)
|
||||||
|
self.assertFalse(a >= b)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_ver_gt(self, a, b):
|
||||||
|
a, b = ver(a), ver(b)
|
||||||
|
self.assertTrue(a > b)
|
||||||
|
self.assertTrue(a >= b)
|
||||||
|
self.assertTrue(a != b)
|
||||||
|
self.assertFalse(a == b)
|
||||||
|
self.assertFalse(a < b)
|
||||||
|
self.assertFalse(a <= b)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_ver_eq(self, a, b):
|
||||||
|
a, b = ver(a), ver(b)
|
||||||
|
self.assertFalse(a > b)
|
||||||
|
self.assertTrue(a >= b)
|
||||||
|
self.assertFalse(a != b)
|
||||||
|
self.assertTrue(a == b)
|
||||||
|
self.assertFalse(a < b)
|
||||||
|
self.assertTrue(a <= b)
|
||||||
|
|
||||||
|
|
||||||
|
def test_two_segments(self):
|
||||||
|
self.assert_ver_eq('1.0', '1.0')
|
||||||
|
self.assert_ver_lt('1.0', '2.0')
|
||||||
|
self.assert_ver_gt('2.0', '1.0')
|
||||||
|
|
||||||
|
|
||||||
|
def test_three_segments(self):
|
||||||
|
self.assert_ver_eq('2.0.1', '2.0.1')
|
||||||
|
self.assert_ver_lt('2.0', '2.0.1')
|
||||||
|
self.assert_ver_gt('2.0.1', '2.0')
|
||||||
|
|
||||||
|
def test_alpha(self):
|
||||||
|
# TODO: not sure whether I like this. 2.0.1a is *usually*
|
||||||
|
# TODO: less than 2.0.1, but special-casing it makes version
|
||||||
|
# TODO: comparison complicated. See version.py
|
||||||
|
self.assert_ver_eq('2.0.1a', '2.0.1a')
|
||||||
|
self.assert_ver_gt('2.0.1a', '2.0.1')
|
||||||
|
self.assert_ver_lt('2.0.1', '2.0.1a')
|
||||||
|
|
||||||
|
def test_patch(self):
|
||||||
|
self.assert_ver_eq('5.5p1', '5.5p1')
|
||||||
|
self.assert_ver_lt('5.5p1', '5.5p2')
|
||||||
|
self.assert_ver_gt('5.5p2', '5.5p1')
|
||||||
|
self.assert_ver_eq('5.5p10', '5.5p10')
|
||||||
|
self.assert_ver_lt('5.5p1', '5.5p10')
|
||||||
|
self.assert_ver_gt('5.5p10', '5.5p1')
|
||||||
|
|
||||||
|
def test_num_alpha_with_no_separator(self):
|
||||||
|
self.assert_ver_lt('10xyz', '10.1xyz')
|
||||||
|
self.assert_ver_gt('10.1xyz', '10xyz')
|
||||||
|
self.assert_ver_eq('xyz10', 'xyz10')
|
||||||
|
self.assert_ver_lt('xyz10', 'xyz10.1')
|
||||||
|
self.assert_ver_gt('xyz10.1', 'xyz10')
|
||||||
|
|
||||||
|
def test_alpha_with_dots(self):
|
||||||
|
self.assert_ver_eq('xyz.4', 'xyz.4')
|
||||||
|
self.assert_ver_lt('xyz.4', '8')
|
||||||
|
self.assert_ver_gt('8', 'xyz.4')
|
||||||
|
self.assert_ver_lt('xyz.4', '2')
|
||||||
|
self.assert_ver_gt('2', 'xyz.4')
|
||||||
|
|
||||||
|
def test_nums_and_patch(self):
|
||||||
|
self.assert_ver_lt('5.5p2', '5.6p1')
|
||||||
|
self.assert_ver_gt('5.6p1', '5.5p2')
|
||||||
|
self.assert_ver_lt('5.6p1', '6.5p1')
|
||||||
|
self.assert_ver_gt('6.5p1', '5.6p1')
|
||||||
|
|
||||||
|
def test_rc_versions(self):
|
||||||
|
self.assert_ver_gt('6.0.rc1', '6.0')
|
||||||
|
self.assert_ver_lt('6.0', '6.0.rc1')
|
||||||
|
|
||||||
|
def test_alpha_beta(self):
|
||||||
|
self.assert_ver_gt('10b2', '10a1')
|
||||||
|
self.assert_ver_lt('10a2', '10b2')
|
||||||
|
|
||||||
|
def test_double_alpha(self):
|
||||||
|
self.assert_ver_eq('1.0aa', '1.0aa')
|
||||||
|
self.assert_ver_lt('1.0a', '1.0aa')
|
||||||
|
self.assert_ver_gt('1.0aa', '1.0a')
|
||||||
|
|
||||||
|
def test_padded_numbers(self):
|
||||||
|
self.assert_ver_eq('10.0001', '10.0001')
|
||||||
|
self.assert_ver_eq('10.0001', '10.1')
|
||||||
|
self.assert_ver_eq('10.1', '10.0001')
|
||||||
|
self.assert_ver_lt('10.0001', '10.0039')
|
||||||
|
self.assert_ver_gt('10.0039', '10.0001')
|
||||||
|
|
||||||
|
def test_close_numbers(self):
|
||||||
|
self.assert_ver_lt('4.999.9', '5.0')
|
||||||
|
self.assert_ver_gt('5.0', '4.999.9')
|
||||||
|
|
||||||
|
def test_date_stamps(self):
|
||||||
|
self.assert_ver_eq('20101121', '20101121')
|
||||||
|
self.assert_ver_lt('20101121', '20101122')
|
||||||
|
self.assert_ver_gt('20101122', '20101121')
|
||||||
|
|
||||||
|
def test_underscores(self):
|
||||||
|
self.assert_ver_eq('2_0', '2_0')
|
||||||
|
self.assert_ver_eq('2.0', '2_0')
|
||||||
|
self.assert_ver_eq('2_0', '2.0')
|
||||||
|
|
||||||
|
def test_rpm_oddities(self):
|
||||||
|
self.assert_ver_eq('1b.fc17', '1b.fc17')
|
||||||
|
self.assert_ver_lt('1b.fc17', '1.fc17')
|
||||||
|
self.assert_ver_gt('1.fc17', '1b.fc17')
|
||||||
|
self.assert_ver_eq('1g.fc17', '1g.fc17')
|
||||||
|
self.assert_ver_gt('1g.fc17', '1.fc17')
|
||||||
|
self.assert_ver_lt('1.fc17', '1g.fc17')
|
||||||
|
|
||||||
|
|
||||||
|
# Stuff below here is not taken from RPM's tests.
|
||||||
|
def test_version_ranges(self):
|
||||||
|
self.assert_ver_lt('1.2:1.4', '1.6')
|
||||||
|
self.assert_ver_gt('1.6', '1.2:1.4')
|
||||||
|
self.assert_ver_eq('1.2:1.4', '1.2:1.4')
|
||||||
|
self.assertNotEqual(ver('1.2:1.4'), ver('1.2:1.6'))
|
||||||
|
|
||||||
|
self.assert_ver_lt('1.2:1.4', '1.5:1.6')
|
||||||
|
self.assert_ver_gt('1.5:1.6', '1.2:1.4')
|
126
lib/spack/spack/test/specs.py
Normal file
126
lib/spack/spack/test/specs.py
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import unittest
|
||||||
|
from spack.spec import *
|
||||||
|
from spack.parse import *
|
||||||
|
|
||||||
|
# Sample output for a complex lexing.
|
||||||
|
complex_lex = [Token(ID, 'mvapich_foo'),
|
||||||
|
Token(DEP),
|
||||||
|
Token(ID, '_openmpi'),
|
||||||
|
Token(AT),
|
||||||
|
Token(ID, '1.2'),
|
||||||
|
Token(COLON),
|
||||||
|
Token(ID, '1.4'),
|
||||||
|
Token(COMMA),
|
||||||
|
Token(ID, '1.6'),
|
||||||
|
Token(PCT),
|
||||||
|
Token(ID, 'intel'),
|
||||||
|
Token(AT),
|
||||||
|
Token(ID, '12.1'),
|
||||||
|
Token(COLON),
|
||||||
|
Token(ID, '12.6'),
|
||||||
|
Token(ON),
|
||||||
|
Token(ID, 'debug'),
|
||||||
|
Token(OFF),
|
||||||
|
Token(ID, 'qt_4'),
|
||||||
|
Token(DEP),
|
||||||
|
Token(ID, 'stackwalker'),
|
||||||
|
Token(AT),
|
||||||
|
Token(ID, '8.1_1e')]
|
||||||
|
|
||||||
|
|
||||||
|
class SpecTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.parser = SpecParser()
|
||||||
|
self.lexer = SpecLexer()
|
||||||
|
|
||||||
|
# ================================================================================
|
||||||
|
# Parse checks
|
||||||
|
# ================================================================================
|
||||||
|
def check_parse(self, expected, spec=None):
|
||||||
|
"""Assert that the provided spec is able to be parsed.
|
||||||
|
If this is called with one argument, it assumes that the string is
|
||||||
|
canonical (i.e., no spaces and ~ instead of - for variants) and that it
|
||||||
|
will convert back to the string it came from.
|
||||||
|
|
||||||
|
If this is called with two arguments, the first argument is the expected
|
||||||
|
canonical form and the second is a non-canonical input to be parsed.
|
||||||
|
"""
|
||||||
|
if spec == None:
|
||||||
|
spec = expected
|
||||||
|
output = self.parser.parse(spec)
|
||||||
|
self.assertEqual(len(output), 1)
|
||||||
|
self.assertEqual(str(output[0]), spec)
|
||||||
|
|
||||||
|
|
||||||
|
def check_lex(self, tokens, spec):
|
||||||
|
"""Check that the provided spec parses to the provided list of tokens."""
|
||||||
|
lex_output = self.lexer.lex(spec)
|
||||||
|
for tok, spec_tok in zip(tokens, lex_output):
|
||||||
|
if tok.type == ID:
|
||||||
|
self.assertEqual(tok, spec_tok)
|
||||||
|
else:
|
||||||
|
# Only check the type for non-identifiers.
|
||||||
|
self.assertEqual(tok.type, spec_tok.type)
|
||||||
|
|
||||||
|
# ================================================================================
|
||||||
|
# Parse checks
|
||||||
|
# ===============================================================================
|
||||||
|
def test_package_names(self):
|
||||||
|
self.check_parse("mvapich")
|
||||||
|
self.check_parse("mvapich_foo")
|
||||||
|
self.check_parse("_mvapich_foo")
|
||||||
|
|
||||||
|
def test_simple_dependence(self):
|
||||||
|
self.check_parse("openmpi ^hwloc")
|
||||||
|
self.check_parse("openmpi ^hwloc ^libunwind")
|
||||||
|
|
||||||
|
def test_dependencies_with_versions(self):
|
||||||
|
self.check_parse("openmpi ^hwloc@1.2e6")
|
||||||
|
self.check_parse("openmpi ^hwloc@1.2e6:")
|
||||||
|
self.check_parse("openmpi ^hwloc@:1.4b7-rc3")
|
||||||
|
self.check_parse("openmpi ^hwloc@1.2e6:1.4b7-rc3")
|
||||||
|
|
||||||
|
def test_full_specs(self):
|
||||||
|
self.check_parse("mvapich_foo ^_openmpi@1.2:1.4,1.6%intel@12.1+debug~qt_4 ^stackwalker@8.1_1e")
|
||||||
|
|
||||||
|
def test_canonicalize(self):
|
||||||
|
self.check_parse(
|
||||||
|
"mvapich_foo ^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug~qt_4 ^stackwalker@8.1_1e")
|
||||||
|
|
||||||
|
# ================================================================================
|
||||||
|
# Lex checks
|
||||||
|
# ================================================================================
|
||||||
|
def test_ambiguous(self):
|
||||||
|
# This first one is ambiguous because - can be in an identifier AND
|
||||||
|
# indicate disabling an option.
|
||||||
|
self.assertRaises(
|
||||||
|
AssertionError, self.check_lex, complex_lex,
|
||||||
|
"mvapich_foo^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug-qt_4^stackwalker@8.1_1e")
|
||||||
|
|
||||||
|
# The following lexes are non-ambiguous (add a space before -qt_4) and should all
|
||||||
|
# result in the tokens in complex_lex
|
||||||
|
def test_minimal_spaces(self):
|
||||||
|
self.check_lex(
|
||||||
|
complex_lex,
|
||||||
|
"mvapich_foo^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug -qt_4^stackwalker@8.1_1e")
|
||||||
|
self.check_lex(
|
||||||
|
complex_lex,
|
||||||
|
"mvapich_foo^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug~qt_4^stackwalker@8.1_1e")
|
||||||
|
|
||||||
|
def test_spaces_between_dependences(self):
|
||||||
|
self.check_lex(
|
||||||
|
complex_lex,
|
||||||
|
"mvapich_foo ^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug -qt_4 ^stackwalker @ 8.1_1e")
|
||||||
|
self.check_lex(
|
||||||
|
complex_lex,
|
||||||
|
"mvapich_foo ^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug~qt_4 ^stackwalker @ 8.1_1e")
|
||||||
|
|
||||||
|
def test_spaces_between_options(self):
|
||||||
|
self.check_lex(
|
||||||
|
complex_lex,
|
||||||
|
"mvapich_foo ^_openmpi @1.2:1.4,1.6 %intel @12.1:12.6 +debug -qt_4 ^stackwalker @8.1_1e")
|
||||||
|
|
||||||
|
def test_way_too_many_spaces(self):
|
||||||
|
self.check_lex(
|
||||||
|
complex_lex,
|
||||||
|
"mvapich_foo ^ _openmpi @ 1.2 : 1.4 , 1.6 % intel @ 12.1 : 12.6 + debug - qt_4 ^ stackwalker @ 8.1_1e")
|
|
@ -1,37 +1,38 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from functools import total_ordering
|
||||||
|
|
||||||
import utils
|
import utils
|
||||||
import spack.error as serr
|
import spack.error as serr
|
||||||
|
|
||||||
|
|
||||||
|
def int_if_int(string):
|
||||||
|
"""Convert a string to int if possible. Otherwise, return a string."""
|
||||||
|
try:
|
||||||
|
return int(string)
|
||||||
|
except:
|
||||||
|
return string
|
||||||
|
|
||||||
|
|
||||||
|
def ver(string):
|
||||||
|
"""Parses either a version or version range from a string."""
|
||||||
|
if ':' in string:
|
||||||
|
start, end = string.split(':')
|
||||||
|
return VersionRange(Version(start), Version(end))
|
||||||
|
else:
|
||||||
|
return Version(string)
|
||||||
|
|
||||||
|
|
||||||
|
@total_ordering
|
||||||
class Version(object):
|
class Version(object):
|
||||||
"""Class to represent versions"""
|
"""Class to represent versions"""
|
||||||
def __init__(self, version_string):
|
def __init__(self, version_string):
|
||||||
|
# preserve the original string
|
||||||
self.version_string = version_string
|
self.version_string = version_string
|
||||||
self.version = canonical(version_string)
|
|
||||||
|
|
||||||
def __cmp__(self, other):
|
# Split version into alphabetical and numeric segments
|
||||||
return cmp(self.version, other.version)
|
segments = re.findall(r'[a-zA-Z]+|[0-9]+', version_string)
|
||||||
|
self.version = tuple(int_if_int(seg) for seg in segments)
|
||||||
@property
|
|
||||||
def major(self):
|
|
||||||
return self.component(0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def minor(self):
|
|
||||||
return self.component(1)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def patch(self):
|
|
||||||
return self.component(2)
|
|
||||||
|
|
||||||
def component(self, i):
|
|
||||||
"""Returns the ith version component"""
|
|
||||||
if len(self.version) > i:
|
|
||||||
return self.version[i]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def up_to(self, index):
|
def up_to(self, index):
|
||||||
"""Return a version string up to the specified component, exclusive.
|
"""Return a version string up to the specified component, exclusive.
|
||||||
|
@ -48,16 +49,99 @@ def __repr__(self):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.version_string
|
return self.version_string
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
"""Version comparison is designed for consistency with the way RPM
|
||||||
|
does things. If you need more complicated versions in installed
|
||||||
|
packages, you should override your package's version string to
|
||||||
|
express it more sensibly.
|
||||||
|
"""
|
||||||
|
assert(other is not None)
|
||||||
|
|
||||||
def canonical(v):
|
# Let VersionRange do all the range-based comparison
|
||||||
"""Get a "canonical" version of a version string, as a tuple."""
|
if type(other) == VersionRange:
|
||||||
def intify(part):
|
return not other < self
|
||||||
try:
|
|
||||||
return int(part)
|
|
||||||
except:
|
|
||||||
return part
|
|
||||||
|
|
||||||
return tuple(intify(v) for v in re.split(r'[_.-]+', v))
|
# simple equality test first.
|
||||||
|
if self.version == other.version:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for a, b in zip(self.version, other.version):
|
||||||
|
if a == b:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# Numbers are always "newer" than letters. This is for
|
||||||
|
# consistency with RPM. See patch #60884 (and details)
|
||||||
|
# from bugzilla #50977 in the RPM project at rpm.org.
|
||||||
|
# Or look at rpmvercmp.c if you want to see how this is
|
||||||
|
# implemented there.
|
||||||
|
if type(a) != type(b):
|
||||||
|
return type(b) == int
|
||||||
|
else:
|
||||||
|
return a < b
|
||||||
|
|
||||||
|
# If the common prefix is equal, the one with more segments is bigger.
|
||||||
|
return len(self.version) < len(other.version)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
"""Implemented to match __lt__. See __lt__."""
|
||||||
|
if type(other) == VersionRange:
|
||||||
|
return False
|
||||||
|
return self.version == other.version
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not (self == other)
|
||||||
|
|
||||||
|
|
||||||
|
@total_ordering
|
||||||
|
class VersionRange(object):
|
||||||
|
def __init__(self, start, end=None):
|
||||||
|
if type(start) == str:
|
||||||
|
start = Version(start)
|
||||||
|
if type(end) == str:
|
||||||
|
end = Version(end)
|
||||||
|
|
||||||
|
self.start = start
|
||||||
|
self.end = end
|
||||||
|
if start and end and end < start:
|
||||||
|
raise ValueError("Invalid Version range: %s" % self)
|
||||||
|
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
if type(other) == Version:
|
||||||
|
return self.end and self.end < other
|
||||||
|
elif type(other) == VersionRange:
|
||||||
|
return self.end and other.start and self.end < other.start
|
||||||
|
else:
|
||||||
|
raise TypeError("Can't compare VersionRange to %s" % type(other))
|
||||||
|
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
if type(other) == Version:
|
||||||
|
return self.start and self.start > other
|
||||||
|
elif type(other) == VersionRange:
|
||||||
|
return self.start and other.end and self.start > other.end
|
||||||
|
else:
|
||||||
|
raise TypeError("Can't compare VersionRange to %s" % type(other))
|
||||||
|
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (type(other) == VersionRange
|
||||||
|
and self.start == other.start
|
||||||
|
and self.end == other.end)
|
||||||
|
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not (self == other)
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
out = ""
|
||||||
|
if self.start:
|
||||||
|
out += str(self.start)
|
||||||
|
out += ":"
|
||||||
|
if self.end:
|
||||||
|
out += str(self.end)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
class VersionParseError(serr.SpackError):
|
class VersionParseError(serr.SpackError):
|
||||||
|
@ -158,7 +242,7 @@ def parse_version_string_with_indices(spec):
|
||||||
|
|
||||||
|
|
||||||
def parse_version(spec):
|
def parse_version(spec):
|
||||||
"""Given a URL or archive name, extract a versino from it and return
|
"""Given a URL or archive name, extract a version from it and return
|
||||||
a version object.
|
a version object.
|
||||||
"""
|
"""
|
||||||
ver, start, end = parse_version_string_with_indices(spec)
|
ver, start, end = parse_version_string_with_indices(spec)
|
||||||
|
@ -175,7 +259,7 @@ def create_version_format(spec):
|
||||||
|
|
||||||
def replace_version(spec, new_version):
|
def replace_version(spec, new_version):
|
||||||
version = create_version_format(spec)
|
version = create_version_format(spec)
|
||||||
|
# TODO: finish this function.
|
||||||
|
|
||||||
def parse_name(spec, ver=None):
|
def parse_name(spec, ver=None):
|
||||||
if ver is None:
|
if ver is None:
|
||||||
|
|
Loading…
Reference in a new issue