From d69c828333f20b4feaf66b14c3dd2d9c4bfd84f5 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 18 Mar 2026 15:24:53 +0100 Subject: [PATCH 1/5] [GR-69544] Add Darwin reproducer for launcher path spaces --- .../src/tests/test_venv.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_venv.py b/graalpython/com.oracle.graal.python.test/src/tests/test_venv.py index cf0d04c771..79fe660104 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_venv.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_venv.py @@ -116,6 +116,43 @@ def test_nested_windows_venv_preserves_base_executable(self): assert f"OUTER_BASE {expected_base}" in out, out assert f"base-executable = {expected_base}" in out, out + def test_macos_venv_launcher_with_space_in_command_path(self): + if sys.platform != "darwin" or sys.implementation.name != "graalpy": + return + real_executable = os.path.realpath(sys.executable) + real_home = os.path.dirname(os.path.dirname(real_executable)) + extra_args = [ + f'--vm.Dpython.EnableBytecodeDSLInterpreter={repr(__graalpython__.is_bytecode_dsl_interpreter).lower()}' + ] + with tempfile.TemporaryDirectory(prefix="graalpy launcher ") as d: + linked_home = os.path.join(d, "home with space") + os.symlink(real_home, linked_home) + linked_executable = os.path.join(linked_home, "bin", os.path.basename(real_executable)) + env_dir = os.path.join(d, "venv") + subprocess.check_output( + [sys.executable] + extra_args + [f"--python.VenvlauncherCommand={linked_executable}", "-m", "venv", env_dir, "--without-pip"], + stderr=subprocess.STDOUT, + ) + env_python = os.path.join(env_dir, BINDIR, f"python{EXESUF}") + with open(os.path.join(env_dir, "pyvenv.cfg"), encoding="utf-8") as cfg: + cfg_data = cfg.read() + assert f"venvlauncher_command = {linked_executable}" in cfg_data, cfg_data + out = subprocess.check_output( + [ + env_python, + "-c", + """if True: + import os, sys + print("Executable", os.path.realpath(sys.executable)) + print("Original", __graalpython__.venvlauncher_command) + """, + ], + stderr=subprocess.STDOUT, + text=True, + ) + assert f"Executable {os.path.realpath(env_python)}" in out, out + assert f"Original {linked_executable}" in out, out + def test_create_and_use_basic_venv(self): run = None run_output = '' From c1288b19c572948fa81379b2460e306ef7fa08dc Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 18 Mar 2026 16:24:30 +0100 Subject: [PATCH 2/5] [GR-69544] Use direct macOS venv launcher reproducer --- .../src/tests/test_venv.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_venv.py b/graalpython/com.oracle.graal.python.test/src/tests/test_venv.py index 79fe660104..e2ce1e9fb0 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_venv.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_venv.py @@ -119,27 +119,29 @@ def test_nested_windows_venv_preserves_base_executable(self): def test_macos_venv_launcher_with_space_in_command_path(self): if sys.platform != "darwin" or sys.implementation.name != "graalpy": return + import venv real_executable = os.path.realpath(sys.executable) real_home = os.path.dirname(os.path.dirname(real_executable)) - extra_args = [ - f'--vm.Dpython.EnableBytecodeDSLInterpreter={repr(__graalpython__.is_bytecode_dsl_interpreter).lower()}' - ] + launcher_template = os.path.join(venv.__path__[0], "scripts", "macos", "graalpy") + assert os.path.exists(launcher_template), launcher_template with tempfile.TemporaryDirectory(prefix="graalpy launcher ") as d: linked_home = os.path.join(d, "home with space") os.symlink(real_home, linked_home) linked_executable = os.path.join(linked_home, "bin", os.path.basename(real_executable)) env_dir = os.path.join(d, "venv") - subprocess.check_output( - [sys.executable] + extra_args + [f"--python.VenvlauncherCommand={linked_executable}", "-m", "venv", env_dir, "--without-pip"], - stderr=subprocess.STDOUT, - ) - env_python = os.path.join(env_dir, BINDIR, f"python{EXESUF}") + bin_dir = os.path.join(env_dir, BINDIR) + os.makedirs(bin_dir) + env_launcher = os.path.join(bin_dir, "graalpy") + shutil.copyfile(launcher_template, env_launcher) + os.chmod(env_launcher, 0o755) + with open(os.path.join(env_dir, "pyvenv.cfg"), "w", encoding="utf-8") as cfg: + cfg.write(f"venvlauncher_command = {linked_executable}\n") with open(os.path.join(env_dir, "pyvenv.cfg"), encoding="utf-8") as cfg: cfg_data = cfg.read() assert f"venvlauncher_command = {linked_executable}" in cfg_data, cfg_data out = subprocess.check_output( [ - env_python, + env_launcher, "-c", """if True: import os, sys @@ -150,7 +152,7 @@ def test_macos_venv_launcher_with_space_in_command_path(self): stderr=subprocess.STDOUT, text=True, ) - assert f"Executable {os.path.realpath(env_python)}" in out, out + assert f"Executable {os.path.realpath(env_launcher)}" in out, out assert f"Original {linked_executable}" in out, out def test_create_and_use_basic_venv(self): From 98bf8a82f7ddf0f662f78a1df852492d5f33c8eb Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 18 Mar 2026 20:41:43 +0100 Subject: [PATCH 3/5] [GR-69544] Fix macOS venv launcher command parsing --- .../python-macos-launcher/src/venvlauncher.c | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/graalpython/python-macos-launcher/src/venvlauncher.c b/graalpython/python-macos-launcher/src/venvlauncher.c index 2752ad1bf8..d7c3b29807 100644 --- a/graalpython/python-macos-launcher/src/venvlauncher.c +++ b/graalpython/python-macos-launcher/src/venvlauncher.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -47,6 +47,7 @@ #include #include #include +#include #include #define GRAAL_PYTHON_EXE_ARG "--python.Executable=" @@ -140,38 +141,56 @@ char *get_pyenvcfg_command(const char *pyenv_cfg_path) { exit(1); } -int count_args(const char *cmd) { - char *copy = strdup(cmd); - int count = 0; - char *token = strtok(copy, " "); - while (token) { - count++; - token = strtok(NULL, " "); +char **split_venv_command_into_args(const char *venv_command, int *argc_out) { + if (access(venv_command, X_OK) == 0) { + char **args = malloc(sizeof(char *)); + if (!args) { + fprintf(stderr, "allocation failed\n"); + exit(1); + } + args[0] = strdup(venv_command); + if (!args[0]) { + fprintf(stderr, "allocation failed\n"); + free(args); + exit(1); + } + *argc_out = 1; + return args; } - free(copy); - return count; -} - -char **split_venv_command_into_args(const char *venv_command, int *argc_out) { + wordexp_t expanded; + int rc = wordexp(venv_command, &expanded, WRDE_NOCMD); + if (rc != 0 || expanded.we_wordc == 0) { + fprintf(stderr, "Failed to parse venvlauncher_command\n"); + if (rc == 0) { + wordfree(&expanded); + } + exit(1); + } - char *copy = strdup(venv_command); - const int capacity = count_args(copy); + const int capacity = (int) expanded.we_wordc; char **args = malloc(capacity * sizeof(char *)); if (!args) { fprintf(stderr, "allocation failed\n"); - free(copy); + wordfree(&expanded); exit(1); } int count = 0; - char *current_token = strtok(copy, " "); - while (current_token) { - args[count++] = strdup(current_token); - current_token = strtok(NULL, " "); + for (size_t i = 0; i < expanded.we_wordc; i++) { + args[count] = strdup(expanded.we_wordv[i]); + if (!args[count]) { + fprintf(stderr, "allocation failed\n"); + for (int j = 0; j < count; j++) { + free(args[j]); + } + free(args); + wordfree(&expanded); + exit(1); + } + count++; } - - free(copy); + wordfree(&expanded); assert(capacity == count); *argc_out = count; return args; From 99ec4ac60daa44af8c8c05609bc3e8ca4b100416 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 18 Mar 2026 21:05:43 +0100 Subject: [PATCH 4/5] [GR-69544] Trim macOS launcher command from pyvenv.cfg --- graalpython/python-macos-launcher/src/venvlauncher.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graalpython/python-macos-launcher/src/venvlauncher.c b/graalpython/python-macos-launcher/src/venvlauncher.c index d7c3b29807..bd018f3f6f 100644 --- a/graalpython/python-macos-launcher/src/venvlauncher.c +++ b/graalpython/python-macos-launcher/src/venvlauncher.c @@ -106,11 +106,11 @@ char *get_pyenvcfg_command(const char *pyenv_cfg_path) { exit(1); } while (isspace((unsigned char) *p)) p++; + char *end = p + strlen(p); + while (end > p && isspace((unsigned char) end[-1])) { + *--end = '\0'; + } if (*p == '\"') { - char *end = p + strlen(p); - while (end > p && (isspace((unsigned char) end[-1]) || end[-1] == '\n')) { - *--end = '\0'; - } if (end <= p + 1 || end[-1] != '\"') { fprintf(stderr, "venv command is not in correct format"); free(current_line); From 31a1a5139bfacc14510d34d49784449fc3545b04 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 18 Mar 2026 22:36:58 +0100 Subject: [PATCH 5/5] [GR-69544] Match quoted macOS venv launcher command --- graalpython/com.oracle.graal.python.test/src/tests/test_venv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_venv.py b/graalpython/com.oracle.graal.python.test/src/tests/test_venv.py index e2ce1e9fb0..e37d7a2e5a 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_venv.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_venv.py @@ -153,7 +153,7 @@ def test_macos_venv_launcher_with_space_in_command_path(self): text=True, ) assert f"Executable {os.path.realpath(env_launcher)}" in out, out - assert f"Original {linked_executable}" in out, out + assert f'Original "{linked_executable}"' in out, out def test_create_and_use_basic_venv(self): run = None