update the init functions python flow to find available python runtimes on user's machine#10673
update the init functions python flow to find available python runtimes on user's machine#10673aalej wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a feature to detect available Python runtimes on the user's machine and prompt the user to select their preferred runtime during the functions initialization process. The setup flow has been updated to write the selected runtime to the configuration and conditionally create the virtual environment only if the selected runtime is detected locally. Feedback on these changes highlights two key issues: first, on Windows, getPythonBinary returns "python.exe" for all runtimes, which can lead to false positives where all runtimes are reported as available with the same version; second, checkPythonVersion lacks a timeout, which could cause the initialization process to hang indefinitely if a binary blocks. Suggestions have been provided to validate the detected runtime versions and to enforce a timeout on the version check process.
| export async function getAvailablePythonRuntimes(): Promise< | ||
| { runtime: supported.Runtime & supported.RuntimeOf<"python">; binary: string; version: string }[] | ||
| > { | ||
| const pythonRuntimes = (Object.keys(supported.RUNTIMES) as supported.Runtime[]).filter( | ||
| (runtime): runtime is supported.Runtime & supported.RuntimeOf<"python"> => | ||
| runtime.startsWith("python"), | ||
| ); | ||
|
|
||
| const results = await Promise.all( | ||
| pythonRuntimes.map(async (runtime) => { | ||
| const binary = getPythonBinary(runtime); | ||
| const version = await checkPythonVersion(binary); | ||
| return version ? { runtime, binary, version } : undefined; | ||
| }), | ||
| ); | ||
|
|
||
| return results.filter((r): r is NonNullable<typeof r> => !!r); | ||
| } |
There was a problem hiding this comment.
On Windows, getPythonBinary always returns "python.exe" for all Python runtimes. This causes getAvailablePythonRuntimes to query the same "python.exe" binary multiple times in parallel, falsely reporting that all Python runtimes (e.g., python310, python311, python312, etc.) are available with the exact same version (the version of the active "python.exe").
To fix this, we should parse the returned version string and verify that it matches the expected major and minor version of the runtime.
export async function getAvailablePythonRuntimes(): Promise<
{ runtime: supported.Runtime & supported.RuntimeOf<"python">; binary: string; version: string }[]
> {
const pythonRuntimes = (Object.keys(supported.RUNTIMES) as supported.Runtime[]).filter(
(runtime): runtime is supported.Runtime & supported.RuntimeOf<"python"> =>
runtime.startsWith("python"),
);
const results = await Promise.all(
pythonRuntimes.map(async (runtime) => {
const binary = getPythonBinary(runtime);
const version = await checkPythonVersion(binary);
if (!version) {
return undefined;
}
const parts = runtime.match(/^python(\d)(\d+)$/);
const match = version.match(/(?:Python\s+)?(\d+)\.(\d+)/i);
if (parts && match) {
const [_, expectedMajor, expectedMinor] = parts;
const [__, major, minor] = match;
if (major !== expectedMajor || minor !== expectedMinor) {
return undefined;
}
}
return { runtime, binary, version };
}),
);
return results.filter((r): r is NonNullable<typeof r> => !!r);
}| export async function checkPythonVersion(binary: string): Promise<string | undefined> { | ||
| return new Promise((resolve) => { | ||
| const child = spawn(binary, ["--version"], { stdio: "pipe" }); | ||
| let output = ""; | ||
| child.stdout?.on("data", (data: Buffer) => { | ||
| output += data.toString(); | ||
| }); | ||
| child.stderr?.on("data", (data: Buffer) => { | ||
| output += data.toString(); | ||
| }); | ||
| child.on("close", (code: number) => { | ||
| if (code === 0) { | ||
| resolve(output.trim()); | ||
| } else { | ||
| resolve(undefined); | ||
| } | ||
| }); | ||
| child.on("error", () => { | ||
| resolve(undefined); | ||
| }); | ||
| }); | ||
| } |
There was a problem hiding this comment.
The checkPythonVersion function spawns a child process to check the Python version but does not enforce a timeout. If a binary hangs indefinitely (e.g., due to a broken installation or interactive prompt), the entire firebase init flow will block forever.
Additionally, the code parameter in the close event listener can be null if the process is terminated by a signal. Specifying code: number explicitly might cause TypeScript compilation errors under strict type checking.
Adding a timeout and letting TypeScript infer the code type improves robustness and type safety.
export async function checkPythonVersion(binary: string): Promise<string | undefined> {
return new Promise((resolve) => {
const child = spawn(binary, ["--version"], { stdio: "pipe" });
let output = "";
const timeout = setTimeout(() => {
child.kill();
resolve(undefined);
}, 1500);
child.stdout?.on("data", (data: Buffer) => {
output += data.toString();
});
child.stderr?.on("data", (data: Buffer) => {
output += data.toString();
});
child.on("close", (code) => {
clearTimeout(timeout);
if (code === 0) {
resolve(output.trim());
} else {
resolve(undefined);
}
});
child.on("error", () => {
clearTimeout(timeout);
resolve(undefined);
});
});
}
Description
We currently always try to use the latest python runtime when we set up functions
firebase-tools/src/init/features/functions/python.ts
Line 83 in 33ff901
For machines that don't have the latest version of python installed, venv creation and dependency installation will fail because that python version isn't available.

This PR introduces an additional prompt to allow users to set up with older version of python on their machine(if latest is not available).

If latest version of python is available on their machine, no prompts occur and we default to using the latest

Scenarios Tested
firebase init functions- No Python3.14firebase init functions- With Python3.14Sample Commands