@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
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
// Utility to find npm packages with gjsify native prebuilds.
|
|
2
2
|
// Packages declare: "gjsify": { "prebuilds": "<dir>" } in their package.json,
|
|
3
|
-
// and (since the OS-axis audit) the `<os>-<arch
|
|
4
|
-
// "gjsify": { "platforms": [...] }.
|
|
3
|
+
// and (since the OS-axis audit) the `<os>-<arch>[-musl]` targets they promise in
|
|
4
|
+
// "gjsify": { "platforms": [...] }. The libc half of that token is the newest
|
|
5
|
+
// axis; which libc a package's artifacts actually NEED is a separate, measured
|
|
6
|
+
// claim (npm's `libc` field + `gjsify.glibcRequires`, held to the binaries by the
|
|
7
|
+
// `prebuild-libc` conformance rule) — this file only resolves DIRECTORIES.
|
|
5
8
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
9
|
+
// TWO passes, and the second one exists because the first cannot see an isolated
|
|
10
|
+
// (pnpm-style) layout: `detectNativePackages(startDir)` walks up from `startDir`
|
|
11
|
+
// and exhaustively scans every `node_modules` it finds, then resolves any package
|
|
12
|
+
// that DECLARED a prebuild without shipping one through its per-platform
|
|
13
|
+
// companion package (`<name>-<token>`), searching from THAT package's own
|
|
14
|
+
// directory. See `resolvePlatformSibling`. Used by:
|
|
8
15
|
// * `gjsify run`, `gjsify info`, `gjsify install` — startDir = process.cwd()
|
|
9
16
|
// * `runGjsBundle()` — startDir = dirname(bundlePath), so DLX-cache layouts
|
|
10
17
|
// (`~/.cache/gjsify/dlx/<sha>/.../node_modules/<pkg>/dist/bundle.js`) get
|
|
@@ -13,11 +20,12 @@
|
|
|
13
20
|
// for packages whose Vala typelibs live in *indirect* deps.
|
|
14
21
|
//
|
|
15
22
|
// PLATFORM RESOLUTION IS A PURE FUNCTION. Everything that depends on the host
|
|
16
|
-
// OS/CPU lives in `resolvePrebuildDirName()` / `buildNativeEnv()
|
|
17
|
-
// which take `platform` / `arch`
|
|
18
|
-
// `process.platform` inline. That is what makes
|
|
19
|
-
// unit-testable from a Linux host
|
|
20
|
-
// the host values are only read at the
|
|
23
|
+
// OS/CPU/libc lives in `resolvePrebuildDirName()` / `buildNativeEnv()` /
|
|
24
|
+
// `resolveHostLibc()`, all of which take `platform` / `arch` / `libc` as
|
|
25
|
+
// parameters instead of reading `process.platform` inline. That is what makes
|
|
26
|
+
// the darwin, win32 AND musl branches unit-testable from a glibc Linux host
|
|
27
|
+
// (see `detect-native-packages.spec.ts`) — the host values are only read at the
|
|
28
|
+
// outermost call site, as a default.
|
|
21
29
|
import { readdirSync, existsSync } from 'node:fs';
|
|
22
30
|
import { join, resolve } from 'node:path';
|
|
23
31
|
import { readPackageJson } from './pkg-json.js';
|
|
@@ -39,10 +47,11 @@ const NODE_ARCH_TO_LEGACY_UNAME = {
|
|
|
39
47
|
};
|
|
40
48
|
/**
|
|
41
49
|
* Arch spellings that name the same target, folded onto the NODE spelling.
|
|
42
|
-
* Mirrors `ARCH_ALIASES` in `
|
|
43
|
-
*
|
|
44
|
-
* platform-support matrix
|
|
45
|
-
*
|
|
50
|
+
* Mirrors `ARCH_ALIASES` in `packages/infra/manifest-conformance/lib/platforms.mjs`
|
|
51
|
+
* — the table `scripts/audit-runtimes.mjs` canonicalises
|
|
52
|
+
* `package.json#gjsify.platforms` through for the platform-support matrix and the
|
|
53
|
+
* prebuild rules. The two MUST agree or a package could pass the audit while the
|
|
54
|
+
* CLI fails to find its artifact.
|
|
46
55
|
*
|
|
47
56
|
* The direction matters: `${process.platform}-${process.arch}` is what a
|
|
48
57
|
* running process can compute about itself, so folding *onto* it means the
|
|
@@ -54,18 +63,104 @@ const ARCH_ALIASES = {
|
|
|
54
63
|
aarch64: 'arm64',
|
|
55
64
|
};
|
|
56
65
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
|
|
66
|
+
* The libc suffix a prebuild target may carry. Only ONE value is spelled out,
|
|
67
|
+
* and that asymmetry is the whole design of the grammar — see
|
|
68
|
+
* {@link canonicalPlatformToken}.
|
|
69
|
+
*/
|
|
70
|
+
const MUSL_SUFFIX = '-musl';
|
|
71
|
+
/**
|
|
72
|
+
* Split a prebuild target into its three axes.
|
|
73
|
+
*
|
|
74
|
+
* TOKEN GRAMMAR: `<os>-<arch>[-musl]`.
|
|
75
|
+
*
|
|
76
|
+
* An UNSUFFIXED token means "the default build". Note what it does NOT mean: it
|
|
77
|
+
* is not a synonym for glibc. For an artifact that links `libc.so.6` the default
|
|
78
|
+
* build IS glibc; for one that links only GLib/GObject/GIO (plus GnuTLS resp.
|
|
79
|
+
* GStreamer) it is libc-AGNOSTIC and loads against whatever libc the host's GLib
|
|
80
|
+
* was built for. The directory name cannot express that difference and does not
|
|
81
|
+
* need to: the package's own `libc` field answers it at install time, and the
|
|
82
|
+
* `prebuild-libc` conformance rule verifies that field against the binaries
|
|
83
|
+
* (`readElfNeeded` — the DT_NEEDED list is where the answer actually lives).
|
|
84
|
+
*
|
|
85
|
+
* DELIBERATELY NOT CHOSEN: renaming every existing `linux-<arch>` directory to
|
|
86
|
+
* `linux-<arch>-gnu`, which is what a symmetric grammar would demand. It would
|
|
87
|
+
* rename ~60 COMMITTED directories, and — per `status/open-todos.md`'s
|
|
88
|
+
* "Nine fixtures re-implement the prebuild-target name instead of importing it"
|
|
89
|
+
* — nine e2e fixtures COMPOSE the token themselves instead of importing it, so
|
|
90
|
+
* the rename would have to be swept by hand in two shapes (the literal path and
|
|
91
|
+
* the computed one), which is exactly how the last vocabulary change missed
|
|
92
|
+
* `tests/e2e/self-host/run.mjs`. The asymmetric grammar buys the same
|
|
93
|
+
* expressiveness for zero renames, and the cost — that "unsuffixed" is not
|
|
94
|
+
* self-describing — is paid by a field that is machine-checked anyway.
|
|
95
|
+
*
|
|
96
|
+
* A `-musl` suffix is only meaningful when the OS half is `linux`: musl targets
|
|
97
|
+
* no other kernel, and npm's own `libc` field is documented as Linux-only. A
|
|
98
|
+
* `darwin-arm64-musl` token is therefore not a target with an odd name, it is a
|
|
99
|
+
* malformed one, and this returns `libc: null` for it so callers report the
|
|
100
|
+
* token rather than half-honouring it.
|
|
101
|
+
*/
|
|
102
|
+
export function parsePlatformToken(token) {
|
|
103
|
+
const isMusl = token.endsWith(MUSL_SUFFIX);
|
|
104
|
+
const base = isMusl ? token.slice(0, -MUSL_SUFFIX.length) : token;
|
|
105
|
+
const dash = base.indexOf('-');
|
|
106
|
+
const os = dash < 0 ? base : base.slice(0, dash);
|
|
107
|
+
const arch = dash < 0 ? '' : base.slice(dash + 1);
|
|
108
|
+
return { os, arch, libc: isMusl && os === 'linux' ? 'musl' : null };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Canonical `<os>-<arch>[-musl]` form — the node spelling — so `linux-x86_64`
|
|
112
|
+
* and `linux-x64` compare equal. Only the *arch* half is normalised: the OS half
|
|
113
|
+
* is always a `process.platform` token (`linux`/`darwin`/`win32`), which has no
|
|
114
|
+
* competing spelling, and the libc half has exactly one spelling by
|
|
115
|
+
* construction.
|
|
116
|
+
*
|
|
117
|
+
* A `-musl` suffix on a non-Linux token is preserved verbatim rather than
|
|
118
|
+
* dropped. Silently canonicalising `darwin-arm64-musl` to `darwin-arm64` would
|
|
119
|
+
* make a malformed declaration compare EQUAL to a valid one, so the audit that
|
|
120
|
+
* rejects it would never see it — the same class of bug as folding the legacy
|
|
121
|
+
* uname spelling on a WRITE path.
|
|
61
122
|
*/
|
|
62
123
|
export function canonicalPlatformToken(token) {
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
124
|
+
const { os, arch, libc } = parsePlatformToken(token);
|
|
125
|
+
if (!arch)
|
|
65
126
|
return token;
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
127
|
+
const canonical = `${os}-${ARCH_ALIASES[arch] ?? arch}`;
|
|
128
|
+
if (libc === 'musl')
|
|
129
|
+
return `${canonical}${MUSL_SUFFIX}`;
|
|
130
|
+
// Not a Linux musl token, so `-musl` (if present at all) is not a libc
|
|
131
|
+
// suffix this grammar recognises — keep the token intact.
|
|
132
|
+
return token.endsWith(MUSL_SUFFIX) ? `${canonical}${MUSL_SUFFIX}` : canonical;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The target tokens that describe THIS host, most-specific first — the WRITE-side
|
|
136
|
+
* vocabulary.
|
|
137
|
+
*
|
|
138
|
+
* This is the single definition of the musl preference order, and three callers
|
|
139
|
+
* share it so it cannot drift: the directory probe ({@link prebuildDirCandidates}),
|
|
140
|
+
* the per-platform sibling package probe ({@link platformPackageName}), and
|
|
141
|
+
* `scripts/stage-prebuild.mjs`, which must stage a musl build into
|
|
142
|
+
* `linux-x64-musl/` and never into `linux-x64/`.
|
|
143
|
+
*
|
|
144
|
+
* On a musl host the suffixed token comes FIRST and the unsuffixed one stays as a
|
|
145
|
+
* fallback: an unsuffixed directory is the default build, which for the
|
|
146
|
+
* libc-agnostic bridges (they record no `libc.so.6` at all) genuinely does load
|
|
147
|
+
* on musl. On glibc the `-musl` token is not offered at all — a musl artifact
|
|
148
|
+
* cannot load against glibc, so probing for it could only ever produce a
|
|
149
|
+
* false positive.
|
|
150
|
+
*
|
|
151
|
+
* @param platform `process.platform` value.
|
|
152
|
+
* @param arch `process.arch` value.
|
|
153
|
+
* @param libc host C library; `undefined`/`null` behaves as glibc (see
|
|
154
|
+
* {@link HostTarget.libc} for why that is the safe default).
|
|
155
|
+
*/
|
|
156
|
+
export function hostPlatformTokens(platform, arch, libc) {
|
|
157
|
+
const canonical = `${platform}-${arch}`;
|
|
158
|
+
// The suffix is Linux-only by construction, so a `libc: 'musl'` handed in
|
|
159
|
+
// for darwin/win32 is ignored rather than trusted — the caller's host facts
|
|
160
|
+
// do not get to invent a target the grammar does not have.
|
|
161
|
+
if (libc === 'musl' && platform === 'linux')
|
|
162
|
+
return [`${canonical}${MUSL_SUFFIX}`, canonical];
|
|
163
|
+
return [canonical];
|
|
69
164
|
}
|
|
70
165
|
/**
|
|
71
166
|
* The prebuild directory names to probe for a host, most-specific first.
|
|
@@ -95,28 +190,46 @@ export function canonicalPlatformToken(token) {
|
|
|
95
190
|
* listing — no extra I/O — so tolerance on the READ side costs nothing, while
|
|
96
191
|
* the single spelling is enforced everywhere a name is WRITTEN.
|
|
97
192
|
*
|
|
193
|
+
* The LIBC AXIS rides in front of all of that: on a musl host the
|
|
194
|
+
* `<os>-<arch>-musl` token is preferred over `<os>-<arch>`, and on glibc the
|
|
195
|
+
* suffixed token is NEVER offered (a musl artifact cannot load against glibc, so
|
|
196
|
+
* a probe for it can only produce a false positive). The `libc` value is a
|
|
197
|
+
* PARAMETER rather than a `process` read for the same reason `platform` and
|
|
198
|
+
* `arch` are: it is the only way the musl branch gets exercised at all, since CI
|
|
199
|
+
* and every developer machine here run glibc.
|
|
200
|
+
*
|
|
98
201
|
* @param platform `process.platform` value.
|
|
99
202
|
* @param arch `process.arch` value.
|
|
100
203
|
* @param declaredPlatforms `package.json#gjsify.platforms`, when present.
|
|
204
|
+
* @param libc host C library; `undefined`/`null` = behave as before the axis
|
|
205
|
+
* existed.
|
|
101
206
|
*/
|
|
102
|
-
export function prebuildDirCandidates(platform, arch, declaredPlatforms) {
|
|
103
|
-
const
|
|
207
|
+
export function prebuildDirCandidates(platform, arch, declaredPlatforms, libc) {
|
|
208
|
+
const hostTokens = hostPlatformTokens(platform, arch, libc);
|
|
104
209
|
const legacyArch = NODE_ARCH_TO_LEGACY_UNAME[arch] ?? arch;
|
|
105
210
|
const out = [];
|
|
106
211
|
const push = (name) => {
|
|
107
212
|
if (!out.includes(name))
|
|
108
213
|
out.push(name);
|
|
109
214
|
};
|
|
110
|
-
// 1. The package's own declared spelling for
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
215
|
+
// 1. The package's own declared spelling for each host token, most-specific
|
|
216
|
+
// token first. Keyed off the SAME host-token list as step 2, so a musl
|
|
217
|
+
// host that finds `linux-x64-musl` in `gjsify.platforms` prefers it over
|
|
218
|
+
// a declared `linux-x64` — the declaration probe must not be able to
|
|
219
|
+
// reorder the libc preference.
|
|
220
|
+
for (const token of hostTokens) {
|
|
221
|
+
for (const declared of declaredPlatforms ?? []) {
|
|
222
|
+
if (typeof declared !== 'string')
|
|
223
|
+
continue;
|
|
224
|
+
if (canonicalPlatformToken(declared) === token)
|
|
225
|
+
push(declared);
|
|
226
|
+
}
|
|
227
|
+
// 2. The canonical node spelling — what everything in the train stages.
|
|
228
|
+
push(token);
|
|
116
229
|
}
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
//
|
|
230
|
+
// 3. The legacy uname spelling, for undeclared pre-rename tarballs. No musl
|
|
231
|
+
// variant: the rename predates the libc axis by years, so no tarball on
|
|
232
|
+
// npm can carry a uname-spelled musl directory.
|
|
120
233
|
push(`${platform}-${legacyArch}`);
|
|
121
234
|
return out;
|
|
122
235
|
}
|
|
@@ -131,28 +244,59 @@ export function prebuildDirCandidates(platform, arch, declaredPlatforms) {
|
|
|
131
244
|
* ships nothing loadable on this host.
|
|
132
245
|
*/
|
|
133
246
|
export function resolvePrebuildDirName(input) {
|
|
134
|
-
const { platform, arch, declaredPlatforms, existingDirs } = input;
|
|
135
|
-
for (const candidate of prebuildDirCandidates(platform, arch, declaredPlatforms)) {
|
|
247
|
+
const { platform, arch, declaredPlatforms, existingDirs, libc } = input;
|
|
248
|
+
for (const candidate of prebuildDirCandidates(platform, arch, declaredPlatforms, libc)) {
|
|
136
249
|
if (existingDirs.includes(candidate))
|
|
137
250
|
return candidate;
|
|
138
251
|
}
|
|
139
252
|
return null;
|
|
140
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* The npm name of the per-platform companion package for a target.
|
|
256
|
+
*
|
|
257
|
+
* `@gjsify/rolldown-native` + `linux-x64` → `@gjsify/rolldown-native-linux-x64`,
|
|
258
|
+
* the pattern `@gjsify/gtk-runtime-darwin-arm64` already ships (and the one
|
|
259
|
+
* napi-rs uses for its `<pkg>-<triple>` siblings, which
|
|
260
|
+
* `napi-node-addon.ts`'s `isNapiRsSibling` already knows).
|
|
261
|
+
*
|
|
262
|
+
* Exported so there is exactly ONE definition of it. The heavy bridges are being
|
|
263
|
+
* split into per-platform packages, and that rollout, the sibling resolution
|
|
264
|
+
* below and the workspace audit all need this string — three independent
|
|
265
|
+
* `` `${name}-${token}` `` template literals would be three places for the
|
|
266
|
+
* separator, the scope handling or the token spelling to drift, and a drifted
|
|
267
|
+
* copy fails as "prebuild not found" in a consumer while every test here stays
|
|
268
|
+
* green.
|
|
269
|
+
*
|
|
270
|
+
* @param baseName the depending package's npm name, scope included.
|
|
271
|
+
* @param token an `<os>-<arch>[-musl]` target.
|
|
272
|
+
*/
|
|
273
|
+
export function platformPackageName(baseName, token) {
|
|
274
|
+
return `${baseName}-${token}`;
|
|
275
|
+
}
|
|
141
276
|
/**
|
|
142
277
|
* Scan all packages in a node_modules directory for gjsify native prebuilds.
|
|
143
278
|
* Handles scoped packages (@scope/name) as well as flat packages.
|
|
144
279
|
*/
|
|
145
280
|
function scanNodeModules(nodeModulesDir, target) {
|
|
146
|
-
const
|
|
281
|
+
const resolved = [];
|
|
282
|
+
const declared = [];
|
|
147
283
|
if (!existsSync(nodeModulesDir))
|
|
148
|
-
return
|
|
284
|
+
return { resolved, declared };
|
|
149
285
|
let entries;
|
|
150
286
|
try {
|
|
151
287
|
entries = readdirSync(nodeModulesDir);
|
|
152
288
|
}
|
|
153
289
|
catch {
|
|
154
|
-
return
|
|
290
|
+
return { resolved, declared };
|
|
155
291
|
}
|
|
292
|
+
const take = (probe) => {
|
|
293
|
+
if (!probe)
|
|
294
|
+
return;
|
|
295
|
+
if (probe.kind === 'resolved')
|
|
296
|
+
resolved.push(probe.pkg);
|
|
297
|
+
else
|
|
298
|
+
declared.push(probe.candidate);
|
|
299
|
+
};
|
|
156
300
|
for (const entry of entries) {
|
|
157
301
|
if (entry.startsWith('.'))
|
|
158
302
|
continue;
|
|
@@ -168,19 +312,15 @@ function scanNodeModules(nodeModulesDir, target) {
|
|
|
168
312
|
}
|
|
169
313
|
for (const scopedPkg of scopeEntries) {
|
|
170
314
|
const pkgDir = join(scopeDir, scopedPkg);
|
|
171
|
-
|
|
172
|
-
if (native)
|
|
173
|
-
results.push(native);
|
|
315
|
+
take(checkPackage(pkgDir, `${entry}/${scopedPkg}`, target));
|
|
174
316
|
}
|
|
175
317
|
}
|
|
176
318
|
else {
|
|
177
319
|
const pkgDir = join(nodeModulesDir, entry);
|
|
178
|
-
|
|
179
|
-
if (native)
|
|
180
|
-
results.push(native);
|
|
320
|
+
take(checkPackage(pkgDir, entry, target));
|
|
181
321
|
}
|
|
182
322
|
}
|
|
183
|
-
return
|
|
323
|
+
return { resolved, declared };
|
|
184
324
|
}
|
|
185
325
|
/** Check a single package directory for gjsify prebuilds metadata. */
|
|
186
326
|
function checkPackage(pkgDir, name, target) {
|
|
@@ -208,17 +348,66 @@ function checkPackage(pkgDir, name, target) {
|
|
|
208
348
|
.map((e) => e.name);
|
|
209
349
|
}
|
|
210
350
|
catch {
|
|
211
|
-
|
|
351
|
+
// No `prebuilds/` tree at all. Still a DECLARED prebuild package — the
|
|
352
|
+
// per-platform-package layout puts the binaries in a sibling, so the
|
|
353
|
+
// absence of a local directory is now a normal state, not an absence of
|
|
354
|
+
// information.
|
|
355
|
+
return { kind: 'declared', candidate: { name, pkgDir } };
|
|
212
356
|
}
|
|
213
357
|
const dirName = resolvePrebuildDirName({
|
|
214
358
|
platform: target.platform,
|
|
215
359
|
arch: target.arch,
|
|
216
360
|
declaredPlatforms,
|
|
217
361
|
existingDirs,
|
|
362
|
+
libc: target.libc,
|
|
218
363
|
});
|
|
219
364
|
if (!dirName)
|
|
220
|
-
return
|
|
221
|
-
return { name, prebuildsDir: join(prebuildsRoot, dirName) };
|
|
365
|
+
return { kind: 'declared', candidate: { name, pkgDir } };
|
|
366
|
+
return { kind: 'resolved', pkg: { name, prebuildsDir: join(prebuildsRoot, dirName) } };
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Find a declared package's prebuild in its PER-PLATFORM COMPANION PACKAGE.
|
|
370
|
+
*
|
|
371
|
+
* The heavy bridges are being split the way napi-rs and `@gjsify/gtk-runtime-*`
|
|
372
|
+
* already are: `@gjsify/rolldown-native` keeps the declaration and the JS facade,
|
|
373
|
+
* `@gjsify/rolldown-native-linux-x64` carries the binary. `detectNativePackages`
|
|
374
|
+
* finds such a companion in a HOISTED layout by accident — the up-walk from the
|
|
375
|
+
* start directory eventually lists it as just another `node_modules` entry — but
|
|
376
|
+
* NOT in an isolated one, where pnpm places it in the virtual store under the
|
|
377
|
+
* depending package and no ancestor of the CWD ever contains it.
|
|
378
|
+
*
|
|
379
|
+
* So the search starts at the DEPENDING PACKAGE's own directory instead of the
|
|
380
|
+
* caller's. That is not a second algorithm; it is the SAME up-walk with a
|
|
381
|
+
* different origin, and the first step of that walk — `<pkgDir>/node_modules/…` —
|
|
382
|
+
* is the isolated-layout case. It is also exactly what napi-rs achieves
|
|
383
|
+
* implicitly by calling `require()` from inside the package: Node's resolver
|
|
384
|
+
* walks up from the requiring FILE, not from the process CWD. We cannot use
|
|
385
|
+
* `require`/`import.meta.resolve` here because the companion package's `exports`
|
|
386
|
+
* map need not expose anything importable — the artifact is a directory, not a
|
|
387
|
+
* module — so the walk is done by hand.
|
|
388
|
+
*
|
|
389
|
+
* @returns the companion's own resolved prebuild directory, or null.
|
|
390
|
+
*/
|
|
391
|
+
function resolvePlatformSibling(candidate, target) {
|
|
392
|
+
const tokens = hostPlatformTokens(target.platform, target.arch, target.libc);
|
|
393
|
+
let dir = resolve(candidate.pkgDir);
|
|
394
|
+
while (true) {
|
|
395
|
+
for (const token of tokens) {
|
|
396
|
+
const siblingName = platformPackageName(candidate.name, token);
|
|
397
|
+
const siblingDir = join(dir, 'node_modules', ...siblingName.split('/'));
|
|
398
|
+
const probe = checkPackage(siblingDir, siblingName, target);
|
|
399
|
+
// A companion package resolves through the ORDINARY path: it
|
|
400
|
+
// declares its own `gjsify.prebuilds` + the one target it exists
|
|
401
|
+
// for. Reusing `checkPackage` is what keeps the split layout from
|
|
402
|
+
// becoming a second contract nothing audits.
|
|
403
|
+
if (probe?.kind === 'resolved')
|
|
404
|
+
return probe.pkg;
|
|
405
|
+
}
|
|
406
|
+
const parent = resolve(dir, '..');
|
|
407
|
+
if (parent === dir)
|
|
408
|
+
return null; // reached filesystem root
|
|
409
|
+
dir = parent;
|
|
410
|
+
}
|
|
222
411
|
}
|
|
223
412
|
/**
|
|
224
413
|
* Walk up the directory tree from `startDir` and merge native packages found
|
|
@@ -237,30 +426,117 @@ function checkPackage(pkgDir, name, target) {
|
|
|
237
426
|
* production call sites take the defaults.
|
|
238
427
|
*/
|
|
239
428
|
export function detectNativePackages(startDir, target = {}) {
|
|
240
|
-
const
|
|
429
|
+
const host = {
|
|
241
430
|
platform: target.platform ?? process.platform,
|
|
242
431
|
arch: target.arch ?? process.arch,
|
|
432
|
+
// The ONE place the host's libc is read, and only as a default — every
|
|
433
|
+
// function below takes it as a parameter so the musl branch is reachable
|
|
434
|
+
// from a glibc test host.
|
|
435
|
+
//
|
|
436
|
+
// `!== undefined` rather than `??`: an explicit `libc: null` means "no
|
|
437
|
+
// libc axis / do not classify", and `??` would treat it as absent and
|
|
438
|
+
// probe the host anyway — which is the one value a caller passes when it
|
|
439
|
+
// specifically does NOT want the host consulted.
|
|
440
|
+
libc: target.libc !== undefined ? target.libc : detectHostLibc(target.platform ?? process.platform),
|
|
243
441
|
};
|
|
244
442
|
const merged = [];
|
|
245
443
|
const seen = new Set();
|
|
444
|
+
/** Declared-but-unresolved packages, first occurrence wins as above. */
|
|
445
|
+
const candidates = [];
|
|
246
446
|
let dir = resolve(startDir);
|
|
247
447
|
while (true) {
|
|
248
448
|
const nodeModulesDir = join(dir, 'node_modules');
|
|
249
449
|
if (existsSync(nodeModulesDir)) {
|
|
250
|
-
|
|
450
|
+
const found = scanNodeModules(nodeModulesDir, host);
|
|
451
|
+
for (const pkg of found.resolved) {
|
|
251
452
|
if (seen.has(pkg.name))
|
|
252
453
|
continue;
|
|
253
454
|
seen.add(pkg.name);
|
|
254
455
|
merged.push(pkg);
|
|
255
456
|
}
|
|
457
|
+
for (const candidate of found.declared) {
|
|
458
|
+
if (candidates.some((c) => c.name === candidate.name))
|
|
459
|
+
continue;
|
|
460
|
+
candidates.push(candidate);
|
|
461
|
+
}
|
|
256
462
|
}
|
|
257
463
|
const parent = resolve(dir, '..');
|
|
258
464
|
if (parent === dir)
|
|
259
465
|
break; // reached filesystem root
|
|
260
466
|
dir = parent;
|
|
261
467
|
}
|
|
468
|
+
// Second pass: per-platform companion packages. Runs AFTER the whole walk so
|
|
469
|
+
// it can dedupe against everything the walk found — in a hoisted layout the
|
|
470
|
+
// companion is already in `merged` under its own name (the walk lists it like
|
|
471
|
+
// any other package), and adding it twice would put the same directory on
|
|
472
|
+
// `GI_TYPELIB_PATH` twice for no benefit. It also cannot ADD a name that a
|
|
473
|
+
// closer `node_modules` already resolved, which keeps Node's
|
|
474
|
+
// nearest-wins semantics intact.
|
|
475
|
+
for (const candidate of candidates) {
|
|
476
|
+
if (seen.has(candidate.name))
|
|
477
|
+
continue;
|
|
478
|
+
const sibling = resolvePlatformSibling(candidate, host);
|
|
479
|
+
if (!sibling || seen.has(sibling.name))
|
|
480
|
+
continue;
|
|
481
|
+
seen.add(sibling.name);
|
|
482
|
+
merged.push(sibling);
|
|
483
|
+
}
|
|
262
484
|
return merged;
|
|
263
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* Decide a host's C library from independently-gathered facts. PURE.
|
|
488
|
+
*
|
|
489
|
+
* Two probes, because neither is available everywhere this CLI runs:
|
|
490
|
+
*
|
|
491
|
+
* 1. `process.report.getReport().header.glibcVersionRuntime` — present iff the
|
|
492
|
+
* running process is linked against glibc. This is the probe `detect-libc`
|
|
493
|
+
* and node-gyp use, and it is authoritative when it answers. It is a
|
|
494
|
+
* NODE-only API: under the committed GJS bundle `@gjsify/process` has no
|
|
495
|
+
* `report`, so on the runtime this project targets FIRST it never answers.
|
|
496
|
+
* 2. Whether musl's dynamic loader is installed. musl always installs it as
|
|
497
|
+
* `/lib/ld-musl-<arch>.so.1`, which is a fact about the SYSTEM rather than
|
|
498
|
+
* about the running process, so it answers under GJS too.
|
|
499
|
+
*
|
|
500
|
+
* The order matters only for speed; the two cannot disagree on a sane host.
|
|
501
|
+
* When NEITHER answers we return `'glibc'`, and that is a claim about the
|
|
502
|
+
* evidence rather than a guess: probe 2 not finding a musl loader means the host
|
|
503
|
+
* has no musl at all, so `-musl` directories are not what it wants. The failure
|
|
504
|
+
* mode of the opposite default — treating an unclassified host as musl — is
|
|
505
|
+
* probing for suffixed directories on every glibc machine, which is noise with no
|
|
506
|
+
* upside.
|
|
507
|
+
*
|
|
508
|
+
* @param platform `process.platform` value; the axis is Linux-only (as npm's own
|
|
509
|
+
* `libc` field is), so every other OS returns null.
|
|
510
|
+
*/
|
|
511
|
+
export function resolveHostLibc(input) {
|
|
512
|
+
if (input.platform !== 'linux')
|
|
513
|
+
return null;
|
|
514
|
+
if (typeof input.glibcVersionRuntime === 'string' && input.glibcVersionRuntime.length > 0)
|
|
515
|
+
return 'glibc';
|
|
516
|
+
return input.muslLoaderPresent ? 'musl' : 'glibc';
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Gather the two host facts {@link resolveHostLibc} decides from.
|
|
520
|
+
*
|
|
521
|
+
* BOTH are gathered unconditionally, even though the first one alone usually
|
|
522
|
+
* answers. They are independent observations — one about the running process, one
|
|
523
|
+
* about the system — and a host can genuinely exhibit both (a glibc-linked Node
|
|
524
|
+
* on a distro that also installs musl, e.g. under gcompat). Collecting them
|
|
525
|
+
* separately keeps the decision itself in one pure function a test can pin,
|
|
526
|
+
* instead of encoding the precedence in an if/else chain here where nothing can
|
|
527
|
+
* reach it.
|
|
528
|
+
*/
|
|
529
|
+
export function detectHostLibc(platform) {
|
|
530
|
+
if (platform !== 'linux')
|
|
531
|
+
return null;
|
|
532
|
+
const header = process.report?.getReport()?.header;
|
|
533
|
+
const glibcVersionRuntime = typeof header?.glibcVersionRuntime === 'string' ? header.glibcVersionRuntime : undefined;
|
|
534
|
+
// Read the directory rather than testing per-arch loader names: those names
|
|
535
|
+
// (`ld-musl-x86_64`, `ld-musl-aarch64`, …) are a THIRD arch vocabulary, and
|
|
536
|
+
// this file already documents why the repo keeps exactly one.
|
|
537
|
+
const muslLoaderPresent = existsSync('/lib') && readdirSync('/lib').some((f) => f.startsWith('ld-musl-'));
|
|
538
|
+
return resolveHostLibc({ platform, glibcVersionRuntime, muslLoaderPresent });
|
|
539
|
+
}
|
|
264
540
|
/**
|
|
265
541
|
* The environment-variable name the host's dynamic loader consults for an
|
|
266
542
|
* additional shared-library search path, plus the separator its search paths
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which kind of directory link this platform uses.
|
|
3
|
+
*
|
|
4
|
+
* Spelled with a NAME for the POSIX case rather than `undefined`, so both
|
|
5
|
+
* branches can be requested explicitly. `dirLinkTarget` takes this as a
|
|
6
|
+
* defaulted parameter, and a defaulted parameter cannot receive `undefined`:
|
|
7
|
+
* passing it IS how you ask for the default. So while POSIX was spelled
|
|
8
|
+
* `undefined`, `dirLinkTarget(link, target, undefined)` meant "use the host's
|
|
9
|
+
* kind" — the POSIX branch was unexercisable on Windows, and the win32 branch
|
|
10
|
+
* unexercisable anywhere else. The suite read as if it covered both and did
|
|
11
|
+
* not, which is precisely the shape the header above blames for the `'dir'`
|
|
12
|
+
* bug reaching a release.
|
|
13
|
+
*/
|
|
14
|
+
export type DirLinkKind = 'junction' | 'symlink';
|
|
15
|
+
/** The link kind directory links use on this platform. */
|
|
16
|
+
export declare const DIR_LINK_KIND: DirLinkKind;
|
|
17
|
+
/**
|
|
18
|
+
* The link type a directory link must pass to `fs.symlink`.
|
|
19
|
+
*
|
|
20
|
+
* `undefined` on POSIX is deliberate: Node's default is correct there, and
|
|
21
|
+
* passing a type would be noise. Derived from {@link DIR_LINK_KIND} so the
|
|
22
|
+
* platform decision has exactly one source.
|
|
23
|
+
*/
|
|
24
|
+
export declare const DIR_LINK_TYPE: 'junction' | undefined;
|
|
25
|
+
/** True when directory links on this platform are NTFS junctions. */
|
|
26
|
+
export declare const dirLinksAreJunctions: () => boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Spell the `target` argument for a link at `linkPath` pointing at `absTarget`.
|
|
29
|
+
*
|
|
30
|
+
* Absolute on Windows because a junction demands it (gotcha 1 above); relative
|
|
31
|
+
* on POSIX so the tree stays movable (gotcha 2).
|
|
32
|
+
*
|
|
33
|
+
* `linkKind` is a PARAMETER, defaulting to the host's, for the same reason
|
|
34
|
+
* `detect-native-packages.ts` takes `platform`/`arch`: the Windows branch is
|
|
35
|
+
* then unit-testable from a Linux host, which is the only place anyone runs the
|
|
36
|
+
* tests. A branch nobody can exercise is how the `'dir'` bug survived to a
|
|
37
|
+
* release — and see {@link DirLinkKind} for why the POSIX side had to be named
|
|
38
|
+
* rather than spelled `undefined` before that promise actually held.
|
|
39
|
+
*
|
|
40
|
+
* BOTH paths must be in the SAME canonical space — either both canonical or
|
|
41
|
+
* both spelled through the same symlinks. The POSIX target is relative to the
|
|
42
|
+
* link's own directory, but the kernel resolves it from the link's REAL
|
|
43
|
+
* directory, so mixing the two spaces yields a link that points nowhere. macOS
|
|
44
|
+
* is where this actually happens: `os.tmpdir()` is `/var/folders/…` and `/var`
|
|
45
|
+
* is a symlink to `/private/var`, so a `linkPath` built from `tmpdir()` and an
|
|
46
|
+
* `absTarget` that has been through `realpathSync` disagree, and the relative
|
|
47
|
+
* path between them walks out of the real tree. `realpathSync` whichever side
|
|
48
|
+
* is not already canonical before calling this.
|
|
49
|
+
*
|
|
50
|
+
* @param linkPath where the link itself will live
|
|
51
|
+
* @param absTarget the directory it should point at, as an ABSOLUTE path
|
|
52
|
+
* @param linkKind override the host's link kind (tests only)
|
|
53
|
+
*/
|
|
54
|
+
export declare function dirLinkTarget(linkPath: string, absTarget: string, linkKind?: DirLinkKind): string;
|
|
55
|
+
/**
|
|
56
|
+
* Link `linkPath` → `absTarget`, choosing symlink-vs-junction and the target
|
|
57
|
+
* spelling for the host.
|
|
58
|
+
*
|
|
59
|
+
* `absTarget` must be absolute; passing a relative path is the mistake gotcha 1
|
|
60
|
+
* describes, and it would only misbehave on Windows, so it is rejected here on
|
|
61
|
+
* every platform rather than left to surface on the one host nobody tests on.
|
|
62
|
+
*/
|
|
63
|
+
export declare function linkDirSync(linkPath: string, absTarget: string): void;
|
|
64
|
+
/** Promise-returning twin of {@link linkDirSync}. */
|
|
65
|
+
export declare function linkDir(linkPath: string, absTarget: string): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Recreate an EXISTING link at a new path, preserving what it points at.
|
|
68
|
+
*
|
|
69
|
+
* A different job from {@link linkDirSync}, and it cannot reuse it: the source
|
|
70
|
+
* link's target is whatever it already is — frequently relative, deliberately so
|
|
71
|
+
* — while a junction demands an absolute one. Copying the target verbatim is
|
|
72
|
+
* therefore right on POSIX and wrong on Windows the moment the target is a
|
|
73
|
+
* directory.
|
|
74
|
+
*
|
|
75
|
+
* So the target's KIND decides: a link to a directory becomes a junction with
|
|
76
|
+
* the target resolved against the source link's own directory (the resolution
|
|
77
|
+
* Node will not do for you — see gotcha 1 above); anything else stays a plain
|
|
78
|
+
* file symlink, which Windows allows unprivileged.
|
|
79
|
+
*
|
|
80
|
+
* A dangling source link keeps the verbatim target: it points nowhere either
|
|
81
|
+
* way, and guessing a kind for it would turn a faithful copy into a fabrication.
|
|
82
|
+
*
|
|
83
|
+
* Primitive fs ops only — this runs under the GJS facade as well as Node, which
|
|
84
|
+
* is why there is no `fs.cp`/`opendir` here.
|
|
85
|
+
*/
|
|
86
|
+
export declare function replicateLinkSync(srcLink: string, dstLink: string): void;
|