@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.
- package/README.md +9 -2
- package/bin/stasis.js +253 -22
- package/package.json +42 -10
- package/src/audit-corrections.js +72 -0
- package/src/audit.js +181 -112
- package/src/bundle-from-lockfile.js +3 -5
- package/src/bundle-reader.js +77 -0
- package/src/cmd/build.js +3 -5
- package/src/cmd/bundle.js +677 -402
- package/src/cmd/github-bundle.js +39 -0
- package/src/cmd/output.js +36 -0
- package/src/cmd/sbom.js +3 -11
- package/src/diff.js +22 -14
- package/src/json-stream.js +283 -0
- package/src/loaders/bash.js +17 -36
- package/src/loaders/cargo.js +1716 -0
- package/src/loaders/foundry.js +923 -0
- package/src/loaders/paths.js +33 -0
- package/src/loaders/php.js +212 -127
- package/src/loaders/rust.js +3234 -166
- package/src/loaders/solidity-ownership.js +531 -0
- package/src/loaders/solidity.js +412 -137
- package/src/loaders/toml.js +68 -0
- package/src/metro-resolver.js +9 -24
- package/src/mock.js +3 -5
- package/src/resolve-fields.js +338 -0
- package/src/resolve-node.js +416 -0
- package/src/resolve-typescript.js +541 -0
- package/src/rollup.js +5 -0
- package/src/sbom.js +35 -19
- package/src/scan.js +211 -90
- package/src/vfs-bundle/entries.js +140 -0
- package/src/vfs-bundle/github.js +199 -0
- package/src/vfs-bundle/prisma.js +439 -0
- package/src/vfs-bundle/soldeer.js +125 -0
- package/src/vfs-bundle/tree.js +613 -0
- package/src/vfs-bundle.js +75 -0
- package/src/why.js +434 -0
- package/src/apis/npm/index.js +0 -36
- package/src/apis/npm/semver.cjs +0 -42
|
@@ -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
|
+
}
|