summaryrefslogtreecommitdiff
path: root/lib/spack/spack/packages/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/spack/spack/packages/__init__.py')
-rw-r--r--lib/spack/spack/packages/__init__.py453
1 files changed, 177 insertions, 276 deletions
diff --git a/lib/spack/spack/packages/__init__.py b/lib/spack/spack/packages/__init__.py
index 5731fb0d99..beb8ecf6ab 100644
--- a/lib/spack/spack/packages/__init__.py
+++ b/lib/spack/spack/packages/__init__.py
@@ -28,14 +28,22 @@ import sys
import string
import inspect
import glob
+import imp
import llnl.util.tty as tty
from llnl.util.filesystem import join_path
-from llnl.util.lang import list_modules
+from llnl.util.lang import memoized
import spack
import spack.error
import spack.spec
+from spack.virtual import ProviderIndex
+
+# Name of module under which packages are imported
+_imported_packages_module = 'spack.packages'
+
+# Name of the package file inside a package directory
+_package_file_name = 'package.py'
# Valid package names can contain '-' but can't start with it.
valid_package_re = r'^\w[\w-]*$'
@@ -43,323 +51,216 @@ valid_package_re = r'^\w[\w-]*$'
# Don't allow consecutive [_-] in package names
invalid_package_re = r'[_-][_-]+'
-instances = {}
-
-
-def _autospec(function):
- """Decorator that automatically converts the argument of a single-arg
- function to a Spec."""
- def converter(arg):
- if not isinstance(arg, spack.spec.Spec):
- arg = spack.spec.Spec(arg)
- return function(arg)
- return converter
-
-
-class ProviderIndex(object):
- """This is a dict of dicts used for finding providers of particular
- virtual dependencies. The dict of dicts looks like:
-
- { vpkg name :
- { full vpkg spec : package providing spec } }
-
- Callers can use this to first find which packages provide a vpkg,
- then find a matching full spec. e.g., in this scenario:
-
- { 'mpi' :
- { mpi@:1.1 : mpich,
- mpi@:2.3 : mpich2@1.9: } }
-
- Calling providers_for(spec) will find specs that provide a
- matching implementation of MPI.
- """
- def __init__(self, specs, **kwargs):
- # TODO: come up with another name for this. This "restricts" values to
- # the verbatim impu specs (i.e., it doesn't pre-apply package's constraints, and
- # keeps things as broad as possible, so it's really the wrong name)
- self.restrict = kwargs.setdefault('restrict', False)
-
- self.providers = {}
-
- for spec in specs:
- if not isinstance(spec, spack.spec.Spec):
- spec = spack.spec.Spec(spec)
-
- if spec.virtual:
- continue
-
- self.update(spec)
-
-
- def update(self, spec):
- if type(spec) != spack.spec.Spec:
- spec = spack.spec.Spec(spec)
-
- assert(not spec.virtual)
-
- pkg = spec.package
- for provided_spec, provider_spec in pkg.provided.iteritems():
- if provider_spec.satisfies(spec, deps=False):
- provided_name = provided_spec.name
- if provided_name not in self.providers:
- self.providers[provided_name] = {}
-
- if self.restrict:
- self.providers[provided_name][provided_spec] = spec
-
- else:
- # Before putting the spec in the map, constrain it so that
- # it provides what was asked for.
- constrained = spec.copy()
- constrained.constrain(provider_spec)
- self.providers[provided_name][provided_spec] = constrained
-
-
- def providers_for(self, *vpkg_specs):
- """Gives specs of all packages that provide virtual packages
- with the supplied specs."""
- providers = set()
- for vspec in vpkg_specs:
- # Allow string names to be passed as input, as well as specs
- if type(vspec) == str:
- vspec = spack.spec.Spec(vspec)
-
- # Add all the providers that satisfy the vpkg spec.
- if vspec.name in self.providers:
- for provider_spec, spec in self.providers[vspec.name].items():
- if provider_spec.satisfies(vspec, deps=False):
- providers.add(spec)
-
- # Return providers in order
- return sorted(providers)
-
-
- # TODO: this is pretty darned nasty, and inefficient.
- def _cross_provider_maps(self, lmap, rmap):
- result = {}
- for lspec in lmap:
- for rspec in rmap:
- try:
- constrained = lspec.copy().constrain(rspec)
- if lmap[lspec].name != rmap[rspec].name:
- continue
- result[constrained] = lmap[lspec].copy().constrain(
- rmap[rspec], deps=False)
- except spack.spec.UnsatisfiableSpecError:
- continue
- return result
-
-
- def __contains__(self, name):
- """Whether a particular vpkg name is in the index."""
- return name in self.providers
-
-
- def satisfies(self, other):
- """Check that providers of virtual specs are compatible."""
- common = set(self.providers) & set(other.providers)
- if not common:
- return True
-
- result = {}
- for name in common:
- crossed = self._cross_provider_maps(self.providers[name],
- other.providers[name])
- if crossed:
- result[name] = crossed
-
- return bool(result)
-
-
-
-@_autospec
-def get(spec):
- if spec.virtual:
- raise UnknownPackageError(spec.name)
-
- if not spec in instances:
- package_class = get_class_for_package_name(spec.name)
- instances[spec.name] = package_class(spec)
-
- return instances[spec.name]
-
-
-@_autospec
-def get_installed(spec):
- return [s for s in installed_package_specs() if s.satisfies(spec)]
-
-
-@_autospec
-def providers_for(vpkg_spec):
- if not hasattr(providers_for, 'index'):
- providers_for.index = ProviderIndex(all_package_names())
-
- providers = providers_for.index.providers_for(vpkg_spec)
- if not providers:
- raise UnknownPackageError("No such virtual package: %s" % vpkg_spec)
- return providers
-
def valid_package_name(pkg_name):
+ """Return whether the pkg_name is valid for use in Spack."""
return (re.match(valid_package_re, pkg_name) and
not re.search(invalid_package_re, pkg_name))
def validate_package_name(pkg_name):
+ """Raise an exception if pkg_name is not valid."""
if not valid_package_name(pkg_name):
raise InvalidPackageNameError(pkg_name)
-def dirname_for_package_name(pkg_name):
- """Get the directory name for a particular package would use, even if it's a
- foo.py package and not a directory with a foo/__init__.py file."""
- return join_path(spack.packages_path, pkg_name)
+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_package_name(pkg_name)
+ class_name = pkg_name.replace('_', '-')
+ class_name = string.capwords(class_name, '-')
+ class_name = class_name.replace('-', '')
-def filename_for_package_name(pkg_name):
- """Get the filename for the module we should load for a particular package.
- The package can be either in a standalone .py file, or it can be in
- a directory with an __init__.py file.
+ # If a class starts with a number, prefix it with Number_ to make it a valid
+ # Python class name.
+ if re.match(r'^[0-9]', class_name):
+ class_name = "Num_%s" % class_name
- Package "foo" in standalone .py file:
- packages/foo.py
+ return class_name
- Package "foo" in directory:
- packages/foo/__init__.py
- The second form is used when there are files (like patches) that need
- to be stored along with the package.
+def _autospec(function):
+ """Decorator that automatically converts the argument of a single-arg
+ function to a Spec."""
+ def converter(self, spec_like):
+ if not isinstance(spec_like, spack.spec.Spec):
+ spec_like = spack.spec.Spec(spec_like)
+ return function(self, spec_like)
+ return converter
- If the package doesn't exist yet, this will just return the name
- of the standalone .py file.
- """
- validate_package_name(pkg_name)
- pkg_dir = dirname_for_package_name(pkg_name)
- if os.path.isdir(pkg_dir):
- init_file = join_path(pkg_dir, '__init__.py')
- return init_file
- else:
- pkg_file = "%s.py" % pkg_dir
- return pkg_file
+class PackageDB(object):
+ def __init__(self, root):
+ """Construct a new package database from a root directory."""
+ self.root = root
+ self.instances = {}
+ self.provider_index = None
-def installed_package_specs():
- return spack.install_layout.all_specs()
+ @_autospec
+ def get(self, spec):
+ if spec.virtual:
+ raise UnknownPackageError(spec.name)
+ if not spec in self.instances:
+ package_class = self.get_class_for_package_name(spec.name)
+ self.instances[spec.name] = package_class(spec)
-def all_package_names():
- """Generator function for all packages."""
- for module in list_modules(spack.packages_path):
- yield module
+ return self.instances[spec.name]
-def all_packages():
- for name in all_package_names():
- yield get(name)
+ @_autospec
+ def get_installed(self, spec):
+ return [s for s in self.installed_package_specs() if s.satisfies(spec)]
-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_package_name(pkg_name)
+ @_autospec
+ def providers_for(self, vpkg_spec):
+ if self.provider_index is None:
+ self.provider_index = ProviderIndex(self.all_package_names())
- class_name = pkg_name.replace('_', '-')
- class_name = string.capwords(class_name, '-')
- class_name = class_name.replace('-', '')
+ providers = self.provider_index.providers_for(vpkg_spec)
+ if not providers:
+ raise UnknownPackageError("No such virtual package: %s" % vpkg_spec)
+ return providers
- # If a class starts with a number, prefix it with Number_ to make it a valid
- # Python class name.
- if re.match(r'^[0-9]', class_name):
- class_name = "Num_%s" % class_name
- return class_name
+ def dirname_for_package_name(self, pkg_name):
+ """Get the directory name for a particular package. This is the
+ directory that contains its package.py file."""
+ return join_path(self.root, pkg_name)
-def exists(pkg_name):
- """Whether a package with the supplied name exists ."""
- return os.path.exists(filename_for_package_name(pkg_name))
+ def filename_for_package_name(self, pkg_name):
+ """Get the filename for the module we should load for a particular
+ package. Packages for a pacakge DB live in
+ ``$root/<package_name>/package.py``
+ This will return a proper package.py path even if the
+ package doesn't exist yet, so callers will need to ensure
+ the package exists before importing.
+ """
+ validate_package_name(pkg_name)
+ pkg_dir = self.dirname_for_package_name(pkg_name)
+ return join_path(pkg_dir, _package_file_name)
-def packages_module():
- # TODO: replace this with a proper package DB class, instead of this hackiness.
- packages_path = re.sub(spack.module_path + '\/+', 'spack.', spack.packages_path)
- packages_module = re.sub(r'/', '.', packages_path)
- return packages_module
+ def installed_package_specs(self):
+ """Read installed package names straight from the install directory
+ layout.
+ """
+ return spack.install_layout.all_specs()
-def get_class_for_package_name(pkg_name):
- file_name = filename_for_package_name(pkg_name)
- 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)
+ @memoized
+ def all_package_names(self):
+ """Generator function for all packages. This looks for
+ ``<pkg_name>/package.py`` files within the root direcotry"""
+ all_package_names = []
+ for pkg_name in os.listdir(self.root):
+ pkg_dir = join_path(self.root, pkg_name)
+ pkg_file = join_path(pkg_dir, _package_file_name)
+ if os.path.isfile(pkg_file):
+ all_package_names.append(pkg_name)
+ all_package_names.sort()
+ return all_package_names
- # Figure out pacakges module from spack.packages_path
- # This allows us to change the module path.
- if not re.match(r'%s' % spack.module_path, spack.packages_path):
- raise RuntimeError("Packages path is not a submodule of spack.")
- class_name = class_name_for_package_name(pkg_name)
- try:
- module_name = "%s.%s" % (packages_module(), pkg_name)
- module = __import__(module_name, fromlist=[class_name])
- except ImportError, e:
- tty.die("Error while importing %s.%s:\n%s" % (pkg_name, class_name, e.message))
+ def all_packages(self):
+ for name in self.all_package_names():
+ yield get(name)
- cls = getattr(module, class_name)
- if not inspect.isclass(cls):
- tty.die("%s.%s is not a class" % (pkg_name, class_name))
- return cls
+ def exists(self, pkg_name):
+ """Whether a package with the supplied name exists ."""
+ return os.path.exists(self.filename_for_package_name(pkg_name))
-def compute_dependents():
- """Reads in all package files and sets dependence information on
- Package objects in memory.
- """
- if not hasattr(compute_dependents, index):
- compute_dependents.index = {}
-
- for pkg in all_packages():
- if pkg._dependents is None:
- pkg._dependents = []
-
- for name, dep in pkg.dependencies.iteritems():
- dpkg = get(name)
- if dpkg._dependents is None:
- dpkg._dependents = []
- dpkg._dependents.append(pkg.name)
-
-
-def graph_dependencies(out=sys.stdout):
- """Print out a graph of all the dependencies between package.
- Graph is in dot format."""
- out.write('digraph G {\n')
- out.write(' label = "Spack Dependencies"\n')
- out.write(' labelloc = "b"\n')
- out.write(' rankdir = "LR"\n')
- out.write(' ranksep = "5"\n')
- out.write('\n')
-
- def quote(string):
- return '"%s"' % string
-
- deps = []
- for pkg in all_packages():
- out.write(' %-30s [label="%s"]\n' % (quote(pkg.name), pkg.name))
- for dep_name, dep in pkg.dependencies.iteritems():
- deps.append((pkg.name, dep_name))
- out.write('\n')
-
- for pair in deps:
- out.write(' "%s" -> "%s"\n' % pair)
- out.write('}\n')
+ @memoized
+ def get_class_for_package_name(self, pkg_name):
+ """Get an instance of the class for a particular package.
+
+ This method uses Python's ``imp`` package to load python
+ source from a Spack package's ``package.py`` file. A
+ normal python import would only load each package once, but
+ because we do this dynamically, the method needs to be
+ memoized to ensure there is only ONE package class
+ instance, per package, per database.
+ """
+ file_path = self.filename_for_package_name(pkg_name)
+
+ if os.path.exists(file_path):
+ if not os.path.isfile(file_path):
+ tty.die("Something's wrong. '%s' is not a file!" % file_path)
+ if not os.access(file_path, os.R_OK):
+ tty.die("Cannot read '%s'!" % file_path)
+ else:
+ raise UnknownPackageError(pkg_name)
+
+ # Figure out pacakges module based on self.root
+ if not re.match(r'%s' % spack.module_path, self.root):
+ raise RuntimeError("Packages path is not a submodule of spack.")
+
+ class_name = class_name_for_package_name(pkg_name)
+ try:
+ module_name = _imported_packages_module + '.' + pkg_name
+ module = imp.load_source(module_name, file_path)
+
+ except ImportError, e:
+ tty.die("Error while importing %s from %s:\n%s" % (
+ pkg_name, file_path, e.message))
+
+ cls = getattr(module, class_name)
+ if not inspect.isclass(cls):
+ tty.die("%s.%s is not a class" % (pkg_name, class_name))
+
+ return cls
+
+
+ def compute_dependents(self):
+ """Reads in all package files and sets dependence information on
+ Package objects in memory.
+ """
+ if not hasattr(compute_dependents, index):
+ compute_dependents.index = {}
+
+ for pkg in all_packages():
+ if pkg._dependents is None:
+ pkg._dependents = []
+
+ for name, dep in pkg.dependencies.iteritems():
+ dpkg = get(name)
+ if dpkg._dependents is None:
+ dpkg._dependents = []
+ dpkg._dependents.append(pkg.name)
+
+
+ def graph_dependencies(self, out=sys.stdout):
+ """Print out a graph of all the dependencies between package.
+ Graph is in dot format."""
+ out.write('digraph G {\n')
+ out.write(' label = "Spack Dependencies"\n')
+ out.write(' labelloc = "b"\n')
+ out.write(' rankdir = "LR"\n')
+ out.write(' ranksep = "5"\n')
+ out.write('\n')
+
+ def quote(string):
+ return '"%s"' % string
+
+ deps = []
+ for pkg in all_packages():
+ out.write(' %-30s [label="%s"]\n' % (quote(pkg.name), pkg.name))
+ for dep_name, dep in pkg.dependencies.iteritems():
+ deps.append((pkg.name, dep_name))
+ out.write('\n')
+
+ for pair in deps:
+ out.write(' "%s" -> "%s"\n' % pair)
+ out.write('}\n')
class InvalidPackageNameError(spack.error.SpackError):