@gjsify/cli 0.27.1 → 0.29.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 +69 -61
- package/lib/commands/debug.d.ts +1 -0
- package/lib/commands/debug.js +20 -7
- package/lib/commands/format.js +17 -8
- package/lib/commands/install.d.ts +27 -0
- package/lib/commands/install.js +25 -8
- package/lib/commands/system-check.js +22 -2
- package/lib/utils/bin-shim.d.ts +26 -0
- package/lib/utils/bin-shim.js +26 -0
- package/lib/utils/check-system-deps.d.ts +34 -0
- package/lib/utils/check-system-deps.js +121 -1
- package/lib/utils/detect-native-packages.d.ts +45 -1
- package/lib/utils/detect-native-packages.js +56 -0
- package/lib/utils/install-backend-native.d.ts +79 -1
- package/lib/utils/install-backend-native.js +139 -32
- package/lib/utils/system-gi.d.ts +71 -0
- package/lib/utils/system-gi.js +221 -0
- package/package.json +17 -17
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import { readdirSync, existsSync } from 'node:fs';
|
|
30
30
|
import { join, resolve } from 'node:path';
|
|
31
31
|
import { readPackageJson } from './pkg-json.js';
|
|
32
|
+
import { splitSearchPath, systemGiLibraryDirs } from './system-gi.js';
|
|
32
33
|
/**
|
|
33
34
|
* Map a Node `process.arch` value to the uname-style spelling the Vala/meson
|
|
34
35
|
* bridges used to stage into (`prebuilds/linux-x86_64/`, the output of
|
|
@@ -558,6 +559,18 @@ export function libraryPathVar(platform) {
|
|
|
558
559
|
return { name: 'PATH', separator: ';' };
|
|
559
560
|
return { name: 'LD_LIBRARY_PATH', separator: ':' };
|
|
560
561
|
}
|
|
562
|
+
/**
|
|
563
|
+
* dyld's *fallback* search path — consulted only AFTER the normal search
|
|
564
|
+
* (rpath / absolute name / dyld cache) has failed.
|
|
565
|
+
*
|
|
566
|
+
* Deliberately NOT part of {@link libraryPathVar}, which answers a different
|
|
567
|
+
* question: that one names the variable that OVERRIDES resolution for every
|
|
568
|
+
* dylib in the process (the right home for a prebuild we ship and want used),
|
|
569
|
+
* this one names the variable that fills in what nothing else could find (the
|
|
570
|
+
* right home for the host's own libdirs). Mixing the two roles is the mistake
|
|
571
|
+
* `buildNativeEnv` documents at length.
|
|
572
|
+
*/
|
|
573
|
+
const DYLD_FALLBACK_VAR = 'DYLD_FALLBACK_LIBRARY_PATH';
|
|
561
574
|
/**
|
|
562
575
|
* Build the typelib + shared-library search-path environment for the detected
|
|
563
576
|
* native packages. Prepends the new paths to any existing values.
|
|
@@ -569,6 +582,40 @@ export function libraryPathVar(platform) {
|
|
|
569
582
|
* (a stock Windows env block uses `Path`) — Windows env names are
|
|
570
583
|
* case-insensitive, but a plain JS object is not, and `{...process.env,
|
|
571
584
|
* PATH: …}` would hand a child process two competing entries.
|
|
585
|
+
*
|
|
586
|
+
* ── DARWIN ALSO GETS THE *HOST'S* GI LIBDIRS ─────────────────────────────────
|
|
587
|
+
*
|
|
588
|
+
* Everything above is about the prebuilds gjsify itself ships. On macOS there is
|
|
589
|
+
* a second, independent gap that has nothing to do with gjsify: a typelib names
|
|
590
|
+
* its library by BARE LEAF (`libgtk-4.1.dylib`), and dyld carries neither
|
|
591
|
+
* `/usr/local/lib` nor `/opt/homebrew/lib` in its default fallback path.
|
|
592
|
+
* Homebrew's `gjs` has an rpath into GLIB's keg only, so it resolves no other
|
|
593
|
+
* keg's leaves — measured: bare `gjs -c "imports.gi.Gtk; Gtk.init()"`, no gjsify
|
|
594
|
+
* in the process, dies on that dlopen (full trace in `utils/system-gi.ts`). This
|
|
595
|
+
* is the ONE place `gjsify run`, `showcase`, `storybook`, `tsc` and `info` all
|
|
596
|
+
* pass through, so it is where the child's launch env is repaired.
|
|
597
|
+
*
|
|
598
|
+
* Two properties are load-bearing:
|
|
599
|
+
*
|
|
600
|
+
* 1. **`DYLD_FALLBACK_LIBRARY_PATH`, never `DYLD_LIBRARY_PATH`.** The override
|
|
601
|
+
* variable wins by LEAF for every dylib in the process, including ones the
|
|
602
|
+
* host already resolved correctly — pointing it at a whole system libdir is
|
|
603
|
+
* how you replace a linked-against library with a same-named different one.
|
|
604
|
+
* The fallback is consulted only after the normal search fails, which is
|
|
605
|
+
* exactly the "nothing else found it" case. Consequence, and it is the
|
|
606
|
+
* intended precedence: gjsify's own prebuilds keep priority through the
|
|
607
|
+
* override variable, the system stack fills in behind it.
|
|
608
|
+
* 2. **Our dirs go FIRST, an inherited fallback is kept BEHIND them, and
|
|
609
|
+
* `/usr/lib` is appended when there is none.** Setting the variable
|
|
610
|
+
* REPLACES dyld's own default fallback list, so writing only our dirs would
|
|
611
|
+
* silently remove `/usr/lib` from the search — a host that exported the
|
|
612
|
+
* variable itself already carries whatever tail it wants. Same composition
|
|
613
|
+
* as node-gi's re-exec.
|
|
614
|
+
*
|
|
615
|
+
* Emitted even when `packages` is empty: the gap is in the host's loader, not in
|
|
616
|
+
* a gjsify prebuild, so a plain `gjsify run script.gjs.js` that touches GTK needs
|
|
617
|
+
* it just as much. Empty off darwin — `ld.so`'s configured cache already resolves
|
|
618
|
+
* these leaves on Linux, and Windows re-reads `PATH` per `LoadLibrary`.
|
|
572
619
|
*/
|
|
573
620
|
export function buildNativeEnv(packages, opts = {}) {
|
|
574
621
|
const platform = opts.platform ?? process.platform;
|
|
@@ -588,5 +635,14 @@ export function buildNativeEnv(packages, opts = {}) {
|
|
|
588
635
|
GI_TYPELIB_PATH: prepend(env['GI_TYPELIB_PATH'], giSeparator),
|
|
589
636
|
};
|
|
590
637
|
out[libVarKey] = prepend(env[libVarKey], libSeparator);
|
|
638
|
+
// The host's own GI libdirs — see the block comment above. `systemGiLibraryDirs`
|
|
639
|
+
// is itself `[]` off darwin, so this is a no-op elsewhere without a second
|
|
640
|
+
// platform test; the variable is only WRITTEN when there is something to put in
|
|
641
|
+
// it, so a Mac with no GI stack installed is left byte-unchanged.
|
|
642
|
+
const systemDirs = (opts.systemGiDirs ?? systemGiLibraryDirs)({ platform, env });
|
|
643
|
+
if (systemDirs.length > 0) {
|
|
644
|
+
const inherited = splitSearchPath(env[DYLD_FALLBACK_VAR]);
|
|
645
|
+
out[DYLD_FALLBACK_VAR] = [...systemDirs, ...(inherited.length > 0 ? inherited : ['/usr/lib'])].join(':');
|
|
646
|
+
}
|
|
591
647
|
return out;
|
|
592
648
|
}
|
|
@@ -5,7 +5,13 @@ interface ParsedSpec {
|
|
|
5
5
|
name: string;
|
|
6
6
|
range: string;
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* One placed package in the resolved tree. Exported as a TYPE only: it is the
|
|
10
|
+
* graph shape {@link computeOptionalFlags} and {@link applyPlatformFilter} take,
|
|
11
|
+
* so a spec can inject a tree by hand instead of driving a whole install to reach
|
|
12
|
+
* them. Nothing outside this module constructs one for real.
|
|
13
|
+
*/
|
|
14
|
+
export interface ResolvedNode {
|
|
9
15
|
/** Package name (e.g. `@gjsify/cli`, `lodash`). */
|
|
10
16
|
name: string;
|
|
11
17
|
version: string;
|
|
@@ -133,6 +139,78 @@ export declare function applyPlatformFilter(nodes: ResolvedNode[], target: Platf
|
|
|
133
139
|
* detection — no script is run, so node-free-ness is preserved.
|
|
134
140
|
*/
|
|
135
141
|
export declare function warnMissingNativeBuilds(nodes: ResolvedNode[], prefix: string, log: Logger): void;
|
|
142
|
+
/**
|
|
143
|
+
* A node's REQUIRED dependency edges: its `dependencies`, minus every name its
|
|
144
|
+
* `optionalDependencies` also lists. The one place that decides what kind an edge
|
|
145
|
+
* is — both the resolve walk and the optionality fixpoint read it, so the two
|
|
146
|
+
* cannot drift.
|
|
147
|
+
*
|
|
148
|
+
* THE SUBTRACTION IS npm's RULE, not a policy of ours: "entries in
|
|
149
|
+
* optionalDependencies will override entries of the same name in dependencies, so
|
|
150
|
+
* it's usually best to only put in one place" (npm's package.json docs). Measured
|
|
151
|
+
* on `@parcel/rust@2.16.4`, which lists all eight per-platform napi packages in
|
|
152
|
+
* both blocks: `npm install` exits 0, installs `@parcel/rust-linux-x64-gnu` only,
|
|
153
|
+
* and writes `"optional": true` for all eight in its `package-lock.json`.
|
|
154
|
+
*
|
|
155
|
+
* WHY IT IS NOT MERELY A DEDUP: the kind decides what an incompatible
|
|
156
|
+
* `os`/`cpu`/`libc` MEANS (`applyPlatformFilter` — fatal vs. inert) and whether a
|
|
157
|
+
* failed resolve is tolerated. Reading `dependencies` alone therefore does not
|
|
158
|
+
* just visit a name twice, it promotes a foreign-platform binary to a required
|
|
159
|
+
* dependency and fails installs npm completes.
|
|
160
|
+
*
|
|
161
|
+
* The inverse direction is deliberately NOT symmetric: a name in
|
|
162
|
+
* `optionalDependencies` alone is optional (no `dependencies` entry to override),
|
|
163
|
+
* which is what {@link resolveDeps}'s second queue loop covers.
|
|
164
|
+
*/
|
|
165
|
+
export declare function requiredDepEntries(node: Pick<ResolvedNode, 'dependencies' | 'optionalDependencies'>): [string, string][];
|
|
166
|
+
/**
|
|
167
|
+
* Recompute every node's `optional` flag as a FIXPOINT over the placed graph.
|
|
168
|
+
* The only writer of the final flag; whatever the nodes arrive carrying is an
|
|
169
|
+
* input nothing checked (a BFS forward guess on the resolve path, a value read
|
|
170
|
+
* back out of a file on the lockfile path) and is overwritten unconditionally.
|
|
171
|
+
*
|
|
172
|
+
* DEFINITION: a node is REQUIRED iff it is reachable from `requiredNames`
|
|
173
|
+
* through REQUIRED edges alone ({@link requiredDepEntries} — `dependencies` minus
|
|
174
|
+
* the names `optionalDependencies` overrides); every other node is optional.
|
|
175
|
+
* Optionality is therefore INHERITED — a plain dependency OF an optional package
|
|
176
|
+
* is still optional — which is what npm's `optionalSet` computes.
|
|
177
|
+
*
|
|
178
|
+
* WHY A FIXPOINT AND NOT A ONE-SHOT PROMOTION AT THE REUSE SITE (the reviewed
|
|
179
|
+
* defect): the walk queues a node's own dep edges at the moment it is placed,
|
|
180
|
+
* carrying the optionality it had THEN. Promoting the node later leaves its
|
|
181
|
+
* children behind, so the answer depends on which edge BFS happened to traverse
|
|
182
|
+
* first — a genuinely required transitive dep reached first through an optional
|
|
183
|
+
* edge stayed flagged optional, was persisted that way, and was then silently
|
|
184
|
+
* skipped by the platform filter on every machine instead of raising
|
|
185
|
+
* EBADPLATFORM. A monotone worklist over the FINISHED graph cannot have that
|
|
186
|
+
* property: "required" only ever spreads, so the result is independent of visit
|
|
187
|
+
* order. npm reaches for the same shape and says so —
|
|
188
|
+
* refs/npm-cli/workspaces/arborist/lib/calc-dep-flags.js: "If a node is changed,
|
|
189
|
+
* we add to the queue and continue until no more changes."
|
|
190
|
+
*
|
|
191
|
+
* PATH-INDEPENDENCE IS THE OTHER REQUIREMENT, and it is why the lockfile carries
|
|
192
|
+
* an `optionalDependencies` map per entry (v4). The walk USED to read
|
|
193
|
+
* `dependencies` only, for exactly that reason — a lockfile entry recorded nothing
|
|
194
|
+
* else, so consulting `node.optionalDependencies` would have made the two paths
|
|
195
|
+
* compute different flags for the same tree. The premise was fixable and the
|
|
196
|
+
* consequence was not: a publisher listing the same name in BOTH blocks means
|
|
197
|
+
* "optional" to npm, came out REQUIRED here, and the shape is not rare — it is
|
|
198
|
+
* `@parcel/rust@2.16.4` (all eight per-platform napi packages in both blocks), so
|
|
199
|
+
* every Linux install of any tree containing parcel failed with EBADPLATFORM on
|
|
200
|
+
* `@parcel/rust-darwin-x64` while `npm install` thinned the same tree and exited
|
|
201
|
+
* 0. "Erring toward REQUIRED errs toward the loud failure" is true and was the
|
|
202
|
+
* wrong trade: the loud failure was not a real one, and no `--force` lifts it
|
|
203
|
+
* (nor should it). So the declaration is persisted and both paths subtract it.
|
|
204
|
+
* Reading a pre-v4 lockfile leaves the map empty, which reproduces the old
|
|
205
|
+
* behaviour for that file alone — see READABLE_LOCKFILE_VERSIONS.
|
|
206
|
+
*
|
|
207
|
+
* Runs before `writeLockfile` so the persisted flag is the final one, and before
|
|
208
|
+
* `applyPlatformFilter` so the fatal-vs-inert decision reads it rather than the
|
|
209
|
+
* guess. Exported so the unit spec can drive it with INJECTED graphs: the whole
|
|
210
|
+
* fatal-vs-inert question is decided by these two functions in composition, and
|
|
211
|
+
* that is worth pinning without a registry.
|
|
212
|
+
*/
|
|
213
|
+
export declare function computeOptionalFlags(nodes: ResolvedNode[], requiredNames: Set<string>, log: Logger): void;
|
|
136
214
|
export declare function parseSpec(raw: string): ParsedSpec;
|
|
137
215
|
export declare function pickVersion(packument: Packument, range: string, preferred?: string): string | null;
|
|
138
216
|
/**
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
// reported. So the DECLARATIONS are locked and the VERDICT is recomputed per
|
|
23
23
|
// host at install time (see applyPlatformFilter).
|
|
24
24
|
//
|
|
25
|
-
//
|
|
26
|
-
// documented, because the first draft of this feature broke
|
|
27
|
-
// as if it did not:
|
|
25
|
+
// THREE INVARIANTS MAKE THAT CLAIM TRUE, and each is structural rather than
|
|
26
|
+
// documented, because the first draft of this feature broke the first two while
|
|
27
|
+
// reading as if it did not — and shipped without the third:
|
|
28
28
|
//
|
|
29
29
|
// 1. THE RESOLVE IS TARGET-BLIND. `resolveDeps` is not given the platform
|
|
30
30
|
// target at all — it cannot be, so it cannot record a target-dependent
|
|
@@ -47,6 +47,31 @@
|
|
|
47
47
|
// fixpoint for the same reason (refs/npm-cli/workspaces/arborist/lib/
|
|
48
48
|
// calc-dep-flags.js: "If a node is changed, we add to the queue and
|
|
49
49
|
// continue until no more changes").
|
|
50
|
+
// 3. OPTIONALITY BELONGS TO THE EDGE, NOT TO THE BLOCK THE NAME WAS READ FROM.
|
|
51
|
+
// A name listed in BOTH `dependencies` and `optionalDependencies` is
|
|
52
|
+
// OPTIONAL: "entries in optionalDependencies will override entries of the
|
|
53
|
+
// same name in dependencies" (npm's package.json docs), which is why every
|
|
54
|
+
// walk over a node's dep edges here goes through {@link requiredDepEntries}
|
|
55
|
+
// instead of `Object.keys(node.dependencies)`. `@parcel/rust@2.16.4` is why
|
|
56
|
+
// it is written down: it declares all eight of its per-platform napi
|
|
57
|
+
// packages in both blocks, so a `dependencies`-only walk made
|
|
58
|
+
// `@parcel/rust-darwin-x64` REQUIRED and every Linux install of any tree
|
|
59
|
+
// containing parcel died with EBADPLATFORM — while `npm install` on the same
|
|
60
|
+
// manifest exits 0, installs
|
|
61
|
+
// `@parcel/rust-linux-x64-gnu`, and records the other seven as
|
|
62
|
+
// `"optional": true` in its own lockfile. The rule was already applied to
|
|
63
|
+
// swallowed resolve failures (`assertRequiredEdgesResolved`: "optionality
|
|
64
|
+
// lives on the edge, not on its endpoints") and missing from the two walks
|
|
65
|
+
// that decide fatal-vs-inert, which is the worst place to have half of it.
|
|
66
|
+
// It costs a lockfile field (`optionalDependencies` per entry, v4): without
|
|
67
|
+
// it the fresh-resolve path and the lockfile path would disagree about the
|
|
68
|
+
// same tree, which is the split invariant 2 exists to close.
|
|
69
|
+
// WHY THE DEFECT LOOKED LIKE IT DID NOT EXIST: the same napi shape is
|
|
70
|
+
// overwhelmingly published in ONE block — measured on the latest of `oxlint`,
|
|
71
|
+
// `esbuild` and `rollup`, all three list their per-platform bindings in
|
|
72
|
+
// `optionalDependencies` only (zero overlap), so they installed correctly
|
|
73
|
+
// throughout and every mock corpus reaches for that shape too. The block a
|
|
74
|
+
// publisher chooses is not a property this installer may assume.
|
|
50
75
|
//
|
|
51
76
|
// Out of scope (still deferred): peerDependencies validation,
|
|
52
77
|
// lifecycle scripts, git/file specs.
|
|
@@ -74,9 +99,12 @@ const LOCKFILE_NAME = 'gjsify-lock.json';
|
|
|
74
99
|
/**
|
|
75
100
|
* v3 adds the per-entry platform declaration (`os`/`cpu`/`libc`) plus
|
|
76
101
|
* `optional`, which together let a foreign-platform package be recorded without
|
|
77
|
-
* being installed.
|
|
102
|
+
* being installed. v4 adds the per-entry `optionalDependencies` map, without
|
|
103
|
+
* which the optionality fixpoint cannot see WHICH of an entry's edges are
|
|
104
|
+
* optional and a name declared in both blocks comes out required (header note,
|
|
105
|
+
* invariant 3). Older lockfiles are still READ (see {@link readLockfile}).
|
|
78
106
|
*/
|
|
79
|
-
const LOCKFILE_VERSION =
|
|
107
|
+
const LOCKFILE_VERSION = 4;
|
|
80
108
|
/**
|
|
81
109
|
* Lockfile versions this CLI can read. A v2 lockfile is a valid pin set that
|
|
82
110
|
* simply predates the platform fields; rejecting it (the old
|
|
@@ -84,8 +112,19 @@ const LOCKFILE_VERSION = 3;
|
|
|
84
112
|
* the following fresh resolve bump every `^`-range to the newest in-range
|
|
85
113
|
* version — the exact silent churn lockfile preservation exists to prevent, on
|
|
86
114
|
* every user's first install after upgrading the CLI.
|
|
115
|
+
*
|
|
116
|
+
* READING an older file is not the same as TRUSTING it for everything: only the
|
|
117
|
+
* current version short-circuits the resolve (see the branch below), so a v2/v3
|
|
118
|
+
* file seeds version preservation and is then rewritten. `--immutable` is the one
|
|
119
|
+
* path that must consume what it was handed verbatim, so there — and only there —
|
|
120
|
+
* a pre-v4 file keeps its pre-v4 edge fidelity: a both-blocks optional dep is
|
|
121
|
+
* judged required, exactly as before this fix. That is a strictly unchanged
|
|
122
|
+
* outcome rather than a new failure, and one plain `gjsify install` upgrades the
|
|
123
|
+
* file. Failing `--immutable` on an old-but-readable lockfile instead would break
|
|
124
|
+
* every CI whose committed file predates this CLI, including this repo's own
|
|
125
|
+
* (still v2).
|
|
87
126
|
*/
|
|
88
|
-
const READABLE_LOCKFILE_VERSIONS = new Set([2, LOCKFILE_VERSION]);
|
|
127
|
+
const READABLE_LOCKFILE_VERSIONS = new Set([2, 3, LOCKFILE_VERSION]);
|
|
89
128
|
export async function installPackagesNative(opts) {
|
|
90
129
|
// Fail clearly on an unsupported Node major BEFORE touching the ABI-locked
|
|
91
130
|
// native deps — otherwise they SIGSEGV mid-extract with no actionable message.
|
|
@@ -161,11 +200,18 @@ async function installPackagesNativeLocked(opts, npmrc, log) {
|
|
|
161
200
|
else {
|
|
162
201
|
// A resolve has to run (new/changed/removed dep, or no lockfile yet).
|
|
163
202
|
//
|
|
164
|
-
// A pre-
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
203
|
+
// A pre-v4 lockfile lands here too, even when it matches the request,
|
|
204
|
+
// because each bump added something the verdict is recomputed FROM and a
|
|
205
|
+
// file that lacks it cannot be consumed verbatim: pre-v3 entries carry no
|
|
206
|
+
// platform declaration (so the foreign-platform tree would keep installing
|
|
207
|
+
// forever), pre-v4 entries carry no `optionalDependencies` map (so a name
|
|
208
|
+
// declared in both blocks looks required and its incompatible siblings
|
|
209
|
+
// fail the install — the `@parcel/rust` shape, header note invariant 3).
|
|
210
|
+
// One resolve — version-preserving, per the seeding below, so nothing
|
|
211
|
+
// bumps — upgrades the file. That matters beyond tidiness here: the
|
|
212
|
+
// lockfile is written BEFORE the platform filter runs, so the failing
|
|
213
|
+
// install left a v3 file behind that would otherwise reproduce its own
|
|
214
|
+
// EBADPLATFORM on every later run without ever resolving again.
|
|
169
215
|
//
|
|
170
216
|
// Unless --refresh-lockfile was passed, seed it with the versions
|
|
171
217
|
// already pinned in the existing lockfile so unchanged deps keep their
|
|
@@ -641,7 +687,13 @@ optionalSpecs) {
|
|
|
641
687
|
name: `${edge.name}@${version}`,
|
|
642
688
|
});
|
|
643
689
|
if (!skipDeps) {
|
|
644
|
-
|
|
690
|
+
// REQUIRED edges only — a name this package also lists in
|
|
691
|
+
// `optionalDependencies` is queued by the loop below instead,
|
|
692
|
+
// once, as optional (header note, invariant 3). Queuing it here
|
|
693
|
+
// as well would place it with a `required: true` forward guess,
|
|
694
|
+
// making a resolve failure fatal and (via the fixpoint) its
|
|
695
|
+
// incompatible platform siblings fatal too.
|
|
696
|
+
for (const [depName, depRange] of requiredDepEntries(node)) {
|
|
645
697
|
queue.push({
|
|
646
698
|
from: installPath,
|
|
647
699
|
name: depName,
|
|
@@ -764,6 +816,38 @@ function requiredTopLevelNames(specs, optionalSpecs) {
|
|
|
764
816
|
}
|
|
765
817
|
return names;
|
|
766
818
|
}
|
|
819
|
+
/**
|
|
820
|
+
* A node's REQUIRED dependency edges: its `dependencies`, minus every name its
|
|
821
|
+
* `optionalDependencies` also lists. The one place that decides what kind an edge
|
|
822
|
+
* is — both the resolve walk and the optionality fixpoint read it, so the two
|
|
823
|
+
* cannot drift.
|
|
824
|
+
*
|
|
825
|
+
* THE SUBTRACTION IS npm's RULE, not a policy of ours: "entries in
|
|
826
|
+
* optionalDependencies will override entries of the same name in dependencies, so
|
|
827
|
+
* it's usually best to only put in one place" (npm's package.json docs). Measured
|
|
828
|
+
* on `@parcel/rust@2.16.4`, which lists all eight per-platform napi packages in
|
|
829
|
+
* both blocks: `npm install` exits 0, installs `@parcel/rust-linux-x64-gnu` only,
|
|
830
|
+
* and writes `"optional": true` for all eight in its `package-lock.json`.
|
|
831
|
+
*
|
|
832
|
+
* WHY IT IS NOT MERELY A DEDUP: the kind decides what an incompatible
|
|
833
|
+
* `os`/`cpu`/`libc` MEANS (`applyPlatformFilter` — fatal vs. inert) and whether a
|
|
834
|
+
* failed resolve is tolerated. Reading `dependencies` alone therefore does not
|
|
835
|
+
* just visit a name twice, it promotes a foreign-platform binary to a required
|
|
836
|
+
* dependency and fails installs npm completes.
|
|
837
|
+
*
|
|
838
|
+
* The inverse direction is deliberately NOT symmetric: a name in
|
|
839
|
+
* `optionalDependencies` alone is optional (no `dependencies` entry to override),
|
|
840
|
+
* which is what {@link resolveDeps}'s second queue loop covers.
|
|
841
|
+
*/
|
|
842
|
+
export function requiredDepEntries(node) {
|
|
843
|
+
const entries = [];
|
|
844
|
+
for (const [name, range] of Object.entries(node.dependencies)) {
|
|
845
|
+
if (name in node.optionalDependencies)
|
|
846
|
+
continue;
|
|
847
|
+
entries.push([name, range]);
|
|
848
|
+
}
|
|
849
|
+
return entries;
|
|
850
|
+
}
|
|
767
851
|
/**
|
|
768
852
|
* Recompute every node's `optional` flag as a FIXPOINT over the placed graph.
|
|
769
853
|
* The only writer of the final flag; whatever the nodes arrive carrying is an
|
|
@@ -771,9 +855,10 @@ function requiredTopLevelNames(specs, optionalSpecs) {
|
|
|
771
855
|
* back out of a file on the lockfile path) and is overwritten unconditionally.
|
|
772
856
|
*
|
|
773
857
|
* DEFINITION: a node is REQUIRED iff it is reachable from `requiredNames`
|
|
774
|
-
* through
|
|
775
|
-
*
|
|
776
|
-
*
|
|
858
|
+
* through REQUIRED edges alone ({@link requiredDepEntries} — `dependencies` minus
|
|
859
|
+
* the names `optionalDependencies` overrides); every other node is optional.
|
|
860
|
+
* Optionality is therefore INHERITED — a plain dependency OF an optional package
|
|
861
|
+
* is still optional — which is what npm's `optionalSet` computes.
|
|
777
862
|
*
|
|
778
863
|
* WHY A FIXPOINT AND NOT A ONE-SHOT PROMOTION AT THE REUSE SITE (the reviewed
|
|
779
864
|
* defect): the walk queues a node's own dep edges at the moment it is placed,
|
|
@@ -788,23 +873,29 @@ function requiredTopLevelNames(specs, optionalSpecs) {
|
|
|
788
873
|
* refs/npm-cli/workspaces/arborist/lib/calc-dep-flags.js: "If a node is changed,
|
|
789
874
|
* we add to the queue and continue until no more changes."
|
|
790
875
|
*
|
|
791
|
-
* PATH-INDEPENDENCE IS THE OTHER REQUIREMENT, and it is why the
|
|
792
|
-
* `
|
|
793
|
-
*
|
|
794
|
-
* `node.optionalDependencies`
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
*
|
|
801
|
-
*
|
|
876
|
+
* PATH-INDEPENDENCE IS THE OTHER REQUIREMENT, and it is why the lockfile carries
|
|
877
|
+
* an `optionalDependencies` map per entry (v4). The walk USED to read
|
|
878
|
+
* `dependencies` only, for exactly that reason — a lockfile entry recorded nothing
|
|
879
|
+
* else, so consulting `node.optionalDependencies` would have made the two paths
|
|
880
|
+
* compute different flags for the same tree. The premise was fixable and the
|
|
881
|
+
* consequence was not: a publisher listing the same name in BOTH blocks means
|
|
882
|
+
* "optional" to npm, came out REQUIRED here, and the shape is not rare — it is
|
|
883
|
+
* `@parcel/rust@2.16.4` (all eight per-platform napi packages in both blocks), so
|
|
884
|
+
* every Linux install of any tree containing parcel failed with EBADPLATFORM on
|
|
885
|
+
* `@parcel/rust-darwin-x64` while `npm install` thinned the same tree and exited
|
|
886
|
+
* 0. "Erring toward REQUIRED errs toward the loud failure" is true and was the
|
|
887
|
+
* wrong trade: the loud failure was not a real one, and no `--force` lifts it
|
|
888
|
+
* (nor should it). So the declaration is persisted and both paths subtract it.
|
|
889
|
+
* Reading a pre-v4 lockfile leaves the map empty, which reproduces the old
|
|
890
|
+
* behaviour for that file alone — see READABLE_LOCKFILE_VERSIONS.
|
|
802
891
|
*
|
|
803
892
|
* Runs before `writeLockfile` so the persisted flag is the final one, and before
|
|
804
893
|
* `applyPlatformFilter` so the fatal-vs-inert decision reads it rather than the
|
|
805
|
-
* guess.
|
|
894
|
+
* guess. Exported so the unit spec can drive it with INJECTED graphs: the whole
|
|
895
|
+
* fatal-vs-inert question is decided by these two functions in composition, and
|
|
896
|
+
* that is worth pinning without a registry.
|
|
806
897
|
*/
|
|
807
|
-
function computeOptionalFlags(nodes, requiredNames, log) {
|
|
898
|
+
export function computeOptionalFlags(nodes, requiredNames, log) {
|
|
808
899
|
const byPath = new Map();
|
|
809
900
|
for (const node of nodes)
|
|
810
901
|
byPath.set(node.installPath, node);
|
|
@@ -833,7 +924,7 @@ function computeOptionalFlags(nodes, requiredNames, log) {
|
|
|
833
924
|
const node = byPath.get(installPath);
|
|
834
925
|
if (!node)
|
|
835
926
|
continue;
|
|
836
|
-
for (const depName of
|
|
927
|
+
for (const [depName] of requiredDepEntries(node)) {
|
|
837
928
|
// Resolve the edge the way the REQUESTER will at runtime — through
|
|
838
929
|
// the ancestor `node_modules` chain — so a nested copy is credited
|
|
839
930
|
// to the requester that nested it and the hoisted one is not
|
|
@@ -904,8 +995,14 @@ function assertRequiredEdgesResolved(nodes, skippedEdges) {
|
|
|
904
995
|
continue;
|
|
905
996
|
// A REQUIRED package's own optionalDependency stays optional whatever
|
|
906
997
|
// the package's flag says: optionality lives on the edge, not on its
|
|
907
|
-
// endpoints
|
|
908
|
-
//
|
|
998
|
+
// endpoints (header note, invariant 3 — the same rule
|
|
999
|
+
// {@link requiredDepEntries} applies to the two graph walks). This branch
|
|
1000
|
+
// is what keeps a darwin-only `fsevents` under a required `chokidar` from
|
|
1001
|
+
// failing every Linux install.
|
|
1002
|
+
//
|
|
1003
|
+
// Membership, not `requiredDepEntries`: the failed edge may name something
|
|
1004
|
+
// no longer in `dependencies` at all, and what is being asked here is
|
|
1005
|
+
// whether the requester declared THIS name optional.
|
|
909
1006
|
//
|
|
910
1007
|
// Safe on both paths because `skippedEdges` is only ever non-empty on
|
|
911
1008
|
// the fresh-resolve path, where `optionalDependencies` is populated; the
|
|
@@ -1137,6 +1234,11 @@ function writeLockfile(lockfilePath, specs, nodes) {
|
|
|
1137
1234
|
resolved: node.tarballUrl,
|
|
1138
1235
|
integrity: node.integrity,
|
|
1139
1236
|
dependencies: Object.keys(node.dependencies).length > 0 ? node.dependencies : undefined,
|
|
1237
|
+
// The EDGE KINDS, for the same reason the platform declaration is
|
|
1238
|
+
// here: `computeOptionalFlags` runs on the lockfile path too and
|
|
1239
|
+
// cannot recompute what the file does not carry (v4, header note
|
|
1240
|
+
// invariant 3). Omitted when empty, which is most packages.
|
|
1241
|
+
optionalDependencies: Object.keys(node.optionalDependencies).length > 0 ? node.optionalDependencies : undefined,
|
|
1140
1242
|
bin: node.bin,
|
|
1141
1243
|
// The DECLARATION, never the verdict: `inert` is per-host and
|
|
1142
1244
|
// deliberately absent from the file (a lockfile that recorded which
|
|
@@ -1208,7 +1310,12 @@ function lockfileToNodes(lockfile) {
|
|
|
1208
1310
|
integrity: entry.integrity,
|
|
1209
1311
|
installPath,
|
|
1210
1312
|
dependencies: entry.dependencies ?? {},
|
|
1211
|
-
|
|
1313
|
+
// Empty for a pre-v4 entry, which is why such a file never
|
|
1314
|
+
// short-circuits the resolve: with no edge kinds recorded, a name the
|
|
1315
|
+
// publisher put in both blocks reads as required here (the pre-fix
|
|
1316
|
+
// behaviour). `--immutable` is the one path that consumes it anyway —
|
|
1317
|
+
// see READABLE_LOCKFILE_VERSIONS.
|
|
1318
|
+
optionalDependencies: entry.optionalDependencies ?? {},
|
|
1212
1319
|
bin: entry.bin,
|
|
1213
1320
|
// Rebuilt from the recorded declaration so `applyPlatformFilter`
|
|
1214
1321
|
// reaches the SAME verdict on this path as on the fresh-resolve
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Injectable host facts — the darwin branch is testable from Linux. */
|
|
2
|
+
export interface SystemGiOptions {
|
|
3
|
+
/** `process.platform` value; defaults to the running process. */
|
|
4
|
+
platform?: string;
|
|
5
|
+
/** Environment to read `GI_TYPELIB_PATH` / `PKG_CONFIG_PATH` from; defaults to `process.env`. */
|
|
6
|
+
env?: Record<string, string | undefined>;
|
|
7
|
+
/** Directory-existence predicate; defaults to a `statSync` probe. */
|
|
8
|
+
existsDir?: (dir: string) => boolean;
|
|
9
|
+
/** `pkg-config`'s `.pc` search directories; defaults to {@link pkgConfigSearchDirs}. */
|
|
10
|
+
searchDirs?: (env: Record<string, string | undefined>) => string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Split an OS search-path variable, dropping empty segments.
|
|
14
|
+
*/
|
|
15
|
+
export declare function splitSearchPath(value: string | undefined, separator?: string): string[];
|
|
16
|
+
/**
|
|
17
|
+
* `pkg-config`'s OWN configured `.pc` search path plus `$PKG_CONFIG_PATH`. Every
|
|
18
|
+
* entry is a `<libdir>/pkgconfig` (or `<datadir>/pkgconfig`) directory, so its
|
|
19
|
+
* parent is a candidate libdir — which is how a prefix nobody hardcoded is
|
|
20
|
+
* found. Measured on the reported host: `pc_path` =
|
|
21
|
+
* `/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig:/usr/lib/pkgconfig:…`,
|
|
22
|
+
* whose first parent is exactly the `/usr/local/lib` that resolves every leaf.
|
|
23
|
+
*
|
|
24
|
+
* `spawnSync` reports a missing executable as `result.error`, not by throwing —
|
|
25
|
+
* an absent `pkg-config` is expected on a runtime-only GTK install, so it is a
|
|
26
|
+
* checked return value here and not an exception path. (`@gjsify/child_process`
|
|
27
|
+
* matches Node here deliberately; see its `spawnSync` note.)
|
|
28
|
+
*/
|
|
29
|
+
export declare function pkgConfigSearchDirs(env: Record<string, string | undefined>): string[];
|
|
30
|
+
/**
|
|
31
|
+
* Directories holding the host's system GObject-Introspection shared libraries —
|
|
32
|
+
* most specific first, deduplicated, each verified on disk.
|
|
33
|
+
*
|
|
34
|
+
* Three sources, in precedence order:
|
|
35
|
+
* 1. `GI_TYPELIB_PATH` — the host stating outright where typelibs live; the
|
|
36
|
+
* libraries are the sibling `dirname()`. Accepted on directory existence
|
|
37
|
+
* alone: an explicit statement is not second-guessed.
|
|
38
|
+
* 2. `pkg-config`'s `.pc` search path → each `<dir>/pkgconfig`'s parent. The
|
|
39
|
+
* general mechanism (any prefix, including a custom one).
|
|
40
|
+
* 3. {@link PROBED_GI_LIBDIRS} — the standard macOS prefixes, for the common
|
|
41
|
+
* case where GTK is installed and pkg-config is not.
|
|
42
|
+
* Sources 2 and 3 are GUESSES at a prefix, so each must show the
|
|
43
|
+
* `girepository-1.0/` marker before it is believed.
|
|
44
|
+
*
|
|
45
|
+
* Empty off darwin, and that is a statement about LOADERS, not about GI. Linux
|
|
46
|
+
* resolves these leaves through `ld.so`'s system-wide configured cache
|
|
47
|
+
* (`/etc/ld.so.conf.d`), which a package install populates — nothing per-process
|
|
48
|
+
* is needed. dyld is the one loader that neither consults a system-wide config
|
|
49
|
+
* NOR re-reads its search path after launch, which is why darwin needs the
|
|
50
|
+
* variable set at launch and nobody else does.
|
|
51
|
+
*
|
|
52
|
+
* win32 is deliberately absent rather than proven unnecessary: `PATH` is the DLL
|
|
53
|
+
* search path, a Windows GTK distribution puts its `bin` there itself, and
|
|
54
|
+
* Windows re-reads that path at every `LoadLibrary` — so if a system GTK ever
|
|
55
|
+
* turns out not to be on it, the repair is a `PATH` prepend (which
|
|
56
|
+
* {@link buildNativeEnv} already does for the prebuilds), never a second
|
|
57
|
+
* variable. Untested there.
|
|
58
|
+
*
|
|
59
|
+
* @returns absolute directories, or `[]` when this platform's loader needs none
|
|
60
|
+
*/
|
|
61
|
+
export declare function systemGiLibraryDirs({ platform, env, existsDir, searchDirs, }?: SystemGiOptions): string[];
|
|
62
|
+
/**
|
|
63
|
+
* Whether `current` (a split `DYLD_FALLBACK_LIBRARY_PATH`) already covers every
|
|
64
|
+
* directory in `wanted` — the "nothing to do" test.
|
|
65
|
+
*
|
|
66
|
+
* Kept in the port even though this CLI's consumer does not branch on it: the
|
|
67
|
+
* mirror's value is that the two files are the SAME file, and a surface that
|
|
68
|
+
* drifts by one export is a mirror nobody can check. `system-gi.spec.ts` pins
|
|
69
|
+
* both.
|
|
70
|
+
*/
|
|
71
|
+
export declare function pathCovers(wanted: readonly string[], current: readonly string[]): boolean;
|