Skip to content

update the init functions python flow to find available python runtimes on user's machine#10673

Open
aalej wants to merge 3 commits into
mainfrom
aalej_py-init-prompt
Open

update the init functions python flow to find available python runtimes on user's machine#10673
aalej wants to merge 3 commits into
mainfrom
aalej_py-init-prompt

Conversation

@aalej

@aalej aalej commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Description

  • Test on Windows

We currently always try to use the latest python runtime when we set up functions

config.set("functions.runtime", latest("python"));

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.
image

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).
image

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

Scenarios Tested

firebase init functions - No Python3.14

  • Shows a list of old version of Python I have and sets up using the selected version

firebase init functions - With Python3.14

  • Skips the prompt and defaults setting up with Python3.14

Sample Commands

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/functions/python.ts
Comment on lines +80 to +97
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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);
}

Comment thread src/functions/python.ts
Comment on lines +54 to +75
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);
});
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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);
    });
  });
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants