Skip to content

Commit ef69780

Browse files
committed
Fix Windows venv base executable propagation for pypa/cibuildwheel#2754
1 parent 75ad8d7 commit ef69780

File tree

4 files changed

+143
-33
lines changed

4 files changed

+143
-33
lines changed

graalpy_virtualenv_seeder/graalpy_virtualenv_seeder/graalpy.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2022, 2026, Oracle and/or its affiliates. All rights reserved.
22
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
33
#
44
# The Universal Permissive License (UPL), Version 1.0
@@ -126,6 +126,7 @@ def _native_lib(cls, lib_dir, _platform):
126126
def set_pyenv_cfg(self):
127127
# GraalPy needs an additional entry in pyvenv.cfg on Windows
128128
super().set_pyenv_cfg()
129+
self.pyenv_cfg["base-executable"] = self.interpreter.system_executable
129130
self.pyenv_cfg["venvlauncher_command"] = self.interpreter.system_executable
130131

131132

graalpython/com.oracle.graal.python.test/src/tests/test_venv.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved.
22
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
33
#
44
# The Universal Permissive License (UPL), Version 1.0
@@ -42,6 +42,7 @@
4242
import subprocess
4343
import sys
4444
import tempfile
45+
import textwrap
4546
import unittest
4647

4748
BINDIR = 'bin' if sys.platform != 'win32' else 'Scripts'
@@ -66,22 +67,54 @@ def test_venv_launcher(self):
6667
import struct
6768
with tempfile.TemporaryDirectory() as d:
6869
tmpfile = os.path.join(d, "venvlauncher.exe")
70+
launcher_command = f'"{os.path.realpath(sys.executable)}" -S'
6971
shutil.copy(os.path.join(venv.__path__[0], "scripts", "nt", "graalpy.exe"), tmpfile)
7072
with open(tmpfile, "ab") as f:
71-
sz = f.write(sys.executable.encode("utf-16le"))
73+
sz = f.write(launcher_command.encode("utf-16le"))
7274
assert f.write(struct.pack("@I", sz)) == 4
7375
try:
7476
out = subprocess.check_output([tmpfile, "-c", """if True:
7577
import sys, os
7678
x = os
7779
print("Hello", sys.executable)
80+
print("Base", sys._base_executable)
7881
print("Original", __graalpython__.venvlauncher_command)
7982
"""], env={"PYLAUNCHER_DEBUG": "1"}, text=True)
8083
except subprocess.CalledProcessError as err:
8184
out = err.output.decode(errors="replace") if err.output else ""
8285
print("out=", out, sep="\n")
8386
assert f"Hello {tmpfile}" in out, out
84-
assert f'Original "{sys.executable}"' in out, out
87+
assert f"Base {os.path.realpath(sys.executable)}" in out, out
88+
assert f'Original {launcher_command}' in out, out
89+
90+
def test_nested_windows_venv_preserves_base_executable(self):
91+
if sys.platform != "win32" or sys.implementation.name != "graalpy":
92+
return
93+
expected_base = os.path.realpath(getattr(sys, "_base_executable", sys.executable))
94+
with tempfile.TemporaryDirectory() as outer_dir, tempfile.TemporaryDirectory() as inner_root:
95+
inner_dir = os.path.join(inner_root, "inner")
96+
extra_args = [
97+
f'--vm.Dpython.EnableBytecodeDSLInterpreter={repr(__graalpython__.is_bytecode_dsl_interpreter).lower()}'
98+
]
99+
subprocess.check_output([sys.executable] + extra_args + ["-m", "venv", outer_dir, "--without-pip"], stderr=subprocess.STDOUT)
100+
outer_python = os.path.join(outer_dir, BINDIR, f"python{EXESUF}")
101+
out = subprocess.check_output([
102+
outer_python,
103+
"-c",
104+
textwrap.dedent(f"""
105+
import os
106+
import sys
107+
import venv
108+
109+
inner_dir = {inner_dir!r}
110+
venv.EnvBuilder(with_pip=False).create(inner_dir)
111+
print("OUTER_BASE", os.path.realpath(sys._base_executable))
112+
with open(os.path.join(inner_dir, "pyvenv.cfg"), encoding="utf-8") as cfg:
113+
print(cfg.read())
114+
""")
115+
], text=True)
116+
assert f"OUTER_BASE {expected_base}" in out, out
117+
assert f"base-executable = {expected_base}" in out, out
85118

