mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix: gmail setup fails to load gcloud when an unsupported Python is first on PATH (#112983)
* fix(gmail-setup): skip python interpreters gcloud can't use resolvePythonExecutablePath accepted the first python3/python found on PATH without checking its version, so macOS' bundled Python 3.9 (earlier on PATH than a Homebrew 3.10-3.14 install) was chosen as CLOUDSDK_PYTHON and gcloud failed to load. Query each candidate's version and skip any outside gcloud's supported 3.10-3.14 range so a compatible interpreter later on PATH is selected instead. Closes #112712 * test(gmail): cover gcloud Python upper bound --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -40,7 +40,7 @@ describe("runGcloud interpreter resolution", () => {
|
||||
await withEnvAsync({ PATH: `${shimDir}${path.delimiter}/usr/bin` }, async () => {
|
||||
runCommandWithTimeoutMock
|
||||
.mockResolvedValueOnce({
|
||||
stdout: `${realPython}\n`,
|
||||
stdout: `${realPython}\n3.12\n`,
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
@@ -71,6 +71,77 @@ describe("runGcloud interpreter resolution", () => {
|
||||
},
|
||||
60_000,
|
||||
);
|
||||
|
||||
itUnix(
|
||||
"skips Python versions below and above gcloud's supported range",
|
||||
async () => {
|
||||
const { runGcloud } = await loadGmailSetupUtils();
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-python-ver-"));
|
||||
try {
|
||||
const oldPython = path.join(tmp, "python-old");
|
||||
await fs.writeFile(oldPython, "#!/bin/sh\nexit 0\n", "utf-8");
|
||||
await fs.chmod(oldPython, 0o755);
|
||||
const goodPython = path.join(tmp, "python-good");
|
||||
await fs.writeFile(goodPython, "#!/bin/sh\nexit 0\n", "utf-8");
|
||||
await fs.chmod(goodPython, 0o755);
|
||||
|
||||
const shimDirs = ["old", "future", "supported"].map((name) =>
|
||||
path.join(tmp, `${name}-shims`),
|
||||
);
|
||||
for (const shimDir of shimDirs) {
|
||||
await fs.mkdir(shimDir, { recursive: true });
|
||||
const shim = path.join(shimDir, "python3");
|
||||
await fs.writeFile(shim, "#!/bin/sh\nexit 0\n", "utf-8");
|
||||
await fs.chmod(shim, 0o755);
|
||||
}
|
||||
|
||||
await withEnvAsync({ PATH: shimDirs.join(path.delimiter) }, async () => {
|
||||
runCommandWithTimeoutMock
|
||||
// python3 -> Python 3.9 (unsupported by gcloud): must be skipped.
|
||||
.mockResolvedValueOnce({
|
||||
stdout: `${oldPython}\n3.9\n`,
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
})
|
||||
// A future Python beyond gcloud's current cap must also be skipped.
|
||||
.mockResolvedValueOnce({
|
||||
stdout: `${path.join(tmp, "python-future")}\n3.15\n`,
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
})
|
||||
// Python 3.12 is supported and should be selected.
|
||||
.mockResolvedValueOnce({
|
||||
stdout: `${goodPython}\n3.12\n`,
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
})
|
||||
.mockResolvedValue({
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
|
||||
await runGcloud(["config", "list"]);
|
||||
|
||||
expect(runCommandWithTimeoutMock).toHaveBeenLastCalledWith(["gcloud", "config", "list"], {
|
||||
timeoutMs: 120_000,
|
||||
env: { CLOUDSDK_PYTHON: goodPython, CLOUDSDK_PYTHON_ARGS: undefined },
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(tmp, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
60_000,
|
||||
);
|
||||
});
|
||||
|
||||
describe("runGcloud", () => {
|
||||
@@ -99,7 +170,7 @@ describe("runGcloud", () => {
|
||||
async () => {
|
||||
runCommandWithTimeoutMock
|
||||
.mockResolvedValueOnce({
|
||||
stdout: `${realPython}\n`,
|
||||
stdout: `${realPython}\n3.12\n`,
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
|
||||
@@ -116,6 +116,18 @@ function ensureGcloudOnPath(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// gcloud requires a Python interpreter in this range to run; picking an
|
||||
// interpreter outside it makes `gcloud` fail to load. See `gcloud topic startup`.
|
||||
const MIN_GCLOUD_PYTHON: readonly [number, number] = [3, 10];
|
||||
const MAX_GCLOUD_PYTHON: readonly [number, number] = [3, 14];
|
||||
|
||||
function isSupportedGcloudPythonVersion(major: number, minor: number): boolean {
|
||||
if (major !== MIN_GCLOUD_PYTHON[0]) {
|
||||
return false;
|
||||
}
|
||||
return minor >= MIN_GCLOUD_PYTHON[1] && minor <= MAX_GCLOUD_PYTHON[1];
|
||||
}
|
||||
|
||||
async function resolvePythonExecutablePath(): Promise<string | undefined> {
|
||||
if (cachedPythonPath !== undefined) {
|
||||
return cachedPythonPath ?? undefined;
|
||||
@@ -123,16 +135,30 @@ async function resolvePythonExecutablePath(): Promise<string | undefined> {
|
||||
const candidates = findExecutablesOnPath(["python3", "python"]);
|
||||
for (const candidate of candidates) {
|
||||
const res = await runCommandWithTimeout(
|
||||
[candidate, "-c", "import os, sys; print(os.path.realpath(sys.executable))"],
|
||||
[
|
||||
candidate,
|
||||
"-c",
|
||||
"import os, sys; print(os.path.realpath(sys.executable)); print('%d.%d' % sys.version_info[:2])",
|
||||
],
|
||||
{ timeoutMs: 2_000 },
|
||||
);
|
||||
if (res.code !== 0) {
|
||||
continue;
|
||||
}
|
||||
const resolved = res.stdout.trim().split(/\s+/)[0];
|
||||
const lines = res.stdout.trim().split(/\r?\n/);
|
||||
const resolved = lines[0]?.trim().split(/\s+/)[0];
|
||||
if (!resolved) {
|
||||
continue;
|
||||
}
|
||||
const version = lines[1]?.trim().match(/^(\d+)\.(\d+)/);
|
||||
if (!version) {
|
||||
continue;
|
||||
}
|
||||
if (!isSupportedGcloudPythonVersion(Number(version[1]), Number(version[2]))) {
|
||||
// Skip interpreters gcloud cannot use (e.g. macOS' bundled Python 3.9)
|
||||
// so a compatible interpreter later on PATH is selected instead.
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
fs.accessSync(resolved, fs.constants.X_OK);
|
||||
cachedPythonPath = resolved;
|
||||
|
||||
Reference in New Issue
Block a user