Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 27 additions & 26 deletions _msbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def main_exe(name):
ConfigurationType='Application',
)


def mainw_exe(name):
return CProject(name,
VersionInfo(FileDescription="Python Install Manager (windowed)"),
Expand All @@ -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
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/manage/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
21 changes: 19 additions & 2 deletions src/manage/install_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,30 @@
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"])
Expand Down Expand Up @@ -281,7 +298,7 @@
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)

Check warning on line 301 in src/manage/install_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/install_command.py#L301

Added line #L301 was not covered by tests
alias_written.add(a["name"].casefold())

for s in i.get("shortcuts", ()):
Expand Down
5 changes: 4 additions & 1 deletion src/pymanager/_launch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/pymanager/launcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions src/pymanager/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -560,15 +561,15 @@ 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,
"Try using '-V:<version>' to select a different runtime, or use "
"'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;
}
Expand Down
8 changes: 6 additions & 2 deletions src/pymanager/msi.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,12 @@
</Component>

<Component Id="TemplatesComponent" Directory="TEMPLATES" Guid="3ACC110C-5C33-45C8-9843-A446718930A0">
<File KeyPath="yes" Source="templates\launcher.exe" Name="launcher.exe" />
<File Source="templates\launcherw.exe" Name="launcherw.exe" />
<File KeyPath="yes" Source="templates\launcher-64.exe" Name="launcher-64.exe" />
<File Source="templates\launcherw-64.exe" Name="launcherw-64.exe" />
<File Source="templates\launcher-arm64.exe" Name="launcher-arm64.exe" />
<File Source="templates\launcherw-arm64.exe" Name="launcherw-arm64.exe" />
<File Source="templates\launcher-32.exe" Name="launcher-32.exe" />
<File Source="templates\launcherw-32.exe" Name="launcherw-32.exe" />
</Component>

</Package>
Expand Down
97 changes: 97 additions & 0 deletions tests/test_install_command.py
Original file line number Diff line number Diff line change
@@ -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")