feat(sea): let users override the base Node binary#279
Conversation
SEA mode previously always downloaded a base Node binary from nodejs.org, so the resulting executable was pinned to an official build. That makes it impossible to ship a SEA on top of a custom runtime -- e.g. a Node built against an older glibc (to run on EL7 / older distros), or one configured differently. The internal SeaOptions / SeaEnhancedOptions already accepted `nodePath` / `useLocalNode`; this just exposes them: --sea-node-path <path> embed a specific Node binary as the base --sea-use-local-node embed the Node running pkg (process.execPath) Both are also settable in the pkg config (seaNodePath / seaUseLocalNode) and via the programmatic exec() API. The two are mutually exclusive. The embedded binary's major version must match the target's (the existing version-skew checks still apply). This is also a prerequisite for adopting `node --build-sea` (Node >= 25.5): that flag is only meaningful once the base Node is no longer a fixed download. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
robertsLando
left a comment
There was a problem hiding this comment.
Recommend reworking around PKG_NODE_PATH rather than new SEA-only flags
pkg already has a documented way to supply a custom base Node binary — the PKG_NODE_PATH env var (pkg-fetch localPlace()), used by standard mode. It does not flow into SEA today: SEA downloads from nodejs.org via its own path in lib/sea.ts, and docs-site/guide/custom-node.md explicitly documents that gap ("SEA mode … does not honour PKG_NODE_PATH … For SEA, use process.execPath … directly").
This PR closes the gap but introduces a second, SEA-only mechanism instead of extending the one that already exists. I'd lean toward the smaller surface / one mental model:
| Layer | Recommendation |
|---|---|
Honor PKG_NODE_PATH in SEA |
Add — fold the env var into the opts.nodePath fallback in getNodejsExecutable (~2 lines). Matches standard mode and retires the custom-node.md contradiction. |
seaNodePath config key + programmatic option |
Keep — the only thing env can't do: live in package.json pkg config / be passed to exec({…}). |
--sea-node-path CLI flag |
Optional — redundant with the env var; keep only for --help discoverability. |
--sea-use-local-node flag |
Drop — it's exactly PKG_NODE_PATH="$(command -v node)". |
| Single-target / platform-match guard | Required regardless of interface (see inline on lib/index.ts). |
Precedence stays pkg's existing convention: CLI > config (seaNodePath) > PKG_NODE_PATH env.
Net: env var + config key + guard is a smaller change than this PR while covering more — it makes PKG_NODE_PATH work in SEA, which the flags alone don't.
The inline threads below are correctness/doc/test findings that apply whichever interface ships.
Nit (not in this diff): pre-existing typo Priovided → Provided at lib/sea.ts:364, in the explicit-path branch your nodePath override now exercises — worth fixing while here.
| // Base-node override (both forms are mutually exclusive). nodePath embeds a | ||
| // specific binary; useLocalNode embeds the Node running pkg. Either lets you | ||
| // ship a SEA built on a custom runtime (e.g. one linked against older glibc). | ||
| if (flags.seaNodePath && flags.seaUseLocalNode) { |
There was a problem hiding this comment.
🔴 Correctness — multi-target silently bakes the wrong binary. The mutual-exclusion guard is good, but the bigger footgun is unguarded: getNodejsExecutable (lib/sea.ts) returns this single binary for every target regardless of platform/arch. With --targets node22-linux-x64,node22-macos-arm64 --sea-node-path /linux/node, both outputs get the linux binary — the macOS output is silently corrupt. --sea-use-local-node is worse: the host's node gets baked into every cross-platform target.
Add a guard here: when seaNodePath / seaUseLocalNode (or PKG_NODE_PATH) is set, require a single target whose platform+arch match the supplied binary. Standard-mode PKG_NODE_PATH carries the same "single target only" caveat (documented in custom-node.md), but there it falls back to fetching the other platforms — here it produces a broken artifact instead.
| /** | ||
| * SEA mode: path to a base Node binary to embed instead of downloading one | ||
| * from nodejs.org. Use to embed a custom build (e.g. one linked against an | ||
| * older glibc). Its major version must match the target's. |
There was a problem hiding this comment.
🟠 This claim isn't enforced. "Its major version must match the target's" — nothing checks it. assertSingleTargetMajor only compares the targets' nodeRange to each other, never to the supplied binary's actual version. So --targets node24 --sea-node-path /node22/bin/node silently produces a working node22 SEA and drops the requested node24.
Either validate major(nodePath --version) === major(targetRange) (cheap — resolveTargetNodeVersion already runs node --version), or reword the doc to say the supplied binary overrides the requested nodeRange.
| --no-signature skip macOS binary signing [default: sign] | ||
| --sea (Experimental) compile given file using node's SEA feature. Requires node v20.0.0 or higher and only single file is supported | ||
| --sea-node-path SEA mode: path to a base Node binary to embed instead of downloading one (must match the target's major version) | ||
| --sea-use-local-node SEA mode: embed the Node binary running pkg (process.execPath) as the base |
There was a problem hiding this comment.
🟠 Docs now stale/contradictory. docs-site/guide/custom-node.md currently tells users SEA "does not honour PKG_NODE_PATH … For SEA, use process.execPath … directly." After this PR that page is wrong and needs updating in the same PR. (If you adopt the PKG_NODE_PATH-in-SEA approach from the review summary, this becomes a one-line "now works in SEA too" rather than documenting a second mechanism.)
| // `--sea-use-local-node` embeds the Node binary running pkg as the SEA base | ||
| // instead of downloading one. Exercises the base-node override end to end (and, | ||
| // on Node >= 25.5 hosts, the in-core `--build-sea` injection path). | ||
| utils.runSeaHostOnly(input, testName, ['--sea-use-local-node']); |
There was a problem hiding this comment.
🟡 --sea-node-path has no e2e coverage. Only --sea-use-local-node gets a runtime test here; the explicit --sea-node-path branch (the exists() check + node --version probe in lib/sea.ts) is never exercised end to end. Cheap add: a second host-only case passing --sea-node-path = the host's process.execPath to hit that branch.
Ah, I initially attempted to use PKG_NODE_PATH, from reading the docs at https://www.npmjs.com/package/@yao-pkg/pkg-fetch, thinking that PKG_NODE_PATH might be honored by the SEA code path, and then found (empirically) that this wasn't the case.
D'oh! I completely missed https://yao-pkg.github.io/pkg/guide/custom-node -- sorry. I'll see if I can implement the suggestions from review shortly 👍 |
…back) Addresses the review on yao-pkg#279 — extend the existing mechanism rather than add a SEA-only one: - Honour PKG_NODE_PATH in SEA: fold it into getNodejsExecutable's nodePath fallback (and the version resolver), matching standard mode. Precedence is CLI (--sea-node-path) > config (seaNodePath) > PKG_NODE_PATH. - Drop the --sea-use-local-node flag + seaUseLocalNode config key — it was just PKG_NODE_PATH="$(command -v node)". Keep --sea-node-path (for --help discoverability) and the seaNodePath config / exec() option. - Required guard: a custom base binary is restricted to a single target whose major matches the binary; otherwise pkg errors instead of silently baking the wrong binary into other targets' outputs. SEA has no per-platform fetch fallback like standard mode, and assertSingleTargetMajor only compares the targets to each other, never to the supplied binary. - Update docs-site/guide/custom-node.md (it previously said SEA does not honour PKG_NODE_PATH). - Tests: drop the seaUseLocalNode unit cases; rework the e2e (now test-95-sea-custom-node) to cover both --sea-node-path and PKG_NODE_PATH. - Fix pre-existing typo "Priovided" -> "Provided" in lib/sea.ts:364. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@robertsLando I think I've addressed everything from review. When this is ready, I can squash the history down to a single commit, if you'd like (left the commits split for now, in case that helps with review). |
… target Follow-up to the yao-pkg#279 review. The earlier single-target guard stopped multi-target runs, but a SINGLE target with a mismatched binary still slipped through — e.g. `--targets node24-linux-x64 --sea-node-path <macOS node>` silently produced a corrupt linux output. Per the reviewer's ask ("require a single target whose platform+arch match the supplied binary"): - Sniff the supplied binary's container format (ELF / Mach-O / PE) and CPU arch from its magic bytes (sniffBinaryTarget) and reject a mismatch against the target — a Mach-O binary for a `linux` target, or an x64 binary for `arm64`. - Reject targets that span more than one distinct platform+arch: one binary can't be several at once, including linux vs alpine vs linuxstatic (mutually exclusive glibc / musl / static — indistinguishable from the header, but the user clearly can't have meant them simultaneously). This replaces the blunt single-target-count check with a per-(platform,arch) one. - The ELF glibc/musl/static flavor isn't in the header, so matching that to linux / alpine / linuxstatic remains the user's responsibility (documented). Tests: a unit test for sniffBinaryTarget (crafted ELF/Mach-O/PE headers), and unhappy-path e2e cases in test-95-sea-custom-node (multi platform/arch, multi linux-flavor, wrong platform, wrong major — all host-independent, exit 2). Updates --help + docs-site/guide/custom-node.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Sorry, I just just realized I missed part of this:
Latest commit sniffs the executable's headers to determine container format and architecture, and then I additionally assert that there isn't some disagreement between the given With that, I think that's everything. |
robertsLando
left a comment
There was a problem hiding this comment.
assertCustomBaseNodeTarget / sniffBinaryTarget — simpler approach
The binary-format sniffing here is more machinery than the problem needs. sniffBinaryTarget parses ELF/Mach-O/PE magic bytes with three machine-code lookup tables (ELF_MACHINE, MACHO_CPU, PE_MACHINE), plus BinaryFormat, formatForPlatform, and FORMAT_LABEL — ~90 lines whose whole job is to guess a binary's platform/arch from its header.
But the base binary is a Node executable, and step 3 already runs it (execFileAsync(binPath, ['--version'])). If we're running it anyway, just ask it everything in one shot:
node -p "process.version + ' ' + process.platform + ' ' + process.arch"
The key point: sniffing's only theoretical edge over running the binary is validating a base that can't run on the build host (a cross-platform base). But step 3's version check already execs the binary unconditionally for a custom path — and so does resolveTargetNodeVersion. So a non-host-runnable base already fails today, before sniffing helps. Sniffing buys nothing the PR actually delivers; it's just inconsistent with the exec already happening (and runs --version twice).
Suggested shape
Keep step 1 (the multi-target guard — simple and useful). Replace the sniff + version exec with one probe:
/** What a Node binary reports about itself when run. */
async function probeNode(
binPath: string,
): Promise<{ version: string; platform: string; arch: string }> {
if (binPath === process.execPath) {
return { version: process.version, platform: process.platform, arch: process.arch };
}
const { stdout } = await execFileAsync(binPath, [
'-p',
"process.version + ' ' + process.platform + ' ' + process.arch",
]);
const [version, platform, arch] = stdout.trim().split(' ');
return { version, platform, arch };
}
/** Target-suffix -> the value Node's process.platform reports. */
const PROCESS_PLATFORM: Record<string, string> = {
macos: 'darwin',
win: 'win32',
// linux / alpine / linuxstatic / freebsd report their own name verbatim
};
async function assertCustomBaseNodeTarget(
targets: (NodeTarget & Partial<Target>)[],
opts: GetNodejsExecutableOptions,
): Promise<void> {
const customNode = resolveCustomBaseNode(opts);
if (!customNode && !opts.useLocalNode) return;
const binPath = customNode ?? process.execPath;
// 1. One binary = one platform+arch. (linux/alpine/linuxstatic stay distinct.)
const combos = new Map<string, { platform: string; arch: string }>();
for (const t of targets) {
combos.set(`${t.platform}-${t.arch}`, {
platform: String(t.platform),
arch: String(t.arch),
});
}
if (combos.size > 1) {
throw wasReported(
`A custom base Node binary applies to a single platform/arch, but the requested ` +
`targets span ${combos.size}: ${[...combos.keys()].join(', ')}. Run pkg once per target.`,
);
}
const { platform, arch } = [...combos.values()][0];
// 2. Ask the binary what it actually is.
const node = await probeNode(binPath);
const expectedPlatform = PROCESS_PLATFORM[platform] ?? platform;
if (node.platform !== expectedPlatform) {
throw wasReported(
`Custom base Node binary is for "${node.platform}", but target "${platform}" ` +
`needs "${expectedPlatform}".`,
);
}
if (node.arch !== arch) {
throw wasReported(
`Custom base Node binary is ${node.arch}, but target arch is "${arch}".`,
);
}
const targetMajor = parseInt(targets[0].nodeRange.replace('node', ''), 10);
const binMajor = parseInt(node.version.replace(/^v/, ''), 10);
if (!Number.isNaN(targetMajor) && binMajor !== targetMajor) {
throw wasReported(
`Custom base Node binary is ${node.version} (major ${binMajor}), but target ` +
`"${targets[0].nodeRange}" requests Node ${targetMajor}.`,
);
}
}Net effect
- Deletes
sniffBinaryTarget,BinaryFormat,ELF_MACHINE/MACHO_CPU/PE_MACHINE,formatForPlatform,FORMAT_LABEL, and theopenimport — ~90 lines and one whole concept gone. process.platformreportslinuxfor glibc/musl/static alike, so the linux-family caveat is preserved for free;process.archmatches theNodeArchvalues directly, so no arch table is needed.probeNodecan also replace the duplicate--versionexec inresolveTargetNodeVersion.
One thing worth stating explicitly in the PR/docs: this makes "the base binary must be runnable on the build host" an honest, documented constraint rather than a half-supported one. That's already true in practice (both code paths exec it), so it isn't a regression — just no longer implying cross-platform bases work.
…dback)
robertsLando pointed out that the ELF/Mach-O/PE header sniffing
(sniffBinaryTarget + ELF_MACHINE/MACHO_CPU/PE_MACHINE tables,
formatForPlatform, FORMAT_LABEL — ~90 lines) is redundant: the custom base
binary is already exec'd for its version in assertCustomBaseNodeTarget and in
resolveTargetNodeVersion, and sniffing's only theoretical edge is validating a
base that can't run on the build host — but that base already fails the version
exec, before sniffing helps.
Replace it with one probe:
node -p "process.version + ' ' + process.platform + ' ' + process.arch"
- add probeNode(binPath) (short-circuits to process.* when binPath is the host
node, so no exec for --sea-use-local-node / host targets)
- assertCustomBaseNodeTarget: keep the multi-target guard (step 1), then compare
the binary's reported process.platform/arch/major to the single target via
probeNode (steps 2-3). Adds a PROCESS_PLATFORM map (macos->darwin, win->win32)
- resolveTargetNodeVersion: reuse probeNode instead of a second `--version` exec
- delete sniffBinaryTarget + its unit test; update custom-node.md to state the
base must be runnable on the build host (already true in practice)
This makes "the base binary must be runnable on the build host" an honest,
documented constraint rather than a half-supported one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@robertsLando I believe the last commit should address your last comment. |
robertsLando
left a comment
There was a problem hiding this comment.
Verdict: needs work — approach is sound (expose nodePath/useLocalNode to SEA, fold in PKG_NODE_PATH, guard the multi-target footgun), but the platform/arch guard falsely rejects single alpine/linuxstatic/armv7l targets, and the tests only exercise the process.execPath short-circuit so CI can't catch it. Strengths: the probeNode consolidation, PKG_NODE_PATH parity with standard mode, and the thorough docs/tests. Findings inline.
| } | ||
|
|
||
| /** Target-suffix -> the value Node reports as `process.platform`. */ | ||
| const PROCESS_PLATFORM: Record<string, string> = { |
There was a problem hiding this comment.
[Major] · Correctness
The comment claims alpine/linuxstatic "report their own name verbatim" — they don't: a musl/static Node binary reports process.platform === 'linux' ('alpine'/'linuxstatic' are pkg-fetch concepts, not Node process.platform values). So a single alpine/linuxstatic target hits expectedPlatform = 'alpine' (line 431) vs node.platform = 'linux' and is falsely rejected.
Why: the PR's own docs promise a single alpine/linuxstatic target works ("matching that to linux/alpine/linuxstatic remains your responsibility") — exactly the musl / old-glibc custom-runtime case this PR exists for. The guard runs before any getNodeOs, so nothing upstream filters it; a legit musl-on-musl build dies on a contradictory error.
Fix: map alpine→linux and linuxstatic→linux here. Cleanest as one total map over knownPlatforms so a newly-added target can't silently fall through ?? platform again.
| `"${platform}" needs "${expectedPlatform}".`, | ||
| ); | ||
| } | ||
| if (node.arch !== arch) { |
There was a problem hiding this comment.
[Major] · Correctness
Same defect as the platform map, but arch has no mapping at all. pkg's armv7l target vs Node's process.arch === 'arm' → 'arm' !== 'armv7l' → false reject of a single armv7l target with a matching custom node.
Why: blocks a valid 32-bit-ARM custom-runtime build with a confusing error.
Fix: add an arch map (armv7l→arm) alongside the platform one, total over knownArchs.
| const testName = 'test-95-sea-custom-node'; | ||
| const newcomers = utils.seaHostOutputs(testName); | ||
| const before = utils.filesBefore(newcomers); | ||
| utils.runSeaHostOnly(input, testName, ['--sea-node-path', process.execPath]); |
There was a problem hiding this comment.
[Major] · Tests
Every test supplies process.execPath, which trips probeNode's binPath === process.execPath short-circuit — so execFileAsync and the PROCESS_PLATFORM/arch comparison for a non-host target never run. The linux,alpine case only trips check #1 (combos.size > 1) and throws before the platform comparison is reached.
Why: the platform/arch mapping bug ships green — CI can't catch it.
Fix: extract the pkg→Node platform/arch normalization into a small pure helper and unit-test it over the full knownPlatforms/knownArchs set (mirrors the config-parse.test.ts style already added in this PR).
| }; | ||
|
|
||
| /** What a Node binary reports about itself when run. */ | ||
| async function probeNode( |
There was a problem hiding this comment.
[Minor] · Robustness
Returning platform/arch is justified — but narrower than it looks. arch is genuinely valuable (Rosetta: an arm64 host runs an x64 node, reports x64, catching an arm64-target mismatch that would otherwise embed the wrong binary). platform: since the binary must be runnable to probe, a successful probe necessarily reports the host's platform, so this effectively enforces "target platform == host platform" — reachable mainly via the useLocalNode/execPath short-circuit. For a genuinely foreign, non-runnable binary, execFileAsync throws ENOEXEC here before the friendly platform/arch error can fire.
Fix: keep the probe (it's free — you already exec for the version), but wrap the exec failure with context: "couldn't run the custom base Node on this host; it must be runnable here to validate platform/arch/version (got …)." Turns a cryptic ENOEXEC into a clear message.
| // matches the host. Ask it directly. | ||
| const { stdout } = await execFileAsync(opts.nodePath, ['--version']); | ||
| return stdout.trim() as NodeVersion; | ||
| return (await probeNode(customNode)).version as NodeVersion; |
There was a problem hiding this comment.
[FYI] · Performance
probeNode runs twice per build — once in assertCustomBaseNodeTarget, once here — each a child-process spawn of the custom node. Negligible for a build, but trivially memoizable if desired.
| * pkg-fetch honours for the traditional build path (`localPlace()`); folding it | ||
| * in here makes it work for SEA too, instead of a separate SEA-only mechanism. | ||
| */ | ||
| function resolveCustomBaseNode( |
There was a problem hiding this comment.
[Nit] · Consistency
PKG_NODE_PATH is passed through resolve() (line 345) but opts.nodePath (the --sea-node-path flag) is returned verbatim — so a relative --sea-node-path is left un-normalized. Harmless today (exec/exists resolve against cwd) but inconsistent.
| * 3. Verify the binary's major matches the requested `nodeRange` | ||
| * (`assertSingleTargetMajor` only compares the targets to each other). | ||
| */ | ||
| async function assertCustomBaseNodeTarget( |
There was a problem hiding this comment.
[Nit] · Types
(NodeTarget & Partial<Target>)[] makes platform/arch optionally undefined; String(t.platform) would then silently key combos as 'undefined-undefined' rather than asserting. Targets are fully resolved by here, so only a type smell.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #279 +/- ##
==========================================
- Coverage 86.36% 85.71% -0.65%
==========================================
Files 22 22
Lines 7297 7462 +165
Branches 1047 1052 +5
==========================================
+ Hits 6302 6396 +94
- Misses 988 1058 +70
- Partials 7 8 +1
🚀 New features to boost your workflow:
|
|
@robertsLando I'll aim to fix everything from review today, and give it another once over. It's kind of a weird time in software development. I've contributed quite a bit to OSS over the years, but this is the first time I've ever used AI assistance. I'm painfully self conscious about the possibility of, despite my best intentions, doing anything that remotely looks like shoving AI slop into a PR and making it a maintainer's problem. While I believe the feature and the general approach is sound (and it looks like there's agreement there), I am very new to not only this code base, but the software itself, so there's undoubtedly some things I'd miss even in a pre-AI world. Given that, and that it seems you have a decent AI assisted review workflow, if you have any tips I can adopt to spare you the time spent in review, I'd be more than happy to integrate them. At the very least, I'll see if I can set up some sort of adversarial prompting to catch things I missed. Many thanks for maintaining |
…ase guard robertsLando's review caught that the guard compared pkg target identifiers directly against Node's process.platform/arch, which falsely rejects legitimate single targets: - alpine / linuxstatic report process.platform === 'linux' (they're pkg-fetch flavors, not Node platform values), so `expectedPlatform = 'alpine'` could never match — breaking the musl/old-glibc custom-runtime case this feature exists for. - armv7l has process.arch === 'arm', and arch had no mapping at all, so a single armv7l target with a matching custom node was rejected too. Fixes: - add total TARGET_PLATFORM_TO_PROCESS / TARGET_ARCH_TO_PROCESS maps and pure expectedProcessPlatform() / expectedProcessArch() helpers (alpine/linuxstatic ->linux, macos->darwin, win->win32, armv7/armv7l->arm, x86->ia32). They throw on an unmapped target instead of silently falling through to a wrong identity default. - unit-test the helpers over the full knownPlatforms/knownArchs sets (sea-target-identity.test.ts) — the e2e can't reach this because every case feeds the host's own node and trips probeNode's process.execPath short-circuit. - wrap probeNode's exec failure with a clear "must be runnable on the build host" message instead of a cryptic ENOEXEC. - memoize probeNode so the target guard and version resolver share one spawn. - resolve() opts.nodePath (the --sea-node-path flag) for parity with the PKG_NODE_PATH branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A second review round flagged that the custom-base major check returned early
when the target nodeRange is unparseable ('latest'): targetMajor is NaN, so the
guard skipped validation entirely. seaEnhanced's later minTargetMajor check
resolves 'latest' to the host major (>= 22), so a too-old custom binary (e.g.
`--sea-node-path ./node18 -t latest-...`) slipped through both checks and would
break at injection instead of failing cleanly.
- extract the major-compatibility logic into a pure, exported
assertBaseMajorSatisfiesTarget(binVersion, nodeRange) and unit-test all
branches (concrete match/mismatch, 'latest' with new/floor/too-old binaries) —
the e2e can't reach the too-old path host-independently.
- for a 'latest'/unparseable target, require the binary major >= the SEA floor
instead of returning unchecked.
- introduce MIN_SEA_TARGET_MAJOR (22) and reuse it in assertHostSeaNodeVersion
and seaEnhanced's target check, so the floor lives in one place.
- document that cross-compilation is unsupported with a custom SEA binary
(the binary must be runnable on the build host to be probed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
probeNode ran `node -p "process.version + ' ' + process.platform + ' ' +
process.arch"` and split the result on spaces. The fields can't contain spaces
so it wasn't buggy, but JSON is clearer and more robust: emit
`JSON.stringify({version, platform, arch})` and parse it, validating the three
fields are strings. Unexpected output now fails with a clear parse error
instead of silently yielding undefined fields that would later trip a confusing
platform/arch mismatch.
Also add e2e coverage for probeNode's real exec + parse path. Cases a-d all pass
process.execPath, which short-circuits probeNode (no spawn), so the parse was
never exercised in CI. Two POSIX-only mock "node" binaries close that gap: a
shebang script reporting a mismatched version (valid JSON -> major-version
guard error) and one emitting non-JSON (-> the new "unexpected output" error).
Windows is skipped (execFile can't run a .bat/.cmd without a shell, and a real
.exe isn't practical here); the parse logic is OS-agnostic JS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Hi @cstrahan ! Thanks for your appreciation. While the review is produced by ai it is mostly driven by my inputs, what I do is to check the changes spot what doesn't convince me and tell the ai to review the changes based on my thoughts, what I generally find is that ai generated reviews are focused on find issues with the current implementation while them are not good to spot when the implementation is actually not following the best path to solve the issue, for a more concrete example on first review I remember I already had the support for that in legacy pkg way (not sea) so I told the ai to focus the implementation around what exists, in the second round I found that the code you were using to check the binary was too complicated while the problem was much easier so I ask him to simplify that till he proposed me a nice more elegant solution. I have nothing really automated here, AI is just a tool but in order to use it at it's most powers you need to know what you are doing and this still means to study the code first, you can tell the ai to help you in this task and also I suggest to spend LOT of time in the plan phase, that's the most important part, do a plan and ask the ai to be critic against it till you find the "best" way to do it :) Hope this helps |
Context: I work on a team that distributes software to on-prem customers, many of whom are on old systems like EL 7 with a glibc as old as 2.17. To date, this has kept us targeting very old versions of node, resulting in a web of old dependencies. I have recently built nodejs v24 on EL 7 (so the resulting binaries will run pretty much anywhere), and I would like to use it together with
@yao-pkg/pkgin enhanced SEA mode so we can finally upgrade all of our on-prem software.Unfortunately, SEA mode previously always downloaded a base Node binary from nodejs.org, so the resulting executable was pinned to an official build. That makes it impossible to ship a SEA on top of a custom runtime -- e.g. a Node built against an older glibc (to run on EL7 / older distros), or one configured differently.
The internal SeaOptions / SeaEnhancedOptions already accepted
nodePath/useLocalNode; this just exposes them:--sea-node-path embed a specific Node binary as the base
--sea-use-local-node embed the Node running pkg (process.execPath)
Both are also settable in the pkg config (seaNodePath / seaUseLocalNode) and via the programmatic exec() API. The two are mutually exclusive. The embedded binary's major version must match the target's (the existing version-skew checks still apply).
This is also a prerequisite for adopting
node --build-sea(Node >= 25.5): that flag is only meaningful once the base Node is no longer a fixed download. I'll have a separate PR for that coming up as well.