feat(standalone): migrate standalone binaries to Node 26 Single Executable Applications (SEAs) - #10899
feat(standalone): migrate standalone binaries to Node 26 Single Executable Applications (SEAs)#10899joehan wants to merge 10 commits into
Conversation
…table Applications (SEAs) - Replace legacy @yao-pkg/pkg with native Node 26 --build-sea and esbuild - Intercept child process execution via in-memory script routing with createRequire to eliminate duplicate Node binary - Replace shelljs runtime dependency with native fs/child_process shell polyfill - Add multi-target build script supporting Linux x64, macOS Universal 2 (arm64 + x64 with lipo), and Windows x64 - Update firepit-builder pipeline to produce SEA artifacts and SHA256 checksums - Document architecture and build workflows in standalone/README.md
Wiz Scan Summary
|
| Scanner | Findings |
|---|---|
| - | |
| - | |
| - | |
| - | |
| 20 |
|
| - | |
| Total | 20 |
To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio.
There was a problem hiding this comment.
Code Review
This pull request migrates the Firepit standalone executable builder from @yao-pkg/pkg to Node.js 26's native Single Executable Application (--build-sea) capabilities and esbuild, removing external dependencies like shelljs in favor of a native inline shell polyfill. Feedback on these changes highlights several critical cross-platform and reliability issues: Unix-specific commands (cp -R and find) are used in build-sea.js which will fail on Windows; unconsumed response streams during HTTP redirects or errors in downloadFile can cause socket leaks; and reading directories without verifying they are files in the pipeline script poses an EISDIR crash risk. Additionally, in firepit.js, executing child scripts via createRequire fails to set up require.main properly, and ImitateNode() does not account for leading Node.js options.
…EA build for automatic cache invalidation
…al binary creation via rcodesign in Linux builds and Dockerfile
…one binary validation
…n, templates bundling, and shebangs
…runtime/node wrapper to is:node
|
/joe-review |
- build-sea.js: use cross-platform fs.cpSync instead of shell cp, add recursive .node file deletion helper, return undefined on rcodesign tool failure, and resume response streams on redirects/errors - firepit.js: set process.argv[1] and execute child scripts with Module.runMain(), parse leading options into execArgv in ImitateNode() - pipeline.js: isolate and stage headless artifacts to outputDir before headful build to avoid binary overwrite, add defensive isFile() check for checksum calculation, and fix Prettier formatting
…e codesign/lipo on macOS only
There was a problem hiding this comment.
TLDR; I believe you need a signing certificate and Apple Developer login credentials stored in secrets so this is signed by Google.
How to verify:
Here's some commands you can run to verify you've done everything correctly. It might even be useful to have this as a verification check in the build process. Crashlytics' upload-symbols is signed correctly so you can compare with that to check your work:
Important parts are:
- The
flags=0x10000(runtime)and theAuthority=Developer ID Application: Google LLC (EQHXZ8M8AV)in thecodesign --displaycommand - The
explicit requirement satisfiedin thecodesign --test-requirement="=notarized"command
$ git clone git@github.com:firebase/firebase-ios-sdk.git
$ codesign --display --verbose=4 ./firebase-ios-sdk/Crashlytics/upload-symbols
Executable=/Users/samedson/Firebase/firebase-ios-sdk/Crashlytics/upload-symbols
Identifier=com.firebase.crashlytics.upload-symbols
Format=Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=3187 flags=0x10000(runtime) hashes=93+2 location=embedded
VersionPlatform=1
VersionMin=720896
VersionSDK=1704448
Hash type=sha256 size=32
CandidateCDHash sha256=4ed69405397835831f8131ccb8d0f3c81a3369d6
CandidateCDHashFull sha256=4ed69405397835831f8131ccb8d0f3c81a3369d664693e5d0d95b9a0e3270f1d
Hash choices=sha256
CMSDigest=4ed69405397835831f8131ccb8d0f3c81a3369d664693e5d0d95b9a0e3270f1d
CMSDigestType=2
Executable Segment base=0
Executable Segment limit=245760
Executable Segment flags=0x1
Page size=4096
CDHash=4ed69405397835831f8131ccb8d0f3c81a3369d6
Signature size=8989
Authority=Developer ID Application: Google LLC (EQHXZ8M8AV)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=Mar 31, 2026 at 5:07:01 PM
Info.plist entries=19
TeamIdentifier=EQHXZ8M8AV
Runtime Version=26.2.0
Sealed Resources=none
Internal requirements count=1 size=200
$ codesign --test-requirement="=notarized" --verify --verbose ./firebase-ios-sdk/Crashlytics/upload-symbols
./firebase-ios-sdk/Crashlytics/upload-symbols: valid on disk
./firebase-ios-sdk/Crashlytics/upload-symbols: satisfies its Designated Requirement
./firebase-ios-sdk/Crashlytics/upload-symbols: explicit requirement satisfied
AI Example for correct codesigning:
// Define these variables at the top of your script or in your environment variables
const DEVELOPER_ID = "Developer ID Application: Your Company LLC (TEAMID)";
// Update the signing line in both the single-architecture loop and the universal binary block:
execSync(
`codesign --sign "${DEVELOPER_ID}" --options runtime --timestamp --force "${outputBinaryPath}"`,
{ stdio: "inherit" }
);
AI Example for notarization:
if (process.platform === "darwin") {
console.log("[build-sea] Step 6: Submitting to Apple Notary Service...");
try {
// Submit the universal binary for notarization
execSync(
`xcrun notarytool submit "${macUniversalBin}" ` +
`--apple-id "${process.env.APPLE_ID}" ` +
`--password "${process.env.APPLE_PASSWORD}" ` +
`--team-id "${process.env.APPLE_TEAM_ID}" ` +
`--wait`, // This keeps the script running until Apple approves/rejects the binary
{ stdio: "inherit" }
);
console.log("[build-sea] Notarization successful! Stapling ticket...");
// Staple the notarization ticket directly to the binary for offline validation
execSync(`xcrun stapler staple "${macUniversalBin}"`, { stdio: "inherit" });
console.log("[build-sea] Binary successfully signed, notarized, and stapled!");
} catch (err) {
console.error("[build-sea] Error during macOS notarization:", err.message);
throw err;
}
}
| execSync(`lipo -create -output "${macUniversalBin}" "${macX64Bin}" "${macArm64Bin}"`, { | ||
| stdio: "inherit" | ||
| }); | ||
| execSync(`codesign --sign - --force "${macUniversalBin}"`, { stdio: "inherit" }); |
There was a problem hiding this comment.
So I think this will only codesign you for your current machine. To codesign for distribution, you'll want to get a signing certificate. Google has processes for this. You'll likely need to store it in GitHub secrets or on the machines of devs doing the release.
| } else if (fs.existsSync(macX64Bin) && !fs.existsSync(macUniversalBin)) { | ||
| fs.copyFileSync(macX64Bin, macUniversalBin); | ||
| fs.chmodSync(macUniversalBin, 0o755); | ||
| } |
There was a problem hiding this comment.
In this section, you may also want to do notarization. I believe some users complained when we released a binary that wasn't notarized. Similar to codesigning, there are macos utilities to do this.
Description
This PR migrates the
firebase-toolsstandalone binaries to use Node.js 26 native Single Executable Applications (--build-sea) andesbuild, completely replacing the deprecated@yao-pkg/pkgcompiler and eliminating the need for external binary injection tools likepostject.Architectural Highlights
--build-sea):firepit.jsandwelcome.jswithesbuild(--bundle --platform=node --target=node26).child_process.fork()orfirebase is:node,firepit.jsintercepts script arguments at the entrypoint and resolves them viacreateRequire.#!/bin/shshebangs and PATH configuration fornpm,node,firebase, andshell.-c --subshell script execution for package lifecycle scripts (e.g.protobufjspostinstall) and predeploy hooks (npm run lint,npm run build).shelljsanduser-homeruntime dependencies with a native cross-platform polyfill (mkdir,rm,cp,chmod,ln,ls,cat,exec) backed by Nodefs,os.homedir(), andchild_process.spawnSync.standalone/build-sea.jsdownloads official Node 26 binaries and builds executables for:firebase-tools-linux)firebase-tools-macos-arm64)firebase-tools-macos-x64)firebase-tools-macosvia nativelipoandcodesignwhen on macOS)firebase-tools-win.exe)WebSocketTransport.stop()terminates active client sockets and includes a safety timeout to prevent shutdown hangs on singleCtrl+C.Verification & Testing
scripts/test-sea-e2e.sh) executed and verified:15.26.0)--tool:setup-check)firebase is:node -e,-p)crypto,fs,path,os)is:npm)fork()runtime/node,runtime/shellwith-c --)