@nubjs/nub 0.0.18 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/launch.js +105 -18
- package/package.json +10 -14
- package/postinstall.js +0 -55
package/bin/launch.js
CHANGED
|
@@ -1,40 +1,127 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Shared launcher used by bin/nub and bin/nubx.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// postinstall could not create the symlink.
|
|
4
|
+
// bin/nub and bin/nubx ship as committed `#!/usr/bin/env node` shims because the
|
|
5
|
+
// cross-platform @nubjs/nub package cannot ship a native binary (it doesn't know
|
|
6
|
+
// the target platform at publish time). On Windows that is the whole story: npm's
|
|
7
|
+
// generated nub.cmd / nubx.cmd invoke `node bin/nub`, which spawns the platform
|
|
8
|
+
// .exe (no shebang/symlink fast path on Windows).
|
|
10
9
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
10
|
+
// On its FIRST POSIX invocation this launcher SELF-HEALS: it rewrites the on-PATH
|
|
11
|
+
// entry that dispatched it — the package manager's bin shim (pnpm cmd-shim) or
|
|
12
|
+
// symlink (npm/bun) — into a MINIMAL `#!/bin/sh` trampoline that exec's the native
|
|
13
|
+
// binary directly. Every later call then resolves PATH -> that tiny sh trampoline
|
|
14
|
+
// -> native, skipping Node entirely (~native cold-start; the sh hop is ~1-2ms on
|
|
15
|
+
// Linux dash/busybox, ~4ms on macOS bash — vs ~50ms for this Node launcher).
|
|
16
|
+
//
|
|
17
|
+
// CRITICAL: the heal target is a minimal sh SCRIPT, never a native binary. A
|
|
18
|
+
// script->binary swap has an irreducible TOCTOU race (the kernel reads `#!/bin/sh`
|
|
19
|
+
// then `/bin/sh` reopens the path and finds a Mach-O -> "cannot execute binary
|
|
20
|
+
// file"; ~24% under a concurrent burst). A script->script swap (both `#!/bin/sh`,
|
|
21
|
+
// always valid scripts) is race-free by construction — a concurrent `/bin/sh`
|
|
22
|
+
// reopening the entry mid-swap always reads a valid trampoline (measured 0/600 vs
|
|
23
|
+
// 146/600). So the heal needs no lock: it is best-effort, atomic (write temp +
|
|
24
|
+
// rename), verify-before-clobber, and a no-op on Windows.
|
|
25
|
+
//
|
|
26
|
+
// The native binary selects its verb from argv[0]'s basename (nub vs nubx); the
|
|
27
|
+
// healed trampoline exec's bin/<verb> in the platform package (which ships both
|
|
28
|
+
// names), so no argv0 override is needed past the heal.
|
|
15
29
|
const { spawnSync } = require("child_process");
|
|
30
|
+
const fs = require("fs");
|
|
31
|
+
const path = require("path");
|
|
16
32
|
const { platformPackage } = require("../platform.js");
|
|
17
33
|
|
|
18
|
-
function resolveBinary() {
|
|
34
|
+
function resolveBinary(verb) {
|
|
19
35
|
const { key, pkg } = platformPackage();
|
|
20
36
|
if (!pkg) {
|
|
21
37
|
console.error(`@nubjs/nub: no prebuilt binary for ${key}`);
|
|
22
38
|
process.exit(1);
|
|
23
39
|
}
|
|
40
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
24
41
|
try {
|
|
25
|
-
return require.resolve(`${pkg}/bin
|
|
42
|
+
return require.resolve(`${pkg}/bin/${verb}${ext}`);
|
|
26
43
|
} catch {
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
// bin/nubx may be absent on an older platform package; fall back to bin/nub.
|
|
45
|
+
try {
|
|
46
|
+
return require.resolve(`${pkg}/bin/nub${ext}`);
|
|
47
|
+
} catch {
|
|
48
|
+
console.error(`@nubjs/nub: the ${pkg} package is not installed. Try: npm rebuild @nubjs/nub`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
29
51
|
}
|
|
30
52
|
}
|
|
31
53
|
|
|
32
|
-
//
|
|
33
|
-
|
|
54
|
+
// POSIX single-quote a string for safe embedding in the sh trampoline.
|
|
55
|
+
function shq(s) { return `'${String(s).replace(/'/g, "'\\''")}'`; }
|
|
56
|
+
|
|
57
|
+
// Verify a PATH entry demonstrably resolves to OUR launcher before replacing it —
|
|
58
|
+
// never clobber an unrelated `nub` (there is an unrelated nub@1.0.0 on npm). For a
|
|
59
|
+
// symlink, realpath(entry) must equal our launcher's realpath. For a pnpm cmd-shim
|
|
60
|
+
// (a regular #!/bin/sh file), every quoted path it references is $basedir-resolved
|
|
61
|
+
// and realpath'd; one must equal our launcher. Comparing realpaths (not substrings)
|
|
62
|
+
// matches pnpm's fresh AND regenerated shim forms and rejects comment-only mentions.
|
|
63
|
+
function leadsToUs(entry, st, ourReal) {
|
|
64
|
+
try {
|
|
65
|
+
if (st.isSymbolicLink()) {
|
|
66
|
+
try { return fs.realpathSync(entry) === ourReal; } catch { return false; }
|
|
67
|
+
}
|
|
68
|
+
if (st.isFile()) {
|
|
69
|
+
const body = fs.readFileSync(entry, "utf8");
|
|
70
|
+
const basedir = path.dirname(entry);
|
|
71
|
+
const quoted = body.match(/"([^"]+)"/g) || [];
|
|
72
|
+
for (const q of quoted) {
|
|
73
|
+
let p = q.slice(1, -1).replace(/\$\{?basedir\}?/g, basedir);
|
|
74
|
+
if (!p.includes("/")) continue;
|
|
75
|
+
if (!path.isAbsolute(p)) p = path.join(basedir, p);
|
|
76
|
+
try { if (fs.realpathSync(p) === ourReal) return true; } catch {}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch {}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Best-effort, never throws. Rewrite the on-PATH `<verb>` entry that dispatched us
|
|
84
|
+
// into a minimal sh trampoline -> the native binary. POSIX only.
|
|
85
|
+
function healPathEntry(verb, nativePath) {
|
|
86
|
+
if (process.platform === "win32") return;
|
|
87
|
+
try {
|
|
88
|
+
const ourBin = path.join(__dirname, verb); // .../@nubjs/nub/bin/<verb>
|
|
89
|
+
let ourReal; try { ourReal = fs.realpathSync(ourBin); } catch { ourReal = ourBin; }
|
|
90
|
+
let nativeReal; try { nativeReal = fs.realpathSync(nativePath); } catch { nativeReal = nativePath; }
|
|
91
|
+
const content = `#!/bin/sh\nexec ${shq(nativeReal)} "$@"\n`;
|
|
92
|
+
|
|
93
|
+
for (const dir of (process.env.PATH || "").split(path.delimiter)) {
|
|
94
|
+
if (!dir) continue;
|
|
95
|
+
const entry = path.join(dir, verb);
|
|
96
|
+
let st;
|
|
97
|
+
try { st = fs.lstatSync(entry); } catch { continue; }
|
|
98
|
+
if (!leadsToUs(entry, st, ourReal)) continue;
|
|
99
|
+
// Atomic replace: write a unique temp in the SAME dir, then rename over the
|
|
100
|
+
// entry (rename is atomic on POSIX; script->script means no exec-format race).
|
|
101
|
+
const tmp = path.join(dir, `.${verb}.nub.${process.pid}.${Date.now()}.tmp`);
|
|
102
|
+
try {
|
|
103
|
+
fs.writeFileSync(tmp, content, { mode: 0o755 });
|
|
104
|
+
fs.chmodSync(tmp, 0o755);
|
|
105
|
+
fs.renameSync(tmp, entry);
|
|
106
|
+
} catch {
|
|
107
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
108
|
+
}
|
|
109
|
+
break; // the first matching PATH entry is the one that dispatched us
|
|
110
|
+
}
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// argv0Name: the verb this stub represents ("nubx" for bin/nubx; undefined => nub).
|
|
34
115
|
module.exports = function launch(argv0Name) {
|
|
35
|
-
const
|
|
116
|
+
const verb = argv0Name || "nub";
|
|
117
|
+
const binPath = resolveBinary(verb);
|
|
118
|
+
// Self-heal the PATH entry on first POSIX call so later calls skip Node entirely.
|
|
119
|
+
healPathEntry(verb, binPath);
|
|
120
|
+
// This call still runs through Node; spawn the native binary. argv0 basename of
|
|
121
|
+
// binPath is the verb (bin/nub or bin/nubx), so the Rust CLI dispatches correctly
|
|
122
|
+
// without an argv0 override.
|
|
36
123
|
const opts = { stdio: "inherit", windowsHide: true };
|
|
37
|
-
if (argv0Name) opts.argv0 = argv0Name;
|
|
124
|
+
if (argv0Name) opts.argv0 = argv0Name; // belt-and-suspenders for the bin/nub fallback path
|
|
38
125
|
const result = spawnSync(binPath, process.argv.slice(2), opts);
|
|
39
126
|
if (result.error) {
|
|
40
127
|
console.error(`@nubjs/nub: failed to launch ${binPath}: ${result.error.message}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nubjs/nub",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "TypeScript-first developer supertool — a fast script runner and TS runtime powered by Node.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "https://github.com/nubjs/nub",
|
|
@@ -11,20 +11,16 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"bin",
|
|
14
|
-
"platform.js"
|
|
15
|
-
"postinstall.js"
|
|
14
|
+
"platform.js"
|
|
16
15
|
],
|
|
17
|
-
"scripts": {
|
|
18
|
-
"postinstall": "node postinstall.js"
|
|
19
|
-
},
|
|
20
16
|
"optionalDependencies": {
|
|
21
|
-
"@nubjs/nub-darwin-arm64": "0.0.
|
|
22
|
-
"@nubjs/nub-darwin-x64": "0.0.
|
|
23
|
-
"@nubjs/nub-linux-x64": "0.0.
|
|
24
|
-
"@nubjs/nub-linux-x64-musl": "0.0.
|
|
25
|
-
"@nubjs/nub-linux-arm64": "0.0.
|
|
26
|
-
"@nubjs/nub-linux-arm64-musl": "0.0.
|
|
27
|
-
"@nubjs/nub-win32-x64": "0.0.
|
|
28
|
-
"@nubjs/nub-win32-arm64": "0.0.
|
|
17
|
+
"@nubjs/nub-darwin-arm64": "0.0.19",
|
|
18
|
+
"@nubjs/nub-darwin-x64": "0.0.19",
|
|
19
|
+
"@nubjs/nub-linux-x64": "0.0.19",
|
|
20
|
+
"@nubjs/nub-linux-x64-musl": "0.0.19",
|
|
21
|
+
"@nubjs/nub-linux-arm64": "0.0.19",
|
|
22
|
+
"@nubjs/nub-linux-arm64-musl": "0.0.19",
|
|
23
|
+
"@nubjs/nub-win32-x64": "0.0.19",
|
|
24
|
+
"@nubjs/nub-win32-arm64": "0.0.19"
|
|
29
25
|
}
|
|
30
26
|
}
|
package/postinstall.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
const { platform } = process;
|
|
2
|
-
const { mkdirSync, unlinkSync, symlinkSync, copyFileSync, chmodSync } = require("fs");
|
|
3
|
-
const { join } = require("path");
|
|
4
|
-
const { platformPackage } = require("./platform.js");
|
|
5
|
-
|
|
6
|
-
const { key, pkg } = platformPackage();
|
|
7
|
-
|
|
8
|
-
if (!pkg) {
|
|
9
|
-
console.error(`@nubjs/nub: no prebuilt binary for ${key}`);
|
|
10
|
-
process.exit(0);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Windows: there is no symlink fast path. npm's generated nub.cmd / nubx.cmd invoke
|
|
14
|
-
// the JS launchers (bin/nub, bin/nubx), which resolve and spawn the platform .exe at
|
|
15
|
-
// runtime. Nothing to do at install time — leave the launchers in place.
|
|
16
|
-
if (platform === "win32") {
|
|
17
|
-
process.exit(0);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
let binSrc;
|
|
21
|
-
try {
|
|
22
|
-
binSrc = require.resolve(`${pkg}/bin/nub`);
|
|
23
|
-
} catch {
|
|
24
|
-
// optionalDependency not installed (wrong platform) — leave the JS launchers as a
|
|
25
|
-
// fallback; they print an actionable error if invoked.
|
|
26
|
-
process.exit(0);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// POSIX fast path: replace each JS launcher with a direct symlink to the platform
|
|
30
|
-
// binary, so `nub` and `nubx` exec the native Rust binary with no Node bootstrap.
|
|
31
|
-
// Both names point at the SAME binary; the Rust CLI selects its verb from argv[0]'s
|
|
32
|
-
// basename (nub vs nubx — see crates/nub-cli/src/cli.rs Argv0::detect), and
|
|
33
|
-
// process.execPath still resolves to the platform binary so the sibling runtime/
|
|
34
|
-
// directory is found by walking up.
|
|
35
|
-
const binDir = join(__dirname, "bin");
|
|
36
|
-
mkdirSync(binDir, { recursive: true });
|
|
37
|
-
for (const name of ["nub", "nubx"]) {
|
|
38
|
-
const dest = join(binDir, name);
|
|
39
|
-
try { unlinkSync(dest); } catch {}
|
|
40
|
-
try {
|
|
41
|
-
symlinkSync(binSrc, dest);
|
|
42
|
-
chmodSync(dest, 0o755);
|
|
43
|
-
} catch {
|
|
44
|
-
// Fallback: copy if symlink fails (e.g. cross-device). Slower path resolution
|
|
45
|
-
// but correct — argv[0] still resolves to a binary named nub/nubx.
|
|
46
|
-
try {
|
|
47
|
-
copyFileSync(binSrc, dest);
|
|
48
|
-
chmodSync(dest, 0o755);
|
|
49
|
-
} catch (err) {
|
|
50
|
-
console.error(`@nubjs/nub: failed to install ${name} binary: ${err.message}`);
|
|
51
|
-
process.exit(0);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
chmodSync(binSrc, 0o755);
|