86119
def test_create_and_use_basic_venv(self):
87120
run = None

graalpython/lib-python/3/venv/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@ def create_configuration(self, context):
254254
# Truffle change: setup our a launcher by adding the path to the creating executable
255255
if (os.name == 'nt' or sys.platform == 'darwin'):
256256
f.write('venvlauncher_command = %s\n' % (__graalpython__.venvlauncher_command or sys.executable))
257+
if os.name == 'nt':
258+
f.write('base-executable = %s\n' % os.path.realpath(getattr(sys, '_base_executable', sys.executable)))
257259
# End of Truffle change
258260

259261
if os.name != 'nt':

graalpython/python-venvlauncher/src/venvlauncher.c

Lines changed: 103 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Copyright (c) 2023, Oracle and/or its affiliates.
1+
/* Copyright (c) 2023, 2026, Oracle and/or its affiliates.
22
* Copyright (C) 1996-2023 Python Software Foundation
33
*
44
* Licensed under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
@@ -7,13 +7,16 @@
77

88
#include <windows.h>
99
#include <pathcch.h>
10+
#include <shellapi.h>
1011
#include <stringapiset.h>
1112
#include <stdio.h>
1213
#include <stdbool.h>
1314
#include <stdint.h>
1415
#include <share.h>
16+
#include <string.h>
1517

1618
#pragma comment(lib, "Pathcch.lib")
19+
#pragma comment(lib, "Shell32.lib")
1720

1821
#define MAXLEN PATHCCH_MAX_CCH
1922
#define MSGSIZE 1024
@@ -200,10 +203,58 @@ launchEnvironment(wchar_t *env, wchar_t *exe)
200203

201204
#define GRAAL_PYTHON_ARGS L"GRAAL_PYTHON_ARGS="
202205
#define GRAAL_PYTHON_EXE_ARG L"--python.Executable="
203-
#define GRAAL_PYTHON_BASE_EXE_ARG L"--python.VenvlauncherCommand="
206+
#define GRAAL_PYTHON_BASE_EXECUTABLE_ARG L"--python.BaseExecutable="
207+
#define GRAAL_PYTHON_VENVLAUNCHER_COMMAND_ARG L"--python.VenvlauncherCommand="
204208
#define GRAAL_PYTHON_COMMAND_CFG "venvlauncher_command = "
209+
#define GRAAL_PYTHON_BASE_EXECUTABLE_CFG "base-executable = "
205210
#define PYVENV_CFG L"pyvenv.cfg"
206211

212+
static int
213+
copyUtf8ToWideChar(const char *in, int inLen, wchar_t *out, size_t outLen)
214+
{
215+
if (outLen == 0) {
216+
return 0;
217+
}
218+
int written = MultiByteToWideChar(CP_UTF8, 0, in, inLen, out, (int)outLen - 1);
219+
if (written <= 0) {
220+
return 0;
221+
}
222+
out[written] = L'\0';
223+
return written;
224+
}
225+
226+
static int
227+
readPyVenvCfgValue(FILE *pyvenvCfgFile, const char *key, wchar_t *out, size_t outLen)
228+
{
229+
char line[MAXLEN];
230+
size_t keyLen = strlen(key);
231+
232+
rewind(pyvenvCfgFile);
233+
while (fgets(line, sizeof(line), pyvenvCfgFile) != NULL) {
234+
if (strncmp(line, key, keyLen) == 0) {
235+
size_t valueLen = strcspn(line + keyLen, "\r\n");
236+
return copyUtf8ToWideChar(line + keyLen, (int)valueLen, out, outLen);
237+
}
238+
}
239+
return 0;
240+
}
241+
242+
static int
243+
extractBaseExecutable(const wchar_t *command, wchar_t *out, size_t outLen)
244+
{
245+
int cmdArgc = 0;
246+
wchar_t **cmdArgv = CommandLineToArgvW(command, &cmdArgc);
247+
if (cmdArgv == NULL || cmdArgc < 1) {
248+
if (cmdArgv != NULL) {
249+
LocalFree(cmdArgv);
250+
}
251+
return -1;
252+
}
253+
int rc = wcscpy_s(out, outLen, cmdArgv[0]);
254+
LocalFree(cmdArgv);
255+
return rc;
256+
}
257+
207258
int
208259
wmain(int argc, wchar_t ** argv)
209260
{
@@ -221,16 +272,16 @@ wmain(int argc, wchar_t ** argv)
221272
wchar_t * newExeStart = NULL;
222273
memset(newExecutable, 0, sizeof(newExecutable));
223274

275+
wchar_t baseExecutable[MAXLEN];
276+
memset(baseExecutable, 0, sizeof(baseExecutable));
277+
224278
wchar_t currentExecutable[MAXLEN];
225279
int currentExecutableSize = sizeof(currentExecutable) / sizeof(currentExecutable[0]);
226280
memset(currentExecutable, 0, sizeof(currentExecutable));
227281

228282
wchar_t pyvenvCfg[MAXLEN];
229283
memset(pyvenvCfg, 0, sizeof(pyvenvCfg));
230284

231-
char pyvenvcfg_command[MAXLEN];
232-
memset(pyvenvcfg_command, 0, sizeof(pyvenvcfg_command));
233-
234285
if (isEnvVarSet(L"PYLAUNCHER_DEBUG")) {
235286
setvbuf(stderr, (char *)NULL, _IONBF, 0);
236287
log_fp = stderr;
@@ -260,24 +311,12 @@ wmain(int argc, wchar_t ** argv)
260311
}
261312
if (pyvenvCfgFile) {
262313
debug(L"pyvenv.cfg at %s\n", pyvenvCfg);
263-
int i = 0;
264-
while (fread_s(pyvenvcfg_command + i, sizeof(pyvenvcfg_command), 1, 1, pyvenvCfgFile)) {
265-
if (pyvenvcfg_command[i] == GRAAL_PYTHON_COMMAND_CFG[i]) {
266-
++i;
267-
} else {
268-
i = 0;
269-
}
270-
if (strcmp(GRAAL_PYTHON_COMMAND_CFG, pyvenvcfg_command) == 0) {
271-
for (i = 0; i < sizeof(pyvenvcfg_command); ++i) {
272-
if (fread_s(pyvenvcfg_command + i, sizeof(pyvenvcfg_command), 1, 1, pyvenvCfgFile) < 1
273-
|| pyvenvcfg_command[i] == '\r'
274-
|| pyvenvcfg_command[i] == '\n') {
275-
newExecutableSize = MultiByteToWideChar(CP_UTF8, 0, pyvenvcfg_command, i, newExecutable + 1, sizeof(newExecutable) - 2) * sizeof(newExecutable[0]);
276-
break;
277-
}
278-
}
279-
break;
280-
}
314+
newExecutableSize = readPyVenvCfgValue(pyvenvCfgFile, GRAAL_PYTHON_COMMAND_CFG, newExecutable + 1, MAXLEN - 1) * sizeof(newExecutable[0]);
315+
if (newExecutableSize) {
316+
debug(L"new executable from pyvenv.cfg: %s\n", newExecutable + 1);
317+
}
318+
if (readPyVenvCfgValue(pyvenvCfgFile, GRAAL_PYTHON_BASE_EXECUTABLE_CFG, baseExecutable, MAXLEN)) {
319+
debug(L"base executable from pyvenv.cfg: %s\n", baseExecutable);
281320
}
282321
} else {
283322
debug(L"no pyvenv.cfg at %s\n", pyvenvCfg);
@@ -323,6 +362,19 @@ wmain(int argc, wchar_t ** argv)
323362
}
324363
debug(L"new exe: %s\n", newExeStart);
325364

365+
if (!baseExecutable[0]) {
366+
exitCode = extractBaseExecutable(newExeStart, baseExecutable, MAXLEN);
367+
if (exitCode) {
368+
debug(L"Failed to extract base executable from launcher command, using current executable instead\n");
369+
exitCode = wcscpy_s(baseExecutable, MAXLEN, currentExecutable);
370+
if (exitCode) {
371+
winerror(exitCode, L"Failed to copy current executable into base executable");
372+
goto abort;
373+
}
374+
}
375+
}
376+
debug(L"base executable: %s\n", baseExecutable);
377+
326378
// calculate the size of the new environment, that is, the size of the previous environment
327379
// plus the size of the GRAAL_PYTHON_ARGS variable with the arguments to pass on
328380
env = GetEnvironmentStringsW();
@@ -340,8 +392,10 @@ wmain(int argc, wchar_t ** argv)
340392
envSize += wcslen(GRAAL_PYTHON_ARGS);
341393
// need room to specify original launcher path
342394
envSize += 1 + wcslen(GRAAL_PYTHON_EXE_ARG) + wcslen(currentExecutable);
343-
// need room to specify base launcher path
344-
envSize += 1 + wcslen(GRAAL_PYTHON_BASE_EXE_ARG) + wcslen(newExeStart);
395+
// need room to specify base executable path
396+
envSize += 1 + wcslen(GRAAL_PYTHON_BASE_EXECUTABLE_ARG) + wcslen(baseExecutable);
397+
// need room to specify original launcher command
398+
envSize += 1 + wcslen(GRAAL_PYTHON_VENVLAUNCHER_COMMAND_ARG) + wcslen(newExeStart);
345399
for (int i = 1; i < argc; ++i) {
346400
// env needs room for \v and arg, no \0
347401
envSize = envSize + 1 + wcslen(argv[i]);
@@ -406,14 +460,34 @@ wmain(int argc, wchar_t ** argv)
406460
newEnvCur[0] = L'\v';
407461
--envSize;
408462
++newEnvCur;
409-
exitCode = wcscpy_s(newEnvCur, envSize, GRAAL_PYTHON_BASE_EXE_ARG);
463+
exitCode = wcscpy_s(newEnvCur, envSize, GRAAL_PYTHON_BASE_EXECUTABLE_ARG);
464+
if (exitCode) {
465+
winerror(exitCode, L"Failed to copy %s", GRAAL_PYTHON_BASE_EXECUTABLE_ARG);
466+
goto abort;
467+
}
468+
debug(L"%s", newEnvCur);
469+
envSize = envSize - wcslen(GRAAL_PYTHON_BASE_EXECUTABLE_ARG);
470+
newEnvCur = newEnvCur + wcslen(GRAAL_PYTHON_BASE_EXECUTABLE_ARG);
471+
exitCode = wcscpy_s(newEnvCur, envSize, baseExecutable);
472+
if (exitCode) {
473+
winerror(exitCode, L"Failed to copy %s into env", baseExecutable);
474+
goto abort;
475+
}
476+
debug(L"%s", newEnvCur);
477+
envSize = envSize - wcslen(baseExecutable);
478+
newEnvCur = newEnvCur + wcslen(baseExecutable);
479+
// specify original launcher command
480+
newEnvCur[0] = L'\v';
481+
--envSize;
482+
++newEnvCur;
483+
exitCode = wcscpy_s(newEnvCur, envSize, GRAAL_PYTHON_VENVLAUNCHER_COMMAND_ARG);
410484
if (exitCode) {
411-
winerror(exitCode, L"Failed to copy %s", GRAAL_PYTHON_BASE_EXE_ARG);
485+
winerror(exitCode, L"Failed to copy %s", GRAAL_PYTHON_VENVLAUNCHER_COMMAND_ARG);
412486
goto abort;
413487
}
414488
debug(L"%s", newEnvCur);
415-
envSize = envSize - wcslen(GRAAL_PYTHON_BASE_EXE_ARG);
416-
newEnvCur = newEnvCur + wcslen(GRAAL_PYTHON_BASE_EXE_ARG);
489+
envSize = envSize - wcslen(GRAAL_PYTHON_VENVLAUNCHER_COMMAND_ARG);
490+
newEnvCur = newEnvCur + wcslen(GRAAL_PYTHON_VENVLAUNCHER_COMMAND_ARG);
417491
exitCode = wcscpy_s(newEnvCur, envSize, newExeStart);
418492
if (exitCode) {
419493
winerror(exitCode, L"Failed to copy %s into env", newExeStart);

0 commit comments

Comments
 (0)