Skip to content

Commit d57e889

Browse files
committed
[GR-69544] Fix macOS venv launcher command parsing
PullRequest: graalpython/4338
2 parents 0ce4748 + 31a1a51 commit d57e889

File tree

2 files changed

+84
-26
lines changed

2 files changed

+84
-26
lines changed

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,45 @@ def test_nested_windows_venv_preserves_base_executable(self):
116116
assert f"OUTER_BASE {expected_base}" in out, out
117117
assert f"base-executable = {expected_base}" in out, out
118118

119+
def test_macos_venv_launcher_with_space_in_command_path(self):
120+
if sys.platform != "darwin" or sys.implementation.name != "graalpy":
121+
return
122+
import venv
123+
real_executable = os.path.realpath(sys.executable)
124+
real_home = os.path.dirname(os.path.dirname(real_executable))
125+
launcher_template = os.path.join(venv.__path__[0], "scripts", "macos", "graalpy")
126+
assert os.path.exists(launcher_template), launcher_template
127+
with tempfile.TemporaryDirectory(prefix="graalpy launcher ") as d:
128+
linked_home = os.path.join(d, "home with space")
129+
os.symlink(real_home, linked_home)
130+
linked_executable = os.path.join(linked_home, "bin", os.path.basename(real_executable))
131+
env_dir = os.path.join(d, "venv")
132+
bin_dir = os.path.join(env_dir, BINDIR)
133+
os.makedirs(bin_dir)
134+
env_launcher = os.path.join(bin_dir, "graalpy")
135+
shutil.copyfile(launcher_template, env_launcher)
136+
os.chmod(env_launcher, 0o755)
137+
with open(os.path.join(env_dir, "pyvenv.cfg"), "w", encoding="utf-8") as cfg:
138+
cfg.write(f"venvlauncher_command = {linked_executable}\n")
139+
with open(os.path.join(env_dir, "pyvenv.cfg"), encoding="utf-8") as cfg:
140+
cfg_data = cfg.read()
141+
assert f"venvlauncher_command = {linked_executable}" in cfg_data, cfg_data
142+
out = subprocess.check_output(
143+
[
144+
env_launcher,
145+
"-c",
146+
"""if True:
147+
import os, sys
148+
print("Executable", os.path.realpath(sys.executable))
149+
print("Original", __graalpython__.venvlauncher_command)
150+
""",
151+
],
152+
stderr=subprocess.STDOUT,
153+
text=True,
154+
)
155+
assert f"Executable {os.path.realpath(env_launcher)}" in out, out
156+
assert f'Original "{linked_executable}"' in out, out
157+
119158
def test_create_and_use_basic_venv(self):
120159
run = None
121160
run_output = ''

graalpython/python-macos-launcher/src/venvlauncher.c

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -47,6 +47,7 @@
4747
#include <string.h>
4848
#include <limits.h>
4949
#include <stdarg.h>
50+
#include <wordexp.h>
5051
#include <mach-o/dyld.h>
5152

5253
#define GRAAL_PYTHON_EXE_ARG "--python.Executable="
@@ -105,11 +106,11 @@ char *get_pyenvcfg_command(const char *pyenv_cfg_path) {
105106
exit(1);
106107
}
107108
while (isspace((unsigned char) *p)) p++;
109+
char *end = p + strlen(p);
110+
while (end > p && isspace((unsigned char) end[-1])) {
111+
*--end = '\0';
112+
}
108113
if (*p == '\"') {
109-
char *end = p + strlen(p);
110-
while (end > p && (isspace((unsigned char) end[-1]) || end[-1] == '\n')) {
111-
*--end = '\0';
112-
}
113114
if (end <= p + 1 || end[-1] != '\"') {
114115
fprintf(stderr, "venv command is not in correct format");
115116
free(current_line);
@@ -140,38 +141,56 @@ char *get_pyenvcfg_command(const char *pyenv_cfg_path) {
140141
exit(1);
141142
}
142143

143-
int count_args(const char *cmd) {
144-
char *copy = strdup(cmd);
145-
int count = 0;
146-
char *token = strtok(copy, " ");
147-
while (token) {
148-
count++;
149-
token = strtok(NULL, " ");
144+
char **split_venv_command_into_args(const char *venv_command, int *argc_out) {
145+
if (access(venv_command, X_OK) == 0) {
146+
char **args = malloc(sizeof(char *));
147+
if (!args) {
148+
fprintf(stderr, "allocation failed\n");
149+
exit(1);
150+
}
151+
args[0] = strdup(venv_command);
152+
if (!args[0]) {
153+
fprintf(stderr, "allocation failed\n");
154+
free(args);
155+
exit(1);
156+
}
157+
*argc_out = 1;
158+
return args;
150159
}
151160

152-
free(copy);
153-
return count;
154-
}
155-
156-
char **split_venv_command_into_args(const char *venv_command, int *argc_out) {
161+
wordexp_t expanded;
162+
int rc = wordexp(venv_command, &expanded, WRDE_NOCMD);
163+
if (rc != 0 || expanded.we_wordc == 0) {
164+
fprintf(stderr, "Failed to parse venvlauncher_command\n");
165+
if (rc == 0) {
166+
wordfree(&expanded);
167+
}
168+
exit(1);
169+
}
157170

158-
char *copy = strdup(venv_command);
159-
const int capacity = count_args(copy);
171+
const int capacity = (int) expanded.we_wordc;
160172
char **args = malloc(capacity * sizeof(char *));
161173
if (!args) {
162174
fprintf(stderr, "allocation failed\n");
163-
free(copy);
175+
wordfree(&expanded);
164176
exit(1);
165177
}
166178

167179
int count = 0;
168-
char *current_token = strtok(copy, " ");
169-
while (current_token) {
170-
args[count++] = strdup(current_token);
171-
current_token = strtok(NULL, " ");
180+
for (size_t i = 0; i < expanded.we_wordc; i++) {
181+
args[count] = strdup(expanded.we_wordv[i]);
182+
if (!args[count]) {
183+
fprintf(stderr, "allocation failed\n");
184+
for (int j = 0; j < count; j++) {
185+
free(args[j]);
186+
}
187+
free(args);
188+
wordfree(&expanded);
189+
exit(1);
190+
}
191+
count++;
172192
}
173-
174-
free(copy);
193+
wordfree(&expanded);
175194
assert(capacity == count);
176195
*argc_out = count;
177196
return args;

0 commit comments

Comments
 (0)