summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn W. Parent <45471568+johnwparent@users.noreply.github.com>2023-06-27 21:26:51 -0400
committerGitHub <noreply@github.com>2023-06-27 18:26:51 -0700
commit78f33bc002816dd690047a7a38077b6ac6617d44 (patch)
treed90f8290c6c5ef2e5f3af69e1373ec4cf318522f
parent25cc7344526f76df1e2c8d7eb0748bccbb8ace63 (diff)
downloadspack-78f33bc002816dd690047a7a38077b6ac6617d44.tar.gz
spack-78f33bc002816dd690047a7a38077b6ac6617d44.tar.bz2
spack-78f33bc002816dd690047a7a38077b6ac6617d44.tar.xz
spack-78f33bc002816dd690047a7a38077b6ac6617d44.zip
Windows: Add PowerShell env support (#37951)
PowerShell requires explicit shell and env support in Spack. This is due to the distinct differences in shell interactions between cmd and pwsh. Add a doskey in pwsh piping 'spack' commands to a powershell script similar to the sh function 'spack'. Add support for PowerShell-specific shell interactions from Spack (set/unset shell variables).
-rw-r--r--bin/spack.bat2
-rw-r--r--bin/spack.ps1132
-rw-r--r--lib/spack/spack/cmd/common/__init__.py6
-rw-r--r--lib/spack/spack/cmd/env.py7
-rw-r--r--lib/spack/spack/environment/shell.py4
-rw-r--r--lib/spack/spack/test/cmd/build_env.py7
-rw-r--r--lib/spack/spack/test/conftest.py18
-rw-r--r--lib/spack/spack/test/util/environment.py22
-rw-r--r--lib/spack/spack/util/environment.py8
-rw-r--r--share/spack/setup-env.ps14
-rwxr-xr-xshare/spack/spack-completion.bash2
11 files changed, 197 insertions, 15 deletions
diff --git a/bin/spack.bat b/bin/spack.bat
index 514c2706d7..9aff863a90 100644
--- a/bin/spack.bat
+++ b/bin/spack.bat
@@ -214,7 +214,7 @@ goto :end_switch
if defined _sp_args (
if NOT "%_sp_args%"=="%_sp_args:--help=%" (
goto :default_case
- ) else if NOT "%_sp_args%"=="%_sp_args: -h=%" (
+ ) else if NOT "%_sp_args%"=="%_sp_args:-h=%" (
goto :default_case
) else if NOT "%_sp_args%"=="%_sp_args:--bat=%" (
goto :default_case
diff --git a/bin/spack.ps1 b/bin/spack.ps1
new file mode 100644
index 0000000000..39fe0167ca
--- /dev/null
+++ b/bin/spack.ps1
@@ -0,0 +1,132 @@
+# Copyright 2013-2023 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)
+# #######################################################################
+
+function Compare-CommonArgs {
+ $CMDArgs = $args[0]
+ # These aruments take precedence and call for no futher parsing of arguments
+ # invoke actual Spack entrypoint with that context and exit after
+ "--help", "-h", "--version", "-V" | ForEach-Object {
+ $arg_opt = $_
+ if(($CMDArgs) -and ([bool]($CMDArgs.Where({$_ -eq $arg_opt})))) {
+ return $true
+ }
+ }
+ return $false
+}
+
+function Read-SpackArgs {
+ $SpackCMD_params = @()
+ $SpackSubCommand = $NULL
+ $SpackSubCommandArgs = @()
+ $args_ = $args[0]
+ $args_ | ForEach-Object {
+ if (!$SpackSubCommand) {
+ if($_.SubString(0,1) -eq "-")
+ {
+ $SpackCMD_params += $_
+ }
+ else{
+ $SpackSubCommand = $_
+ }
+ }
+ else{
+ $SpackSubCommandArgs += $_
+ }
+ }
+ return $SpackCMD_params, $SpackSubCommand, $SpackSubCommandArgs
+}
+
+function Invoke-SpackCD {
+ if (Compare-CommonArgs $SpackSubCommandArgs) {
+ python $Env:SPACK_ROOT/bin/spack cd -h
+ }
+ else {
+ $LOC = $(python $Env:SPACK_ROOT/bin/spack location $SpackSubCommandArgs)
+ if (($NULL -ne $LOC)){
+ if ( Test-Path -Path $LOC){
+ Set-Location $LOC
+ }
+ else{
+ exit 1
+ }
+ }
+ else {
+ exit 1
+ }
+ }
+}
+
+function Invoke-SpackEnv {
+ if (Compare-CommonArgs $SpackSubCommandArgs[0]) {
+ python $Env:SPACK_ROOT/bin/spack env -h
+ }
+ else {
+ $SubCommandSubCommand = $SpackSubCommandArgs[0]
+ $SubCommandSubCommandArgs = $SpackSubCommandArgs[1..$SpackSubCommandArgs.Count]
+ switch ($SubCommandSubCommand) {
+ "activate" {
+ if (Compare-CommonArgs $SubCommandSubCommandArgs) {
+ python $Env:SPACK_ROOT/bin/spack env activate $SubCommandSubCommandArgs
+ }
+ elseif ([bool]($SubCommandSubCommandArgs.Where({$_ -eq "--pwsh"}))) {
+ python $Env:SPACK_ROOT/bin/spack env activate $SubCommandSubCommandArgs
+ }
+ elseif (!$SubCommandSubCommandArgs) {
+ python $Env:SPACK_ROOT/bin/spack env activate $SubCommandSubCommandArgs
+ }
+ else {
+ $SpackEnv = $(python $Env:SPACK_ROOT/bin/spack $SpackCMD_params env activate "--pwsh" $SubCommandSubCommandArgs)
+ $ExecutionContext.InvokeCommand($SpackEnv)
+ }
+ }
+ "deactivate" {
+ if ([bool]($SubCommandSubCommandArgs.Where({$_ -eq "--pwsh"}))) {
+ python $Env:SPACK_ROOT/bin/spack env deactivate $SubCommandSubCommandArgs
+ }
+ elseif($SubCommandSubCommandArgs) {
+ python $Env:SPACK_ROOT/bin/spack env deactivate -h
+ }
+ else {
+ $SpackEnv = $(python $Env:SPACK_ROOT/bin/spack $SpackCMD_params env deactivate --pwsh)
+ $ExecutionContext.InvokeCommand($SpackEnv)
+ }
+ }
+ default {python $Env:SPACK_ROOT/bin/spack $SpackCMD_params $SpackSubCommand $SpackSubCommandArgs}
+ }
+ }
+}
+
+function Invoke-SpackLoad {
+ if (Compare-CommonArgs $SpackSubCommandArgs) {
+ python $Env:SPACK_ROOT/bin/spack $SpackCMD_params $SpackSubCommand $SpackSubCommandArgs
+ }
+ elseif ([bool]($SpackSubCommandArgs.Where({($_ -eq "--pwsh") -or ($_ -eq "--list")}))) {
+ python $Env:SPACK_ROOT/bin/spack $SpackCMD_params $SpackSubCommand $SpackSubCommandArgs
+ }
+ else {
+ $SpackEnv = $(python $Env:SPACK_ROOT/bin/spack $SpackCMD_params $SpackSubCommand "--pwsh" $SpackSubCommandArgs)
+ $ExecutionContext.InvokeCommand($SpackEnv)
+ }
+}
+
+
+$SpackCMD_params, $SpackSubCommand, $SpackSubCommandArgs = Read-SpackArgs $args
+
+if (Compare-CommonArgs $SpackCMD_params) {
+ python $Env:SPACK_ROOT/bin/spack $SpackCMD_params $SpackSubCommand $SpackSubCommandArgs
+ exit $LASTEXITCODE
+}
+
+# Process Spack commands with special conditions
+# all other commands are piped directly to Spack
+switch($SpackSubCommand)
+{
+ "cd" {Invoke-SpackCD}
+ "env" {Invoke-SpackEnv}
+ "load" {Invoke-SpackLoad}
+ "unload" {Invoke-SpackLoad}
+ default {python $Env:SPACK_ROOT/bin/spack $SpackCMD_params $SpackSubCommand $SpackSubCommandArgs}
+}
diff --git a/lib/spack/spack/cmd/common/__init__.py b/lib/spack/spack/cmd/common/__init__.py
index 28f4f291a2..51329a1b6c 100644
--- a/lib/spack/spack/cmd/common/__init__.py
+++ b/lib/spack/spack/cmd/common/__init__.py
@@ -36,7 +36,10 @@ def shell_init_instructions(cmd, equivalent):
" source %s/setup-env.fish" % spack.paths.share_path,
"",
color.colorize("@*c{For Windows batch:}"),
- " source %s/spack_cmd.bat" % spack.paths.share_path,
+ " %s\\spack_cmd.bat" % spack.paths.bin_path,
+ "",
+ color.colorize("@*c{For PowerShell:}"),
+ " %s\\setup-env.ps1" % spack.paths.share_path,
"",
"Or, if you do not want to use shell support, run "
+ ("one of these" if shell_specific else "this")
@@ -50,6 +53,7 @@ def shell_init_instructions(cmd, equivalent):
equivalent.format(sh_arg="--csh ") + " # csh/tcsh",
equivalent.format(sh_arg="--fish") + " # fish",
equivalent.format(sh_arg="--bat ") + " # batch",
+ equivalent.format(sh_arg="--pwsh") + " # powershell",
]
else:
msg += [" " + equivalent]
diff --git a/lib/spack/spack/cmd/env.py b/lib/spack/spack/cmd/env.py
index 6774b15afd..93e22031d8 100644
--- a/lib/spack/spack/cmd/env.py
+++ b/lib/spack/spack/cmd/env.py
@@ -86,6 +86,13 @@ def env_activate_setup_parser(subparser):
const="bat",
help="print bat commands to activate the environment",
)
+ shells.add_argument(
+ "--pwsh",
+ action="store_const",
+ dest="shell",
+ const="pwsh",
+ help="print powershell commands to activate environment",
+ )
view_options = subparser.add_mutually_exclusive_group()
view_options.add_argument(
diff --git a/lib/spack/spack/environment/shell.py b/lib/spack/spack/environment/shell.py
index 4df66ac28c..c6d2b06c98 100644
--- a/lib/spack/spack/environment/shell.py
+++ b/lib/spack/spack/environment/shell.py
@@ -42,6 +42,8 @@ def activate_header(env, shell, prompt=None):
cmds += 'set "SPACK_ENV=%s"\n' % env.path
# TODO: despacktivate
# TODO: prompt
+ elif shell == "pwsh":
+ cmds += "$Env:SPACK_ENV=%s\n" % env.path
else:
if "color" in os.getenv("TERM", "") and prompt:
prompt = colorize("@G{%s}" % prompt, color=True, enclose=True)
@@ -79,6 +81,8 @@ def deactivate_header(shell):
cmds += 'set "SPACK_ENV="\n'
# TODO: despacktivate
# TODO: prompt
+ elif shell == "pwsh":
+ cmds += "Remove-Item Env:SPACK_ENV"
else:
cmds += "if [ ! -z ${SPACK_ENV+x} ]; then\n"
cmds += "unset SPACK_ENV; export SPACK_ENV;\n"
diff --git a/lib/spack/spack/test/cmd/build_env.py b/lib/spack/spack/test/cmd/build_env.py
index a03d9760bf..c27b292cb1 100644
--- a/lib/spack/spack/test/cmd/build_env.py
+++ b/lib/spack/spack/test/cmd/build_env.py
@@ -35,12 +35,15 @@ def test_build_env_requires_a_spec(args):
_out_file = "env.out"
+@pytest.mark.parametrize("shell", ["pwsh", "bat"] if sys.platform == "win32" else ["bash"])
@pytest.mark.usefixtures("config", "mock_packages", "working_env")
-def test_dump(tmpdir):
+def test_dump(shell_as, shell, tmpdir):
with tmpdir.as_cwd():
build_env("--dump", _out_file, "zlib")
with open(_out_file) as f:
- if sys.platform == "win32":
+ if shell == "pwsh":
+ assert any(line.startswith("$Env:PATH") for line in f.readlines())
+ elif shell == "bat":
assert any(line.startswith('set "PATH=') for line in f.readlines())
else:
assert any(line.startswith("PATH=") for line in f.readlines())
diff --git a/lib/spack/spack/test/conftest.py b/lib/spack/spack/test/conftest.py
index f0e5f7fdb2..c97a830bbe 100644
--- a/lib/spack/spack/test/conftest.py
+++ b/lib/spack/spack/test/conftest.py
@@ -1920,3 +1920,21 @@ def default_mock_concretization(config, mock_packages, concretized_specs_cache):
return concretized_specs_cache[key].copy()
return _func
+
+
+@pytest.fixture
+def shell_as(shell):
+ if sys.platform != "win32":
+ yield
+ return
+ if shell not in ("pwsh", "bat"):
+ raise RuntimeError("Shell must be one of supported Windows shells (pwsh|bat)")
+ try:
+ # fetch and store old shell type
+ _shell = os.environ.get("SPACK_SHELL", None)
+ os.environ["SPACK_SHELL"] = shell
+ yield
+ finally:
+ # restore old shell if one was set
+ if _shell:
+ os.environ["SPACK_SHELL"] = _shell
diff --git a/lib/spack/spack/test/util/environment.py b/lib/spack/spack/test/util/environment.py
index 801c2d19f4..481a58db47 100644
--- a/lib/spack/spack/test/util/environment.py
+++ b/lib/spack/spack/test/util/environment.py
@@ -113,13 +113,16 @@ def test_path_put_first(prepare_environment_for_tests):
assert envutil.get_path("TEST_ENV_VAR") == expected
-def test_dump_environment(prepare_environment_for_tests, tmpdir):
+@pytest.mark.parametrize("shell", ["pwsh", "bat"] if sys.platform == "win32" else ["bash"])
+def test_dump_environment(prepare_environment_for_tests, shell_as, shell, tmpdir):
test_paths = "/a:/b/x:/b/c"
os.environ["TEST_ENV_VAR"] = test_paths
dumpfile_path = str(tmpdir.join("envdump.txt"))
envutil.dump_environment(dumpfile_path)
with open(dumpfile_path, "r") as dumpfile:
- if sys.platform == "win32":
+ if shell == "pwsh":
+ assert "$Env:TEST_ENV_VAR={}\n".format(test_paths) in list(dumpfile)
+ elif shell == "bat":
assert 'set "TEST_ENV_VAR={}"\n'.format(test_paths) in list(dumpfile)
else:
assert "TEST_ENV_VAR={0}; export TEST_ENV_VAR\n".format(test_paths) in list(dumpfile)
@@ -164,11 +167,14 @@ def test_escape_double_quotes_in_shell_modifications():
to_validate.set("QUOTED_VAR", '"MY_VAL"')
- cmds = to_validate.shell_modifications()
-
- if sys.platform != "win32":
+ if sys.platform == "win32":
+ cmds = to_validate.shell_modifications(shell="bat")
+ assert r'set "VAR=$PATH;$ANOTHER_PATH"' in cmds
+ assert r'set "QUOTED_VAR="MY_VAL"' in cmds
+ cmds = to_validate.shell_modifications(shell="pwsh")
+ assert r"$Env:VAR=$PATH;$ANOTHER_PATH" in cmds
+ assert r'$Env:QUOTED_VAR="MY_VAL"' in cmds
+ else:
+ cmds = to_validate.shell_modifications()
assert 'export VAR="$PATH:$ANOTHER_PATH"' in cmds
assert r'export QUOTED_VAR="\"MY_VAL\""' in cmds
- else:
- assert "export VAR=$PATH;$ANOTHER_PATH" in cmds
- assert r'export QUOTED_VAR="MY_VAL"' in cmds
diff --git a/lib/spack/spack/util/environment.py b/lib/spack/spack/util/environment.py
index 3c63543551..da6832e4dd 100644
--- a/lib/spack/spack/util/environment.py
+++ b/lib/spack/spack/util/environment.py
@@ -47,6 +47,7 @@ _SHELL_SET_STRINGS = {
"csh": "setenv {0} {1};\n",
"fish": "set -gx {0} {1};\n",
"bat": 'set "{0}={1}"\n',
+ "pwsh": "$Env:{0}={1}\n",
}
@@ -55,6 +56,7 @@ _SHELL_UNSET_STRINGS = {
"csh": "unsetenv {0};\n",
"fish": "set -e {0};\n",
"bat": 'set "{0}="\n',
+ "pwsh": "Remove-Item Env:{0}\n",
}
@@ -172,7 +174,9 @@ BASH_FUNCTION_FINDER = re.compile(r"BASH_FUNC_(.*?)\(\)")
def _win_env_var_to_set_line(var: str, val: str) -> str:
- return f'set "{var}={val}"'
+ is_pwsh = os.environ.get("SPACK_SHELL", None) == "pwsh"
+ env_set_phrase = f"$Env:{var}={val}" if is_pwsh else f'set "{var}={val}"'
+ return env_set_phrase
def _nix_env_var_to_source_line(var: str, val: str) -> str:
@@ -693,7 +697,7 @@ class EnvironmentModifications:
def shell_modifications(
self,
- shell: str = "sh",
+ shell: str = "sh" if sys.platform != "win32" else os.environ.get("SPACK_SHELL", "bat"),
explicit: bool = False,
env: Optional[MutableMapping[str, str]] = None,
) -> str:
diff --git a/share/spack/setup-env.ps1 b/share/spack/setup-env.ps1
index edf9613ec5..d3bed93d9b 100644
--- a/share/spack/setup-env.ps1
+++ b/share/spack/setup-env.ps1
@@ -46,6 +46,10 @@ if ($null -eq $Env:EDITOR)
$Env:EDITOR = "notepad"
}
+# Set spack shell so we can detect powershell context
+$Env:SPACK_SHELL="pwsh"
+
+doskey /exename=powershell.exe spack=$Env:SPACK_ROOT\bin\spack.ps1 $args
Write-Output "*****************************************************************"
Write-Output "**************** Spack Package Manager **************************"
diff --git a/share/spack/spack-completion.bash b/share/spack/spack-completion.bash
index abf41cb052..2e98698e0f 100755
--- a/share/spack/spack-completion.bash
+++ b/share/spack/spack-completion.bash
@@ -935,7 +935,7 @@ _spack_env() {
_spack_env_activate() {
if $list_options
then
- SPACK_COMPREPLY="-h --help --sh --csh --fish --bat -v --with-view -V --without-view -p --prompt --temp -d --dir"
+ SPACK_COMPREPLY="-h --help --sh --csh --fish --bat --pwsh -v --with-view -V --without-view -p --prompt --temp -d --dir"
else
_environments
fi