diff --git a/_msbuild.py b/_msbuild.py index d0dfbe6..2d183ab 100644 --- a/_msbuild.py +++ b/_msbuild.py @@ -107,6 +107,7 @@ def main_exe(name): ConfigurationType='Application', ) + def mainw_exe(name): return CProject(name, VersionInfo(FileDescription="Python Install Manager (windowed)"), @@ -127,6 +128,26 @@ def mainw_exe(name): ) +def launcher_exe(name, platform, windowed=False): + return CProject(name, + VersionInfo( + FileDescription="Python launcher" + (" (windowed)" if windowed else ""), + OriginalFilename=f"{name}.exe" + ), + CPP_SETTINGS, + Property('StaticLibcppLinkage', 'true'), + ItemDefinition('Link', SubSystem='WINDOWS' if windowed else 'CONSOLE'), + Manifest('default.manifest'), + ResourceFile('pywicon.rc' if windowed else 'pyicon.rc'), + CSourceFile('launcher.cpp'), + CSourceFile('_launch.cpp'), + IncludeFile('*.h'), + source='src/pymanager', + ConfigurationType='Application', + Platform=platform, + ) + + PACKAGE = Package('python-manager', PyprojectTomlFile('pyproject.toml'), # MSIX manifest @@ -150,32 +171,12 @@ def mainw_exe(name): Package( 'templates', File('src/pymanager/templates/template.py'), - CProject('launcher', - VersionInfo(FileDescription="Python launcher", OriginalFilename="launcher.exe"), - CPP_SETTINGS, - Property('StaticLibcppLinkage', 'true'), - ItemDefinition('Link', SubSystem='CONSOLE'), - Manifest('default.manifest'), - ResourceFile('pyicon.rc'), - CSourceFile('launcher.cpp'), - CSourceFile('_launch.cpp'), - IncludeFile('*.h'), - source='src/pymanager', - ConfigurationType='Application', - ), - CProject('launcherw', - VersionInfo(FileDescription="Python launcher (windowed)", OriginalFilename="launcherw.exe"), - CPP_SETTINGS, - Property('StaticLibcppLinkage', 'true'), - ItemDefinition('Link', SubSystem='WINDOWS'), - Manifest('default.manifest'), - ResourceFile('pywicon.rc'), - CSourceFile('launcher.cpp'), - CSourceFile('_launch.cpp'), - IncludeFile('*.h'), - source='src/pymanager', - ConfigurationType='Application', - ), + launcher_exe("launcher-64", "x64", windowed=False), + launcher_exe("launcherw-64", "x64", windowed=True), + launcher_exe("launcher-arm64", "ARM64", windowed=False), + launcher_exe("launcherw-arm64", "ARM64", windowed=True), + launcher_exe("launcher-32", "Win32", windowed=False), + launcher_exe("launcherw-32", "Win32", windowed=True), ), # Directory for MSIX resources diff --git a/src/manage/commands.py b/src/manage/commands.py index 083263b..4d13a82 100644 --- a/src/manage/commands.py +++ b/src/manage/commands.py @@ -274,7 +274,10 @@ def execute(self): # Default: Python "start_folder": (str, None), - # Overrides for launcher executables + # Overrides for launcher executables. Platform-specific versions will be + # chosen automatically by inserting the last hypenated part of the tag + # before the suffix, falling back on the default platform or '-64' and + # eventually the unmodified version. See install_command._write_alias(). # Default: .\launcher.exe and .\launcherw.exe "launcher_exe": (str, None, "path"), "launcherw_exe": (str, None, "path"), diff --git a/src/manage/install_command.py b/src/manage/install_command.py index f492085..0a1958a 100644 --- a/src/manage/install_command.py +++ b/src/manage/install_command.py @@ -215,13 +215,30 @@ def _calc(prefix, filename, calculate_dest=calculate_dest): LOGGER.debug("Attempted to overwrite: %s", dest) -def _write_alias(cmd, alias, target): +def _if_exists(launcher, plat): + plat_launcher = launcher.parent / f"{launcher.stem}{plat}{launcher.suffix}" + if plat_launcher.is_file(): + return plat_launcher + return launcher + + +def _write_alias(cmd, install, alias, target): p = (cmd.global_dir / alias["name"]) ensure_tree(p) unlink(p) launcher = cmd.launcher_exe if alias.get("windowed"): launcher = cmd.launcherw_exe or launcher + plat = install["tag"].rpartition("-")[-1] + if plat: + LOGGER.debug("Checking for launcher for platform -%s", plat) + launcher = _if_exists(launcher, f"-{plat}") + if not launcher.is_file(): + LOGGER.debug("Checking for launcher for default platform %s", cmd.default_platform) + launcher = _if_exists(launcher, cmd.default_platform) + if not launcher.is_file(): + LOGGER.debug("Checking for launcher for -64") + launcher = _if_exists(launcher, "-64") LOGGER.debug("Create %s linking to %s using %s", alias["name"], target, launcher) if not launcher or not launcher.is_file(): LOGGER.warn("Skipping %s alias because the launcher template was not found.", alias["name"]) @@ -281,7 +298,7 @@ def update_all_shortcuts(cmd, path_warning=True): if not target.is_file(): LOGGER.warn("Skipping alias '%s' because target '%s' does not exist", a["name"], a["target"]) continue - _write_alias(cmd, a, target) + _write_alias(cmd, i, a, target) alias_written.add(a["name"].casefold()) for s in i.get("shortcuts", ()): diff --git a/src/pymanager/_launch.cpp b/src/pymanager/_launch.cpp index e7ae2d3..6058338 100644 --- a/src/pymanager/_launch.cpp +++ b/src/pymanager/_launch.cpp @@ -14,7 +14,10 @@ ctrl_c_handler(DWORD code) static int dup_handle(HANDLE input, HANDLE *output) { - static HANDLE self = GetCurrentProcess(); + static HANDLE self = NULL; + if (self == NULL) { + self = GetCurrentProcess(); + } if (input == NULL || input == INVALID_HANDLE_VALUE) { *output = input; return 0; diff --git a/src/pymanager/launcher.cpp b/src/pymanager/launcher.cpp index 85e6371..c456c8f 100644 --- a/src/pymanager/launcher.cpp +++ b/src/pymanager/launcher.cpp @@ -117,7 +117,7 @@ get_executable(wchar_t *executable, unsigned int bufferSize) return HRESULT_FROM_WIN32(GetLastError()); } - wcscat_s(config, L".__target__"); + wcscat_s(config, MAXLEN, L".__target__"); HANDLE hFile = CreateFileW(config, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, NULL); @@ -150,7 +150,7 @@ try_load_python3_dll(const wchar_t *executable, unsigned int bufferSize, void ** return ERROR_DLL_LOAD_DISABLED; #else wchar_t directory[MAXLEN]; - wcscpy_s(directory, executable); + wcscpy_s(directory, MAXLEN, executable); wchar_t *sep = wcsrchr(directory, L'\\'); if (!sep) { return ERROR_RELATIVE_PATH; diff --git a/src/pymanager/main.cpp b/src/pymanager/main.cpp index 270d8d4..0277d0c 100644 --- a/src/pymanager/main.cpp +++ b/src/pymanager/main.cpp @@ -541,7 +541,8 @@ wmain(int argc, wchar_t **argv) if (err) { // Most 'not found' errors have been handled above. These are internal - fprintf(stderr, "INTERNAL ERROR 0x%08X. Please report to https://github.com/python/pymanager\n", err); + fprintf(stderr, "[ERROR] Internal error 0x%08X. " + "Please report to https://github.com/python/pymanager\n", err); goto error; } @@ -560,7 +561,7 @@ wmain(int argc, wchar_t **argv) case ERROR_EXE_MACHINE_TYPE_MISMATCH: case HRESULT_FROM_WIN32(ERROR_EXE_MACHINE_TYPE_MISMATCH): fprintf(stderr, - "[FATAL ERROR] Executable '%ls' is for a different kind of " + "[ERROR] Executable '%ls' is for a different kind of " "processor architecture.\n", executable.c_str()); fprintf(stderr, @@ -568,7 +569,7 @@ wmain(int argc, wchar_t **argv) "'py install' to install one for your CPU.\n"); break; default: - fprintf(stderr, "[FATAL ERROR] Failed to launch '%ls' (0x%08X)\n", executable.c_str(), err); + fprintf(stderr, "[ERROR] Failed to launch '%ls' (0x%08X)\n", executable.c_str(), err); fprintf(stderr, "This may be a corrupt install or a system configuration issue.\n"); break; } diff --git a/src/pymanager/msi.wxs b/src/pymanager/msi.wxs index 9abd466..a9a33c1 100644 --- a/src/pymanager/msi.wxs +++ b/src/pymanager/msi.wxs @@ -107,8 +107,12 @@ - - + + + + + + diff --git a/tests/test_install_command.py b/tests/test_install_command.py new file mode 100644 index 0000000..e0cc129 --- /dev/null +++ b/tests/test_install_command.py @@ -0,0 +1,97 @@ +import pytest +import secrets +from manage import install_command as IC + + +@pytest.fixture +def alias_checker(tmp_path): + with AliasChecker(tmp_path) as checker: + yield checker + +class AliasChecker: + class Cmd: + global_dir = "out" + launcher_exe = "launcher.txt" + launcherw_exe = "launcherw.txt" + default_platform = "-64" + + def __init__(self, platform=None): + if platform: + self.default_platform = platform + + + def __init__(self, tmp_path): + self.Cmd.global_dir = tmp_path / "out" + self.Cmd.launcher_exe = tmp_path / "launcher.txt" + self.Cmd.launcherw_exe = tmp_path / "launcherw.txt" + self._expect_target = "target-" + secrets.token_hex(32) + self._expect = { + "-32": "-32-" + secrets.token_hex(32), + "-64": "-64-" + secrets.token_hex(32), + "-arm64": "-arm64-" + secrets.token_hex(32), + "w-32": "w-32-" + secrets.token_hex(32), + "w-64": "w-64-" + secrets.token_hex(32), + "w-arm64": "w-arm64-" + secrets.token_hex(32), + } + for k, v in self._expect.items(): + (tmp_path / f"launcher{k}.txt").write_text(v) + + def __enter__(self): + return self + + def __exit__(self, *exc_info): + pass + + def check(self, cmd, tag, name, expect, windowed=0): + IC._write_alias( + cmd, + {"tag": tag}, + {"name": f"{name}.txt", "windowed": windowed}, + self._expect_target, + ) + print(*cmd.global_dir.glob("*"), sep="\n") + assert (cmd.global_dir / f"{name}.txt").is_file() + assert (cmd.global_dir / f"{name}.txt.__target__").is_file() + assert (cmd.global_dir / f"{name}.txt").read_text() == expect + assert (cmd.global_dir / f"{name}.txt.__target__").read_text() == self._expect_target + + def check_32(self, cmd, tag, name): + self.check(cmd, tag, name, self._expect["-32"]) + + def check_w32(self, cmd, tag, name): + self.check(cmd, tag, name, self._expect["w-32"], windowed=1) + + def check_64(self, cmd, tag, name): + self.check(cmd, tag, name, self._expect["-64"]) + + def check_w64(self, cmd, tag, name): + self.check(cmd, tag, name, self._expect["w-64"], windowed=1) + + def check_arm64(self, cmd, tag, name): + self.check(cmd, tag, name, self._expect["-arm64"]) + + def check_warm64(self, cmd, tag, name): + self.check(cmd, tag, name, self._expect["w-arm64"], windowed=1) + + +def test_write_alias_tag_with_platform(alias_checker): + alias_checker.check_32(alias_checker.Cmd, "1.0-32", "testA") + alias_checker.check_w32(alias_checker.Cmd, "1.0-32", "testB") + alias_checker.check_64(alias_checker.Cmd, "1.0-64", "testC") + alias_checker.check_w64(alias_checker.Cmd, "1.0-64", "testD") + alias_checker.check_arm64(alias_checker.Cmd, "1.0-arm64", "testE") + alias_checker.check_warm64(alias_checker.Cmd, "1.0-arm64", "testF") + + +def test_write_alias_default_platform(alias_checker): + alias_checker.check_32(alias_checker.Cmd("-32"), "1.0", "testA") + alias_checker.check_w32(alias_checker.Cmd("-32"), "1.0", "testB") + alias_checker.check_64(alias_checker.Cmd, "1.0", "testC") + alias_checker.check_w64(alias_checker.Cmd, "1.0", "testD") + alias_checker.check_arm64(alias_checker.Cmd("-arm64"), "1.0", "testE") + alias_checker.check_warm64(alias_checker.Cmd("-arm64"), "1.0", "testF") + + +def test_write_alias_fallback_platform(alias_checker): + alias_checker.check_64(alias_checker.Cmd("-spam"), "1.0", "testA") + alias_checker.check_w64(alias_checker.Cmd("-spam"), "1.0", "testB")