@gjsify/cli 0.21.0 → 0.23.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/README.md +24 -1
- package/dist/cli.gjs.mjs +257 -184
- package/lib/bundler-pick.js +12 -4
- package/lib/commands/affected.js +22 -10
- package/lib/commands/check.js +38 -29
- package/lib/commands/flatpak/build.js +19 -18
- package/lib/commands/foreach.js +48 -41
- package/lib/commands/info.js +11 -5
- package/lib/commands/install.d.ts +16 -0
- package/lib/commands/install.js +103 -32
- package/lib/commands/onboard.js +6 -1
- package/lib/commands/pack.js +69 -4
- package/lib/commands/run.js +26 -14
- package/lib/commands/trust.js +4 -62
- package/lib/commands/workspace.js +49 -30
- package/lib/utils/bin-shim.d.ts +66 -0
- package/lib/utils/bin-shim.js +246 -0
- package/lib/utils/build-cache.d.ts +42 -6
- package/lib/utils/build-cache.js +114 -42
- package/lib/utils/detect-native-packages.d.ts +112 -6
- package/lib/utils/detect-native-packages.js +201 -29
- package/lib/utils/install-backend-native.d.ts +55 -0
- package/lib/utils/install-backend-native.js +152 -13
- package/lib/utils/install-global.d.ts +16 -0
- package/lib/utils/install-global.js +58 -8
- package/lib/utils/oxc-resolve.js +18 -55
- package/lib/utils/publish-headers.d.ts +2 -3
- package/lib/utils/publish-headers.js +64 -3
- package/lib/utils/publishable-packages.d.ts +22 -0
- package/lib/utils/publishable-packages.js +93 -0
- package/lib/utils/run-gjs.d.ts +6 -7
- package/lib/utils/run-gjs.js +4 -3
- package/lib/utils/spawn.d.ts +82 -0
- package/lib/utils/spawn.js +139 -0
- package/lib/utils/stdin.d.ts +25 -0
- package/lib/utils/stdin.js +132 -0
- package/package.json +23 -18
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Utility to find npm packages with gjsify native prebuilds.
|
|
2
|
-
// Packages declare: "gjsify": { "prebuilds": "<dir>" } in their package.json
|
|
3
|
-
//
|
|
2
|
+
// Packages declare: "gjsify": { "prebuilds": "<dir>" } in their package.json,
|
|
3
|
+
// and (since the OS-axis audit) the `<os>-<arch>` targets they promise in
|
|
4
|
+
// "gjsify": { "platforms": [...] }.
|
|
4
5
|
//
|
|
5
6
|
// One algorithm — `detectNativePackages(startDir)` — walks up from `startDir`
|
|
6
7
|
// and exhaustively scans every `node_modules` it finds. Used by:
|
|
@@ -10,24 +11,126 @@
|
|
|
10
11
|
// their full transitive prebuild set picked up automatically. The
|
|
11
12
|
// transitive walk is what makes `gjsify showcase` / `gjsify dlx` work
|
|
12
13
|
// for packages whose Vala typelibs live in *indirect* deps.
|
|
14
|
+
//
|
|
15
|
+
// PLATFORM RESOLUTION IS A PURE FUNCTION. Everything that depends on the host
|
|
16
|
+
// OS/CPU lives in `resolvePrebuildDirName()` / `buildNativeEnv()`, both of
|
|
17
|
+
// which take `platform` / `arch` as parameters instead of reading
|
|
18
|
+
// `process.platform` inline. That is what makes the darwin and win32 branches
|
|
19
|
+
// unit-testable from a Linux host (see `detect-native-packages.spec.ts`) —
|
|
20
|
+
// the host values are only read at the outermost call site, as a default.
|
|
13
21
|
import { readdirSync, existsSync } from 'node:fs';
|
|
14
22
|
import { join, resolve } from 'node:path';
|
|
15
23
|
import { readPackageJson } from './pkg-json.js';
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Map a Node `process.arch` value to the uname-style spelling used by the
|
|
26
|
+
* Vala/meson bridges, which stage into `prebuilds/linux-x86_64/` (the output
|
|
27
|
+
* of `uname -m` on the build host).
|
|
28
|
+
*/
|
|
29
|
+
const NODE_ARCH_TO_UNAME = {
|
|
30
|
+
x64: 'x86_64',
|
|
31
|
+
arm64: 'aarch64',
|
|
32
|
+
arm: 'armv7',
|
|
33
|
+
ia32: 'i686',
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Arch spellings that name the same target. Mirrors `ARCH_ALIASES` in
|
|
37
|
+
* `scripts/audit-runtimes.mjs` — that script canonicalises
|
|
38
|
+
* `package.json#gjsify.platforms` the same way for the platform-support
|
|
39
|
+
* matrix, and the two MUST agree or a package could pass the audit while the
|
|
40
|
+
* CLI fails to find its artifact.
|
|
41
|
+
*/
|
|
42
|
+
const ARCH_ALIASES = {
|
|
43
|
+
x64: 'x86_64',
|
|
44
|
+
amd64: 'x86_64',
|
|
45
|
+
arm64: 'aarch64',
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Canonical `<os>-<arch>` form so `linux-x64` and `linux-x86_64` — or
|
|
49
|
+
* `darwin-arm64` and `darwin-aarch64` — compare equal. Only the *arch* half is
|
|
50
|
+
* normalised; the OS half is always a `process.platform` token
|
|
51
|
+
* (`linux`/`darwin`/`win32`), which has no competing spelling.
|
|
52
|
+
*/
|
|
53
|
+
export function canonicalPlatformToken(token) {
|
|
54
|
+
const dash = token.indexOf('-');
|
|
55
|
+
if (dash < 0)
|
|
56
|
+
return token;
|
|
57
|
+
const os = token.slice(0, dash);
|
|
58
|
+
const arch = token.slice(dash + 1);
|
|
59
|
+
return `${os}-${ARCH_ALIASES[arch] ?? arch}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The prebuild directory names to probe for a host, most-specific first.
|
|
63
|
+
*
|
|
64
|
+
* There are two live spellings in this repo and neither can be declared the
|
|
65
|
+
* winner:
|
|
66
|
+
*
|
|
67
|
+
* * the Vala/meson bridges stage `prebuilds/linux-x86_64/` — uname-style,
|
|
68
|
+
* because the meson jobs name the directory after `uname -m`;
|
|
69
|
+
* * `@gjsify/node-gi` (and `@gjsify/napi`'s prebuild staging) use
|
|
70
|
+
* `prebuilds/${process.platform}-${process.arch}/` — node-style
|
|
71
|
+
* (`linux-x64`, `darwin-arm64`, `win32-x64`).
|
|
72
|
+
*
|
|
73
|
+
* So we probe both, and — when the package declares `gjsify.platforms` — we
|
|
74
|
+
* probe the package's OWN spelling first. That is strictly better than
|
|
75
|
+
* guessing: a package that declares `darwin-arm64` tells us the exact
|
|
76
|
+
* directory name it stages, whatever convention it picked.
|
|
77
|
+
*
|
|
78
|
+
* Ordering of the generic fallbacks is per-OS rather than fixed, so the first
|
|
79
|
+
* probe is the spelling that platform's toolchain actually produces:
|
|
80
|
+
* uname-style on Linux (`linux-x86_64`), node-style on macOS/Windows
|
|
81
|
+
* (`darwin-arm64`, `win32-x64` — on Apple silicon `uname -m` also reports
|
|
82
|
+
* `arm64`, so there node-style *is* the native spelling).
|
|
83
|
+
*
|
|
84
|
+
* @param platform `process.platform` value.
|
|
85
|
+
* @param arch `process.arch` value.
|
|
86
|
+
* @param declaredPlatforms `package.json#gjsify.platforms`, when present.
|
|
87
|
+
*/
|
|
88
|
+
export function prebuildDirCandidates(platform, arch, declaredPlatforms) {
|
|
89
|
+
const unameArch = NODE_ARCH_TO_UNAME[arch] ?? arch;
|
|
90
|
+
const preferred = platform === 'linux' ? unameArch : arch;
|
|
91
|
+
const alternate = platform === 'linux' ? arch : unameArch;
|
|
92
|
+
const generic = [`${platform}-${preferred}`, `${platform}-${alternate}`];
|
|
93
|
+
const hostCanonical = canonicalPlatformToken(generic[0]);
|
|
94
|
+
const out = [];
|
|
95
|
+
const push = (name) => {
|
|
96
|
+
if (!out.includes(name))
|
|
97
|
+
out.push(name);
|
|
23
98
|
};
|
|
24
|
-
|
|
99
|
+
// 1. The package's own declared spelling for THIS host, if it declares one.
|
|
100
|
+
for (const declared of declaredPlatforms ?? []) {
|
|
101
|
+
if (typeof declared !== 'string')
|
|
102
|
+
continue;
|
|
103
|
+
if (canonicalPlatformToken(declared) === hostCanonical)
|
|
104
|
+
push(declared);
|
|
105
|
+
}
|
|
106
|
+
// 2. The two generic spellings.
|
|
107
|
+
for (const name of generic)
|
|
108
|
+
push(name);
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Pick the prebuild directory a host should load, given the directories that
|
|
113
|
+
* actually exist under `<pkg>/<prebuilds>/`.
|
|
114
|
+
*
|
|
115
|
+
* Pure: no `process.*` reads, no filesystem access. This is the function the
|
|
116
|
+
* darwin and win32 branches are unit-tested through on a Linux host.
|
|
117
|
+
*
|
|
118
|
+
* @returns the chosen directory NAME (not a path), or `null` when the package
|
|
119
|
+
* ships nothing loadable on this host.
|
|
120
|
+
*/
|
|
121
|
+
export function resolvePrebuildDirName(input) {
|
|
122
|
+
const { platform, arch, declaredPlatforms, existingDirs } = input;
|
|
123
|
+
for (const candidate of prebuildDirCandidates(platform, arch, declaredPlatforms)) {
|
|
124
|
+
if (existingDirs.includes(candidate))
|
|
125
|
+
return candidate;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
25
128
|
}
|
|
26
129
|
/**
|
|
27
130
|
* Scan all packages in a node_modules directory for gjsify native prebuilds.
|
|
28
131
|
* Handles scoped packages (@scope/name) as well as flat packages.
|
|
29
132
|
*/
|
|
30
|
-
function scanNodeModules(nodeModulesDir,
|
|
133
|
+
function scanNodeModules(nodeModulesDir, target) {
|
|
31
134
|
const results = [];
|
|
32
135
|
if (!existsSync(nodeModulesDir))
|
|
33
136
|
return results;
|
|
@@ -53,14 +156,14 @@ function scanNodeModules(nodeModulesDir, arch) {
|
|
|
53
156
|
}
|
|
54
157
|
for (const scopedPkg of scopeEntries) {
|
|
55
158
|
const pkgDir = join(scopeDir, scopedPkg);
|
|
56
|
-
const native = checkPackage(pkgDir, `${entry}/${scopedPkg}`,
|
|
159
|
+
const native = checkPackage(pkgDir, `${entry}/${scopedPkg}`, target);
|
|
57
160
|
if (native)
|
|
58
161
|
results.push(native);
|
|
59
162
|
}
|
|
60
163
|
}
|
|
61
164
|
else {
|
|
62
165
|
const pkgDir = join(nodeModulesDir, entry);
|
|
63
|
-
const native = checkPackage(pkgDir, entry,
|
|
166
|
+
const native = checkPackage(pkgDir, entry, target);
|
|
64
167
|
if (native)
|
|
65
168
|
results.push(native);
|
|
66
169
|
}
|
|
@@ -68,20 +171,42 @@ function scanNodeModules(nodeModulesDir, arch) {
|
|
|
68
171
|
return results;
|
|
69
172
|
}
|
|
70
173
|
/** Check a single package directory for gjsify prebuilds metadata. */
|
|
71
|
-
function checkPackage(pkgDir, name,
|
|
174
|
+
function checkPackage(pkgDir, name, target) {
|
|
72
175
|
const pkgJson = readPackageJson(join(pkgDir, 'package.json'));
|
|
73
176
|
if (!pkgJson)
|
|
74
177
|
return null;
|
|
75
178
|
const gjsifyMeta = pkgJson['gjsify'];
|
|
76
179
|
if (!gjsifyMeta || typeof gjsifyMeta !== 'object')
|
|
77
180
|
return null;
|
|
78
|
-
const
|
|
181
|
+
const meta = gjsifyMeta;
|
|
182
|
+
const prebuildsField = meta['prebuilds'];
|
|
79
183
|
if (typeof prebuildsField !== 'string')
|
|
80
184
|
return null;
|
|
81
|
-
const
|
|
82
|
-
|
|
185
|
+
const declaredRaw = meta['platforms'];
|
|
186
|
+
const declaredPlatforms = Array.isArray(declaredRaw)
|
|
187
|
+
? declaredRaw.filter((p) => typeof p === 'string')
|
|
188
|
+
: undefined;
|
|
189
|
+
const prebuildsRoot = join(pkgDir, prebuildsField);
|
|
190
|
+
let existingDirs;
|
|
191
|
+
try {
|
|
192
|
+
// A prebuild dir may legitimately be a symlink (workspace/dev layouts),
|
|
193
|
+
// so accept both real directories and links.
|
|
194
|
+
existingDirs = readdirSync(prebuildsRoot, { withFileTypes: true })
|
|
195
|
+
.filter((e) => e.isDirectory() || e.isSymbolicLink())
|
|
196
|
+
.map((e) => e.name);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const dirName = resolvePrebuildDirName({
|
|
202
|
+
platform: target.platform,
|
|
203
|
+
arch: target.arch,
|
|
204
|
+
declaredPlatforms,
|
|
205
|
+
existingDirs,
|
|
206
|
+
});
|
|
207
|
+
if (!dirName)
|
|
83
208
|
return null;
|
|
84
|
-
return { name, prebuildsDir };
|
|
209
|
+
return { name, prebuildsDir: join(prebuildsRoot, dirName) };
|
|
85
210
|
}
|
|
86
211
|
/**
|
|
87
212
|
* Walk up the directory tree from `startDir` and merge native packages found
|
|
@@ -95,16 +220,22 @@ function checkPackage(pkgDir, name, arch) {
|
|
|
95
220
|
*
|
|
96
221
|
* Deduplication: the first match for a given package name wins (closer
|
|
97
222
|
* node_modules shadows outer ones), matching Node.js resolution semantics.
|
|
223
|
+
*
|
|
224
|
+
* @param target overrides the host platform/arch. Only tests pass this; the
|
|
225
|
+
* production call sites take the defaults.
|
|
98
226
|
*/
|
|
99
|
-
export function detectNativePackages(startDir) {
|
|
100
|
-
const
|
|
227
|
+
export function detectNativePackages(startDir, target = {}) {
|
|
228
|
+
const resolved = {
|
|
229
|
+
platform: target.platform ?? process.platform,
|
|
230
|
+
arch: target.arch ?? process.arch,
|
|
231
|
+
};
|
|
101
232
|
const merged = [];
|
|
102
233
|
const seen = new Set();
|
|
103
234
|
let dir = resolve(startDir);
|
|
104
235
|
while (true) {
|
|
105
236
|
const nodeModulesDir = join(dir, 'node_modules');
|
|
106
237
|
if (existsSync(nodeModulesDir)) {
|
|
107
|
-
for (const pkg of scanNodeModules(nodeModulesDir,
|
|
238
|
+
for (const pkg of scanNodeModules(nodeModulesDir, resolved)) {
|
|
108
239
|
if (seen.has(pkg.name))
|
|
109
240
|
continue;
|
|
110
241
|
seen.add(pkg.name);
|
|
@@ -119,14 +250,55 @@ export function detectNativePackages(startDir) {
|
|
|
119
250
|
return merged;
|
|
120
251
|
}
|
|
121
252
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
253
|
+
* The environment-variable name the host's dynamic loader consults for an
|
|
254
|
+
* additional shared-library search path, plus the separator its search paths
|
|
255
|
+
* use.
|
|
256
|
+
*
|
|
257
|
+
* * **Linux / other ELF** — `LD_LIBRARY_PATH`, `:`-separated.
|
|
258
|
+
* * **macOS** — `DYLD_LIBRARY_PATH`. `dyld` does not read `LD_LIBRARY_PATH`
|
|
259
|
+
* at all, which is why `.github/workflows/napi.yml` sets `DYLD_LIBRARY_PATH`
|
|
260
|
+
* by hand on every macOS gate step.
|
|
261
|
+
* * **Windows** — there is no dedicated variable; `LoadLibrary` searches the
|
|
262
|
+
* process `PATH`, `;`-separated. This is the same mechanism
|
|
263
|
+
* `@gjsify/node-gi`'s `maybePrependGtkRuntimeDllPath()` uses to make its
|
|
264
|
+
* bundled GTK DLLs resolvable.
|
|
124
265
|
*/
|
|
125
|
-
export function
|
|
266
|
+
export function libraryPathVar(platform) {
|
|
267
|
+
if (platform === 'darwin')
|
|
268
|
+
return { name: 'DYLD_LIBRARY_PATH', separator: ':' };
|
|
269
|
+
if (platform === 'win32')
|
|
270
|
+
return { name: 'PATH', separator: ';' };
|
|
271
|
+
return { name: 'LD_LIBRARY_PATH', separator: ':' };
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Build the typelib + shared-library search-path environment for the detected
|
|
275
|
+
* native packages. Prepends the new paths to any existing values.
|
|
276
|
+
*
|
|
277
|
+
* Pure w.r.t. the host: `platform` and `env` are parameters, so the darwin and
|
|
278
|
+
* win32 branches are exercised by unit tests running on Linux.
|
|
279
|
+
*
|
|
280
|
+
* On Windows the `PATH` key is written back under the *host's own* spelling
|
|
281
|
+
* (a stock Windows env block uses `Path`) — Windows env names are
|
|
282
|
+
* case-insensitive, but a plain JS object is not, and `{...process.env,
|
|
283
|
+
* PATH: …}` would hand a child process two competing entries.
|
|
284
|
+
*/
|
|
285
|
+
export function buildNativeEnv(packages, opts = {}) {
|
|
286
|
+
const platform = opts.platform ?? process.platform;
|
|
287
|
+
const env = opts.env ?? process.env;
|
|
126
288
|
const dirs = packages.map((p) => p.prebuildsDir);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
289
|
+
// GLib's search-path separator is `;` on Windows (G_SEARCHPATH_SEPARATOR),
|
|
290
|
+
// `:` everywhere else — the same split as the loader variable below.
|
|
291
|
+
const giSeparator = platform === 'win32' ? ';' : ':';
|
|
292
|
+
const { name: libVarName, separator: libSeparator } = libraryPathVar(platform);
|
|
293
|
+
// Windows env names are case-insensitive; reuse whatever casing the host
|
|
294
|
+
// block already uses so we replace rather than shadow it.
|
|
295
|
+
const libVarKey = platform === 'win32'
|
|
296
|
+
? (Object.keys(env).find((k) => k.toLowerCase() === libVarName.toLowerCase()) ?? libVarName)
|
|
297
|
+
: libVarName;
|
|
298
|
+
const prepend = (value, separator) => [...dirs, ...(value ? [value] : [])].join(separator);
|
|
299
|
+
const out = {
|
|
300
|
+
GI_TYPELIB_PATH: prepend(env['GI_TYPELIB_PATH'], giSeparator),
|
|
301
|
+
};
|
|
302
|
+
out[libVarKey] = prepend(env[libVarKey], libSeparator);
|
|
303
|
+
return out;
|
|
132
304
|
}
|
|
@@ -4,11 +4,66 @@ interface ParsedSpec {
|
|
|
4
4
|
name: string;
|
|
5
5
|
range: string;
|
|
6
6
|
}
|
|
7
|
+
interface ResolvedNode {
|
|
8
|
+
/** Package name (e.g. `@gjsify/cli`, `lodash`). */
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
tarballUrl: string;
|
|
12
|
+
integrity?: string;
|
|
13
|
+
/** Where this node lives relative to the install prefix. Always
|
|
14
|
+
* starts with `node_modules/`; nested entries look like
|
|
15
|
+
* `node_modules/<parent>/node_modules/<dep>`. */
|
|
16
|
+
installPath: string;
|
|
17
|
+
/** `dependencies` field from the packument (range strings keyed by name). */
|
|
18
|
+
dependencies: Record<string, string>;
|
|
19
|
+
/** `optionalDependencies` field from the packument. */
|
|
20
|
+
optionalDependencies: Record<string, string>;
|
|
21
|
+
bin?: string | Record<string, string>;
|
|
22
|
+
}
|
|
7
23
|
export interface InstalledTopLevel {
|
|
8
24
|
name: string;
|
|
9
25
|
version: string;
|
|
10
26
|
}
|
|
11
27
|
export declare function installPackagesNative(opts: InstallOptions): Promise<InstalledTopLevel[]>;
|
|
28
|
+
/**
|
|
29
|
+
* Warn about a native package that installed WITHOUT a usable binary.
|
|
30
|
+
*
|
|
31
|
+
* gjsify install is node-free (it runs under GJS) and does NOT run a package's
|
|
32
|
+
* `install`/`postinstall` lifecycle script — running e.g. `node-gyp rebuild` would
|
|
33
|
+
* need Node + a C++ toolchain, breaking that property (see the header's "out of
|
|
34
|
+
* scope"). Such a package works only if it SHIPS a prebuild for the platform (the
|
|
35
|
+
* sanctioned path — `prebuilds/<platform>-<arch>/…`) or is later built by hand. When
|
|
36
|
+
* neither is present the package silently loads no binary (the `@gjsify/node-gi
|
|
37
|
+
* 0.21.0` case: installed, but `node_gi.node` absent → the consumer hit a runtime
|
|
38
|
+
* failure with no hint). Surface it at install time with an actionable line. Pure
|
|
39
|
+
* detection — no script is run, so node-free-ness is preserved.
|
|
40
|
+
*/
|
|
41
|
+
export declare function warnMissingNativeBuilds(nodes: ResolvedNode[], prefix: string, log: Logger): void;
|
|
12
42
|
export declare function parseSpec(raw: string): ParsedSpec;
|
|
13
43
|
export declare function pickVersion(packument: Packument, range: string, preferred?: string): string | null;
|
|
44
|
+
/**
|
|
45
|
+
* Materialise ONE `node_modules/.bin/<name>` entry for `targetAbs`.
|
|
46
|
+
*
|
|
47
|
+
* Exported (and platform-injectable) so the Windows branch is unit-tested from
|
|
48
|
+
* a Linux host — the file layout and shim contents are asserted there; only the
|
|
49
|
+
* `symlinkSync`/`CreateSymbolicLink` syscall behaviour itself is OS-bound.
|
|
50
|
+
*
|
|
51
|
+
* POSIX: a relative symlink, with a copy as the fallback (some filesystems and
|
|
52
|
+
* container overlays reject symlinks).
|
|
53
|
+
*
|
|
54
|
+
* Windows: `.bin/<name>` is not on `PATHEXT` and Windows has no shebang
|
|
55
|
+
* handling, so a symlink OR a copy of a `#!/usr/bin/env node` script is
|
|
56
|
+
* unrunnable — and a *file* symlink additionally needs elevation or Developer
|
|
57
|
+
* Mode, so the previous code reliably fell into its copy fallback and produced
|
|
58
|
+
* a dead entry. npm solves this with three sibling files (`<name>` for
|
|
59
|
+
* git-bash/MSYS/WSL, `<name>.cmd` for cmd.exe, `<name>.ps1` for pwsh); we write
|
|
60
|
+
* the same set from the same templates. See `./bin-shim.ts`.
|
|
61
|
+
*/
|
|
62
|
+
export declare function writeBinEntry(opts: {
|
|
63
|
+
binDir: string;
|
|
64
|
+
binName: string;
|
|
65
|
+
targetAbs: string;
|
|
66
|
+
platform?: string;
|
|
67
|
+
}): void;
|
|
68
|
+
type Logger = (fmt: string, ...args: unknown[]) => void;
|
|
14
69
|
export {};
|
|
@@ -24,6 +24,7 @@ import { acquireInstallLock } from './install-lock.js';
|
|
|
24
24
|
import { getCachedTarball, getForeignCachedTarball, putCachedTarball, } from './install-tarball-cache.js';
|
|
25
25
|
import { getCachedPackument, putCachedPackument } from './install-packument-cache.js';
|
|
26
26
|
import { assertNativeBackendNodeVersion } from './node-version.js';
|
|
27
|
+
import { buildCmdShim, parseShebang } from './bin-shim.js';
|
|
27
28
|
// 16-wide download pool. Matched to the shared Soup.Session's lifted
|
|
28
29
|
// `max-conns-per-host` (see @gjsify/fetch `getSharedSession`) — a higher pool
|
|
29
30
|
// here only translates into real concurrency because that cap was raised from
|
|
@@ -121,6 +122,7 @@ async function installPackagesNativeLocked(opts, npmrc, log) {
|
|
|
121
122
|
log('install: downloading %d tarball(s)', nodes.length);
|
|
122
123
|
await downloadAndExtractAll(nodes, opts.prefix, npmrc, log, opts.signal, progress);
|
|
123
124
|
await linkBins(nodes, opts.prefix, log);
|
|
125
|
+
warnMissingNativeBuilds(nodes, opts.prefix, log);
|
|
124
126
|
log('install: done');
|
|
125
127
|
// Surface the top-level requested packages so callers can update
|
|
126
128
|
// package.json with the resolved version (mirrors `npm install --save`
|
|
@@ -132,6 +134,69 @@ function errMsg(err) {
|
|
|
132
134
|
return err.message;
|
|
133
135
|
return String(err);
|
|
134
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Warn about a native package that installed WITHOUT a usable binary.
|
|
139
|
+
*
|
|
140
|
+
* gjsify install is node-free (it runs under GJS) and does NOT run a package's
|
|
141
|
+
* `install`/`postinstall` lifecycle script — running e.g. `node-gyp rebuild` would
|
|
142
|
+
* need Node + a C++ toolchain, breaking that property (see the header's "out of
|
|
143
|
+
* scope"). Such a package works only if it SHIPS a prebuild for the platform (the
|
|
144
|
+
* sanctioned path — `prebuilds/<platform>-<arch>/…`) or is later built by hand. When
|
|
145
|
+
* neither is present the package silently loads no binary (the `@gjsify/node-gi
|
|
146
|
+
* 0.21.0` case: installed, but `node_gi.node` absent → the consumer hit a runtime
|
|
147
|
+
* failure with no hint). Surface it at install time with an actionable line. Pure
|
|
148
|
+
* detection — no script is run, so node-free-ness is preserved.
|
|
149
|
+
*/
|
|
150
|
+
export function warnMissingNativeBuilds(nodes, prefix, log) {
|
|
151
|
+
const plat = process.platform;
|
|
152
|
+
const arch = process.arch;
|
|
153
|
+
const buildScriptKeys = ['preinstall', 'install', 'postinstall'];
|
|
154
|
+
for (const n of nodes) {
|
|
155
|
+
const pkgDir = path.join(prefix, n.installPath);
|
|
156
|
+
let scripts;
|
|
157
|
+
try {
|
|
158
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'));
|
|
159
|
+
scripts = manifest?.scripts;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
continue; // no/unreadable manifest — nothing to assess
|
|
163
|
+
}
|
|
164
|
+
if (!scripts || typeof scripts !== 'object')
|
|
165
|
+
continue;
|
|
166
|
+
const scriptKey = buildScriptKeys.find((k) => typeof scripts[k] === 'string');
|
|
167
|
+
if (scriptKey === undefined)
|
|
168
|
+
continue; // no native build lifecycle script
|
|
169
|
+
// Does it ship a prebuild for THIS platform (any prebuilds/<plat>-…/ with a
|
|
170
|
+
// file), or carry a locally-built build/Release binary?
|
|
171
|
+
let hasBinary = false;
|
|
172
|
+
try {
|
|
173
|
+
const prebuildsDir = path.join(pkgDir, 'prebuilds');
|
|
174
|
+
for (const entry of fs.readdirSync(prebuildsDir)) {
|
|
175
|
+
if (entry.startsWith(`${plat}-`) && fs.readdirSync(path.join(prebuildsDir, entry)).length > 0) {
|
|
176
|
+
hasBinary = true;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
/* no prebuilds dir */
|
|
183
|
+
}
|
|
184
|
+
if (!hasBinary) {
|
|
185
|
+
try {
|
|
186
|
+
const releaseDir = path.join(pkgDir, 'build', 'Release');
|
|
187
|
+
hasBinary = fs.readdirSync(releaseDir).some((f) => f.endsWith('.node') || f.endsWith('.so'));
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
/* no build/Release */
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (!hasBinary) {
|
|
194
|
+
log('install: WARNING — %s declares a native `%s` build script but ships no %s-%s prebuild, ' +
|
|
195
|
+
'and gjsify install does not run build scripts (node-free). It will not load until built: ' +
|
|
196
|
+
'run `node-gyp rebuild` in %s, or use a version that ships a %s-%s prebuild.', n.name, scriptKey, plat, arch, n.installPath, plat, arch);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
135
200
|
function topLevelResolutions(specs, nodes) {
|
|
136
201
|
// Top-level installs live at `node_modules/<name>` (no nesting). Build
|
|
137
202
|
// a name → root-node lookup limited to the top-level set.
|
|
@@ -918,7 +983,11 @@ async function extractOne(node, prefix, npmrc, log, signal) {
|
|
|
918
983
|
// so a read-only HOME / out-of-disk cache volume doesn't break the install.
|
|
919
984
|
putCachedTarball(node.integrity, bytes);
|
|
920
985
|
}
|
|
921
|
-
|
|
986
|
+
// `rmWithRetry` (not a bare rmSync): on Windows the delete of a freshly
|
|
987
|
+
// extracted tree fails with EBUSY/EPERM while any handle is still open, and
|
|
988
|
+
// stays pending afterwards — so the mkdirSync on the next line would hit
|
|
989
|
+
// EPERM/ENOTEMPTY. No-op cost on POSIX. This runs for EVERY package.
|
|
990
|
+
rmWithRetry(dest);
|
|
922
991
|
fs.mkdirSync(dest, { recursive: true });
|
|
923
992
|
await extractWithStallGuard(bytes, dest, node, signal);
|
|
924
993
|
return false;
|
|
@@ -1050,6 +1119,73 @@ function depth(installPath) {
|
|
|
1050
1119
|
// `node_modules/foo` = 1, `node_modules/foo/node_modules/bar` = 2, etc.
|
|
1051
1120
|
return installPath.split('/node_modules/').length;
|
|
1052
1121
|
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Read a bin file's `#!` line, if it has one. Returns `null` for an
|
|
1124
|
+
* unreadable/empty file or a first line that is not a shebang — the same
|
|
1125
|
+
* fallbacks npm's cmd-shim applies (it then invokes the target directly).
|
|
1126
|
+
*/
|
|
1127
|
+
function readShebang(file) {
|
|
1128
|
+
let head;
|
|
1129
|
+
try {
|
|
1130
|
+
head = fs.readFileSync(file, 'utf-8');
|
|
1131
|
+
}
|
|
1132
|
+
catch {
|
|
1133
|
+
return null;
|
|
1134
|
+
}
|
|
1135
|
+
const firstLine = head.trimStart().split(/\r*\n/, 1)[0] ?? '';
|
|
1136
|
+
return parseShebang(firstLine);
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Materialise ONE `node_modules/.bin/<name>` entry for `targetAbs`.
|
|
1140
|
+
*
|
|
1141
|
+
* Exported (and platform-injectable) so the Windows branch is unit-tested from
|
|
1142
|
+
* a Linux host — the file layout and shim contents are asserted there; only the
|
|
1143
|
+
* `symlinkSync`/`CreateSymbolicLink` syscall behaviour itself is OS-bound.
|
|
1144
|
+
*
|
|
1145
|
+
* POSIX: a relative symlink, with a copy as the fallback (some filesystems and
|
|
1146
|
+
* container overlays reject symlinks).
|
|
1147
|
+
*
|
|
1148
|
+
* Windows: `.bin/<name>` is not on `PATHEXT` and Windows has no shebang
|
|
1149
|
+
* handling, so a symlink OR a copy of a `#!/usr/bin/env node` script is
|
|
1150
|
+
* unrunnable — and a *file* symlink additionally needs elevation or Developer
|
|
1151
|
+
* Mode, so the previous code reliably fell into its copy fallback and produced
|
|
1152
|
+
* a dead entry. npm solves this with three sibling files (`<name>` for
|
|
1153
|
+
* git-bash/MSYS/WSL, `<name>.cmd` for cmd.exe, `<name>.ps1` for pwsh); we write
|
|
1154
|
+
* the same set from the same templates. See `./bin-shim.ts`.
|
|
1155
|
+
*/
|
|
1156
|
+
export function writeBinEntry(opts) {
|
|
1157
|
+
const { binDir, binName, targetAbs } = opts;
|
|
1158
|
+
const platform = opts.platform ?? process.platform;
|
|
1159
|
+
const linkPath = path.join(binDir, binName);
|
|
1160
|
+
if (platform === 'win32') {
|
|
1161
|
+
const relPosix = path.relative(binDir, targetAbs).split('\\').join('/');
|
|
1162
|
+
const { sh, cmd, ps1 } = buildCmdShim(relPosix, readShebang(targetAbs));
|
|
1163
|
+
for (const [file, contents] of [
|
|
1164
|
+
[linkPath, sh],
|
|
1165
|
+
[`${linkPath}.cmd`, cmd],
|
|
1166
|
+
[`${linkPath}.ps1`, ps1],
|
|
1167
|
+
]) {
|
|
1168
|
+
rmWithRetry(file);
|
|
1169
|
+
fs.writeFileSync(file, contents);
|
|
1170
|
+
try {
|
|
1171
|
+
fs.chmodSync(file, 0o755);
|
|
1172
|
+
}
|
|
1173
|
+
catch {
|
|
1174
|
+
/* inert on Windows; harmless under WSL/MSYS */
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
rmWithRetry(linkPath);
|
|
1180
|
+
const rel = path.relative(binDir, targetAbs);
|
|
1181
|
+
try {
|
|
1182
|
+
fs.symlinkSync(rel, linkPath);
|
|
1183
|
+
}
|
|
1184
|
+
catch {
|
|
1185
|
+
fs.copyFileSync(targetAbs, linkPath);
|
|
1186
|
+
fs.chmodSync(linkPath, 0o755);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1053
1189
|
async function linkBins(nodes, prefix, log) {
|
|
1054
1190
|
// Only root-level packages publish bins into the top-level
|
|
1055
1191
|
// `node_modules/.bin/`. Nested-package bins are addressable by their
|
|
@@ -1077,23 +1213,26 @@ async function linkBins(nodes, prefix, log) {
|
|
|
1077
1213
|
catch {
|
|
1078
1214
|
/* best effort */
|
|
1079
1215
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
const rel = path.relative(binDir, targetAbs);
|
|
1083
|
-
try {
|
|
1084
|
-
fs.symlinkSync(rel, linkPath);
|
|
1085
|
-
created++;
|
|
1086
|
-
}
|
|
1087
|
-
catch {
|
|
1088
|
-
fs.copyFileSync(targetAbs, linkPath);
|
|
1089
|
-
fs.chmodSync(linkPath, 0o755);
|
|
1090
|
-
created++;
|
|
1091
|
-
}
|
|
1216
|
+
writeBinEntry({ binDir, binName, targetAbs });
|
|
1217
|
+
created++;
|
|
1092
1218
|
}
|
|
1093
1219
|
}
|
|
1094
1220
|
if (created > 0)
|
|
1095
1221
|
log('bin: linked %d entry(ies) under .bin/', created);
|
|
1096
1222
|
}
|
|
1223
|
+
/**
|
|
1224
|
+
* `fs.rmSync(..., { force: true })` with the Windows retry npm/rimraf apply.
|
|
1225
|
+
*
|
|
1226
|
+
* On Windows a delete fails with `EBUSY`/`EPERM` while any process holds a
|
|
1227
|
+
* handle (antivirus scan, an editor, a still-terminating child), and the
|
|
1228
|
+
* deletion stays *pending* until the last handle closes — so an immediately
|
|
1229
|
+
* following create can hit `EPERM`/`ENOTEMPTY`. Node implements the retry loop
|
|
1230
|
+
* for us behind `maxRetries`; it is a no-op on POSIX, where the first attempt
|
|
1231
|
+
* always succeeds.
|
|
1232
|
+
*/
|
|
1233
|
+
function rmWithRetry(target) {
|
|
1234
|
+
fs.rmSync(target, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
1235
|
+
}
|
|
1097
1236
|
function normalizeBin(pkgName, bin) {
|
|
1098
1237
|
const out = new Map();
|
|
1099
1238
|
if (typeof bin === 'string') {
|
|
@@ -86,6 +86,22 @@ export interface LinkedBin {
|
|
|
86
86
|
* simplicity and so we don't need to discriminate by runtime here.
|
|
87
87
|
*/
|
|
88
88
|
export declare function linkGlobalBins(packageNames: string[], layout: GlobalLayout): LinkedBin[];
|
|
89
|
+
/**
|
|
90
|
+
* Build the POSIX-sh `export` lines that prepend the given prebuild
|
|
91
|
+
* directories to GI_TYPELIB_PATH and the host's shared-library search path.
|
|
92
|
+
* Any pre-existing value inherited from the user's environment is preserved as
|
|
93
|
+
* a suffix so user-installed typelibs/libraries still resolve.
|
|
94
|
+
*
|
|
95
|
+
* The library variable is host-dependent — `LD_LIBRARY_PATH` on Linux,
|
|
96
|
+
* `DYLD_LIBRARY_PATH` on macOS (dyld never reads the ELF one), so a launcher
|
|
97
|
+
* written on a Mac used to export a variable nothing consults, leaving every
|
|
98
|
+
* `darwin-arm64` prebuild unloadable. `platform` is a parameter so both
|
|
99
|
+
* branches are unit-testable from either host.
|
|
100
|
+
*
|
|
101
|
+
* Returns the empty string when no prebuilds were found — avoids emitting an
|
|
102
|
+
* inert assignment in the launcher.
|
|
103
|
+
*/
|
|
104
|
+
export declare function buildLauncherEnvPreamble(prebuildsDirs: string[], platform?: string): string;
|
|
89
105
|
/** Returns `true` if `binDir` is on the user's PATH. */
|
|
90
106
|
export declare function binDirOnPath(binDir: string): boolean;
|
|
91
107
|
/**
|