Support spack env activate --with-view <name> <env> (#40549)

Currently `spack env activate --with-view` exists, but is a no-op.

So, it is not too much of a breaking change to make this redundant flag
accept a value `spack env activate --with-view <name>` which activates
a particular view by name.

The view name is stored in `SPACK_ENV_VIEW`.

This also fixes an issue where deactivating a view that was activated
with `--without-view` possibly removes entries from PATH, since now we
keep track of whether the default view was "enabled" or not.
This commit is contained in:
Harmen Stoppels 2023-10-17 15:40:48 +02:00 committed by GitHub
parent 348e5cb522
commit bd165ebc4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 120 additions and 75 deletions

View file

@ -8,6 +8,7 @@
import shutil
import sys
import tempfile
from typing import Optional
import llnl.string as string
import llnl.util.filesystem as fs
@ -96,22 +97,16 @@ def env_activate_setup_parser(subparser):
view_options = subparser.add_mutually_exclusive_group()
view_options.add_argument(
"-v",
"--with-view",
action="store_const",
dest="with_view",
const=True,
default=True,
help="update PATH, etc., with associated view",
"-v",
metavar="name",
help="set runtime environment variables for specific view",
)
view_options.add_argument(
"-V",
"--without-view",
action="store_const",
dest="with_view",
const=False,
default=True,
help="do not update PATH, etc., with associated view",
"-V",
action="store_true",
help="do not set runtime environment variables for any view",
)
subparser.add_argument(
@ -197,10 +192,20 @@ def env_activate(args):
# Activate new environment
active_env = ev.Environment(env_path)
# Check if runtime environment variables are requested, and if so, for what view.
view: Optional[str] = None
if args.with_view:
view = args.with_view
if not active_env.has_view(view):
tty.die(f"The environment does not have a view named '{view}'")
elif not args.without_view and active_env.has_view(ev.default_view_name):
view = ev.default_view_name
cmds += spack.environment.shell.activate_header(
env=active_env, shell=args.shell, prompt=env_prompt if args.prompt else None
env=active_env, shell=args.shell, prompt=env_prompt if args.prompt else None, view=view
)
env_mods.extend(spack.environment.shell.activate(env=active_env, add_view=args.with_view))
env_mods.extend(spack.environment.shell.activate(env=active_env, view=view))
cmds += env_mods.shell_modifications(args.shell)
sys.stdout.write(cmds)

View file

@ -365,6 +365,7 @@
read,
root,
spack_env_var,
spack_env_view_var,
update_yaml,
)
@ -397,5 +398,6 @@
"read",
"root",
"spack_env_var",
"spack_env_view_var",
"update_yaml",
]

View file

