From 93dfa9918d633885bcbed054e2971a67b0b123f2 Mon Sep 17 00:00:00 2001 From: macodev00 <273427913+macodev00@users.noreply.github.com> Date: Sun, 20 Sep 2026 08:28:54 +0000 Subject: [PATCH 1/4] fix(cli): hint at UEK8 ENOEXEC when the SEA cannot exec Oracle UEK8 rejects Node SEA PT_NOTE blobs larger than 4 MB, so npx t3 and the installer fail with a bare exec-format error. Print a targeted hint (RHCK / from-source) from the npm launcher, legacy launcher, and install.sh, and document it next to Intel Macs. --- docs/user/install.md | 11 +++ packages/shared/src/legacyCliLauncher.test.ts | 40 ++++++++++- packages/shared/src/legacyCliLauncher.ts | 22 +++++- scripts/build-npm-platform-packages.test.ts | 53 ++++++++++++++ scripts/build-npm-platform-packages.ts | 14 +++- scripts/install.sh | 11 ++- scripts/install.test.ts | 70 +++++++++++++++++++ 7 files changed, 216 insertions(+), 5 deletions(-) diff --git a/docs/user/install.md b/docs/user/install.md index 2a7e7dc7e749..906a88ec6995 100644 --- a/docs/user/install.md +++ b/docs/user/install.md @@ -53,6 +53,17 @@ node apps/server/dist/bin.mjs `t3 update` and the background service do not apply to a server run this way; update it with `git pull` and a rebuild. +### Oracle Linux (UEK8) + +Oracle's UEK8 kernel refuses to run the `t3` CLI (`ENOEXEC` / "Exec format +error"). The CLI is a Node single-executable whose embedded JavaScript is an +ELF note larger than 4 MB, and UEK8 rejects notes that size. The desktop app +is unaffected. + +Boot Oracle's RHCK (or any mainline-based kernel) instead of UEK8, or build +from source the same way as Intel Macs above and run +`node apps/server/dist/bin.mjs`. + ## Desktop app Download a release from [GitHub Releases](https://github.com/pingdotgg/t3code/releases), diff --git a/packages/shared/src/legacyCliLauncher.test.ts b/packages/shared/src/legacyCliLauncher.test.ts index b6630f468a83..332f639e217b 100644 --- a/packages/shared/src/legacyCliLauncher.test.ts +++ b/packages/shared/src/legacyCliLauncher.test.ts @@ -6,7 +6,7 @@ import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import { expect, it } from "vite-plus/test"; -import { legacyCliLauncherScript } from "./legacyCliLauncher.ts"; +import { legacyCliLauncherScript, linuxCliExecFormatErrorHint } from "./legacyCliLauncher.ts"; // oxlint-disable-next-line t3code/no-global-process-runtime -- This test launches a real host executable. const hostPlatform = NodeOS.platform(); @@ -60,3 +60,41 @@ process.send({ args: process.argv.slice(2) }); } }, ); + +it.skipIf(hostPlatform !== "linux")( + "hints at UEK8 when the platform executable cannot exec", + async () => { + const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-legacy-enoexec-")); + const entry = NodePath.join(root, "node_modules/t3/dist/bin.mjs"); + const executable = NodePath.join( + root, + `node_modules/@t3code/t3-${hostPlatform}-${hostArch}/t3`, + ); + await NodeFSP.mkdir(NodePath.dirname(entry), { recursive: true }); + await NodeFSP.mkdir(NodePath.dirname(executable), { recursive: true }); + await NodeFSP.writeFile( + NodePath.join(NodePath.dirname(executable), "package.json"), + '{"type":"commonjs"}', + ); + await NodeFSP.writeFile(entry, legacyCliLauncherScript()); + await NodeFSP.writeFile(executable, "#!/bin/sh\nexit 126\n"); + await NodeFSP.chmod(executable, 0o755); + const child = NodeChildProcess.fork(entry, ["--version"], { silent: true }); + let stderr = ""; + child.stderr?.on("data", (chunk: Buffer) => { + stderr += chunk.toString(); + }); + try { + const [code] = await NodeEvents.EventEmitter.once(child, "exit"); + expect(code).toBe(126); + expect(stderr).toContain(linuxCliExecFormatErrorHint); + } finally { + if (child.exitCode === null && child.signalCode === null) { + const exit = NodeEvents.EventEmitter.once(child, "exit"); + child.kill("SIGTERM"); + await exit; + } + await NodeFSP.rm(root, { recursive: true, force: true }); + } + }, +); diff --git a/packages/shared/src/legacyCliLauncher.ts b/packages/shared/src/legacyCliLauncher.ts index b852e2310dba..061f29f64de9 100644 --- a/packages/shared/src/legacyCliLauncher.ts +++ b/packages/shared/src/legacyCliLauncher.ts @@ -10,6 +10,18 @@ * Remove it once no supported release predates the executable (after the * first stable release that ships it). */ + +/** + * UEK8's `load_elf_binary()` returns ENOEXEC when a PT_NOTE exceeds 4 MB. + * The CLI is a Node SEA, so that blob is large by design. Node spawn uses + * execvp, which retries ENOEXEC via /bin/sh, so the failure often arrives as + * status 126 with no error object. ENOEXEC can also mean a corrupt or + * wrong-architecture file; this is the likely cause on Linux when the file + * exists. + */ +export const linuxCliExecFormatErrorHint = + "t3: Oracle Linux UEK8 kernels reject Node SEA notes larger than 4 MB (ENOEXEC). Boot Oracle RHCK or a mainline-based kernel, or build from source and run node apps/server/dist/bin.mjs. ENOEXEC can also mean a corrupt or wrong-architecture file."; + export function legacyCliLauncherScript(): string { return `import { spawn } from "node:child_process"; import { constants } from "node:os"; @@ -25,6 +37,9 @@ const child = spawn(executable, process.argv.slice(2), { const fail = (error) => { if (!error) return; process.stderr.write("t3: " + error.message + "\\n"); + if (error.code === "ENOEXEC" && process.platform === "linux") { + process.stderr.write(${JSON.stringify(linuxCliExecFormatErrorHint + "\n")}); + } child.kill("SIGTERM"); process.exitCode = 1; }; @@ -37,6 +52,11 @@ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) { process.on(signal, () => child.kill(signal)); } child.on("error", (error) => { fail(error); process.exit(1); }); -child.on("exit", (code, signal) => process.exit(code ?? 128 + (constants.signals[signal] || 1))); +child.on("exit", (code, signal) => { + if (code === 126 && process.platform === "linux") { + process.stderr.write(${JSON.stringify(linuxCliExecFormatErrorHint + "\n")}); + } + process.exit(code ?? 128 + (constants.signals[signal] || 1)); +}); `; } diff --git a/scripts/build-npm-platform-packages.test.ts b/scripts/build-npm-platform-packages.test.ts index 5fc0d859fc3d..70f912d0a0c9 100644 --- a/scripts/build-npm-platform-packages.test.ts +++ b/scripts/build-npm-platform-packages.test.ts @@ -1,4 +1,5 @@ import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { linuxCliExecFormatErrorHint } from "@t3tools/shared/legacyCliLauncher"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; @@ -10,6 +11,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { buildNpmPlatformPackages, + NPM_LAUNCHER_SCRIPT, NpmPackagesArchivesMissingError, } from "./build-npm-platform-packages.ts"; @@ -258,4 +260,55 @@ it.layer(NodeServices.layer)("build-npm-platform-packages", (it) => { assert.include(unsupported.stderr, "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/pingdotgg/t3code/releases"); }), ); + + it.effect.skipIf(HostProcessPlatform.defaultValue() !== "linux")( + "hints at UEK8 when the platform executable cannot exec", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const hostPlatform = yield* HostProcessPlatform; + const hostArch = yield* HostProcessArchitecture; + const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-npm-enoexec-" }); + const platformDir = path.join(root, `@t3code/t3-${hostPlatform}-${hostArch}`); + const launcherDir = path.join(root, "t3"); + yield* fs.makeDirectory(platformDir, { recursive: true }); + yield* fs.makeDirectory(path.join(launcherDir, "bin"), { recursive: true }); + yield* fs.writeFileString(path.join(platformDir, "package.json"), '{"type":"commonjs"}'); + yield* fs.writeFileString(path.join(platformDir, "t3"), "#!/bin/sh\nexit 126\n"); + yield* fs.chmod(path.join(platformDir, "t3"), 0o755); + yield* fs.writeFileString(path.join(launcherDir, "bin/t3.js"), NPM_LAUNCHER_SCRIPT); + const env = { ...process.env, NODE_PATH: root } as Record; + const execvpFallback = yield* run(process.execPath, ["bin/t3.js", "--version"], { + cwd: launcherDir, + env, + }); + assert.equal(execvpFallback.exitCode, 126); + assert.include(execvpFallback.stderr, linuxCliExecFormatErrorHint); + + // Node spawnSync uses execvp, which retries ENOEXEC via /bin/sh. The + // raw error is still possible if that fallback is skipped. + yield* fs.writeFileString( + path.join(root, "enoexec-preload.cjs"), + [ + '"use strict";', + 'const childProcess = require("node:child_process");', + "childProcess.spawnSync = () => {", + ' const error = new Error("spawnSync ENOEXEC");', + ' error.code = "ENOEXEC";', + " return { error, status: null, signal: null };", + "};", + "", + ].join("\n"), + ); + const enoexec = yield* run( + process.execPath, + ["-r", path.join(root, "enoexec-preload.cjs"), "bin/t3.js", "--version"], + { cwd: launcherDir, env }, + ); + assert.equal(enoexec.exitCode, 1); + assert.include(enoexec.stderr, "failed to start"); + assert.include(enoexec.stderr, linuxCliExecFormatErrorHint); + }), + ); }); diff --git a/scripts/build-npm-platform-packages.ts b/scripts/build-npm-platform-packages.ts index 6a134d99e2c5..99c1263451f3 100644 --- a/scripts/build-npm-platform-packages.ts +++ b/scripts/build-npm-platform-packages.ts @@ -19,7 +19,10 @@ * bundleDependencies needs an arborist tree these flattened installs are * not), whereas `npm publish ` uploads the bytes as given. */ -import { legacyCliLauncherScript } from "@t3tools/shared/legacyCliLauncher"; +import { + legacyCliLauncherScript, + linuxCliExecFormatErrorHint, +} from "@t3tools/shared/legacyCliLauncher"; import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as Effect from "effect/Effect"; @@ -190,7 +193,8 @@ export function npmLauncherPackageManifest( /** * The launcher every `npx t3` runs. Plain CommonJS with no dependencies so it * loads on any Node that npm itself runs on; the real work happens in the - * single-executable it execs. + * single-executable it execs. Linux ENOEXEC / exit 126 prints a UEK8 hint + * because Node spawnSync retries exec format errors through /bin/sh. */ export const NPM_LAUNCHER_SCRIPT = `#!/usr/bin/env node "use strict"; @@ -221,8 +225,14 @@ const executable = join(packageDir, process.platform === "win32" ? "t3.exe" : "t const result = spawnSync(executable, process.argv.slice(2), { stdio: "inherit" }); if (result.error) { process.stderr.write("t3: failed to start " + executable + ": " + result.error.message + "\\n"); + if (process.platform === "linux" && result.error.code === "ENOEXEC") { + process.stderr.write(${JSON.stringify(linuxCliExecFormatErrorHint + "\n")}); + } process.exit(1); } +if (process.platform === "linux" && result.status === 126) { + process.stderr.write(${JSON.stringify(linuxCliExecFormatErrorHint + "\n")}); +} // A child killed by a signal has no status; report it the way a shell would. process.exit(result.status ?? 128 + (constants.signals[result.signal] || 1)); `; diff --git a/scripts/install.sh b/scripts/install.sh index 421d8d021aef..5568106711b0 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -208,7 +208,16 @@ else step "Extracting T3 Code..." tar -xzf "${staging}/${archive}" -C "$staging" --strip-components=1 rm -f "${staging}/${archive}" "${staging}/SHA256SUMS" - "${staging}/t3" --version >/dev/null || fail "the downloaded executable does not run" + if ! smoke_out="$("${staging}/t3" --version 2>&1)"; then + if [ "$platform" = linux ]; then + case "$smoke_out" in + *"Exec format error"* | *"ENOEXEC"*) + fail "the downloaded executable does not run. Oracle Linux UEK8 kernels reject Node SEA notes larger than 4 MB (ENOEXEC). Boot Oracle RHCK or a mainline-based kernel, or build from source and run node apps/server/dist/bin.mjs. ENOEXEC can also mean a corrupt or wrong-architecture file." + ;; + esac + fi + fail "the downloaded executable does not run" + fi printf '%s\n' "$version" > "${staging}/.install-complete" rm -rf "$target_dir" diff --git a/scripts/install.test.ts b/scripts/install.test.ts index 9ded5cb51166..5a77ac652499 100644 --- a/scripts/install.test.ts +++ b/scripts/install.test.ts @@ -112,4 +112,74 @@ describe.skipIf(HostProcessPlatform.defaultValue() !== "linux")("installer termi } }, ); + + it("hints at UEK8 when the extracted executable cannot exec", async () => { + const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-install-enoexec-")); + const version = "1.2.3"; + const stem = `t3-${version}-linux-${HostProcessArchitecture.defaultValue()}`; + const archiveName = `${stem}.tar.gz`; + await NodeFSP.mkdir(NodePath.join(root, stem)); + const elf = Buffer.alloc(64); + elf.write("\x7fELF"); + elf[4] = 2; + elf[5] = 1; + elf[6] = 1; + await NodeFSP.writeFile(NodePath.join(root, stem, "t3"), elf, { mode: 0o755 }); + NodeChildProcess.execFileSync("tar", [ + "-czf", + NodePath.join(root, archiveName), + "-C", + root, + stem, + ]); + const archive = await NodeFSP.readFile(NodePath.join(root, archiveName)); + const checksum = NodeCrypto.createHash("sha256").update(archive).digest("hex"); + const server = NodeHttp.createServer((request, response) => { + if (request.url?.endsWith("/SHA256SUMS")) { + response.end(`${checksum} ${archiveName}\n`); + } else { + response.end(archive); + } + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Expected a TCP listener"); + const child = NodeChildProcess.spawn( + "sh", + [NodePath.resolve(import.meta.dirname, "install.sh")], + { + env: { + ...process.env, + T3CODE_VERSION: version, + T3CODE_HOME: NodePath.join(root, "home"), + T3CODE_INSTALL_BIN_DIR: NodePath.join(root, "bin"), + T3CODE_RELEASE_BASE_URL: `http://127.0.0.1:${address.port}`, + }, + stdio: ["ignore", "pipe", "pipe"], + }, + ); + let output = ""; + const collect = (chunk: Buffer) => { + output += chunk.toString(); + }; + child.stdout.on("data", collect); + child.stderr.on("data", collect); + try { + const code = await new Promise((resolve, reject) => { + child.on("error", reject); + child.on("close", resolve); + }); + expect(code).not.toBe(0); + expect(output).toContain("the downloaded executable does not run"); + expect(output).toContain("UEK8"); + expect(output).toContain("RHCK"); + expect(output).toContain("node apps/server/dist/bin.mjs"); + expect(await NodeFSP.readdir(NodePath.join(root, "home/runtime/versions"))).toEqual([]); + } finally { + if (child.exitCode === null) child.kill(); + server.closeAllConnections(); + await new Promise((resolve) => server.close(() => resolve())); + await NodeFSP.rm(root, { recursive: true, force: true }); + } + }); }); From b3ac480f5a34243ea5eb8e6360e30dce34633e9d Mon Sep 17 00:00:00 2001 From: macodev00 <273427913+macodev00@users.noreply.github.com> Date: Sun, 20 Sep 2026 09:54:32 +0000 Subject: [PATCH 2/4] docs(cli): add TSDoc on legacyCliLauncherScript CodeRabbit docstring coverage is scoped to functions this diff touches. The UEK8 hint constant sat between the file comment and legacyCliLauncherScript, so that function no longer had an attached docstring (0% of 1). Restore a one-line TSDoc on it. Co-authored-by: maco --- packages/shared/src/legacyCliLauncher.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/shared/src/legacyCliLauncher.ts b/packages/shared/src/legacyCliLauncher.ts index 061f29f64de9..ff5b7971a1e2 100644 --- a/packages/shared/src/legacyCliLauncher.ts +++ b/packages/shared/src/legacyCliLauncher.ts @@ -22,6 +22,7 @@ export const linuxCliExecFormatErrorHint = "t3: Oracle Linux UEK8 kernels reject Node SEA notes larger than 4 MB (ENOEXEC). Boot Oracle RHCK or a mainline-based kernel, or build from source and run node apps/server/dist/bin.mjs. ENOEXEC can also mean a corrupt or wrong-architecture file."; +/** Return `dist/bin.mjs` source that forwards the process to the sibling platform `t3` executable. */ export function legacyCliLauncherScript(): string { return `import { spawn } from "node:child_process"; import { constants } from "node:os"; From e1d1d3d4d3f50326ffab0499a06fcca9d0b141c8 Mon Sep 17 00:00:00 2001 From: macodev00 <273427913+macodev00@users.noreply.github.com> Date: Mon, 21 Sep 2026 06:32:55 +0000 Subject: [PATCH 3/4] fix(cli): document UEK8 PT_NOTE p_filesz workaround in ENOEXEC hint Issue 12628's verified workaround is capping the installed binary's PT_NOTE p_filesz at 4 MB. Put that next to RHCK and the from-source path in the shared hint, installer diagnostic, and install notes. Always print the Linux installer hint: some shells swallow ENOEXEC and leave no diagnostic text to match. --- docs/user/install.md | 9 +++++---- packages/shared/src/legacyCliLauncher.ts | 15 +++++++++------ scripts/build-npm-platform-packages.ts | 3 ++- scripts/install.sh | 11 +++++------ scripts/install.test.ts | 2 ++ 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/docs/user/install.md b/docs/user/install.md index 906a88ec6995..6df863567499 100644 --- a/docs/user/install.md +++ b/docs/user/install.md @@ -56,12 +56,13 @@ update it with `git pull` and a rebuild. ### Oracle Linux (UEK8) Oracle's UEK8 kernel refuses to run the `t3` CLI (`ENOEXEC` / "Exec format -error"). The CLI is a Node single-executable whose embedded JavaScript is an -ELF note larger than 4 MB, and UEK8 rejects notes that size. The desktop app +error"). The CLI is a Node single-executable whose embedded JavaScript is a +`PT_NOTE` larger than 4 MB, and UEK8 rejects notes that size. The desktop app is unaffected. -Boot Oracle's RHCK (or any mainline-based kernel) instead of UEK8, or build -from source the same way as Intel Macs above and run +Boot Oracle's RHCK (or any mainline-based kernel) instead of UEK8, patch the +installed binary's `PT_NOTE` `p_filesz` down to 4 MB (reapply after every +update), or build from source the same way as Intel Macs above and run `node apps/server/dist/bin.mjs`. ## Desktop app diff --git a/packages/shared/src/legacyCliLauncher.ts b/packages/shared/src/legacyCliLauncher.ts index ff5b7971a1e2..554d67c20026 100644 --- a/packages/shared/src/legacyCliLauncher.ts +++ b/packages/shared/src/legacyCliLauncher.ts @@ -12,15 +12,18 @@ */ /** - * UEK8's `load_elf_binary()` returns ENOEXEC when a PT_NOTE exceeds 4 MB. - * The CLI is a Node SEA, so that blob is large by design. Node spawn uses - * execvp, which retries ENOEXEC via /bin/sh, so the failure often arrives as - * status 126 with no error object. ENOEXEC can also mean a corrupt or - * wrong-architecture file; this is the likely cause on Linux when the file + * UEK8's `load_elf_binary()` returns ENOEXEC when a PT_NOTE exceeds 4 MB + * (`MAX_FILE_NOTE_SIZE`). The CLI is a Node SEA, so `NODE_SEA_BLOB` is large + * by design. The kernel only checks the PT_NOTE program header `p_filesz`, + * not the note payload: capping that field at 4 MB on the installed binary + * lets `execve` succeed, and must be reapplied after every update. Node spawn + * uses execvp, which retries ENOEXEC via /bin/sh, so the failure often + * arrives as status 126 with no error object. ENOEXEC can also mean a corrupt + * or wrong-architecture file; this is the likely cause on Linux when the file * exists. */ export const linuxCliExecFormatErrorHint = - "t3: Oracle Linux UEK8 kernels reject Node SEA notes larger than 4 MB (ENOEXEC). Boot Oracle RHCK or a mainline-based kernel, or build from source and run node apps/server/dist/bin.mjs. ENOEXEC can also mean a corrupt or wrong-architecture file."; + "t3: Oracle Linux UEK8 kernels reject Node SEA PT_NOTE segments larger than 4 MB (ENOEXEC). Boot Oracle RHCK or a mainline-based kernel; patch the installed binary's PT_NOTE p_filesz down to 4 MB (reapply after every update); or build from source and run node apps/server/dist/bin.mjs. ENOEXEC can also mean a corrupt or wrong-architecture file."; /** Return `dist/bin.mjs` source that forwards the process to the sibling platform `t3` executable. */ export function legacyCliLauncherScript(): string { diff --git a/scripts/build-npm-platform-packages.ts b/scripts/build-npm-platform-packages.ts index 99c1263451f3..ba2c4ff47477 100644 --- a/scripts/build-npm-platform-packages.ts +++ b/scripts/build-npm-platform-packages.ts @@ -194,7 +194,8 @@ export function npmLauncherPackageManifest( * The launcher every `npx t3` runs. Plain CommonJS with no dependencies so it * loads on any Node that npm itself runs on; the real work happens in the * single-executable it execs. Linux ENOEXEC / exit 126 prints a UEK8 hint - * because Node spawnSync retries exec format errors through /bin/sh. + * (RHCK, PT_NOTE `p_filesz` cap, or from-source) because Node spawnSync + * retries exec format errors through /bin/sh. */ export const NPM_LAUNCHER_SCRIPT = `#!/usr/bin/env node "use strict"; diff --git a/scripts/install.sh b/scripts/install.sh index 5568106711b0..06be3143da11 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -208,13 +208,12 @@ else step "Extracting T3 Code..." tar -xzf "${staging}/${archive}" -C "$staging" --strip-components=1 rm -f "${staging}/${archive}" "${staging}/SHA256SUMS" - if ! smoke_out="$("${staging}/t3" --version 2>&1)"; then + # Some shells retry ENOEXEC as a script, so stderr may not mention it. + # Keep this diagnostic aligned with linuxCliExecFormatErrorHint in + # packages/shared/src/legacyCliLauncher.ts (PT_NOTE p_filesz cap). + if ! "${staging}/t3" --version >/dev/null 2>&1; then if [ "$platform" = linux ]; then - case "$smoke_out" in - *"Exec format error"* | *"ENOEXEC"*) - fail "the downloaded executable does not run. Oracle Linux UEK8 kernels reject Node SEA notes larger than 4 MB (ENOEXEC). Boot Oracle RHCK or a mainline-based kernel, or build from source and run node apps/server/dist/bin.mjs. ENOEXEC can also mean a corrupt or wrong-architecture file." - ;; - esac + fail "the downloaded executable does not run. This can be caused by Oracle Linux UEK8 kernels rejecting Node SEA PT_NOTE segments larger than 4 MB (ENOEXEC). Boot Oracle RHCK or a mainline-based kernel; patch the installed binary's PT_NOTE p_filesz down to 4 MB (reapply after every update); or build from source and run node apps/server/dist/bin.mjs. ENOEXEC can also mean a corrupt or wrong-architecture file." fi fail "the downloaded executable does not run" fi diff --git a/scripts/install.test.ts b/scripts/install.test.ts index 5a77ac652499..a50be0b0d8c3 100644 --- a/scripts/install.test.ts +++ b/scripts/install.test.ts @@ -173,6 +173,8 @@ describe.skipIf(HostProcessPlatform.defaultValue() !== "linux")("installer termi expect(output).toContain("the downloaded executable does not run"); expect(output).toContain("UEK8"); expect(output).toContain("RHCK"); + expect(output).toContain("PT_NOTE"); + expect(output).toContain("p_filesz"); expect(output).toContain("node apps/server/dist/bin.mjs"); expect(await NodeFSP.readdir(NodePath.join(root, "home/runtime/versions"))).toEqual([]); } finally { From 41c865e76d8bd5a68993ad16510b5cd1c4d3053d Mon Sep 17 00:00:00 2001 From: macodev00 <273427913+macodev00@users.noreply.github.com> Date: Tue, 22 Sep 2026 06:33:40 +0000 Subject: [PATCH 4/4] fix(cli): flush UEK8 ENOEXEC hint before process.exit On Linux, stderr writes to pipes are async, so process.exit can drop the UEK8 hint. Exit from that write's callback on both launchers, and ignore a later child event so it cannot exit first. Derive the installer smoke-test archive name from the host platform. Co-authored-by: maco --- packages/shared/src/legacyCliLauncher.ts | 25 ++++++++++++++++-------- scripts/build-npm-platform-packages.ts | 21 +++++++++++--------- scripts/install.test.ts | 3 ++- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/packages/shared/src/legacyCliLauncher.ts b/packages/shared/src/legacyCliLauncher.ts index 554d67c20026..5cc0c2d0284a 100644 --- a/packages/shared/src/legacyCliLauncher.ts +++ b/packages/shared/src/legacyCliLauncher.ts @@ -27,6 +27,7 @@ export const linuxCliExecFormatErrorHint = /** Return `dist/bin.mjs` source that forwards the process to the sibling platform `t3` executable. */ export function legacyCliLauncherScript(): string { + // Linux pipe/socket stderr writes are async; process.exit() would drop the UEK8 hint. return `import { spawn } from "node:child_process"; import { constants } from "node:os"; import { dirname, join } from "node:path"; @@ -38,12 +39,17 @@ const ipc = process.send !== undefined; const child = spawn(executable, process.argv.slice(2), { stdio: ipc ? ["inherit", "inherit", "inherit", "ipc"] : "inherit", }); +const linuxHint = ${JSON.stringify(linuxCliExecFormatErrorHint + "\n")}; +let exiting = false; +const exitAfterWrite = (code, extra) => { + if (exiting) return; + exiting = true; + if (extra) process.stderr.write(extra, () => process.exit(code)); + else process.exit(code); +}; const fail = (error) => { if (!error) return; process.stderr.write("t3: " + error.message + "\\n"); - if (error.code === "ENOEXEC" && process.platform === "linux") { - process.stderr.write(${JSON.stringify(linuxCliExecFormatErrorHint + "\n")}); - } child.kill("SIGTERM"); process.exitCode = 1; }; @@ -55,12 +61,15 @@ if (ipc) { for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) { process.on(signal, () => child.kill(signal)); } -child.on("error", (error) => { fail(error); process.exit(1); }); +child.on("error", (error) => { + fail(error); + exitAfterWrite(1, error.code === "ENOEXEC" && process.platform === "linux" ? linuxHint : undefined); +}); child.on("exit", (code, signal) => { - if (code === 126 && process.platform === "linux") { - process.stderr.write(${JSON.stringify(linuxCliExecFormatErrorHint + "\n")}); - } - process.exit(code ?? 128 + (constants.signals[signal] || 1)); + exitAfterWrite( + code ?? 128 + (constants.signals[signal] || 1), + code === 126 && process.platform === "linux" ? linuxHint : undefined, + ); }); `; } diff --git a/scripts/build-npm-platform-packages.ts b/scripts/build-npm-platform-packages.ts index ba2c4ff47477..9f671dcbb044 100644 --- a/scripts/build-npm-platform-packages.ts +++ b/scripts/build-npm-platform-packages.ts @@ -224,18 +224,21 @@ try { const executable = join(packageDir, process.platform === "win32" ? "t3.exe" : "t3"); const result = spawnSync(executable, process.argv.slice(2), { stdio: "inherit" }); +const linuxHint = ${JSON.stringify(linuxCliExecFormatErrorHint + "\n")}; +const exitAfterWrite = (code, extra) => { + if (extra) process.stderr.write(extra, () => process.exit(code)); + else process.exit(code); +}; if (result.error) { process.stderr.write("t3: failed to start " + executable + ": " + result.error.message + "\\n"); - if (process.platform === "linux" && result.error.code === "ENOEXEC") { - process.stderr.write(${JSON.stringify(linuxCliExecFormatErrorHint + "\n")}); - } - process.exit(1); -} -if (process.platform === "linux" && result.status === 126) { - process.stderr.write(${JSON.stringify(linuxCliExecFormatErrorHint + "\n")}); + exitAfterWrite(1, process.platform === "linux" && result.error.code === "ENOEXEC" ? linuxHint : undefined); +} else { + // A child killed by a signal has no status; report it the way a shell would. + exitAfterWrite( + result.status ?? 128 + (constants.signals[result.signal] || 1), + process.platform === "linux" && result.status === 126 ? linuxHint : undefined, + ); } -// A child killed by a signal has no status; report it the way a shell would. -process.exit(result.status ?? 128 + (constants.signals[result.signal] || 1)); `; const runCommand = Effect.fn("runCommand")(function* ( diff --git a/scripts/install.test.ts b/scripts/install.test.ts index a50be0b0d8c3..324b514fb453 100644 --- a/scripts/install.test.ts +++ b/scripts/install.test.ts @@ -116,7 +116,8 @@ describe.skipIf(HostProcessPlatform.defaultValue() !== "linux")("installer termi it("hints at UEK8 when the extracted executable cannot exec", async () => { const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-install-enoexec-")); const version = "1.2.3"; - const stem = `t3-${version}-linux-${HostProcessArchitecture.defaultValue()}`; + const installerPlatform = HostProcessPlatform.defaultValue() === "darwin" ? "darwin" : "linux"; + const stem = `t3-${version}-${installerPlatform}-${HostProcessArchitecture.defaultValue()}`; const archiveName = `${stem}.tar.gz`; await NodeFSP.mkdir(NodePath.join(root, stem)); const elf = Buffer.alloc(64);