Checkpoint commit: much-improved spec class.
Still organizing things.
This commit is contained in:
parent
157737efbe
commit
618571b807
27 changed files with 1419 additions and 369 deletions
|
@ -19,6 +19,7 @@ sys.path.insert(0, SPACK_LIB_PATH)
|
|||
del SPACK_FILE, SPACK_PREFIX, SPACK_LIB_PATH
|
||||
import spack
|
||||
import spack.tty as tty
|
||||
from spack.error import SpackError
|
||||
|
||||
# Command parsing
|
||||
parser = argparse.ArgumentParser(
|
||||
|
@ -50,5 +51,12 @@ spack.debug = args.debug
|
|||
command = spack.cmd.get_command(args.command)
|
||||
try:
|
||||
command(parser, args)
|
||||
except SpackError, e:
|
||||
if spack.debug:
|
||||
# In debug mode, raise with a full stack trace.
|
||||
raise
|
||||
else:
|
||||
# Otherwise print a nice simple message.
|
||||
tty.die(e.message)
|
||||
except KeyboardInterrupt:
|
||||
tty.die("Got a keyboard interrupt from the user.")
|
||||
|
|
|
@ -14,7 +14,8 @@ def setup_parser(subparser):
|
|||
help="delete and re-expand the entire stage directory")
|
||||
subparser.add_argument('-d', "--dist", action="store_true", dest='dist',
|
||||
help="delete the downloaded archive.")
|
||||
subparser.add_argument('packages', nargs=argparse.REMAINDER, help="specs of packages to clean")
|
||||
subparser.add_argument('packages', nargs=argparse.REMAINDER,
|
||||
help="specs of packages to clean")
|
||||
|
||||
|
||||
def clean(parser, args):
|
||||
|
|
9
lib/spack/spack/cmd/compilers.py
Normal file
9
lib/spack/spack/cmd/compilers.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
import spack.compilers
|
||||
import spack.tty as tty
|
||||
from spack.colify import colify
|
||||
|
||||
description = "List available compilers"
|
||||
|
||||
def compilers(parser, args):
|
||||
tty.msg("Supported compilers")
|
||||
colify(spack.compilers.supported_compilers(), indent=4)
|
|
@ -57,13 +57,13 @@ def create(parser, args):
|
|||
|
||||
# make a stage and fetch the archive.
|
||||
try:
|
||||
stage = Stage("%s-%s" % (name, version), url)
|
||||
stage = Stage("spack-create/%s-%s" % (name, version), url)
|
||||
archive_file = stage.fetch()
|
||||
except spack.FailedDownloadException, e:
|
||||
tty.die(e.message)
|
||||
|
||||
md5 = spack.md5(archive_file)
|
||||
class_name = packages.class_for(name)
|
||||
class_name = packages.class_name_for_package_name(name)
|
||||
|
||||
# Write outa template for the file
|
||||
tty.msg("Editing %s." % path)
|
||||
|
|
|
@ -9,53 +9,15 @@
|
|||
import spack.url as url
|
||||
import spack.tty as tty
|
||||
|
||||
|
||||
description ="List spack packages"
|
||||
|
||||
def setup_parser(subparser):
|
||||
subparser.add_argument('-v', '--versions', metavar="PACKAGE", dest='version_package',
|
||||
help='List available versions of a package (experimental).')
|
||||
subparser.add_argument('-i', '--installed', action='store_true', dest='installed',
|
||||
help='List installed packages for each platform along with versions.')
|
||||
|
||||
|
||||
def list(parser, args):
|
||||
if args.installed:
|
||||
pkgs = packages.installed_packages()
|
||||
for sys_type in pkgs:
|
||||
print "%s:" % sys_type
|
||||
package_vers = []
|
||||
for pkg in pkgs[sys_type]:
|
||||
pv = [pkg.name + "@" + v for v in pkg.installed_versions]
|
||||
package_vers.extend(pv)
|
||||
colify(sorted(package_vers), indent=4)
|
||||
|
||||
elif args.version_package:
|
||||
pkg = packages.get(args.version_package)
|
||||
|
||||
# Run curl but grab the mime type from the http headers
|
||||
try:
|
||||
listing = spack.curl('-s', '-L', pkg.list_url, return_output=True)
|
||||
except CalledProcessError:
|
||||
tty.die("Fetching %s failed." % pkg.list_url,
|
||||
"'list -v' requires an internet connection.")
|
||||
|
||||
url_regex = os.path.basename(url.wildcard_version(pkg.url))
|
||||
strings = re.findall(url_regex, listing)
|
||||
|
||||
versions = []
|
||||
wildcard = pkg.version.wildcard()
|
||||
for s in strings:
|
||||
match = re.search(wildcard, s)
|
||||
if match:
|
||||
versions.append(ver(match.group(0)))
|
||||
|
||||
if not versions:
|
||||
tty.die("Found no versions for %s" % pkg.name,
|
||||
"Listing versions is experimental. You may need to add the list_url",
|
||||
"attribute to the package to tell Spack where to look for versions.")
|
||||
|
||||
colify(str(v) for v in reversed(sorted(set(versions))))
|
||||
|
||||
colify(str(pkg) for pkg in packages.installed_packages())
|
||||
else:
|
||||
colify(packages.all_package_names())
|
||||
|
|
17
lib/spack/spack/cmd/spec.py
Normal file
17
lib/spack/spack/cmd/spec.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
import argparse
|
||||
import spack.cmd
|
||||
|
||||
import spack.tty as tty
|
||||
import spack
|
||||
|
||||
description = "parse specs and print them out to the command line."
|
||||
|
||||
def setup_parser(subparser):
|
||||
subparser.add_argument('specs', nargs=argparse.REMAINDER, help="specs of packages")
|
||||
|
||||
def spec(parser, args):
|
||||
specs = spack.cmd.parse_specs(args.specs)
|
||||
for spec in specs:
|
||||
print spec.colorized()
|
||||
print " --> ", spec.concretized().colorized()
|
||||
print spec.concretized().concrete()
|
20
lib/spack/spack/cmd/versions.py
Normal file
20
lib/spack/spack/cmd/versions.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
import os
|
||||
import re
|
||||
from subprocess import CalledProcessError
|
||||
|
||||
import spack
|
||||
import spack.packages as packages
|
||||
import spack.url as url
|
||||
import spack.tty as tty
|
||||
from spack.colify import colify
|
||||
from spack.version import ver
|
||||
|
||||
description ="List available versions of a package"
|
||||
|
||||
def setup_parser(subparser):
|
||||
subparser.add_argument('package', metavar='PACKAGE', help='Package to list versions for')
|
||||
|
||||
|
||||
def versions(parser, args):
|
||||
pkg = packages.get(args.package)
|
||||
colify(reversed(pkg.available_versions))
|
|
@ -94,9 +94,10 @@ def colify(elts, **options):
|
|||
indent = options.get("indent", 0)
|
||||
padding = options.get("padding", 2)
|
||||
|
||||
# elts needs to be in an array so we can count the elements
|
||||
if not type(elts) == list:
|
||||
elts = list(elts)
|
||||
# elts needs to be an array of strings so we can count the elements
|
||||
elts = [str(elt) for elt in elts]
|
||||
if not elts:
|
||||
return
|
||||
|
||||
if not output.isatty():
|
||||
for elt in elts:
|
||||
|
|
|
@ -97,9 +97,11 @@ def __call__(self, match):
|
|||
elif m == '@.':
|
||||
return self.escape(0)
|
||||
elif m == '@' or (style and not color):
|
||||
raise ColorParseError("Incomplete color format: '%s'" % m)
|
||||
raise ColorParseError("Incomplete color format: '%s' in %s"
|
||||
% (m, match.string))
|
||||
elif color not in colors:
|
||||
raise ColorParseError("invalid color specifier: '%s'" % color)
|
||||
raise ColorParseError("invalid color specifier: '%s' in '%s'"
|
||||
% (color, match.string))
|
||||
|
||||
colored_text = ''
|
||||
if text:
|
||||
|
@ -141,6 +143,10 @@ def cprint(string, stream=sys.stdout, color=None):
|
|||
"""Same as cwrite, but writes a trailing newline to the stream."""
|
||||
cwrite(string + "\n", stream, color)
|
||||
|
||||
def cescape(string):
|
||||
"""Replace all @ with @@ in the string provided."""
|
||||
return str(string).replace('@', '@@')
|
||||
|
||||
|
||||
class ColorStream(object):
|
||||
def __init__(self, stream, color=None):
|
||||
|
|
16
lib/spack/spack/compilers/__init__.py
Normal file
16
lib/spack/spack/compilers/__init__.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
#
|
||||
# This needs to be expanded for full compiler support.
|
||||
#
|
||||
|
||||
import spack
|
||||
import spack.compilers.gcc
|
||||
from spack.utils import list_modules, memoized
|
||||
|
||||
|
||||
@memoized
|
||||
def supported_compilers():
|
||||
return [c for c in list_modules(spack.compilers_path)]
|
||||
|
||||
|
||||
def get_compiler():
|
||||
return Compiler('gcc', spack.compilers.gcc.get_version())
|
15
lib/spack/spack/compilers/gcc.py
Normal file
15
lib/spack/spack/compilers/gcc.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
#
|
||||
# This is a stub module. It should be expanded when we implement full
|
||||
# compiler support.
|
||||
#
|
||||
|
||||
import subprocess
|
||||
from spack.version import Version
|
||||
|
||||
cc = 'gcc'
|
||||
cxx = 'g++'
|
||||
fortran = 'gfortran'
|
||||
|
||||
def get_version():
|
||||
v = subprocess.check_output([cc, '-dumpversion'])
|
||||
return Version(v)
|
15
lib/spack/spack/compilers/intel.py
Normal file
15
lib/spack/spack/compilers/intel.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
#
|
||||
# This is a stub module. It should be expanded when we implement full
|
||||
# compiler support.
|
||||
#
|
||||
|
||||
import subprocess
|
||||
from spack.version import Version
|
||||
|
||||
cc = 'icc'
|
||||
cxx = 'icc'
|
||||
fortran = 'ifort'
|
||||
|
||||
def get_version():
|
||||
v = subprocess.check_output([cc, '-dumpversion'])
|
||||
return Version(v)
|
|
@ -1,20 +0,0 @@
|
|||
"""
|
||||
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):
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def package(self):
|
||||
return packages.get(self.name)
|
||||
|
||||
def __str__(self):
|
||||
return "<dep: %s>" % self.name
|
98
lib/spack/spack/directory_layout.py
Normal file
98
lib/spack/spack/directory_layout.py
Normal file
|
@ -0,0 +1,98 @@
|
|||
import exceptions
|
||||
import re
|
||||
import os
|
||||
|
||||
import spack.spec as spec
|
||||
from spack.utils import *
|
||||
from spack.error import SpackError
|
||||
|
||||
|
||||
class DirectoryLayout(object):
|
||||
"""A directory layout is used to associate unique paths with specs.
|
||||
Different installations are going to want differnet layouts for their
|
||||
install, and they can use this to customize the nesting structure of
|
||||
spack installs.
|
||||
"""
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
|
||||
|
||||
def all_specs(self):
|
||||
"""To be implemented by subclasses to traverse all specs for which there is
|
||||
a directory within the root.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def relative_path_for_spec(self, spec):
|
||||
"""Implemented by subclasses to return a relative path from the install
|
||||
root to a unique location for the provided spec."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def path_for_spec(self, spec):
|
||||
"""Return an absolute path from the root to a directory for the spec."""
|
||||
if not spec.concrete:
|
||||
raise ValueError("path_for_spec requires a concrete spec.")
|
||||
|
||||
path = self.relative_path_for_spec(spec)
|
||||
assert(not path.startswith(self.root))
|
||||
return os.path.join(self.root, path)
|
||||
|
||||
|
||||
def remove_path_for_spec(self, spec):
|
||||
"""Removes a prefix and any empty parent directories from the root."""
|
||||
path = self.path_for_spec(spec)
|
||||
assert(path.startswith(self.root))
|
||||
|
||||
if os.path.exists(path):
|
||||
shutil.rmtree(path, True)
|
||||
|
||||
path = os.path.dirname(path)
|
||||
while not os.listdir(path) and path != self.root:
|
||||
os.rmdir(path)
|
||||
path = os.path.dirname(path)
|
||||
|
||||
|
||||
def traverse_dirs_at_depth(root, depth, path_tuple=(), curdepth=0):
|
||||
"""For each directory at <depth> within <root>, return a tuple representing
|
||||
the ancestors of that directory.
|
||||
"""
|
||||
if curdepth == depth and curdepth != 0:
|
||||
yield path_tuple
|
||||
elif depth > curdepth:
|
||||
for filename in os.listdir(root):
|
||||
child = os.path.join(root, filename)
|
||||
if os.path.isdir(child):
|
||||
child_tuple = path_tuple + (filename,)
|
||||
for tup in traverse_dirs_at_depth(
|
||||
child, depth, child_tuple, curdepth+1):
|
||||
yield tup
|
||||
|
||||
|
||||
class DefaultDirectoryLayout(DirectoryLayout):
|
||||
def __init__(self, root):
|
||||
super(DefaultDirectoryLayout, self).__init__(root)
|
||||
|
||||
|
||||
def relative_path_for_spec(self, spec):
|
||||
if not spec.concrete:
|
||||
raise ValueError("relative_path_for_spec requires a concrete spec.")
|
||||
|
||||
return new_path(
|
||||
spec.architecture,
|
||||
spec.compiler,
|
||||
"%s@%s%s%s" % (spec.name,
|
||||
spec.version,
|
||||
spec.variants,
|
||||
spec.dependencies))
|
||||
|
||||
|
||||
def all_specs(self):
|
||||
if not os.path.isdir(self.root):
|
||||
return
|
||||
|
||||
for path in traverse_dirs_at_depth(self.root, 3):
|
||||
arch, compiler, last_dir = path
|
||||
spec_str = "%s%%%s=%s" % (last_dir, compiler, arch)
|
||||
yield spec.parse(spec_str)
|
|
@ -2,6 +2,7 @@
|
|||
from version import Version
|
||||
from utils import *
|
||||
import arch
|
||||
from directory_layout import DefaultDirectoryLayout
|
||||
|
||||
# This lives in $prefix/lib/spac/spack/__file__
|
||||
prefix = ancestor(__file__, 4)
|
||||
|
@ -10,16 +11,23 @@
|
|||
spack_file = new_path(prefix, "bin", "spack")
|
||||
|
||||
# spack directory hierarchy
|
||||
lib_path = new_path(prefix, "lib", "spack")
|
||||
env_path = new_path(lib_path, "env")
|
||||
module_path = new_path(lib_path, "spack")
|
||||
packages_path = new_path(module_path, "packages")
|
||||
test_path = new_path(module_path, "test")
|
||||
lib_path = new_path(prefix, "lib", "spack")
|
||||
env_path = new_path(lib_path, "env")
|
||||
module_path = new_path(lib_path, "spack")
|
||||
packages_path = new_path(module_path, "packages")
|
||||
compilers_path = new_path(module_path, "compilers")
|
||||
test_path = new_path(module_path, "test")
|
||||
|
||||
var_path = new_path(prefix, "var", "spack")
|
||||
stage_path = new_path(var_path, "stage")
|
||||
var_path = new_path(prefix, "var", "spack")
|
||||
stage_path = new_path(var_path, "stage")
|
||||
|
||||
install_path = new_path(prefix, "opt")
|
||||
install_path = new_path(prefix, "opt")
|
||||
|
||||
#
|
||||
# This controls how spack lays out install prefixes and
|
||||
# stage directories.
|
||||
#
|
||||
install_layout = DefaultDirectoryLayout(install_path)
|
||||
|
||||
# Version information
|
||||
spack_version = Version("0.2")
|
||||
|
|
|
@ -61,6 +61,7 @@ def __call__(self, package_self, *args, **kwargs):
|
|||
If none is found, call the default function that this was
|
||||
initialized with. If there is no default, raise an error.
|
||||
"""
|
||||
# TODO: make this work with specs.
|
||||
sys_type = package_self.sys_type
|
||||
function = self.function_map.get(sys_type, self.default)
|
||||
if function:
|
||||
|
|
60
lib/spack/spack/none_compare.py
Normal file
60
lib/spack/spack/none_compare.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
"""
|
||||
Functions for comparing values that may potentially be None.
|
||||
Functions prefixed with 'none_low_' treat None as less than all other values.
|
||||
Functions prefixed with 'none_high_' treat None as greater than all other values.
|
||||
"""
|
||||
|
||||
def none_low_lt(lhs, rhs):
|
||||
"""Less-than comparison. None is lower than any value."""
|
||||
return lhs != rhs and (lhs == None or (rhs != None and lhs < rhs))
|
||||
|
||||
|
||||
def none_low_le(lhs, rhs):
|
||||
"""Less-than-or-equal comparison. None is less than any value."""
|
||||
return lhs == rhs or none_low_lt(lhs, rhs)
|
||||
|
||||
|
||||
def none_low_gt(lhs, rhs):
|
||||
"""Greater-than comparison. None is less than any value."""
|
||||
return lhs != rhs and not none_low_lt(lhs, rhs)
|
||||
|
||||
|
||||
def none_low_ge(lhs, rhs):
|
||||
"""Greater-than-or-equal comparison. None is less than any value."""
|
||||
return lhs == rhs or none_low_gt(lhs, rhs)
|
||||
|
||||
|
||||
def none_low_min(lhs, rhs):
|
||||
"""Minimum function where None is less than any value."""
|
||||
if lhs == None or rhs == None:
|
||||
return None
|
||||
else:
|
||||
return min(lhs, rhs)
|
||||
|
||||
|
||||
def none_high_lt(lhs, rhs):
|
||||
"""Less-than comparison. None is greater than any value."""
|
||||
return lhs != rhs and (rhs == None or (lhs != None and lhs < rhs))
|
||||
|
||||
|
||||
def none_high_le(lhs, rhs):
|
||||
"""Less-than-or-equal comparison. None is greater than any value."""
|
||||
return lhs == rhs or none_high_lt(lhs, rhs)
|
||||
|
||||
|
||||
def none_high_gt(lhs, rhs):
|
||||
"""Greater-than comparison. None is greater than any value."""
|
||||
return lhs != rhs and not none_high_lt(lhs, rhs)
|
||||
|
||||
|
||||
def none_high_ge(lhs, rhs):
|
||||
"""Greater-than-or-equal comparison. None is greater than any value."""
|
||||
return lhs == rhs or none_high_gt(lhs, rhs)
|
||||
|
||||
|
||||
def none_high_max(lhs, rhs):
|
||||
"""Maximum function where None is greater than any value."""
|
||||
if lhs == None or rhs == None:
|
||||
return None
|
||||
else:
|
||||
return max(lhs, rhs)
|
|
@ -9,7 +9,6 @@
|
|||
rundown on spack and how it differs from homebrew, look at the
|
||||
README.
|
||||
"""
|
||||
import sys
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
|
@ -18,18 +17,18 @@
|
|||
import shutil
|
||||
|
||||
from spack import *
|
||||
import spack.spec
|
||||
import packages
|
||||
import tty
|
||||
import attr
|
||||
import validate
|
||||
import url
|
||||
import arch
|
||||
|
||||
|
||||
from spec import Compiler
|
||||
from version import Version
|
||||
from version import *
|
||||
from multi_function import platform
|
||||
from stage import Stage
|
||||
from dependency import *
|
||||
|
||||
|
||||
class Package(object):
|
||||
|
@ -106,6 +105,21 @@ def install(self, prefix):
|
|||
install() This function tells spack how to build and install the
|
||||
software it downloaded.
|
||||
|
||||
Optional Attributes
|
||||
---------------------
|
||||
You can also optionally add these attributes, if needed:
|
||||
list_url
|
||||
Webpage to scrape for available version strings. Default is the
|
||||
directory containing the tarball; use this if the default isn't
|
||||
correct so that invoking 'spack versions' will work for this
|
||||
package.
|
||||
|
||||
url_version(self, version)
|
||||
When spack downloads packages at particular versions, it just
|
||||
converts version to string with str(version). Override this if
|
||||
your package needs special version formatting in its URL. boost
|
||||
is an example of a package that needs this.
|
||||
|
||||
Creating Packages
|
||||
===================
|
||||
As a package creator, you can probably ignore most of the preceding
|
||||
|
@ -209,7 +223,7 @@ class SomePackage(Package):
|
|||
|
||||
A package's lifecycle over a run of Spack looks something like this:
|
||||
|
||||
packge p = new Package() # Done for you by spack
|
||||
p = Package() # Done for you by spack
|
||||
|
||||
p.do_fetch() # called by spack commands in spack/cmd.
|
||||
p.do_stage() # see spack.stage.Stage docs.
|
||||
|
@ -231,9 +245,15 @@ class SomePackage(Package):
|
|||
clean() (some of them do this), and others to provide custom behavior.
|
||||
"""
|
||||
|
||||
#
|
||||
# These variables are per-package metadata will be defined by subclasses.
|
||||
#
|
||||
"""By default a package has no dependencies."""
|
||||
dependencies = []
|
||||
|
||||
#
|
||||
# These are default values for instance variables.
|
||||
#
|
||||
"""By default we build in parallel. Subclasses can override this."""
|
||||
parallel = True
|
||||
|
||||
|
@ -243,19 +263,14 @@ class SomePackage(Package):
|
|||
"""Controls whether install and uninstall check deps before running."""
|
||||
ignore_dependencies = False
|
||||
|
||||
# TODO: multi-compiler support
|
||||
"""Default compiler for this package"""
|
||||
compiler = Compiler('gcc')
|
||||
|
||||
|
||||
def __init__(self, sys_type = arch.sys_type()):
|
||||
# Check for attributes that derived classes must set.
|
||||
def __init__(self, spec):
|
||||
# These attributes are required for all packages.
|
||||
attr.required(self, 'homepage')
|
||||
attr.required(self, 'url')
|
||||
attr.required(self, 'md5')
|
||||
|
||||
# Architecture for this package.
|
||||
self.sys_type = sys_type
|
||||
# this determines how the package should be built.
|
||||
self.spec = spec
|
||||
|
||||
# Name of package is the name of its module (the file that contains it)
|
||||
self.name = inspect.getmodulename(self.module.__file__)
|
||||
|
@ -277,16 +292,16 @@ def __init__(self, sys_type = arch.sys_type()):
|
|||
elif type(self.version) == string:
|
||||
self.version = Version(self.version)
|
||||
|
||||
# This adds a bunch of convenience commands to the package's module scope.
|
||||
self.add_commands_to_module()
|
||||
|
||||
# Empty at first; only compute dependents if necessary
|
||||
# Empty at first; only compute dependent packages if necessary
|
||||
self._dependents = None
|
||||
|
||||
# stage used to build this package.
|
||||
self.stage = Stage(self.stage_name, self.url)
|
||||
# This is set by scraping a web page.
|
||||
self._available_versions = None
|
||||
|
||||
# Set a default list URL (place to find lots of versions)
|
||||
# stage used to build this package.
|
||||
self.stage = Stage("%s-%s" % (self.name, self.version), self.url)
|
||||
|
||||
# Set a default list URL (place to find available versions)
|
||||
if not hasattr(self, 'list_url'):
|
||||
self.list_url = os.path.dirname(self.url)
|
||||
|
||||
|
@ -356,6 +371,24 @@ def dependents(self):
|
|||
return tuple(self._dependents)
|
||||
|
||||
|
||||
def sanity_check(self):
|
||||
"""Ensure that this package and its dependencies don't have conflicting
|
||||
requirements."""
|
||||
deps = sorted(self.all_dependencies, key=lambda d: d.name)
|
||||
|
||||
|
||||
|
||||
@property
|
||||
@memoized
|
||||
def all_dependencies(self):
|
||||
"""Set of all transitive dependencies of this package."""
|
||||
all_deps = set(self.dependencies)
|
||||
for dep in self.dependencies:
|
||||
dep_pkg = packages.get(dep.name)
|
||||
all_deps = all_deps.union(dep_pkg.all_dependencies)
|
||||
return all_deps
|
||||
|
||||
|
||||
@property
|
||||
def installed(self):
|
||||
return os.path.exists(self.prefix)
|
||||
|
@ -379,35 +412,10 @@ def all_dependents(self):
|
|||
return tuple(all_deps)
|
||||
|
||||
|
||||
@property
|
||||
def stage_name(self):
|
||||
return "%s-%s" % (self.name, self.version)
|
||||
|
||||
#
|
||||
# Below properties determine the path where this package is installed.
|
||||
#
|
||||
@property
|
||||
def platform_path(self):
|
||||
"""Directory for binaries for the current platform."""
|
||||
return new_path(install_path, self.sys_type)
|
||||
|
||||
|
||||
@property
|
||||
def package_path(self):
|
||||
"""Directory for different versions of this package. Lives just above prefix."""
|
||||
return new_path(self.platform_path, self.name)
|
||||
|
||||
|
||||
@property
|
||||
def installed_versions(self):
|
||||
return [ver for ver in os.listdir(self.package_path)
|
||||
if os.path.isdir(new_path(self.package_path, ver))]
|
||||
|
||||
|
||||
@property
|
||||
def prefix(self):
|
||||
"""Packages are installed in $spack_prefix/opt/<sys_type>/<name>/<version>"""
|
||||
return new_path(self.package_path, self.version)
|
||||
"""Get the prefix into which this package should be installed."""
|
||||
return spack.install_layout.path_for_spec(self.spec)
|
||||
|
||||
|
||||
def url_version(self, version):
|
||||
|
@ -417,24 +425,14 @@ def url_version(self, version):
|
|||
override this, e.g. for boost versions where you need to ensure that there
|
||||
are _'s in the download URL.
|
||||
"""
|
||||
return version.string
|
||||
return str(version)
|
||||
|
||||
|
||||
def remove_prefix(self):
|
||||
"""Removes the prefix for a package along with any empty parent directories."""
|
||||
if self.dirty:
|
||||
return
|
||||
|
||||
if os.path.exists(self.prefix):
|
||||
shutil.rmtree(self.prefix, True)
|
||||
|
||||
for dir in (self.package_path, self.platform_path):
|
||||
if not os.path.isdir(dir):
|
||||
continue
|
||||
if not os.listdir(dir):
|
||||
os.rmdir(dir)
|
||||
else:
|
||||
break
|
||||
spack.install_layout.remove_path_for_spec(self.spec)
|
||||
|
||||
|
||||
def do_fetch(self):
|
||||
|
@ -469,6 +467,9 @@ def do_install(self):
|
|||
"""This class should call this version of the install method.
|
||||
Package implementations should override install().
|
||||
"""
|
||||
if not self.spec.concrete:
|
||||
raise ValueError("Can only install concrete packages.")
|
||||
|
||||
if os.path.exists(self.prefix):
|
||||
tty.msg("%s is already installed." % self.name)
|
||||
tty.pkg(self.prefix)
|
||||
|
@ -480,6 +481,10 @@ def do_install(self):
|
|||
self.do_stage()
|
||||
self.setup_install_environment()
|
||||
|
||||
# Add convenience commands to the package's module scope to
|
||||
# make building easier.
|
||||
self.add_commands_to_module()
|
||||
|
||||
tty.msg("Building %s." % self.name)
|
||||
try:
|
||||
self.install(self.prefix)
|
||||
|
@ -599,6 +604,34 @@ def do_clean_dist(self):
|
|||
tty.msg("Successfully cleaned %s" % self.name)
|
||||
|
||||
|
||||
@property
|
||||
def available_versions(self):
|
||||
if not self._available_versions:
|
||||
self._available_versions = VersionList()
|
||||
try:
|
||||
# Run curl but grab the mime type from the http headers
|
||||
listing = spack.curl('-s', '-L', self.list_url, return_output=True)
|
||||
url_regex = os.path.basename(url.wildcard_version(self.url))
|
||||
strings = re.findall(url_regex, listing)
|
||||
wildcard = self.version.wildcard()
|
||||
for s in strings:
|
||||
match = re.search(wildcard, s)
|
||||
if match:
|
||||
self._available_versions.add(ver(match.group(0)))
|
||||
|
||||
except CalledProcessError:
|
||||
tty.warn("Fetching %s failed." % self.list_url,
|
||||
"Package.available_versions requires an internet connection.",
|
||||
"Version list may be incomplete.")
|
||||
|
||||
if not self._available_versions:
|
||||
tty.warn("Found no versions for %s" % self.name,
|
||||
"Packate.available_versions may require adding the list_url attribute",
|
||||
"to the package to tell Spack where to look for versions.")
|
||||
self._available_versions = [self.version]
|
||||
return self._available_versions
|
||||
|
||||
|
||||
class MakeExecutable(Executable):
|
||||
"""Special Executable for make so the user can specify parallel or
|
||||
not on a per-invocation basis. Using 'parallel' as a kwarg will
|
||||
|
|
|
@ -7,68 +7,51 @@
|
|||
|
||||
import spack
|
||||
import spack.error
|
||||
import spack.spec
|
||||
from spack.utils import *
|
||||
import spack.arch as arch
|
||||
|
||||
|
||||
# Valid package names -- can contain - but can't start with it.
|
||||
valid_package = r'^\w[\w-]*$'
|
||||
# Valid package names can contain '-' but can't start with it.
|
||||
valid_package_re = r'^\w[\w-]*$'
|
||||
|
||||
# Don't allow consecutive [_-] in package names
|
||||
invalid_package = r'[_-][_-]+'
|
||||
invalid_package_re = r'[_-][_-]+'
|
||||
|
||||
instances = {}
|
||||
|
||||
def get(spec):
|
||||
spec = spack.spec.make_spec(spec)
|
||||
if not spec in instances:
|
||||
package_class = get_class_for_package_name(spec.name)
|
||||
instances[spec] = package_class(spec)
|
||||
|
||||
def get(pkg, arch=arch.sys_type()):
|
||||
key = (pkg, arch)
|
||||
if not key in instances:
|
||||
package_class = get_class(pkg)
|
||||
instances[key] = package_class(arch)
|
||||
return instances[key]
|
||||
return instances[spec]
|
||||
|
||||
|
||||
class InvalidPackageNameError(spack.error.SpackError):
|
||||
"""Raised when we encounter a bad package name."""
|
||||
def __init__(self, name):
|
||||
super(InvalidPackageNameError, self).__init__(
|
||||
"Invalid package name: " + name)
|
||||
self.name = name
|
||||
def valid_package_name(pkg_name):
|
||||
return (re.match(valid_package_re, pkg_name) and
|
||||
not re.search(invalid_package_re, pkg_name))
|
||||
|
||||
|
||||
def valid_name(pkg):
|
||||
return re.match(valid_package, pkg) and not re.search(invalid_package, pkg)
|
||||
def validate_package_name(pkg_name):
|
||||
if not valid_package_name(pkg_name):
|
||||
raise InvalidPackageNameError(pkg_name)
|
||||
|
||||
|
||||
def validate_name(pkg):
|
||||
if not valid_name(pkg):
|
||||
raise InvalidPackageNameError(pkg)
|
||||
|
||||
|
||||
def filename_for(pkg):
|
||||
def filename_for_package_name(pkg_name):
|
||||
"""Get the filename where a package name should be stored."""
|
||||
validate_name(pkg)
|
||||
return new_path(spack.packages_path, "%s.py" % pkg)
|
||||
validate_package_name(pkg_name)
|
||||
return new_path(spack.packages_path, "%s.py" % pkg_name)
|
||||
|
||||
|
||||
def installed_packages(**kwargs):
|
||||
"""Returns a dict from systype strings to lists of Package objects."""
|
||||
pkgs = {}
|
||||
if not os.path.isdir(spack.install_path):
|
||||
return pkgs
|
||||
|
||||
for sys_type in os.listdir(spack.install_path):
|
||||
sys_type = sys_type
|
||||
sys_path = new_path(spack.install_path, sys_type)
|
||||
pkgs[sys_type] = [get(pkg) for pkg in os.listdir(sys_path)
|
||||
if os.path.isdir(new_path(sys_path, pkg))]
|
||||
return pkgs
|
||||
def installed_packages():
|
||||
return spack.install_layout.all_specs()
|
||||
|
||||
|
||||
def all_package_names():
|
||||
"""Generator function for all packages."""
|
||||
for mod in list_modules(spack.packages_path):
|
||||
yield mod
|
||||
for module in list_modules(spack.packages_path):
|
||||
yield module
|
||||
|
||||
|
||||
def all_packages():
|
||||
|
@ -76,12 +59,12 @@ def all_packages():
|
|||
yield get(name)
|
||||
|
||||
|
||||
def class_for(pkg):
|
||||
def class_name_for_package_name(pkg_name):
|
||||
"""Get a name for the class the package file should contain. Note that
|
||||
conflicts don't matter because the classes are in different modules.
|
||||
"""
|
||||
validate_name(pkg)
|
||||
class_name = string.capwords(pkg.replace('_', '-'), '-')
|
||||
validate_package_name(pkg_name)
|
||||
class_name = string.capwords(pkg_name.replace('_', '-'), '-')
|
||||
|
||||
# If a class starts with a number, prefix it with Number_ to make it a valid
|
||||
# Python class name.
|
||||
|
@ -91,25 +74,27 @@ def class_for(pkg):
|
|||
return class_name
|
||||
|
||||
|
||||
def get_class(pkg):
|
||||
file = filename_for(pkg)
|
||||
def get_class_for_package_name(pkg_name):
|
||||
file_name = filename_for_package_name(pkg_name)
|
||||
|
||||
if os.path.exists(file):
|
||||
if not os.path.isfile(file):
|
||||
tty.die("Something's wrong. '%s' is not a file!" % file)
|
||||
if not os.access(file, os.R_OK):
|
||||
tty.die("Cannot read '%s'!" % file)
|
||||
if os.path.exists(file_name):
|
||||
if not os.path.isfile(file_name):
|
||||
tty.die("Something's wrong. '%s' is not a file!" % file_name)
|
||||
if not os.access(file_name, os.R_OK):
|
||||
tty.die("Cannot read '%s'!" % file_name)
|
||||
else:
|
||||
raise UnknownPackageError(pkg_name)
|
||||
|
||||
class_name = pkg.capitalize()
|
||||
class_name = pkg_name.capitalize()
|
||||
try:
|
||||
module_name = "%s.%s" % (__name__, pkg)
|
||||
module_name = "%s.%s" % (__name__, pkg_name)
|
||||
module = __import__(module_name, fromlist=[class_name])
|
||||
except ImportError, e:
|
||||
tty.die("Error while importing %s.%s:\n%s" % (pkg, class_name, e.message))
|
||||
tty.die("Error while importing %s.%s:\n%s" % (pkg_name, class_name, e.message))
|
||||
|
||||
klass = getattr(module, class_name)
|
||||
if not inspect.isclass(klass):
|
||||
tty.die("%s.%s is not a class" % (pkg, class_name))
|
||||
tty.die("%s.%s is not a class" % (pkg_name, class_name))
|
||||
|
||||
return klass
|
||||
|
||||
|
@ -152,3 +137,19 @@ def quote(string):
|
|||
for pair in deps:
|
||||
out.write(' "%s" -> "%s"\n' % pair)
|
||||
out.write('}\n')
|
||||
|
||||
|
||||
|
||||
class InvalidPackageNameError(spack.error.SpackError):
|
||||
"""Raised when we encounter a bad package name."""
|
||||
def __init__(self, name):
|
||||
super(InvalidPackageNameError, self).__init__(
|
||||
"Invalid package name: " + name)
|
||||
self.name = name
|
||||
|
||||
|
||||
class UnknownPackageError(spack.error.SpackError):
|
||||
"""Raised when we encounter a package spack doesn't have."""
|
||||
def __init__(self, name):
|
||||
super(UnknownPackageError, self).__init__("Package %s not found." % name)
|
||||
self.name = name
|
||||
|
|
|
@ -45,18 +45,19 @@ class Mpileaks(Package):
|
|||
spack install mpileaks ^mpich
|
||||
"""
|
||||
import sys
|
||||
from dependency import Dependency
|
||||
import spack.spec
|
||||
|
||||
|
||||
def depends_on(*args):
|
||||
def depends_on(*specs):
|
||||
"""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))
|
||||
for string in specs:
|
||||
for spec in spack.spec.parse(string):
|
||||
dependencies.append(spec)
|
||||
|
||||
|
||||
def provides(*args):
|
||||
|
|
|
@ -68,101 +68,275 @@
|
|||
import tty
|
||||
import spack.parse
|
||||
import spack.error
|
||||
from spack.version import Version, VersionRange
|
||||
from spack.color import ColorStream
|
||||
import spack.compilers
|
||||
import spack.compilers.gcc
|
||||
import spack.packages as packages
|
||||
import spack.arch as arch
|
||||
from spack.version import *
|
||||
from spack.color import *
|
||||
|
||||
# Color formats for various parts of specs when using color output.
|
||||
compiler_fmt = '@g'
|
||||
version_fmt = '@c'
|
||||
architecture_fmt = '@m'
|
||||
variant_enabled_fmt = '@B'
|
||||
variant_disabled_fmt = '@r'
|
||||
"""This map determines the coloring of specs when using color output.
|
||||
We make the fields different colors to enhance readability.
|
||||
See spack.color for descriptions of the color codes.
|
||||
"""
|
||||
color_formats = {'%' : '@g', # compiler
|
||||
'@' : '@c', # version
|
||||
'=' : '@m', # architecture
|
||||
'+' : '@B', # enable variant
|
||||
'~' : '@r', # disable variant
|
||||
'^' : '@.'} # dependency
|
||||
|
||||
"""Regex used for splitting by spec field separators."""
|
||||
separators = '[%s]' % ''.join(color_formats.keys())
|
||||
|
||||
|
||||
class SpecError(spack.error.SpackError):
|
||||
"""Superclass for all errors that occur while constructing specs."""
|
||||
def __init__(self, message):
|
||||
super(SpecError, self).__init__(message)
|
||||
def colorize_spec(spec):
|
||||
"""Returns a spec colorized according to the colors specified in
|
||||
color_formats."""
|
||||
class insert_color:
|
||||
def __init__(self):
|
||||
self.last = None
|
||||
|
||||
class DuplicateDependencyError(SpecError):
|
||||
"""Raised when the same dependency occurs in a spec twice."""
|
||||
def __init__(self, message):
|
||||
super(DuplicateDependencyError, self).__init__(message)
|
||||
def __call__(self, match):
|
||||
# ignore compiler versions (color same as compiler)
|
||||
sep = match.group(0)
|
||||
if self.last == '%' and sep == '@':
|
||||
return cescape(sep)
|
||||
self.last = sep
|
||||
|
||||
class DuplicateVariantError(SpecError):
|
||||
"""Raised when the same variant occurs in a spec twice."""
|
||||
def __init__(self, message):
|
||||
super(DuplicateVariantError, self).__init__(message)
|
||||
return '%s%s' % (color_formats[sep], cescape(sep))
|
||||
|
||||
class DuplicateCompilerError(SpecError):
|
||||
"""Raised when the same compiler occurs in a spec twice."""
|
||||
def __init__(self, message):
|
||||
super(DuplicateCompilerError, self).__init__(message)
|
||||
|
||||
class DuplicateArchitectureError(SpecError):
|
||||
"""Raised when the same architecture occurs in a spec twice."""
|
||||
def __init__(self, message):
|
||||
super(DuplicateArchitectureError, self).__init__(message)
|
||||
return colorize(re.sub(separators, insert_color(), str(spec)) + '@.')
|
||||
|
||||
|
||||
class Compiler(object):
|
||||
def __init__(self, name):
|
||||
"""The Compiler field represents the compiler or range of compiler
|
||||
versions that a package should be built with. Compilers have a
|
||||
name and a version list.
|
||||
"""
|
||||
def __init__(self, name, version=None):
|
||||
if name not in spack.compilers.supported_compilers():
|
||||
raise UnknownCompilerError(name)
|
||||
|
||||
self.name = name
|
||||
self.versions = []
|
||||
self.versions = VersionList()
|
||||
if version:
|
||||
self.versions.add(version)
|
||||
|
||||
def add_version(self, version):
|
||||
self.versions.append(version)
|
||||
|
||||
def stringify(self, **kwargs):
|
||||
color = kwargs.get("color", False)
|
||||
def _add_version(self, version):
|
||||
self.versions.add(version)
|
||||
|
||||
out = StringIO()
|
||||
out.write("%s{%%%s}" % (compiler_fmt, self.name))
|
||||
|
||||
if self.versions:
|
||||
vlist = ",".join(str(v) for v in sorted(self.versions))
|
||||
out.write("%s{@%s}" % (compiler_fmt, vlist))
|
||||
return out.getvalue()
|
||||
@property
|
||||
def concrete(self):
|
||||
return self.versions.concrete
|
||||
|
||||
|
||||
def _concretize(self):
|
||||
"""If this spec could describe more than one version, variant, or build
|
||||
of a package, this will resolve it to be concrete.
|
||||
"""
|
||||
# TODO: support compilers other than GCC.
|
||||
if self.concrete:
|
||||
return
|
||||
gcc_version = spack.compilers.gcc.get_version()
|
||||
self.versions = VersionList([gcc_version])
|
||||
|
||||
|
||||
def concretized(self):
|
||||
clone = self.copy()
|
||||
clone._concretize()
|
||||
return clone
|
||||
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
if not self.concrete:
|
||||
raise SpecError("Spec is not concrete: " + str(self))
|
||||
return self.versions[0]
|
||||
|
||||
|
||||
def copy(self):
|
||||
clone = Compiler(self.name)
|
||||
clone.versions = self.versions.copy()
|
||||
return clone
|
||||
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.name, self.versions) == (other.name, other.versions)
|
||||
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.versions))
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.stringify()
|
||||
out = self.name
|
||||
if self.versions:
|
||||
vlist = ",".join(str(v) for v in sorted(self.versions))
|
||||
out += "@%s" % vlist
|
||||
return out
|
||||
|
||||
|
||||
@total_ordering
|
||||
class Variant(object):
|
||||
"""Variants are named, build-time options for a package. Names depend
|
||||
on the particular package being built, and each named variant can
|
||||
be enabled or disabled.
|
||||
"""
|
||||
def __init__(self, name, enabled):
|
||||
self.name = name
|
||||
self.enabled = enabled
|
||||
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == other.name and self.enabled == other.enabled
|
||||
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
return (self.name, self.enabled)
|
||||
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.tuple)
|
||||
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.tuple < other.tuple
|
||||
|
||||
|
||||
def __str__(self):
|
||||
out = '+' if self.enabled else '~'
|
||||
return out + self.name
|
||||
|
||||
|
||||
|
||||
@total_ordering
|
||||
class HashableMap(dict):
|
||||
"""This is a hashable, comparable dictionary. Hash is performed on
|
||||
a tuple of the values in the dictionary."""
|
||||
def __eq__(self, other):
|
||||
return (len(self) == len(other) and
|
||||
sorted(self.values()) == sorted(other.values()))
|
||||
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
|
||||
def __lt__(self, other):
|
||||
return tuple(sorted(self.values())) < tuple(sorted(other.values()))
|
||||
|
||||
|
||||
def __hash__(self):
|
||||
return hash(tuple(sorted(self.values())))
|
||||
|
||||
|
||||
def copy(self):
|
||||
"""Type-agnostic clone method. Preserves subclass type."""
|
||||
# Construct a new dict of my type
|
||||
T = type(self)
|
||||
clone = T()
|
||||
|
||||
# Copy everything from this dict into it.
|
||||
for key in self:
|
||||
clone[key] = self[key]
|
||||
return clone
|
||||
|
||||
|
||||
class VariantMap(HashableMap):
|
||||
def __str__(self):
|
||||
sorted_keys = sorted(self.keys())
|
||||
return ''.join(str(self[key]) for key in sorted_keys)
|
||||
|
||||
|
||||
class DependencyMap(HashableMap):
|
||||
"""Each spec has a DependencyMap containing specs for its dependencies.
|
||||
The DependencyMap is keyed by name. """
|
||||
@property
|
||||
def concrete(self):
|
||||
return all(d.concrete for d in self.values())
|
||||
|
||||
|
||||
def __str__(self):
|
||||
sorted_keys = sorted(self.keys())
|
||||
return ''.join(
|
||||
["^" + str(self[name]) for name in sorted_keys])
|
||||
|
||||
|
||||
@total_ordering
|
||||
class Spec(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self._package = None
|
||||
self.versions = []
|
||||
self.variants = {}
|
||||
self.versions = VersionList()
|
||||
self.variants = VariantMap()
|
||||
self.architecture = None
|
||||
self.compiler = None
|
||||
self.dependencies = {}
|
||||
self.dependencies = DependencyMap()
|
||||
|
||||
def add_version(self, version):
|
||||
self.versions.append(version)
|
||||
#
|
||||
# Private routines here are called by the parser when building a spec.
|
||||
#
|
||||
def _add_version(self, version):
|
||||
"""Called by the parser to add an allowable version."""
|
||||
self.versions.add(version)
|
||||
|
||||
def add_variant(self, name, enabled):
|
||||
|
||||
def _add_variant(self, name, enabled):
|
||||
"""Called by the parser to add a variant."""
|
||||
if name in self.variants: raise DuplicateVariantError(
|
||||
"Cannot specify variant '%s' twice" % name)
|
||||
self.variants[name] = enabled
|
||||
self.variants[name] = Variant(name, enabled)
|
||||
|
||||
def add_compiler(self, compiler):
|
||||
|
||||
def _set_compiler(self, compiler):
|
||||
"""Called by the parser to set the compiler."""
|
||||
if self.compiler: raise DuplicateCompilerError(
|
||||
"Spec for '%s' cannot have two compilers." % self.name)
|
||||
self.compiler = compiler
|
||||
|
||||
def add_architecture(self, architecture):
|
||||
|
||||
def _set_architecture(self, architecture):
|
||||
"""Called by the parser to set the architecture."""
|
||||
if self.architecture: raise DuplicateArchitectureError(
|
||||
"Spec for '%s' cannot have two architectures." % self.name)
|
||||
self.architecture = architecture
|
||||
|
||||
def add_dependency(self, dep):
|
||||
|
||||
def _add_dependency(self, dep):
|
||||
"""Called by the parser to add another spec as a dependency."""
|
||||
if dep.name in self.dependencies:
|
||||
raise DuplicateDependencyError("Cannot depend on '%s' twice" % dep)
|
||||
self.dependencies[dep.name] = dep
|
||||
|
||||
def canonicalize(self):
|
||||
"""Ensures that the spec is in canonical form.
|
||||
|
||||
@property
|
||||
def concrete(self):
|
||||
return (self.versions.concrete
|
||||
# TODO: support variants
|
||||
and self.architecture
|
||||
and self.compiler and self.compiler.concrete
|
||||
and self.dependencies.concrete)
|
||||
|
||||
|
||||
def _concretize(self):
|
||||
"""A spec is concrete if it describes one build of a package uniquely.
|
||||
This will ensure that this spec is concrete.
|
||||
|
||||
If this spec could describe more than one version, variant, or build
|
||||
of a package, this will resolve it to be concrete.
|
||||
|
||||
Ensures that the spec is in canonical form.
|
||||
|
||||
This means:
|
||||
1. All dependencies of this package and of its dependencies are
|
||||
|
@ -173,49 +347,164 @@ def canonicalize(self):
|
|||
that each package exists an that spec criteria don't violate package
|
||||
criteria.
|
||||
"""
|
||||
pass
|
||||
# TODO: modularize the process of selecting concrete versions.
|
||||
# There should be a set of user-configurable policies for these decisions.
|
||||
self.check_sanity()
|
||||
|
||||
@property
|
||||
def package(self):
|
||||
if self._package == None:
|
||||
self._package = packages.get(self.name)
|
||||
return self._package
|
||||
|
||||
def stringify(self, **kwargs):
|
||||
color = kwargs.get("color", False)
|
||||
|
||||
out = ColorStream(StringIO(), color)
|
||||
out.write("%s" % self.name)
|
||||
|
||||
if self.versions:
|
||||
vlist = ",".join(str(v) for v in sorted(self.versions))
|
||||
out.write("%s{@%s}" % (version_fmt, vlist))
|
||||
# take the system's architecture for starters
|
||||
if not self.architecture:
|
||||
self.architecture = arch.sys_type()
|
||||
|
||||
if self.compiler:
|
||||
out.write(self.compiler.stringify(color=color))
|
||||
self.compiler._concretize()
|
||||
|
||||
for name in sorted(self.variants.keys()):
|
||||
enabled = self.variants[name]
|
||||
if enabled:
|
||||
out.write('%s{+%s}' % (variant_enabled_fmt, name))
|
||||
else:
|
||||
out.write('%s{~%s}' % (variant_disabled_fmt, name))
|
||||
# TODO: handle variants.
|
||||
|
||||
if self.architecture:
|
||||
out.write("%s{=%s}" % (architecture_fmt, self.architecture))
|
||||
pkg = packages.get(self.name)
|
||||
|
||||
for name in sorted(self.dependencies.keys()):
|
||||
dep = " ^" + self.dependencies[name].stringify(color=color)
|
||||
out.write(dep, raw=True)
|
||||
# Take the highest version in a range
|
||||
if not self.versions.concrete:
|
||||
preferred = self.versions.highest() or pkg.version
|
||||
self.versions = VersionList([preferred])
|
||||
|
||||
return out.getvalue()
|
||||
# Ensure dependencies have right versions
|
||||
|
||||
|
||||
|
||||
def check_sanity(self):
|
||||
"""Check names of packages and dependency validity."""
|
||||
self.check_package_name_sanity()
|
||||
self.check_dependency_sanity()
|
||||
self.check_dependence_constraint_sanity()
|
||||
|
||||
|
||||
def check_package_name_sanity(self):
|
||||
"""Ensure that all packages mentioned in the spec exist."""
|
||||
packages.get(self.name)
|
||||
for dep in self.dependencies.values():
|
||||
packages.get(dep.name)
|
||||
|
||||
|
||||
def check_dependency_sanity(self):
|
||||
"""Ensure that dependencies specified on the spec are actual
|
||||
dependencies of the package it represents.
|
||||
"""
|
||||
pkg = packages.get(self.name)
|
||||
dep_names = set(dep.name for dep in pkg.all_dependencies)
|
||||
invalid_dependencies = [d.name for d in self.dependencies.values()
|
||||
if d.name not in dep_names]
|
||||
if invalid_dependencies:
|
||||
raise InvalidDependencyException(
|
||||
"The packages (%s) are not dependencies of %s" %
|
||||
(','.join(invalid_dependencies), self.name))
|
||||
|
||||
|
||||
def check_dependence_constraint_sanity(self):
|
||||
"""Ensure that package's dependencies have consistent constraints on
|
||||
their dependencies.
|
||||
"""
|
||||
pkg = packages.get(self.name)
|
||||
specs = {}
|
||||
for spec in pkg.all_dependencies:
|
||||
if not spec.name in specs:
|
||||
specs[spec.name] = spec
|
||||
continue
|
||||
|
||||
merged = specs[spec.name]
|
||||
|
||||
# Specs in deps can't be disjoint.
|
||||
if not spec.versions.overlaps(merged.versions):
|
||||
raise InvalidConstraintException(
|
||||
"One package %s, version constraint %s conflicts with %s"
|
||||
% (pkg.name, spec.versions, merged.versions))
|
||||
|
||||
|
||||
def merge(self, other):
|
||||
"""Considering these specs as constraints, attempt to merge.
|
||||
Raise an exception if specs are disjoint.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def concretized(self):
|
||||
clone = self.copy()
|
||||
clone._concretize()
|
||||
return clone
|
||||
|
||||
|
||||
def copy(self):
|
||||
clone = Spec(self.name)
|
||||
clone.versions = self.versions.copy()
|
||||
clone.variants = self.variants.copy()
|
||||
clone.architecture = self.architecture
|
||||
clone.compiler = None
|
||||
if self.compiler:
|
||||
clone.compiler = self.compiler.copy()
|
||||
clone.dependencies = self.dependencies.copy()
|
||||
return clone
|
||||
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
if not self.concrete:
|
||||
raise SpecError("Spec is not concrete: " + str(self))
|
||||
return self.versions[0]
|
||||
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
return (self.name, self.versions, self.variants,
|
||||
self.architecture, self.compiler, self.dependencies)
|
||||
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
return (self.name, self.versions, self.variants, self.architecture,
|
||||
self.compiler, self.dependencies)
|
||||
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.tuple == other.tuple
|
||||
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.tuple < other.tuple
|
||||
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.tuple)
|
||||
|
||||
|
||||
def colorized(self):
|
||||
return colorize_spec(self)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def write(self, stream=sys.stdout):
|
||||
isatty = stream.isatty()
|
||||
stream.write(self.stringify(color=isatty))
|
||||
|
||||
def __str__(self):
|
||||
return self.stringify()
|
||||
out = self.name
|
||||
|
||||
# If the version range is entirely open, omit it
|
||||
if self.versions and self.versions != VersionList([':']):
|
||||
out += "@%s" % self.versions
|
||||
|
||||
if self.compiler:
|
||||
out += "%%%s" % self.compiler
|
||||
|
||||
out += str(self.variants)
|
||||
|
||||
if self.architecture:
|
||||
out += "=%s" % self.architecture
|
||||
|
||||
out += str(self.dependencies)
|
||||
return out
|
||||
|
||||
|
||||
#
|
||||
# These are possible token types in the spec grammar.
|
||||
|
@ -254,7 +543,7 @@ def do_parse(self):
|
|||
if not specs:
|
||||
self.last_token_error("Dependency has no package")
|
||||
self.expect(ID)
|
||||
specs[-1].add_dependency(self.spec())
|
||||
specs[-1]._add_dependency(self.spec())
|
||||
|
||||
else:
|
||||
self.unexpected_token()
|
||||
|
@ -265,28 +554,34 @@ def do_parse(self):
|
|||
def spec(self):
|
||||
self.check_identifier()
|
||||
spec = Spec(self.token.value)
|
||||
added_version = False
|
||||
|
||||
while self.next:
|
||||
if self.accept(AT):
|
||||
vlist = self.version_list()
|
||||
for version in vlist:
|
||||
spec.add_version(version)
|
||||
spec._add_version(version)
|
||||
added_version = True
|
||||
|
||||
elif self.accept(ON):
|
||||
spec.add_variant(self.variant(), True)
|
||||
spec._add_variant(self.variant(), True)
|
||||
|
||||
elif self.accept(OFF):
|
||||
spec.add_variant(self.variant(), False)
|
||||
spec._add_variant(self.variant(), False)
|
||||
|
||||
elif self.accept(PCT):
|
||||
spec.add_compiler(self.compiler())
|
||||
spec._set_compiler(self.compiler())
|
||||
|
||||
elif self.accept(EQ):
|
||||
spec.add_architecture(self.architecture())
|
||||
spec._set_architecture(self.architecture())
|
||||
|
||||
else:
|
||||
break
|
||||
|
||||
# If there was no version in the spec, consier it an open range
|
||||
if not added_version:
|
||||
spec.versions = VersionList([':'])
|
||||
|
||||
return spec
|
||||
|
||||
|
||||
|
@ -318,12 +613,9 @@ def version(self):
|
|||
# No colon and no id: invalid version.
|
||||
self.next_token_error("Invalid version specifier")
|
||||
|
||||
if not start and not end:
|
||||
self.next_token_error("Lone colon: version range needs a version")
|
||||
else:
|
||||
if start: start = Version(start)
|
||||
if end: end = Version(end)
|
||||
return VersionRange(start, end)
|
||||
if start: start = Version(start)
|
||||
if end: end = Version(end)
|
||||
return VersionRange(start, end)
|
||||
|
||||
|
||||
def version_list(self):
|
||||
|
@ -341,7 +633,7 @@ def compiler(self):
|
|||
if self.accept(AT):
|
||||
vlist = self.version_list()
|
||||
for version in vlist:
|
||||
compiler.add_version(version)
|
||||
compiler._add_version(version)
|
||||
return compiler
|
||||
|
||||
|
||||
|
@ -357,3 +649,79 @@ def check_identifier(self):
|
|||
def parse(string):
|
||||
"""Returns a list of specs from an input string."""
|
||||
return SpecParser().parse(string)
|
||||
|
||||
|
||||
def parse_one(string):
|
||||
"""Parses a string containing only one spec, then returns that
|
||||
spec. If more than one spec is found, raises a ValueError.
|
||||
"""
|
||||
spec_list = parse(string)
|
||||
if len(spec_list) > 1:
|
||||
raise ValueError("string contains more than one spec!")
|
||||
elif len(spec_list) < 1:
|
||||
raise ValueError("string contains no specs!")
|
||||
return spec_list[0]
|
||||
|
||||
|
||||
def make_spec(spec_like):
|
||||
if type(spec_like) == str:
|
||||
specs = parse(spec_like)
|
||||
if len(specs) != 1:
|
||||
raise ValueError("String contains multiple specs: '%s'" % spec_like)
|
||||
return specs[0]
|
||||
|
||||
elif type(spec_like) == Spec:
|
||||
return spec_like
|
||||
|
||||
else:
|
||||
raise TypeError("Can't make spec out of %s" % type(spec_like))
|
||||
|
||||
|
||||
class SpecError(spack.error.SpackError):
|
||||
"""Superclass for all errors that occur while constructing specs."""
|
||||
def __init__(self, message):
|
||||
super(SpecError, self).__init__(message)
|
||||
|
||||
|
||||
class DuplicateDependencyError(SpecError):
|
||||
"""Raised when the same dependency occurs in a spec twice."""
|
||||
def __init__(self, message):
|
||||
super(DuplicateDependencyError, self).__init__(message)
|
||||
|
||||
|
||||
class DuplicateVariantError(SpecError):
|
||||
"""Raised when the same variant occurs in a spec twice."""
|
||||
def __init__(self, message):
|
||||
super(DuplicateVariantError, self).__init__(message)
|
||||
|
||||
|
||||
class DuplicateCompilerError(SpecError):
|
||||
"""Raised when the same compiler occurs in a spec twice."""
|
||||
def __init__(self, message):
|
||||
super(DuplicateCompilerError, self).__init__(message)
|
||||
|
||||
|
||||
class UnknownCompilerError(SpecError):
|
||||
"""Raised when the user asks for a compiler spack doesn't know about."""
|
||||
def __init__(self, compiler_name):
|
||||
super(UnknownCompilerError, self).__init__(
|
||||
"Unknown compiler: %s" % compiler_name)
|
||||
|
||||
|
||||
class DuplicateArchitectureError(SpecError):
|
||||
"""Raised when the same architecture occurs in a spec twice."""
|
||||
def __init__(self, message):
|
||||
super(DuplicateArchitectureError, self).__init__(message)
|
||||
|
||||
|
||||
class InvalidDependencyException(SpecError):
|
||||
"""Raised when a dependency in a spec is not actually a dependency
|
||||
of the package."""
|
||||
def __init__(self, message):
|
||||
super(InvalidDependencyException, self).__init__(message)
|
||||
|
||||
|
||||
class InvalidConstraintException(SpecError):
|
||||
"""Raised when a package dependencies conflict."""
|
||||
def __init__(self, message):
|
||||
super(InvalidConstraintException, self).__init__(message)
|
||||
|
|
|
@ -18,8 +18,8 @@ def __init__(self, url):
|
|||
|
||||
class Stage(object):
|
||||
"""A Stage object manaages a directory where an archive is downloaded,
|
||||
expanded, and built before being installed. A stage's lifecycle looks
|
||||
like this:
|
||||
expanded, and built before being installed. It also handles downloading
|
||||
the archive. A stage's lifecycle looks like this:
|
||||
|
||||
setup() Create the stage directory.
|
||||
fetch() Fetch a source archive into the stage.
|
||||
|
@ -32,21 +32,16 @@ class Stage(object):
|
|||
in a tmp directory. Otherwise, stages are created directly in
|
||||
spack.stage_path.
|
||||
"""
|
||||
|
||||
def __init__(self, stage_name, url):
|
||||
def __init__(self, path, url):
|
||||
"""Create a stage object.
|
||||
Parameters:
|
||||
stage_name Name of the stage directory that will be created.
|
||||
url URL of the archive to be downloaded into this stage.
|
||||
path Relative path from the stage root to where the stage will
|
||||
be created.
|
||||
url URL of the archive to be downloaded into this stage.
|
||||
"""
|
||||
self.stage_name = stage_name
|
||||
self.path = os.path.join(spack.stage_path, path)
|
||||
self.url = url
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
"""Absolute path to the stage directory."""
|
||||
return spack.new_path(spack.stage_path, self.stage_name)
|
||||
|
||||
|
||||
def setup(self):
|
||||
"""Creates the stage directory.
|
||||
|
@ -103,8 +98,7 @@ def setup(self):
|
|||
if username:
|
||||
tmp_dir = spack.new_path(tmp_dir, username)
|
||||
spack.mkdirp(tmp_dir)
|
||||
tmp_dir = tempfile.mkdtemp(
|
||||
'.stage', self.stage_name + '-', tmp_dir)
|
||||
tmp_dir = tempfile.mkdtemp('.stage', 'spack-stage-', tmp_dir)
|
||||
|
||||
os.symlink(tmp_dir, self.path)
|
||||
|
||||
|
|
13
lib/spack/spack/test/concretize.py
Normal file
13
lib/spack/spack/test/concretize.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
import unittest
|
||||
import spack.spec
|
||||
|
||||
|
||||
class ConcretizeTest(unittest.TestCase):
|
||||
|
||||
def check_concretize(self, abstract_spec):
|
||||
abstract = spack.spec.parse_one(abstract_spec)
|
||||
self.assertTrue(abstract.concretized().concrete)
|
||||
|
||||
|
||||
def test_packages(self):
|
||||
self.check_concretize("libelf")
|
|
@ -1,6 +1,7 @@
|
|||
import unittest
|
||||
import spack.spec
|
||||
from spack.spec import *
|
||||
from spack.parse import *
|
||||
from spack.parse import Token, ParseError
|
||||
|
||||
# Sample output for a complex lexing.
|
||||
complex_lex = [Token(ID, 'mvapich_foo'),
|
||||
|
@ -29,10 +30,6 @@
|
|||
|
||||
|
||||
class SpecTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.parser = SpecParser()
|
||||
self.lexer = SpecLexer()
|
||||
|
||||
# ================================================================================
|
||||
# Parse checks
|
||||
# ================================================================================
|
||||
|
@ -47,14 +44,14 @@ def check_parse(self, expected, spec=None):
|
|||
"""
|
||||
if spec == None:
|
||||
spec = expected
|
||||
output = self.parser.parse(spec)
|
||||
output = spack.spec.parse(spec)
|
||||
parsed = (" ".join(str(spec) for spec in output))
|
||||
self.assertEqual(expected, parsed)
|
||||
|
||||
|
||||
def check_lex(self, tokens, spec):
|
||||
"""Check that the provided spec parses to the provided list of tokens."""
|
||||
lex_output = self.lexer.lex(spec)
|
||||
lex_output = SpecLexer().lex(spec)
|
||||
for tok, spec_tok in zip(tokens, lex_output):
|
||||
if tok.type == ID:
|
||||
self.assertEqual(tok, spec_tok)
|
||||
|
@ -71,31 +68,33 @@ def test_package_names(self):
|
|||
self.check_parse("_mvapich_foo")
|
||||
|
||||
def test_simple_dependence(self):
|
||||
self.check_parse("openmpi ^hwloc")
|
||||
self.check_parse("openmpi ^hwloc ^libunwind")
|
||||
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")
|
||||
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")
|
||||
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",
|
||||
"mvapich_foo^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug~qt_4^stackwalker@8.1_1e",
|
||||
"mvapich_foo ^_openmpi@1.6,1.2:1.4%intel@12.1:12.6+debug~qt_4 ^stackwalker@8.1_1e")
|
||||
|
||||
self.check_parse(
|
||||
"mvapich_foo ^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug~qt_4 ^stackwalker@8.1_1e",
|
||||
"mvapich_foo^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug~qt_4^stackwalker@8.1_1e",
|
||||
"mvapich_foo ^stackwalker@8.1_1e ^_openmpi@1.6,1.2:1.4%intel@12.1:12.6~qt_4+debug")
|
||||
|
||||
self.check_parse(
|
||||
"x ^y@1,2:3,4%intel@1,2,3,4+a~b+c~d+e~f",
|
||||
"x^y@1,2:3,4%intel@1,2,3,4+a~b+c~d+e~f",
|
||||
"x ^y~f+e~d+c~b+a@4,2:3,1%intel@4,3,2,1")
|
||||
|
||||
self.check_parse("x^y", "x@: ^y@:")
|
||||
|
||||
def test_parse_errors(self):
|
||||
self.assertRaises(ParseError, self.check_parse, "x@@1.2")
|
||||
self.assertRaises(ParseError, self.check_parse, "x ^y@@1.2")
|
||||
|
@ -111,11 +110,11 @@ def test_duplicate_depdendence(self):
|
|||
|
||||
def test_duplicate_compiler(self):
|
||||
self.assertRaises(DuplicateCompilerError, self.check_parse, "x%intel%intel")
|
||||
self.assertRaises(DuplicateCompilerError, self.check_parse, "x%intel%gnu")
|
||||
self.assertRaises(DuplicateCompilerError, self.check_parse, "x%gnu%intel")
|
||||
self.assertRaises(DuplicateCompilerError, self.check_parse, "x%intel%gcc")
|
||||
self.assertRaises(DuplicateCompilerError, self.check_parse, "x%gcc%intel")
|
||||
self.assertRaises(DuplicateCompilerError, self.check_parse, "x ^y%intel%intel")
|
||||
self.assertRaises(DuplicateCompilerError, self.check_parse, "x ^y%intel%gnu")
|
||||
self.assertRaises(DuplicateCompilerError, self.check_parse, "x ^y%gnu%intel")
|
||||
self.assertRaises(DuplicateCompilerError, self.check_parse, "x ^y%intel%gcc")
|
||||
self.assertRaises(DuplicateCompilerError, self.check_parse, "x ^y%gcc%intel")
|
||||
|
||||
|
||||
# ================================================================================
|
||||
|
|
|
@ -39,6 +39,26 @@ def assert_ver_eq(self, a, b):
|
|||
self.assertTrue(a <= b)
|
||||
|
||||
|
||||
def assert_in(self, needle, haystack):
|
||||
self.assertTrue(ver(needle) in ver(haystack))
|
||||
|
||||
|
||||
def assert_not_in(self, needle, haystack):
|
||||
self.assertFalse(ver(needle) in ver(haystack))
|
||||
|
||||
|
||||
def assert_canonical(self, canonical_list, version_list):
|
||||
self.assertEqual(ver(canonical_list), ver(version_list))
|
||||
|
||||
|
||||
def assert_overlaps(self, v1, v2):
|
||||
self.assertTrue(ver(v1).overlaps(ver(v2)))
|
||||
|
||||
|
||||
def assert_no_overlap(self, v1, v2):
|
||||
self.assertFalse(ver(v1).overlaps(ver(v2)))
|
||||
|
||||
|
||||
def test_two_segments(self):
|
||||
self.assert_ver_eq('1.0', '1.0')
|
||||
self.assert_ver_lt('1.0', '2.0')
|
||||
|
@ -50,6 +70,7 @@ def test_three_segments(self):
|
|||
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
|
||||
|
@ -58,6 +79,7 @@ def test_alpha(self):
|
|||
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')
|
||||
|
@ -66,6 +88,7 @@ def test_patch(self):
|
|||
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')
|
||||
|
@ -73,6 +96,7 @@ def test_num_alpha_with_no_separator(self):
|
|||
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')
|
||||
|
@ -80,25 +104,30 @@ def test_alpha_with_dots(self):
|
|||
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')
|
||||
|
@ -106,20 +135,24 @@ def test_padded_numbers(self):
|
|||
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')
|
||||
|
@ -139,3 +172,89 @@ def test_version_ranges(self):
|
|||
|
||||
self.assert_ver_lt('1.2:1.4', '1.5:1.6')
|
||||
self.assert_ver_gt('1.5:1.6', '1.2:1.4')
|
||||
|
||||
|
||||
def test_contains(self):
|
||||
self.assert_in('1.3', '1.2:1.4')
|
||||
self.assert_in('1.2.5', '1.2:1.4')
|
||||
self.assert_in('1.3.5', '1.2:1.4')
|
||||
self.assert_in('1.3.5-7', '1.2:1.4')
|
||||
self.assert_not_in('1.1', '1.2:1.4')
|
||||
self.assert_not_in('1.5', '1.2:1.4')
|
||||
self.assert_not_in('1.4.2', '1.2:1.4')
|
||||
|
||||
self.assert_in('1.2.8', '1.2.7:1.4')
|
||||
self.assert_in('1.2.7:1.4', ':')
|
||||
self.assert_not_in('1.2.5', '1.2.7:1.4')
|
||||
self.assert_not_in('1.4.1', '1.2.7:1.4')
|
||||
|
||||
|
||||
def test_in_list(self):
|
||||
self.assert_in('1.2', ['1.5', '1.2', '1.3'])
|
||||
self.assert_in('1.2.5', ['1.5', '1.2:1.3'])
|
||||
self.assert_in('1.5', ['1.5', '1.2:1.3'])
|
||||
self.assert_not_in('1.4', ['1.5', '1.2:1.3'])
|
||||
|
||||
self.assert_in('1.2.5:1.2.7', [':'])
|
||||
self.assert_in('1.2.5:1.2.7', ['1.5', '1.2:1.3'])
|
||||
self.assert_not_in('1.2.5:1.5', ['1.5', '1.2:1.3'])
|
||||
self.assert_not_in('1.1:1.2.5', ['1.5', '1.2:1.3'])
|
||||
|
||||
|
||||
def test_ranges_overlap(self):
|
||||
self.assert_overlaps('1.2', '1.2')
|
||||
self.assert_overlaps('1.2.1', '1.2.1')
|
||||
self.assert_overlaps('1.2.1b', '1.2.1b')
|
||||
|
||||
self.assert_overlaps('1.2:1.7', '1.6:1.9')
|
||||
self.assert_overlaps(':1.7', '1.6:1.9')
|
||||
self.assert_overlaps(':1.7', ':1.9')
|
||||
self.assert_overlaps(':1.7', '1.6:')
|
||||
self.assert_overlaps('1.2:', '1.6:1.9')
|
||||
self.assert_overlaps('1.2:', ':1.9')
|
||||
self.assert_overlaps('1.2:', '1.6:')
|
||||
self.assert_overlaps(':', ':')
|
||||
self.assert_overlaps(':', '1.6:1.9')
|
||||
|
||||
|
||||
def test_lists_overlap(self):
|
||||
self.assert_overlaps('1.2b:1.7,5', '1.6:1.9,1')
|
||||
self.assert_overlaps('1,2,3,4,5', '3,4,5,6,7')
|
||||
self.assert_overlaps('1,2,3,4,5', '5,6,7')
|
||||
self.assert_overlaps('1,2,3,4,5', '5:7')
|
||||
self.assert_overlaps('1,2,3,4,5', '3, 6:7')
|
||||
self.assert_overlaps('1, 2, 4, 6.5', '3, 6:7')
|
||||
self.assert_overlaps('1, 2, 4, 6.5', ':, 5, 8')
|
||||
self.assert_overlaps('1, 2, 4, 6.5', ':')
|
||||
self.assert_no_overlap('1, 2, 4', '3, 6:7')
|
||||
self.assert_no_overlap('1,2,3,4,5', '6,7')
|
||||
self.assert_no_overlap('1,2,3,4,5', '6:7')
|
||||
|
||||
|
||||
def test_canonicalize_list(self):
|
||||
self.assert_canonical(['1.2', '1.3', '1.4'],
|
||||
['1.2', '1.3', '1.3', '1.4'])
|
||||
|
||||
self.assert_canonical(['1.2', '1.3:1.4'],
|
||||
['1.2', '1.3', '1.3:1.4'])
|
||||
|
||||
self.assert_canonical(['1.2', '1.3:1.4'],
|
||||
['1.2', '1.3:1.4', '1.4'])
|
||||
|
||||
self.assert_canonical(['1.3:1.4'],
|
||||
['1.3:1.4', '1.3', '1.3.1', '1.3.9', '1.4'])
|
||||
|
||||
self.assert_canonical(['1.3:1.4'],
|
||||
['1.3', '1.3.1', '1.3.9', '1.4', '1.3:1.4'])
|
||||
|
||||
self.assert_canonical(['1.3:1.5'],
|
||||
['1.3', '1.3.1', '1.3.9', '1.4:1.5', '1.3:1.4'])
|
||||
|
||||
self.assert_canonical(['1.3:1.5'],
|
||||
['1.3, 1.3.1,1.3.9,1.4:1.5,1.3:1.4'])
|
||||
|
||||
self.assert_canonical(['1.3:1.5'],
|
||||
['1.3, 1.3.1,1.3.9,1.4 : 1.5 , 1.3 : 1.4'])
|
||||
|
||||
self.assert_canonical([':'],
|
||||
[':,1.3, 1.3.1,1.3.9,1.4 : 1.5 , 1.3 : 1.4'])
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import sys
|
||||
import spack
|
||||
from spack.color import cprint
|
||||
from spack.color import *
|
||||
|
||||
indent = " "
|
||||
|
||||
def msg(message, *args):
|
||||
cprint("@*b{==>} @*w{%s}" % str(message))
|
||||
cprint("@*b{==>} @*w{%s}" % cescape(message))
|
||||
for arg in args:
|
||||
print indent + str(arg)
|
||||
|
||||
|
||||
def info(message, *args, **kwargs):
|
||||
format = kwargs.get('format', '*b')
|
||||
cprint("@%s{==>} %s" % (format, str(message)))
|
||||
cprint("@%s{==>} %s" % (format, cescape(message)))
|
||||
for arg in args:
|
||||
print indent + str(arg)
|
||||
|
||||
|
|
|
@ -1,29 +1,83 @@
|
|||
"""
|
||||
This file implements Version and version-ish objects. These are:
|
||||
|
||||
Version
|
||||
A single version of a package.
|
||||
VersionRange
|
||||
A range of versions of a package.
|
||||
VersionList
|
||||
A list of Versions and VersionRanges.
|
||||
|
||||
All of these types support the following operations, which can
|
||||
be called on any of the types:
|
||||
|
||||
__eq__, __ne__, __lt__, __gt__, __ge__, __le__, __hash__
|
||||
__contains__
|
||||
overlaps
|
||||
merge
|
||||
concrete
|
||||
True if the Version, VersionRange or VersionList represents
|
||||
a single version.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from bisect import bisect_left
|
||||
from functools import total_ordering
|
||||
|
||||
import utils
|
||||
from none_compare import *
|
||||
import spack.error
|
||||
|
||||
# Valid version characters
|
||||
VALID_VERSION = r'[A-Za-z0-9_.-]'
|
||||
|
||||
|
||||
def int_if_int(string):
|
||||
"""Convert a string to int if possible. Otherwise, return a string."""
|
||||
try:
|
||||
return int(string)
|
||||
except:
|
||||
except ValueError:
|
||||
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))
|
||||
def coerce_versions(a, b):
|
||||
"""Convert both a and b to the 'greatest' type between them, in this order:
|
||||
Version < VersionRange < VersionList
|
||||
This is used to simplify comparison operations below so that we're always
|
||||
comparing things that are of the same type.
|
||||
"""
|
||||
order = (Version, VersionRange, VersionList)
|
||||
ta, tb = type(a), type(b)
|
||||
|
||||
def check_type(t):
|
||||
if t not in order:
|
||||
raise TypeError("coerce_versions cannot be called on %s" % t)
|
||||
check_type(ta)
|
||||
check_type(tb)
|
||||
|
||||
if ta == tb:
|
||||
return (a, b)
|
||||
elif order.index(ta) > order.index(tb):
|
||||
if ta == VersionRange:
|
||||
return (a, VersionRange(b, b))
|
||||
else:
|
||||
return (a, VersionList([b]))
|
||||
else:
|
||||
return Version(string)
|
||||
if tb == VersionRange:
|
||||
return (VersionRange(a, a), b)
|
||||
else:
|
||||
return (VersionList([a]), b)
|
||||
|
||||
|
||||
def coerced(method):
|
||||
"""Decorator that ensures that argument types of a method are coerced."""
|
||||
def coercing_method(a, b):
|
||||
if type(a) == type(b) or a is None or b is None:
|
||||
return method(a, b)
|
||||
else:
|
||||
ca, cb = coerce_versions(a, b)
|
||||
return getattr(ca, method.__name__)(cb)
|
||||
return coercing_method
|
||||
|
||||
|
||||
@total_ordering
|
||||
|
@ -33,7 +87,8 @@ def __init__(self, string):
|
|||
if not re.match(VALID_VERSION, string):
|
||||
raise ValueError("Bad characters in version string: %s" % string)
|
||||
|
||||
# preserve the original string
|
||||
# preserve the original string, but trimmed.
|
||||
string = string.strip()
|
||||
self.string = string
|
||||
|
||||
# Split version into alphabetical and numeric segments
|
||||
|
@ -52,6 +107,15 @@ def up_to(self, index):
|
|||
"""
|
||||
return '.'.join(str(x) for x in self[:index])
|
||||
|
||||
|
||||
def lowest(self):
|
||||
return self
|
||||
|
||||
|
||||
def highest(self):
|
||||
return self
|
||||
|
||||
|
||||
def wildcard(self):
|
||||
"""Create a regex that will match variants of this version string."""
|
||||
def a_or_n(seg):
|
||||
|
@ -75,31 +139,39 @@ def a_or_n(seg):
|
|||
wc += ')?' * (len(seg_res) - 1)
|
||||
return wc
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
for v in self.version:
|
||||
yield v
|
||||
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return tuple(self.version[idx])
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return self.string
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.string
|
||||
|
||||
|
||||
@property
|
||||
def concrete(self):
|
||||
return self
|
||||
|
||||
@coerced
|
||||
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)
|
||||
|
||||
# Let VersionRange do all the range-based comparison
|
||||
if type(other) == VersionRange:
|
||||
return not other < self
|
||||
if other is None:
|
||||
return False
|
||||
|
||||
# Coerce if other is not a Version
|
||||
# simple equality test first.
|
||||
if self.version == other.version:
|
||||
return False
|
||||
|
@ -121,22 +193,42 @@ def __lt__(self, other):
|
|||
# If the common prefix is equal, the one with more segments is bigger.
|
||||
return len(self.version) < len(other.version)
|
||||
|
||||
|
||||
@coerced
|
||||
def __eq__(self, other):
|
||||
"""Implemented to match __lt__. See __lt__."""
|
||||
if type(other) != Version:
|
||||
return False
|
||||
return self.version == other.version
|
||||
return (other is not None and
|
||||
type(other) == Version and self.version == other.version)
|
||||
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.version)
|
||||
|
||||
|
||||
@coerced
|
||||
def __contains__(self, other):
|
||||
return self == other
|
||||
|
||||
|
||||
@coerced
|
||||
def overlaps(self, other):
|
||||
return self == other
|
||||
|
||||
|
||||
@coerced
|
||||
def merge(self, other):
|
||||
if self == other:
|
||||
return self
|
||||
else:
|
||||
return VersionList([self, other])
|
||||
|
||||
|
||||
@total_ordering
|
||||
class VersionRange(object):
|
||||
def __init__(self, start, end=None):
|
||||
def __init__(self, start, end):
|
||||
if type(start) == str:
|
||||
start = Version(start)
|
||||
if type(end) == str:
|
||||
|
@ -148,37 +240,74 @@ def __init__(self, start, end=None):
|
|||
raise ValueError("Invalid Version range: %s" % self)
|
||||
|
||||
|
||||
def lowest(self):
|
||||
return self.start
|
||||
|
||||
|
||||
def highest(self):
|
||||
return self.end
|
||||
|
||||
|
||||
@coerced
|
||||
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))
|
||||
"""Sort VersionRanges lexicographically so that they are ordered first
|
||||
by start and then by end. None denotes an open range, so None in
|
||||
the start position is less than everything except None, and None in
|
||||
the end position is greater than everything but None.
|
||||
"""
|
||||
if other is None:
|
||||
return False
|
||||
|
||||
return (none_low_lt(self.start, other.start) or
|
||||
(self.start == other.start and
|
||||
none_high_lt(self.end, other.end)))
|
||||
|
||||
|
||||
@coerced
|
||||
def __eq__(self, other):
|
||||
return (type(other) == VersionRange
|
||||
and self.start == other.start
|
||||
and self.end == other.end)
|
||||
return (other is not None and
|
||||
type(other) == VersionRange and
|
||||
self.start == other.start and self.end == other.end)
|
||||
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
|
||||
@property
|
||||
def concrete(self):
|
||||
return self.start if self.start == self.end else None
|
||||
|
||||
|
||||
@coerced
|
||||
def __contains__(self, other):
|
||||
return (none_low_ge(other.start, self.start) and
|
||||
none_high_le(other.end, self.end))
|
||||
|
||||
|
||||
@coerced
|
||||
def overlaps(self, other):
|
||||
return (other in self or self in other or
|
||||
((self.start == None or other.end == None or
|
||||
self.start <= other.end) and
|
||||
(other.start == None or self.end == None or
|
||||
other.start <= self.end)))
|
||||
|
||||
|
||||
@coerced
|
||||
def merge(self, other):
|
||||
return VersionRange(none_low_min(self.start, other.start),
|
||||
none_high_max(self.end, other.end))
|
||||
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.start, self.end))
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
def __str__(self):
|
||||
out = ""
|
||||
if self.start:
|
||||
|
@ -187,3 +316,179 @@ def __str__(self):
|
|||
if self.end:
|
||||
out += str(self.end)
|
||||
return out
|
||||
|
||||
|
||||
@total_ordering
|
||||
class VersionList(object):
|
||||
"""Sorted, non-redundant list of Versions and VersionRanges."""
|
||||
def __init__(self, vlist=None):
|
||||
self.versions = []
|
||||
if vlist != None:
|
||||
vlist = list(vlist)
|
||||
for v in vlist:
|
||||
self.add(ver(v))
|
||||
|
||||
|
||||
def add(self, version):
|
||||
if type(version) in (Version, VersionRange):
|
||||
# This normalizes single-value version ranges.
|
||||
if version.concrete:
|
||||
version = version.concrete
|
||||
|
||||
i = bisect_left(self, version)
|
||||
|
||||
while i-1 >= 0 and version.overlaps(self[i-1]):
|
||||
version = version.merge(self[i-1])
|
||||
del self.versions[i-1]
|
||||
i -= 1
|
||||
|
||||
while i < len(self) and version.overlaps(self[i]):
|
||||
version = version.merge(self[i])
|
||||
del self.versions[i]
|
||||
|
||||
self.versions.insert(i, version)
|
||||
|
||||
elif type(version) == VersionList:
|
||||
for v in version:
|
||||
self.add(v)
|
||||
|
||||
else:
|
||||
raise TypeError("Can't add %s to VersionList" % type(version))
|
||||
|
||||
|
||||
@property
|
||||
def concrete(self):
|
||||
if len(self) == 1:
|
||||
return self[0].concrete
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def copy(self):
|
||||
return VersionList(self)
|
||||
|
||||
|
||||
def lowest(self):
|
||||
"""Get the lowest version in the list."""
|
||||
if not self:
|
||||
return None
|
||||
else:
|
||||
return self[0].lowest()
|
||||
|
||||
|
||||
def highest(self):
|
||||
"""Get the highest version in the list."""
|
||||
if not self:
|
||||
return None
|
||||
else:
|
||||
return self[-1].highest()
|
||||
|
||||
|
||||
@coerced
|
||||
def overlaps(self, other):
|
||||
if not other or not self:
|
||||
return False
|
||||
|
||||
i = o = 0
|
||||
while i < len(self) and o < len(other):
|
||||
if self[i].overlaps(other[o]):
|
||||
return True
|
||||
elif self[i] < other[o]:
|
||||
i += 1
|
||||
else:
|
||||
o += 1
|
||||
return False
|
||||
|
||||
|
||||
@coerced
|
||||
def merge(self, other):
|
||||
return VersionList(self.versions + other.versions)
|
||||
|
||||
|
||||
@coerced
|
||||
def __contains__(self, other):
|
||||
if len(self) == 0:
|
||||
return False
|
||||
|
||||
for version in other:
|
||||
i = bisect_left(self, other)
|
||||
if i == 0:
|
||||
if version not in self[0]:
|
||||
return False
|
||||
elif all(version not in v for v in self[i-1:]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.versions[index]
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
for v in self.versions:
|
||||
yield v
|
||||
|
||||
|
||||
def __len__(self):
|
||||
return len(self.versions)
|
||||
|
||||
|
||||
@coerced
|
||||
def __eq__(self, other):
|
||||
return other is not None and self.versions == other.versions
|
||||
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
|
||||
@coerced
|
||||
def __lt__(self, other):
|
||||
return other is not None and self.versions < other.versions
|
||||
|
||||
|
||||
def __hash__(self):
|
||||
return hash(tuple(self.versions))
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return ",".join(str(v) for v in self.versions)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.versions)
|
||||
|
||||
|
||||
def _string_to_version(string):
|
||||
"""Converts a string to a Version, VersionList, or VersionRange.
|
||||
This is private. Client code should use ver().
|
||||
"""
|
||||
string = string.replace(' ','')
|
||||
|
||||
if ',' in string:
|
||||
return VersionList(string.split(','))
|
||||
|
||||
elif ':' in string:
|
||||
s, e = string.split(':')
|
||||
start = Version(s) if s else None
|
||||
end = Version(e) if e else None
|
||||
return VersionRange(start, end)
|
||||
|
||||
else:
|
||||
return Version(string)
|
||||
|
||||
|
||||
def ver(obj):
|
||||
"""Parses a Version, VersionRange, or VersionList from a string
|
||||
or list of strings.
|
||||
"""
|
||||
t = type(obj)
|
||||
if t == list:
|
||||
return VersionList(obj)
|
||||
elif t == str:
|
||||
return _string_to_version(obj)
|
||||
elif t in (Version, VersionRange, VersionList):
|
||||
return obj
|
||||
else:
|
||||
raise TypeError("ver() can't convert %s to version!" % t)
|
||||
|
|
Loading…
Reference in a new issue