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..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 @@ -116,6 +116,45 @@ 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 + import venv + real_executable = os.path.realpath(sys.executable) + real_home = os.path.dirname(os.path.dirname(real_executable)) + 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") + 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_launcher, + "-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_launcher)}" in out, out + assert f'Original "{linked_executable}"' in out, out + def test_create_and_use_basic_venv(self): run = None run_output = '' diff --git a/graalpython/python-macos-launcher/src/venvlauncher.c b/graalpython/python-macos-launcher/src/venvlauncher.c index 2752ad1bf8..bd018f3f6f 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=" @@ -105,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); @@ -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;