From b3b14179f521e7de1d495c003738f6375c6862ef Mon Sep 17 00:00:00 2001 From: Steve Yoo Date: Mon, 10 Aug 2026 11:07:51 -0400 Subject: [PATCH] Avoid PSModulePath inheritance during update on Windows --- .../next-release/bugfix-update-40840.json | 5 ++ awscli/customizations/update.py | 15 ++++-- tests/unit/customizations/test_update.py | 51 ++++++++++++++++++- 3 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 .changes/next-release/bugfix-update-40840.json diff --git a/.changes/next-release/bugfix-update-40840.json b/.changes/next-release/bugfix-update-40840.json new file mode 100644 index 000000000000..25643a4a8a3b --- /dev/null +++ b/.changes/next-release/bugfix-update-40840.json @@ -0,0 +1,5 @@ +{ + "type": "bugfix", + "category": "``update``", + "description": "Reset PSModulePath before launching new PowerShell console to avoid inheriting an incompatible PSModulePath" +} diff --git a/awscli/customizations/update.py b/awscli/customizations/update.py index 14949c32245c..df0e1d061ce8 100644 --- a/awscli/customizations/update.py +++ b/awscli/customizations/update.py @@ -211,6 +211,10 @@ def _do_update(self): f.write('set AWS_CLI_DISTRIBUTION_SOURCE_OVERRIDE=update-exe\n') if self._no_color: f.write('set NO_COLOR=1\n') + # Clear the inherited PSModulePath so the launched PowerShell + # rebuilds its own default. + # https://github.com/aws/aws-cli/issues/10532 + f.write('set PSModulePath=\n') f.write('ping -n 3 127.0.0.1 >nul 2>&1\n') f.write(f'"{ps_exe}" {ps_args}\n') @@ -228,10 +232,13 @@ def _run_install(self, cmd): ) def _find_powershell(self): - path = shutil.which('powershell') - if not path: - raise UpdateError('powershell.exe not found on PATH.') - return path + for name in ('powershell', 'pwsh'): + path = shutil.which(name) + if path: + return path + raise UpdateError( + 'Neither powershell.exe nor pwsh.exe was found on PATH.' + ) def _is_system_install(self, install_metadata): if 'script_install' in install_metadata: diff --git a/tests/unit/customizations/test_update.py b/tests/unit/customizations/test_update.py index 7e7246085afb..12a4b2c8171c 100644 --- a/tests/unit/customizations/test_update.py +++ b/tests/unit/customizations/test_update.py @@ -222,7 +222,14 @@ def test_resolves_symlink_before_checking_install_dir( class TestWindowsUpdateCommand: - def _command(self, install, elevated=True, runner=None, downloader=None): + def _command( + self, + install, + elevated=True, + runner=None, + downloader=None, + powershell_path='powershell.exe', + ): return WindowsUpdateCommand( mock_session(), source='exe', @@ -230,7 +237,7 @@ def _command(self, install, elevated=True, runner=None, downloader=None): downloader=downloader or mock.Mock(), is_elevated=elevated, runner=runner or mock.Mock(), - powershell_path='powershell.exe', + powershell_path=powershell_path, ) def _run(self, install, color='auto', **kwargs): @@ -275,6 +282,46 @@ def test_wrapper_sets_no_color_when_color_off(self): assert 'set NO_COLOR=1' in wrapper + def test_wrapper_clears_psmodulepath(self): + _, wrapper = self._run(USER_INSTALL) + + assert 'set PSModulePath=\n' in wrapper + + def test_prefers_windows_powershell_when_available(self, monkeypatch): + def which(name): + return { + 'powershell': 'C:\\powershell.exe', + 'pwsh': 'C:\\pwsh.exe', + }.get(name) + + monkeypatch.setattr(update_module.shutil, 'which', which) + + _, wrapper = self._run(USER_INSTALL, powershell_path=None) + + assert '"C:\\powershell.exe" -NoProfile' in wrapper + + def test_falls_back_to_pwsh_when_windows_powershell_missing( + self, monkeypatch + ): + def which(name): + return 'C:\\pwsh.exe' if name == 'pwsh' else None + + monkeypatch.setattr(update_module.shutil, 'which', which) + + _, wrapper = self._run(USER_INSTALL, powershell_path=None) + + assert '"C:\\pwsh.exe" -NoProfile' in wrapper + + def test_raises_when_no_powershell_found(self, monkeypatch): + def which(name): + return None + + monkeypatch.setattr(update_module.shutil, 'which', which) + command = self._command(USER_INSTALL, powershell_path=None) + + with pytest.raises(UpdateError, match='Neither powershell'): + command([], global_args()) + def test_system_install_requires_elevation(self): runner = mock.Mock() command = self._command(SYSTEM_INSTALL, elevated=False, runner=runner)