@gjsify/cli 0.26.1 → 0.27.1
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 +188 -183
- package/lib/bundler-pick.js +12 -4
- package/lib/cli-app.js +3 -1
- package/lib/commands/affected.js +1 -1
- 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/pack.d.ts +32 -0
- package/lib/commands/pack.js +11 -2
- 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 +27 -2
- 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 +34 -6
- package/lib/utils/dir-link.js +23 -9
- 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 +36 -17
- package/lib/utils/install-packument-cache.js +59 -31
- 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
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A manifest's platform declaration. npm allows BOTH a bare string and an
|
|
3
|
+
* array for each field (`"os": "darwin"` and `"os": ["darwin"]` are the same
|
|
4
|
+
* declaration), which is why every consumer goes through {@link checkList}.
|
|
5
|
+
*/
|
|
6
|
+
export type PlatformFieldValue = string | string[];
|
|
7
|
+
/** The `os`/`cpu`/`libc` slice of a package manifest (or packument version). */
|
|
8
|
+
export interface PlatformDeclaration {
|
|
9
|
+
os?: PlatformFieldValue;
|
|
10
|
+
cpu?: PlatformFieldValue;
|
|
11
|
+
libc?: PlatformFieldValue;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The host a package is being checked AGAINST. `os`/`cpu` use Node's
|
|
15
|
+
* `process.platform`/`process.arch` vocabulary (`linux`, `darwin`, `win32`;
|
|
16
|
+
* `x64`, `arm64`, …) — the same spelling `gjsify.platforms` uses, per the OS
|
|
17
|
+
* axis in AGENTS.md.
|
|
18
|
+
*
|
|
19
|
+
* `libc` is `'glibc'` | `'musl'` | undefined. Undefined means UNKNOWN, not
|
|
20
|
+
* "no libc": npm treats a package that DECLARES a libc while the host's is
|
|
21
|
+
* unknown as incompatible (better to skip an optional binary than to hand a
|
|
22
|
+
* musl binary to a glibc process), and so does {@link checkPlatform}.
|
|
23
|
+
*/
|
|
24
|
+
export interface PlatformTarget {
|
|
25
|
+
os: string;
|
|
26
|
+
cpu: string;
|
|
27
|
+
libc?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Result of one compatibility check. `current`/`required` carry npm's
|
|
31
|
+
* EBADPLATFORM payload shape verbatim so a log line and a thrown error read
|
|
32
|
+
* the same and can be diffed against npm's own output.
|
|
33
|
+
*/
|
|
34
|
+
export interface PlatformVerdict {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
current: PlatformTarget;
|
|
37
|
+
required: PlatformDeclaration;
|
|
38
|
+
}
|
|
39
|
+
/** Error thrown for an INCOMPATIBLE REQUIRED dependency. Mirrors npm's shape. */
|
|
40
|
+
export interface BadPlatformError extends Error {
|
|
41
|
+
code: 'EBADPLATFORM';
|
|
42
|
+
pkgid: string;
|
|
43
|
+
current: PlatformTarget;
|
|
44
|
+
required: PlatformDeclaration;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Does `value` satisfy an npm platform field list?
|
|
48
|
+
*
|
|
49
|
+
* Verbatim port of `checkList` in npm-install-checks
|
|
50
|
+
* (refs/npm-cli/node_modules/npm-install-checks/lib/index.js). Four rules,
|
|
51
|
+
* and the ORDER of the last two is what makes negation work:
|
|
52
|
+
*
|
|
53
|
+
* 1. a bare string coerces to a one-element list;
|
|
54
|
+
* 2. a list that is EXACTLY `['any']` always passes — note the length check:
|
|
55
|
+
* `['any', 'x']` does NOT short-circuit, `'any'` is then just a token
|
|
56
|
+
* that matches nothing. Reproduced deliberately;
|
|
57
|
+
* 3. an exact match on a NEGATED entry (`!win32`) fails immediately, even if
|
|
58
|
+
* a positive entry later in the list would have matched;
|
|
59
|
+
* 4. otherwise pass when at least one positive entry matched, OR when every
|
|
60
|
+
* entry was a negation (a pure blocklist means "everything else is fine").
|
|
61
|
+
*
|
|
62
|
+
* Consequence worth knowing before you "fix" it: an EMPTY list passes (rule 4,
|
|
63
|
+
* `negated === list.length === 0`). npm behaves that way and manifests in the
|
|
64
|
+
* wild do ship `"cpu": []`.
|
|
65
|
+
*
|
|
66
|
+
* `value` may be undefined (an unknown host libc). Undefined never equals a
|
|
67
|
+
* list entry, so it only ever matches through the all-negations branch — which
|
|
68
|
+
* is why {@link checkPlatform} needs its extra unknown-libc rule.
|
|
69
|
+
*/
|
|
70
|
+
export declare function checkList(value: string | undefined, list: PlatformFieldValue): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Check a manifest's `os`/`cpu`/`libc` against a host triple.
|
|
73
|
+
*
|
|
74
|
+
* Port of `checkPlatform` (same source file). Two subtleties kept as-is:
|
|
75
|
+
*
|
|
76
|
+
* - a field is consulted only when TRUTHY, so `""` reads as "not declared"
|
|
77
|
+
* (while `[]` is truthy and goes through {@link checkList}, which passes it);
|
|
78
|
+
* - a package that DECLARES `libc` while the host libc is unknown is
|
|
79
|
+
* incompatible, unconditionally. This is not derivable from `checkList`
|
|
80
|
+
* (`['!musl']` would otherwise pass on an unknown host) and it is the
|
|
81
|
+
* conservative direction: a musl binary loaded into a glibc process fails
|
|
82
|
+
* at `dlopen`, long after the install said it succeeded.
|
|
83
|
+
*
|
|
84
|
+
* Returns a verdict instead of throwing, because the CALLER decides what an
|
|
85
|
+
* incompatibility means — fatal for a required dependency, silently inert for
|
|
86
|
+
* an optional one (npm makes the same split, in arborist rather than here).
|
|
87
|
+
*/
|
|
88
|
+
export declare function checkPlatform(target: PlatformDeclaration, host: PlatformTarget): PlatformVerdict;
|
|
89
|
+
/** Does this manifest slice restrict its platform at all? */
|
|
90
|
+
export declare function declaresPlatform(target: PlatformDeclaration): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* The EBADPLATFORM error npm throws for a required dependency the host cannot
|
|
93
|
+
* run. Same `code`/`pkgid`/`current`/`required` properties, so a consumer's
|
|
94
|
+
* error handling (and a human reading the message) sees what npm reports.
|
|
95
|
+
*/
|
|
96
|
+
export declare function badPlatformError(pkgid: string, verdict: PlatformVerdict): BadPlatformError;
|
|
97
|
+
/**
|
|
98
|
+
* Everything the libc probe touches outside itself. Injected so the musl and
|
|
99
|
+
* report-less branches are unit-testable from a glibc host.
|
|
100
|
+
*/
|
|
101
|
+
export interface LibcProbeHost {
|
|
102
|
+
/** Read a file as UTF-8. `null` when it cannot be read at all. */
|
|
103
|
+
readTextFile(path: string): string | null;
|
|
104
|
+
/** List a directory. `null` when it cannot be read. */
|
|
105
|
+
listDir(path: string): string[] | null;
|
|
106
|
+
/**
|
|
107
|
+
* The two libc-relevant facts out of Node's diagnostic report, or `null` on
|
|
108
|
+
* a runtime that has none. GJS has no `process.report`, which is exactly
|
|
109
|
+
* why this is a separate seam rather than an inline `process.report` read.
|
|
110
|
+
*
|
|
111
|
+
* FLATTENED on purpose: the raw report nests the first field under
|
|
112
|
+
* `header`. A probe that had to mirror Node's report schema would be
|
|
113
|
+
* tedious to fake in a spec for no gain — the only question asked of it is
|
|
114
|
+
* these two values.
|
|
115
|
+
*/
|
|
116
|
+
diagnosticReport(): {
|
|
117
|
+
glibcVersionRuntime?: unknown;
|
|
118
|
+
sharedObjects?: unknown;
|
|
119
|
+
} | null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Last-resort probe for a runtime with no `process.report`: look for the
|
|
123
|
+
* dynamic loader itself. `ld-musl-<arch>.so.1` means musl,
|
|
124
|
+
* `ld-linux-<arch>.so.2` (or `ld-linux.so.2`) means glibc.
|
|
125
|
+
*
|
|
126
|
+
* This logic used to live inline in `oxc-resolve.ts`, where it decided which
|
|
127
|
+
* `@oxlint/binding-*` package to name in an install hint — and where a lazy
|
|
128
|
+
* `require('node:fs')` inside its try/catch threw a ReferenceError that the
|
|
129
|
+
* catch swallowed, silently pinning every host to `gnu`. It is here now so
|
|
130
|
+
* there is ONE definition with ONE set of tests behind it; `oxc-resolve.ts`
|
|
131
|
+
* calls this.
|
|
132
|
+
*
|
|
133
|
+
* glibc WINS a tie: a glibc workstation with a musl cross-toolchain installed
|
|
134
|
+
* carries both loaders and is the realistic mixed case, whereas an Alpine box
|
|
135
|
+
* carrying an `ld-linux-*` stub is not. (npm's report probe orders the two the
|
|
136
|
+
* same way.)
|
|
137
|
+
*/
|
|
138
|
+
export declare function libcFromLoaderScan(host: LibcProbeHost, dirs?: readonly string[]): string | null;
|
|
139
|
+
/**
|
|
140
|
+
* Detect the host libc family. `undefined` = unknown (and, per
|
|
141
|
+
* {@link checkPlatform}, that makes every libc-DECLARING package incompatible).
|
|
142
|
+
*
|
|
143
|
+
* Only meaningful on Linux; npm returns undefined for every other OS and so do
|
|
144
|
+
* we — a declaration like `"libc": ["glibc"]` on a darwin package would
|
|
145
|
+
* otherwise be judged against a value the platform has no concept of.
|
|
146
|
+
*
|
|
147
|
+
* Chain: ldd text → `process.report` when the runtime HAS one → loader scan.
|
|
148
|
+
* The middle step keeps NODE PARITY exactly, including its `null` answer: we
|
|
149
|
+
* deliberately do NOT fall through from a null report into the loader scan,
|
|
150
|
+
* because the same lockfile must resolve to the same tree under Node and under
|
|
151
|
+
* GJS. Diverging there would make "install once, run the bundle anywhere"
|
|
152
|
+
* depend on which runtime ran the installer.
|
|
153
|
+
*/
|
|
154
|
+
export declare function detectLibc(os: string, host: LibcProbeHost): string | undefined;
|
|
155
|
+
/** The real host probe: node:fs reads (which `@gjsify/fs` serves under GJS). */
|
|
156
|
+
export declare function defaultLibcProbeHost(): LibcProbeHost;
|
|
157
|
+
/** Test-only: forget the memoized libc so a spec can re-probe with a new host. */
|
|
158
|
+
export declare function resetLibcCache(): void;
|
|
159
|
+
/** Memoizing wrapper over {@link detectLibc}. */
|
|
160
|
+
export declare function hostLibc(os: string, host?: LibcProbeHost): string | undefined;
|
|
161
|
+
/**
|
|
162
|
+
* Explicit target overrides. npm exposes these as the config keys `os`, `cpu`
|
|
163
|
+
* and `libc`, which means an env `npm_config_<key>` sets them just as a
|
|
164
|
+
* `--<key>` flag does; `gjsify install` mirrors both spellings (its
|
|
165
|
+
* `--os/--cpu/--libc` flags write these keys, and `loadNpmrc` already honours
|
|
166
|
+
* `npm_config_registry` the same way).
|
|
167
|
+
*
|
|
168
|
+
* Pure: the env block is a parameter. Empty/whitespace values are ignored
|
|
169
|
+
* rather than treated as "the empty platform", so `npm_config_libc=` behaves
|
|
170
|
+
* like an absent key.
|
|
171
|
+
*/
|
|
172
|
+
export declare function readPlatformOverrides(env: Record<string, string | undefined>): Partial<PlatformTarget>;
|
|
173
|
+
/**
|
|
174
|
+
* Is the platform check bypassed? npm's `--force` (config key `force`)
|
|
175
|
+
* suppresses the EBADPLATFORM THROW for a required dependency — it does NOT
|
|
176
|
+
* un-skip an incompatible OPTIONAL one, which npm marks inert regardless
|
|
177
|
+
* ("We ignore the --force and --engine-strict flags" — arborist
|
|
178
|
+
* build-ideal-tree.js, the `node.optional && !node.inert` branch). Callers
|
|
179
|
+
* must keep that asymmetry: forcing a required install is a user overriding a
|
|
180
|
+
* declaration about their own machine; forcing an optional one would only
|
|
181
|
+
* download a binary nothing can load.
|
|
182
|
+
*/
|
|
183
|
+
export declare function readPlatformForce(env: Record<string, string | undefined>): boolean;
|
|
184
|
+
/** Sources {@link resolveHostPlatform} may read. All optional, all injectable. */
|
|
185
|
+
export interface HostPlatformSources {
|
|
186
|
+
/** Defaults to `process.platform`. */
|
|
187
|
+
platform?: string;
|
|
188
|
+
/** Defaults to `process.arch`. */
|
|
189
|
+
arch?: string;
|
|
190
|
+
/** Defaults to `process.env`; read for the npm config-key overrides. */
|
|
191
|
+
env?: Record<string, string | undefined>;
|
|
192
|
+
/** Defaults to {@link defaultLibcProbeHost}. */
|
|
193
|
+
probe?: LibcProbeHost;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The triple an install resolves against: the running host, with the npm
|
|
197
|
+
* config keys applied on top.
|
|
198
|
+
*
|
|
199
|
+
* The libc probe runs only when the target OS is linux AND libc was not
|
|
200
|
+
* overridden — so `--os=darwin` on a Linux host does no libc I/O and yields
|
|
201
|
+
* `libc: undefined`, exactly as npm's `currentEnv.libc(os)` does. That is what
|
|
202
|
+
* makes a single host able to test every branch (requirement behind the
|
|
203
|
+
* `--os/--cpu/--libc` flags), and what keeps the check honest when a user
|
|
204
|
+
* resolves a tree for a machine they are not on.
|
|
205
|
+
*
|
|
206
|
+
* Under GJS `process.platform`/`process.arch` come from `@gjsify/process`,
|
|
207
|
+
* which derives them from `uname` through
|
|
208
|
+
* `packages/gjs/utils/src/platform-names.ts`. That table is written against
|
|
209
|
+
* NODE's vocabulary (`linux`/`darwin`/`win32`, `x64`/`arm64`), which is the
|
|
210
|
+
* same vocabulary npm's `os`/`cpu` fields use — so the values are directly
|
|
211
|
+
* comparable and need no translation layer here.
|
|
212
|
+
*/
|
|
213
|
+
export declare function resolveHostPlatform(sources?: HostPlatformSources): PlatformTarget;
|
|
214
|
+
/** Human-readable target, for a one-line install log. */
|
|
215
|
+
export declare function describePlatformTarget(target: PlatformTarget): string;
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Adapted from npm-install-checks 9.0.0
|
|
3
|
+
// (refs/npm-cli/node_modules/npm-install-checks/lib/{index,current-env}.js).
|
|
4
|
+
// Copyright (c) Isaac Z. Schlueter and npm contributors. ISC.
|
|
5
|
+
// Modifications: TypeScript; host values are PARAMETERS instead of ambient
|
|
6
|
+
// reads; the libc probe gains a GJS path (no `process.report` there).
|
|
7
|
+
//
|
|
8
|
+
// `os` / `cpu` / `libc` compatibility for `gjsify install`.
|
|
9
|
+
//
|
|
10
|
+
// WHY THIS EXISTS: the resolver used to place every optionalDependency a
|
|
11
|
+
// packument listed, without ever reading the version's `os`/`cpu`/`libc`
|
|
12
|
+
// fields. Measured on a cold tree of this workspace: 183 foreign-platform
|
|
13
|
+
// packages / 3361 MB (darwin/win32/arm64 binary siblings of rolldown, oxlint,
|
|
14
|
+
// lightningcss, esbuild, …) plus 9 musl-only packages / 308 MB — 3.67 GB of a
|
|
15
|
+
// 5.6 GB node_modules that npm would never have written. That is not a size
|
|
16
|
+
// nit: those trees also make `detectNativePackages` walk foreign prebuilds,
|
|
17
|
+
// and an extract of several GB is what pushed installs into the 30-minute
|
|
18
|
+
// wall-clock budget (see the comment in commands/install.ts).
|
|
19
|
+
//
|
|
20
|
+
// PARITY IS THE POINT, so this is a PORT, not an interpretation: the rules
|
|
21
|
+
// below are npm's, quirks included (see `checkList`). A gjsify install that
|
|
22
|
+
// placed a different SET than npm for the same manifest would be a worse
|
|
23
|
+
// failure than placing too much — a consumer's build then works under one
|
|
24
|
+
// installer and not the other, with nothing in either tree to point at.
|
|
25
|
+
//
|
|
26
|
+
// PURITY: every decision function takes the host triple as an argument. Only
|
|
27
|
+
// `resolveHostPlatform()` (and the default probe it builds) reads
|
|
28
|
+
// `process.platform`/`process.arch`/the filesystem, so a Linux host can
|
|
29
|
+
// unit-test the darwin/win32/musl branches by injection — the philosophy
|
|
30
|
+
// `detect-native-packages.ts` documents at its top, applied here.
|
|
31
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
32
|
+
/**
|
|
33
|
+
* Does `value` satisfy an npm platform field list?
|
|
34
|
+
*
|
|
35
|
+
* Verbatim port of `checkList` in npm-install-checks
|
|
36
|
+
* (refs/npm-cli/node_modules/npm-install-checks/lib/index.js). Four rules,
|
|
37
|
+
* and the ORDER of the last two is what makes negation work:
|
|
38
|
+
*
|
|
39
|
+
* 1. a bare string coerces to a one-element list;
|
|
40
|
+
* 2. a list that is EXACTLY `['any']` always passes — note the length check:
|
|
41
|
+
* `['any', 'x']` does NOT short-circuit, `'any'` is then just a token
|
|
42
|
+
* that matches nothing. Reproduced deliberately;
|
|
43
|
+
* 3. an exact match on a NEGATED entry (`!win32`) fails immediately, even if
|
|
44
|
+
* a positive entry later in the list would have matched;
|
|
45
|
+
* 4. otherwise pass when at least one positive entry matched, OR when every
|
|
46
|
+
* entry was a negation (a pure blocklist means "everything else is fine").
|
|
47
|
+
*
|
|
48
|
+
* Consequence worth knowing before you "fix" it: an EMPTY list passes (rule 4,
|
|
49
|
+
* `negated === list.length === 0`). npm behaves that way and manifests in the
|
|
50
|
+
* wild do ship `"cpu": []`.
|
|
51
|
+
*
|
|
52
|
+
* `value` may be undefined (an unknown host libc). Undefined never equals a
|
|
53
|
+
* list entry, so it only ever matches through the all-negations branch — which
|
|
54
|
+
* is why {@link checkPlatform} needs its extra unknown-libc rule.
|
|
55
|
+
*/
|
|
56
|
+
export function checkList(value, list) {
|
|
57
|
+
const entries = typeof list === 'string' ? [list] : list;
|
|
58
|
+
if (entries.length === 1 && entries[0] === 'any')
|
|
59
|
+
return true;
|
|
60
|
+
let negated = 0;
|
|
61
|
+
let match = false;
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
const negate = entry.charAt(0) === '!';
|
|
64
|
+
const test = negate ? entry.slice(1) : entry;
|
|
65
|
+
if (negate) {
|
|
66
|
+
negated++;
|
|
67
|
+
if (value === test)
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
match = match || value === test;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return match || negated === entries.length;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Check a manifest's `os`/`cpu`/`libc` against a host triple.
|
|
78
|
+
*
|
|
79
|
+
* Port of `checkPlatform` (same source file). Two subtleties kept as-is:
|
|
80
|
+
*
|
|
81
|
+
* - a field is consulted only when TRUTHY, so `""` reads as "not declared"
|
|
82
|
+
* (while `[]` is truthy and goes through {@link checkList}, which passes it);
|
|
83
|
+
* - a package that DECLARES `libc` while the host libc is unknown is
|
|
84
|
+
* incompatible, unconditionally. This is not derivable from `checkList`
|
|
85
|
+
* (`['!musl']` would otherwise pass on an unknown host) and it is the
|
|
86
|
+
* conservative direction: a musl binary loaded into a glibc process fails
|
|
87
|
+
* at `dlopen`, long after the install said it succeeded.
|
|
88
|
+
*
|
|
89
|
+
* Returns a verdict instead of throwing, because the CALLER decides what an
|
|
90
|
+
* incompatibility means — fatal for a required dependency, silently inert for
|
|
91
|
+
* an optional one (npm makes the same split, in arborist rather than here).
|
|
92
|
+
*/
|
|
93
|
+
export function checkPlatform(target, host) {
|
|
94
|
+
const osOk = target.os ? checkList(host.os, target.os) : true;
|
|
95
|
+
const cpuOk = target.cpu ? checkList(host.cpu, target.cpu) : true;
|
|
96
|
+
let libcOk = target.libc ? checkList(host.libc, target.libc) : true;
|
|
97
|
+
if (target.libc && !host.libc)
|
|
98
|
+
libcOk = false;
|
|
99
|
+
return {
|
|
100
|
+
ok: osOk && cpuOk && libcOk,
|
|
101
|
+
current: { os: host.os, cpu: host.cpu, libc: host.libc },
|
|
102
|
+
required: { os: target.os, cpu: target.cpu, libc: target.libc },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Does this manifest slice restrict its platform at all? */
|
|
106
|
+
export function declaresPlatform(target) {
|
|
107
|
+
return Boolean(target.os || target.cpu || target.libc);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The EBADPLATFORM error npm throws for a required dependency the host cannot
|
|
111
|
+
* run. Same `code`/`pkgid`/`current`/`required` properties, so a consumer's
|
|
112
|
+
* error handling (and a human reading the message) sees what npm reports.
|
|
113
|
+
*/
|
|
114
|
+
export function badPlatformError(pkgid, verdict) {
|
|
115
|
+
const err = new Error(`Unsupported platform for ${pkgid}: wanted ${JSON.stringify(verdict.required)} ` +
|
|
116
|
+
`(current: ${JSON.stringify(verdict.current)})`);
|
|
117
|
+
err.code = 'EBADPLATFORM';
|
|
118
|
+
err.pkgid = pkgid;
|
|
119
|
+
err.current = verdict.current;
|
|
120
|
+
err.required = verdict.required;
|
|
121
|
+
return err;
|
|
122
|
+
}
|
|
123
|
+
/** npm's `ldd` location — a data file to us, never executed. */
|
|
124
|
+
const LDD_PATH = '/usr/bin/ldd';
|
|
125
|
+
/**
|
|
126
|
+
* Directories probed for a dynamic loader when there is no `process.report`.
|
|
127
|
+
* `/lib` and `/lib64` are the classic locations; `/usr/lib` covers the
|
|
128
|
+
* merged-/usr layouts (Fedora, Arch, Debian ≥ bookworm) where `/lib` is a
|
|
129
|
+
* symlink and, on some containers, absent from the image entirely.
|
|
130
|
+
*/
|
|
131
|
+
const LOADER_DIRS = ['/lib', '/lib64', '/usr/lib'];
|
|
132
|
+
function isMuslSharedObject(file) {
|
|
133
|
+
// npm's own predicate, verbatim: both spellings appear across distros.
|
|
134
|
+
return file.includes('libc.musl-') || file.includes('ld-musl-');
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Read the libc family out of `/usr/bin/ldd`, which on every mainstream distro
|
|
138
|
+
* is a TEXT script naming its own implementation. This is npm's primary probe
|
|
139
|
+
* and it is pure file I/O, so it works unchanged under GJS.
|
|
140
|
+
*
|
|
141
|
+
* Three-valued on purpose, and the distinction drives the fallback chain:
|
|
142
|
+
* - `'musl'` / `'glibc'` — decided;
|
|
143
|
+
* - `null` — the file was READ but names neither implementation. npm treats
|
|
144
|
+
* that as final (do not consult the report), so we do too;
|
|
145
|
+
* - `undefined` — unreadable (no ldd at all: a scratch/distroless image),
|
|
146
|
+
* the only case that falls through.
|
|
147
|
+
*/
|
|
148
|
+
function libcFromLddFile(host) {
|
|
149
|
+
const content = host.readTextFile(LDD_PATH);
|
|
150
|
+
if (content === null)
|
|
151
|
+
return undefined;
|
|
152
|
+
if (content.includes('musl'))
|
|
153
|
+
return 'musl';
|
|
154
|
+
if (content.includes('GNU C Library'))
|
|
155
|
+
return 'glibc';
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* npm's `process.report`-based probe (Node only). Takes the already-collected
|
|
160
|
+
* report rather than the probe host: `getReport()` walks the whole process
|
|
161
|
+
* state, so `detectLibc` collects it once and asks both questions of the same
|
|
162
|
+
* snapshot.
|
|
163
|
+
*/
|
|
164
|
+
function libcFromReportData(report) {
|
|
165
|
+
if (report.glibcVersionRuntime)
|
|
166
|
+
return 'glibc';
|
|
167
|
+
if (Array.isArray(report.sharedObjects) && report.sharedObjects.some((so) => isMuslSharedObject(String(so)))) {
|
|
168
|
+
return 'musl';
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Last-resort probe for a runtime with no `process.report`: look for the
|
|
174
|
+
* dynamic loader itself. `ld-musl-<arch>.so.1` means musl,
|
|
175
|
+
* `ld-linux-<arch>.so.2` (or `ld-linux.so.2`) means glibc.
|
|
176
|
+
*
|
|
177
|
+
* This logic used to live inline in `oxc-resolve.ts`, where it decided which
|
|
178
|
+
* `@oxlint/binding-*` package to name in an install hint — and where a lazy
|
|
179
|
+
* `require('node:fs')` inside its try/catch threw a ReferenceError that the
|
|
180
|
+
* catch swallowed, silently pinning every host to `gnu`. It is here now so
|
|
181
|
+
* there is ONE definition with ONE set of tests behind it; `oxc-resolve.ts`
|
|
182
|
+
* calls this.
|
|
183
|
+
*
|
|
184
|
+
* glibc WINS a tie: a glibc workstation with a musl cross-toolchain installed
|
|
185
|
+
* carries both loaders and is the realistic mixed case, whereas an Alpine box
|
|
186
|
+
* carrying an `ld-linux-*` stub is not. (npm's report probe orders the two the
|
|
187
|
+
* same way.)
|
|
188
|
+
*/
|
|
189
|
+
export function libcFromLoaderScan(host, dirs = LOADER_DIRS) {
|
|
190
|
+
let sawMusl = false;
|
|
191
|
+
for (const dir of dirs) {
|
|
192
|
+
const entries = host.listDir(dir);
|
|
193
|
+
if (!entries)
|
|
194
|
+
continue;
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
if (entry.startsWith('ld-linux'))
|
|
197
|
+
return 'glibc';
|
|
198
|
+
if (entry.startsWith('ld-musl-'))
|
|
199
|
+
sawMusl = true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return sawMusl ? 'musl' : null;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Detect the host libc family. `undefined` = unknown (and, per
|
|
206
|
+
* {@link checkPlatform}, that makes every libc-DECLARING package incompatible).
|
|
207
|
+
*
|
|
208
|
+
* Only meaningful on Linux; npm returns undefined for every other OS and so do
|
|
209
|
+
* we — a declaration like `"libc": ["glibc"]` on a darwin package would
|
|
210
|
+
* otherwise be judged against a value the platform has no concept of.
|
|
211
|
+
*
|
|
212
|
+
* Chain: ldd text → `process.report` when the runtime HAS one → loader scan.
|
|
213
|
+
* The middle step keeps NODE PARITY exactly, including its `null` answer: we
|
|
214
|
+
* deliberately do NOT fall through from a null report into the loader scan,
|
|
215
|
+
* because the same lockfile must resolve to the same tree under Node and under
|
|
216
|
+
* GJS. Diverging there would make "install once, run the bundle anywhere"
|
|
217
|
+
* depend on which runtime ran the installer.
|
|
218
|
+
*/
|
|
219
|
+
export function detectLibc(os, host) {
|
|
220
|
+
if (os !== 'linux')
|
|
221
|
+
return undefined;
|
|
222
|
+
const fromFile = libcFromLddFile(host);
|
|
223
|
+
if (fromFile !== undefined)
|
|
224
|
+
return fromFile ?? undefined;
|
|
225
|
+
const report = host.diagnosticReport();
|
|
226
|
+
if (report !== null)
|
|
227
|
+
return libcFromReportData(report) ?? undefined;
|
|
228
|
+
return libcFromLoaderScan(host) ?? undefined;
|
|
229
|
+
}
|
|
230
|
+
/** The real host probe: node:fs reads (which `@gjsify/fs` serves under GJS). */
|
|
231
|
+
export function defaultLibcProbeHost() {
|
|
232
|
+
return {
|
|
233
|
+
readTextFile(path) {
|
|
234
|
+
try {
|
|
235
|
+
return readFileSync(path, 'utf-8');
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Genuinely expected: distroless/scratch images ship no ldd,
|
|
239
|
+
// and `/lib64` is absent on plenty of layouts. The caller
|
|
240
|
+
// treats null as "probe the next source", so swallowing here
|
|
241
|
+
// is the control flow, not a hidden failure.
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
listDir(path) {
|
|
246
|
+
try {
|
|
247
|
+
return readdirSync(path);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
diagnosticReport() {
|
|
254
|
+
// `process.report` is a Node diagnostic API that GJS's `process`
|
|
255
|
+
// polyfill does not implement, so the existence check below is a
|
|
256
|
+
// genuine per-runtime variance probe, not paranoia. The cast is
|
|
257
|
+
// needed for a second reason: `excludeNetwork` is real (npm sets it)
|
|
258
|
+
// but missing from @types/node's `ProcessReport`, so the typed
|
|
259
|
+
// `process.report` cannot express the assignment.
|
|
260
|
+
const report = process.report;
|
|
261
|
+
if (!report || typeof report.getReport !== 'function')
|
|
262
|
+
return null;
|
|
263
|
+
// npm flips `excludeNetwork` first: gathering the network section
|
|
264
|
+
// does DNS work that can take seconds on a host with an
|
|
265
|
+
// unreachable resolver. Restore it so we don't mutate global
|
|
266
|
+
// diagnostic state for the rest of the process.
|
|
267
|
+
const previous = report.excludeNetwork;
|
|
268
|
+
report.excludeNetwork = true;
|
|
269
|
+
try {
|
|
270
|
+
const raw = report.getReport();
|
|
271
|
+
return { glibcVersionRuntime: raw.header?.glibcVersionRuntime, sharedObjects: raw.sharedObjects };
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
report.excludeNetwork = previous;
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Memoized libc answer. npm caches the same way (a module-level `family`):
|
|
281
|
+
* the probe reads files, and a resolve asks for the host triple once per
|
|
282
|
+
* package. `libcProbed` is a separate flag because `undefined` is a legitimate
|
|
283
|
+
* ANSWER (unknown libc), not a "not computed yet" marker.
|
|
284
|
+
*/
|
|
285
|
+
let cachedLibc;
|
|
286
|
+
let libcProbed = false;
|
|
287
|
+
/** Test-only: forget the memoized libc so a spec can re-probe with a new host. */
|
|
288
|
+
export function resetLibcCache() {
|
|
289
|
+
cachedLibc = undefined;
|
|
290
|
+
libcProbed = false;
|
|
291
|
+
}
|
|
292
|
+
/** Memoizing wrapper over {@link detectLibc}. */
|
|
293
|
+
export function hostLibc(os, host = defaultLibcProbeHost()) {
|
|
294
|
+
if (!libcProbed) {
|
|
295
|
+
cachedLibc = detectLibc(os, host);
|
|
296
|
+
libcProbed = true;
|
|
297
|
+
}
|
|
298
|
+
return cachedLibc;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Explicit target overrides. npm exposes these as the config keys `os`, `cpu`
|
|
302
|
+
* and `libc`, which means an env `npm_config_<key>` sets them just as a
|
|
303
|
+
* `--<key>` flag does; `gjsify install` mirrors both spellings (its
|
|
304
|
+
* `--os/--cpu/--libc` flags write these keys, and `loadNpmrc` already honours
|
|
305
|
+
* `npm_config_registry` the same way).
|
|
306
|
+
*
|
|
307
|
+
* Pure: the env block is a parameter. Empty/whitespace values are ignored
|
|
308
|
+
* rather than treated as "the empty platform", so `npm_config_libc=` behaves
|
|
309
|
+
* like an absent key.
|
|
310
|
+
*/
|
|
311
|
+
export function readPlatformOverrides(env) {
|
|
312
|
+
const pick = (key) => {
|
|
313
|
+
const raw = env[key];
|
|
314
|
+
if (typeof raw !== 'string')
|
|
315
|
+
return undefined;
|
|
316
|
+
const trimmed = raw.trim();
|
|
317
|
+
return trimmed === '' ? undefined : trimmed;
|
|
318
|
+
};
|
|
319
|
+
const out = {};
|
|
320
|
+
const os = pick('npm_config_os');
|
|
321
|
+
const cpu = pick('npm_config_cpu');
|
|
322
|
+
const libc = pick('npm_config_libc');
|
|
323
|
+
if (os !== undefined)
|
|
324
|
+
out.os = os;
|
|
325
|
+
if (cpu !== undefined)
|
|
326
|
+
out.cpu = cpu;
|
|
327
|
+
if (libc !== undefined)
|
|
328
|
+
out.libc = libc;
|
|
329
|
+
return out;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Is the platform check bypassed? npm's `--force` (config key `force`)
|
|
333
|
+
* suppresses the EBADPLATFORM THROW for a required dependency — it does NOT
|
|
334
|
+
* un-skip an incompatible OPTIONAL one, which npm marks inert regardless
|
|
335
|
+
* ("We ignore the --force and --engine-strict flags" — arborist
|
|
336
|
+
* build-ideal-tree.js, the `node.optional && !node.inert` branch). Callers
|
|
337
|
+
* must keep that asymmetry: forcing a required install is a user overriding a
|
|
338
|
+
* declaration about their own machine; forcing an optional one would only
|
|
339
|
+
* download a binary nothing can load.
|
|
340
|
+
*/
|
|
341
|
+
export function readPlatformForce(env) {
|
|
342
|
+
const raw = env.npm_config_force;
|
|
343
|
+
if (typeof raw !== 'string')
|
|
344
|
+
return false;
|
|
345
|
+
const v = raw.trim().toLowerCase();
|
|
346
|
+
return v === 'true' || v === '1';
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* The triple an install resolves against: the running host, with the npm
|
|
350
|
+
* config keys applied on top.
|
|
351
|
+
*
|
|
352
|
+
* The libc probe runs only when the target OS is linux AND libc was not
|
|
353
|
+
* overridden — so `--os=darwin` on a Linux host does no libc I/O and yields
|
|
354
|
+
* `libc: undefined`, exactly as npm's `currentEnv.libc(os)` does. That is what
|
|
355
|
+
* makes a single host able to test every branch (requirement behind the
|
|
356
|
+
* `--os/--cpu/--libc` flags), and what keeps the check honest when a user
|
|
357
|
+
* resolves a tree for a machine they are not on.
|
|
358
|
+
*
|
|
359
|
+
* Under GJS `process.platform`/`process.arch` come from `@gjsify/process`,
|
|
360
|
+
* which derives them from `uname` through
|
|
361
|
+
* `packages/gjs/utils/src/platform-names.ts`. That table is written against
|
|
362
|
+
* NODE's vocabulary (`linux`/`darwin`/`win32`, `x64`/`arm64`), which is the
|
|
363
|
+
* same vocabulary npm's `os`/`cpu` fields use — so the values are directly
|
|
364
|
+
* comparable and need no translation layer here.
|
|
365
|
+
*/
|
|
366
|
+
export function resolveHostPlatform(sources = {}) {
|
|
367
|
+
const env = sources.env ?? process.env;
|
|
368
|
+
const overrides = readPlatformOverrides(env);
|
|
369
|
+
const os = overrides.os ?? sources.platform ?? process.platform;
|
|
370
|
+
const cpu = overrides.cpu ?? sources.arch ?? process.arch;
|
|
371
|
+
const libc = overrides.libc ?? (os === 'linux' ? hostLibc(os, sources.probe ?? defaultLibcProbeHost()) : undefined);
|
|
372
|
+
return { os, cpu, libc };
|
|
373
|
+
}
|
|
374
|
+
/** Human-readable target, for a one-line install log. */
|
|
375
|
+
export function describePlatformTarget(target) {
|
|
376
|
+
return `${target.os}-${target.cpu}${target.libc ? `-${target.libc}` : ''}`;
|
|
377
|
+
}
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
// (parent-dir walker precedent).
|
|
37
37
|
import { createRequire } from 'node:module';
|
|
38
38
|
import { pathToFileURL } from 'node:url';
|
|
39
|
-
import { join, resolve } from 'node:path';
|
|
39
|
+
import { delimiter, join, resolve } from 'node:path';
|
|
40
40
|
import { findWorkspaceRoot } from './workspace-root.js';
|
|
41
41
|
/**
|
|
42
42
|
* Resolve a bare npm specifier through multiple createRequire anchors.
|
|
@@ -54,7 +54,14 @@ export function resolveNpmPackage(specifier, opts = {}) {
|
|
|
54
54
|
// in the lookup. Mirrors Node's NODE_PATH semantics.
|
|
55
55
|
const envPath = process.env['GJSIFY_NODE_PATH'];
|
|
56
56
|
if (envPath) {
|
|
57
|
-
|
|
57
|
+
// `path.delimiter`, not `':'` — Node's own NODE_PATH uses `;` on
|
|
58
|
+
// Windows, and this override claims to mirror NODE_PATH semantics. Split
|
|
59
|
+
// on a colon there and even a SINGLE entry is destroyed: `C:\tools`
|
|
60
|
+
// becomes `['C', '\tools']`, neither of which resolves, so the documented
|
|
61
|
+
// override silently did nothing on Windows and fell through to anchor 2.
|
|
62
|
+
// `commands/run.ts` and `utils/run-lifecycle-script.ts` already get this
|
|
63
|
+
// right; this was the site that was missed.
|
|
64
|
+
for (const dir of envPath.split(delimiter).filter(Boolean)) {
|
|
58
65
|
const hit = tryResolveFromDir(specifier, resolve(dir));
|
|
59
66
|
if (hit)
|
|
60
67
|
return hit;
|
package/lib/utils/run-gjs.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { type NativeEnv } from './detect-native-packages.js';
|
|
|
6
6
|
* `$ …` echo. Which library variable is set is host-dependent
|
|
7
7
|
* (`LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` / `PATH`) — see `buildNativeEnv`.
|
|
8
8
|
*/
|
|
9
|
-
export declare function computeNativeEnvForBundle(bundlePath: string, cwd?: string): {
|
|
9
|
+
export declare function computeNativeEnvForBundle(bundlePath: string, cwd?: string, inherited?: Record<string, string | undefined>): {
|
|
10
10
|
env: NativeEnv;
|
|
11
11
|
envPrefix: string;
|
|
12
12
|
};
|
package/lib/utils/run-gjs.js
CHANGED
|
@@ -26,15 +26,31 @@ import { detectNativePackages, buildNativeEnv } from './detect-native-packages.j
|
|
|
26
26
|
* `$ …` echo. Which library variable is set is host-dependent
|
|
27
27
|
* (`LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` / `PATH`) — see `buildNativeEnv`.
|
|
28
28
|
*/
|
|
29
|
-
export function computeNativeEnvForBundle(bundlePath, cwd = process.cwd()) {
|
|
29
|
+
export function computeNativeEnvForBundle(bundlePath, cwd = process.cwd(), inherited = process.env) {
|
|
30
30
|
const resolvedBundle = resolve(bundlePath);
|
|
31
31
|
const cwdPackages = detectNativePackages(cwd);
|
|
32
32
|
const bundlePackages = detectNativePackages(dirname(resolvedBundle));
|
|
33
33
|
const seen = new Set(cwdPackages.map((p) => p.name));
|
|
34
34
|
const nativePackages = [...cwdPackages, ...bundlePackages.filter((p) => !seen.has(p.name))];
|
|
35
|
-
|
|
35
|
+
// `inherited` feeds BOTH halves — the composition and the comparison. Split
|
|
36
|
+
// them and the win32 branch disagrees with itself: `buildNativeEnv` writes
|
|
37
|
+
// the library variable back under the host's own spelling (`Path` in a
|
|
38
|
+
// stock env block), so a comparison keyed on the canonical `PATH` would
|
|
39
|
+
// look up a key that is not there and call every run a change.
|
|
40
|
+
const env = buildNativeEnv(nativePackages, { env: inherited });
|
|
41
|
+
// Show only what this CLI actually CHANGED. `buildNativeEnv` prepends the
|
|
42
|
+
// detected directories to the inherited value and returns the result
|
|
43
|
+
// unconditionally, so with no native packages it hands back the host's own
|
|
44
|
+
// variable verbatim — and the echo then claimed the CLI had set it.
|
|
45
|
+
//
|
|
46
|
+
// Harmless on Linux, where `LD_LIBRARY_PATH` is usually unset and the empty
|
|
47
|
+
// filter caught it. On Windows the library variable IS `PATH`, which is
|
|
48
|
+
// never empty, so every run printed a ~2 kB dump of the host PATH in front
|
|
49
|
+
// of a command the user is invited to copy — burying the command, and
|
|
50
|
+
// claiming a change that never happened. Comparing against the inherited
|
|
51
|
+
// value is what makes the echo honest on both.
|
|
36
52
|
const envPrefix = Object.entries(env)
|
|
37
|
-
.filter(([, value]) => value !== undefined && value !== '')
|
|
53
|
+
.filter(([key, value]) => value !== undefined && value !== '' && value !== inherited[key])
|
|
38
54
|
.map(([key, value]) => `${key}=${value}`)
|
|
39
55
|
.join(' ');
|
|
40
56
|
return { env, envPrefix };
|