diff --git a/lib/spack/spack/cmd/env.py b/lib/spack/spack/cmd/env.py index ee9d556b67..d308b89a6c 100644 --- a/lib/spack/spack/cmd/env.py +++ b/lib/spack/spack/cmd/env.py @@ -9,6 +9,7 @@ import shutil import sys import tempfile +from pathlib import Path from typing import Optional import llnl.string as string @@ -44,6 +45,7 @@ "deactivate", "create", ["remove", "rm"], + ["rename", "mv"], ["list", "ls"], ["status", "st"], "loads", @@ -472,11 +474,82 @@ def env_remove(args): tty.msg(f"Successfully removed environment '{bad_env_name}'") +# +# env rename +# +def env_rename_setup_parser(subparser): + """rename an existing environment""" + subparser.add_argument( + "mv_from", metavar="from", help="name (or path) of existing environment" + ) + subparser.add_argument( + "mv_to", metavar="to", help="new name (or path) for existing environment" + ) + subparser.add_argument( + "-d", + "--dir", + action="store_true", + help="the specified arguments correspond to directory paths", + ) + subparser.add_argument( + "-f", "--force", action="store_true", help="allow overwriting of an existing environment" + ) + + +def env_rename(args): + """Rename an environment. + + This renames a managed environment or moves an anonymous environment. + """ + + # Directory option has been specified + if args.dir: + if not ev.is_env_dir(args.mv_from): + tty.die("The specified path does not correspond to a valid spack environment") + from_path = Path(args.mv_from) + if not args.force: + if ev.is_env_dir(args.mv_to): + tty.die( + "The new path corresponds to an existing environment;" + " specify the --force flag to overwrite it." + ) + if Path(args.mv_to).exists(): + tty.die("The new path already exists; specify the --force flag to overwrite it.") + to_path = Path(args.mv_to) + + # Name option being used + elif ev.exists(args.mv_from): + from_path = ev.environment.environment_dir_from_name(args.mv_from) + if not args.force and ev.exists(args.mv_to): + tty.die( + "The new name corresponds to an existing environment;" + " specify the --force flag to overwrite it." + ) + to_path = ev.environment.root(args.mv_to) + + # Neither + else: + tty.die("The specified name does not correspond to a managed spack environment") + + # Guard against renaming from or to an active environment + active_env = ev.active_environment() + if active_env: + from_env = ev.Environment(from_path) + if from_env.path == active_env.path: + tty.die("Cannot rename active environment") + if to_path == active_env.path: + tty.die(f"{args.mv_to} is an active environment") + + shutil.rmtree(to_path, ignore_errors=True) + fs.rename(from_path, to_path) + tty.msg(f"Successfully renamed environment {args.mv_from} to {args.mv_to}") + + # # env list # def env_list_setup_parser(subparser): - """list available environments""" + """list managed environments""" def env_list(args): diff --git a/lib/spack/spack/test/cmd/env.py b/lib/spack/spack/test/cmd/env.py index aafca18544..0cf09421c0 100644 --- a/lib/spack/spack/test/cmd/env.py +++ b/lib/spack/spack/test/cmd/env.py @@ -188,6 +188,127 @@ def test_env_remove(capfd): assert "bar" not in out +def test_env_rename_managed(capfd): + # Need real environment + with pytest.raises(spack.main.SpackCommandError): + env("rename", "foo", "bar") + assert ( + "The specified name does not correspond to a managed spack environment" + in capfd.readouterr()[0] + ) + + env("create", "foo") + + out = env("list") + assert "foo" in out + + out = env("rename", "foo", "bar") + assert "Successfully renamed environment foo to bar" in out + + out = env("list") + assert "foo" not in out + assert "bar" in out + + bar = ev.read("bar") + with bar: + # Cannot rename active environment + with pytest.raises(spack.main.SpackCommandError): + env("rename", "bar", "baz") + assert "Cannot rename active environment" in capfd.readouterr()[0] + + env("create", "qux") + + # Cannot rename to an active environment (even with force flag) + with pytest.raises(spack.main.SpackCommandError): + env("rename", "-f", "qux", "bar") + assert "bar is an active environment" in capfd.readouterr()[0] + + # Can rename inactive environment when another's active + out = env("rename", "qux", "quux") + assert "Successfully renamed environment qux to quux" in out + + out = env("list") + assert "bar" in out + assert "baz" not in out + + env("create", "baz") + + # Cannot rename to existing environment without --force + with pytest.raises(spack.main.SpackCommandError): + env("rename", "bar", "baz") + errmsg = ( + "The new name corresponds to an existing environment;" + " specify the --force flag to overwrite it." + ) + assert errmsg in capfd.readouterr()[0] + + env("rename", "-f", "bar", "baz") + out = env("list") + assert "bar" not in out + assert "baz" in out + + +def test_env_rename_anonymous(capfd, tmpdir): + # Need real environment + with pytest.raises(spack.main.SpackCommandError): + env("rename", "-d", "./non-existing", "./also-non-existing") + assert ( + "The specified path does not correspond to a valid spack environment" + in capfd.readouterr()[0] + ) + + anon_foo = str(tmpdir / "foo") + env("create", "-d", anon_foo) + + anon_bar = str(tmpdir / "bar") + out = env("rename", "-d", anon_foo, anon_bar) + assert f"Successfully renamed environment {anon_foo} to {anon_bar}" in out + assert not ev.is_env_dir(anon_foo) + assert ev.is_env_dir(anon_bar) + + # Cannot rename active environment + anon_baz = str(tmpdir / "baz") + env("activate", "--sh", "-d", anon_bar) + with pytest.raises(spack.main.SpackCommandError): + env("rename", "-d", anon_bar, anon_baz) + assert "Cannot rename active environment" in capfd.readouterr()[0] + env("deactivate", "--sh") + + assert ev.is_env_dir(anon_bar) + assert not ev.is_env_dir(anon_baz) + + # Cannot rename to existing environment without --force + env("create", "-d", anon_baz) + with pytest.raises(spack.main.SpackCommandError): + env("rename", "-d", anon_bar, anon_baz) + errmsg = ( + "The new path corresponds to an existing environment;" + " specify the --force flag to overwrite it." + ) + assert errmsg in capfd.readouterr()[0] + assert ev.is_env_dir(anon_bar) + assert ev.is_env_dir(anon_baz) + + env("rename", "-f", "-d", anon_bar, anon_baz) + assert not ev.is_env_dir(anon_bar) + assert ev.is_env_dir(anon_baz) + + # Cannot rename to existing (non-environment) path without --force + qux = tmpdir / "qux" + qux.mkdir() + anon_qux = str(qux) + assert not ev.is_env_dir(anon_qux) + + with pytest.raises(spack.main.SpackCommandError): + env("rename", "-d", anon_baz, anon_qux) + errmsg = "The new path already exists; specify the --force flag to overwrite it." + assert errmsg in capfd.readouterr()[0] + + env("rename", "-f", "-d", anon_baz, anon_qux) + assert not ev.is_env_dir(anon_baz) + assert ev.is_env_dir(anon_qux) + + def test_concretize(): e = ev.create("test") e.add("mpileaks") @@ -3133,7 +3254,7 @@ def test_create_and_activate_managed(tmp_path): env("deactivate") -def test_create_and_activate_unmanaged(tmp_path): +def test_create_and_activate_anonymous(tmp_path): with fs.working_dir(str(tmp_path)): env_dir = os.path.join(str(tmp_path), "foo") shell = env("activate", "--without-view", "--create", "--sh", "-d", env_dir) diff --git a/share/spack/spack-completion.bash b/share/spack/spack-completion.bash index dc0d1987a6..e76f757f19 100755 --- a/share/spack/spack-completion.bash +++ b/share/spack/spack-completion.bash @@ -1032,7 +1032,7 @@ _spack_env() { then SPACK_COMPREPLY="-h --help" else - SPACK_COMPREPLY="activate deactivate create remove rm list ls status st loads view update revert depfile" + SPACK_COMPREPLY="activate deactivate create remove rm rename mv list ls status st loads view update revert depfile" fi } @@ -1076,6 +1076,24 @@ _spack_env_rm() { fi } +_spack_env_rename() { + if $list_options + then + SPACK_COMPREPLY="-h --help -d --dir -f --force" + else + SPACK_COMPREPLY="" + fi +} + +_spack_env_mv() { + if $list_options + then + SPACK_COMPREPLY="-h --help -d --dir -f --force" + else + SPACK_COMPREPLY="" + fi +} + _spack_env_list() { SPACK_COMPREPLY="-h --help" } diff --git a/share/spack/spack-completion.fish b/share/spack/spack-completion.fish index ff7c3b3ba1..a8ff367416 100755 --- a/share/spack/spack-completion.fish +++ b/share/spack/spack-completion.fish @@ -1472,8 +1472,10 @@ complete -c spack -n '__fish_spack_using_command_pos 0 env' -f -a deactivate -d complete -c spack -n '__fish_spack_using_command_pos 0 env' -f -a create -d 'create a new environment' complete -c spack -n '__fish_spack_using_command_pos 0 env' -f -a remove -d 'remove an existing environment' complete -c spack -n '__fish_spack_using_command_pos 0 env' -f -a rm -d 'remove an existing environment' -complete -c spack -n '__fish_spack_using_command_pos 0 env' -f -a list -d 'list available environments' -complete -c spack -n '__fish_spack_using_command_pos 0 env' -f -a ls -d 'list available environments' +complete -c spack -n '__fish_spack_using_command_pos 0 env' -f -a rename -d 'rename an existing environment' +complete -c spack -n '__fish_spack_using_command_pos 0 env' -f -a mv -d 'rename an existing environment' +complete -c spack -n '__fish_spack_using_command_pos 0 env' -f -a list -d 'list managed environments' +complete -c spack -n '__fish_spack_using_command_pos 0 env' -f -a ls -d 'list managed environments' complete -c spack -n '__fish_spack_using_command_pos 0 env' -f -a status -d 'print whether there is an active environment' complete -c spack -n '__fish_spack_using_command_pos 0 env' -f -a st -d 'print whether there is an active environment' complete -c spack -n '__fish_spack_using_command_pos 0 env' -f -a loads -d 'list modules for an installed environment \'(see spack module loads)\'' @@ -1561,6 +1563,26 @@ complete -c spack -n '__fish_spack_using_command env rm' -s h -l help -d 'show t complete -c spack -n '__fish_spack_using_command env rm' -s y -l yes-to-all -f -a yes_to_all complete -c spack -n '__fish_spack_using_command env rm' -s y -l yes-to-all -d 'assume "yes" is the answer to every confirmation request' +# spack env rename +set -g __fish_spack_optspecs_spack_env_rename h/help d/dir f/force + +complete -c spack -n '__fish_spack_using_command env rename' -s h -l help -f -a help +complete -c spack -n '__fish_spack_using_command env rename' -s h -l help -d 'show this help message and exit' +complete -c spack -n '__fish_spack_using_command env rename' -s d -l dir -f -a dir +complete -c spack -n '__fish_spack_using_command env rename' -s d -l dir -d 'the specified arguments correspond to directory paths' +complete -c spack -n '__fish_spack_using_command env rename' -s f -l force -f -a force +complete -c spack -n '__fish_spack_using_command env rename' -s f -l force -d 'allow overwriting of an existing environment' + +# spack env mv +set -g __fish_spack_optspecs_spack_env_mv h/help d/dir f/force + +complete -c spack -n '__fish_spack_using_command env mv' -s h -l help -f -a help +complete -c spack -n '__fish_spack_using_command env mv' -s h -l help -d 'show this help message and exit' +complete -c spack -n '__fish_spack_using_command env mv' -s d -l dir -f -a dir +complete -c spack -n '__fish_spack_using_command env mv' -s d -l dir -d 'the specified arguments correspond to directory paths' +complete -c spack -n '__fish_spack_using_command env mv' -s f -l force -f -a force +complete -c spack -n '__fish_spack_using_command env mv' -s f -l force -d 'allow overwriting of an existing environment' + # spack env list set -g __fish_spack_optspecs_spack_env_list h/help complete -c spack -n '__fish_spack_using_command env list' -s h -l help -f -a help