-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: migrate firepit standalone to node 26 sea #10892
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| # Firepit Standalone Executable Builder | ||
|
|
||
| Firepit packages `firebase-tools` into a Single Executable Application (SEA) | ||
| using Node.js 26 native `--build-sea` capabilities. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - **Node.js 26.0.0+** installed (required for native `--build-sea`). | ||
| - **macOS / Linux / Windows** operating system. | ||
| - **Xcode Command Line Tools** (macOS only, for `codesign` and `lipo`). | ||
|
|
||
| ## Quick Start (Local Development) | ||
|
|
||
| To build a standalone executable for your current architecture: | ||
|
|
||
| ```bash | ||
| # Ensure Node 26 is active | ||
| nvm use 26 | ||
|
|
||
| # Build the executable | ||
| npm run build:sea | ||
|
|
||
| # Run the binary | ||
| ./dist/firepit-darwin-arm64 --version | ||
| ``` | ||
|
|
||
| If Node 26 is not your default active Node version, set `NODE_BIN`: | ||
|
|
||
| ```bash | ||
| NODE_BIN=/path/to/node26/bin/node node build-sea.js | ||
| ``` | ||
|
|
||
| ## Architecture & Cross-Compilation | ||
|
|
||
| Firepit builds native Single Executable Applications for `arm64` and `x64` | ||
| architectures. | ||
|
|
||
| ### Building `arm64` Binaries (Apple Silicon) | ||
|
|
||
| Run the build using an `arm64` Node 26 runtime: | ||
|
|
||
| ```bash | ||
| NODE_BIN=/Users/$USER/.nvm/versions/node/v26.6.0/bin/node \ | ||
| node build-sea.js | ||
| ``` | ||
|
|
||
| Output: `dist/firepit-darwin-arm64` | ||
|
|
||
| ### Building `x64` Binaries (Intel / Rosetta) | ||
|
|
||
| Run the build using an `x64` Node 26 runtime: | ||
|
|
||
| ```bash | ||
| NODE_BIN=/Users/$USER/.nvm/versions/node/v26.6.0-x64/bin/node \ | ||
| node build-sea.js | ||
| ``` | ||
|
|
||
| Output: `dist/firepit-darwin-x64` | ||
|
|
||
| ## Creating Universal Binaries (macOS Universal 2) | ||
|
|
||
| To combine `arm64` and `x64` macOS executables into a single Universal 2 | ||
| binary using Apple's `lipo` utility: | ||
|
|
||
| ```bash | ||
| # 1. Combine binaries with lipo | ||
| lipo -create -output dist/firebase-tools-darwin-universal \ | ||
| dist/firepit-darwin-arm64 \ | ||
| dist/firepit-darwin-x64 | ||
|
|
||
| # 2. Re-sign the universal binary with ad-hoc signature | ||
| codesign --sign - --force dist/firebase-tools-darwin-universal | ||
|
|
||
| # 3. Test execution | ||
| ./dist/firebase-tools-darwin-universal --version | ||
| ``` | ||
|
|
||
| Verify architecture support with `file`: | ||
|
|
||
| ```bash | ||
| file dist/firebase-tools-darwin-universal | ||
| ``` | ||
|
|
||
| Expected output: | ||
| `dist/firebase-tools-darwin-universal:` | ||
| ` Mach-O universal binary with 2 architectures` | ||
|
|
||
| ## Production Release Pipeline | ||
|
|
||
| To build official release tarballs and headless binaries end-to-end: | ||
|
|
||
| ```bash | ||
| cd ../scripts/firepit-builder | ||
| NODE_BIN=/path/to/node26/bin/node node ./pipeline.js \ | ||
| --package="/path/to/firebase-tools" | ||
| ``` | ||
|
|
||
| Artifacts are output to `/tmp/firepit_artifacts/`. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,98 @@ | ||||||||||||||||||||||||||||||
| const fs = require("fs"); | ||||||||||||||||||||||||||||||
| const path = require("path"); | ||||||||||||||||||||||||||||||
| const { execSync } = require("child_process"); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const standaloneDir = __dirname; | ||||||||||||||||||||||||||||||
| const distDir = path.join(standaloneDir, "dist"); | ||||||||||||||||||||||||||||||
| const vendorDir = path.join(standaloneDir, "vendor"); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const configPath = path.join(standaloneDir, "config.js"); | ||||||||||||||||||||||||||||||
| if (!fs.existsSync(configPath)) { | ||||||||||||||||||||||||||||||
| fs.copyFileSync(path.join(standaloneDir, "config.template.js"), configPath); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const esbuildBin = path.join(standaloneDir, "node_modules", ".bin", "esbuild"); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function getNodeBinaryPath() { | ||||||||||||||||||||||||||||||
| if (process.env.NODE_BIN && fs.existsSync(process.env.NODE_BIN)) { | ||||||||||||||||||||||||||||||
| return process.env.NODE_BIN; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return process.execPath; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const hostNodeBin = getNodeBinaryPath(); | ||||||||||||||||||||||||||||||
| console.log(`[build-sea] Using host Node binary: ${hostNodeBin}`); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| console.log("[build-sea] 1. Bundling firepit JS with esbuild..."); | ||||||||||||||||||||||||||||||
| const bundlePath = path.join(distDir, "firepit-bundle.js"); | ||||||||||||||||||||||||||||||
| execSync( | ||||||||||||||||||||||||||||||
| `"${esbuildBin}" "${path.join(standaloneDir, "firepit.js")}" --bundle --platform=node --target=node26 --outfile="${bundlePath}"`, | ||||||||||||||||||||||||||||||
| { stdio: "inherit", cwd: standaloneDir } | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const welcomeSrc = path.join(standaloneDir, "welcome.js"); | ||||||||||||||||||||||||||||||
| if (fs.existsSync(welcomeSrc)) { | ||||||||||||||||||||||||||||||
| fs.copyFileSync(welcomeSrc, path.join(distDir, "welcome.js")); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| console.log("[build-sea] 2. Packaging vendor directory..."); | ||||||||||||||||||||||||||||||
| const vendorTarPath = path.join(distDir, "vendor.tar.gz"); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Ensure vendor bin directory contains standard Node binary | ||||||||||||||||||||||||||||||
| const vendorBinDir = path.join(vendorDir, "bin"); | ||||||||||||||||||||||||||||||
| if (fs.existsSync(vendorDir)) { | ||||||||||||||||||||||||||||||
| if (!fs.existsSync(vendorBinDir)) { | ||||||||||||||||||||||||||||||
| fs.mkdirSync(vendorBinDir, { recursive: true }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| const isWin = process.platform === "win32"; | ||||||||||||||||||||||||||||||
| const nodeBinName = isWin ? "node.exe" : "node"; | ||||||||||||||||||||||||||||||
| const targetNodeBin = path.join(vendorBinDir, nodeBinName); | ||||||||||||||||||||||||||||||
| fs.copyFileSync(hostNodeBin, targetNodeBin); | ||||||||||||||||||||||||||||||
| fs.chmodSync(targetNodeBin, 0o755); | ||||||||||||||||||||||||||||||
| execSync( | ||||||||||||||||||||||||||||||
| `tar -czf "${vendorTarPath}" --exclude="*.map" --exclude="*.md" --exclude="*.ts" --exclude="*.d.ts" --exclude="test" --exclude="tests" --exclude="docs" -C "${vendorDir}" .`, | ||||||||||||||||||||||||||||||
| { stdio: "inherit" } | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| // Minimal dummy archive if vendor directory does not exist yet | ||||||||||||||||||||||||||||||
| const tempVendor = path.join(distDir, "temp_vendor"); | ||||||||||||||||||||||||||||||
| fs.mkdirSync(tempVendor, { recursive: true }); | ||||||||||||||||||||||||||||||
| execSync(`tar -czf "${vendorTarPath}" -C "${tempVendor}" .`, { stdio: "inherit" }); | ||||||||||||||||||||||||||||||
| fs.rmSync(tempVendor, { recursive: true, force: true }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| console.log("[build-sea] 3. Building Node 26 Single Executable Application..."); | ||||||||||||||||||||||||||||||
| const isWin = process.platform === "win32"; | ||||||||||||||||||||||||||||||
| const isMac = process.platform === "darwin"; | ||||||||||||||||||||||||||||||
| let arch = process.arch; | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| const fileOut = execSync(`file "${hostNodeBin}"`, { encoding: "utf8" }); | ||||||||||||||||||||||||||||||
| if (fileOut.includes("x86_64")) arch = "x64"; | ||||||||||||||||||||||||||||||
| else if (fileOut.includes("arm64")) arch = "arm64"; | ||||||||||||||||||||||||||||||
| } catch (e) {} | ||||||||||||||||||||||||||||||
|
Comment on lines
+67
to
+72
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On macOS, the official Node.js binaries are typically universal binaries containing both
Suggested change
|
||||||||||||||||||||||||||||||
| let targetBinaryName = `firepit-${process.platform}-${arch}`; | ||||||||||||||||||||||||||||||
| if (isWin) targetBinaryName += ".exe"; | ||||||||||||||||||||||||||||||
| const targetBinaryPath = path.join(distDir, targetBinaryName); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const seaConfigPath = path.join(distDir, "sea-config.json"); | ||||||||||||||||||||||||||||||
| const seaConfig = { | ||||||||||||||||||||||||||||||
| main: bundlePath, | ||||||||||||||||||||||||||||||
| output: targetBinaryPath, | ||||||||||||||||||||||||||||||
| executable: hostNodeBin, | ||||||||||||||||||||||||||||||
| disableExperimentalSEAWarning: true, | ||||||||||||||||||||||||||||||
| assets: { | ||||||||||||||||||||||||||||||
| "vendor.tar.gz": vendorTarPath | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
| fs.writeFileSync(seaConfigPath, JSON.stringify(seaConfig, null, 2)); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Native Node 26 --build-sea workflow | ||||||||||||||||||||||||||||||
| execSync(`"${hostNodeBin}" --build-sea "${seaConfigPath}"`, { stdio: "inherit" }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (isMac) { | ||||||||||||||||||||||||||||||
| console.log(`[build-sea] Ad-hoc signing macOS binary ${targetBinaryName}...`); | ||||||||||||||||||||||||||||||
| execSync(`codesign --sign - --force "${targetBinaryPath}"`, { stdio: "inherit" }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| fs.chmodSync(targetBinaryPath, 0o755); | ||||||||||||||||||||||||||||||
| console.log(`[build-sea] Successfully created SEA binary: ${targetBinaryPath}`); | ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of spawning a child process to run the
esbuildCLI, which can be fragile on Windows due to shell execution and file extension differences, you can use theesbuildJavaScript API directly. This is faster, more reliable, and fully cross-platform.