@exodus/stasis 1.0.0-beta.3 → 1.0.0-beta.5

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.
@@ -0,0 +1,1699 @@
1
+ // Cargo manifests for the Rust loader: Cargo.toml, Cargo.lock and cargo's configuration read by
2
+ // @preventive/lockfile, per-bundle package lookup, dependency resolution among in-tree crates (the
3
+ // package's own lib, workspace `path` deps, `cargo vendor`ed registry crates) and feature
4
+ // resolution done the way `cargo build` does it, so `#[cfg(feature = "…")]` can be decided per
5
+ // crate: @preventive/lockfile's, cargo's own resolver, where the build's lockfile and target are
6
+ // known; a replay of the manifests otherwise.
7
+
8
+ import { spawnSync } from 'node:child_process'
9
+ import { createHash } from 'node:crypto'
10
+ import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
11
+ import { homedir } from 'node:os'
12
+ import { dirname, isAbsolute, join, parse, posix, relative, resolve, sep } from 'node:path'
13
+
14
+ import { readText } from '@exodus/stasis-core/bundle-util'
15
+ import { diskHost } from '@exodus/stasis-core/host'
16
+ import { canonicalizePath } from '@exodus/stasis-core/state-util'
17
+ import { relativeEscapes, toPosix } from '@exodus/stasis-core/util'
18
+
19
+ import { LockfileError, linkCargo, parseCargoConfig, parseCargoLock, parseCargoManifest as readCargoManifest, readCargoVendor, resolveCargoFeatures } from '@preventive/lockfile/cargo.js'
20
+ import { matches, parseVersion, parseVersionReq } from '@preventive/lockfile/rust-semver.js'
21
+
22
+ import { isFile } from '../resolve-typescript.js'
23
+ import { TomlError, isTomlTable, nameErrors, readToml, splitTopLevel } from './toml.js'
24
+
25
+ // `cargo vendor` copies registry crates in-tree under this dir.
26
+ export const VENDOR_DIR = 'vendor'
27
+
28
+ // A crate name as source spells it (`use proc_macro2`): Cargo allows `-`, rustc doesn't.
29
+ export const normName = (name) => name.replaceAll('-', '_')
30
+
31
+ // The file's bytes, or null when it can't be read.
32
+ function readFileOrNull(file) {
33
+ try {
34
+ return readFileSync(file)
35
+ } catch {
36
+ return null
37
+ }
38
+ }
39
+
40
+ // `map`'s value for `key` (a Map or WeakMap), computed on the first ask.
41
+ export function cached(map, key, compute) {
42
+ let value = map.get(key)
43
+ if (value === undefined) {
44
+ value = compute()
45
+ map.set(key, value)
46
+ }
47
+ return value
48
+ }
49
+
50
+ // `dir` and each directory above it, the root last.
51
+ function* ancestors(dir) {
52
+ for (;;) {
53
+ yield dir
54
+ const parent = dirname(dir)
55
+ if (parent === dir) return
56
+ dir = parent
57
+ }
58
+ }
59
+
60
+ // A TOML value that is a table, else null.
61
+ const table = (v) => (isTomlTable(v) ? v : null)
62
+
63
+ // --- Paths ---------------------------------------------------------------------------
64
+
65
+ // Project-relative `sub` under `dir`, normalized; null when it escapes the bundle root.
66
+ export function normalizeRel(dir, sub) {
67
+ if (isAbsolute(sub) || posix.isAbsolute(sub)) return null
68
+ const rel = posix.normalize(posix.join(dir === '.' ? '' : dir, sub))
69
+ if (rel === '..' || rel.startsWith('../') || posix.isAbsolute(rel)) return null
70
+ return rel
71
+ }
72
+
73
+ // --- Cargo.toml -----------------------------------------------------------------------
74
+
75
+ // A `[features]` entry asking a feature of a dependency: `key/feat` or `key?/feat`.
76
+ const DEP_FEATURE_RE = /^([^/?]+)(\?)?\/(.+)$/u
77
+
78
+ // `--features a,b pkg/c` (cargo's syntax: repeatable, comma- or space-separated) → the list of names.
79
+ export function parseFeatureList(values) {
80
+ return [...new Set(values.flatMap((s) => s.split(/[\s,]+/u)).map((s) => s.trim()).filter(Boolean))]
81
+ }
82
+
83
+ // Whether a source file belongs to a test or bench target of the package at `pkgDir` (`tests/*.rs`,
84
+ // `benches/*.rs` and what they declare): rustc compiles those with `cfg(test)`.
85
+ export function isTestTargetPath(pkgDir, fileRel) {
86
+ const first = targetDirOf(pkgDir, fileRel)
87
+ return first === 'tests' || first === 'benches'
88
+ }
89
+ // Whether a file belongs to a target cargo builds with the dev-dependencies: a test, bench or
90
+ // example (`examples/*.rs`), which unlike the other two isn't compiled with `cfg(test)`.
91
+ function linksDevDepsPath(pkgDir, fileRel) {
92
+ return isTestTargetPath(pkgDir, fileRel) || targetDirOf(pkgDir, fileRel) === 'examples'
93
+ }
94
+ // The first directory of a file's path inside its package.
95
+ function targetDirOf(pkgDir, fileRel) {
96
+ const inside = pkgDir === '.' || pkgDir === '' ? fileRel : (fileRel.startsWith(`${pkgDir}/`) ? fileRel.slice(pkgDir.length + 1) : fileRel)
97
+ return inside.split('/')[0]
98
+ }
99
+
100
+ // `read()`, with a TomlError or LockfileError it throws naming `file`: what @preventive/lockfile
101
+ // refuses stops the build, saying which file.
102
+ const readNamed = (file, read) => nameErrors(file, read, [TomlError, LockfileError])
103
+
104
+ // A Cargo.toml as the loader reads it: @preventive/lockfile's reading (`cargo`) -- the package, its
105
+ // features (with the one each optional dependency no `dep:` names turns on) and every dependency
106
+ // table, what a member inherits from its workspace applied, refused where cargo would refuse it or
107
+ // the reader can't tell how cargo reads it -- plus what that reading leaves out: the lib target
108
+ // (`[lib] name`, `path`) and the build script (`[package] build`). `workspace` is the workspace
109
+ // root's, read before, for a member that inherits from it. Dependency keys are the `use` spelling
110
+ // (`-` → `_`); each table is a request of its own, `<kind>` or `<kind>@<platform>` (`normal`,
111
+ // `build`, `dev@cfg(windows)`): `[dependencies] rand = "0.7"` beside `[build-dependencies] rand =
112
+ // "0.8"` are two crates, and sha2's dev-dependency on digest asks digest for nothing a build of
113
+ // sha2's dependents sees; each entry has its `kind` and `target` (null for none) apart too. Throws
114
+ // a TomlError or LockfileError naming `file`.
115
+ export const parseCargoManifest = (text, file = null, workspace = null) => manifestOf(text, readToml(text, file), file, workspace)
116
+ // parseCargoManifest of `text` read as `doc` already.
117
+ function manifestOf(text, doc, file, workspace) {
118
+ const root = workspace?.cargo.workspace === undefined ? undefined : workspace.cargo
119
+ const cargo = readNamed(file, () => readCargoManifest(text, isTomlTable(doc.workspace) ? undefined : root))
120
+ const pkg = cargo.package
121
+ const lib = table(doc.lib)
122
+ const build = table(doc.package ?? doc.project)?.build
123
+ const deps = new Map()
124
+ for (const d of pkg?.dependencies ?? []) {
125
+ const key = normName(d.name)
126
+ if (!deps.has(key)) deps.set(key, { key, name: d.name, kinds: new Map() })
127
+ deps.get(key).kinds.set(d.target === undefined ? d.kind : `${d.kind}@${d.target}`, {
128
+ kind: d.kind,
129
+ target: d.target ?? null,
130
+ version: d.version ?? null,
131
+ path: d.source.type === 'path' ? d.source.path : null,
132
+ source: d.source.type,
133
+ // where from: a git dependency's repository URL, another registry's name or index than
134
+ // crates.io's
135
+ origin: d.source.type === 'git' ? d.source.url : (d.source.type === 'registry' ? d.source.registry ?? d.source.index ?? null : null),
136
+ package: d.package,
137
+ renamed: d.package !== d.name,
138
+ inherited: d.inherited,
139
+ optional: d.optional,
140
+ defaultFeatures: d.defaultFeatures,
141
+ features: d.features,
142
+ })
143
+ }
144
+ return {
145
+ cargo,
146
+ package: pkg === undefined ? null : { name: pkg.name, version: pkg.version, edition: pkg.edition, build: typeof build === 'string' || build === false ? build : null },
147
+ resolver: cargo.workspace?.resolver ?? pkg?.resolver ?? null,
148
+ isWorkspace: cargo.workspace !== undefined,
149
+ lib: { name: typeof lib?.name === 'string' ? lib.name : null, path: typeof lib?.path === 'string' ? lib.path : null, procMacro: pkg?.procMacro === true },
150
+ deps,
151
+ }
152
+ }
153
+
154
+ // Whether `version` satisfies the Cargo requirement `req`, by the semver crate's rules
155
+ // (@preventive/lockfile's rust-semver.js): a prerelease only where the requirement names one of
156
+ // its major, minor and patch.
157
+ const satisfies = (version, req) => {
158
+ const v = parseVersion(version)
159
+ const comparators = parseVersionReq(req)
160
+ return v !== undefined && comparators !== undefined && matches(comparators, v)
161
+ }
162
+
163
+ // --- cargo metadata -------------------------------------------------------------------
164
+
165
+ // The Cargo.lock governing `baseDir`: beside the workspace root's Cargo.toml -- the nearest manifest
166
+ // at or above `baseDir` with a `[workspace]` table, as cargo finds it -- else beside `baseDir`'s
167
+ // own. A lock further up belongs to some other project: passing `--locked` for it would make
168
+ // cargo refuse to create the one it needs.
169
+ export function findCargoLock(baseDir) {
170
+ let root = baseDir
171
+ for (const dir of ancestors(baseDir)) {
172
+ const file = join(dir, 'Cargo.toml')
173
+ const text = readText(diskHost, file)
174
+ if (text !== null && 'workspace' in readToml(text, file)) {
175
+ root = dir
176
+ break
177
+ }
178
+ }
179
+ const candidate = join(root, 'Cargo.lock')
180
+ return existsSync(candidate) ? candidate : null
181
+ }
182
+
183
+ // --- cfg predicates ---------------------------------------------------------------------
184
+
185
+ // A cfg predicate with its whitespace collapsed outside string literals: `my = "a b"` and
186
+ // `my = "a b"` are two values.
187
+ export const normalizeCfg = (pred) => pred.replaceAll(/"(?:[^"\\]|\\.)*"|\s+/gu, (m) => (m.startsWith('"') ? m : ' ')).trim()
188
+
189
+ // A cfg name as written, a raw identifier's `r#` dropped -- but `r#true` and `r#false`, names apart
190
+ // from the literals `true` and `false`.
191
+ export const cfgName = (written) => (/^r#(?:true|false)$/u.test(written) ? written : written.replace(/^r#/u, ''))
192
+
193
+ // A leaf: `unix`, `target_os = "linux"`, a raw identifier's `r#` dropped.
194
+ const CFG_LEAF_RE = /^(?:r#)?([A-Za-z_]\w*)\s*(?:=\s*"([^"]*)")?$/u
195
+
196
+ // The cfg keys rustc sets from the target alone (`rustc --print cfg --target <triple>`), decided by
197
+ // membership in that set when the target is known. Not among them: `debug_assertions`, `panic`,
198
+ // `overflow_checks` and the like, which the build profile sets; `target_feature`, which the build
199
+ // may add to or take from (`-C target-feature=-crt-static`, `-C target-cpu=native`); and
200
+ // `target_thread_local`, which a stable rustc never prints.
201
+ export const TARGET_CFG_KEYS = new Set(['unix', 'windows', 'target_abi', 'target_arch', 'target_endian', 'target_env', 'target_family', 'target_has_atomic', 'target_os', 'target_pointer_width', 'target_vendor'])
202
+
203
+ // Three-valued evaluation of a cfg predicate: `false` when it can never hold in the build (so the
204
+ // item it gates is dead code for the bundle), `true` when it always does, `null` when the loader
205
+ // can't tell. `all`/`any`/`not` compose. `test` holds only in a test/bench target (`env.test`);
206
+ // `doctest` and `doc` never do when a program is built; `feature = "x"` is decided against
207
+ // `env.features`, the crate's features on for certain (see createCargoContext) -- unknown for one
208
+ // of `env.maybeFeatures`, on only in some builds, and without either set; a target leaf (`unix`,
209
+ // `target_os = …`) against `env.target`, the platform's cfg set as rustc prints it
210
+ // (rustcTargetCfgs), and unknown without one; every other leaf stays unknown. `env.units`, a list
211
+ // of such envs, is code compiled several ways (a crate built for the target and, as a
212
+ // build-dependency, for the host): false only when false in each, true only when true in each.
213
+ // A predicate an unknown leaf occurs in more than once is decided when it comes out the same
214
+ // whatever the unknown leaves are (up to six of them): `all(any(test, kani), not(kani))` -- zerocopy's
215
+ // `#[cfg(any(test, kani))] mod tests { #[cfg(not(kani))] mod compatibility { … } }` -- never holds.
216
+ const MAX_FREE_CFG_LEAVES = 6
217
+ export function evalCfg(pred, env = {}) {
218
+ if (env.units) return unanimous(env.units.map((u) => evalCfg(pred, u)))
219
+ const free = { counts: null } // unknown leaf → how often it occurs, once one is met
220
+ const r = evalCfgWith(pred, env, null, free)
221
+ if (r !== null || free.counts === null || free.counts.size > MAX_FREE_CFG_LEAVES || [...free.counts.values()].every((n) => n === 1)) return r
222
+ const leaves = [...free.counts.keys()]
223
+ let out
224
+ for (let bits = 0; bits < 1 << leaves.length; bits++) {
225
+ const v = evalCfgWith(pred, env, new Map(leaves.map((l, k) => [l, ((bits >> k) & 1) === 1])))
226
+ if (out === undefined) out = v
227
+ else if (v !== out) return null
228
+ }
229
+ return out
230
+ }
231
+ // evalCfg's three-valued pass: an unknown leaf takes its value from `assume` (leaf → value) when
232
+ // that has it, else is counted in `free.counts`.
233
+ function evalCfgWith(pred, env, assume, free = null) {
234
+ const p = pred.trim()
235
+ const m = /^(all|any|not)\s*\(([\s\S]*)\)$/u.exec(p)
236
+ if (!m) {
237
+ if (p === 'true' || p === 'false') return p === 'true' // the literals (rustc 1.88); `r#true` is a name
238
+ const kv = CFG_LEAF_RE.exec(p)
239
+ const known = kv === null ? null : evalCfgKey(kv[1], kv[2] ?? null, env)
240
+ if (known !== null) return known
241
+ // The same leaf however it is spaced, its value as written.
242
+ const leaf = kv === null ? p : (kv[2] === undefined ? kv[1] : `${kv[1]}="${kv[2]}"`)
243
+ if (assume?.has(leaf)) return assume.get(leaf)
244
+ if (free !== null) (free.counts ??= new Map()).set(leaf, (free.counts.get(leaf) ?? 0) + 1)
245
+ return null
246
+ }
247
+ const args = splitTopLevel(m[2]).map((a) => a.trim()).filter(Boolean).map((a) => evalCfgWith(a, env, assume, free))
248
+ if (m[1] === 'not') return args.length === 1 && args[0] !== null ? !args[0] : null
249
+ if (m[1] === 'all') return args.includes(false) ? false : (args.every((a) => a === true) ? true : null)
250
+ return args.includes(true) ? true : (args.every((a) => a === false) ? false : null)
251
+ }
252
+ // The verdict on one leaf other than a literal, by its key and value (`target_os = "linux"`; null
253
+ // for a bare `unix`), as evalCfg gives it -- `env.units` too.
254
+ export function evalCfgKey(key, value, env) {
255
+ if (env.units) return unanimous(env.units.map((u) => evalCfgKey(key, value, u)))
256
+ if (value === null && key === 'test') return env.test === true
257
+ if (value === null && (key === 'doctest' || key === 'doc')) return false
258
+ if (key === 'feature' && value !== null) return env.features ? (env.features.has(value) ? true : (env.maybeFeatures?.has(value) ? null : false)) : null
259
+ if (env.target && TARGET_CFG_KEYS.has(key)) return env.target.has(value === null ? key : `${key}="${value}"`)
260
+ return null
261
+ }
262
+ // Code compiled several ways: false only when false in each, true only when true in each.
263
+ const unanimous = (each) => (each.every((r) => r === false) ? false : (each.every((r) => r === true) ? true : null))
264
+
265
+ // `rustc --print cfg` output → the set of cfg leaves as printed, one per line (`unix`,
266
+ // `target_os="linux"`).
267
+ export function parseRustcCfg(text) {
268
+ return new Set(text.split('\n').map((l) => l.trim()).filter(Boolean))
269
+ }
270
+
271
+ // How long a toolchain query may take before it counts as hung.
272
+ const TOOL_TIMEOUT_MS = 60_000
273
+ // Never let rustup install a toolchain on the loader's behalf, whatever a `rust-toolchain` file says.
274
+ const TOOL_ENV = { ...process.env, RUSTUP_AUTO_INSTALL: '0' }
275
+ // Where rustc is run from: the user's home directory, else the filesystem root. A rustup proxy
276
+ // picks its toolchain from the `rust-toolchain(.toml)` files of the working directory and its
277
+ // parents, and such a file may name a `path` to any binary -- so never the project being bundled
278
+ // (untrusted input: its toolchain file must not choose what runs), and never a temp dir (anyone
279
+ // can plant a file in a world-writable one). The home directory is the user's -- unless it is the
280
+ // bundle root (`baseDir`) or lies inside it (a project bundled from a home directory, a CI job
281
+ // whose HOME is its checkout): then the root, whose toolchain file, if any, is the machine's.
282
+ function toolCwd(baseDir) {
283
+ const home = homedir()
284
+ const root = parse(process.cwd()).root
285
+ if (!home) return root
286
+ if (baseDir === null) return home
287
+ // Real paths: a home directory or bundle root reached through a link is where it really is.
288
+ const base = canonicalizePath(baseDir)
289
+ const homeAbs = canonicalizePath(home)
290
+ return homeAbs === base || homeAbs.startsWith(base.endsWith(sep) ? base : base + sep) ? root : home
291
+ }
292
+
293
+ // Run rustc: `$RUSTC` when set, as cargo honours it (a target only another toolchain knows, such
294
+ // as Solana's `sbf-solana-solana` in its platform-tools), else `rustc` from PATH -- a rustup proxy
295
+ // resolving to the user's default toolchain (or `RUSTUP_TOOLCHAIN`), from toolCwd. With
296
+ // RUSTUP_AUTO_INSTALL=0 an uninstalled toolchain is an error, not a download.
297
+ function rustc(args, what, baseDir) {
298
+ const bin = process.env.RUSTC || 'rustc'
299
+ const r = spawnSync(bin, args, { cwd: toolCwd(baseDir), encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: TOOL_TIMEOUT_MS, env: TOOL_ENV })
300
+ if (r.error) throw new Error(`${bin} could not run (${r.error.message}); ${what} needs rustc on PATH (or $RUSTC)`, { cause: r.error })
301
+ if (r.status !== 0) throw new Error(`${bin} ${args.join(' ')} failed (exit ${r.status}):\n${(r.stderr ?? '').trim()}`)
302
+ return r.stdout
303
+ }
304
+
305
+ // The cfg set of a target: `{ triple, cfgs }` from `rustc --print cfg --target <triple>`, which
306
+ // needs only rustc's built-in knowledge of the target, not its standard library. `host` is the
307
+ // running rustc's host triple (`rustc -vV`). `baseDir` is the bundle root, which rustc is never
308
+ // run from (toolCwd).
309
+ export function rustcTargetCfgs(target, baseDir = null) {
310
+ const triple = target === 'host' ? /^host: (\S+)$/mu.exec(rustc(['-vV'], '--cargo-target=host', baseDir))?.[1] ?? null : target
311
+ if (triple === null) throw new Error('rustc -vV printed no host triple')
312
+ return { triple, cfgs: parseRustcCfg(rustc(['--print', 'cfg', '--target', triple], '--cargo-target', baseDir)) }
313
+ }
314
+
315
+ // Run `cargo metadata` in `dir` (the directory the build runs in, inside the bundle root) and
316
+ // return its JSON. Opt-in (`--cargo`) because it runs cargo, which reads the project's
317
+ // `.cargo/config.toml` and may touch the registry and Cargo.lock (doc/file-formats.md has the full
318
+ // caveat); never run on a bundle root unasked. With a lockfile present, `--locked` keeps the
319
+ // resolution the one the build uses. The feature flags pass through; `platform`, a target triple,
320
+ // restricts the graph to that target's dependencies.
321
+ function runCargoMetadata(dir, { features = [], noDefaultFeatures = false, allFeatures = false, platform = null } = {}) {
322
+ const args = ['metadata', '--format-version', '1']
323
+ // The workspace's lock may sit above the bundle root: never let metadata rewrite it.
324
+ if (findCargoLock(dir) !== null) args.push('--locked')
325
+ if (allFeatures) args.push('--all-features')
326
+ if (noDefaultFeatures) args.push('--no-default-features')
327
+ for (const f of features) args.push('--features', f)
328
+ if (platform !== null) args.push('--filter-platform', platform)
329
+ const r = spawnSync('cargo', args, { cwd: dir, encoding: 'utf8', maxBuffer: 1024 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] })
330
+ if (r.error) throw new Error(`cargo metadata could not run (${r.error.message}); is cargo on PATH?`, { cause: r.error })
331
+ if (r.status !== 0) throw new Error(`cargo metadata failed (exit ${r.status}):\n${(r.stderr ?? '').trim()}`)
332
+ return JSON.parse(r.stdout)
333
+ }
334
+
335
+ // `cargo metadata` JSON → `{ enabled: Map<dir, Set<feature>>, deps: Map<dir, Map<useName, dir>>,
336
+ // lacking: [{ dir, key, name, kinds }] }` over the packages the bundle can carry: those whose
337
+ // manifest lies inside the bundle root, plus registry packages cargo read from `~/.cargo/registry`
338
+ // (no `.cargo/config.toml` redirecting crates.io to `vendor/`) that `locate(name, version)` finds
339
+ // vendored in-tree -- `cargo vendor` copies exactly the lockfile's versions, so name + version
340
+ // identify the dir. Anything else (an unvendored registry crate, a path dep outside the root)
341
+ // can't be bundled: it is in `lacking`, once per package depending on it (`dir`), by its package
342
+ // name (`name`; `key` with `-` → `_`), with the kinds of the tables naming it (`normal`, `dev`,
343
+ // `build`). `deps` maps each package's dependencies by the name code refers to them with (renames
344
+ // applied, `-` → `_`).
345
+ export function resolutionFromMetadata(metadata, baseDir, { locate = null } = {}) {
346
+ let realBase = baseDir
347
+ try {
348
+ realBase = realpathSync(baseDir)
349
+ } catch { /* keep the lexical path */ }
350
+ const relDir = (manifestPath) => {
351
+ if (typeof manifestPath !== 'string') return null
352
+ const dir = dirname(manifestPath)
353
+ for (const base of new Set([baseDir, realBase])) {
354
+ const rel = toPosix(relative(base, dir))
355
+ if (rel === '') return '.'
356
+ if (!relativeEscapes(rel)) return rel
357
+ }
358
+ return null
359
+ }
360
+ const dirOf = new Map()
361
+ for (const p of metadata.packages ?? []) {
362
+ const dir = relDir(p.manifest_path) ?? locate?.(p.name, p.version) ?? null
363
+ if (dir !== null) dirOf.set(p.id, dir)
364
+ }
365
+ const nameOf = new Map((metadata.packages ?? []).map((p) => [p.id, p.name]))
366
+ const enabled = new Map()
367
+ const deps = new Map()
368
+ const lacking = []
369
+ for (const node of metadata.resolve?.nodes ?? []) {
370
+ const dir = dirOf.get(node.id)
371
+ if (dir === undefined) continue
372
+ enabled.set(dir, new Set((node.features ?? []).filter((f) => typeof f === 'string')))
373
+ const byName = new Map()
374
+ for (const d of node.deps ?? []) {
375
+ const target = dirOf.get(d.pkg)
376
+ if (target !== undefined && typeof d.name === 'string') byName.set(normName(d.name), target)
377
+ else if (target === undefined) {
378
+ const name = nameOf.get(d.pkg) ?? d.name
379
+ lacking.push({ dir, key: normName(name), name, kinds: new Set((Array.isArray(d.dep_kinds) ? d.dep_kinds : [{ kind: null }]).map((k) => k?.kind ?? 'normal')) })
380
+ }
381
+ }
382
+ deps.set(dir, byName)
383
+ }
384
+ return { enabled, deps, lacking }
385
+ }
386
+
387
+ // The cargo config file in absolute directory `abs`: `.cargo/config` when it exists -- cargo
388
+ // prefers the extensionless file over `.cargo/config.toml` when both are there -- else
389
+ // `.cargo/config.toml`; null when there is neither.
390
+ const cargoConfigAt = (abs) => ['.cargo/config', '.cargo/config.toml'].map((f) => join(abs, f)).find((file) => isFile(file)) ?? null
391
+
392
+ // The cargo configs a build run in `from` (a project-relative dir) reads, as cargo finds them: the
393
+ // config of that directory and of every one above it to the filesystem's root, the nearest first
394
+ // -- those above the bundle root too, read but never bundled; `$CARGO_HOME`'s, the machine's, is
395
+ // not -- as `{ dir, abs, file, text, doc }`: `dir` the directory holding its `.cargo`,
396
+ // project-relative (null above the bundle root), `abs` the same absolute, `file` the config's
397
+ // path from the bundle root. Throws a TomlError naming a config that isn't TOML.
398
+ function cargoConfigs(baseDir, from) {
399
+ const baseAbs = resolve(baseDir)
400
+ const out = []
401
+ for (const abs of ancestors(join(baseAbs, from))) {
402
+ const found = cargoConfigAt(abs)
403
+ const text = found === null ? null : readText(diskHost, found)
404
+ if (text !== null) {
405
+ const file = toPosix(relative(baseAbs, found))
406
+ out.push({ dir: projectRel(baseAbs, abs), abs, file, text, doc: readToml(text, file) })
407
+ }
408
+ }
409
+ return out
410
+ }
411
+ // A path a cargo config or a manifest in absolute directory `absDir` writes (`.`: that directory),
412
+ // as cargo takes it -- relative to that directory, an absolute one as it is -- project-relative
413
+ // from the bundle root `baseAbs`; null outside it. A config's paths are relative to the directory
414
+ // holding its `.cargo` (cargoConfigs' `abs`).
415
+ const projectRel = (baseAbs, absDir, path = '.') => normalizeRel('.', toPosix(relative(baseAbs, resolve(absDir, path))))
416
+
417
+ // The directory `cargo vendor` filled, as cargo configs `configs` (cargoConfigs) say: their
418
+ // `[source]` tables merged as cargo merges them, each source's each key from the nearest config
419
+ // writing it -- the `directory` of the source `[source.crates-io] replace-with` names, through any
420
+ // chain of replacements (`cargo vendor` suggests `vendored-sources`, but any name does), else of
421
+ // the one source some `replace-with` names, relative to the directory holding the `.cargo` of the
422
+ // config writing it; the default `vendor` (at the bundle root) when nothing replaces a source
423
+ // with a directory. A configured directory outside the bundle root, or that doesn't exist, is
424
+ // warned about (a stale `vendor/` beside it is not what cargo builds from) unless `quiet`.
425
+ function vendorDirOf(baseDir, configs, { quiet = false } = {}) {
426
+ // source name -> { replaceWith, directory: { path, config } }
427
+ const sources = new Map()
428
+ for (const config of configs) {
429
+ for (const [name, fields] of Object.entries(isTomlTable(config.doc.source) ? config.doc.source : {})) {
430
+ if (!isTomlTable(fields)) continue
431
+ const s = sources.get(name) ?? sources.set(name, { replaceWith: null, directory: null }).get(name)
432
+ if (s.replaceWith === null && typeof fields['replace-with'] === 'string') s.replaceWith = fields['replace-with']
433
+ if (s.directory === null && typeof fields.directory === 'string') s.directory = { path: fields.directory, config }
434
+ }
435
+ }
436
+ const follow = (name) => {
437
+ const seen = new Set()
438
+ for (let s = sources.get(name); s && !seen.has(s); s = sources.get(s.replaceWith)) {
439
+ seen.add(s)
440
+ if (s.directory !== null) return s.directory
441
+ if (s.replaceWith === null) return null
442
+ }
443
+ return null
444
+ }
445
+ const replaced = new Set([...sources.values()].map((s) => (s.replaceWith === null ? null : follow(s.replaceWith))).filter((d) => d !== null))
446
+ const found = follow('crates-io') ?? (replaced.size === 1 ? [...replaced][0] : null)
447
+ if (found === null) return VENDOR_DIR
448
+ const norm = projectRel(resolve(baseDir), found.config.abs, found.path)?.replace(/\/+$/u, '') || null
449
+ if (norm === null) {
450
+ if (!quiet) console.warn(`[loader.cargo] ${found.config.file} names a vendored source directory outside the bundle root: ${found.path}`)
451
+ return VENDOR_DIR
452
+ }
453
+ if (!quiet && !existsSync(join(baseDir, norm))) console.warn(`[loader.cargo] ${found.config.file} names a vendored source directory that doesn't exist: ${norm}`)
454
+ return norm
455
+ }
456
+
457
+ // The `--cfg` names in rustflags: `--cfg name`, `--cfg=name`, `--cfg 'name="value"'`.
458
+ function cfgFlags(flags) {
459
+ const out = []
460
+ for (let i = 0; i < flags.length; i++) {
461
+ const flag = flags[i]
462
+ const arg = flag === '--cfg' ? flags[++i] : (flag.startsWith('--cfg=') ? flag.slice('--cfg='.length) : null)
463
+ const name = typeof arg === 'string' ? /^(?:r#)?[A-Za-z_]\w*/u.exec(arg)?.[0] : undefined
464
+ if (name !== undefined) out.push(cfgName(name))
465
+ }
466
+ return out
467
+ }
468
+ // The cfgs the rustflags of a build may set: those of the `build.rustflags` and
469
+ // `target.<…>.rustflags` of each of its cargo configs (`configs`, see cargoConfigs: which applies,
470
+ // and how cargo joins them, is the build's business), and of the environment cargo reads them
471
+ // from (CARGO_ENCODED_RUSTFLAGS, RUSTFLAGS, CARGO_BUILD_RUSTFLAGS).
472
+ function rustflagsCfgsOf(configs) {
473
+ const names = new Set()
474
+ const add = (value) => {
475
+ const flags = Array.isArray(value) ? value.filter((f) => typeof f === 'string') : (typeof value === 'string' ? value.split(/\s+/u).filter(Boolean) : [])
476
+ for (const name of cfgFlags(flags)) names.add(name)
477
+ }
478
+ // The `rustflags` of the `build` table and of every `target.<…>` one.
479
+ for (const { doc } of configs) {
480
+ add(table(doc.build)?.rustflags)
481
+ for (const target of Object.values(table(doc.target) ?? {})) add(table(target)?.rustflags)
482
+ }
483
+ const { env } = process
484
+ if (env.CARGO_ENCODED_RUSTFLAGS) add(env.CARGO_ENCODED_RUSTFLAGS.split('\u001F'))
485
+ for (const v of [env.RUSTFLAGS, env.CARGO_BUILD_RUSTFLAGS]) if (v) add(v)
486
+ return names
487
+ }
488
+ // What code a build script runs -- its own, or a lib it calls -- prints as
489
+ // `cargo:rustc-cfg=<name>` (or `cargo::`), among the texts of its files, by what they write
490
+ // outside full-line comments: the names it may set, and whether it may set one the loader can't
491
+ // read -- a name it formats, whole (`rustc-cfg={}`, as cfg_aliases' `cfg_aliases!` does) or in
492
+ // part (`rustc-cfg=os_{}`), the directive written apart from the name (build-rs's `rustc_cfg`),
493
+ // `autocfg`'s probes.
494
+ const RUSTC_CFG_RE = /cargo::?rustc-cfg=((?:r#)?[A-Za-z_]\w*)?(\{)?/gu
495
+ const CFG_MENTION_RE = /rustc[-_]cfg|\bautocfg\b/u
496
+ function cfgsPrinted(texts) {
497
+ const names = new Set()
498
+ let any = false
499
+ for (const raw of texts) {
500
+ if (!CFG_MENTION_RE.test(raw)) continue // most code says nothing of cfgs: no comments to strip
501
+ const text = raw.replaceAll(/^[ \t]*\/\/.*$/gmu, '')
502
+ for (const m of text.matchAll(RUSTC_CFG_RE)) {
503
+ if (m[1] === undefined || m[2] !== undefined) any = true
504
+ else names.add(cfgName(m[1]))
505
+ }
506
+ if (CFG_MENTION_RE.test(text.replaceAll(RUSTC_CFG_RE, ''))) any = true
507
+ }
508
+ return { names, any }
509
+ }
510
+
511
+ // Whether a JSON value is a plain object (not an array, not null).
512
+ const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
513
+
514
+ // A source URL as cargo compares them: no trailing `/` or `.git`, the scheme and host lowercased.
515
+ const canonicalUrl = (url) => url.trim().replace(/\/+$/u, '').replace(/\.git$/u, '').replace(/^([a-z][\w+.-]*:\/\/)([^/]*)/iu, (m) => m.toLowerCase())
516
+ // crates.io's index, as a `[patch.<url>]` may name it.
517
+ const CRATES_IO_INDEXES = new Set(['https://github.com/rust-lang/crates.io-index', 'sparse+https://index.crates.io'].map(canonicalUrl))
518
+
519
+ // A `members` pattern's segment as a regex (glob's syntax, as cargo takes it: `*`, `?`, `[…]`).
520
+ const globSegment = (seg) => new RegExp(`^${seg.replaceAll(/[.+^${}()|\\]/gu, '\\$&').replaceAll('[!', '[^').replaceAll('*', '[^/]*').replaceAll('?', '[^/]')}$`, 'u')
521
+
522
+ // The stand-in platforms cargo's resolver is run against (see resolveGraph): each carries a mark
523
+ // of its own, and the cfgs and a mark of the name of the platform it stands in for, where known.
524
+ const MARK_TARGET = '__stasis_target'
525
+ const MARK_HOST = '__stasis_host'
526
+ const MARK_NAME = '__stasis_target_name'
527
+ const standInPlatform = (mark, info) => ({ name: mark, cfg: [mark, ...(info === null ? [] : [...info.cfgs, `${MARK_NAME}="${info.triple}"`])] })
528
+ // `graph` with the platform of every target-specific dependency `to(platform)`.
529
+ function retarget(graph, to) {
530
+ const packages = Object.create(null)
531
+ for (const [key, pkg] of Object.entries(graph.packages)) {
532
+ packages[key] = { ...pkg, dependencies: pkg.dependencies.map((d) => (d.target === undefined ? d : { ...d, target: to(d.target) })) }
533
+ }
534
+ return { ...graph, packages }
535
+ }
536
+
537
+ // --- Context ----------------------------------------------------------------------------
538
+
539
+ // No custom cfg a build sets (see cfgsSetFor).
540
+ const NO_CFGS_SET = { names: new Set(), any: false }
541
+
542
+ // A compile unit: the feature context a file's code sees (`target`: the build's; `host`: that of
543
+ // what is built for the host -- build-dependencies, proc-macro crates and their dependencies --
544
+ // which resolver 2 keeps apart) and the platform it is compiled for, as `<features>|<platform>`.
545
+ // A package's code is compiled as `target|target`, its build script as `target|host`, a
546
+ // build-dependency's or proc-macro's code as `host|host`; one file may be compiled as several.
547
+ const unitKey = (featureCtx, platform) => `${featureCtx}|${platform}`
548
+ export const TARGET_UNIT = unitKey('target', 'target')
549
+ export const HOST_UNIT = unitKey('host', 'host')
550
+ const unitFeatureCtx = (unit) => unit.slice(0, unit.indexOf('|'))
551
+ const unitPlatform = (unit) => unit.slice(unit.indexOf('|') + 1)
552
+
553
+ // Per-bundle Cargo state: manifest lookup (memoized per directory), crate-name resolution against
554
+ // in-tree sources, and feature resolution for the packages owning `entries` (the crate roots being
555
+ // bundled, like `cargo build -p …`). `features` / `noDefaultFeatures` / `allFeatures` mirror
556
+ // cargo's flags for those root packages (`pkg/feat` targets one of them). `cargo: true` takes the
557
+ // dependency graph and features from `cargo metadata` (see runCargoMetadata) instead. `target`
558
+ // names the build's target -- a triple or `host`, asked of rustc (rustcTargetCfgs), or its `{
559
+ // triple, cfgs }` outright -- so target-specific dependency tables and `#[cfg(unix)]`-style code
560
+ // are decided; without one they are kept. The host's platform is known when the target is the
561
+ // host, or given as `host` (`{ triple, cfgs }`). `baseDir` is the bundle root; every path in and
562
+ // out is project-relative POSIX.
563
+ export function createCargoContext(baseDir, { entries = [], features = [], noDefaultFeatures = false, allFeatures = false, cargo = false, target = null, host = null } = {}) {
564
+ const targetInfo = target === null ? null : (typeof target === 'string' ? rustcTargetCfgs(target, baseDir) : target)
565
+ const hostInfo = target === 'host' ? targetInfo : host
566
+ const manifests = new Map()
567
+ // dir -> the manifest as `{ file, text, buf, doc }` (its bytes, and its table tree), or null
568
+ // (rootOf's look for [workspace])
569
+ const tables = new Map()
570
+ const tableOf = (dir) => cached(tables, dir, () => {
571
+ const file = posix.join(dir, 'Cargo.toml')
572
+ const buf = readFileOrNull(join(baseDir, file))
573
+ const text = buf === null ? null : buf.toString('utf8')
574
+ return text === null ? null : { file, text, buf, doc: readToml(text, file) }
575
+ })
576
+ // The workspace root of the package whose manifest is at `dir` (its table tree `doc`), as cargo
577
+ // finds it: its `[package] workspace` path, else the nearest manifest above it with a
578
+ // [workspace] that doesn't exclude it -- one that does is passed over, as cargo passes it --
579
+ // inside the bundle root (a project-relative dir), else above it (outerWorkspace). A [workspace]
580
+ // that doesn't exclude the package is its workspace, `members` or not: cargo either takes it as
581
+ // a member there (a member's path dependency is one) or refuses to build it. Null for a root, a
582
+ // vendored crate (a published manifest inherits nothing) or a package outside any workspace.
583
+ const rootOf = (dir, doc) => {
584
+ if (isTomlTable(doc.workspace) || isVendoredDir(dir)) return null
585
+ const named = table(doc.package)?.workspace
586
+ if (typeof named === 'string') return normalizeRel(dir, named) ?? outerWorkspace(join(baseAbs, dir, named)) ?? null
587
+ const pkgAbs = join(baseAbs, dir)
588
+ for (let d = dir; d !== '.' && d !== '';) {
589
+ d = posix.dirname(d)
590
+ const ws = tableOf(d)?.doc
591
+ if (isTomlTable(ws?.workspace) && !excludes(ws, join(baseAbs, d), pkgAbs)) return d
592
+ }
593
+ for (const abs of ancestors(dirname(baseAbs))) {
594
+ const ws = outerWorkspace(abs)
595
+ if (ws !== undefined && !excludes(ws.doc, abs, pkgAbs)) return ws
596
+ }
597
+ return null
598
+ }
599
+ // Whether the [workspace] of the manifest `doc` at absolute `abs` excludes the package at
600
+ // absolute `pkgAbs`, as cargo has it: under an `exclude` path and under no `members` one.
601
+ const excludes = (doc, abs, pkgAbs) => {
602
+ const under = (p) => typeof p === 'string' && (pkgAbs === join(abs, p) || pkgAbs.startsWith(`${join(abs, p)}${sep}`))
603
+ const list = (v) => (Array.isArray(v) ? v : [])
604
+ return list(doc.workspace.exclude).some(under) && !list(doc.workspace.members).some(under)
605
+ }
606
+ // The workspace root at absolute `abs`, above the bundle root: the manifest there with a
607
+ // [workspace], read for what members inherit, the resolver it sets and its `[patch]` -- never
608
+ // bundled, nor its lockfile read -- as `{ dir: null, outside: abs, file, doc, … }` (see
609
+ // readManifest; `file` its path from the bundle root); undefined for no [workspace] there.
610
+ const baseAbs = resolve(baseDir)
611
+ const outer = new Map()
612
+ const outerWorkspace = (abs) => {
613
+ if (!outer.has(abs)) {
614
+ const file = join(abs, 'Cargo.toml')
615
+ const text = readText(diskHost, file)
616
+ const label = toPosix(relative(baseAbs, file))
617
+ const doc = text === null ? null : readToml(text, label)
618
+ outer.set(abs, isTomlTable(doc?.workspace) ? { dir: null, outside: abs, file: label, doc, ...manifestOf(text, doc, label, null) } : undefined)
619
+ }
620
+ return outer.get(abs)
621
+ }
622
+ // A manifest as parseCargoManifest reads it, at `file` (`dir`'s Cargo.toml), with each path
623
+ // dependency's directory, project-relative, on its entry (`dir`; null outside the bundle root):
624
+ // an inherited one's path is the workspace root's.
625
+ // `workspaceRoot` is the manifest of the workspace root it inherits from (rootOf), or null.
626
+ // A vendored package's is as the vendor directory lists it, not yet checked (see readManifest).
627
+ const manifestAt = (dir) => cached(manifests, dir, () => {
628
+ const raw = tableOf(dir)
629
+ const root = raw === null ? null : rootOf(dir, raw.doc)
630
+ const ws = root === null ? null : (typeof root === 'string' ? readManifest(root) : root)
631
+ const m = raw === null ? null : { dir, file: raw.file, ...manifestOf(raw.text, raw.doc, raw.file, ws), workspaceRoot: ws, unchecked: isVendoredDir(dir) }
632
+ for (const d of m?.deps.values() ?? []) {
633
+ for (const r of d.kinds.values()) r.dir = r.path === null ? null : manifestPath(r.inherited && ws !== null ? ws : m, r.path)
634
+ }
635
+ return m
636
+ })
637
+ // The manifest at `dir` as the build takes it in (manifestAt): a vendored package's checked
638
+ // against its `.cargo-checksum.json` (checkVendored), the bytes read, before what it says --
639
+ // features, dependencies -- decides anything. Only a listing of the vendor directory reads a copy
640
+ // unchecked: cargo checks the files of a copy it builds, not of every copy there.
641
+ const readManifest = (dir) => {
642
+ const m = manifestAt(dir)
643
+ if (m?.unchecked) {
644
+ checkVendored(dir, m.file, tableOf(dir).buf)
645
+ m.unchecked = false
646
+ }
647
+ return m
648
+ }
649
+ // The directory a path manifest `m` writes names, project-relative (null outside the bundle
650
+ // root): relative to its directory, inside the bundle root or above it (`outside`).
651
+ const manifestPath = (m, path) => (m.outside === undefined ? normalizeRel(m.dir, path) : projectRel(baseAbs, m.outside, path))
652
+ // Manifests at or above `dir`, nearest first, up to the bundle root.
653
+ const manifestsAbove = function* (dir) {
654
+ for (;;) {
655
+ const m = readManifest(dir)
656
+ if (m) yield m
657
+ if (dir === '.' || dir === '') return
658
+ dir = posix.dirname(dir)
659
+ }
660
+ }
661
+ // The package owning a file: the nearest manifest above it with a [package]; memoized per
662
+ // directory (every lookup below starts here, several times per file).
663
+ const packageByDir = new Map()
664
+ const packageFor = (fileRel) => {
665
+ const dir = posix.dirname(fileRel)
666
+ return cached(packageByDir, dir, () => {
667
+ for (const m of manifestsAbove(dir)) if (m.package) return m
668
+ return null
669
+ })
670
+ }
671
+ // The workspace root manifest of package `m` (see rootOf): its own when it is a root, an outer
672
+ // one above the bundle root (`outside`) too; `m` itself outside any workspace.
673
+ const rootManifestOf = (m) => (m.isWorkspace ? m : (m.workspaceRoot ?? m))
674
+ // The workspace root of the package at `dir` inside the bundle root, or null (outside any, or
675
+ // one above the bundle root).
676
+ const workspaceFor = (dir) => {
677
+ const m = readManifest(dir)
678
+ const root = m === null ? null : rootManifestOf(m)
679
+ return root?.isWorkspace === true && root.outside === undefined ? root : null
680
+ }
681
+ // The manifest's lib target root when it is on disk (`[lib] path`, default `src/lib.rs`);
682
+ // memoized on the manifest, since it is asked for once per crate reference.
683
+ // A vendored package's `[lib] path` may not leave its directory (a published crate's never
684
+ // does; one that tries is reaching for the project's files).
685
+ const libPath = (m) => {
686
+ if (m.libRoot === undefined) {
687
+ const rel = normalizeRel(m.dir, m.lib.path ?? 'src/lib.rs')
688
+ const inside = rel !== null && (!isVendoredDir(m.dir) || rel.startsWith(`${m.dir}/`))
689
+ if (rel !== null && !inside) console.warn(`[loader.cargo] Refusing lib path outside its package: ${m.lib.path} in ${m.dir}`)
690
+ m.libRoot = inside && isFile(join(baseDir, rel)) ? rel : null
691
+ }
692
+ return m.libRoot
693
+ }
694
+ const libName = (m) => normName(m.lib.name ?? m.package?.name ?? '')
695
+ // The package's build script when it is on disk (see buildScriptOf); memoized on the manifest.
696
+ const buildScript = (m) => {
697
+ if (m.buildScript === undefined) {
698
+ const rel = m.package.build === false ? null : normalizeRel(m.dir, m.package.build ?? 'build.rs')
699
+ const inside = rel !== null && (!isVendoredDir(m.dir) || rel.startsWith(`${m.dir}/`))
700
+ if (rel !== null && !inside && isFile(join(baseDir, rel))) console.warn(`[loader.cargo] Refusing build script outside its package: ${m.package.build} in ${m.dir}`)
701
+ m.buildScript = inside && isFile(join(baseDir, rel)) ? rel : null
702
+ }
703
+ return m.buildScript
704
+ }
705
+ // Whether `fileRel` belongs to a test or bench target of its package (compiled with `cfg(test)`).
706
+ const isTestTarget = (fileRel) => isTestTargetPath(packageFor(fileRel)?.dir ?? '.', fileRel)
707
+ // Whether `fileRel` belongs to a target built with the dev-dependencies (tests, benches,
708
+ // examples).
709
+ const linksDevDeps = (fileRel) => linksDevDepsPath(packageFor(fileRel)?.dir ?? '.', fileRel)
710
+ // The packages an entry is a test, bench or example target of: their dev-dependencies are
711
+ // linked (that build is `cargo test`'s). Memoized.
712
+ let devRoots = null
713
+ const devRootDirs = () => (devRoots ??= new Set(entries.filter((e) => linksDevDeps(e)).map((e) => packageFor(e)?.dir)))
714
+ const version = (m) => m.package.version
715
+
716
+ // --- dependency resolution
717
+
718
+ // The directory cargo is taken to run in for the build (`runDir`): the one of the package the
719
+ // first entry belongs to -- `cargo build` there, which finds its workspace root above -- unless
720
+ // that entry is a vendored crate's (in the vendor directory the bundle root's configs name),
721
+ // whose own config cargo never reads; else the bundle root. What the cargo config sets for the
722
+ // build -- the vendor directory, `[patch]`, rustflags -- is what the configs cargo reads when
723
+ // run there say (`configs`, see cargoConfigs), and those are the configs a bundle carries.
724
+ const rootConfigs = cargoConfigs(baseDir, '.')
725
+ const rootVendorDir = vendorDirOf(baseDir, rootConfigs, { quiet: true })
726
+ const ownEntry = entries.find((e) => !e.startsWith(`${rootVendorDir}/`))
727
+ const packageDirAbove = (dir) => {
728
+ for (let d = dir; ; d = posix.dirname(d)) {
729
+ const doc = tableOf(d)?.doc
730
+ if (isTomlTable(doc?.package ?? doc?.project)) return d
731
+ if (d === '.' || d === '') return '.'
732
+ }
733
+ }
734
+ const runDir = ownEntry === undefined ? '.' : packageDirAbove(posix.dirname(ownEntry))
735
+ const configs = runDir === '.' ? rootConfigs : cargoConfigs(baseDir, runDir)
736
+ // The vendor directory of the build the entries are in, as those configs say.
737
+ const vendorDir = vendorDirOf(baseDir, configs)
738
+ // Whether a package directory is a vendored crate's (a registry crate `cargo vendor` copied in).
739
+ const isVendoredDir = (dir) => dir !== vendorDir && dir.startsWith(`${vendorDir}/`)
740
+ // The vendored crates, `<vendorDir>/<dir>/`, as `[{ version, dir, git }]`, indexed `byName`
741
+ // (normalized package name; the dir may hyphenate a snake_case name, and an older duplicate
742
+ // version lives in `<name>-<version>/`) and `byLib` (lib name, for the crates whose `[lib] name`
743
+ // differs: `md-5` → `md5`). `git`: whether the copy is of a git repository -- its
744
+ // `.cargo-checksum.json` has no package checksum, which a registry's always does -- or null
745
+ // without that file.
746
+ let vendorIndex = null
747
+ const vendored = () => {
748
+ if (vendorIndex === null) {
749
+ vendorIndex = { byName: new Map(), byLib: new Map() }
750
+ let dirs = []
751
+ try {
752
+ dirs = readdirSync(join(baseDir, vendorDir))
753
+ } catch { /* no vendor dir */ }
754
+ for (const d of dirs) {
755
+ if (d.startsWith('.')) continue // a directory source reads none of these
756
+ const m = manifestAt(`${vendorDir}/${d}`)
757
+ if (!m?.package) continue
758
+ const sums = checksumOf(m.dir)
759
+ const entry = { version: version(m), dir: m.dir, git: sums?.git ?? null }
760
+ for (const [index, key] of [[vendorIndex.byName, normName(m.package.name)], [vendorIndex.byLib, libName(m)]]) {
761
+ if (!index.has(key)) index.set(key, [])
762
+ index.get(key).push(entry)
763
+ }
764
+ }
765
+ }
766
+ return vendorIndex
767
+ }
768
+ // A vendored package's `.cargo-checksum.json`, parsed, or null when it has none.
769
+ const checksums = new Map()
770
+ const checksumOf = (dir) => cached(checksums, dir, () => {
771
+ const file = posix.join(dir, '.cargo-checksum.json')
772
+ const text = readText(diskHost, join(baseDir, file))
773
+ let value = null
774
+ if (text !== null) {
775
+ try {
776
+ value = JSON.parse(text)
777
+ } catch (err) {
778
+ throw new Error(`${file}: not JSON: ${err.message}`, { cause: err })
779
+ }
780
+ }
781
+ // A registry's copy has a package checksum; a git checkout's has none. `files`: each file's
782
+ // sha256, as `cargo vendor` listed them, path relative to the package.
783
+ return value === null ? null : { text, git: (value?.package ?? null) === null, files: isPlainObject(value?.files) ? value.files : {} }
784
+ })
785
+ // Throws when `fileRel`, a file of the vendored package at `dir`, isn't byte for byte (`buf`) the
786
+ // file its `.cargo-checksum.json` lists -- edited after `cargo vendor`: cargo refuses to build
787
+ // the package, and so the bundle does. A file the list doesn't name, or a package without one,
788
+ // passes. Once per file (`verified`): what passed isn't hashed again.
789
+ const verified = new Set()
790
+ const checkVendored = (dir, fileRel, buf) => {
791
+ if (verified.has(fileRel)) return
792
+ const listed = checksumOf(dir)?.files[fileRel.slice(dir.length + 1)]
793
+ if (typeof listed === 'string') {
794
+ const actual = createHash('sha256').update(buf).digest('hex')
795
+ if (actual !== listed) throw new Error(`${fileRel} isn't the file ${dir}/.cargo-checksum.json lists (sha256 ${actual}, listed ${listed}): changed since \`cargo vendor\`, which cargo refuses to build`)
796
+ }
797
+ verified.add(fileRel)
798
+ }
799
+ // The package the build is of: the first entry's package that isn't vendored -- the one cargo is
800
+ // run in (runDir), taken now that the vendor directory is known; null when no entry has one.
801
+ // Memoized.
802
+ let buildPkg
803
+ const buildPackage = () => {
804
+ if (buildPkg === undefined) buildPkg = entries.map(packageFor).find((m) => m && !isVendoredDir(m.dir)) ?? null
805
+ return buildPkg
806
+ }
807
+ // The workspace root of the build: its package's (rootManifestOf), inside the bundle root or
808
+ // above it (`outside`: read, never bundled), the package itself outside any workspace; the bundle
809
+ // root's manifest when no entry has a package. Its Cargo.lock is the build's, and its resolver
810
+ // and `[patch]` apply to every package of the build.
811
+ const buildWorkspaceRoot = () => {
812
+ const pkg = buildPackage()
813
+ return pkg === null ? readManifest('.') : rootManifestOf(pkg)
814
+ }
815
+ // The build's Cargo.lock, project-relative: beside its workspace root, as cargo finds it -- a
816
+ // vendored crate's own published lock plays no part; null when that root lies above the bundle
817
+ // root (its lockfile is cargo's, never read, and one beside the package isn't the build's).
818
+ const buildLockPath = () => {
819
+ const root = buildWorkspaceRoot()
820
+ return root?.outside === undefined ? posix.join(root?.dir ?? '.', 'Cargo.lock') : null
821
+ }
822
+ // The build's Cargo.lock (buildLockPath), read by @preventive/lockfile (`version = 3` or `4`; an
823
+ // older one, or a lock that isn't what cargo writes or could be read two ways, stops the build),
824
+ // as `{ file, text, lock, byId }` (`byId`: `name version` → the keys of that version's
825
+ // packages); null when there is none in the bundle root.
826
+ let lock
827
+ const lockfile = () => {
828
+ if (lock === undefined) {
829
+ const file = buildLockPath()
830
+ const text = file === null ? null : readText(diskHost, join(baseDir, file))
831
+ lock = null
832
+ if (text !== null) {
833
+ const parsed = readNamed(file, () => parseCargoLock(text))
834
+ const byId = new Map()
835
+ for (const [key, p] of Object.entries(parsed.packages)) {
836
+ const id = `${normName(p.name)} ${p.version}`
837
+ if (!byId.has(id)) byId.set(id, [])
838
+ byId.get(id).push(key)
839
+ }
840
+ lock = { file, lock: parsed, byId }
841
+ }
842
+ }
843
+ return lock
844
+ }
845
+ // `--cargo`: the graph and features as cargo resolved them, with registry packages it read from
846
+ // the registry cache matched to their `vendor/` copy by name + version.
847
+ const metadata = cargo
848
+ ? resolutionFromMetadata(runCargoMetadata(join(baseDir, runDir), { features, noDefaultFeatures, allFeatures, platform: targetInfo?.triple ?? null }), baseDir, {
849
+ locate: (name, ver) => (typeof name === 'string' ? vendored().byName.get(normName(name))?.find((c) => c.version === ver)?.dir ?? null : null),
850
+ })
851
+ : null
852
+ // Where the build takes its `[patch]` tables from, in the order they apply: the cargo configs'
853
+ // (`configs`, nearest first), as @preventive/lockfile reads them, then the workspace root
854
+ // manifest's (buildWorkspaceRoot), as `{ patch, at, from }`: `at(path)` the directory a patch's
855
+ // path names, project-relative (null outside the bundle root) -- relative to the directory
856
+ // holding a config's `.cargo`, or to the manifest's -- `from` the file. Throws a TomlError or
857
+ // LockfileError naming a config.
858
+ let patchMemo = null
859
+ const patchSources = () => {
860
+ if (patchMemo === null) {
861
+ const root = buildWorkspaceRoot()
862
+ patchMemo = [
863
+ ...configs.map((c) => ({ patch: readNamed(c.file, () => parseCargoConfig([c.text])).patch, at: (path) => projectRel(baseAbs, c.abs, path), from: c.file })),
864
+ ...(root ? [{ patch: root.cargo.patch, at: (path) => manifestPath(root, path), from: root.file }] : []),
865
+ ]
866
+ }
867
+ return patchMemo
868
+ }
869
+ // The `[patch]` of crate `crate` for what request `r` comes from, as `{ spec, dir, from }`, the
870
+ // first patchSources has -- `spec` as @preventive/lockfile reads it, `dir` the patch's directory
871
+ // for a path (null outside the bundle root), `from` the file saying so. Undefined when nothing
872
+ // patches it.
873
+ const patchFor = (crate, r) => {
874
+ for (const { patch, at, from } of patchSources()) {
875
+ for (const [source, specs] of Object.entries(patch)) {
876
+ if (!patchesSource(source, r)) continue
877
+ for (const [name, spec] of Object.entries(specs)) {
878
+ if (normName(name) !== crate) continue
879
+ return { spec, dir: spec.source.type === 'path' ? at(spec.source.path) : null, from }
880
+ }
881
+ }
882
+ }
883
+ return undefined
884
+ }
885
+ // Whether a `[patch.<source>]` table patches what request `r` comes from: crates.io's
886
+ // (`crates-io`, or its index URL) for a crates.io dependency, the registry's name or index for
887
+ // another, the repository for a git dependency -- URLs compared as cargo canonicalizes them (no
888
+ // trailing `/` or `.git`, the scheme's and host's case aside).
889
+ const patchesSource = (source, r) => {
890
+ if (r.source === 'git') return r.origin !== null && canonicalUrl(source) === canonicalUrl(r.origin)
891
+ if (r.source !== 'registry') return false
892
+ if (r.origin === null) return source === 'crates-io' || CRATES_IO_INDEXES.has(canonicalUrl(source))
893
+ return source === r.origin || canonicalUrl(source) === canonicalUrl(r.origin)
894
+ }
895
+ // Where a lockfile's package comes from: `path`, `git` or `registry` (sparse or not).
896
+ const sourceKind = (source) => (source === undefined ? 'path' : (source.startsWith('git+') ? 'git' : 'registry'))
897
+ // The keys of the build's lockfile that are package `m`: its path package, or for a vendored copy
898
+ // the locked package of its name and version from where the copy came from (a git checkout or a
899
+ // registry, see vendored).
900
+ const lockKeysOf = (lk, m) => {
901
+ const keys = lk.byId.get(`${normName(m.package.name)} ${version(m)}`) ?? []
902
+ if (!isVendoredDir(m.dir)) return keys.filter((k) => lk.lock.packages[k].source === undefined)
903
+ const git = checksumOf(m.dir)?.git
904
+ const kind = git === undefined ? null : (git ? 'git' : 'registry')
905
+ return keys.filter((k) => lk.lock.packages[k].source !== undefined && (kind === null || sourceKind(lk.lock.packages[k].source) === kind))
906
+ }
907
+ // Dependency → package, memoized per (package, request): the fixed-point loop asks many times.
908
+ const depTargets = new Map()
909
+ // The in-tree package a dependency of `m` resolves to through one of its tables (`request`): a
910
+ // `path` dep, else a `[patch]` path override (patchFor) whose version satisfies the requirement,
911
+ // else the vendored crate of that name from where the dependency says (a git dependency, or one a
912
+ // git `[patch]` replaces, takes a copy of a git checkout; any other a registry's) -- the version
913
+ // Cargo.lock records for `m` from that source when it satisfies the requirement, else the one
914
+ // vendored version that does. A package can depend on two versions of one crate (`borsh = "1"`
915
+ // beside `borsh0-9 = { package = "borsh", version = "0.9" }`, or one per table): the lock then
916
+ // lists both under it, and the requirement tells which is which. Null when it isn't in-tree -- a
917
+ // path (or patch) outside the bundle root, or no vendored copy -- and, warned, when the vendored
918
+ // copies don't settle it: the locked version isn't among them, none satisfies the requirement,
919
+ // or several do and no lock chooses. Cargo would build none of those from what the bundle holds,
920
+ // so none is guessed.
921
+ // What it warns of is said once, when it is first asked for unless `quiet`, else when
922
+ // warnDep asks: the replay lays out every declaration, and warns only of those the build links.
923
+ const resolveDep = (m, dep, request, { quiet = false } = {}) => {
924
+ const byRequest = cached(depTargets, m.dir, () => new Map())
925
+ const found = cached(byRequest, request, () => {
926
+ const warnings = []
927
+ return { t: resolveDepUncached(m, dep, request, (w) => warnings.push(w)), warnings }
928
+ })
929
+ if (!quiet) warnDep(m, request)
930
+ return found.t
931
+ }
932
+ const warnDep = (m, request) => {
933
+ for (const w of depTargets.get(m.dir)?.get(request)?.warnings.splice(0) ?? []) console.warn(w)
934
+ }
935
+ const resolveDepUncached = (m, dep, request, warn) => {
936
+ const asPackage = (dir) => {
937
+ const t = dir === null ? null : readManifest(dir)
938
+ return t?.package ? t : null
939
+ }
940
+ // cargo metadata knows exactly which package each dependency edge points at.
941
+ const known = metadata?.deps.get(m.dir)?.get(dep.key)
942
+ if (known !== undefined) return asPackage(known)
943
+ const who = `${m.package.name} ${version(m)}`
944
+ if (request.path) {
945
+ const t = asPackage(request.dir)
946
+ if (request.dir === null) warn(`[loader.cargo] ${who}'s dependency ${dep.name} is a path outside the bundle root: ${request.path}`)
947
+ else if (t === null) warn(`[loader.cargo] ${who}'s dependency ${dep.name} names ${request.dir}, which holds no Cargo.toml with a [package]`)
948
+ return t
949
+ }
950
+ const crate = normName(request.package)
951
+ const req = request.version
952
+ const fits = (ver) => req === null || satisfies(ver, req)
953
+ let kind = request.source
954
+ const patch = patchFor(crate, request)
955
+ if (patch?.spec.source.type === 'path') {
956
+ if (patch.dir === null) {
957
+ warn(`[loader.cargo] ${patch.from} patches ${crate} with a path outside the bundle root`)
958
+ return null
959
+ }
960
+ const t = asPackage(patch.dir)
961
+ // cargo uses a patch only where its version satisfies the requirement ("patch … was not used").
962
+ if (t !== null && fits(version(t))) return t
963
+ warn(t === null
964
+ ? `[loader.cargo] ${patch.from} patches ${crate} with ${patch.dir}, which holds no Cargo.toml with a [package]`
965
+ : `[loader.cargo] ${patch.from}'s patch of ${crate} (${version(t)}) doesn't satisfy ${who}'s requirement ${req}: not used`)
966
+ } else if (patch?.spec.source.type === 'git') {
967
+ kind = 'git'
968
+ }
969
+ // A copy of where the dependency comes from (a copy without `.cargo-checksum.json` could be either).
970
+ const candidates = (vendored().byName.get(crate) ?? []).filter((c) => c.git === null || c.git === (kind === 'git'))
971
+ if (candidates.length === 0) return null
972
+ const vendoredList = candidates.map((c) => c.version).toSorted((a, b) => b.localeCompare(a, 'en', { numeric: true })).join(', ')
973
+ const lk = lockfile()
974
+ if (lk) {
975
+ const pins = lockKeysOf(lk, m)
976
+ .flatMap((k) => lk.lock.packages[k].dependencies)
977
+ .map((k) => lk.lock.packages[k])
978
+ .filter((p) => normName(p.name) === crate && sourceKind(p.source) === kind)
979
+ .map((p) => p.version)
980
+ const want = pins.find(fits) ?? null
981
+ // A lock whose pin the requirement no longer allows is out of date: cargo would resolve
982
+ // again, so the requirement decides.
983
+ if (want === null && pins.length > 0) warn(`[loader.cargo] ${lk.file} pins ${who} to ${crate} ${pins.join(', ')}, which ${req} doesn't allow: the lock is out of date`)
984
+ if (want !== null) {
985
+ const hit = candidates.find((c) => c.version === want)
986
+ if (hit) return readManifest(hit.dir)
987
+ warn(`[loader.cargo] ${who} is locked to ${crate} ${want}, which isn't vendored (vendored: ${vendoredList})`)
988
+ return null
989
+ }
990
+ }
991
+ const fitting = candidates.filter((c) => fits(c.version))
992
+ if (fitting.length === 1) return readManifest(fitting[0].dir)
993
+ if (fitting.length === 0) warn(`[loader.cargo] No vendored version of ${crate} satisfies ${who}'s requirement ${req} (vendored: ${vendoredList})`)
994
+ else warn(`[loader.cargo] Several vendored versions of ${crate} satisfy ${who}'s requirement ${req ?? '*'} (${fitting.map((c) => c.version).join(', ')}) and no Cargo.lock says which`)
995
+ return null
996
+ }
997
+
998
+ // --- feature resolution
999
+
1000
+ // Cargo's feature resolver: v2 (edition 2021+, or `resolver = "2"`/`"3"`) leaves dev-dependencies
1001
+ // out of a normal build's unification, resolves what is built for the host apart from what is
1002
+ // built for the target, and ignores the tables of platforms not being built; v1 unifies them all.
1003
+ // It is the workspace's setting: the build's workspace root's (buildWorkspaceRoot) `resolver`,
1004
+ // else its edition -- never that of a vendored crate an entry is in, where cargo isn't run.
1005
+ let resolverMemo = null
1006
+ const resolverVersion = () => {
1007
+ if (resolverMemo === null) {
1008
+ const root = buildWorkspaceRoot()
1009
+ resolverMemo = root?.resolver ?? (Number(root?.package?.edition ?? 0) >= 2021 ? 2 : 1)
1010
+ }
1011
+ return resolverMemo
1012
+ }
1013
+ // The feature context a unit's code sees (resolver 2 keeps the host's apart; resolver 1 has one).
1014
+ const featureCtx = (c) => (resolverVersion() === 1 ? 'target' : c)
1015
+ const isProcMacroPkg = (m) => m?.lib.procMacro === true
1016
+ // The platform a context compiles for, as far as the loader knows it: the target's triple and
1017
+ // cfgs when `target` was given; the host's only when the target is the host (`host`), since the
1018
+ // machine that builds need not be the one that bundles.
1019
+ const platformInfo = (platform) => (platform === 'target' ? targetInfo : hostInfo)
1020
+ // Whether a table for platform `spec` (a `cfg(…)` or a triple; null or undefined for none) is
1021
+ // one for `platform`: `yes`, `no`, or `maybe` when its cfgs aren't known or don't decide it.
1022
+ const tableOn = (spec, platform) => {
1023
+ if (spec === null || spec === undefined) return 'yes'
1024
+ const info = platformInfo(platform)
1025
+ if (info === null) return 'maybe'
1026
+ const cfg = /^cfg\((.*)\)$/u.exec(spec)
1027
+ const holds = cfg ? evalCfg(cfg[1], { target: info.cfgs }) : spec === info.triple
1028
+ return holds === true ? 'yes' : (holds === false ? 'no' : 'maybe')
1029
+ }
1030
+ // A package in a context, as the resolutions key it: resolver 1's one context is `target`'s.
1031
+ const nodeKey = (c, dir) => `${featureCtx(c)}\0${dir}`
1032
+
1033
+ // Both feature resolutions of `graph` (linkCargo's, or the replay's) by cargo's resolver, for the
1034
+ // build of `packages`, members of it, per (context, dir) as nodeKey keys them -- `dirOf(key)` the
1035
+ // dir of a package key, undefined for none -- resolver 1's one set under `target`, the normal one
1036
+ // where there is one: `sure` with the features on for certain, `all` with those that may be, and
1037
+ // `result`, the resolver's own answer for `all`. Cargo's resolver takes the platforms it builds
1038
+ // for as known, and the loader may not know them: it runs against stand-ins (standInPlatform), a
1039
+ // target-specific table the loader can't decide on one -- a platform it doesn't know, a cfg it
1040
+ // can't decide there (`cfg(loom)`, which rustflags may set) -- off for `sure` and on for `all`,
1041
+ // the rest for cargo to decide as written. What cargo's resolver refuses -- a feature asked of a
1042
+ // package that hasn't it -- stops the build, naming `file`.
1043
+ const resolveGraph = (graph, packages, file, dirOf) => {
1044
+ const build = { packages, features, allFeatures, noDefaultFeatures, dev: entries.some((e) => linksDevDeps(e)), host: standInPlatform(MARK_HOST, hostInfo), targets: [standInPlatform(MARK_TARGET, targetInfo)] }
1045
+ let undecided = false
1046
+ const decided = (on) => retarget(graph, (spec) => {
1047
+ const at = (platform) => {
1048
+ if (tableOn(spec, platform) !== 'maybe') return /^cfg\((.*)\)$/su.exec(spec)?.[1] ?? `${MARK_NAME} = "${spec}"`
1049
+ undecided = true
1050
+ return String(on)
1051
+ }
1052
+ return `cfg(any(all(${MARK_TARGET}, ${at('target')}), all(${MARK_HOST}, ${at('host')})))`
1053
+ })
1054
+ const featuresOf = (g) => readNamed(file, () => resolveCargoFeatures(g, build))
1055
+ const sure = featuresOf(decided(false))
1056
+ const all = undecided ? featuresOf(decided(true)) : sure
1057
+ const byNode = (result) => {
1058
+ const out = new Map()
1059
+ for (const [key, { normal, host: onHost }] of Object.entries(result)) {
1060
+ const dir = dirOf(key)
1061
+ if (dir === undefined) continue
1062
+ if (onHost !== undefined) out.set(nodeKey('host', dir), new Set(onHost))
1063
+ if (normal !== undefined) out.set(nodeKey('target', dir), new Set(normal))
1064
+ }
1065
+ return out
1066
+ }
1067
+ return { sure: byNode(sure), all: byNode(all), result: all }
1068
+ }
1069
+
1070
+ // --- the replay: cargo's feature resolver, by @preventive/lockfile, over the manifests
1071
+
1072
+ // The graph cargo's feature resolver takes, as linkCargo gives one, laid out from the manifests
1073
+ // where the lockfile's can't be: each package in-tree the entries' packages (`roots`) reach, by
1074
+ // its dir, each of its declarations resolved as resolveDep resolves it -- a path, a `[patch]`, the
1075
+ // vendored copy the lockfile or the requirement picks -- but a dependency's own dev-dependencies,
1076
+ // which no build of the entries has. A declaration nothing in-tree answers resolves to a stand-in
1077
+ // of its own, which depends on nothing and has every feature its dependent may ask of it: the
1078
+ // declaration's, `default`, and what a `name/feature` or `name?/feature` of the package's
1079
+ // features, or of `--cargo-features` for a root, names. So does, unresolved (`unbuilt`), a
1080
+ // dev-dependency of a root that no entry is a test, bench or example of, under resolver 2: cargo's
1081
+ // resolver takes the dev targets of every member as built or none, and the entries build only
1082
+ // theirs. The members are the roots, and there is no root package: `--cargo-features` goes to each
1083
+ // that has it. `edges`: each declaration's resolved key, with its package and request (`m`, `d`,
1084
+ // `request`), and whether it is a stand-in.
1085
+ const replayGraph = (roots) => {
1086
+ const packages = Object.create(null)
1087
+ const edges = []
1088
+ const rootDirs = new Set(roots.map((m) => m.dir))
1089
+ const unbuiltDev = (m) => resolverVersion() !== 1 && !devRootDirs().has(m.dir)
1090
+ const queue = [...roots]
1091
+ for (const m of queue) {
1092
+ if (m.dir in packages) continue
1093
+ const pkg = m.cargo.package
1094
+ const dependencies = pkg.dependencies.map((d, index) => {
1095
+ if (d.kind === 'dev' && !rootDirs.has(m.dir)) return { ...d, resolved: undefined, active: false }
1096
+ const dep = m.deps.get(normName(d.name))
1097
+ const request = dep.kinds.get(d.target === undefined ? d.kind : `${d.kind}@${d.target}`)
1098
+ const unbuilt = d.kind === 'dev' && unbuiltDev(m)
1099
+ const t = unbuilt ? null : resolveDep(m, dep, request, { quiet: true })
1100
+ if (t !== null) queue.push(t)
1101
+ const key = t?.dir ?? `\0${m.dir}\0${index}`
1102
+ edges.push({ key, m, d, request, standIn: t === null, unbuilt })
1103
+ return { ...d, resolved: key, active: true }
1104
+ })
1105
+ packages[m.dir] = { name: pkg.name, version: pkg.version, source: undefined, checksum: undefined, manifest: pkg, dependencies }
1106
+ }
1107
+ for (const { key, m, d } of edges.filter((e) => e.standIn)) {
1108
+ const asked = new Set(['default', ...d.features])
1109
+ for (const value of [...Object.values(m.cargo.package.features).flat(), ...(rootDirs.has(m.dir) ? parseFeatureList(features) : [])]) {
1110
+ const named = DEP_FEATURE_RE.exec(value)
1111
+ if (named?.[1] === d.name) asked.add(named[3])
1112
+ }
1113
+ const manifest = { name: d.package, features: Object.fromEntries([...asked].map((f) => [f, []])), dependencies: [], procMacro: false, procMacroTarget: false }
1114
+ Object.setPrototypeOf(manifest.features, null)
1115
+ packages[key] = { name: d.package, version: '0.0.0', source: undefined, checksum: undefined, manifest, dependencies: [] }
1116
+ }
1117
+ return { graph: { resolver: resolverVersion(), root: undefined, members: [...rootDirs], packages }, edges }
1118
+ }
1119
+
1120
+ // The replay's resolution, as resolveGraph gives it, and the dependencies the build links that
1121
+ // nothing in-tree answers -- a stand-in it reaches -- as `dir key` → `{ dir, key, name, kinds }`,
1122
+ // the kinds of their tables (see lackingDependencies). What resolveDep warned of, it says now of
1123
+ // the declarations whose package the build reaches.
1124
+ const replay = () => {
1125
+ const roots = [...new Set(entries.map(packageFor).filter(Boolean))]
1126
+ if (roots.length === 0) return { resolved: { sure: new Map(), all: new Map() }, lacking: new Map() }
1127
+ const { graph, edges } = replayGraph(roots)
1128
+ const standIns = new Set(edges.filter((e) => e.standIn).map((e) => e.key))
1129
+ const resolved = resolveGraph(graph, graph.members, buildWorkspaceRoot()?.file ?? null, (key) => (standIns.has(key) ? undefined : key))
1130
+ const lacking = new Map()
1131
+ for (const { key, m, d, request, standIn, unbuilt } of edges) {
1132
+ if (unbuilt || !(key in resolved.result)) continue
1133
+ warnDep(m, request)
1134
+ if (!standIn) continue
1135
+ const id = `${m.dir}\0${normName(d.name)}`
1136
+ if (!lacking.has(id)) lacking.set(id, { dir: m.dir, key: normName(d.name), name: d.name, kinds: new Set() })
1137
+ lacking.get(id).kinds.add(d.kind)
1138
+ }
1139
+ return { resolved, lacking }
1140
+ }
1141
+
1142
+ // --- the exact resolution: cargo's resolver, by @preventive/lockfile
1143
+
1144
+ // The directories a workspace `members` pattern names, project-relative (glob's syntax, as cargo
1145
+ // takes it: `*`, `?` and `[…]` within a segment, `**` for any depth); null when it leaves the
1146
+ // bundle root.
1147
+ const subdirs = (dir) => {
1148
+ try {
1149
+ return readdirSync(join(baseDir, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (dir === '.' ? e.name : `${dir}/${e.name}`))
1150
+ } catch {
1151
+ return []
1152
+ }
1153
+ }
1154
+ const globDirs = (rootDir, pattern) => {
1155
+ let dirs = [rootDir]
1156
+ for (const seg of pattern.split('/').filter((x) => x !== '' && x !== '.')) {
1157
+ const next = []
1158
+ for (const dir of dirs) {
1159
+ if (seg === '**') {
1160
+ const all = [dir]
1161
+ for (let i = 0; i < all.length; i++) all.push(...subdirs(all[i]).filter((d) => !posix.basename(d).startsWith('.')))
1162
+ next.push(...all)
1163
+ } else if (/[*?[]/u.test(seg)) {
1164
+ const re = globSegment(seg)
1165
+ next.push(...subdirs(dir).filter((d) => re.test(posix.basename(d))))
1166
+ } else {
1167
+ const rel = normalizeRel(dir, seg)
1168
+ if (rel === null) return null
1169
+ next.push(rel)
1170
+ }
1171
+ }
1172
+ dirs = next
1173
+ }
1174
+ return dirs
1175
+ }
1176
+ // The workspace's members, project-relative, as cargo finds them: the root package, the
1177
+ // packages its `members` patterns name, and the path dependencies of members inside the root's
1178
+ // directory, less what `exclude` names; null when a pattern leaves the bundle root.
1179
+ const membersOf = (root) => {
1180
+ const ws = root.cargo.workspace
1181
+ if (ws === undefined) return [root.dir]
1182
+ const excluded = ws.exclude.map((e) => normalizeRel(root.dir, e)).filter((e) => e !== null)
1183
+ const isExcluded = (dir) => excluded.some((e) => dir === e || dir.startsWith(`${e}/`))
1184
+ const inRoot = (dir) => root.dir === '.' || dir === root.dir || dir.startsWith(`${root.dir}/`)
1185
+ const out = new Set(root.package ? [root.dir] : [])
1186
+ for (const pattern of ws.members) {
1187
+ const dirs = globDirs(root.dir, pattern)
1188
+ if (dirs === null) return null
1189
+ for (const dir of dirs) if (!isExcluded(dir) && readManifest(dir)?.package) out.add(dir)
1190
+ }
1191
+ // Set iteration is live: a path dependency added is walked in turn.
1192
+ for (const dir of out) {
1193
+ for (const d of readManifest(dir).deps.values()) {
1194
+ for (const r of d.kinds.values()) {
1195
+ const sub = r.dir
1196
+ if (sub !== null && inRoot(sub) && !isExcluded(sub) && readManifest(sub)?.package) out.add(sub)
1197
+ }
1198
+ }
1199
+ }
1200
+ return [...out]
1201
+ }
1202
+ // The build as cargo resolves it, where the loader holds all that takes -- the build's lockfile
1203
+ // and target, a manifest for every package the lockfile has (each path package inside the
1204
+ // bundle root, every other one vendored, `.cargo-checksum.json` and all) and entries whose
1205
+ // packages are workspace members: @preventive/lockfile lays the lockfile's graph over the
1206
+ // manifests (linkCargo) and turns on the features `cargo build -p <the entries' packages>` does
1207
+ // (resolveCargoFeatures), cargo's rules throughout -- each table's own source, a [patch] from the
1208
+ // root manifest or the cargo config, proc-macros and build-dependencies built for the host, the
1209
+ // command line's features handed out as cargo hands them out. What either refuses -- a lockfile
1210
+ // out of date with the manifests, a vendored copy whose checksum isn't the lockfile's, a feature
1211
+ // asked of a package that hasn't it -- stops the build. A table the loader can't decide on a
1212
+ // platform -- the host's, where the target isn't `host` -- is resolved both ways (resolveGraph).
1213
+ // `{ graph, dirOf, keyOf, resolved }` -- package key ↔ dir, and the features per (context, dir)
1214
+ // as resolveGraph gives them -- or null when something it takes is missing, for the replay to
1215
+ // decide: no lockfile, no target, a package not vendored, a path outside the bundle root.
1216
+ let exactMemo
1217
+ let exactWhy = null // why there is none, for the replay to say
1218
+ const exact = () => {
1219
+ if (exactMemo === undefined) exactMemo = exactUncached()
1220
+ return exactMemo
1221
+ }
1222
+ const notExact = (why) => {
1223
+ exactWhy = why
1224
+ return null
1225
+ }
1226
+ const exactUncached = () => {
1227
+ if (metadata) return null
1228
+ if (targetInfo === null) return notExact('no --cargo-target')
1229
+ const lockPath = buildLockPath()
1230
+ if (lockPath === null) return notExact('the workspace root lies above the bundle root')
1231
+ const lk = lockfile()
1232
+ if (lk === null) return notExact(`no ${lockPath}`)
1233
+ const root = buildWorkspaceRoot()
1234
+ if (root === null) return notExact('no package owns the entries')
1235
+ const memberDirs = membersOf(root)
1236
+ if (memberDirs === null) return notExact(`a members pattern of ${root.file} leaves the bundle root`)
1237
+ // The path packages: the members, the path dependencies of each, the path [patch]es.
1238
+ const keyOf = new Map() // dir -> package key
1239
+ const dirOf = new Map() // package key -> dir
1240
+ const queue = [...memberDirs]
1241
+ for (const { patch, at, from } of patchSources()) {
1242
+ for (const specs of Object.values(patch)) {
1243
+ for (const spec of Object.values(specs)) {
1244
+ if (spec.source.type !== 'path') continue
1245
+ const dir = at(spec.source.path)
1246
+ if (dir === null) return notExact(`${from} patches with a path outside the bundle root`)
1247
+ queue.push(dir)
1248
+ }
1249
+ }
1250
+ }
1251
+ for (const dir of queue) {
1252
+ if (keyOf.has(dir)) continue
1253
+ const m = readManifest(dir)
1254
+ if (!m?.package) return notExact(`${posix.join(dir, 'Cargo.toml')} isn't a package's`)
1255
+ const key = `${m.package.name} ${m.package.version}`
1256
+ keyOf.set(dir, key)
1257
+ dirOf.set(key, dir)
1258
+ for (const d of m.deps.values()) {
1259
+ for (const r of d.kinds.values()) {
1260
+ if (r.path === null) continue
1261
+ if (r.dir === null) return notExact(`${m.package.name}'s path dependency ${r.path} lies outside the bundle root`)
1262
+ queue.push(r.dir)
1263
+ }
1264
+ }
1265
+ }
1266
+ // The vendor directory as a directory source reads it, and every package of the lockfile there
1267
+ // (a copy the lockfile doesn't list needs no checksums: cargo never reads it).
1268
+ const vendor = Object.create(null)
1269
+ const held = new Map() // `name version` -> whether its copy has its checksums
1270
+ for (const sub of subdirs(vendorDir)) {
1271
+ const name = posix.basename(sub)
1272
+ const m = name.startsWith('.') ? null : manifestAt(sub)
1273
+ if (!m?.package) continue
1274
+ const sums = checksumOf(sub)
1275
+ const id = `${m.package.name} ${m.package.version}`
1276
+ held.set(id, held.get(id) === true || sums !== null)
1277
+ if (sums !== null) vendor[name] = { manifest: tableOf(sub).text, checksum: sums.text }
1278
+ }
1279
+ for (const [key, p] of Object.entries(lk.lock.packages)) {
1280
+ if (p.source === undefined && !dirOf.has(key)) return notExact(`${lk.file} locks the path package ${p.name} ${p.version}, which isn't in-tree`)
1281
+ if (p.source !== undefined && !held.has(`${p.name} ${p.version}`)) return notExact(`${p.name} ${p.version} is locked but not vendored`)
1282
+ if (p.source !== undefined && !held.get(`${p.name} ${p.version}`)) return notExact(`the vendored copy of ${p.name} ${p.version} has no .cargo-checksum.json`)
1283
+ }
1284
+ const copies = readNamed(lk.file, () => readCargoVendor(lk.lock, vendor))
1285
+ const manifestsByKey = Object.create(null)
1286
+ for (const [key, dir] of dirOf) manifestsByKey[key] = readManifest(dir).cargo
1287
+ for (const [key, { directory }] of Object.entries(copies)) {
1288
+ const dir = `${vendorDir}/${directory}`
1289
+ keyOf.set(dir, key)
1290
+ dirOf.set(key, dir)
1291
+ manifestsByKey[key] = readManifest(dir).cargo
1292
+ }
1293
+ const memberKeys = memberDirs.map((dir) => keyOf.get(dir))
1294
+ const entryKeys = [...new Set(entries.map(packageFor).filter(Boolean).map((m) => keyOf.get(m.dir)))]
1295
+ if (entryKeys.length === 0) return notExact('no package owns the entries')
1296
+ if (entryKeys.some((k) => !memberKeys.includes(k))) return notExact('an entry\'s package isn\'t a member of the workspace')
1297
+ const config = readNamed(configs[0]?.file ?? null, () => parseCargoConfig(configs.map((c) => c.text)))
1298
+ const graph = readNamed(lk.file, () => linkCargo(lk.lock, manifestsByKey, { workspace: root.cargo, members: memberKeys, config }))
1299
+ return { graph, dirOf, keyOf, resolved: resolveGraph(graph, entryKeys, root.file, (key) => dirOf.get(key)) }
1300
+ }
1301
+
1302
+ // Both resolutions, per (context, package dir): `sure` without the undecided tables, `all` with
1303
+ // them -- the exact resolution's where there is one, the replay's otherwise. `--cargo`: cargo
1304
+ // metadata gives one feature set per package, the union over everything cargo would build for
1305
+ // the workspace (dev and build dependencies, every platform), so a feature in it is on only maybe
1306
+ // in either context -- code a `cfg(not(feature = "x"))` keeps in some build is never dropped --
1307
+ // and one outside it is off. `lacking` (the replay's): the dependencies no package in-tree
1308
+ // answers, `dir key` → `{ dir, key, name, kinds }` -- the kinds of its active tables -- as
1309
+ // `cargo metadata`'s (see lackingDependencies).
1310
+ let resolved = null
1311
+ let lacking = null
1312
+ const ensureResolved = () => {
1313
+ if (resolved !== null) return resolved
1314
+ if (metadata) {
1315
+ const byNode = new Map([...metadata.enabled].flatMap(([dir, set]) => [[nodeKey('target', dir), set], [nodeKey('host', dir), set]]))
1316
+ resolved = { sure: new Map(), all: byNode }
1317
+ } else {
1318
+ resolved = exact()?.resolved ?? null
1319
+ if (resolved === null) ({ resolved, lacking } = replay())
1320
+ }
1321
+ return resolved
1322
+ }
1323
+ // Per node, the features on only maybe: in `all`, not in `sure` (one object per node).
1324
+ const maybeByNode = new Map()
1325
+ const maybeOf = (node) => cached(maybeByNode, node, () => {
1326
+ const { sure, all } = ensureResolved()
1327
+ const on = sure.get(node)
1328
+ const maybe = new Set([...(all.get(node) ?? [])].filter((f) => !on?.has(f)))
1329
+ return maybe.size === 0 ? null : maybe
1330
+ })
1331
+ // A package in the `all` resolution but not the `sure` one is built only maybe: nothing of its
1332
+ // is on for certain.
1333
+ const NO_FEATURES = new Set()
1334
+
1335
+ // --- cfgs a build may set
1336
+ let rustflagCfgs = null
1337
+ const cfgsSetMemo = new Map()
1338
+ // A crate root's text -- a build script's, a lib's -- and that of the modules it declares (`mod
1339
+ // probe;`, beside it), a few levels down: where its `cargo:rustc-cfg=` lines are.
1340
+ const crateTexts = (root) => {
1341
+ const texts = []
1342
+ const queue = [[root, 0]]
1343
+ const seen = new Set()
1344
+ for (let qi = 0; qi < queue.length; qi++) {
1345
+ const [rel, depth] = queue[qi]
1346
+ if (seen.has(rel)) continue
1347
+ seen.add(rel)
1348
+ const text = readText(diskHost, join(baseDir, rel))
1349
+ if (text === null) continue
1350
+ texts.push(text)
1351
+ if (depth >= 3) continue
1352
+ const dir = posix.dirname(rel)
1353
+ const sub = qi === 0 || posix.basename(rel) === 'mod.rs' ? dir : posix.join(dir, posix.basename(rel, '.rs'))
1354
+ for (const m of text.matchAll(/\bmod\s+([A-Za-z_]\w*)\s*;/gu)) {
1355
+ for (const f of [posix.join(sub, `${m[1]}.rs`), posix.join(sub, m[1], 'mod.rs')]) if (isFile(join(baseDir, f))) queue.push([f, depth + 1])
1356
+ }
1357
+ }
1358
+ return texts
1359
+ }
1360
+
1361
+ // What the build script of package `m` may print through the crates it calls (cfgsPrinted of
1362
+ // their libs: cfg_aliases' `cfg_aliases!` prints the names its input gives) -- its
1363
+ // build-dependencies for the host, and what they depend on in turn. One the bundle root lacks
1364
+ // may print any for all the loader knows, which needs no lib read. Asked once per package
1365
+ // (cfgsSetFor).
1366
+ const helpersPrinted = (m) => {
1367
+ const crates = new Map() // dir -> manifest
1368
+ if (linksLacking(m, 'build', crates)) return { names: new Set(), any: true }
1369
+ const names = new Set()
1370
+ for (const t of crates.values()) {
1371
+ const printed = libPrinted(t)
1372
+ if (printed.any) return printed
1373
+ for (const name of printed.names) names.add(name)
1374
+ }
1375
+ return { names, any: false }
1376
+ }
1377
+ // Whether package `p` links, through its `kind` dependencies for the host and what they depend
1378
+ // on in turn, a crate the bundle root lacks; those it links in-tree go into `crates` (dir →
1379
+ // manifest).
1380
+ const linksLacking = (p, kind, crates) => [...p.deps.values()].some((d) => [...d.kinds.values()].some((r) => {
1381
+ if (r.kind !== kind || tableOn(r.target, 'host') === 'no') return false
1382
+ const t = resolveDep(p, d, r)
1383
+ if (t === null) return true
1384
+ if (crates.has(t.dir)) return false
1385
+ crates.set(t.dir, t)
1386
+ return linksLacking(t, 'normal', crates)
1387
+ }))
1388
+ // What package `t`'s lib may print for a build script calling it (cfgsPrinted); memoized per
1389
+ // package.
1390
+ const libPrints = new Map()
1391
+ const libPrinted = (t) => cached(libPrints, t.dir, () => {
1392
+ const lib = libPath(t)
1393
+ return lib === null ? NO_CFGS_SET : cfgsPrinted(crateTexts(lib))
1394
+ })
1395
+
1396
+ // The in-tree crate roots a name resolves to from package `m` (null: no owning package), leaving
1397
+ // out `m`'s own lib (which depends on the asking file), as `[{ file, key }]`: the package its
1398
+ // dependency of that name resolves to (path dep, `[patch]`, or the vendored version Cargo.lock
1399
+ // says); a dependency whose lib is named that (`md-5` is used as `md5`); or -- for a file no
1400
+ // manifest claims -- the one vendored crate of that lib or package name. A package's own name for
1401
+ // a crate is its manifest's word: a declared dependency that doesn't resolve in-tree (a path
1402
+ // outside the bundle root, a version no vendored copy has) never falls back to some vendored copy
1403
+ // of that name, and a name it doesn't declare is no crate of its. Which of the dependency's
1404
+ // tables count is the asking code's (`roles`, one per crate root the file is compiled in): a build
1405
+ // script's are the `[build-dependencies]`, a test, bench or example target's the
1406
+ // `[dependencies]` and `[dev-dependencies]`, other code's the `[dependencies]` (any table when
1407
+ // none of those has it) -- each only where its platform may be one the code is compiled for
1408
+ // (`platforms`, the file's units'; a build script's the host): a table the target's cfgs rule out
1409
+ // is no dependency of a build for it, under either resolver. Tables that may each apply -- one
1410
+ // per platform when no target decides them, or one per role -- give one candidate each, `key`
1411
+ // naming which (`*` for the untargeted table, else its platform; `build-script ` before a build
1412
+ // script's beside another role's). Memoized per (package, name, roles, platforms): every file of
1413
+ // a package asks for the same few crates.
1414
+ const crateTargets = new Map()
1415
+ const depCrate = (m, norm, roles, platforms) => {
1416
+ const memo = `${m?.dir ?? ''}\0${norm}\0${roles.join()}\0${platforms.join()}`
1417
+ return cached(crateTargets, memo, () => depCrateUncached(m, norm, roles, platforms))
1418
+ }
1419
+ const ROLE_KINDS = { build: ['build'], test: ['normal', 'dev'], normal: ['normal'] }
1420
+ // The role of the code of crate root `root` in package `m` (see depCrate).
1421
+ const roleOf = (m, root) => (root === buildScript(m) ? 'build' : (linksDevDeps(root) ? 'test' : 'normal'))
1422
+ // Of a dependency's tables (`{ kind, target, … }`, the manifest's or the linked graph's, in
1423
+ // order), the packages the asking code links: for each role, the first table of each platform
1424
+ // that may apply and that `find` finds a package for, as `{ t, key }` (see depCrate). `any`: a
1425
+ // role with no table of its kinds takes any table instead (a declared name).
1426
+ const linkedTables = (all, roles, platforms, find, any) => {
1427
+ const out = []
1428
+ const seen = new Set()
1429
+ for (const role of roles) {
1430
+ const own = all.filter((r) => ROLE_KINDS[role].includes(r.kind))
1431
+ const plats = role === 'build' ? ['host'] : platforms
1432
+ for (const r of own.length === 0 && any ? all : own) {
1433
+ const on = r.target ?? null
1434
+ const key = `${role === 'build' && roles.length > 1 ? 'build-script ' : ''}${on === null ? '*' : (/^cfg\((.*)\)$/su.exec(on)?.[1] ?? on)}`
1435
+ if (seen.has(key) || !plats.some((p) => tableOn(on, r.kind === 'build' ? 'host' : p) !== 'no')) continue
1436
+ const t = find(r)
1437
+ if (t === null) continue
1438
+ seen.add(key)
1439
+ out.push({ t, key })
1440
+ }
1441
+ }
1442
+ return out
1443
+ }
1444
+ // The candidates as crate roots, one per file.
1445
+ const asRoots = (found) => {
1446
+ const out = []
1447
+ for (const { t, key } of found) {
1448
+ const file = libPath(t)
1449
+ if (file !== null && !out.some((c) => c.file === file)) out.push({ file, key })
1450
+ }
1451
+ return out
1452
+ }
1453
+ // depCrate from the exact resolution: the package's dependency of that key, else the unrenamed
1454
+ // one whose lib is named that, as the lockfile resolves it.
1455
+ const exactCrate = (x, key, norm, roles, platforms) => {
1456
+ const deps = x.graph.packages[key].dependencies
1457
+ const packageOf = (d) => (d.resolved === undefined ? null : readManifest(x.dirOf.get(d.resolved)))
1458
+ const declared = deps.filter((d) => normName(d.name) === norm)
1459
+ if (declared.length > 0) return asRoots(linkedTables(declared, roles, platforms, packageOf, true))
1460
+ return asRoots(linkedTables(deps.filter((d) => d.name === d.package && d.resolved !== undefined && libName(packageOf(d)) === norm), roles, platforms, packageOf, false))
1461
+ }
1462
+ const depCrateUncached = (m, norm, roles, platforms) => {
1463
+ const x = m ? exact() : null
1464
+ const key = x?.keyOf.get(m.dir)
1465
+ if (key !== undefined) return exactCrate(x, key, norm, roles, platforms)
1466
+ if (m) {
1467
+ const d = m.deps.get(norm)
1468
+ if (d) return asRoots(linkedTables([...d.kinds.values()], roles, platforms, (r) => resolveDep(m, d, r), true))
1469
+ // A dependency whose lib is named `norm`: only one that may be is resolved (a path
1470
+ // dependency's manifest says, a vendored crate's must have that lib name), so a name that is
1471
+ // no crate (`u8::MAX`) resolves -- and warns about -- nothing.
1472
+ const mayBe = (r) => {
1473
+ if (!r.path) return vendored().byLib.has(norm)
1474
+ const t = r.dir === null ? null : readManifest(r.dir)
1475
+ return Boolean(t?.package) && libName(t) === norm
1476
+ }
1477
+ for (const other of m.deps.values()) {
1478
+ const named = [...other.kinds.values()].filter((r) => !r.renamed && mayBe(r)) // a rename is used by its key, not its lib name
1479
+ const found = linkedTables(named, roles, platforms, (r) => {
1480
+ const t = resolveDep(m, other, r)
1481
+ return t && libName(t) === norm ? t : null
1482
+ }, false)
1483
+ if (found.length > 0) return asRoots(found)
1484
+ }
1485
+ return []
1486
+ }
1487
+ const { byLib, byName } = vendored()
1488
+ const found = new Set()
1489
+ for (const index of [byLib, byName]) for (const c of index.get(norm) ?? []) found.add(c.dir)
1490
+ if (found.size > 1) {
1491
+ console.warn(`[loader.cargo] Several vendored crates are named ${norm} (${[...found].join(', ')}) and no manifest says which`)
1492
+ return []
1493
+ }
1494
+ return asRoots([...found].map((dir) => ({ t: readManifest(dir), key: '*' })))
1495
+ }
1496
+
1497
+ const crateCandidates = (name, fromFile, { roots = null, units = null } = {}) => {
1498
+ const norm = normName(name)
1499
+ const m = packageFor(fromFile)
1500
+ if (m && libName(m) === norm) {
1501
+ const lib = libPath(m)
1502
+ if (lib && lib !== fromFile) return [{ file: lib, key: '*' }]
1503
+ }
1504
+ const roles = m === null ? ['normal'] : [...new Set((roots === null || roots.size === 0 ? [fromFile] : [...roots]).map((r) => roleOf(m, r)))]
1505
+ const platforms = units === null || units.size === 0 ? ['target'] : [...new Set([...units].map(unitPlatform))]
1506
+ return depCrate(m, norm, roles, platforms)
1507
+ }
1508
+
1509
+ // The files describing package `m`'s build, on disk (see buildFilesFor).
1510
+ const buildFiles = (m) => {
1511
+ const out = [{ path: posix.join(m.dir, 'Cargo.toml'), kind: 'manifest' }]
1512
+ if (isVendoredDir(m.dir)) {
1513
+ const checksum = posix.join(m.dir, '.cargo-checksum.json')
1514
+ if (isFile(join(baseDir, checksum))) out.push({ path: checksum, kind: 'checksum' })
1515
+ return out
1516
+ }
1517
+ const ws = workspaceFor(m.dir)
1518
+ if (ws && ws.dir !== m.dir) out.push({ path: posix.join(ws.dir, 'Cargo.toml'), kind: 'manifest' })
1519
+ // The build's lockfile and cargo configs, not the package's own: cargo reads only those.
1520
+ const lockPath = buildLockPath()
1521
+ if (lockPath !== null && isFile(join(baseDir, lockPath))) out.push({ path: lockPath, kind: 'lock' })
1522
+ for (const c of configs) if (c.dir !== null) out.push({ path: c.file, kind: 'config' })
1523
+ return out
1524
+ }
1525
+
1526
+ return {
1527
+ // Where `cargo vendor` put the registry crates (project-relative): `vendor`, or what
1528
+ // .cargo/config.toml names. A package under it is a vendored dependency.
1529
+ vendorDir,
1530
+ // Identity of the package owning `fileRel` -- `{ dir, name, version }` from the nearest
1531
+ // Cargo.toml with a [package] -- or null when no manifest claims it.
1532
+ packageInfo(fileRel) {
1533
+ const m = packageFor(fileRel)
1534
+ return m ? { dir: m.dir, name: m.package.name, version: version(m) } : null
1535
+ },
1536
+ // Whether `fileRel` is the lib target root of the package owning it (a crate root by role,
1537
+ // whatever its name).
1538
+ isLibRoot(fileRel) {
1539
+ const m = packageFor(fileRel)
1540
+ return m !== null && libPath(m) === fileRel
1541
+ },
1542
+ // The in-tree crate roots a crate name as used in source (`use name::…`, `extern crate name`)
1543
+ // names from `fromFile`, as `[{ file, key }]`: the owning package's own lib (from another of
1544
+ // its files), else what depCrate finds -- for the code of each crate root the file is compiled
1545
+ // in (`roots`; the file's own role without them), on the platforms of its compile units
1546
+ // (`units`; the target's without them). Several when tables that may each apply name different
1547
+ // packages (see depCrate); none for anything else (a registry dep that isn't vendored, std, a
1548
+ // name that isn't a crate).
1549
+ crateCandidates,
1550
+ // The first of crateCandidates, or null.
1551
+ resolveCrate: (name, fromFile, opts) => crateCandidates(name, fromFile, opts)[0]?.file ?? null,
1552
+ // Where the features come from: `{ mode, why }` -- `cargo` (cargo's resolver over the
1553
+ // lockfile), `metadata` (`cargo metadata`), or `replay` (the manifests replayed) with `why`
1554
+ // cargo's resolver couldn't run.
1555
+ resolution() {
1556
+ ensureResolved()
1557
+ return metadata ? { mode: 'metadata', why: null } : (exact() === null ? { mode: 'replay', why: exactWhy } : { mode: 'cargo', why: null })
1558
+ },
1559
+ isTestTarget,
1560
+ // The dependencies the build links that nothing in-tree answers, for the packages at `dirs` (the
1561
+ // bundled ones): declared, and active in the build as far as the resolution tells (its tables
1562
+ // for the build's platforms, an optional one turned on; maybe ones too), but a registry crate
1563
+ // not vendored, a path outside the bundle root -- as `{ key, name, from }` (the dependency's
1564
+ // key and name in its dependent's manifest, and that package as `name version`). Code may name
1565
+ // such a crate by another name than the manifest's (`md-5` is used as `md5`), or not at all, so
1566
+ // the bundle can't tell from the code that it lacks one. Dev-dependencies count only for a
1567
+ // package an entry is a test, bench or example target of; build-dependencies only for a
1568
+ // package with a build script, and with `buildScripts` (build scripts bundled). Cargo's
1569
+ // resolver needs every locked package in-tree, so it has none.
1570
+ lackingDependencies(dirs, { buildScripts = false } = {}) {
1571
+ ensureResolved()
1572
+ const counts = (kind, dir) => (kind === 'dev' ? devRootDirs().has(dir) : (kind !== 'build' || (buildScripts && buildScript(readManifest(dir)) !== null)))
1573
+ const out = []
1574
+ const seen = new Set()
1575
+ for (const { dir, key, name, kinds } of [...(lacking?.values() ?? []), ...(metadata?.lacking ?? [])]) {
1576
+ const m = dirs.has(dir) ? readManifest(dir) : null
1577
+ const from = m?.package ? `${m.package.name} ${version(m)}` : null
1578
+ if (from === null || seen.has(`${from}\0${key}`) || ![...kinds].some((k) => counts(k, dir))) continue
1579
+ seen.add(`${from}\0${key}`)
1580
+ out.push({ key, name, from })
1581
+ }
1582
+ return out
1583
+ },
1584
+ // Whether `fromFile`'s package declares a dependency of that name (in any table, by the key
1585
+ // code uses): then the name is that crate -- rustc refuses a `use` path whose lead a glob
1586
+ // import also provides, as ambiguous (E0659) -- in-tree or not.
1587
+ declaresCrate(name, fromFile) {
1588
+ return packageFor(fromFile)?.deps.has(normName(name)) === true
1589
+ },
1590
+ // The build's target: its triple and cfg set (`unix`, `target_os="linux"`, … as rustc prints
1591
+ // them), or null for both when none was given.
1592
+ targetTriple: targetInfo?.triple ?? null,
1593
+ targetCfgs: targetInfo?.cfgs ?? null,
1594
+ // The files that describe the build of `fileRel`'s package, project-relative, as `{ path,
1595
+ // kind }`, those on disk. A vendored package's: its Cargo.toml (`manifest`) and the
1596
+ // `.cargo-checksum.json` cargo checks its files against (`checksum`) -- the lock and config a
1597
+ // registry crate was published with play no part in a build that depends on it. Any other
1598
+ // package's: its own Cargo.toml and, inside the bundle root, the workspace's above it
1599
+ // (`manifest`); and the build's Cargo.lock (`lock`) and the cargo configs it reads (`config`,
1600
+ // see `runDir`), those inside the bundle root -- not a path dependency's own, which no build
1601
+ // of the entries reads. Empty for a file no manifest claims.
1602
+ buildFilesFor(fileRel) {
1603
+ const m = packageFor(fileRel)
1604
+ if (!m) return []
1605
+ m.buildFiles ??= buildFiles(m)
1606
+ return m.buildFiles
1607
+ },
1608
+ // Throws when `fileRel` is a vendored package's file whose bytes (`buf`) aren't the ones its
1609
+ // `.cargo-checksum.json` lists -- edited after `cargo vendor`: cargo refuses to build it, and
1610
+ // so the bundle does. A file the list doesn't name, or a package without one, passes; one
1611
+ // checked already -- a vendored Cargo.toml, as its package was read -- isn't hashed again.
1612
+ checkVendoredFile(fileRel, buf) {
1613
+ const m = packageFor(fileRel)
1614
+ if (m !== null && isVendoredDir(m.dir)) checkVendored(m.dir, fileRel, buf)
1615
+ },
1616
+ // Whether `fileRel` belongs to a vendored package (a registry crate `cargo vendor` copied in).
1617
+ isVendored(fileRel) {
1618
+ const m = packageFor(fileRel)
1619
+ return m !== null && isVendoredDir(m.dir)
1620
+ },
1621
+ // The build script of `fileRel`'s package, project-relative, when there is one on disk:
1622
+ // `[package] build = "…"`, else `build.rs` beside the manifest; `build = false` means none. A
1623
+ // vendored package's may not name a file outside the package (a published crate never does).
1624
+ buildScriptOf(fileRel) {
1625
+ const m = packageFor(fileRel)
1626
+ return m ? buildScript(m) : null
1627
+ },
1628
+ // The custom cfgs the build of `fileRel`'s package may set (a `--cfg` a default build lacks is
1629
+ // otherwise presumed off): `{ names, any }` -- the names its build script prints as
1630
+ // `cargo:rustc-cfg=…`, itself or through a crate it calls, and those the rustflags set, and
1631
+ // whether the script may set one the loader can't read. One object per package (and one for
1632
+ // every package that sets none).
1633
+ cfgsSetFor(fileRel) {
1634
+ const m = packageFor(fileRel)
1635
+ return cached(cfgsSetMemo, m?.dir ?? '\0', () => {
1636
+ rustflagCfgs ??= rustflagsCfgsOf(configs)
1637
+ const script = m ? buildScript(m) : null
1638
+ const printed = script === null ? NO_CFGS_SET : cfgsPrinted(crateTexts(script))
1639
+ const helpers = script === null || printed.any ? NO_CFGS_SET : helpersPrinted(m)
1640
+ const any = printed.any || helpers.any
1641
+ const names = new Set([...rustflagCfgs, ...printed.names, ...helpers.names])
1642
+ return names.size === 0 && !any ? NO_CFGS_SET : { names, any }
1643
+ })
1644
+ },
1645
+ // The features on for certain for the package owning `fileRel`, compiled as `unit` (see
1646
+ // unitOfCrate), in the build of the root packages; null when that is unknown: no owning
1647
+ // manifest, no root package to resolve from, or a package the resolved build doesn't pull in
1648
+ // (its gated code is then kept, not dropped).
1649
+ featuresFor(fileRel, unit = TARGET_UNIT) {
1650
+ const m = packageFor(fileRel)
1651
+ if (!m) return null
1652
+ const node = nodeKey(unitFeatureCtx(unit), m.dir)
1653
+ const { sure, all } = ensureResolved()
1654
+ return sure.get(node) ?? (all.has(node) ? NO_FEATURES : null)
1655
+ },
1656
+ // The features on only in some of the builds the loader can't tell apart (a target-specific
1657
+ // table it can't decide, or what only such a table requests), for the same package and unit;
1658
+ // null when there are none.
1659
+ maybeFeaturesFor(fileRel, unit = TARGET_UNIT) {
1660
+ const m = packageFor(fileRel)
1661
+ if (!m) return null
1662
+ return maybeOf(nodeKey(unitFeatureCtx(unit), m.dir))
1663
+ },
1664
+ // Every package of a context the build may pull in: dir -> { on, maybe }, the features on for
1665
+ // certain and those on only maybe (a package only an undecided table pulls in has every
1666
+ // feature of its maybe). For diagnostics and tests.
1667
+ featureResolution(context = 'target') {
1668
+ const out = new Map()
1669
+ const prefix = nodeKey(context, '')
1670
+ for (const node of ensureResolved().all.keys()) {
1671
+ if (node.startsWith(prefix)) out.set(node.slice(prefix.length), { on: ensureResolved().sure.get(node) ?? NO_FEATURES, maybe: maybeOf(node) ?? NO_FEATURES })
1672
+ }
1673
+ return out
1674
+ },
1675
+ // Every resolved package of a context: dir -> Set<feature on for certain>. For diagnostics and tests.
1676
+ resolvedFeatures(context = 'target') {
1677
+ const out = new Map()
1678
+ const prefix = nodeKey(context, '')
1679
+ for (const [node, set] of ensureResolved().sure) if (node.startsWith(prefix)) out.set(node.slice(prefix.length), set)
1680
+ return out
1681
+ },
1682
+ // The compile unit of a crate root `libFile` named from code compiled as `fromUnit`: a proc-macro
1683
+ // crate, and anything code compiled for the host names (a build script's build-dependencies,
1684
+ // a proc-macro's dependencies), is built for the host in the host's feature context; else the
1685
+ // namer's unit.
1686
+ unitOfCrate(libFile, fromUnit = TARGET_UNIT) {
1687
+ return isProcMacroPkg(packageFor(libFile)) || unitPlatform(fromUnit) === 'host' ? HOST_UNIT : fromUnit
1688
+ },
1689
+ // The unit of a build script of a package compiled as `pkgUnit`: for the host, with the
1690
+ // package's features.
1691
+ buildScriptUnit(pkgUnit = TARGET_UNIT) {
1692
+ return unitKey(unitFeatureCtx(pkgUnit), 'host')
1693
+ },
1694
+ // The cfgs and triple of the platform `unit` compiles for, as far as known (see platformInfo).
1695
+ platformOf(unit = TARGET_UNIT) {
1696
+ return platformInfo(unitPlatform(unit))
1697
+ },
1698
+ }
1699
+ }