@nubjs/nub 0.0.44 → 0.0.46
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 +68 -7
- package/package.json +9 -9
package/bin/launch.js
CHANGED
|
@@ -57,6 +57,64 @@ function resolveBinary(verb) {
|
|
|
57
57
|
// POSIX single-quote a string for safe embedding in the sh trampoline.
|
|
58
58
|
function shq(s) { return `'${String(s).replace(/'/g, "'\\''")}'`; }
|
|
59
59
|
|
|
60
|
+
// True iff `p` is executable by THIS process (the +x bits that matter to us).
|
|
61
|
+
function isExecutable(p) {
|
|
62
|
+
try { fs.accessSync(p, fs.constants.X_OK); return true; } catch { return false; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Make `binPath` runnable by us, returning a path that IS executable — `binPath`
|
|
66
|
+
// itself when we can fix it in place, otherwise a user-owned executable copy.
|
|
67
|
+
//
|
|
68
|
+
// npm strips +x on extract from any file that isn't a `bin`-field entry, and the
|
|
69
|
+
// platform packages declare no `bin` field, so the native binary lands 0o644. The
|
|
70
|
+
// install-time `postinstall.js` chmods it back — but when npm SKIPS lifecycle scripts
|
|
71
|
+
// (npm v12's default, or `ignore-scripts=true`) that never runs, leaving a 0o644
|
|
72
|
+
// binary at runtime. The launcher's in-place chmod recovers the common case (we own
|
|
73
|
+
// the file). It CANNOT recover the canonical container case: `root` does `npm i -g`,
|
|
74
|
+
// the image drops to a non-root `USER`, and that user's first `nub` can neither run
|
|
75
|
+
// the 0o644 binary nor chmod a file it doesn't own — `spawn` dies EACCES (verified:
|
|
76
|
+
// scripts-off install + non-root run + root-owned 0o644 binary). For THAT case we
|
|
77
|
+
// copy the binary into a user-owned cache dir at 0o755 and run the copy. This is the
|
|
78
|
+
// scripts-off complement to postinstall's install-time chmod: self-heal that holds
|
|
79
|
+
// even when we're not the binary's owner. Best-effort; returns the original path if
|
|
80
|
+
// every recovery fails (the caller surfaces the spawn error).
|
|
81
|
+
function ensureExecutable(binPath, verb) {
|
|
82
|
+
if (process.platform === "win32") return binPath; // .exe needs no +x bit
|
|
83
|
+
if (isExecutable(binPath)) return binPath;
|
|
84
|
+
// Common case: we own the file (e.g. `sudo`-free user-prefix install) — chmod in place.
|
|
85
|
+
try { fs.chmodSync(binPath, 0o755); } catch {}
|
|
86
|
+
if (isExecutable(binPath)) return binPath;
|
|
87
|
+
// Non-owner / read-only store: stage a user-owned executable copy in the cache.
|
|
88
|
+
// Key the copy on the source path + size + mtime so a binary upgrade re-stages and
|
|
89
|
+
// we never exec stale bytes; an existing fresh+executable copy is reused as-is.
|
|
90
|
+
try {
|
|
91
|
+
const st = fs.statSync(binPath);
|
|
92
|
+
const base = process.env.XDG_CACHE_HOME ||
|
|
93
|
+
(os.homedir() && os.homedir() !== "/" ? path.join(os.homedir(), ".cache") : null);
|
|
94
|
+
if (!base) return binPath;
|
|
95
|
+
// The copy's FILENAME must stay the bare verb (`nub`/`nubx`): the native binary
|
|
96
|
+
// selects its verb from argv[0]'s basename, and the healed sh trampoline `exec`s
|
|
97
|
+
// this path with no argv0 override — a tagged filename like `nubx-<tag>` would make
|
|
98
|
+
// `nubx` misclassify as plain `nub`. So the staleness tag goes in the DIRECTORY,
|
|
99
|
+
// and the executable keeps its real name: `<cache>/nub/bin/<tag>/<verb>`.
|
|
100
|
+
const tag = `${st.size}-${Math.trunc(st.mtimeMs)}`;
|
|
101
|
+
const dir = path.join(base, "nub", "bin", tag);
|
|
102
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
103
|
+
const dest = path.join(dir, verb);
|
|
104
|
+
if (isExecutable(dest)) {
|
|
105
|
+
const dst = fs.statSync(dest);
|
|
106
|
+
if (dst.size === st.size) return dest; // already staged, current, runnable
|
|
107
|
+
}
|
|
108
|
+
// Atomic stage: copy to a unique temp in the same dir, chmod, rename into place.
|
|
109
|
+
const tmp = path.join(dir, `.${verb}.${process.pid}.${Date.now()}.tmp`);
|
|
110
|
+
fs.copyFileSync(binPath, tmp);
|
|
111
|
+
fs.chmodSync(tmp, 0o755);
|
|
112
|
+
fs.renameSync(tmp, dest);
|
|
113
|
+
if (isExecutable(dest)) return dest;
|
|
114
|
+
} catch {}
|
|
115
|
+
return binPath; // give up; caller's spawn surfaces the real error
|
|
116
|
+
}
|
|
117
|
+
|
|
60
118
|
// Verify a PATH entry demonstrably resolves to OUR launcher before replacing it —
|
|
61
119
|
// never clobber an unrelated `nub` (there is an unrelated nub@1.0.0 on npm). For a
|
|
62
120
|
// symlink, realpath(entry) must equal our launcher's realpath. For a pnpm cmd-shim
|
|
@@ -128,15 +186,18 @@ function healPathEntry(verb, nativePath) {
|
|
|
128
186
|
// argv0Name: the verb this stub represents ("nubx" for bin/nubx; undefined => nub).
|
|
129
187
|
module.exports = function launch(argv0Name) {
|
|
130
188
|
const verb = argv0Name || "nub";
|
|
131
|
-
const
|
|
189
|
+
const resolved = resolveBinary(verb);
|
|
132
190
|
// Ensure the platform binary is executable. npm strips the +x bit on install from
|
|
133
191
|
// files that aren't `bin`-field entries, and the platform package declares no bin
|
|
134
|
-
// field — so the
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
192
|
+
// field — so the binary lands 0o644 and both spawn (below) and the healed sh
|
|
193
|
+
// trampoline's `exec` would EACCES / "Permission denied". `postinstall.js` chmods it
|
|
194
|
+
// at INSTALL time, but that's skipped under npm v12's scripts-off default (or
|
|
195
|
+
// `ignore-scripts=true`); ensureExecutable is the runtime net for that. It chmods in
|
|
196
|
+
// place when we own the file, and falls back to a user-owned executable COPY when we
|
|
197
|
+
// don't (the root-installs-then-drops-to-non-root container case the launcher's bare
|
|
198
|
+
// chmod cannot reach). Returns a path that IS executable; the heal + spawn both use
|
|
199
|
+
// it so the fast-path trampoline targets the runnable file too.
|
|
200
|
+
const binPath = ensureExecutable(resolved, verb);
|
|
140
201
|
// Self-heal the PATH entry on first POSIX call so later calls skip Node entirely.
|
|
141
202
|
healPathEntry(verb, binPath);
|
|
142
203
|
// This call still runs through Node; spawn the native binary. argv0 basename of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nubjs/nub",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.46",
|
|
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",
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
"LICENSE"
|
|
37
37
|
],
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@nubjs/nub-darwin-arm64": "0.0.
|
|
40
|
-
"@nubjs/nub-darwin-x64": "0.0.
|
|
41
|
-
"@nubjs/nub-linux-x64": "0.0.
|
|
42
|
-
"@nubjs/nub-linux-x64-musl": "0.0.
|
|
43
|
-
"@nubjs/nub-linux-arm64": "0.0.
|
|
44
|
-
"@nubjs/nub-linux-arm64-musl": "0.0.
|
|
45
|
-
"@nubjs/nub-win32-x64": "0.0.
|
|
46
|
-
"@nubjs/nub-win32-arm64": "0.0.
|
|
39
|
+
"@nubjs/nub-darwin-arm64": "0.0.46",
|
|
40
|
+
"@nubjs/nub-darwin-x64": "0.0.46",
|
|
41
|
+
"@nubjs/nub-linux-x64": "0.0.46",
|
|
42
|
+
"@nubjs/nub-linux-x64-musl": "0.0.46",
|
|
43
|
+
"@nubjs/nub-linux-arm64": "0.0.46",
|
|
44
|
+
"@nubjs/nub-linux-arm64-musl": "0.0.46",
|
|
45
|
+
"@nubjs/nub-win32-x64": "0.0.46",
|
|
46
|
+
"@nubjs/nub-win32-arm64": "0.0.46"
|
|
47
47
|
}
|
|
48
48
|
}
|