Checkpoint commit: much-improved spec class.
Still organizing things.
This commit is contained in:
		| @@ -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) | ||||
| @@ -14,6 +15,7 @@ | ||||
| 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") | ||||
| @@ -21,6 +23,12 @@ | ||||
|  | ||||
| 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,9 +613,6 @@ 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) | ||||
| @@ -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. | ||||
|              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 Version(string) | ||||
|             return (a, VersionList([b])) | ||||
|     else: | ||||
|         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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Todd Gamblin
					Todd Gamblin