Skip to content

feat(standalone): migrate standalone binaries to Node 26 Single Executable Applications (SEAs) - #10899

Open
joehan wants to merge 10 commits into
mainfrom
jh-sea-hybrid
Open

feat(standalone): migrate standalone binaries to Node 26 Single Executable Applications (SEAs)#10899
joehan wants to merge 10 commits into
mainfrom
jh-sea-hybrid

Conversation

@joehan

@joehan joehan commented Aug 6, 2026

Copy link
Copy Markdown
Member

Description

This PR migrates the firebase-tools standalone binaries to use Node.js 26 native Single Executable Applications (--build-sea) and esbuild, completely replacing the deprecated @yao-pkg/pkg compiler and eliminating the need for external binary injection tools like postject.


Architectural Highlights

  1. Native Node 26 SEA Packaging (--build-sea):
    • Compiles firepit.js and welcome.js with esbuild (--bundle --platform=node --target=node26).
    • Uses Node 26 native SEA compilation to inject JavaScript bundles and compressed assets into the executable without third-party binary injectors.
  2. In-Memory Subprocess Routing & Script Resolution:
    • When child processes are spawned via child_process.fork() or firebase is:node, firepit.js intercepts script arguments at the entrypoint and resolves them via createRequire.
    • Distinguishes executable binaries from JavaScript files to avoid CWD binary name collision errors.
    • Avoids packaging a duplicate 100 MB Node runtime binary inside the asset tarball, saving ~40 MB compressed download size and ~100 MB on-disk extraction footprint.
  3. Cross-Platform Runtime Wrappers:
    • Generates executable runtime wrappers with appropriate #!/bin/sh shebangs and PATH configuration for npm, node, firebase, and shell.
    • Transparently handles npm -c -- subshell script execution for package lifecycle scripts (e.g. protobufjs postinstall) and predeploy hooks (npm run lint, npm run build).
  4. Native Inline Shell Polyfill:
    • Replaces the heavy shelljs and user-home runtime dependencies with a native cross-platform polyfill (mkdir, rm, cp, chmod, ln, ls, cat, exec) backed by Node fs, os.homedir(), and child_process.spawnSync.
  5. Multi-Target Build Script:
    • standalone/build-sea.js downloads official Node 26 binaries and builds executables for:
      • Linux x86_64 (firebase-tools-linux)
      • macOS Apple Silicon (firebase-tools-macos-arm64)
      • macOS Intel (firebase-tools-macos-x64)
      • macOS Universal 2 (firebase-tools-macos via native lipo and codesign when on macOS)
      • Windows x86_64 (firebase-tools-win.exe)
    • Uses strictly native host tools without downloading third-party signing binaries.
    • Explicitly preserves project templates (including TypeScript init templates) while excluding unnecessary dev build artifacts.
  6. Graceful Logging Emulator Shutdown:
    • WebSocketTransport.stop() terminates active client sockets and includes a safety timeout to prevent shutdown hangs on single Ctrl+C.

Verification & Testing

  • Comprehensive 18-assertion automated test suite (scripts/test-sea-e2e.sh) executed and verified:
    • Clean environment purge & staging download
    • Version command reporting (15.26.0)
    • Global and subcommand help menus
    • Standalone setup diagnostic flag (--tool:setup-check)
    • Subprocess inline evaluation (firebase is:node -e, -p)
    • Subprocess core module resolution (crypto, fs, path, os)
    • Embedded NPM execution & package management (is:npm)
    • External child script execution via fork()
    • Exit code propagation (0 and non-zero)
    • Runtime wrapper execution (runtime/node, runtime/shell with -c --)
    • NPM lifecycle & predeploy script execution
    • Warm boot execution latency benchmark (< 1.5s)
    • Synthetic Firebase project emulator exec interface

…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-9635d3485b

wiz-9635d3485b Bot commented Aug 6, 2026

Copy link
Copy Markdown

Wiz Scan Summary

⚠️ Many findings detected
Many findings were detected, but only a subset of the findings are displayed inline due to API constraints. To view all findings inline, please click here.
Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings 20 Medium 41 Low
Software Management Finding Software Management Findings -
Total 20 Medium 41 Low

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio.

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

Comment thread standalone/build-sea.js Outdated
Comment thread standalone/build-sea.js
Comment thread standalone/build-sea.js
Comment thread standalone/build-sea.js
Comment thread standalone/firepit.js
Comment thread standalone/firepit.js
Comment thread standalone/build-sea.js
Comment thread scripts/firepit-builder/pipeline.js
Comment thread standalone/build-sea.js Fixed
Comment thread standalone/build-sea.js Fixed
Comment thread standalone/build-sea.js Fixed
Comment thread standalone/build-sea.js Fixed
@joehan

joehan commented Aug 14, 2026

Copy link
Copy Markdown
Member Author

/joe-review

Comment thread standalone/build-sea.js Outdated
Comment thread scripts/firepit-builder/pipeline.js
- 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
Comment thread scripts/firepit-builder/Dockerfile
@joehan
joehan requested review from andrewheard and samedson August 20, 2026 18:35

@samedson samedson 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.

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 the Authority=Developer ID Application: Google LLC (EQHXZ8M8AV) in the codesign --display command
  • The explicit requirement satisfied in the codesign --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;
  }
}

Comment thread standalone/build-sea.js
execSync(`lipo -create -output "${macUniversalBin}" "${macX64Bin}" "${macArm64Bin}"`, {
stdio: "inherit"
});
execSync(`codesign --sign - --force "${macUniversalBin}"`, { stdio: "inherit" });

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.

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.

Comment thread standalone/build-sea.js
} else if (fs.existsSync(macX64Bin) && !fs.existsSync(macUniversalBin)) {
fs.copyFileSync(macX64Bin, macUniversalBin);
fs.chmodSync(macUniversalBin, 0o755);
}

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.

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.

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.

4 participants