@gjsify/cli 0.26.0 → 0.27.0
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/dist/cli.gjs.mjs +184 -182
- package/lib/cli-app.js +3 -1
- package/lib/commands/affected.js +146 -15
- package/lib/commands/build.js +21 -14
- package/lib/commands/check.js +40 -4
- package/lib/commands/clear.d.ts +8 -0
- package/lib/commands/clear.js +90 -0
- package/lib/commands/copy.d.ts +8 -0
- package/lib/commands/copy.js +87 -0
- package/lib/commands/dlx.js +8 -1
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/install.d.ts +4 -0
- package/lib/commands/install.js +164 -15
- package/lib/commands/onboard.js +17 -14
- package/lib/commands/publish.js +18 -4
- package/lib/commands/storybook.js +5 -1
- package/lib/commands/test.js +29 -2
- package/lib/commands/uninstall.js +44 -2
- package/lib/commands/upgrade.js +1 -4
- package/lib/templates/install.mjs.tmpl +110 -19
- package/lib/utils/bin-shim.d.ts +73 -0
- package/lib/utils/bin-shim.js +97 -2
- package/lib/utils/build-cache.d.ts +7 -27
- package/lib/utils/build-cache.js +34 -4
- package/lib/utils/check-system-deps.d.ts +15 -1
- package/lib/utils/check-system-deps.js +126 -2
- package/lib/utils/clear-targets.d.ts +30 -0
- package/lib/utils/clear-targets.js +114 -0
- package/lib/utils/copy-targets.d.ts +41 -0
- package/lib/utils/copy-targets.js +145 -0
- package/lib/utils/detect-native-packages.d.ts +160 -5
- package/lib/utils/detect-native-packages.js +326 -50
- package/lib/utils/dir-link.d.ts +86 -0
- package/lib/utils/dir-link.js +140 -0
- package/lib/utils/dlx-cache.d.ts +30 -3
- package/lib/utils/dlx-cache.js +61 -7
- package/lib/utils/install-backend-native.d.ts +109 -1
- package/lib/utils/install-backend-native.js +622 -59
- package/lib/utils/install-backend.js +17 -15
- package/lib/utils/install-global.js +6 -34
- package/lib/utils/install-lock.js +1 -1
- package/lib/utils/install-packument-cache.d.ts +51 -9
- package/lib/utils/install-packument-cache.js +88 -25
- package/lib/utils/oxc-resolve.d.ts +27 -1
- package/lib/utils/oxc-resolve.js +53 -26
- package/lib/utils/pkg-json.d.ts +24 -0
- package/lib/utils/pkg-json.js +27 -1
- package/lib/utils/platform-check.d.ts +215 -0
- package/lib/utils/platform-check.js +377 -0
- package/lib/utils/resolve-npm-package.js +9 -2
- package/lib/utils/run-gjs.d.ts +1 -1
- package/lib/utils/run-gjs.js +19 -3
- package/lib/utils/spawn.js +50 -2
- package/lib/utils/win32-command.d.ts +54 -0
- package/lib/utils/win32-command.js +140 -0
- package/package.json +19 -20
package/lib/commands/upgrade.js
CHANGED
|
@@ -448,10 +448,7 @@ function printTable(candidates) {
|
|
|
448
448
|
'kind' +
|
|
449
449
|
ANSI.reset;
|
|
450
450
|
console.log(head);
|
|
451
|
-
console.log(' '.repeat(idxW + 2) +
|
|
452
|
-
ANSI.dim +
|
|
453
|
-
'─'.repeat(nameW + fanW + curW + newW + 16) +
|
|
454
|
-
ANSI.reset);
|
|
451
|
+
console.log(' '.repeat(idxW + 2) + ANSI.dim + '─'.repeat(nameW + fanW + curW + newW + 16) + ANSI.reset);
|
|
455
452
|
for (let i = 0; i < candidates.length; i++) {
|
|
456
453
|
const c = candidates[i];
|
|
457
454
|
const idx = `${i + 1}.`.padEnd(idxW);
|
|
@@ -26,8 +26,11 @@
|
|
|
26
26
|
* Test hooks (set by tests/e2e/install-script/run.mjs):
|
|
27
27
|
* GJSIFY_INSTALL_BOOTSTRAP_URL override the cli.gjs.mjs download origin
|
|
28
28
|
* (accepts file:// for offline tests)
|
|
29
|
-
* GJSIFY_INSTALL_BOOTSTRAP_SHA256_URL override the .sha256 companion URL
|
|
30
|
-
*
|
|
29
|
+
* GJSIFY_INSTALL_BOOTSTRAP_SHA256_URL override the .sha256 companion URL.
|
|
30
|
+
* An EMPTY string is the only way to install
|
|
31
|
+
* without verification, and it warns loudly; a
|
|
32
|
+
* digest URL that is set but unreachable is a
|
|
33
|
+
* hard failure, never a silent downgrade.
|
|
31
34
|
* GJSIFY_GLOBAL_PREFIX override install prefix (forwarded to cli)
|
|
32
35
|
* GJSIFY_GLOBAL_BIN_DIR override bin dir (forwarded to cli)
|
|
33
36
|
* GJSIFY_INSTALL_REGISTRY override npm registry (forwarded as npm_config_registry)
|
|
@@ -50,6 +53,11 @@ const DEFAULT_BOOTSTRAP_SHA256_URL = `${DEFAULT_BOOTSTRAP_URL}.sha256`;
|
|
|
50
53
|
|
|
51
54
|
const USER_AGENT = 'gjsify-installer/1.0';
|
|
52
55
|
|
|
56
|
+
// The bootstrap cache is CONTENT-ADDRESSED: `cli-<sha256>.gjs.mjs`. See
|
|
57
|
+
// `resolveBootstrap()` for why the digest, not a fixed name, keys it.
|
|
58
|
+
const CACHE_PREFIX = 'cli-';
|
|
59
|
+
const CACHE_SUFFIX = '.gjs.mjs';
|
|
60
|
+
|
|
53
61
|
function info(msg) {
|
|
54
62
|
print(`[gjsify] ${msg}`);
|
|
55
63
|
}
|
|
@@ -168,35 +176,118 @@ function writeBytes(path, bytes) {
|
|
|
168
176
|
Gio.File.new_for_path(path).replace_contents(bytes, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null);
|
|
169
177
|
}
|
|
170
178
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
function readBytesOrNull(path) {
|
|
180
|
+
try {
|
|
181
|
+
const [, bytes] = Gio.File.new_for_path(path).load_contents(null);
|
|
182
|
+
return bytes;
|
|
183
|
+
} catch {
|
|
184
|
+
// An absent — or unreadable, e.g. truncated by a killed earlier run —
|
|
185
|
+
// cache entry is a MISS, not an error. `load_contents` is a real
|
|
186
|
+
// `throws="1"` GIR call, so this catch has a genuine throw path.
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Drop every other cached bootstrap so the content-addressed cache stays
|
|
193
|
+
* bounded. Each entry is a ~6.6 MB bundle and the digest changes with every
|
|
194
|
+
* release — without this, keying the cache by content would trade "the cache
|
|
195
|
+
* can never be warm" for "the cache grows forever".
|
|
196
|
+
*/
|
|
197
|
+
function pruneCache(dir, keepBasename) {
|
|
198
|
+
const children = Gio.File.new_for_path(dir).enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
|
|
199
|
+
for (;;) {
|
|
200
|
+
const info_ = children.next_file(null);
|
|
201
|
+
if (info_ === null) break;
|
|
202
|
+
const name = info_.get_name();
|
|
203
|
+
if (name === keepBasename || !name.startsWith(CACHE_PREFIX) || !name.endsWith(CACHE_SUFFIX)) continue;
|
|
204
|
+
Gio.File.new_for_path(GLib.build_filenamev([dir, name])).delete(null);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Resolve a VERIFIED bootstrap bundle and return its path.
|
|
210
|
+
*
|
|
211
|
+
* The digest is fetched BEFORE the bundle, and the cache is keyed by it. That
|
|
212
|
+
* ordering is the mechanism, not an optimisation — it fixes two independent
|
|
213
|
+
* defects that both came from doing it the other way round:
|
|
214
|
+
*
|
|
215
|
+
* 1. Verification was skippable by anyone who could break ONE request. The
|
|
216
|
+
* bundle was downloaded first and verified after, and a failed `.sha256`
|
|
217
|
+
* fetch merely logged "skipping verification" and carried on — so a proxy,
|
|
218
|
+
* a 404, or a captive portal silently downgraded the install to no
|
|
219
|
+
* verification at all. Fetching the digest first makes that impossible:
|
|
220
|
+
* no digest, no install.
|
|
221
|
+
* 2. The cache could never be warm. It wrote a fixed `cli.gjs.mjs` and never
|
|
222
|
+
* read it back, so every run re-downloaded ~6.6 MB. A content-addressed
|
|
223
|
+
* name lets a matching entry short-circuit the download entirely.
|
|
224
|
+
*
|
|
225
|
+
* Skipping verification is still possible, but only as an EXPLICIT act
|
|
226
|
+
* (`GJSIFY_INSTALL_BOOTSTRAP_SHA256_URL=''`), and it says so on stderr.
|
|
227
|
+
*/
|
|
228
|
+
async function resolveBootstrap(session, bootstrapUrl, sha256Url) {
|
|
229
|
+
let expected = null;
|
|
230
|
+
if (sha256Url) {
|
|
231
|
+
info(`Fetching published SHA-256 from ${sha256Url} ...`);
|
|
232
|
+
let sumBytes;
|
|
177
233
|
try {
|
|
178
|
-
|
|
179
|
-
sumExpected = new TextDecoder().decode(sumBytes).trim().split(/\s+/)[0];
|
|
234
|
+
sumBytes = await fetchBytes(session, sha256Url);
|
|
180
235
|
} catch (err) {
|
|
181
|
-
error(`Could not fetch ${sha256Url}
|
|
236
|
+
error(`Could not fetch ${sha256Url}: ${err.message}`);
|
|
237
|
+
error('Refusing to install an UNVERIFIED bootstrap bundle.');
|
|
238
|
+
error("To bootstrap without verification, set GJSIFY_INSTALL_BOOTSTRAP_SHA256_URL='' explicitly.");
|
|
239
|
+
exit(1);
|
|
182
240
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
exit(1);
|
|
188
|
-
}
|
|
241
|
+
expected = (new TextDecoder().decode(sumBytes).trim().split(/\s+/)[0] ?? '').toLowerCase();
|
|
242
|
+
if (!/^[0-9a-f]{64}$/.test(expected)) {
|
|
243
|
+
error(`${sha256Url} did not contain a SHA-256 digest (read ${JSON.stringify(expected.slice(0, 80))}).`);
|
|
244
|
+
exit(1);
|
|
189
245
|
}
|
|
246
|
+
} else {
|
|
247
|
+
error("SHA-256 verification is DISABLED (GJSIFY_INSTALL_BOOTSTRAP_SHA256_URL='') — the bundle is unverified.");
|
|
190
248
|
}
|
|
249
|
+
|
|
191
250
|
const dir = cacheDir();
|
|
251
|
+
const basename = expected
|
|
252
|
+
? `${CACHE_PREFIX}${expected}${CACHE_SUFFIX}`
|
|
253
|
+
: `${CACHE_PREFIX}unverified${CACHE_SUFFIX}`;
|
|
254
|
+
const bundlePath = GLib.build_filenamev([dir, basename]);
|
|
255
|
+
|
|
256
|
+
if (expected) {
|
|
257
|
+
// Re-hash rather than trusting the filename: the name is only a CLAIM
|
|
258
|
+
// about the contents, and a truncated entry keeps its name.
|
|
259
|
+
const cached = readBytesOrNull(bundlePath);
|
|
260
|
+
if (cached && sha256Hex(cached).toLowerCase() === expected) {
|
|
261
|
+
info(`Reusing verified bootstrap ${bundlePath} (${cached.length} bytes)`);
|
|
262
|
+
return bundlePath;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
info(`Downloading bootstrap from ${bootstrapUrl} ...`);
|
|
267
|
+
const bundleBytes = await fetchBytes(session, bootstrapUrl);
|
|
268
|
+
if (expected) {
|
|
269
|
+
const actual = sha256Hex(bundleBytes).toLowerCase();
|
|
270
|
+
if (actual !== expected) {
|
|
271
|
+
error(`SHA-256 mismatch: expected ${expected}, got ${actual}`);
|
|
272
|
+
exit(1);
|
|
273
|
+
}
|
|
274
|
+
info('SHA-256 verified.');
|
|
275
|
+
}
|
|
276
|
+
|
|
192
277
|
try {
|
|
193
278
|
ensureDir(dir);
|
|
194
279
|
} catch {
|
|
195
280
|
/* exists */
|
|
196
281
|
}
|
|
197
|
-
const bundlePath = GLib.build_filenamev([dir, 'cli.gjs.mjs']);
|
|
198
282
|
writeBytes(bundlePath, bundleBytes);
|
|
199
283
|
info(`Bootstrap cached at ${bundlePath} (${bundleBytes.length} bytes)`);
|
|
284
|
+
try {
|
|
285
|
+
pruneCache(dir, basename);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
// Housekeeping only: a read-only or concurrently-modified cache dir
|
|
288
|
+
// must never fail an install.
|
|
289
|
+
info(`Could not prune the bootstrap cache: ${err.message}`);
|
|
290
|
+
}
|
|
200
291
|
return bundlePath;
|
|
201
292
|
}
|
|
202
293
|
|
|
@@ -232,7 +323,7 @@ async function main() {
|
|
|
232
323
|
const session = new Soup.Session();
|
|
233
324
|
let bundlePath;
|
|
234
325
|
try {
|
|
235
|
-
bundlePath = await
|
|
326
|
+
bundlePath = await resolveBootstrap(session, opts.bootstrapUrl, opts.bootstrapSha256Url);
|
|
236
327
|
} catch (err) {
|
|
237
328
|
error(`Bootstrap download failed: ${err.message}`);
|
|
238
329
|
exit(1);
|
package/lib/utils/bin-shim.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HostLibc } from './detect-native-packages.js';
|
|
1
2
|
/** The three sibling files a Windows bin entry consists of. */
|
|
2
3
|
export interface WindowsShimFiles {
|
|
3
4
|
/** POSIX `sh` script — the extension-less `<name>` file. */
|
|
@@ -104,6 +105,20 @@ export declare function buildLauncherShims(opts: {
|
|
|
104
105
|
* {@link buildLauncherShims} from the same baked list, and `PATH` doubles as the
|
|
105
106
|
* DLL search path with `;` separators. It keeps the snapshot behaviour.
|
|
106
107
|
*
|
|
108
|
+
* MACOS CAVEAT — the `${DYLD_LIBRARY_PATH:+…}` half of the export can never
|
|
109
|
+
* fire, and the export itself does not survive a script boundary. System
|
|
110
|
+
* Integrity Protection strips every `DYLD_*` variable from the environment
|
|
111
|
+
* whenever a PROTECTED binary is exec'd, and `/bin/sh` is protected — so by the
|
|
112
|
+
* time this preamble runs, an inherited `DYLD_LIBRARY_PATH` is already gone
|
|
113
|
+
* (measured: `DYLD_LIBRARY_PATH=x /bin/sh -c 'echo $DYLD_LIBRARY_PATH'` prints
|
|
114
|
+
* nothing, while the same through Homebrew's unprotected `node` prints `x`).
|
|
115
|
+
* The value this preamble exports is likewise dropped again at the next
|
|
116
|
+
* `/bin/sh -c` hop, which is how every package script runs. Do NOT "simplify"
|
|
117
|
+
* the preservation away as dead code — it is load-bearing on Linux. And do not
|
|
118
|
+
* rely on `DYLD_LIBRARY_PATH` alone to make a darwin prebuild loadable across a
|
|
119
|
+
* script chain: the self-relative `@loader_path` rpath that
|
|
120
|
+
* `check-prebuild-loader-path.mjs` enforces is what actually has to carry it.
|
|
121
|
+
*
|
|
107
122
|
* @param scanRoot Directory whose `node_modules` is globbed at launch time.
|
|
108
123
|
* @param bakedDirs Prebuild dirs found at write time; entries outside `scanRoot`
|
|
109
124
|
* are embedded (entries inside it are dropped — the scan finds them).
|
|
@@ -112,4 +127,62 @@ export declare function buildLauncherShims(opts: {
|
|
|
112
127
|
export declare function buildNativeEnvPreamble(scanRoot: string, bakedDirs?: readonly string[], target?: {
|
|
113
128
|
platform?: string;
|
|
114
129
|
arch?: string;
|
|
130
|
+
libc?: HostLibc | null;
|
|
131
|
+
}): string;
|
|
132
|
+
/** POSIX `sh` single-quoting for a launcher's target path. */
|
|
133
|
+
export declare function shQuoteArg(s: string): string;
|
|
134
|
+
/**
|
|
135
|
+
* Expand a `bin` / `gjsify.bin` declaration into `binName → relative target`.
|
|
136
|
+
*
|
|
137
|
+
* The string form is npm's shorthand for `{ <package-name-without-scope>: <path> }`.
|
|
138
|
+
*/
|
|
139
|
+
export declare function normalizeBinMap(pkgName: string, bin: string | Record<string, string>): Map<string, string>;
|
|
140
|
+
/**
|
|
141
|
+
* Which bin map a package wants installed, and whether it is the GJS-runnable one.
|
|
142
|
+
*
|
|
143
|
+
* `gjsify.bin` WINS over npm `bin` when declared. The npm field can only ever
|
|
144
|
+
* name the Node entry — npm has no way to express "and this other file is the
|
|
145
|
+
* one to run when the host has `gjs` but no `node`" — so a package that ships
|
|
146
|
+
* both declares the GJS bundle here. Honouring it is what makes a Node-LESS GJS
|
|
147
|
+
* host work at all: on such a host the npm bin's `#!/usr/bin/env node` shebang
|
|
148
|
+
* fails with `env: can't execute 'node'`, and because the launcher is also the
|
|
149
|
+
* only thing that can export `GI_TYPELIB_PATH` before the GJS runtime starts,
|
|
150
|
+
* that failure takes `gjsify build` down with it (the typelib lookup happens
|
|
151
|
+
* before the bundle's first line — the CLI cannot repair it from the inside).
|
|
152
|
+
*
|
|
153
|
+
* Measured on a postmarketOS/aarch64 phone with gjs 1.88 and no node: install
|
|
154
|
+
* and prebuild loading both worked, and only the `#!/usr/bin/env node` shim in
|
|
155
|
+
* `node_modules/.bin/` stood between that host and a working `gjsify build`.
|
|
156
|
+
*
|
|
157
|
+
* `isGjsBin` lets the caller keep plain-Node packages on the plain symlink path
|
|
158
|
+
* — this must not change how `lodash`'s bin is linked.
|
|
159
|
+
*/
|
|
160
|
+
export declare function pickBinMap(pkgName: string, pkgJson: {
|
|
161
|
+
bin?: string | Record<string, string>;
|
|
162
|
+
gjsify?: {
|
|
163
|
+
bin?: string | Record<string, string>;
|
|
164
|
+
};
|
|
165
|
+
}): {
|
|
166
|
+
map: Map<string, string>;
|
|
167
|
+
isGjsBin: boolean;
|
|
168
|
+
} | null;
|
|
169
|
+
/**
|
|
170
|
+
* The POSIX `sh` launcher gjsify writes for a bin it owns.
|
|
171
|
+
*
|
|
172
|
+
* Why a launcher and not a symlink: when a GJS bundle is reached through a
|
|
173
|
+
* symlink, the kernel resolves it to find the executable but hands the ORIGINAL
|
|
174
|
+
* path to it, so `gjs -m <symlink>` makes `import.meta.url` point at the link's
|
|
175
|
+
* directory and every asset read relative to it looks in the wrong place. An
|
|
176
|
+
* explicit `exec` with the real path avoids that.
|
|
177
|
+
*
|
|
178
|
+
* `.gjs.mjs`/`.mjs` targets are wrapped in `gjs -m` rather than exec'd directly
|
|
179
|
+
* because not every published bundle carries a `#!/usr/bin/env -S gjs -m` line,
|
|
180
|
+
* and direct-exec'ing a shebang-less `.mjs` falls through to `/bin/sh`, which
|
|
181
|
+
* then parses JavaScript as shell.
|
|
182
|
+
*/
|
|
183
|
+
export declare function buildShLauncher(targetAbs: string, opts: {
|
|
184
|
+
envPreamble?: string;
|
|
185
|
+
isGjsBundle: boolean;
|
|
115
186
|
}): string;
|
|
187
|
+
/** True when a bin target is a GJS-runnable bundle rather than a Node script. */
|
|
188
|
+
export declare function isGjsBundlePath(target: string): boolean;
|
package/lib/utils/bin-shim.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// All three re-derive the interpreter from the target's shebang, so a bin
|
|
25
25
|
// declaring `#!/usr/bin/env node` and one declaring `#!/usr/bin/env -S gjs -m`
|
|
26
26
|
// both work without the caller knowing which it is.
|
|
27
|
-
import { libraryPathVar, prebuildDirCandidates } from './detect-native-packages.js';
|
|
27
|
+
import { detectHostLibc, libraryPathVar, prebuildDirCandidates } from './detect-native-packages.js';
|
|
28
28
|
// Matches `#!<prog> <args>` and `#!/usr/bin/env [-S] [K=V ...] <prog> <args>`.
|
|
29
29
|
// Verbatim from cmd-shim's `shebangExpr`.
|
|
30
30
|
const SHEBANG_RE = /^#!\s*(?:\/usr\/bin\/env\s+(?:-S\s+)?((?:[^ \t=]+=[^ \t=]+\s+)*))?([^ \t]+)(.*)$/;
|
|
@@ -289,6 +289,20 @@ function shQuote(s) {
|
|
|
289
289
|
* {@link buildLauncherShims} from the same baked list, and `PATH` doubles as the
|
|
290
290
|
* DLL search path with `;` separators. It keeps the snapshot behaviour.
|
|
291
291
|
*
|
|
292
|
+
* MACOS CAVEAT — the `${DYLD_LIBRARY_PATH:+…}` half of the export can never
|
|
293
|
+
* fire, and the export itself does not survive a script boundary. System
|
|
294
|
+
* Integrity Protection strips every `DYLD_*` variable from the environment
|
|
295
|
+
* whenever a PROTECTED binary is exec'd, and `/bin/sh` is protected — so by the
|
|
296
|
+
* time this preamble runs, an inherited `DYLD_LIBRARY_PATH` is already gone
|
|
297
|
+
* (measured: `DYLD_LIBRARY_PATH=x /bin/sh -c 'echo $DYLD_LIBRARY_PATH'` prints
|
|
298
|
+
* nothing, while the same through Homebrew's unprotected `node` prints `x`).
|
|
299
|
+
* The value this preamble exports is likewise dropped again at the next
|
|
300
|
+
* `/bin/sh -c` hop, which is how every package script runs. Do NOT "simplify"
|
|
301
|
+
* the preservation away as dead code — it is load-bearing on Linux. And do not
|
|
302
|
+
* rely on `DYLD_LIBRARY_PATH` alone to make a darwin prebuild loadable across a
|
|
303
|
+
* script chain: the self-relative `@loader_path` rpath that
|
|
304
|
+
* `check-prebuild-loader-path.mjs` enforces is what actually has to carry it.
|
|
305
|
+
*
|
|
292
306
|
* @param scanRoot Directory whose `node_modules` is globbed at launch time.
|
|
293
307
|
* @param bakedDirs Prebuild dirs found at write time; entries outside `scanRoot`
|
|
294
308
|
* are embedded (entries inside it are dropped — the scan finds them).
|
|
@@ -297,6 +311,15 @@ function shQuote(s) {
|
|
|
297
311
|
export function buildNativeEnvPreamble(scanRoot, bakedDirs = [], target = {}) {
|
|
298
312
|
const platform = target.platform ?? process.platform;
|
|
299
313
|
const arch = target.arch ?? process.arch;
|
|
314
|
+
// The libc is a HOST fact, so it belongs in the glob just as much as the
|
|
315
|
+
// arch does. Omitting it made the launcher probe only `linux-<arch>` on a
|
|
316
|
+
// musl host — survivable while every prebuild sat in the depending
|
|
317
|
+
// package's own tree, but ADR 0017 moved each binary into a per-target
|
|
318
|
+
// package (`<name>-linux-arm64-musl`, shipping `prebuilds/linux-arm64-musl/`),
|
|
319
|
+
// so the directory the launcher must find is now the one it did not name.
|
|
320
|
+
// `detectNativePackages` has always been libc-aware; this is the same
|
|
321
|
+
// question asked in the shell, and the two must not answer it differently.
|
|
322
|
+
const libc = target.libc !== undefined ? target.libc : detectHostLibc(platform);
|
|
300
323
|
const { name: libVar, separator } = libraryPathVar(platform);
|
|
301
324
|
if (platform === 'win32') {
|
|
302
325
|
if (bakedDirs.length === 0)
|
|
@@ -317,7 +340,7 @@ export function buildNativeEnvPreamble(scanRoot, bakedDirs = [], target = {}) {
|
|
|
317
340
|
// platforms is correct here — the shell cannot read a package.json, and a
|
|
318
341
|
// package declaring ONLY a non-canonical spelling of this host has not
|
|
319
342
|
// existed since the audit made that state impossible.
|
|
320
|
-
const candidates = prebuildDirCandidates(platform, arch);
|
|
343
|
+
const candidates = prebuildDirCandidates(platform, arch, undefined, libc);
|
|
321
344
|
const patterns = [];
|
|
322
345
|
for (const candidate of candidates) {
|
|
323
346
|
// Scoped (`@gjsify/x`) and unscoped packages, matching `scanNodeModules`.
|
|
@@ -349,3 +372,75 @@ export function buildNativeEnvPreamble(scanRoot, bakedDirs = [], target = {}) {
|
|
|
349
372
|
`fi\n` +
|
|
350
373
|
`unset gjsify_np gjsify_d gjsify_t\n`);
|
|
351
374
|
}
|
|
375
|
+
/** POSIX `sh` single-quoting for a launcher's target path. */
|
|
376
|
+
export function shQuoteArg(s) {
|
|
377
|
+
return shQuote(s);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Expand a `bin` / `gjsify.bin` declaration into `binName → relative target`.
|
|
381
|
+
*
|
|
382
|
+
* The string form is npm's shorthand for `{ <package-name-without-scope>: <path> }`.
|
|
383
|
+
*/
|
|
384
|
+
export function normalizeBinMap(pkgName, bin) {
|
|
385
|
+
const out = new Map();
|
|
386
|
+
if (typeof bin === 'string') {
|
|
387
|
+
const baseName = pkgName.startsWith('@') ? pkgName.slice(pkgName.indexOf('/') + 1) : pkgName;
|
|
388
|
+
out.set(baseName, bin);
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
for (const [k, v] of Object.entries(bin))
|
|
392
|
+
out.set(k, v);
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Which bin map a package wants installed, and whether it is the GJS-runnable one.
|
|
397
|
+
*
|
|
398
|
+
* `gjsify.bin` WINS over npm `bin` when declared. The npm field can only ever
|
|
399
|
+
* name the Node entry — npm has no way to express "and this other file is the
|
|
400
|
+
* one to run when the host has `gjs` but no `node`" — so a package that ships
|
|
401
|
+
* both declares the GJS bundle here. Honouring it is what makes a Node-LESS GJS
|
|
402
|
+
* host work at all: on such a host the npm bin's `#!/usr/bin/env node` shebang
|
|
403
|
+
* fails with `env: can't execute 'node'`, and because the launcher is also the
|
|
404
|
+
* only thing that can export `GI_TYPELIB_PATH` before the GJS runtime starts,
|
|
405
|
+
* that failure takes `gjsify build` down with it (the typelib lookup happens
|
|
406
|
+
* before the bundle's first line — the CLI cannot repair it from the inside).
|
|
407
|
+
*
|
|
408
|
+
* Measured on a postmarketOS/aarch64 phone with gjs 1.88 and no node: install
|
|
409
|
+
* and prebuild loading both worked, and only the `#!/usr/bin/env node` shim in
|
|
410
|
+
* `node_modules/.bin/` stood between that host and a working `gjsify build`.
|
|
411
|
+
*
|
|
412
|
+
* `isGjsBin` lets the caller keep plain-Node packages on the plain symlink path
|
|
413
|
+
* — this must not change how `lodash`'s bin is linked.
|
|
414
|
+
*/
|
|
415
|
+
export function pickBinMap(pkgName, pkgJson) {
|
|
416
|
+
const gjsifyBin = pkgJson.gjsify?.bin;
|
|
417
|
+
if (gjsifyBin !== undefined)
|
|
418
|
+
return { map: normalizeBinMap(pkgName, gjsifyBin), isGjsBin: true };
|
|
419
|
+
const npmBin = pkgJson.bin;
|
|
420
|
+
if (npmBin !== undefined)
|
|
421
|
+
return { map: normalizeBinMap(pkgName, npmBin), isGjsBin: false };
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* The POSIX `sh` launcher gjsify writes for a bin it owns.
|
|
426
|
+
*
|
|
427
|
+
* Why a launcher and not a symlink: when a GJS bundle is reached through a
|
|
428
|
+
* symlink, the kernel resolves it to find the executable but hands the ORIGINAL
|
|
429
|
+
* path to it, so `gjs -m <symlink>` makes `import.meta.url` point at the link's
|
|
430
|
+
* directory and every asset read relative to it looks in the wrong place. An
|
|
431
|
+
* explicit `exec` with the real path avoids that.
|
|
432
|
+
*
|
|
433
|
+
* `.gjs.mjs`/`.mjs` targets are wrapped in `gjs -m` rather than exec'd directly
|
|
434
|
+
* because not every published bundle carries a `#!/usr/bin/env -S gjs -m` line,
|
|
435
|
+
* and direct-exec'ing a shebang-less `.mjs` falls through to `/bin/sh`, which
|
|
436
|
+
* then parses JavaScript as shell.
|
|
437
|
+
*/
|
|
438
|
+
export function buildShLauncher(targetAbs, opts) {
|
|
439
|
+
const preamble = opts.isGjsBundle ? (opts.envPreamble ?? '') : '';
|
|
440
|
+
const exec = opts.isGjsBundle ? `exec gjs -m ${shQuote(targetAbs)} "$@"` : `exec ${shQuote(targetAbs)} "$@"`;
|
|
441
|
+
return `#!/bin/sh\n${preamble}${exec}\n`;
|
|
442
|
+
}
|
|
443
|
+
/** True when a bin target is a GJS-runnable bundle rather than a Node script. */
|
|
444
|
+
export function isGjsBundlePath(target) {
|
|
445
|
+
return target.endsWith('.gjs.mjs') || target.endsWith('.mjs');
|
|
446
|
+
}
|
|
@@ -5,8 +5,14 @@ import { type Workspace } from '@gjsify/workspace';
|
|
|
5
5
|
* v2: output ownership moved from the whole candidate dir to per-child
|
|
6
6
|
* "output units" (`lib/esm` vs `lib/types`) — a v1 manifest recording `lib`
|
|
7
7
|
* would restore over the sibling subtree a different script owns.
|
|
8
|
+
*
|
|
9
|
+
* v3: a `.tsbuildinfo` inside a candidate dir no longer counts as a loose file
|
|
10
|
+
* (see {@link outputUnits}). The six packages that emit one into `lib/` were
|
|
11
|
+
* still on v2's whole-dir ownership and could restore over a sibling unit; a v2
|
|
12
|
+
* manifest for them records `lib`, so it must not be replayed against the
|
|
13
|
+
* per-child units this version stores.
|
|
8
14
|
*/
|
|
9
|
-
export declare const BUILD_CACHE_LAYOUT_VERSION = "
|
|
15
|
+
export declare const BUILD_CACHE_LAYOUT_VERSION = "v3";
|
|
10
16
|
/** Conventional per-package build output directories, checked in this order. */
|
|
11
17
|
export declare const OUTPUT_DIR_CANDIDATES: readonly ["lib", "dist", "dist-templates"];
|
|
12
18
|
/** Keep at most this many keys per package; evict the least recently stored/used. */
|
|
@@ -51,32 +57,6 @@ export declare function hashPackageInputs(location: string): string;
|
|
|
51
57
|
* as an integrity stamp of what was stored.
|
|
52
58
|
*/
|
|
53
59
|
export declare function hashOutputDirs(location: string, dirs: readonly string[]): string;
|
|
54
|
-
/**
|
|
55
|
-
* The cacheable output units of a package — the granularity at which a cache
|
|
56
|
-
* entry claims ownership.
|
|
57
|
-
*
|
|
58
|
-
* A candidate dir (`lib/`) is NOT automatically one unit, because the package
|
|
59
|
-
* convention splits it between two DIFFERENT scripts: `build:gjsify` writes
|
|
60
|
-
* `lib/esm/**` (bundler output) and `build:types` writes `lib/types/**` (the
|
|
61
|
-
* `gjsify tsc` declaration emit). At whole-`lib/` granularity both scripts
|
|
62
|
-
* claim the same unit, so a cache HIT on either one does its delete-then-copy
|
|
63
|
-
* over the WHOLE tree and silently destroys the other's output — measured:
|
|
64
|
-
* `build:gjsify` hitting the cache after `build:types` ran left the package
|
|
65
|
-
* with no `lib/types/` at all, and since the script never ran, nothing
|
|
66
|
-
* regenerated them. That ships a tarball whose `types` entry point does not
|
|
67
|
-
* exist (the failure `assertTypeDeclarationsShipped` exists to catch).
|
|
68
|
-
*
|
|
69
|
-
* So: when a candidate dir contains ONLY sub-directories, each child is its
|
|
70
|
-
* own unit (`lib/esm`, `lib/types`) and the two scripts stop overlapping. A
|
|
71
|
-
* candidate holding loose files degrades to the whole dir — a build writing
|
|
72
|
-
* directly into `lib/` genuinely owns it, and the coarse unit is still
|
|
73
|
-
* correct, just less precise.
|
|
74
|
-
*
|
|
75
|
-
* The safety rule is unchanged and in fact tightened: a unit the build never
|
|
76
|
-
* modifies is never recorded, so it can never be restored over (`@gjsify/tsc`'s
|
|
77
|
-
* committed, no-build `lib/lib*.d.ts` are loose files → one `lib` unit → never
|
|
78
|
-
* touched by any build → never captured).
|
|
79
|
-
*/
|
|
80
60
|
export declare function outputUnits(location: string): string[];
|
|
81
61
|
/**
|
|
82
62
|
* Cheap per-unit change signature (existence + file list + sizes + mtimes).
|
package/lib/utils/build-cache.js
CHANGED
|
@@ -49,18 +49,25 @@
|
|
|
49
49
|
// or a permission error degrade to "run the script uncached" — a cache
|
|
50
50
|
// hiccup must never break a build.
|
|
51
51
|
import { createHash } from 'node:crypto';
|
|
52
|
-
import { chmodSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readlinkSync, readSync, renameSync, rmSync,
|
|
52
|
+
import { chmodSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readlinkSync, readSync, renameSync, rmSync, copyFileSync, writeFileSync, } from 'node:fs';
|
|
53
53
|
import { dirname, join } from 'node:path';
|
|
54
54
|
import { fileURLToPath } from 'node:url';
|
|
55
55
|
import { buildDependencyGraph } from '@gjsify/workspace';
|
|
56
|
+
import { replicateLinkSync } from './dir-link.js';
|
|
56
57
|
/**
|
|
57
58
|
* Bump to invalidate every existing cache entry (schema/semantic changes).
|
|
58
59
|
*
|
|
59
60
|
* v2: output ownership moved from the whole candidate dir to per-child
|
|
60
61
|
* "output units" (`lib/esm` vs `lib/types`) — a v1 manifest recording `lib`
|
|
61
62
|
* would restore over the sibling subtree a different script owns.
|
|
63
|
+
*
|
|
64
|
+
* v3: a `.tsbuildinfo` inside a candidate dir no longer counts as a loose file
|
|
65
|
+
* (see {@link outputUnits}). The six packages that emit one into `lib/` were
|
|
66
|
+
* still on v2's whole-dir ownership and could restore over a sibling unit; a v2
|
|
67
|
+
* manifest for them records `lib`, so it must not be replayed against the
|
|
68
|
+
* per-child units this version stores.
|
|
62
69
|
*/
|
|
63
|
-
export const BUILD_CACHE_LAYOUT_VERSION = '
|
|
70
|
+
export const BUILD_CACHE_LAYOUT_VERSION = 'v3';
|
|
64
71
|
/** Conventional per-package build output directories, checked in this order. */
|
|
65
72
|
export const OUTPUT_DIR_CANDIDATES = ['lib', 'dist', 'dist-templates'];
|
|
66
73
|
/** Keep at most this many keys per package; evict the least recently stored/used. */
|
|
@@ -259,7 +266,26 @@ export function hashOutputDirs(location, dirs) {
|
|
|
259
266
|
* modifies is never recorded, so it can never be restored over (`@gjsify/tsc`'s
|
|
260
267
|
* committed, no-build `lib/lib*.d.ts` are loose files → one `lib` unit → never
|
|
261
268
|
* touched by any build → never captured).
|
|
269
|
+
*
|
|
270
|
+
* A `.tsbuildinfo` does NOT count as a loose file, and that exemption is
|
|
271
|
+
* load-bearing rather than cosmetic. It is incremental-compiler STATE, not a
|
|
272
|
+
* build output — nothing ships it — yet six packages point `tsBuildInfoFile`
|
|
273
|
+
* inside their emit dir, and its mere presence flipped `lib/` back to whole-dir
|
|
274
|
+
* ownership, re-creating for those packages exactly the clobber the per-child
|
|
275
|
+
* split above was introduced to fix. Measured on `@gjsify/semver`: `lib/`
|
|
276
|
+
* held `esm/`, `types/` and `tsconfig.build.tsbuildinfo`, so a `build:types`
|
|
277
|
+
* cache hit restored the whole `lib` and DELETED the `lib/esm` a previous step
|
|
278
|
+
* had just produced — leaving the Node CLI entry unloadable
|
|
279
|
+
* (`ERR_MODULE_NOT_FOUND: @gjsify/semver/lib/esm/index.js`) on a cold macOS
|
|
280
|
+
* tree, where that entry is the only usable one.
|
|
281
|
+
*
|
|
282
|
+
* Excluding it also keeps it OUT of every stored unit, which is the right
|
|
283
|
+
* outcome independently: a restored `.tsbuildinfo` describes an emit that did
|
|
284
|
+
* not happen in this tree, and tsc then treats the project as up to date,
|
|
285
|
+
* writes nothing and exits 0 — the silent no-op `verify-package-outputs.mjs`
|
|
286
|
+
* exists to catch.
|
|
262
287
|
*/
|
|
288
|
+
const isBuildInfo = (name) => name.endsWith('.tsbuildinfo');
|
|
263
289
|
export function outputUnits(location) {
|
|
264
290
|
const units = [];
|
|
265
291
|
for (const dir of OUTPUT_DIR_CANDIDATES) {
|
|
@@ -268,7 +294,7 @@ export function outputUnits(location) {
|
|
|
268
294
|
continue;
|
|
269
295
|
let entries;
|
|
270
296
|
try {
|
|
271
|
-
entries = readdirSync(abs).filter((name) => name !== 'node_modules');
|
|
297
|
+
entries = readdirSync(abs).filter((name) => name !== 'node_modules' && !isBuildInfo(name));
|
|
272
298
|
}
|
|
273
299
|
catch {
|
|
274
300
|
continue;
|
|
@@ -417,7 +443,11 @@ function copyDirRecursive(from, to) {
|
|
|
417
443
|
const dst = join(to, entry);
|
|
418
444
|
const st = lstatSync(src);
|
|
419
445
|
if (st.isSymbolicLink()) {
|
|
420
|
-
symlinkSync(readlinkSync(src), dst)
|
|
446
|
+
// Not `symlinkSync(readlinkSync(src), dst)`: replicating a link to a
|
|
447
|
+
// DIRECTORY verbatim needs a junction on Windows, and the target has
|
|
448
|
+
// to be resolved for it. See `dir-link.ts` — the one definition of
|
|
449
|
+
// that rule, added after the same mistake shipped in `dlx-cache.ts`.
|
|
450
|
+
replicateLinkSync(src, dst);
|
|
421
451
|
}
|
|
422
452
|
else if (st.isDirectory()) {
|
|
423
453
|
copyDirRecursive(src, dst);
|
|
@@ -12,7 +12,21 @@ export interface DepCheck {
|
|
|
12
12
|
/** For optional deps: which @gjsify/* packages need this lib */
|
|
13
13
|
requiredBy?: string[];
|
|
14
14
|
}
|
|
15
|
-
export type PackageManager = 'apt' | 'dnf' | 'pacman' | 'zypper' | 'apk' | 'unknown';
|
|
15
|
+
export type PackageManager = 'apt' | 'dnf' | 'pacman' | 'zypper' | 'apk' | 'brew' | 'unknown';
|
|
16
|
+
/**
|
|
17
|
+
* The package manager whose install command we may suggest.
|
|
18
|
+
*
|
|
19
|
+
* Probe order is PLATFORM-FIRST, not a single global list. On macOS the Linux
|
|
20
|
+
* managers are absent, so a flat list ending in `brew` would work by accident —
|
|
21
|
+
* but Homebrew also runs on Linux, where it is the LAST resort rather than the
|
|
22
|
+
* first (a Fedora host with linuxbrew installed wants `sudo dnf install`, whose
|
|
23
|
+
* packages carry the distro's own GI typelibs). Keying the order on
|
|
24
|
+
* `process.platform` states that intent instead of leaving it to list position.
|
|
25
|
+
*
|
|
26
|
+
* Returning `unknown` is a supported answer, not a failure: `buildInstallCommand`
|
|
27
|
+
* then prints no command rather than a wrong one. That is the honest result on a
|
|
28
|
+
* MacPorts or Nix host, and it is why this must never fall back to "probably brew".
|
|
29
|
+
*/
|
|
16
30
|
export declare function detectPackageManager(): PackageManager;
|
|
17
31
|
/**
|
|
18
32
|
* Run all dependency checks. Used by `gjsify check` to show full system status.
|