diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index e77b4d761d1..6db9b6364ff 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -68,18 +68,62 @@ jobs: - name: Smoke test binary run: | - DIST="./packages/cli/dist/superset-${{ matrix.target }}" + # Resolve dist as an absolute path before changing cwd. The smoke + # tests below run from /tmp instead of the workspace because + # Node's module resolution walks up from cwd before consulting + # NODE_PATH — staying in $GITHUB_WORKSPACE leaks into the host + # repo's node_modules and silently masks bundle issues. + DIST="$(pwd)/packages/cli/dist/superset-${{ matrix.target }}" "$DIST/bin/superset" --version "$DIST/bin/superset" --help "$DIST/lib/node" --version test -f "$DIST/lib/host-service.js" || (echo "missing host-service.js" >&2; exit 1) test -f "$DIST/lib/pty-daemon.js" || (echo "missing pty-daemon.js" >&2; exit 1) + if [[ "${{ matrix.target }}" == darwin-* ]]; then + ARCH="${{ matrix.target }}" + ARCH="${ARCH#darwin-}" + HELPER="$DIST/lib/node_modules/node-pty/prebuilds/darwin-$ARCH/spawn-helper" + test -x "$HELPER" || (echo "spawn-helper not executable: $HELPER" >&2; exit 1) + fi + cd /tmp NODE_PATH="$DIST/lib/node_modules" "$DIST/lib/node" -e ' for (const m of ["better-sqlite3", "node-pty", "@parcel/watcher", "libsql"]) { require(m); console.log(m, "OK"); } ' + # Actually spawn a PTY against the bundled Node + bundled prebuild. + # Catches the runtime failures `require()` can't see — spawn-helper + # exec bit, pty.node ABI mismatch, arch mismatch, etc. Asserts + # node-pty resolves to the bundled copy so a host-tree leak + # (described above) doesn't silently make this pass. + NODE_PATH="$DIST/lib/node_modules" DIST="$DIST" "$DIST/lib/node" -e ' + const resolved = require.resolve("node-pty/lib/unixTerminal"); + if (!resolved.startsWith(process.env.DIST)) { + console.error("node-pty leaked from non-bundled tree:", resolved); + process.exit(1); + } + const pty = require("node-pty"); + const term = pty.spawn("/bin/sh", ["-c", "echo SPAWN_OK"], { + name: "xterm", cols: 80, rows: 24, + cwd: process.env.HOME || "/", env: process.env, + }); + let got = ""; + let exited = null; + const check = () => { + if (got.includes("SPAWN_OK") && exited && exited.exitCode === 0) { + console.log("pty spawn OK"); process.exit(0); + } + console.error("pty spawn FAIL exit=" + (exited && exited.exitCode) + " got=" + JSON.stringify(got)); + process.exit(1); + }; + term.onData((d) => { got += d.toString(); }); + // onExit can fire before the final onData chunk is delivered + // (SIGCHLD and EOF on the pty master are independent events). + // Defer the assertion one tick so any in-flight onData runs first. + term.onExit((e) => { exited = e; setTimeout(check, 100); }); + setTimeout(() => { console.error("pty spawn timeout"); process.exit(1); }, 5000); + ' - name: Upload tarball uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 diff --git a/packages/cli/scripts/build-dist-linux-docker.sh b/packages/cli/scripts/build-dist-linux-docker.sh index 5365d959052..a2db150aeaa 100755 --- a/packages/cli/scripts/build-dist-linux-docker.sh +++ b/packages/cli/scripts/build-dist-linux-docker.sh @@ -62,17 +62,47 @@ docker run --rm --platform "$PLATFORM" \ cd packages/cli bun run build:dist --target="$TARGET" - DIST="./dist/superset-${TARGET}" + DIST="$(pwd)/dist/superset-${TARGET}" "$DIST/bin/superset" --version "$DIST/bin/superset" --help | head -5 "$DIST/lib/node" --version test -f "$DIST/lib/host-service.js" || (echo "missing host-service.js" >&2; exit 1) test -f "$DIST/lib/pty-daemon.js" || (echo "missing pty-daemon.js" >&2; exit 1) + # Run from /tmp so Node module resolution does not walk up into the + # repo and leak into a non-bundled node-pty (host-tree shadowing). + cd /tmp NODE_PATH="$DIST/lib/node_modules" "$DIST/lib/node" -e " for (const m of [\"better-sqlite3\", \"node-pty\", \"@parcel/watcher\", \"libsql\"]) { require(m); console.log(m, \"OK\"); } " - echo "[docker-build] tarball: $(ls -la dist/superset-${TARGET}.tar.gz)" + NODE_PATH="$DIST/lib/node_modules" DIST="$DIST" "$DIST/lib/node" -e " + const resolved = require.resolve(\"node-pty/lib/unixTerminal\"); + if (!resolved.startsWith(process.env.DIST)) { + console.error(\"node-pty leaked from non-bundled tree:\", resolved); + process.exit(1); + } + const pty = require(\"node-pty\"); + const term = pty.spawn(\"/bin/sh\", [\"-c\", \"echo SPAWN_OK\"], { + name: \"xterm\", cols: 80, rows: 24, + cwd: process.env.HOME || \"/\", env: process.env, + }); + let got = \"\"; + let exited = null; + const check = () => { + if (got.includes(\"SPAWN_OK\") && exited && exited.exitCode === 0) { + console.log(\"pty spawn OK\"); process.exit(0); + } + console.error(\"pty spawn FAIL exit=\" + (exited && exited.exitCode) + \" got=\" + JSON.stringify(got)); + process.exit(1); + }; + term.onData((d) => { got += d.toString(); }); + // onExit can fire before the final onData chunk is delivered (SIGCHLD + // and EOF on the pty master are independent events). Defer the + // assertion one tick so any in-flight onData callback runs first. + term.onExit((e) => { exited = e; setTimeout(check, 100); }); + setTimeout(() => { console.error(\"pty spawn timeout\"); process.exit(1); }, 5000); + " + echo "[docker-build] tarball: $(ls -la "$DIST.tar.gz")" ' diff --git a/packages/cli/scripts/build-dist.ts b/packages/cli/scripts/build-dist.ts index 206a186a01e..65647d71a23 100644 --- a/packages/cli/scripts/build-dist.ts +++ b/packages/cli/scripts/build-dist.ts @@ -343,6 +343,27 @@ async function fixNativeBinariesForNode( ); rmSync(nodePtyBuild, { recursive: true, force: true }); } + + // node-pty's `prebuilds/darwin-{arch}/spawn-helper` ships from npm with + // mode 0644. node-pty posix_spawnp's it as the actual fork helper at + // terminal-open time — without +x the kernel returns EACCES and the + // failure surfaces only as the cryptic "posix_spawnp failed" with no + // errno. The normal install path runs `npm rebuild` which fixes the + // mode; we ship raw prebuilds so we have to fix it ourselves. + if (platform === "darwin") { + const { arch } = targetParts(target); + const spawnHelper = join( + destModules, + "node-pty", + "prebuilds", + `darwin-${arch}`, + "spawn-helper", + ); + if (existsSync(spawnHelper)) { + console.log(`[build-dist] chmod +x ${spawnHelper}`); + chmodSync(spawnHelper, 0o755); + } + } } async function buildCli(target: Target, outputPath: string): Promise {