Skip to content

Commit 6cdb847

Browse files
authored
PowerShell: Fix assumption that $LASTEXITCODE is always defined (#1962)
Signed-off-by: Nathan Rusch <[email protected]>
1 parent fe784a5 commit 6cdb847

File tree

6 files changed

+79
-13
lines changed

6 files changed

+79
-13
lines changed

src/rez/bind/hello_world.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,25 +47,31 @@ def bind(path, version_range=None, opts=None, parser=None):
4747
def make_root(variant, root):
4848
binpath = make_dirs(root, "bin")
4949
binpathtmp = make_dirs(root, "bintmp")
50-
filepath = os.path.join(binpathtmp, "hello_world")
5150

5251
create_executable_script(
53-
filepath,
52+
os.path.join(binpathtmp, "hello_world"),
5453
hello_world_source,
5554
py_script_mode=ExecutableScriptMode.single,
5655
)
56+
create_executable_script(
57+
os.path.join(binpathtmp, "hello_world_gui"),
58+
hello_world_source,
59+
program="pythonw",
60+
py_script_mode=ExecutableScriptMode.single,
61+
)
5762

5863
# We want to use ScriptMaker on all platofrms. This allows us to
5964
# correctly setup the script to work everywhere, even on Windows.
6065
# create_executable_script should be fixed to use ScriptMaker
6166
# instead.
6267
maker = ScriptMaker(binpathtmp, make_dirs(binpath))
6368
maker.make("hello_world")
69+
maker.make("hello_world_gui")
6470
shutil.rmtree(binpathtmp)
6571

6672
with make_package("hello_world", path, make_root=make_root) as pkg:
6773
pkg.version = version
68-
pkg.tools = ["hello_world"]
74+
pkg.tools = ["hello_world", "hello_world_gui"]
6975
pkg.commands = commands
7076

7177
return pkg.installed_variants

src/rez/data/tests/builds/packages/whack/package.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@
44
uuid = "4e9f63cbc4794453b0031f0c5ff50759"
55
description = "a deliberately broken package"
66

7+
private_build_requires = ["python"]
8+
79
build_command = "python {root}/build.py {install}"

src/rez/tests/test_build.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ def test_build_whack(self, shell):
141141
"""Test that a broken build fails correctly.
142142
"""
143143
config.override("default_shell", shell)
144+
self.inject_python_repo()
144145

145146
working_dir = os.path.join(self.src_root, "whack")
146147
builder = self._create_builder(working_dir)

src/rez/tests/test_shells.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
from rez.rex import literal, expandable
1212
from rez.plugin_managers import plugin_manager
1313
from rez.utils.execution import ExecutableScriptMode, _get_python_script_files
14-
from rez.tests.util import TestBase, TempdirMixin, per_available_shell, \
15-
install_dependent
14+
from rez.utils.platform_ import platform_
15+
from rez.tests.util import TestBase, TempdirMixin, get_available_shells, \
16+
per_available_shell, install_dependent
1617
from rez.bind import hello_world
1718
from rez.config import config
1819
import unittest
@@ -252,6 +253,39 @@ def test_command_returncode(self, shell):
252253
p.wait()
253254
self.assertEqual(p.returncode, 66)
254255

256+
@unittest.skipIf(platform_.name != "windows", "GUI entrypoint test is only relevant on Windows")
257+
@unittest.skipIf("pwsh" not in get_available_shells(), "PowerShell unavailable or disabled")
258+
def test_pwsh_lastexitcode_gui(self):
259+
"""This validates some semi-unintuitive behavior on Windows, where GUI applications
260+
will "return" immediately without any exit status when launched from a shell.
261+
"""
262+
sh = create_shell("pwsh")
263+
_, _, _, command = sh.startup_capabilities(command=True)
264+
265+
if command:
266+
def actions_callback(ex):
267+
"""Action callback to enable PowerShell's "strict" mode."""
268+
ex.command("Set-StrictMode -version Latest")
269+
270+
r = self._create_context(["hello_world"])
271+
command = "hello_world -q -r 66"
272+
commands = (command, command.split())
273+
for cmd in commands:
274+
with r.execute_shell(shell="pwsh", command=cmd, actions_callback=actions_callback,
275+
stdout=subprocess.PIPE) as p:
276+
p.wait()
277+
self.assertEqual(p.returncode, 66)
278+
279+
command = "hello_world_gui -q -r 49"
280+
commands = (command, command.split())
281+
for cmd in commands:
282+
with r.execute_shell(shell="pwsh", command=cmd, actions_callback=actions_callback,
283+
stdout=subprocess.PIPE) as p:
284+
p.wait()
285+
# The GUI application should return control to the shell immediately, and that
286+
# should bubble up through the rez shell as a 0 exit status.
287+
self.assertEqual(p.returncode, 0)
288+
255289
@per_available_shell()
256290
def test_norc(self, shell):
257291
sh = create_shell(shell)

src/rez/tests/util.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,21 @@ def wrapper(self, *args, **kwargs):
203203
return decorator
204204

205205

206+
def get_available_shells():
207+
"""Helper to get all available shells in a testing context."""
208+
shells = get_shell_types()
209+
210+
only_shell = os.getenv("__REZ_SELFTEST_SHELL")
211+
if only_shell:
212+
shells = [only_shell]
213+
214+
# filter to only those shells available
215+
return [
216+
x for x in shells
217+
if get_shell_class(x).is_available()
218+
]
219+
220+
206221
def per_available_shell(exclude=None):
207222
"""Function decorator that runs the function over all available shell types.
208223
"""

src/rezplugins/shell/_utils/powershell_base.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,17 +146,25 @@ def _record_shell(ex, files, bind_rez=True, print_msg=False):
146146
if shell_command:
147147
executor.command(shell_command)
148148

149-
# Forward exit call to parent PowerShell process
149+
# Translate the status of the most recent command into an exit code.
150150
#
151-
# Note that in powershell, $LASTEXITCODE is only set after running an
152-
# executable - in other cases (such as when a command is not found),
153-
# only the bool $? var is set.
151+
# Note that in PowerShell, `$LASTEXITCODE` is only set after calling a
152+
# native command (i.e. an executable), or another script that uses the
153+
# `exit` keyword. Otherwise, only the boolean `$?` variable is set (to
154+
# True if the last command succeeded and False if it failed).
155+
# See https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables # noqa
156+
#
157+
# Additionally, if PowerShell is running in strict mode, references to
158+
# uninitialized variables will error instead of simply returning 0 or
159+
# `$null`, so we use `Test-Path` here to verify that `$LASTEXITCODE` has
160+
# been set before using it.
161+
# See https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/set-strictmode?view=powershell-7.5#description # noqa
154162
#
155163
executor.command(
156-
"if(! $? -or $LASTEXITCODE) {\n"
157-
" if ($LASTEXITCODE) {\n"
158-
" exit $LASTEXITCODE\n"
159-
" }\n"
164+
"if ((Test-Path variable:LASTEXITCODE) -and $LASTEXITCODE) {\n"
165+
" exit $LASTEXITCODE\n"
166+
"}\n"
167+
"if (! $?) {\n"
160168
" exit 1\n"
161169
"}"
162170
)

0 commit comments

Comments
 (0)