summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Josef Scheibel <scheibel1@llnl.gov>2018-05-18 17:53:58 -0700
committerTodd Gamblin <tgamblin@llnl.gov>2018-11-09 00:31:24 -0800
commit31cb2041c334f852b945b421fffb5ffcacc89e1e (patch)
treef53125cfb8725288b98213a9e2a8e90fc2c56921
parent4b2f51d063111d0966404ff7f1eea627f2d286d8 (diff)
downloadspack-31cb2041c334f852b945b421fffb5ffcacc89e1e.tar.gz
spack-31cb2041c334f852b945b421fffb5ffcacc89e1e.tar.bz2
spack-31cb2041c334f852b945b421fffb5ffcacc89e1e.tar.xz
spack-31cb2041c334f852b945b421fffb5ffcacc89e1e.zip
env: add spack env command, along with env.yaml schema and tests
Co-authored-by: Elizabeth Fischer <rpf2116@columbia.edu>
-rw-r--r--.gitignore1
-rw-r--r--lib/spack/spack/cmd/env.py795
-rw-r--r--lib/spack/spack/schema/env.py43
-rw-r--r--lib/spack/spack/test/cmd/env.py149
4 files changed, 988 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index f42ebd911e..ee71398e44 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
/db
/var/spack/stage
/var/spack/cache
+/var/spack/environments
/var/spack/repos/*/index.yaml
/var/spack/repos/*/lock
*.pyc
diff --git a/lib/spack/spack/cmd/env.py b/lib/spack/spack/cmd/env.py
new file mode 100644
index 0000000000..bbc98044ef
--- /dev/null
+++ b/lib/spack/spack/cmd/env.py
@@ -0,0 +1,795 @@
+# Copyright 2013-2018 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+import llnl.util.tty as tty
+import spack
+import llnl.util.filesystem as fs
+import spack.modules
+import spack.util.spack_json as sjson
+import spack.util.spack_yaml as syaml
+import spack.schema.env
+import spack.config
+import spack.cmd.spec
+import spack.cmd.install
+import spack.cmd.uninstall
+import spack.cmd.module
+import spack.cmd.common.arguments as arguments
+from spack.config import ConfigScope
+from spack.spec import Spec, CompilerSpec, FlagMap
+from spack.repo import Repo
+from spack.version import VersionList
+from contextlib import contextmanager
+
+import argparse
+try:
+ from itertools import izip_longest as zip_longest
+except ImportError:
+ from itertools import zip_longest
+import os
+import sys
+import shutil
+
+description = "group a subset of packages"
+section = "environment"
+level = "long"
+
+_db_dirname = fs.join_path(spack.paths.var_path, 'environments')
+
+
+def get_env_root(name):
+ """Given an environment name, determines its root directory"""
+ return fs.join_path(_db_dirname, name)
+
+
+def get_dotenv_dir(env_root):
+ """@return Directory in an environment that is owned by Spack"""
+ return fs.join_path(env_root, '.env')
+
+
+def get_write_paths(env_root):
+ """Determines the names of temporary and permanent directories to
+ write machine-generated environment info."""
+ tmp_new = fs.join_path(env_root, '.env.new')
+ dest = get_dotenv_dir(env_root)
+ tmp_old = fs.join_path(env_root, '.env.old')
+ return tmp_new, dest, tmp_old
+
+
+class Environment(object):
+ def clear(self):
+ self.user_specs = list()
+ self.concretized_order = list()
+ self.specs_by_hash = dict()
+
+ def __init__(self, name):
+ self.name = name
+ self.clear()
+
+ # Default config
+ self.yaml = {
+ 'configs': ['<env>'],
+ 'specs': []
+ }
+
+ @property
+ def path(self):
+ return get_env_root(self.name)
+
+ def repo_path(self):
+ return fs.join_path(get_dotenv_dir(self.path), 'repo')
+
+ def add(self, user_spec, report_existing=True):
+ """Add a single user_spec (non-concretized) to the Environment"""
+ query_spec = Spec(user_spec)
+ existing = set(x for x in self.user_specs
+ if Spec(x).name == query_spec.name)
+ if existing:
+ if report_existing:
+ tty.die("Package {0} was already added to {1}"
+ .format(query_spec.name, self.name))
+ else:
+ tty.msg("Package {0} was already added to {1}"
+ .format(query_spec.name, self.name))
+ else:
+ tty.msg('Adding %s to environment %s' % (user_spec, self.name))
+ self.user_specs.append(user_spec)
+
+ def remove(self, query_spec):
+ """Remove specs from an environment that match a query_spec"""
+ query_spec = Spec(query_spec)
+ match_index = -1
+ for i, spec in enumerate(self.user_specs):
+ if Spec(spec).name == query_spec.name:
+ match_index = i
+ break
+
+ if match_index < 0:
+ tty.die("Not found: {0}".format(query_spec))
+
+ del self.user_specs[match_index]
+ if match_index < len(self.concretized_order):
+ spec_hash = self.concretized_order[match_index]
+ del self.concretized_order[match_index]
+ del self.specs_by_hash[spec_hash]
+
+ def concretize(self, force=False):
+ """Concretize user_specs in an Environment, creating (fully
+ concretized) specs.
+
+ force: bool
+ If set, re-concretize ALL specs, even those that were
+ already concretized.
+ """
+
+ if force:
+ # Clear previously concretized specs
+ self.specs_by_hash = dict()
+ self.concretized_order = list()
+
+ num_concretized = len(self.concretized_order)
+ new_specs = list()
+ for user_spec in self.user_specs[num_concretized:]:
+ tty.msg('Concretizing %s' % user_spec)
+
+ spec = spack.cmd.parse_specs(user_spec)[0]
+ spec.concretize()
+ new_specs.append(spec)
+ dag_hash = spec.dag_hash()
+ self.specs_by_hash[dag_hash] = spec
+ self.concretized_order.append(spec.dag_hash())
+
+ # Display concretized spec to the user
+ sys.stdout.write(spec.tree(
+ recurse_dependencies=True, install_status=True,
+ hashlen=7, hashes=True))
+
+ return new_specs
+
+ def install(self, install_args=None):
+ """Do a `spack install` on all the (concretized)
+ specs in an Environment."""
+
+ # Make sure log directory exists
+ logs = fs.join_path(self.path, 'logs')
+ try:
+ os.makedirs(logs)
+ except OSError:
+ if not os.path.isdir(logs):
+ raise
+
+ for concretized_hash in self.concretized_order:
+ spec = self.specs_by_hash[concretized_hash]
+
+ # Parse cli arguments and construct a dictionary
+ # that will be passed to Package.do_install API
+ kwargs = dict()
+ if install_args:
+ spack.cmd.install.update_kwargs_from_args(install_args, kwargs)
+ with pushd(self.path):
+ spec.package.do_install(**kwargs)
+
+ # Link the resulting log file into logs dir
+ logname = '%s-%s.log' % (spec.name, spec.dag_hash(7))
+ logpath = fs.join_path(logs, logname)
+ try:
+ os.remove(logpath)
+ except OSError:
+ pass
+ os.symlink(spec.package.build_log_path, logpath)
+
+ def uninstall(self, args):
+ """Uninstall all the specs in an Environment."""
+ specs = self._get_environment_specs(recurse_dependencies=True)
+ args.all = False
+ spack.cmd.uninstall.uninstall_specs(args, specs)
+
+ def list(self, stream, **kwargs):
+ """List the specs in an environment."""
+ for user_spec, concretized_hash in zip_longest(
+ self.user_specs, self.concretized_order):
+
+ stream.write('========= {0}\n'.format(user_spec))
+
+ if concretized_hash:
+ concretized_spec = self.specs_by_hash[concretized_hash]
+ stream.write(concretized_spec.tree(**kwargs))
+
+ def upgrade_dependency(self, dep_name, dry_run=False):
+ # TODO: if you have
+ # w -> x -> y
+ # and
+ # v -> x -> y
+ # then it would be desirable to ensure that w and v refer to the
+ # same x after upgrading y. This is not currently guaranteed.
+ new_order = list()
+ new_deps = list()
+ for i, spec_hash in enumerate(self.concretized_order):
+ spec = self.specs_by_hash[spec_hash]
+ if dep_name in spec:
+ if dry_run:
+ tty.msg("Would upgrade {0} for {1}"
+ .format(spec[dep_name].format(), spec.format()))
+ else:
+ new_spec = upgrade_dependency_version(spec, dep_name)
+ new_order.append(new_spec.dag_hash())
+ self.specs_by_hash[new_spec.dag_hash()] = new_spec
+ new_deps.append(new_spec[dep_name])
+ else:
+ new_order.append(spec_hash)
+
+ if not dry_run:
+ self.concretized_order = new_order
+ return new_deps[0] if new_deps else None
+
+ def reset_os_and_compiler(self, compiler=None):
+ new_order = list()
+ new_specs_by_hash = {}
+ for spec_hash in self.concretized_order:
+ spec = self.specs_by_hash[spec_hash]
+ new_spec = reset_os_and_compiler(spec, compiler)
+ new_order.append(new_spec.dag_hash())
+ new_specs_by_hash[new_spec.dag_hash()] = new_spec
+ self.concretized_order = new_order
+ self.specs_by_hash = new_specs_by_hash
+
+ def _get_environment_specs(self, recurse_dependencies=True):
+ """Returns the specs of all the packages in an environment.
+ If these specs appear under different user_specs, only one copy
+ is added to the list returned."""
+ package_to_spec = {}
+ spec_list = list()
+
+ for spec_hash in self.concretized_order:
+ spec = self.specs_by_hash[spec_hash]
+
+ specs = spec.traverse(deptype=('link', 'run')) \
+ if recurse_dependencies else (spec,)
+ for dep in specs:
+ if dep.name in package_to_spec:
+ tty.warn("{0} takes priority over {1}"
+ .format(package_to_spec[dep.name].format(),
+ dep.format()))
+ else:
+ package_to_spec[dep.name] = dep
+ spec_list.append(dep)
+
+ return spec_list
+
+ def to_dict(self):
+ """Used in serializing to JSON"""
+ concretized_order = list(self.concretized_order)
+ concrete_specs = dict()
+ for spec in self.specs_by_hash.values():
+ for s in spec.traverse():
+ if s.dag_hash() not in concrete_specs:
+ concrete_specs[s.dag_hash()] = (
+ s.to_node_dict(all_deps=True))
+ format = {
+ 'user_specs': self.user_specs,
+ 'concretized_order': concretized_order,
+ 'concrete_specs': concrete_specs,
+ }
+ return format
+
+ @staticmethod
+ def from_dict(name, d):
+ """Used in deserializing from JSON"""
+ env = Environment(name)
+ env.user_specs = list(d['user_specs'])
+ env.concretized_order = list(d['concretized_order'])
+ specs_dict = d['concrete_specs']
+
+ hash_to_node_dict = specs_dict
+ root_hashes = set(env.concretized_order)
+
+ specs_by_hash = {}
+ for dag_hash, node_dict in hash_to_node_dict.items():
+ specs_by_hash[dag_hash] = Spec.from_node_dict(node_dict)
+
+ for dag_hash, node_dict in hash_to_node_dict.items():
+ for dep_name, dep_hash, deptypes in (
+ Spec.dependencies_from_node_dict(node_dict)):
+ specs_by_hash[dag_hash]._add_dependency(
+ specs_by_hash[dep_hash], deptypes)
+
+ env.specs_by_hash = dict(
+ (x, y) for x, y in specs_by_hash.items() if x in root_hashes)
+
+ return env
+
+
+def reset_os_and_compiler(spec, compiler=None):
+ spec = spec.copy()
+ for x in spec.traverse():
+ x.compiler = None
+ x.architecture = None
+ x.compiler_flags = FlagMap(x)
+ x._concrete = False
+ x._hash = None
+ if compiler:
+ spec.compiler = CompilerSpec(compiler)
+ spec.concretize()
+ return spec
+
+
+def upgrade_dependency_version(spec, dep_name):
+ spec = spec.copy()
+ for x in spec.traverse():
+ x._concrete = False
+ x._normal = False
+ x._hash = None
+ spec[dep_name].versions = VersionList(':')
+ spec.concretize()
+ return spec
+
+
+def check_consistent_env(env_root):
+ tmp_new, dest, tmp_old = get_write_paths(env_root)
+ if os.path.exists(tmp_new) or os.path.exists(tmp_old):
+ tty.die("Partial write state, run 'spack env repair'")
+
+
+def write(environment, new_repo=None):
+ """Writes an in-memory environment back to its location on disk,
+ in an atomic manner."""
+
+ tmp_new, dest, tmp_old = get_write_paths(get_env_root(environment.name))
+
+ # Write the machine-generated stuff
+ fs.mkdirp(tmp_new)
+ # create one file for the environment object
+ with open(fs.join_path(tmp_new, 'environment.json'), 'w') as f:
+ sjson.dump(environment.to_dict(), stream=f)
+
+ dest_repo_dir = fs.join_path(tmp_new, 'repo')
+ if new_repo:
+ shutil.copytree(new_repo.root, dest_repo_dir)
+ elif os.path.exists(environment.repo_path()):
+ shutil.copytree(environment.repo_path(), dest_repo_dir)
+
+ # Swap in new directory atomically
+ if os.path.exists(dest):
+ shutil.move(dest, tmp_old)
+ shutil.move(tmp_new, dest)
+ if os.path.exists(tmp_old):
+ shutil.rmtree(tmp_old)
+
+
+def repair(environment_name):
+ """Recovers from crash during critical section of write().
+ Possibilities:
+
+ tmp_new, dest
+ tmp_new, tmp_old
+ tmp_old, dest
+ """
+ tmp_new, dest, tmp_old = get_write_paths(get_env_root(environment_name))
+ if os.path.exists(tmp_old):
+ if not os.path.exists(dest):
+ shutil.move(tmp_new, dest)
+ else:
+ shutil.rmtree(tmp_old)
+ tty.info("Previous update completed")
+ elif os.path.exists(tmp_new):
+ tty.info("Previous update did not complete")
+ else:
+ tty.info("Previous update may have completed")
+
+ if os.path.exists(tmp_new):
+ shutil.rmtree(tmp_new)
+
+
+def read(environment_name):
+ # Check that env is in a consistent state on disk
+ env_root = get_env_root(environment_name)
+
+ # Read env.yaml file
+ env_yaml = spack.config._read_config_file(
+ fs.join_path(env_root, 'env.yaml'),
+ spack.schema.env.schema)
+
+ dotenv_dir = get_dotenv_dir(env_root)
+ with open(fs.join_path(dotenv_dir, 'environment.json'), 'r') as f:
+ environment_dict = sjson.load(f)
+ environment = Environment.from_dict(environment_name, environment_dict)
+ if env_yaml:
+ environment.yaml = env_yaml['env']
+
+ return environment
+
+
+# =============== Modifies Environment
+
+def environment_create(args):
+ if os.path.exists(get_env_root(args.environment)):
+ raise tty.die("Environment already exists: " + args.environment)
+
+ _environment_create(args.environment)
+
+
+def _environment_create(name, init_config=None):
+ environment = Environment(name)
+
+ user_specs = list()
+ config_sections = {}
+ if init_config:
+ for key, val in init_config.items():
+ if key == 'user_specs':
+ user_specs.extend(val)
+ else:
+ config_sections[key] = val
+
+ for user_spec in user_specs:
+ environment.add(user_spec)
+
+ write(environment)
+
+ # When creating the environment, the user may specify configuration
+ # to place in the environment initially. Spack does not interfere
+ # with this configuration after initialization so it is handled here
+ if len(config_sections) > 0:
+ config_basedir = fs.join_path(environment.path, 'config')
+ os.mkdir(config_basedir)
+ for key, val in config_sections.items():
+ yaml_section = syaml.dump({key: val}, default_flow_style=False)
+ yaml_file = '{0}.yaml'.format(key)
+ yaml_path = fs.join_path(config_basedir, yaml_file)
+ with open(yaml_path, 'w') as f:
+ f.write(yaml_section)
+
+
+def environment_add(args):
+ check_consistent_env(get_env_root(args.environment))
+ environment = read(args.environment)
+ parsed_specs = spack.cmd.parse_specs(args.package)
+
+ if args.all:
+ # Don't allow command-line specs with --all
+ if len(parsed_specs) > 0:
+ tty.die('Cannot specify --all and specs too on the command line')
+
+ yaml_specs = environment.yaml['specs']
+ if len(yaml_specs) == 0:
+ tty.msg('No specs to add from env.yaml')
+
+ # Add list of specs from env.yaml file
+ for user_spec, _ in yaml_specs.items(): # OrderedDict
+ environment.add(str(user_spec), report_existing=False)
+ else:
+ for spec in parsed_specs:
+ environment.add(str(spec))
+
+ write(environment)
+
+
+def environment_remove(args):
+ check_consistent_env(get_env_root(args.environment))
+ environment = read(args.environment)
+ if args.all:
+ environment.clear()
+ else:
+ for spec in spack.cmd.parse_specs(args.package):
+ environment.remove(spec.format())
+ write(environment)
+
+
+def environment_spec(args):
+ environment = read(args.environment)
+ prepare_repository(environment, use_repo=args.use_repo)
+ prepare_config_scope(environment)
+ spack.cmd.spec.spec(None, args)
+
+
+def environment_concretize(args):
+ check_consistent_env(get_env_root(args.environment))
+ environment = read(args.environment)
+ _environment_concretize(
+ environment, use_repo=args.use_repo, force=args.force)
+
+
+def _environment_concretize(environment, use_repo=False, force=False):
+ """Function body separated out to aid in testing."""
+
+ # Change global search paths
+ repo = prepare_repository(environment, use_repo=use_repo)
+ prepare_config_scope(environment)
+
+ new_specs = environment.concretize(force=force)
+
+ for spec in new_specs:
+ for dep in spec.traverse():
+ dump_to_environment_repo(dep, repo)
+
+ # Moves <env>/.env.new to <env>/.env
+ write(environment, repo)
+
+# =============== Does not Modify Environment
+
+
+def environment_install(args):
+ check_consistent_env(get_env_root(args.environment))
+ environment = read(args.environment)
+ prepare_repository(environment, use_repo=args.use_repo)
+ environment.install(args)
+
+
+def environment_uninstall(args):
+ check_consistent_env(get_env_root(args.environment))
+ environment = read(args.environment)
+ prepare_repository(environment)
+ environment.uninstall(args)
+
+# =======================================
+
+
+def dump_to_environment_repo(spec, repo):
+ dest_pkg_dir = repo.dirname_for_package_name(spec.name)
+ if not os.path.exists(dest_pkg_dir):
+ spack.repo.path.dump_provenance(spec, dest_pkg_dir)
+
+
+def prepare_repository(environment, remove=None, use_repo=False):
+ """Adds environment's repository to the global search path of repos"""
+ import tempfile
+ repo_stage = tempfile.mkdtemp()
+ new_repo_dir = fs.join_path(repo_stage, 'repo')
+ if os.path.exists(environment.repo_path()):
+ shutil.copytree(environment.repo_path(), new_repo_dir)
+ else:
+ spack.repo.create_repo(new_repo_dir, environment.name)
+ if remove:
+ remove_dirs = []
+ repo = Repo(new_repo_dir)
+ for pkg_name in remove:
+ remove_dirs.append(repo.dirname_for_package_name(pkg_name))
+ for d in remove_dirs:
+ shutil.rmtree(d)
+ repo = Repo(new_repo_dir)
+ if use_repo:
+ spack.repo.put_first(repo)
+ return repo
+
+
+def prepare_config_scope(environment):
+ """Adds environment's scope to the global search path
+ of configuration scopes"""
+
+ # Load up configs
+ for config_spec in environment.yaml['configs']:
+ config_name = os.path.split(config_spec)[1]
+ if config_name == '<env>':
+ # Use default config for the environment; doesn't have to exist
+ config_dir = fs.join_path(environment.path, 'config')
+ if not os.path.isdir(config_dir):
+ continue
+ config_name = environment.name
+ else:
+ # Use external user-provided config
+ config_dir = os.path.normpath(os.path.join(
+ environment.path, config_spec.format(**os.environ)))
+ if not os.path.isdir(config_dir):
+ tty.die('Spack config %s (%s) not found' %
+ (config_name, config_dir))
+
+ tty.msg('Using Spack config %s scope at %s' %
+ (config_name, config_dir))
+ spack.config.config.push_scope(ConfigScope(config_name, config_dir))
+
+
+def environment_relocate(args):
+ environment = read(args.environment)
+ prepare_repository(environment, use_repo=args.use_repo)
+ environment.reset_os_and_compiler(compiler=args.compiler)
+ write(environment)
+
+
+def environment_list(args):
+ # TODO? option to list packages w/ multiple instances?
+ environment = read(args.environment)
+ import sys
+ environment.list(
+ sys.stdout, recurse_dependencies=args.recurse_dependencies,
+ hashes=args.long or args.very_long,
+ hashlen=None if args.very_long else 7,
+ install_status=args.install_status)
+
+
+def environment_stage(args):
+ environment = read(args.environment)
+ prepare_repository(environment, use_repo=args.use_repo)
+ for spec in environment.specs_by_hash.values():
+ for dep in spec.traverse():
+ dep.package.do_stage()
+
+
+def environment_location(args):
+ environment = read(args.environment)
+ print(environment.path)
+
+
+@contextmanager
+def redirect_stdout(ofname):
+ """Redirects STDOUT to (by default) a file within the environment;
+ or else a user-specified filename."""
+ with open(ofname, 'w') as f:
+ original = sys.stdout
+ sys.stdout = f
+ yield
+ sys.stdout = original
+
+
+@contextmanager
+def pushd(dir):
+ original = os.getcwd()
+ os.chdir(dir)
+ yield
+ os.chdir(original)
+
+
+def environment_loads(args):
+ # Set the module types that have been selected
+ module_types = args.module_type
+ if module_types is None:
+ # If no selection has been made select all of them
+ module_types = ['tcl']
+
+ module_types = list(set(module_types))
+
+ environment = read(args.environment)
+ recurse_dependencies = args.recurse_dependencies
+ args.recurse_dependencies = False
+ ofname = fs.join_path(environment.path, 'loads')
+ with redirect_stdout(ofname):
+ specs = environment._get_environment_specs(
+ recurse_dependencies=recurse_dependencies)
+ spack.cmd.module.loads(module_types, specs, args)
+
+ print('To load this environment, type:')
+ print(' source %s' % ofname)
+
+
+def environment_upgrade_dependency(args):
+ environment = read(args.environment)
+ repo = prepare_repository(
+ environment, use_repo=args.use_repo, remove=[args.dep_name])
+ new_dep = environment.upgrade_dependency(args.dep_name, args.dry_run)
+ if not args.dry_run and new_dep:
+ dump_to_environment_repo(new_dep, repo)
+ write(environment, repo)
+
+
+def add_use_repo_argument(cmd_parser):
+ cmd_parser.add_argument(
+ '--use-env-repo', action='store_true', dest='use_repo',
+ help='Use package definitions stored in the environment'
+ )
+
+
+def setup_parser(subparser):
+ subparser.add_argument(
+ 'environment',
+ help="The environment you are working with"
+ )
+
+ sp = subparser.add_subparsers(
+ metavar='SUBCOMMAND', dest='environment_command')
+
+ create_parser = sp.add_parser('create', help='Make an environment')
+ create_parser.add_argument(
+ '--init-file', dest='init_file',
+ help='File with user specs to add and configuration yaml to use'
+ )
+
+ add_parser = sp.add_parser('add', help='Add a spec to an environment')
+ add_parser.add_argument(
+ '-a', '--all', action='store_true', dest='all',
+ help="Add all specs listed in env.yaml")
+ add_parser.add_argument(
+ 'package',
+ nargs=argparse.REMAINDER,
+ help="Spec of the package to add"
+ )
+
+ remove_parser = sp.add_parser(
+ 'remove', help='Remove a spec from this environment')
+ remove_parser.add_argument(
+ '-a', '--all', action='store_true', dest='all',
+ help="Remove all specs from (clear) the environment")
+ remove_parser.add_argument(
+ 'package',
+ nargs=argparse.REMAINDER,
+ help="Spec of the package to remove"
+ )
+
+ spec_parser = sp.add_parser(
+ 'spec', help='Concretize sample spec')
+ spack.cmd.spec.add_common_arguments(spec_parser)
+ add_use_repo_argument(spec_parser)
+
+ concretize_parser = sp.add_parser(
+ 'concretize', help='Concretize user specs')
+ concretize_parser.add_argument(
+ '-f', '--force', action='store_true',
+ help="Re-concretize even if already concretized.")
+ add_use_repo_argument(concretize_parser)
+
+ relocate_parser = sp.add_parser(
+ 'relocate',
+ help='Reconcretize environment with new OS and/or compiler')
+ relocate_parser.add_argument(
+ '--compiler',
+ help="Compiler spec to use"
+ )
+ add_use_repo_argument(relocate_parser)
+
+ list_parser = sp.add_parser('list', help='List specs in an environment')
+ arguments.add_common_arguments(
+ list_parser,
+ ['recurse_dependencies', 'long', 'very_long', 'install_status'])
+
+ loads_parser = sp.add_parser(
+ 'loads',
+ help='List modules for an installed environment '
+ '(see spack module loads)')
+ spack.cmd.modules.add_loads_arguments(loads_parser)
+
+ sp.add_parser(
+ 'location',
+ help='Print the root directory of the environment')
+
+ upgrade_parser = sp.add_parser(
+ 'upgrade',
+ help='''Upgrade a dependency package in an environment to the latest
+version''')
+ upgrade_parser.add_argument(
+ 'dep_name', help='Dependency package to upgrade')
+ upgrade_parser.add_argument(
+ '--dry-run', action='store_true', dest='dry_run',
+ help="Just show the updates that would take place")
+ add_use_repo_argument(upgrade_parser)
+
+ stage_parser = sp.add_parser(
+ 'stage',
+ help='Download all source files for all packages in an environment')
+ add_use_repo_argument(stage_parser)
+
+ config_update_parser = sp.add_parser(
+ 'update-config',
+ help='Add config yaml file to environment')
+ config_update_parser.add_argument(
+ 'config_files',
+ nargs=argparse.REMAINDER,
+ help="Configuration files to add"
+ )
+
+ install_parser = sp.add_parser(
+ 'install',
+ help='Install all concretized specs in an environment')
+ spack.cmd.install.add_common_arguments(install_parser)
+ add_use_repo_argument(install_parser)
+
+ uninstall_parser = sp.add_parser(
+ 'uninstall',
+ help='Uninstall all concretized specs in an environment')
+ spack.cmd.uninstall.add_common_arguments(uninstall_parser)
+
+
+def env(parser, args, **kwargs):
+ action = {
+ 'create': environment_create,
+ 'add': environment_add,
+ 'spec': environment_spec,
+ 'concretize': environment_concretize,
+ 'list': environment_list,
+ 'loads': environment_loads,
+ 'location': environment_location,
+ 'remove': environment_remove,
+ 'relocate': environment_relocate,
+ 'upgrade': environment_upgrade_dependency,
+ 'stage': environment_stage,
+ 'install': environment_install,
+ 'uninstall': environment_uninstall
+ }
+ action[args.environment_command](args)
diff --git a/lib/spack/spack/schema/env.py b/lib/spack/spack/schema/env.py
new file mode 100644
index 0000000000..dab4d6a5bd
--- /dev/null
+++ b/lib/spack/spack/schema/env.py
@@ -0,0 +1,43 @@
+# Copyright 2013-2018 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+"""Schema for env.yaml configuration file.
+
+.. literalinclude:: ../spack/schema/env.py
+ :lines: 32-
+"""
+
+
+schema = {
+ '$schema': 'http://json-schema.org/schema#',
+ 'title': 'Spack Environments user configuration file schema',
+ 'type': 'object',
+ 'additionalProperties': False,
+ 'properties': {
+ 'env': {
+ 'type': 'object',
+ 'default': {},
+ 'properties': {
+ 'configs': {
+ 'type': 'array',
+ 'default': [],
+ 'items': {'type': 'string'}
+ },
+ 'specs': {
+ 'type': 'object',
+ 'default': {},
+ 'additionalProperties': False,
+ 'patternProperties': {
+ r'\w[\w-]*': { # user spec
+ 'type': 'object',
+ 'default': {},
+ 'additionalProperties': False,
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/spack/spack/test/cmd/env.py b/lib/spack/spack/test/cmd/env.py
new file mode 100644
index 0000000000..dbb3aa9f1f
--- /dev/null
+++ b/lib/spack/spack/test/cmd/env.py
@@ -0,0 +1,149 @@
+# Copyright 2013-2018 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+import unittest
+import tempfile
+import shutil
+import pytest
+try:
+ from StringIO import StringIO
+except ImportError:
+ from io import StringIO
+
+import spack.cmd.env
+import spack.modules
+import spack.util.spack_yaml as syaml
+from spack.cmd.env import (Environment, prepare_repository,
+ _environment_concretize, prepare_config_scope,
+ _environment_create)
+from spack.version import Version
+
+
+class TestEnvironment(unittest.TestCase):
+ def setUp(self):
+ self.env_dir = spack.cmd.env._db_dirname
+ spack.cmd.env._db_dirname = tempfile.mkdtemp()
+
+ def tearDown(self):
+ shutil.rmtree(spack.cmd.env._db_dirname)
+ spack.cmd.env._db_dirname = self.env_dir
+
+ def test_add(self):
+ c = Environment('test')
+ c.add('mpileaks')
+ assert 'mpileaks' in c.user_specs
+
+ @pytest.mark.usefixtures('config', 'mutable_mock_packages')
+ def test_concretize(self):
+ c = Environment('test')
+ c.add('mpileaks')
+ c.concretize()
+ env_specs = c._get_environment_specs()
+ assert any(x.name == 'mpileaks' for x in env_specs)
+
+ @pytest.mark.usefixtures('config', 'mutable_mock_packages',
+ 'install_mockery', 'mock_fetch')
+ def test_env_install(self):
+ c = Environment('test')
+ c.add('cmake-client')
+ c.concretize()
+ c.install()
+ env_specs = c._get_environment_specs()
+ spec = next(x for x in env_specs if x.name == 'cmake-client')
+ assert spec.package.installed
+
+ @pytest.mark.usefixtures('config', 'mutable_mock_packages')
+ def test_remove_after_concretize(self):
+ c = Environment('test')
+ c.add('mpileaks')
+ c.concretize()
+ c.add('python')
+ c.concretize()
+ c.remove('mpileaks')
+ env_specs = c._get_environment_specs()
+ assert not any(x.name == 'mpileaks' for x in env_specs)
+
+ @pytest.mark.usefixtures('config', 'mutable_mock_packages')
+ def test_reset_compiler(self):
+ c = Environment('test')
+ c.add('mpileaks')
+ c.concretize()
+
+ first_spec = c.specs_by_hash[c.concretized_order[0]]
+ available = set(['gcc', 'clang'])
+ available.remove(first_spec.compiler.name)
+ new_compiler = next(iter(available))
+ c.reset_os_and_compiler(compiler=new_compiler)
+
+ new_spec = c.specs_by_hash[c.concretized_order[0]]
+ assert new_spec.compiler != first_spec.compiler
+
+ @pytest.mark.usefixtures('config', 'mutable_mock_packages')
+ def test_environment_list(self):
+ c = Environment('test')
+ c.add('mpileaks')
+ c.concretize()
+ c.add('python')
+ mock_stream = StringIO()
+ c.list(mock_stream)
+ list_content = mock_stream.getvalue()
+ assert 'mpileaks' in list_content
+ assert 'python' in list_content
+ mpileaks_spec = c.specs_by_hash[c.concretized_order[0]]
+ assert mpileaks_spec.format() in list_content
+
+ @pytest.mark.usefixtures('config', 'mutable_mock_packages')
+ def test_upgrade_dependency(self):
+ c = Environment('test')
+ c.add('mpileaks ^callpath@0.9')
+ c.concretize()
+
+ c.upgrade_dependency('callpath')
+ env_specs = c._get_environment_specs()
+ callpath_dependents = list(x for x in env_specs if 'callpath' in x)
+ assert callpath_dependents
+ for spec in callpath_dependents:
+ assert spec['callpath'].version == Version('1.0')
+
+ @pytest.mark.usefixtures('config', 'mutable_mock_packages')
+ def test_init_config(self):
+ test_config = """user_specs:
+ - mpileaks
+packages:
+ mpileaks:
+ version: [2.2]
+"""
+ spack.package_prefs.PackagePrefs._packages_config_cache = None
+ spack.package_prefs.PackagePrefs._spec_cache = {}
+
+ _environment_create('test', syaml.load(StringIO(test_config)))
+ c = spack.cmd.env.read('test')
+ prepare_config_scope(c)
+ c.concretize()
+ assert any(x.satisfies('mpileaks@2.2')
+ for x in c._get_environment_specs())
+
+ @pytest.mark.usefixtures('config', 'mutable_mock_packages')
+ def test_to_dict(self):
+ c = Environment('test')
+ c.add('mpileaks')
+ c.concretize()
+ context_dict = c.to_dict()
+ c_copy = Environment.from_dict('test_copy', context_dict)
+ assert c.specs_by_hash == c_copy.specs_by_hash
+
+ @pytest.mark.usefixtures('config', 'mutable_mock_packages')
+ def test_prepare_repo(self):
+ c = Environment('testx')
+ c.add('mpileaks')
+ _environment_concretize(c)
+ repo = None
+ try:
+ repo = prepare_repository(c)
+ package = repo.get(spack.spec.Spec('mpileaks'))
+ assert package.namespace.split('.')[-1] == 'testx'
+ finally:
+ if repo:
+ shutil.rmtree(repo.root)