@ -64,6 +64,8 @@
#: environment variable used to indicate the active environment
spack_env_var = "SPACK_ENV"
#: environment variable used to indicate the active environment view
spack_env_view_var = "SPACK_ENV_VIEW"
#: currently activated environment
_active_environment: Optional["Environment"] = None
@ -1595,16 +1597,14 @@ def concretize_and_add(self, user_spec, concrete_spec=None, tests=False):
@property
def default_view(self):
if not self.views:
raise SpackEnvironmentError("{0} does not have a view enabled".format(self.name))
if default_view_name not in self.views:
raise SpackEnvironmentError(
"{0} does not have a default view enabled".format(self.name)
)
if not self.has_view(default_view_name):
raise SpackEnvironmentError(f"{self.name} does not have a default view enabled")
return self.views[default_view_name]
def has_view(self, view_name: str) -> bool:
return view_name in self.views
def update_default_view(self, path_or_bool: Union[str, bool]) -> None:
"""Updates the path of the default view.
@ -1690,14 +1690,14 @@ def check_views(self):
"Loading the environment view will require reconcretization." % self.name
)
def _env_modifications_for_default_view(self, reverse=False):
def _env_modifications_for_view(self, view: ViewDescriptor, reverse: bool = False):
all_mods = spack.util.environment.EnvironmentModifications()
visited = set()
errors = []
for root_spec in self.concrete_roots():
if root_spec in self.default_view and root_spec.installed and root_spec.package:
if root_spec in view and root_spec.installed and root_spec.package:
for spec in root_spec.traverse(deptype="run", root=True):
if spec.name in visited:
# It is expected that only one instance of the package
@ -1714,7 +1714,7 @@ def _env_modifications_for_default_view(self, reverse=False):
visited.add(spec.name)
try:
mods = uenv.environment_modifications_for_spec(spec, self.default_view)
mods = uenv.environment_modifications_for_spec(spec, view)
except Exception as e:
msg = "couldn't get environment settings for %s" % spec.format(
"{name}@{version} /{hash:7}"
@ -1726,22 +1726,22 @@ def _env_modifications_for_default_view(self, reverse=False):
return all_mods, errors
def add_default_view_to_env(self, env_mod):
"""
Collect the environment modifications to activate an environment using the
default view. Removes duplicate paths.
def add_view_to_env(
self, env_mod: spack.util.environment.EnvironmentModifications, view: str
) -> spack.util.environment.EnvironmentModifications:
"""Collect the environment modifications to activate an environment using the provided
view. Removes duplicate paths.
Args:
env_mod (spack.util.environment.EnvironmentModifications): the environment
modifications object that is modified.
"""
if default_view_name not in self.views:
# No default view to add to shell
env_mod: the environment modifications object that is modified.
view: the name of the view to activate."""
descriptor = self.views.get(view)
if not descriptor:
return env_mod
env_mod.extend(uenv.unconditional_environment_modifications(self.default_view))
env_mod.extend(uenv.unconditional_environment_modifications(descriptor))
mods, errors = self._env_modifications_for_default_view()
mods, errors = self._env_modifications_for_view(descriptor)
env_mod.extend(mods)
if errors:
for err in errors:
@ -1753,22 +1753,22 @@ def add_default_view_to_env(self, env_mod):
return env_mod
def rm_default_view_from_env(self, env_mod):
"""
Collect the environment modifications to deactivate an environment using the
default view. Reverses the action of ``add_default_view_to_env``.
def rm_view_from_env(
self, env_mod: spack.util.environment.EnvironmentModifications, view: str
) -> spack.util.environment.EnvironmentModifications:
"""Collect the environment modifications to deactivate an environment using the provided
view. Reverses the action of ``add_view_to_env``.
Args:
env_mod (spack.util.environment.EnvironmentModifications): the environment
modifications object that is modified.
"""
if default_view_name not in self.views:
# No default view to add to shell
env_mod: the environment modifications object that is modified.
view: the name of the view to deactivate."""
descriptor = self.views.get(view)
if not descriptor:
return env_mod
env_mod.extend(uenv.unconditional_environment_modifications(self.default_view).reversed())
env_mod.extend(uenv.unconditional_environment_modifications(descriptor).reversed())
mods, _ = self._env_modifications_for_default_view(reverse=True)
mods, _ = self._env_modifications_for_view(descriptor, reverse=True)
env_mod.extend(mods)
return env_mod

View file

@ -3,6 +3,7 @@
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os
from typing import Optional
import llnl.util.tty as tty
from llnl.util.tty.color import colorize
@ -13,12 +14,14 @@
from spack.util.environment import EnvironmentModifications
def activate_header(env, shell, prompt=None):
def activate_header(env, shell, prompt=None, view: Optional[str] = None):
# Construct the commands to run
cmds = ""
if shell == "csh":
# TODO: figure out how to make color work for csh
cmds += "setenv SPACK_ENV %s;\n" % env.path
if view:
cmds += "setenv SPACK_ENV_VIEW %s;\n" % view
cmds += 'alias despacktivate "spack env deactivate";\n'
if prompt:
cmds += "if (! $?SPACK_OLD_PROMPT ) "
@ -29,6 +32,8 @@ def activate_header(env, shell, prompt=None):
prompt = colorize("@G{%s} " % prompt, color=True)
cmds += "set -gx SPACK_ENV %s;\n" % env.path
if view:
cmds += "set -gx SPACK_ENV_VIEW %s;\n" % view
cmds += "function despacktivate;\n"
cmds += " spack env deactivate;\n"
cmds += "end;\n"
@ -40,15 +45,21 @@ def activate_header(env, shell, prompt=None):
elif shell == "bat":
# TODO: Color
cmds += 'set "SPACK_ENV=%s"\n' % env.path
if view:
cmds += 'set "SPACK_ENV_VIEW=%s"\n' % view
# TODO: despacktivate
# TODO: prompt
elif shell == "pwsh":
cmds += "$Env:SPACK_ENV='%s'\n" % env.path
if view:
cmds += "$Env:SPACK_ENV_VIEW='%s'\n" % view
else:
if "color" in os.getenv("TERM", "") and prompt:
prompt = colorize("@G{%s}" % prompt, color=True, enclose=True)
cmds += "export SPACK_ENV=%s;\n" % env.path
if view:
cmds += "export SPACK_ENV_VIEW=%s;\n" % view
cmds += "alias despacktivate='spack env deactivate';\n"
if prompt:
cmds += "if [ -z ${SPACK_OLD_PS1+x} ]; then\n"
@ -66,12 +77,14 @@ def deactivate_header(shell):
cmds = ""
if shell == "csh":
cmds += "unsetenv SPACK_ENV;\n"
cmds += "unsetenv SPACK_ENV_VIEW;\n"
cmds += "if ( $?SPACK_OLD_PROMPT ) "
cmds += ' eval \'set prompt="$SPACK_OLD_PROMPT" &&'
cmds += " unsetenv SPACK_OLD_PROMPT';\n"
cmds += "unalias despacktivate;\n"
elif shell == "fish":
cmds += "set -e SPACK_ENV;\n"
cmds += "set -e SPACK_ENV_VIEW;\n"
cmds += "functions -e despacktivate;\n"
#
# NOTE: Not changing fish_prompt (above) => no need to restore it here.
@ -79,14 +92,19 @@ def deactivate_header(shell):
elif shell == "bat":
# TODO: Color
cmds += 'set "SPACK_ENV="\n'
cmds += 'set "SPACK_ENV_VIEW="\n'
# TODO: despacktivate
# TODO: prompt
elif shell == "pwsh":
cmds += "Set-Item -Path Env:SPACK_ENV\n"
cmds += "Set-Item -Path Env:SPACK_ENV_VIEW\n"
else:
cmds += "if [ ! -z ${SPACK_ENV+x} ]; then\n"
cmds += "unset SPACK_ENV; export SPACK_ENV;\n"
cmds += "fi;\n"
cmds += "if [ ! -z ${SPACK_ENV_VIEW+x} ]; then\n"
cmds += "unset SPACK_ENV_VIEW; export SPACK_ENV_VIEW;\n"
cmds += "fi;\n"
cmds += "alias despacktivate > /dev/null 2>&1 && unalias despacktivate;\n"
cmds += "if [ ! -z ${SPACK_OLD_PS1+x} ]; then\n"
cmds += " if [ \"$SPACK_OLD_PS1\" = '$$$$' ]; then\n"
@ -100,24 +118,23 @@ def deactivate_header(shell):
return cmds
def activate(env, use_env_repo=False, add_view=True):
"""
Activate an environment and append environment modifications
def activate(
env: ev.Environment, use_env_repo=False, view: Optional[str] = "default"
) -> EnvironmentModifications:
"""Activate an environment and append environment modifications
To activate an environment, we add its configuration scope to the
existing Spack configuration, and we set active to the current
environment.
Arguments:
env (spack.environment.Environment): the environment to activate
use_env_repo (bool): use the packages exactly as they appear in the
environment's repository
add_view (bool): generate commands to add view to path variables
env: the environment to activate
use_env_repo: use the packages exactly as they appear in the environment's repository
view: generate commands to add runtime environment variables for named view
Returns:
spack.util.environment.EnvironmentModifications: Environment variables
modifications to activate environment.
"""
modifications to activate environment."""
ev.activate(env, use_env_repo=use_env_repo)
env_mods = EnvironmentModifications()
@ -129,9 +146,9 @@ def activate(env, use_env_repo=False, add_view=True):
# become PATH variables.
#
try:
if add_view and ev.default_view_name in env.views:
if view and env.has_view(view):
with spack.store.STORE.db.read_transaction():
env.add_default_view_to_env(env_mods)
env.add_view_to_env(env_mods, view)
except (spack.repo.UnknownPackageError, spack.repo.UnknownNamespaceError) as e:
tty.error(e)
tty.die(
@ -145,17 +162,15 @@ def activate(env, use_env_repo=False, add_view=True):
return env_mods
def deactivate():
"""
Deactivate an environment and collect corresponding environment modifications.
def deactivate() -> EnvironmentModifications:
"""Deactivate an environment and collect corresponding environment modifications.
Note: unloads the environment in its current state, not in the state it was
loaded in, meaning that specs that were removed from the spack environment
after activation are not unloaded.
Returns:
spack.util.environment.EnvironmentModifications: Environment variables
modifications to activate environment.
Environment variables modifications to activate environment.
"""
env_mods = EnvironmentModifications()
active = ev.active_environment()
@ -163,10 +178,12 @@ def deactivate():
if active is None:
return env_mods
if ev.default_view_name in active.views:
active_view = os.getenv(ev.spack_env_view_var)
if active_view and active.has_view(active_view):
try:
with spack.store.STORE.db.read_transaction():
active.rm_default_view_from_env(env_mods)
active.rm_view_from_env(env_mods, active_view)
except (spack.repo.UnknownPackageError, spack.repo.UnknownNamespaceError) as e:
tty.warn(e)
tty.warn(

View file

@ -663,7 +663,7 @@ def test_env_view_external_prefix(tmp_path, mutable_database, mock_packages):
e.write()
env_mod = spack.util.environment.EnvironmentModifications()
e.add_default_view_to_env(env_mod)
e.add_view_to_env(env_mod, "default")
env_variables = {}
env_mod.apply_modifications(env_variables)
assert str(fake_bin) in env_variables["PATH"]
@ -2356,7 +2356,7 @@ def test_env_activate_sh_prints_shell_output(tmpdir, mock_stage, mock_fetch, ins
This is a cursory check; ``share/spack/qa/setup-env-test.sh`` checks
for correctness.
"""
env("create", "test", add_view=True)
env("create", "test")
out = env("activate", "--sh", "test")
assert "export SPACK_ENV=" in out
@ -2371,7 +2371,7 @@ def test_env_activate_sh_prints_shell_output(tmpdir, mock_stage, mock_fetch, ins
def test_env_activate_csh_prints_shell_output(tmpdir, mock_stage, mock_fetch, install_mockery):
"""Check the shell commands output by ``spack env activate --csh``."""
env("create", "test", add_view=True)
env("create", "test")
out = env("activate", "--csh", "test")
assert "setenv SPACK_ENV" in out
@ -2388,7 +2388,7 @@ def test_env_activate_csh_prints_shell_output(tmpdir, mock_stage, mock_fetch, in
def test_env_activate_default_view_root_unconditional(mutable_mock_env_path):
"""Check that the root of the default view in the environment is added
to the shell unconditionally."""
env("create", "test", add_view=True)
env("create", "test")
with ev.read("test") as e:
viewdir = e.default_view.root
@ -2403,6 +2403,27 @@ def test_env_activate_default_view_root_unconditional(mutable_mock_env_path):
)
def test_env_activate_custom_view(tmp_path: pathlib.Path, mock_packages):
"""Check that an environment can be activated with a non-default view."""
env_template = tmp_path / "spack.yaml"
default_dir = tmp_path / "defaultdir"
nondefaultdir = tmp_path / "nondefaultdir"
with open(env_template, "w") as f:
f.write(
f"""\
spack:
specs: [a]
view:
default:
root: {default_dir}
nondefault:
root: {nondefaultdir}"""
)
env("create", "test", str(env_template))
shell = env("activate", "--sh", "--with-view", "nondefault", "test")
assert os.path.join(nondefaultdir, "bin") in shell
def test_concretize_user_specs_together():
e = ev.create("coconcretization")
e.unify = True

View file

@ -1016,7 +1016,7 @@ _spack_env() {
_spack_env_activate() {
if $list_options
then
SPACK_COMPREPLY="-h --help --sh --csh --fish --bat --pwsh -v --with-view -V --without-view -p --prompt --temp -d --dir"
SPACK_COMPREPLY="-h --help --sh --csh --fish --bat --pwsh --with-view -v --without-view -V -p --prompt --temp -d --dir"
else
_environments
fi

View file

@ -1427,7 +1427,7 @@ complete -c spack -n '__fish_spack_using_command env' -s h -l help -f -a help
complete -c spack -n '__fish_spack_using_command env' -s h -l help -d 'show this help message and exit'
# spack env activate
set -g __fish_spack_optspecs_spack_env_activate h/help sh csh fish bat pwsh v/with-view V/without-view p/prompt temp d/dir=
set -g __fish_spack_optspecs_spack_env_activate h/help sh csh fish bat pwsh v/with-view= V/without-view p/prompt temp d/dir=
complete -c spack -n '__fish_spack_using_command_pos 0 env activate' -f -a '(__fish_spack_environments)'
complete -c spack -n '__fish_spack_using_command env activate' -s h -l help -f -a help
complete -c spack -n '__fish_spack_using_command env activate' -s h -l help -d 'show this help message and exit'
@ -1441,10 +1441,10 @@ complete -c spack -n '__fish_spack_using_command env activate' -l bat -f -a shel
complete -c spack -n '__fish_spack_using_command env activate' -l bat -d 'print bat commands to activate the environment'
complete -c spack -n '__fish_spack_using_command env activate' -l pwsh -f -a shell
complete -c spack -n '__fish_spack_using_command env activate' -l pwsh -d 'print powershell commands to activate environment'
complete -c spack -n '__fish_spack_using_command env activate' -s v -l with-view -f -a with_view
complete -c spack -n '__fish_spack_using_command env activate' -s v -l with-view -d 'update PATH, etc., with associated view'
complete -c spack -n '__fish_spack_using_command env activate' -s V -l without-view -f -a with_view
complete -c spack -n '__fish_spack_using_command env activate' -s V -l without-view -d 'do not update PATH, etc., with associated view'
complete -c spack -n '__fish_spack_using_command env activate' -l with-view -s v -r -f -a with_view
complete -c spack -n '__fish_spack_using_command env activate' -l with-view -s v -r -d 'set runtime environment variables for specific view'
complete -c spack -n '__fish_spack_using_command env activate' -l without-view -s V -f -a without_view
complete -c spack -n '__fish_spack_using_command env activate' -l without-view -s V -d 'do not set runtime environment variables for any view'
complete -c spack -n '__fish_spack_using_command env activate' -s p -l prompt -f -a prompt
complete -c spack -n '__fish_spack_using_command env activate' -s p -l prompt -d 'decorate the command line prompt when activating'
complete -c spack -n '__fish_spack_using_command env activate' -l temp -f -a temp