From d9a0e4676d9d5e31f4a1d636f472eeb96075aa6e Mon Sep 17 00:00:00 2001 From: aalej Date: Wed, 17 Jun 2026 23:00:18 +0800 Subject: [PATCH 1/3] add prompt to let user select python runtime --- src/functions/python.ts | 50 ++++++++++++++++ src/init/features/functions/python.ts | 86 ++++++++++++++++++++++----- 2 files changed, 122 insertions(+), 14 deletions(-) diff --git a/src/functions/python.ts b/src/functions/python.ts index a56bac9bed6..bf92a5b146d 100644 --- a/src/functions/python.ts +++ b/src/functions/python.ts @@ -3,6 +3,8 @@ import { spawn } from "cross-spawn"; import * as cp from "child_process"; import { logger } from "../logger"; import { IS_WINDOWS } from "../utils"; +import * as supported from "../deploy/functions/runtimes/supported"; +import { getPythonBinary } from "../deploy/functions/runtimes/python"; /** * Default directory for python virtual environment. @@ -45,3 +47,51 @@ export function runWithVirtualEnv( env: envs as any, }); } + +/** + * Check if a python binary is available and return its version. + */ +export async function checkPythonVersion(binary: string): Promise { + 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); + }); + }); +} + +/** + * Get all available python runtimes on the user machine. + */ +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 => !!r); +} diff --git a/src/init/features/functions/python.ts b/src/init/features/functions/python.ts index 4999409f649..4d37de345b1 100644 --- a/src/init/features/functions/python.ts +++ b/src/init/features/functions/python.ts @@ -1,13 +1,16 @@ import { ChildProcess } from "child_process"; +import * as ora from "ora"; import { wrapSpawn } from "../../spawn"; import { Config } from "../../../config"; import { getPythonBinary } from "../../../deploy/functions/runtimes/python"; -import { runWithVirtualEnv } from "../../../functions/python"; -import { confirm } from "../../../prompt"; -import { latest } from "../../../deploy/functions/runtimes/supported"; +import { getAvailablePythonRuntimes, runWithVirtualEnv } from "../../../functions/python"; +import { confirm, select } from "../../../prompt"; +import { latest, Runtime, RuntimeOf } from "../../../deploy/functions/runtimes/supported"; import { readTemplateSync } from "../../../templates"; import { FirebaseError } from "../../../error"; +import { logger } from "../../../logger"; +import { configForCodebase } from "../../../functions/projectConfig"; const MAIN_TEMPLATE = readTemplateSync("init/functions/python/main.py"); const REQUIREMENTS_TEMPLATE = readTemplateSync("init/functions/python/requirements.txt"); @@ -27,9 +30,12 @@ function waitForProcess(p: ChildProcess): Promise { * Helper to spawn a process and create a python virtual environment. */ async function createVirtualEnv(pythonBinary: string, cwd: string): Promise { + const spinner = ora("Creating Python virtual environment...").start(); try { await wrapSpawn(pythonBinary, ["-m", "venv", "venv"], cwd); + spinner.succeed("Created Python virtual environment."); } catch (err) { + spinner.fail("Failed to create Python virtual environment."); throw new FirebaseError( `Failed to create virtual environment. Please make sure Python is installed and in your PATH.`, ); @@ -66,6 +72,47 @@ async function installDependencies(pythonBinary: string, cwd: string): Promise; version: string }[], +): Promise> { + const latestPythonRuntime = latest("python"); + let selectedRuntime: Runtime & RuntimeOf<"python">; + + const isLatestRuntimeAvailable = availableRuntimes.some((r) => r.runtime === latestPythonRuntime); + // If the latest runtime is available, use it by default + if (isLatestRuntimeAvailable) { + selectedRuntime = latestPythonRuntime; + } else if (availableRuntimes.length >= 1) { + const choices = availableRuntimes.map((r) => ({ + name: `${r.runtime} (${r.version})`, + value: r.runtime, + })); + if (!choices.some((c) => c.value === latestPythonRuntime)) { + choices.push({ + name: `${latestPythonRuntime} (not available on your machine)`, + value: latestPythonRuntime, + }); + } + const runtime = await select({ + message: "Which Python runtime would you like to use for this codebase?", + default: choices[0].value, + choices, + }); + + selectedRuntime = runtime; + } else { + logger.warn( + "No Python runtimes were detected on your machine. " + + `The latest supported runtime ${latestPythonRuntime} will be set in your config, but you may encounter issues deploying or emulating functions.`, + ); + selectedRuntime = latestPythonRuntime; + } + return selectedRuntime; +} + /** * Create a Python Firebase Functions project. */ @@ -79,21 +126,32 @@ export async function setup(setup: any, config: Config): Promise { await config.askWriteProjectFile(`${setup.functions.source}/.gitignore`, GITIGNORE_TEMPLATE); await config.askWriteProjectFile(`${setup.functions.source}/main.py`, MAIN_TEMPLATE); - // Write the latest supported runtime version to the config. - config.set("functions.runtime", latest("python")); + const availableRuntimes = await getAvailablePythonRuntimes(); + const selectedRuntime = await promptSelectRuntime(availableRuntimes); + const cbconfig = configForCodebase(setup.config.functions, setup.functions.codebase); + cbconfig.runtime = selectedRuntime; + + // Write the selected runtime version to the config. + config.set("functions.runtime", selectedRuntime); // Add python specific ignores to config. config.set("functions.ignore", ["venv", "__pycache__"]); // We resolve the version-specific Python binary (e.g. python3.10) to ensure we execute // the correct version of Python chosen for the functions codebase. - const pythonBinary = getPythonBinary(latest("python")); - await createVirtualEnv(pythonBinary, sourceDir); - - const install = await confirm({ - message: "Do you want to install dependencies now?", - default: true, - }); - if (install) { - await installDependencies(pythonBinary, sourceDir); + const pythonBinary = getPythonBinary(selectedRuntime); + const runtimeDetected = availableRuntimes.some((r) => r.runtime === selectedRuntime); + if (runtimeDetected) { + await createVirtualEnv(pythonBinary, sourceDir); + const install = await confirm({ + message: "Do you want to install dependencies now?", + default: true, + }); + if (install) { + await installDependencies(pythonBinary, sourceDir); + } + } else { + logger.info( + `Skipping virtual environment creation because ${selectedRuntime} is not available on your machine.`, + ); } } From 0af42d5f9aa2d8b8207872b72891860bfb178d5d Mon Sep 17 00:00:00 2001 From: aalej Date: Thu, 18 Jun 2026 01:14:54 +0800 Subject: [PATCH 2/3] update tests --- package.json | 1 + src/init/features/functions.spec.ts | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/package.json b/package.json index 8c427e701df..90ab2fd897e 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "lint:ts": "eslint --cache --config .eslintrc.js --ext .ts,.js .", "mocha:fast": "mocha 'src/**/*.spec.{ts,js}'", "mocha": "nyc mocha 'src/**/*.spec.{ts,js}'", + "test:custom": "nyc mocha 'src/init/features/functions.spec.ts'", "prepare": "npm run clean && npm run build:publish", "test": "npm run lint:quiet && npm run test:compile && npm run mocha", "test:appdistribution": "npm run lint:quiet && npm run test:compile && nyc mocha 'src/appdistribution/*.spec.{ts,js}'", diff --git a/src/init/features/functions.spec.ts b/src/init/features/functions.spec.ts index e938d1cdbc4..587a75f0b93 100644 --- a/src/init/features/functions.spec.ts +++ b/src/init/features/functions.spec.ts @@ -10,6 +10,7 @@ import { RC } from "../../rc"; import * as experiments from "../../experiments"; import * as spawn from "cross-spawn"; import * as initSpawn from "../spawn"; +import * as pythonUtils from "../../functions/python"; const TEST_SOURCE_DEFAULT = "functions"; const TEST_CODEBASE_DEFAULT = "default"; @@ -135,16 +136,22 @@ describe("functions", () => { describe("python project", () => { let spawnStub: sinon.SinonStub; let wrapSpawnStub: sinon.SinonStub; + let getAvailablePythonRuntimesStub: sinon.SinonStub; beforeEach(() => { spawnStub = sandbox.stub(spawn, "spawn"); wrapSpawnStub = sandbox.stub(initSpawn, "wrapSpawn"); + getAvailablePythonRuntimesStub = sandbox.stub(pythonUtils, "getAvailablePythonRuntimes"); + getAvailablePythonRuntimesStub.resolves([ + { runtime: "python314", binary: "python3.14", version: "Python 3.14.0" }, + ]); }); it("creates a new python codebase with the correct configuration", async () => { const config = new Config("{}", { projectDir: "test", cwd: "test" }); const setup = { config: { functions: [] }, rcfile: {} }; prompt.select.onFirstCall().resolves("python"); + prompt.select.onSecondCall().resolves("python314"); // do not install dependencies prompt.confirm.onFirstCall().resolves(false); askWriteProjectFileStub = sandbox.stub(config, "askWriteProjectFile"); From 0c0d2caf9e7f8555ddf5dcc0d148e4fa7be80184 Mon Sep 17 00:00:00 2001 From: aalej Date: Thu, 18 Jun 2026 01:32:27 +0800 Subject: [PATCH 3/3] remove test:custom in package.json --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 90ab2fd897e..8c427e701df 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "lint:ts": "eslint --cache --config .eslintrc.js --ext .ts,.js .", "mocha:fast": "mocha 'src/**/*.spec.{ts,js}'", "mocha": "nyc mocha 'src/**/*.spec.{ts,js}'", - "test:custom": "nyc mocha 'src/init/features/functions.spec.ts'", "prepare": "npm run clean && npm run build:publish", "test": "npm run lint:quiet && npm run test:compile && npm run mocha", "test:appdistribution": "npm run lint:quiet && npm run test:compile && nyc mocha 'src/appdistribution/*.spec.{ts,js}'",