diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 7bbfd7565..9557321f7 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -104,6 +104,8 @@ ${Using:StrFunc} StrStr var /global INIT_CONDA var /global REG_PY +var /global NO_REGISTRY + var /global INSTDIR_JUSTME var /global INSTALLER_VERSION var /global INSTALLER_NAME_FULL @@ -262,8 +264,7 @@ FunctionEnd Function InitializeVariables StrCpy $CheckPathLength "{{ 1 if check_path_length else 0 }}" - StrCpy $ARGV_NoRegistry "0" - StrCpy $ARGV_KeepPkgCache "{{ 1 if keep_pkgs else 0 }}" + StrCpy $NO_REGISTRY "0" # Package cache option StrCpy $Ana_ClearPkgCache_State {{ '${BST_UNCHECKED}' if keep_pkgs else '${BST_CHECKED}' }} @@ -425,9 +426,23 @@ FunctionEnd ClearErrors ${GetOptions} $ARGV "/KeepPkgCache=" $ARGV_KeepPkgCache + ${IfNot} ${Errors} + ${If} $ARGV_KeepPkgCache = "1" + StrCpy $Ana_ClearPkgCache_State ${BST_UNCHECKED} + ${ElseIf} $ARGV_KeepPkgCache = "0" + StrCpy $Ana_ClearPkgCache_State ${BST_CHECKED} + ${EndIf} + ${EndIf} ClearErrors ${GetOptions} $ARGV "/NoRegistry=" $ARGV_NoRegistry + ${IfNot} ${Errors} + ${If} $ARGV_NoRegistry = "1" + StrCpy $NO_REGISTRY "1" + ${ElseIf} $ARGV_NoRegistry = "0" + StrCpy $NO_REGISTRY "0" + ${EndIf} + ${EndIf} ClearErrors ${GetOptions} $ARGV "/NoScripts=" $ARGV_NoScripts @@ -1055,7 +1070,7 @@ Function OnDirectoryLeave ; With windows 10, we can enable support for long path, for earlier ; version, suggest user to use shorter installation path ${If} ${AtLeastWin10} - ${AndIfNot} $ARGV_NoRegistry = "1" + ${AndIfNot} $NO_REGISTRY = "1" ; If we have admin right, we enable long path on windows ${If} ${UAC_IsAdmin} WriteRegDWORD HKLM "SYSTEM\CurrentControlSet\Control\FileSystem" "LongPathsEnabled" 1 @@ -1619,7 +1634,7 @@ Section "Install" ${EndIf} {%- endif %} - ${If} $ARGV_NoRegistry == "0" + ${If} $NO_REGISTRY == "0" # Registry uninstall info WriteRegStr SHCTX "${UNINSTREG}" "DisplayName" "${UNINSTALL_NAME}" WriteRegStr SHCTX "${UNINSTREG}" "DisplayVersion" "${VERSION}" diff --git a/news/1132-fix-cli-args b/news/1132-fix-cli-args new file mode 100644 index 000000000..9aae82d89 --- /dev/null +++ b/news/1132-fix-cli-args @@ -0,0 +1,19 @@ +### Enhancements + +* + +### Bug fixes + +* EXE: Fixed an issue for silent installers where command-line argument `/KeepPkgCache` was ignored and `/NoRegistry` would reset the default value. (#1132) + +### Deprecations + +* + +### Docs + +* + +### Other + +* diff --git a/tests/test_examples.py b/tests/test_examples.py index 3fc38104f..2242f9187 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -34,6 +34,8 @@ if sys.platform == "darwin": from constructor.osxpkg import calculate_install_dir elif sys.platform.startswith("win"): + import winreg + import ntsecuritycon as con import win32security @@ -60,6 +62,49 @@ KEEP_ARTIFACTS_PATH = None +def _is_program_installed(partial_name: str) -> bool: + """ + Checks if a program is listed in the Windows 'Installed apps' menu. + We search by looking for a partial name to avoid having to account for Python version and arch. + Returns True if a match is found, otherwise False. + """ + + if not sys.platform.startswith("win"): + return False + + # For its current purpose HKEY_CURRENT_USER is sufficient, + # but additional registry locations could be added later. + UNINSTALL_PATHS = [ + (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), + ] + + partial_name = partial_name.lower() + + for hive, path in UNINSTALL_PATHS: + try: + reg_key = winreg.OpenKey(hive, path) + except FileNotFoundError: + continue + + subkey_count = winreg.QueryInfoKey(reg_key)[0] + + for i in range(subkey_count): + try: + subkey_name = winreg.EnumKey(reg_key, i) + subkey = winreg.OpenKey(reg_key, subkey_name) + + display_name, _ = winreg.QueryValueEx(subkey, "DisplayName") + + if partial_name in display_name.lower(): + return True + + except (FileNotFoundError, OSError, TypeError): + # Some keys may lack DisplayName or have unexpected value types + continue + + return False + + def _execute( cmd: Iterable[str], installer_input=None, check=True, timeout=420, **env_vars ) -> subprocess.CompletedProcess: @@ -1038,8 +1083,6 @@ def test_initialization(tmp_path, request, monkeypatch, method): ) if installer.suffix == ".exe": try: - import winreg - paths = [] for root, keyname in ( (winreg.HKEY_CURRENT_USER, r"Environment"), @@ -1423,3 +1466,39 @@ def test_regressions(tmp_path, request): check_subprocess=True, uninstall=True, ) + + +@pytest.mark.parametrize("no_registry", (0, 1)) +@pytest.mark.skipif(not ON_CI, reason="CI only") +@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only") +def test_not_in_installed_menu_list_(tmp_path, request, no_registry): + """Verify the app is in the Installed Apps Menu (or not), based on the CLI arg '/NoRegistry'. + If NoRegistry=0, we expect to find the installer in the Menu, otherwise not. + """ + input_path = _example_path("extra_files") # The specific example we use here is not important + options = ["/InstallationType=JustMe", f"/NoRegistry={no_registry}"] + for installer, install_dir in create_installer(input_path, tmp_path): + _run_installer( + input_path, + installer, + install_dir, + request=request, + check_subprocess=True, + uninstall=False, + options=options, + ) + + # Use the installer file name for the registry search + installer_file_name_parts = Path(installer).name.split("-") + name = installer_file_name_parts[0] + version = installer_file_name_parts[1] + partial_name = f"{name} {version}" + + is_in_installed_apps_menu = _is_program_installed(partial_name) + _run_uninstaller_exe(install_dir) + + # If no_registry=0 we expect is_in_installed_apps_menu=True + # If no_registry=1 we expect is_in_installed_apps_menu=False + assert is_in_installed_apps_menu == (no_registry == 0), ( + f"Unable to find program '{partial_name}' in the 'Installed apps' menu" + )