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

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,1716 @@
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
+ // Where `cargo vendor` copied the vendored package at `dir` from, as its `.cargo-checksum.json`
786
+ // tells: `registry` or `git` (a git checkout); null without that file, when it could be either.
787
+ const copySource = (dir) => {
788
+ const git = checksumOf(dir)?.git
789
+ return git === undefined ? null : (git ? 'git' : 'registry')
790
+ }
791
+ // Throws when `fileRel`, a file of the vendored package at `dir`, isn't byte for byte (`buf`) the
792
+ // file its `.cargo-checksum.json` lists -- edited after `cargo vendor`: cargo refuses to build
793
+ // the package, and so the bundle does. A file the list doesn't name, or a package without one,
794
+ // passes. Once per file (`verified`): what passed isn't hashed again.
795
+ const verified = new Set()
796
+ const checkVendored = (dir, fileRel, buf) => {
797
+ if (verified.has(fileRel)) return
798
+ const listed = checksumOf(dir)?.files[fileRel.slice(dir.length + 1)]
799
+ if (typeof listed === 'string') {
800
+ const actual = createHash('sha256').update(buf).digest('hex')
801
+ 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`)
802
+ }
803
+ verified.add(fileRel)
804
+ }
805
+ // The package the build is of: the first entry's package that isn't vendored -- the one cargo is
806
+ // run in (runDir), taken now that the vendor directory is known; null when no entry has one.
807
+ // Memoized.
808
+ let buildPkg
809
+ const buildPackage = () => {
810
+ if (buildPkg === undefined) buildPkg = entries.map(packageFor).find((m) => m && !isVendoredDir(m.dir)) ?? null
811
+ return buildPkg
812
+ }
813
+ // The workspace root of the build: its package's (rootManifestOf), inside the bundle root or
814
+ // above it (`outside`: read, never bundled), the package itself outside any workspace; the bundle
815
+ // root's manifest when no entry has a package. Its Cargo.lock is the build's, and its resolver
816
+ // and `[patch]` apply to every package of the build.
817
+ const buildWorkspaceRoot = () => {
818
+ const pkg = buildPackage()
819
+ return pkg === null ? readManifest('.') : rootManifestOf(pkg)
820
+ }
821
+ // The build's Cargo.lock, project-relative: beside its workspace root, as cargo finds it -- a
822
+ // vendored crate's own published lock plays no part; null when that root lies above the bundle
823
+ // root (its lockfile is cargo's, never read, and one beside the package isn't the build's).
824
+ const buildLockPath = () => {
825
+ const root = buildWorkspaceRoot()
826
+ return root?.outside === undefined ? posix.join(root?.dir ?? '.', 'Cargo.lock') : null
827
+ }
828
+ // The build's Cargo.lock (buildLockPath), read by @preventive/lockfile (`version = 3` or `4`; an
829
+ // older one, or a lock that isn't what cargo writes or could be read two ways, stops the build),
830
+ // as `{ file, text, lock, byId }` (`byId`: `name version` → the keys of that version's
831
+ // packages); null when there is none in the bundle root.
832
+ let lock
833
+ const lockfile = () => {
834
+ if (lock === undefined) {
835
+ const file = buildLockPath()
836
+ const text = file === null ? null : readText(diskHost, join(baseDir, file))
837
+ lock = null
838
+ if (text !== null) {
839
+ const parsed = readNamed(file, () => parseCargoLock(text))
840
+ const byId = new Map()
841
+ for (const [key, p] of Object.entries(parsed.packages)) {
842
+ const id = `${normName(p.name)} ${p.version}`
843
+ if (!byId.has(id)) byId.set(id, [])
844
+ byId.get(id).push(key)
845
+ }
846
+ lock = { file, lock: parsed, byId }
847
+ }
848
+ }
849
+ return lock
850
+ }
851
+ // `--cargo`: the graph and features as cargo resolved them, with registry packages it read from
852
+ // the registry cache matched to their `vendor/` copy by name + version.
853
+ const metadata = cargo
854
+ ? resolutionFromMetadata(runCargoMetadata(join(baseDir, runDir), { features, noDefaultFeatures, allFeatures, platform: targetInfo?.triple ?? null }), baseDir, {
855
+ locate: (name, ver) => (typeof name === 'string' ? vendored().byName.get(normName(name))?.find((c) => c.version === ver)?.dir ?? null : null),
856
+ })
857
+ : null
858
+ // Where the build takes its `[patch]` tables from, in the order they apply: the cargo configs'
859
+ // (`configs`, nearest first), as @preventive/lockfile reads them, then the workspace root
860
+ // manifest's (buildWorkspaceRoot), as `{ patch, at, from }`: `at(path)` the directory a patch's
861
+ // path names, project-relative (null outside the bundle root) -- relative to the directory
862
+ // holding a config's `.cargo`, or to the manifest's -- `from` the file. Throws a TomlError or
863
+ // LockfileError naming a config.
864
+ let patchMemo = null
865
+ const patchSources = () => {
866
+ if (patchMemo === null) {
867
+ const root = buildWorkspaceRoot()
868
+ patchMemo = [
869
+ ...configs.map((c) => ({ patch: readNamed(c.file, () => parseCargoConfig([c.text])).patch, at: (path) => projectRel(baseAbs, c.abs, path), from: c.file })),
870
+ ...(root ? [{ patch: root.cargo.patch, at: (path) => manifestPath(root, path), from: root.file }] : []),
871
+ ]
872
+ }
873
+ return patchMemo
874
+ }
875
+ // The `[patch]` of crate `crate` for what request `r` comes from, as `{ spec, dir, from }`, the
876
+ // first patchSources has -- `spec` as @preventive/lockfile reads it, `dir` the patch's directory
877
+ // for a path (null outside the bundle root), `from` the file saying so. Undefined when nothing
878
+ // patches it.
879
+ const patchFor = (crate, r) => {
880
+ for (const { patch, at, from } of patchSources()) {
881
+ for (const [source, specs] of Object.entries(patch)) {
882
+ if (!patchesSource(source, r)) continue
883
+ for (const [name, spec] of Object.entries(specs)) {
884
+ if (normName(name) !== crate) continue
885
+ return { spec, dir: spec.source.type === 'path' ? at(spec.source.path) : null, from }
886
+ }
887
+ }
888
+ }
889
+ return undefined
890
+ }
891
+ // Whether a `[patch.<source>]` table patches what request `r` comes from: crates.io's
892
+ // (`crates-io`, or its index URL) for a crates.io dependency, the registry's name or index for
893
+ // another, the repository for a git dependency -- URLs compared as cargo canonicalizes them (no
894
+ // trailing `/` or `.git`, the scheme's and host's case aside).
895
+ const patchesSource = (source, r) => {
896
+ if (r.source === 'git') return r.origin !== null && canonicalUrl(source) === canonicalUrl(r.origin)
897
+ if (r.source !== 'registry') return false
898
+ if (r.origin === null) return source === 'crates-io' || CRATES_IO_INDEXES.has(canonicalUrl(source))
899
+ return source === r.origin || canonicalUrl(source) === canonicalUrl(r.origin)
900
+ }
901
+ // Where a lockfile's package comes from: `path`, `git` or `registry` (sparse or not).
902
+ const sourceKind = (source) => (source === undefined ? 'path' : (source.startsWith('git+') ? 'git' : 'registry'))
903
+ // The keys of the build's lockfile that are package `m`: its path package, or for a vendored copy
904
+ // the locked package of its name and version from where the copy came from (a git checkout or a
905
+ // registry, see vendored).
906
+ const lockKeysOf = (lk, m) => {
907
+ const keys = lk.byId.get(`${normName(m.package.name)} ${version(m)}`) ?? []
908
+ if (!isVendoredDir(m.dir)) return keys.filter((k) => lk.lock.packages[k].source === undefined)
909
+ const kind = copySource(m.dir)
910
+ return keys.filter((k) => lk.lock.packages[k].source !== undefined && (kind === null || sourceKind(lk.lock.packages[k].source) === kind))
911
+ }
912
+ // Dependency → package, memoized per (package, request): the fixed-point loop asks many times.
913
+ const depTargets = new Map()
914
+ // The in-tree package a dependency of `m` resolves to through one of its tables (`request`): a
915
+ // `path` dep, else a `[patch]` path override (patchFor) whose version satisfies the requirement,
916
+ // else the vendored crate of that name from where the dependency says (a git dependency, or one a
917
+ // git `[patch]` replaces, takes a copy of a git checkout; any other a registry's) -- the version
918
+ // Cargo.lock records for `m` from that source when it satisfies the requirement, else the one
919
+ // vendored version that does. A package can depend on two versions of one crate (`borsh = "1"`
920
+ // beside `borsh0-9 = { package = "borsh", version = "0.9" }`, or one per table): the lock then
921
+ // lists both under it, and the requirement tells which is which. Null when it isn't in-tree -- a
922
+ // path (or patch) outside the bundle root, or no vendored copy -- and, warned, when the vendored
923
+ // copies don't settle it: the locked version isn't among them, none satisfies the requirement,
924
+ // or several do and no lock chooses. Cargo would build none of those from what the bundle holds,
925
+ // so none is guessed.
926
+ // What it warns of is said once, when it is first asked for unless `quiet`, else when
927
+ // warnDep asks: the replay lays out every declaration, and warns only of those the build links.
928
+ const resolveDep = (m, dep, request, { quiet = false } = {}) => {
929
+ const byRequest = cached(depTargets, m.dir, () => new Map())
930
+ const found = cached(byRequest, request, () => {
931
+ const warnings = []
932
+ return { t: resolveDepUncached(m, dep, request, (w) => warnings.push(w)), warnings }
933
+ })
934
+ if (!quiet) warnDep(m, request)
935
+ return found.t
936
+ }
937
+ const warnDep = (m, request) => {
938
+ for (const w of depTargets.get(m.dir)?.get(request)?.warnings.splice(0) ?? []) console.warn(w)
939
+ }
940
+ const resolveDepUncached = (m, dep, request, warn) => {
941
+ const asPackage = (dir) => {
942
+ const t = dir === null ? null : readManifest(dir)
943
+ return t?.package ? t : null
944
+ }
945
+ // cargo metadata knows exactly which package each dependency edge points at.
946
+ const known = metadata?.deps.get(m.dir)?.get(dep.key)
947
+ if (known !== undefined) return asPackage(known)
948
+ const who = `${m.package.name} ${version(m)}`
949
+ if (request.path) {
950
+ const t = asPackage(request.dir)
951
+ if (request.dir === null) warn(`[loader.cargo] ${who}'s dependency ${dep.name} is a path outside the bundle root: ${request.path}`)
952
+ else if (t === null) warn(`[loader.cargo] ${who}'s dependency ${dep.name} names ${request.dir}, which holds no Cargo.toml with a [package]`)
953
+ return t
954
+ }
955
+ const crate = normName(request.package)
956
+ const req = request.version
957
+ const fits = (ver) => req === null || satisfies(ver, req)
958
+ let kind = request.source
959
+ const patch = patchFor(crate, request)
960
+ if (patch?.spec.source.type === 'path') {
961
+ if (patch.dir === null) {
962
+ warn(`[loader.cargo] ${patch.from} patches ${crate} with a path outside the bundle root`)
963
+ return null
964
+ }
965
+ const t = asPackage(patch.dir)
966
+ // cargo uses a patch only where its version satisfies the requirement ("patch … was not used").
967
+ if (t !== null && fits(version(t))) return t
968
+ warn(t === null
969
+ ? `[loader.cargo] ${patch.from} patches ${crate} with ${patch.dir}, which holds no Cargo.toml with a [package]`
970
+ : `[loader.cargo] ${patch.from}'s patch of ${crate} (${version(t)}) doesn't satisfy ${who}'s requirement ${req}: not used`)
971
+ } else if (patch?.spec.source.type === 'git') {
972
+ kind = 'git'
973
+ }
974
+ // A copy of where the dependency comes from (a copy without `.cargo-checksum.json` could be either).
975
+ const candidates = (vendored().byName.get(crate) ?? []).filter((c) => c.git === null || c.git === (kind === 'git'))
976
+ if (candidates.length === 0) return null
977
+ const vendoredList = candidates.map((c) => c.version).toSorted((a, b) => b.localeCompare(a, 'en', { numeric: true })).join(', ')
978
+ const lk = lockfile()
979
+ if (lk) {
980
+ const pins = lockKeysOf(lk, m)
981
+ .flatMap((k) => lk.lock.packages[k].dependencies)
982
+ .map((k) => lk.lock.packages[k])
983
+ .filter((p) => normName(p.name) === crate && sourceKind(p.source) === kind)
984
+ .map((p) => p.version)
985
+ const want = pins.find(fits) ?? null
986
+ // A lock whose pin the requirement no longer allows is out of date: cargo would resolve
987
+ // again, so the requirement decides.
988
+ 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`)
989
+ if (want !== null) {
990
+ const hit = candidates.find((c) => c.version === want)
991
+ if (hit) return readManifest(hit.dir)
992
+ warn(`[loader.cargo] ${who} is locked to ${crate} ${want}, which isn't vendored (vendored: ${vendoredList})`)
993
+ return null
994
+ }
995
+ }
996
+ const fitting = candidates.filter((c) => fits(c.version))
997
+ if (fitting.length === 1) return readManifest(fitting[0].dir)
998
+ if (fitting.length === 0) warn(`[loader.cargo] No vendored version of ${crate} satisfies ${who}'s requirement ${req} (vendored: ${vendoredList})`)
999
+ 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`)
1000
+ return null
1001
+ }
1002
+
1003
+ // --- feature resolution
1004
+
1005
+ // Cargo's feature resolver: v2 (edition 2021, or `resolver = "2"`) leaves dev-dependencies out of
1006
+ // a normal build's unification, resolves what is built for the host apart from what is built for
1007
+ // the target, and ignores the tables of platforms not being built; v1 unifies them all. v3
1008
+ // (edition 2024, or `resolver = "3"`) resolves features as v2 does, and picks versions by
1009
+ // `rust-version` too, which the lockfile and the vendored copies have decided here. It is the
1010
+ // workspace's setting: the build's workspace root's (buildWorkspaceRoot) `resolver`, else its
1011
+ // edition's, as linkCargo gives it -- never that of a vendored crate an entry is in, where cargo
1012
+ // isn't run.
1013
+ let resolverMemo = null
1014
+ const resolverVersion = () => {
1015
+ if (resolverMemo === null) {
1016
+ const root = buildWorkspaceRoot()
1017
+ const edition = Number(root?.package?.edition ?? 0)
1018
+ resolverMemo = root?.resolver ?? (edition >= 2024 ? 3 : (edition >= 2021 ? 2 : 1))
1019
+ }
1020
+ return resolverMemo
1021
+ }
1022
+ // The feature context a unit's code sees (resolver 2 keeps the host's apart; resolver 1 has one).
1023
+ const featureCtx = (c) => (resolverVersion() === 1 ? 'target' : c)
1024
+ const isProcMacroPkg = (m) => m?.lib.procMacro === true
1025
+ // The platform a context compiles for, as far as the loader knows it: the target's triple and
1026
+ // cfgs when `target` was given; the host's only when the target is the host (`host`), since the
1027
+ // machine that builds need not be the one that bundles.
1028
+ const platformInfo = (platform) => (platform === 'target' ? targetInfo : hostInfo)
1029
+ // Whether a table for platform `spec` (a `cfg(…)` or a triple; null or undefined for none) is
1030
+ // one for `platform`: `yes`, `no`, or `maybe` when its cfgs aren't known or don't decide it.
1031
+ const tableOn = (spec, platform) => {
1032
+ if (spec === null || spec === undefined) return 'yes'
1033
+ const info = platformInfo(platform)
1034
+ if (info === null) return 'maybe'
1035
+ const cfg = /^cfg\((.*)\)$/u.exec(spec)
1036
+ const holds = cfg ? evalCfg(cfg[1], { target: info.cfgs }) : spec === info.triple
1037
+ return holds === true ? 'yes' : (holds === false ? 'no' : 'maybe')
1038
+ }
1039
+ // A package in a context, as the resolutions key it: resolver 1's one context is `target`'s.
1040
+ const nodeKey = (c, dir) => `${featureCtx(c)}\0${dir}`
1041
+
1042
+ // Both feature resolutions of `graph` (linkCargo's, or the replay's) by cargo's resolver, for the
1043
+ // build of `packages`, members of it, per (context, dir) as nodeKey keys them -- `dirOf(key)` the
1044
+ // dir of a package key, undefined for none -- resolver 1's one set under `target`, the normal one
1045
+ // where there is one: `sure` with the features on for certain, `all` with those that may be, and
1046
+ // `result`, the resolver's own answer for `all`. Cargo's resolver takes the platforms it builds
1047
+ // for as known, and the loader may not know them: it runs against stand-ins (standInPlatform), a
1048
+ // target-specific table the loader can't decide on one -- a platform it doesn't know, a cfg it
1049
+ // can't decide there (`cfg(loom)`, which rustflags may set) -- off for `sure` and on for `all`,
1050
+ // the rest for cargo to decide as written. What cargo's resolver refuses -- a feature asked of a
1051
+ // package that hasn't it -- stops the build, naming `file`.
1052
+ const resolveGraph = (graph, packages, file, dirOf) => {
1053
+ const build = { packages, features, allFeatures, noDefaultFeatures, dev: entries.some((e) => linksDevDeps(e)), host: standInPlatform(MARK_HOST, hostInfo), targets: [standInPlatform(MARK_TARGET, targetInfo)] }
1054
+ let undecided = false
1055
+ const decided = (on) => retarget(graph, (spec) => {
1056
+ const at = (platform) => {
1057
+ if (tableOn(spec, platform) !== 'maybe') return /^cfg\((.*)\)$/su.exec(spec)?.[1] ?? `${MARK_NAME} = "${spec}"`
1058
+ undecided = true
1059
+ return String(on)
1060
+ }
1061
+ return `cfg(any(all(${MARK_TARGET}, ${at('target')}), all(${MARK_HOST}, ${at('host')})))`
1062
+ })
1063
+ const featuresOf = (g) => readNamed(file, () => resolveCargoFeatures(g, build))
1064
+ const sure = featuresOf(decided(false))
1065
+ const all = undecided ? featuresOf(decided(true)) : sure
1066
+ const byNode = (result) => {
1067
+ const out = new Map()
1068
+ for (const [key, { normal, host: onHost }] of Object.entries(result)) {
1069
+ const dir = dirOf(key)
1070
+ if (dir === undefined) continue
1071
+ if (onHost !== undefined) out.set(nodeKey('host', dir), new Set(onHost))
1072
+ if (normal !== undefined) out.set(nodeKey('target', dir), new Set(normal))
1073
+ }
1074
+ return out
1075
+ }
1076
+ return { sure: byNode(sure), all: byNode(all), result: all }
1077
+ }
1078
+
1079
+ // --- the replay: cargo's feature resolver, by @preventive/lockfile, over the manifests
1080
+
1081
+ // The graph cargo's feature resolver takes, as linkCargo gives one, laid out from the manifests
1082
+ // where the lockfile's can't be: each package in-tree the entries' packages (`roots`) reach, by
1083
+ // its dir, each of its declarations resolved as resolveDep resolves it -- a path, a `[patch]`, the
1084
+ // vendored copy the lockfile or the requirement picks -- but a dependency's own dev-dependencies,
1085
+ // which no build of the entries has. A declaration nothing in-tree answers resolves to a stand-in
1086
+ // of its own, which depends on nothing and has every feature its dependent may ask of it: the
1087
+ // declaration's, `default`, and what a `name/feature` or `name?/feature` of the package's
1088
+ // features, or of `--cargo-features` for a root, names. So does, unresolved (`unbuilt`), a
1089
+ // dev-dependency of a root that no entry is a test, bench or example of, under resolver 2: cargo's
1090
+ // resolver takes the dev targets of every member as built or none, and the entries build only
1091
+ // theirs. The members are the roots, and there is no root package: `--cargo-features` goes to each
1092
+ // that has it. `edges`: each declaration's resolved key, with its package and request (`m`, `d`,
1093
+ // `request`), and whether it is a stand-in.
1094
+ const replayGraph = (roots) => {
1095
+ const packages = Object.create(null)
1096
+ const edges = []
1097
+ const rootDirs = new Set(roots.map((m) => m.dir))
1098
+ const unbuiltDev = (m) => resolverVersion() !== 1 && !devRootDirs().has(m.dir)
1099
+ const queue = [...roots]
1100
+ for (const m of queue) {
1101
+ if (m.dir in packages) continue
1102
+ const pkg = m.cargo.package
1103
+ const dependencies = pkg.dependencies.map((d, index) => {
1104
+ if (d.kind === 'dev' && !rootDirs.has(m.dir)) return { ...d, resolved: undefined, active: false }
1105
+ const dep = m.deps.get(normName(d.name))
1106
+ const request = dep.kinds.get(d.target === undefined ? d.kind : `${d.kind}@${d.target}`)
1107
+ const unbuilt = d.kind === 'dev' && unbuiltDev(m)
1108
+ const t = unbuilt ? null : resolveDep(m, dep, request, { quiet: true })
1109
+ if (t !== null) queue.push(t)
1110
+ const key = t?.dir ?? `\0${m.dir}\0${index}`
1111
+ edges.push({ key, m, d, request, standIn: t === null, unbuilt })
1112
+ return { ...d, resolved: key, active: true }
1113
+ })
1114
+ packages[m.dir] = { name: pkg.name, version: pkg.version, source: undefined, checksum: undefined, manifest: pkg, dependencies }
1115
+ }
1116
+ for (const { key, m, d } of edges.filter((e) => e.standIn)) {
1117
+ const asked = new Set(['default', ...d.features])
1118
+ for (const value of [...Object.values(m.cargo.package.features).flat(), ...(rootDirs.has(m.dir) ? parseFeatureList(features) : [])]) {
1119
+ const named = DEP_FEATURE_RE.exec(value)
1120
+ if (named?.[1] === d.name) asked.add(named[3])
1121
+ }
1122
+ const manifest = { name: d.package, features: Object.fromEntries([...asked].map((f) => [f, []])), dependencies: [], procMacro: false, procMacroTarget: false }
1123
+ Object.setPrototypeOf(manifest.features, null)
1124
+ packages[key] = { name: d.package, version: '0.0.0', source: undefined, checksum: undefined, manifest, dependencies: [] }
1125
+ }
1126
+ return { graph: { resolver: resolverVersion(), root: undefined, members: [...rootDirs], packages }, edges }
1127
+ }
1128
+
1129
+ // The replay's resolution, as resolveGraph gives it, and the dependencies the build links that
1130
+ // nothing in-tree answers -- a stand-in it reaches -- as `dir key` → `{ dir, key, name, kinds }`,
1131
+ // the kinds of their tables (see lackingDependencies). What resolveDep warned of, it says now of
1132
+ // the declarations whose package the build reaches.
1133
+ const replay = () => {
1134
+ const roots = [...new Set(entries.map(packageFor).filter(Boolean))]
1135
+ if (roots.length === 0) return { resolved: { sure: new Map(), all: new Map() }, lacking: new Map() }
1136
+ const { graph, edges } = replayGraph(roots)
1137
+ const standIns = new Set(edges.filter((e) => e.standIn).map((e) => e.key))
1138
+ const resolved = resolveGraph(graph, graph.members, buildWorkspaceRoot()?.file ?? null, (key) => (standIns.has(key) ? undefined : key))
1139
+ const lacking = new Map()
1140
+ for (const { key, m, d, request, standIn, unbuilt } of edges) {
1141
+ if (unbuilt || !(key in resolved.result)) continue
1142
+ warnDep(m, request)
1143
+ if (!standIn) continue
1144
+ const id = `${m.dir}\0${normName(d.name)}`
1145
+ if (!lacking.has(id)) lacking.set(id, { dir: m.dir, key: normName(d.name), name: d.name, kinds: new Set() })
1146
+ lacking.get(id).kinds.add(d.kind)
1147
+ }
1148
+ return { resolved, lacking }
1149
+ }
1150
+
1151
+ // --- the exact resolution: cargo's resolver, by @preventive/lockfile
1152
+
1153
+ // The directories a workspace `members` pattern names, project-relative (glob's syntax, as cargo
1154
+ // takes it: `*`, `?` and `[…]` within a segment, `**` for any depth); null when it leaves the
1155
+ // bundle root.
1156
+ const subdirs = (dir) => {
1157
+ try {
1158
+ return readdirSync(join(baseDir, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (dir === '.' ? e.name : `${dir}/${e.name}`))
1159
+ } catch {
1160
+ return []
1161
+ }
1162
+ }
1163
+ const globDirs = (rootDir, pattern) => {
1164
+ let dirs = [rootDir]
1165
+ for (const seg of pattern.split('/').filter((x) => x !== '' && x !== '.')) {
1166
+ const next = []
1167
+ for (const dir of dirs) {
1168
+ if (seg === '**') {
1169
+ const all = [dir]
1170
+ for (let i = 0; i < all.length; i++) all.push(...subdirs(all[i]).filter((d) => !posix.basename(d).startsWith('.')))
1171
+ next.push(...all)
1172
+ } else if (/[*?[]/u.test(seg)) {
1173
+ const re = globSegment(seg)
1174
+ next.push(...subdirs(dir).filter((d) => re.test(posix.basename(d))))
1175
+ } else {
1176
+ const rel = normalizeRel(dir, seg)
1177
+ if (rel === null) return null
1178
+ next.push(rel)
1179
+ }
1180
+ }
1181
+ dirs = next
1182
+ }
1183
+ return dirs
1184
+ }
1185
+ // The workspace's members, project-relative, as cargo finds them: the root package, the
1186
+ // packages its `members` patterns name, and the path dependencies of members inside the root's
1187
+ // directory, less what `exclude` names; null when a pattern leaves the bundle root.
1188
+ const membersOf = (root) => {
1189
+ const ws = root.cargo.workspace
1190
+ if (ws === undefined) return [root.dir]
1191
+ const excluded = ws.exclude.map((e) => normalizeRel(root.dir, e)).filter((e) => e !== null)
1192
+ const isExcluded = (dir) => excluded.some((e) => dir === e || dir.startsWith(`${e}/`))
1193
+ const inRoot = (dir) => root.dir === '.' || dir === root.dir || dir.startsWith(`${root.dir}/`)
1194
+ const out = new Set(root.package ? [root.dir] : [])
1195
+ for (const pattern of ws.members) {
1196
+ const dirs = globDirs(root.dir, pattern)
1197
+ if (dirs === null) return null
1198
+ for (const dir of dirs) if (!isExcluded(dir) && readManifest(dir)?.package) out.add(dir)
1199
+ }
1200
+ // Set iteration is live: a path dependency added is walked in turn.
1201
+ for (const dir of out) {
1202
+ for (const d of readManifest(dir).deps.values()) {
1203
+ for (const r of d.kinds.values()) {
1204
+ const sub = r.dir
1205
+ if (sub !== null && inRoot(sub) && !isExcluded(sub) && readManifest(sub)?.package) out.add(sub)
1206
+ }
1207
+ }
1208
+ }
1209
+ return [...out]
1210
+ }
1211
+ // The build as cargo resolves it, where the loader holds all that takes -- the build's lockfile
1212
+ // and target, a manifest for every package the lockfile has (each path package inside the
1213
+ // bundle root, every other one vendored, `.cargo-checksum.json` and all) and entries whose
1214
+ // packages are workspace members: @preventive/lockfile lays the lockfile's graph over the
1215
+ // manifests (linkCargo) and turns on the features `cargo build -p <the entries' packages>` does
1216
+ // (resolveCargoFeatures), cargo's rules throughout -- each table's own source, a [patch] from the
1217
+ // root manifest or the cargo config, proc-macros and build-dependencies built for the host, the
1218
+ // command line's features handed out as cargo hands them out. What either refuses -- a lockfile
1219
+ // out of date with the manifests, a vendored copy whose checksum isn't the lockfile's, a feature
1220
+ // asked of a package that hasn't it -- stops the build. A table the loader can't decide on a
1221
+ // platform -- the host's, where the target isn't `host` -- is resolved both ways (resolveGraph).
1222
+ // `{ graph, dirOf, keyOf, resolved }` -- package key ↔ dir, and the features per (context, dir)
1223
+ // as resolveGraph gives them -- or null when something it takes is missing, for the replay to
1224
+ // decide: no lockfile, no target, a package not vendored, a path outside the bundle root.
1225
+ let exactMemo
1226
+ let exactWhy = null // why there is none, for the replay to say
1227
+ const exact = () => {
1228
+ if (exactMemo === undefined) exactMemo = exactUncached()
1229
+ return exactMemo
1230
+ }
1231
+ const notExact = (why) => {
1232
+ exactWhy = why
1233
+ return null
1234
+ }
1235
+ const exactUncached = () => {
1236
+ if (metadata) return null
1237
+ if (targetInfo === null) return notExact('no --cargo-target')
1238
+ const lockPath = buildLockPath()
1239
+ if (lockPath === null) return notExact('the workspace root lies above the bundle root')
1240
+ const lk = lockfile()
1241
+ if (lk === null) return notExact(`no ${lockPath}`)
1242
+ const root = buildWorkspaceRoot()
1243
+ if (root === null) return notExact('no package owns the entries')
1244
+ const memberDirs = membersOf(root)
1245
+ if (memberDirs === null) return notExact(`a members pattern of ${root.file} leaves the bundle root`)
1246
+ // The path packages: the members, the path dependencies of each, the path [patch]es.
1247
+ const keyOf = new Map() // dir -> package key
1248
+ const dirOf = new Map() // package key -> dir
1249
+ const queue = [...memberDirs]
1250
+ for (const { patch, at, from } of patchSources()) {
1251
+ for (const specs of Object.values(patch)) {
1252
+ for (const spec of Object.values(specs)) {
1253
+ if (spec.source.type !== 'path') continue
1254
+ const dir = at(spec.source.path)
1255
+ if (dir === null) return notExact(`${from} patches with a path outside the bundle root`)
1256
+ queue.push(dir)
1257
+ }
1258
+ }
1259
+ }
1260
+ for (const dir of queue) {
1261
+ if (keyOf.has(dir)) continue
1262
+ const m = readManifest(dir)
1263
+ if (!m?.package) return notExact(`${posix.join(dir, 'Cargo.toml')} isn't a package's`)
1264
+ const key = `${m.package.name} ${m.package.version}`
1265
+ keyOf.set(dir, key)
1266
+ dirOf.set(key, dir)
1267
+ for (const d of m.deps.values()) {
1268
+ for (const r of d.kinds.values()) {
1269
+ if (r.path === null) continue
1270
+ if (r.dir === null) return notExact(`${m.package.name}'s path dependency ${r.path} lies outside the bundle root`)
1271
+ queue.push(r.dir)
1272
+ }
1273
+ }
1274
+ }
1275
+ // The vendor directory as a directory source reads it, and every package of the lockfile there
1276
+ // (a copy the lockfile doesn't list needs no checksums: cargo never reads it).
1277
+ const vendor = Object.create(null)
1278
+ const held = new Map() // `name version` -> whether its copy has its checksums
1279
+ for (const sub of subdirs(vendorDir)) {
1280
+ const name = posix.basename(sub)
1281
+ const m = name.startsWith('.') ? null : manifestAt(sub)
1282
+ if (!m?.package) continue
1283
+ const sums = checksumOf(sub)
1284
+ const id = `${m.package.name} ${m.package.version}`
1285
+ held.set(id, held.get(id) === true || sums !== null)
1286
+ if (sums !== null) vendor[name] = { manifest: tableOf(sub).text, checksum: sums.text }
1287
+ }
1288
+ for (const [key, p] of Object.entries(lk.lock.packages)) {
1289
+ if (p.source === undefined && !dirOf.has(key)) return notExact(`${lk.file} locks the path package ${p.name} ${p.version}, which isn't in-tree`)
1290
+ if (p.source !== undefined && !held.has(`${p.name} ${p.version}`)) return notExact(`${p.name} ${p.version} is locked but not vendored`)
1291
+ 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`)
1292
+ }
1293
+ const copies = readNamed(lk.file, () => readCargoVendor(lk.lock, vendor))
1294
+ const manifestsByKey = Object.create(null)
1295
+ for (const [key, dir] of dirOf) manifestsByKey[key] = readManifest(dir).cargo
1296
+ for (const [key, { directory }] of Object.entries(copies)) {
1297
+ const dir = `${vendorDir}/${directory}`
1298
+ keyOf.set(dir, key)
1299
+ dirOf.set(key, dir)
1300
+ manifestsByKey[key] = readManifest(dir).cargo
1301
+ }
1302
+ const memberKeys = memberDirs.map((dir) => keyOf.get(dir))
1303
+ const entryKeys = [...new Set(entries.map(packageFor).filter(Boolean).map((m) => keyOf.get(m.dir)))]
1304
+ if (entryKeys.length === 0) return notExact('no package owns the entries')
1305
+ if (entryKeys.some((k) => !memberKeys.includes(k))) return notExact('an entry\'s package isn\'t a member of the workspace')
1306
+ const config = readNamed(configs[0]?.file ?? null, () => parseCargoConfig(configs.map((c) => c.text)))
1307
+ const graph = readNamed(lk.file, () => linkCargo(lk.lock, manifestsByKey, { workspace: root.cargo, members: memberKeys, config }))
1308
+ return { graph, dirOf, keyOf, resolved: resolveGraph(graph, entryKeys, root.file, (key) => dirOf.get(key)) }
1309
+ }
1310
+
1311
+ // Both resolutions, per (context, package dir): `sure` without the undecided tables, `all` with
1312
+ // them -- the exact resolution's where there is one, the replay's otherwise. `--cargo`: cargo
1313
+ // metadata gives one feature set per package, the union over everything cargo would build for
1314
+ // the workspace (dev and build dependencies, every platform), so a feature in it is on only maybe
1315
+ // in either context -- code a `cfg(not(feature = "x"))` keeps in some build is never dropped --
1316
+ // and one outside it is off. `lacking` (the replay's): the dependencies no package in-tree
1317
+ // answers, `dir key` → `{ dir, key, name, kinds }` -- the kinds of its active tables -- as
1318
+ // `cargo metadata`'s (see lackingDependencies).
1319
+ let resolved = null
1320
+ let lacking = null
1321
+ const ensureResolved = () => {
1322
+ if (resolved !== null) return resolved
1323
+ if (metadata) {
1324
+ const byNode = new Map([...metadata.enabled].flatMap(([dir, set]) => [[nodeKey('target', dir), set], [nodeKey('host', dir), set]]))
1325
+ resolved = { sure: new Map(), all: byNode }
1326
+ } else {
1327
+ resolved = exact()?.resolved ?? null
1328
+ if (resolved === null) ({ resolved, lacking } = replay())
1329
+ }
1330
+ return resolved
1331
+ }
1332
+ // Per node, the features on only maybe: in `all`, not in `sure` (one object per node).
1333
+ const maybeByNode = new Map()
1334
+ const maybeOf = (node) => cached(maybeByNode, node, () => {
1335
+ const { sure, all } = ensureResolved()
1336
+ const on = sure.get(node)
1337
+ const maybe = new Set([...(all.get(node) ?? [])].filter((f) => !on?.has(f)))
1338
+ return maybe.size === 0 ? null : maybe
1339
+ })
1340
+ // A package in the `all` resolution but not the `sure` one is built only maybe: nothing of its
1341
+ // is on for certain.
1342
+ const NO_FEATURES = new Set()
1343
+
1344
+ // --- cfgs a build may set
1345
+ let rustflagCfgs = null
1346
+ const cfgsSetMemo = new Map()
1347
+ // A crate root's text -- a build script's, a lib's -- and that of the modules it declares (`mod
1348
+ // probe;`, beside it), a few levels down: where its `cargo:rustc-cfg=` lines are.
1349
+ const crateTexts = (root) => {
1350
+ const texts = []
1351
+ const queue = [[root, 0]]
1352
+ const seen = new Set()
1353
+ for (let qi = 0; qi < queue.length; qi++) {
1354
+ const [rel, depth] = queue[qi]
1355
+ if (seen.has(rel)) continue
1356
+ seen.add(rel)
1357
+ const text = readText(diskHost, join(baseDir, rel))
1358
+ if (text === null) continue
1359
+ texts.push(text)
1360
+ if (depth >= 3) continue
1361
+ const dir = posix.dirname(rel)
1362
+ const sub = qi === 0 || posix.basename(rel) === 'mod.rs' ? dir : posix.join(dir, posix.basename(rel, '.rs'))
1363
+ for (const m of text.matchAll(/\bmod\s+([A-Za-z_]\w*)\s*;/gu)) {
1364
+ 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])
1365
+ }
1366
+ }
1367
+ return texts
1368
+ }
1369
+
1370
+ // What the build script of package `m` may print through the crates it calls (cfgsPrinted of
1371
+ // their libs: cfg_aliases' `cfg_aliases!` prints the names its input gives) -- its
1372
+ // build-dependencies for the host, and what they depend on in turn. One the bundle root lacks
1373
+ // may print any for all the loader knows, which needs no lib read. Asked once per package
1374
+ // (cfgsSetFor).
1375
+ const helpersPrinted = (m) => {
1376
+ const crates = new Map() // dir -> manifest
1377
+ if (linksLacking(m, 'build', crates)) return { names: new Set(), any: true }
1378
+ const names = new Set()
1379
+ for (const t of crates.values()) {
1380
+ const printed = libPrinted(t)
1381
+ if (printed.any) return printed
1382
+ for (const name of printed.names) names.add(name)
1383
+ }
1384
+ return { names, any: false }
1385
+ }
1386
+ // Whether package `p` links, through its `kind` dependencies for the host and what they depend
1387
+ // on in turn, a crate the bundle root lacks; those it links in-tree go into `crates` (dir →
1388
+ // manifest).
1389
+ const linksLacking = (p, kind, crates) => [...p.deps.values()].some((d) => [...d.kinds.values()].some((r) => {
1390
+ if (r.kind !== kind || tableOn(r.target, 'host') === 'no') return false
1391
+ const t = resolveDep(p, d, r)
1392
+ if (t === null) return true
1393
+ if (crates.has(t.dir)) return false
1394
+ crates.set(t.dir, t)
1395
+ return linksLacking(t, 'normal', crates)
1396
+ }))
1397
+ // What package `t`'s lib may print for a build script calling it (cfgsPrinted); memoized per
1398
+ // package.
1399
+ const libPrints = new Map()
1400
+ const libPrinted = (t) => cached(libPrints, t.dir, () => {
1401
+ const lib = libPath(t)
1402
+ return lib === null ? NO_CFGS_SET : cfgsPrinted(crateTexts(lib))
1403
+ })
1404
+
1405
+ // The in-tree crate roots a name resolves to from package `m` (null: no owning package), leaving
1406
+ // out `m`'s own lib (which depends on the asking file), as `[{ file, key }]`: the package its
1407
+ // dependency of that name resolves to (path dep, `[patch]`, or the vendored version Cargo.lock
1408
+ // says); a dependency whose lib is named that (`md-5` is used as `md5`); or -- for a file no
1409
+ // manifest claims -- the one vendored crate of that lib or package name. A package's own name for
1410
+ // a crate is its manifest's word: a declared dependency that doesn't resolve in-tree (a path
1411
+ // outside the bundle root, a version no vendored copy has) never falls back to some vendored copy
1412
+ // of that name, and a name it doesn't declare is no crate of its. Which of the dependency's
1413
+ // tables count is the asking code's (`roles`, one per crate root the file is compiled in): a build
1414
+ // script's are the `[build-dependencies]`, a test, bench or example target's the
1415
+ // `[dependencies]` and `[dev-dependencies]`, other code's the `[dependencies]` (any table when
1416
+ // none of those has it) -- each only where its platform may be one the code is compiled for
1417
+ // (`platforms`, the file's units'; a build script's the host): a table the target's cfgs rule out
1418
+ // is no dependency of a build for it, under either resolver. Tables that may each apply -- one
1419
+ // per platform when no target decides them, or one per role -- give one candidate each, `key`
1420
+ // naming which (`*` for the untargeted table, else its platform; `build-script ` before a build
1421
+ // script's beside another role's). Memoized per (package, name, roles, platforms): every file of
1422
+ // a package asks for the same few crates.
1423
+ const crateTargets = new Map()
1424
+ const depCrate = (m, norm, roles, platforms) => {
1425
+ const memo = `${m?.dir ?? ''}\0${norm}\0${roles.join()}\0${platforms.join()}`
1426
+ return cached(crateTargets, memo, () => depCrateUncached(m, norm, roles, platforms))
1427
+ }
1428
+ const ROLE_KINDS = { build: ['build'], test: ['normal', 'dev'], normal: ['normal'] }
1429
+ // The role of the code of crate root `root` in package `m` (see depCrate).
1430
+ const roleOf = (m, root) => (root === buildScript(m) ? 'build' : (linksDevDeps(root) ? 'test' : 'normal'))
1431
+ // Of a dependency's tables (`{ kind, target, … }`, the manifest's or the linked graph's, in
1432
+ // order), the packages the asking code links: for each role, the first table of each platform
1433
+ // that may apply and that `find` finds a package for, as `{ t, key }` (see depCrate). `any`: a
1434
+ // role with no table of its kinds takes any table instead (a declared name).
1435
+ const linkedTables = (all, roles, platforms, find, any) => {
1436
+ const out = []
1437
+ const seen = new Set()
1438
+ for (const role of roles) {
1439
+ const own = all.filter((r) => ROLE_KINDS[role].includes(r.kind))
1440
+ const plats = role === 'build' ? ['host'] : platforms
1441
+ for (const r of own.length === 0 && any ? all : own) {
1442
+ const on = r.target ?? null
1443
+ const key = `${role === 'build' && roles.length > 1 ? 'build-script ' : ''}${on === null ? '*' : (/^cfg\((.*)\)$/su.exec(on)?.[1] ?? on)}`
1444
+ if (seen.has(key) || !plats.some((p) => tableOn(on, r.kind === 'build' ? 'host' : p) !== 'no')) continue
1445
+ const t = find(r)
1446
+ if (t === null) continue
1447
+ seen.add(key)
1448
+ out.push({ t, key })
1449
+ }
1450
+ }
1451
+ return out
1452
+ }
1453
+ // The candidates as crate roots, one per file.
1454
+ const asRoots = (found) => {
1455
+ const out = []
1456
+ for (const { t, key } of found) {
1457
+ const file = libPath(t)
1458
+ if (file !== null && !out.some((c) => c.file === file)) out.push({ file, key })
1459
+ }
1460
+ return out
1461
+ }
1462
+ // depCrate from the exact resolution: the package's dependency of that key, else the unrenamed
1463
+ // one whose lib is named that, as the lockfile resolves it.
1464
+ const exactCrate = (x, key, norm, roles, platforms) => {
1465
+ const deps = x.graph.packages[key].dependencies
1466
+ const packageOf = (d) => (d.resolved === undefined ? null : readManifest(x.dirOf.get(d.resolved)))
1467
+ const declared = deps.filter((d) => normName(d.name) === norm)
1468
+ if (declared.length > 0) return asRoots(linkedTables(declared, roles, platforms, packageOf, true))
1469
+ return asRoots(linkedTables(deps.filter((d) => d.name === d.package && d.resolved !== undefined && libName(packageOf(d)) === norm), roles, platforms, packageOf, false))
1470
+ }
1471
+ const depCrateUncached = (m, norm, roles, platforms) => {
1472
+ const x = m ? exact() : null
1473
+ const key = x?.keyOf.get(m.dir)
1474
+ if (key !== undefined) return exactCrate(x, key, norm, roles, platforms)
1475
+ if (m) {
1476
+ const d = m.deps.get(norm)
1477
+ if (d) return asRoots(linkedTables([...d.kinds.values()], roles, platforms, (r) => resolveDep(m, d, r), true))
1478
+ // A dependency whose lib is named `norm`: only one that may be is resolved (a path
1479
+ // dependency's manifest says, a vendored crate's must have that lib name), so a name that is
1480
+ // no crate (`u8::MAX`) resolves -- and warns about -- nothing.
1481
+ const mayBe = (r) => {
1482
+ if (!r.path) return vendored().byLib.has(norm)
1483
+ const t = r.dir === null ? null : readManifest(r.dir)
1484
+ return Boolean(t?.package) && libName(t) === norm
1485
+ }
1486
+ for (const other of m.deps.values()) {
1487
+ const named = [...other.kinds.values()].filter((r) => !r.renamed && mayBe(r)) // a rename is used by its key, not its lib name
1488
+ const found = linkedTables(named, roles, platforms, (r) => {
1489
+ const t = resolveDep(m, other, r)
1490
+ return t && libName(t) === norm ? t : null
1491
+ }, false)
1492
+ if (found.length > 0) return asRoots(found)
1493
+ }
1494
+ return []
1495
+ }
1496
+ const { byLib, byName } = vendored()
1497
+ const found = new Set()
1498
+ for (const index of [byLib, byName]) for (const c of index.get(norm) ?? []) found.add(c.dir)
1499
+ if (found.size > 1) {
1500
+ console.warn(`[loader.cargo] Several vendored crates are named ${norm} (${[...found].join(', ')}) and no manifest says which`)
1501
+ return []
1502
+ }
1503
+ return asRoots([...found].map((dir) => ({ t: readManifest(dir), key: '*' })))
1504
+ }
1505
+
1506
+ const crateCandidates = (name, fromFile, { roots = null, units = null } = {}) => {
1507
+ const norm = normName(name)
1508
+ const m = packageFor(fromFile)
1509
+ if (m && libName(m) === norm) {
1510
+ const lib = libPath(m)
1511
+ if (lib && lib !== fromFile) return [{ file: lib, key: '*' }]
1512
+ }
1513
+ const roles = m === null ? ['normal'] : [...new Set((roots === null || roots.size === 0 ? [fromFile] : [...roots]).map((r) => roleOf(m, r)))]
1514
+ const platforms = units === null || units.size === 0 ? ['target'] : [...new Set([...units].map(unitPlatform))]
1515
+ return depCrate(m, norm, roles, platforms)
1516
+ }
1517
+
1518
+ // The files describing package `m`'s build, on disk (see buildFilesFor).
1519
+ const buildFiles = (m) => {
1520
+ const out = [{ path: posix.join(m.dir, 'Cargo.toml'), kind: 'manifest' }]
1521
+ if (isVendoredDir(m.dir)) {
1522
+ const checksum = posix.join(m.dir, '.cargo-checksum.json')
1523
+ if (isFile(join(baseDir, checksum))) out.push({ path: checksum, kind: 'checksum' })
1524
+ return out
1525
+ }
1526
+ const ws = workspaceFor(m.dir)
1527
+ if (ws && ws.dir !== m.dir) out.push({ path: posix.join(ws.dir, 'Cargo.toml'), kind: 'manifest' })
1528
+ // The build's lockfile and cargo configs, not the package's own: cargo reads only those.
1529
+ const lockPath = buildLockPath()
1530
+ if (lockPath !== null && isFile(join(baseDir, lockPath))) out.push({ path: lockPath, kind: 'lock' })
1531
+ for (const c of configs) if (c.dir !== null) out.push({ path: c.file, kind: 'config' })
1532
+ return out
1533
+ }
1534
+
1535
+ return {
1536
+ // Where `cargo vendor` put the registry crates (project-relative): `vendor`, or what
1537
+ // .cargo/config.toml names. A package under it is a vendored dependency.
1538
+ vendorDir,
1539
+ // Identity of the package owning `fileRel` -- `{ dir, name, version }` from the nearest
1540
+ // Cargo.toml with a [package] -- or null when no manifest claims it.
1541
+ packageInfo(fileRel) {
1542
+ const m = packageFor(fileRel)
1543
+ return m ? { dir: m.dir, name: m.package.name, version: version(m) } : null
1544
+ },
1545
+ // Whether `fileRel` is the lib target root of the package owning it (a crate root by role,
1546
+ // whatever its name).
1547
+ isLibRoot(fileRel) {
1548
+ const m = packageFor(fileRel)
1549
+ return m !== null && libPath(m) === fileRel
1550
+ },
1551
+ // The in-tree crate roots a crate name as used in source (`use name::…`, `extern crate name`)
1552
+ // names from `fromFile`, as `[{ file, key }]`: the owning package's own lib (from another of
1553
+ // its files), else what depCrate finds -- for the code of each crate root the file is compiled
1554
+ // in (`roots`; the file's own role without them), on the platforms of its compile units
1555
+ // (`units`; the target's without them). Several when tables that may each apply name different
1556
+ // packages (see depCrate); none for anything else (a registry dep that isn't vendored, std, a
1557
+ // name that isn't a crate).
1558
+ crateCandidates,
1559
+ // The first of crateCandidates, or null.
1560
+ resolveCrate: (name, fromFile, opts) => crateCandidates(name, fromFile, opts)[0]?.file ?? null,
1561
+ // Where the features come from: `{ mode, why }` -- `cargo` (cargo's resolver over the
1562
+ // lockfile), `metadata` (`cargo metadata`), or `replay` (the manifests replayed) with `why`
1563
+ // cargo's resolver couldn't run.
1564
+ resolution() {
1565
+ ensureResolved()
1566
+ return metadata ? { mode: 'metadata', why: null } : (exact() === null ? { mode: 'replay', why: exactWhy } : { mode: 'cargo', why: null })
1567
+ },
1568
+ isTestTarget,
1569
+ // The dependencies the build links that nothing in-tree answers, for the packages at `dirs` (the
1570
+ // bundled ones): declared, and active in the build as far as the resolution tells (its tables
1571
+ // for the build's platforms, an optional one turned on; maybe ones too), but a registry crate
1572
+ // not vendored, a path outside the bundle root -- as `{ key, name, from }` (the dependency's
1573
+ // key and name in its dependent's manifest, and that package as `name version`). Code may name
1574
+ // such a crate by another name than the manifest's (`md-5` is used as `md5`), or not at all, so
1575
+ // the bundle can't tell from the code that it lacks one. Dev-dependencies count only for a
1576
+ // package an entry is a test, bench or example target of; build-dependencies only for a
1577
+ // package with a build script, and with `buildScripts` (build scripts bundled). Cargo's
1578
+ // resolver needs every locked package in-tree, so it has none.
1579
+ lackingDependencies(dirs, { buildScripts = false } = {}) {
1580
+ ensureResolved()
1581
+ const counts = (kind, dir) => (kind === 'dev' ? devRootDirs().has(dir) : (kind !== 'build' || (buildScripts && buildScript(readManifest(dir)) !== null)))
1582
+ const out = []
1583
+ const seen = new Set()
1584
+ for (const { dir, key, name, kinds } of [...(lacking?.values() ?? []), ...(metadata?.lacking ?? [])]) {
1585
+ const m = dirs.has(dir) ? readManifest(dir) : null
1586
+ const from = m?.package ? `${m.package.name} ${version(m)}` : null
1587
+ if (from === null || seen.has(`${from}\0${key}`) || ![...kinds].some((k) => counts(k, dir))) continue
1588
+ seen.add(`${from}\0${key}`)
1589
+ out.push({ key, name, from })
1590
+ }
1591
+ return out
1592
+ },
1593
+ // Whether `fromFile`'s package declares a dependency of that name (in any table, by the key
1594
+ // code uses): then the name is that crate -- rustc refuses a `use` path whose lead a glob
1595
+ // import also provides, as ambiguous (E0659) -- in-tree or not.
1596
+ declaresCrate(name, fromFile) {
1597
+ return packageFor(fromFile)?.deps.has(normName(name)) === true
1598
+ },
1599
+ // The build's target: its triple and cfg set (`unix`, `target_os="linux"`, … as rustc prints
1600
+ // them), or null for both when none was given.
1601
+ targetTriple: targetInfo?.triple ?? null,
1602
+ targetCfgs: targetInfo?.cfgs ?? null,
1603
+ // The files that describe the build of `fileRel`'s package, project-relative, as `{ path,
1604
+ // kind }`, those on disk. A vendored package's: its Cargo.toml (`manifest`) and the
1605
+ // `.cargo-checksum.json` cargo checks its files against (`checksum`) -- the lock and config a
1606
+ // registry crate was published with play no part in a build that depends on it. Any other
1607
+ // package's: its own Cargo.toml and, inside the bundle root, the workspace's above it
1608
+ // (`manifest`); and the build's Cargo.lock (`lock`) and the cargo configs it reads (`config`,
1609
+ // see `runDir`), those inside the bundle root -- not a path dependency's own, which no build
1610
+ // of the entries reads. Empty for a file no manifest claims.
1611
+ buildFilesFor(fileRel) {
1612
+ const m = packageFor(fileRel)
1613
+ if (!m) return []
1614
+ m.buildFiles ??= buildFiles(m)
1615
+ return m.buildFiles
1616
+ },
1617
+ // Throws when `fileRel` is a vendored package's file whose bytes (`buf`) aren't the ones its
1618
+ // `.cargo-checksum.json` lists -- edited after `cargo vendor`: cargo refuses to build it, and
1619
+ // so the bundle does. A file the list doesn't name, or a package without one, passes; one
1620
+ // checked already -- a vendored Cargo.toml, as its package was read -- isn't hashed again.
1621
+ checkVendoredFile(fileRel, buf) {
1622
+ const m = packageFor(fileRel)
1623
+ if (m !== null && isVendoredDir(m.dir)) checkVendored(m.dir, fileRel, buf)
1624
+ },
1625
+ // Whether `fileRel` belongs to a vendored package (a crate `cargo vendor` copied in, a registry's
1626
+ // or a git checkout's: see vendoredFrom).
1627
+ isVendored(fileRel) {
1628
+ const m = packageFor(fileRel)
1629
+ return m !== null && isVendoredDir(m.dir)
1630
+ },
1631
+ // Where `cargo vendor` copied the vendored package owning `fileRel` from, as its
1632
+ // `.cargo-checksum.json` tells: `registry` (a package checksum) or `git` (a git checkout's: none);
1633
+ // null when it has no such file, and so could be either, or isn't vendored.
1634
+ vendoredFrom(fileRel) {
1635
+ const m = packageFor(fileRel)
1636
+ return m !== null && isVendoredDir(m.dir) ? copySource(m.dir) : null
1637
+ },
1638
+ // The build script of `fileRel`'s package, project-relative, when there is one on disk:
1639
+ // `[package] build = "…"`, else `build.rs` beside the manifest; `build = false` means none. A
1640
+ // vendored package's may not name a file outside the package (a published crate never does).
1641
+ buildScriptOf(fileRel) {
1642
+ const m = packageFor(fileRel)
1643
+ return m ? buildScript(m) : null
1644
+ },
1645
+ // The custom cfgs the build of `fileRel`'s package may set (a `--cfg` a default build lacks is
1646
+ // otherwise presumed off): `{ names, any }` -- the names its build script prints as
1647
+ // `cargo:rustc-cfg=…`, itself or through a crate it calls, and those the rustflags set, and
1648
+ // whether the script may set one the loader can't read. One object per package (and one for
1649
+ // every package that sets none).
1650
+ cfgsSetFor(fileRel) {
1651
+ const m = packageFor(fileRel)
1652
+ return cached(cfgsSetMemo, m?.dir ?? '\0', () => {
1653
+ rustflagCfgs ??= rustflagsCfgsOf(configs)
1654
+ const script = m ? buildScript(m) : null
1655
+ const printed = script === null ? NO_CFGS_SET : cfgsPrinted(crateTexts(script))
1656
+ const helpers = script === null || printed.any ? NO_CFGS_SET : helpersPrinted(m)
1657
+ const any = printed.any || helpers.any
1658
+ const names = new Set([...rustflagCfgs, ...printed.names, ...helpers.names])
1659
+ return names.size === 0 && !any ? NO_CFGS_SET : { names, any }
1660
+ })
1661
+ },
1662
+ // The features on for certain for the package owning `fileRel`, compiled as `unit` (see
1663
+ // unitOfCrate), in the build of the root packages; null when that is unknown: no owning
1664
+ // manifest, no root package to resolve from, or a package the resolved build doesn't pull in
1665
+ // (its gated code is then kept, not dropped).
1666
+ featuresFor(fileRel, unit = TARGET_UNIT) {
1667
+ const m = packageFor(fileRel)
1668
+ if (!m) return null
1669
+ const node = nodeKey(unitFeatureCtx(unit), m.dir)
1670
+ const { sure, all } = ensureResolved()
1671
+ return sure.get(node) ?? (all.has(node) ? NO_FEATURES : null)
1672
+ },
1673
+ // The features on only in some of the builds the loader can't tell apart (a target-specific
1674
+ // table it can't decide, or what only such a table requests), for the same package and unit;
1675
+ // null when there are none.
1676
+ maybeFeaturesFor(fileRel, unit = TARGET_UNIT) {
1677
+ const m = packageFor(fileRel)
1678
+ if (!m) return null
1679
+ return maybeOf(nodeKey(unitFeatureCtx(unit), m.dir))
1680
+ },
1681
+ // Every package of a context the build may pull in: dir -> { on, maybe }, the features on for
1682
+ // certain and those on only maybe (a package only an undecided table pulls in has every
1683
+ // feature of its maybe). For diagnostics and tests.
1684
+ featureResolution(context = 'target') {
1685
+ const out = new Map()
1686
+ const prefix = nodeKey(context, '')
1687
+ for (const node of ensureResolved().all.keys()) {
1688
+ if (node.startsWith(prefix)) out.set(node.slice(prefix.length), { on: ensureResolved().sure.get(node) ?? NO_FEATURES, maybe: maybeOf(node) ?? NO_FEATURES })
1689
+ }
1690
+ return out
1691
+ },
1692
+ // Every resolved package of a context: dir -> Set<feature on for certain>. For diagnostics and tests.
1693
+ resolvedFeatures(context = 'target') {
1694
+ const out = new Map()
1695
+ const prefix = nodeKey(context, '')
1696
+ for (const [node, set] of ensureResolved().sure) if (node.startsWith(prefix)) out.set(node.slice(prefix.length), set)
1697
+ return out
1698
+ },
1699
+ // The compile unit of a crate root `libFile` named from code compiled as `fromUnit`: a proc-macro
1700
+ // crate, and anything code compiled for the host names (a build script's build-dependencies,
1701
+ // a proc-macro's dependencies), is built for the host in the host's feature context; else the
1702
+ // namer's unit.
1703
+ unitOfCrate(libFile, fromUnit = TARGET_UNIT) {
1704
+ return isProcMacroPkg(packageFor(libFile)) || unitPlatform(fromUnit) === 'host' ? HOST_UNIT : fromUnit
1705
+ },
1706
+ // The unit of a build script of a package compiled as `pkgUnit`: for the host, with the
1707
+ // package's features.
1708
+ buildScriptUnit(pkgUnit = TARGET_UNIT) {
1709
+ return unitKey(unitFeatureCtx(pkgUnit), 'host')
1710
+ },
1711
+ // The cfgs and triple of the platform `unit` compiles for, as far as known (see platformInfo).
1712
+ platformOf(unit = TARGET_UNIT) {
1713
+ return platformInfo(unitPlatform(unit))
1714
+ },
1715
+ }
1716
+ }