fix(bundle): Rust loader follow-ups: lexer offsets, dead items, paths through re-exports and macros, include!, review fixes, --cargo-target, --cargo-manifests - #175
Conversation
5cd70fc to
0accd58
Compare
876f2b6 to
494e9f0
Compare
e95e7af to
79243b2
Compare
d26953f to
abd3f50
Compare
abd3f50 to
589afce
Compare
f973192 to
07c7b18
Compare
|
The The cause is two of main's PRs meeting:
So, over that empty tree, the test's No fix exists yet (#200 doesn't touch this), and the fix is outside this PR's scope, so it isn't ported here. A proposed patch for main: the early check skips the path check and leaves it to the check over the real tree. With it, all 64 vfs-bundle tests pass on main. --- a/stasis/src/cmd/bundle.js
+++ b/stasis/src/cmd/bundle.js
@@ function classifyEntries
-function classifyEntries(name, { cwd = process.cwd(), entries, …, cargoAllFeatures, host = diskHost }) {
+function classifyEntries(name, { cwd = process.cwd(), entries, …, cargoAllFeatures, host = diskHost, checkPaths = true }) {
if (!Array.isArray(entries) || entries.length === 0) {
throw new Error(`${name}: at least one entry file is required`)
}
- const dirError = directoryEntryError(entries, cwd, host)
+ // An early check (over an empty tree, before anything is fetched) leaves the paths to the real one.
+ const dirError = checkPaths ? directoryEntryError(entries, cwd, host) : null
if (dirError !== null) throw new Error(`${name}: ${dirError}`)
--- a/stasis/src/vfs-bundle/github.js
+++ b/stasis/src/vfs-bundle/github.js
- checkVfsOptions('buildGitHubBundle', pm, packageManager, { ...options, entries: options.entries ?? ['index.js'], cwd: '/', host: vfsHost(new Vfs()) })
+ checkVfsOptions('buildGitHubBundle', pm, packageManager, { ...options, entries: options.entries ?? ['index.js'], cwd: '/', host: vfsHost(new Vfs()), checkPaths: false })
--- a/stasis/src/vfs-bundle.js
+++ b/stasis/src/vfs-bundle.js
- checkVfsOptions('suggestedEntries', { kind: 'js' }, undefined, { ...resolution, entries: ['index.js'], cwd: '/', host: vfsHost(new Vfs()) })
+ checkVfsOptions('suggestedEntries', { kind: 'js' }, undefined, { ...resolution, entries: ['index.js'], cwd: '/', host: vfsHost(new Vfs()), checkPaths: false })Generated by Claude Code |
07c7b18 to
60ea71e
Compare
… through re-exports and macros, include!, review fixes, --cargo-target, --cargo-manifests Follow-ups to the Rust loader from #173, squashed from the review rounds of this PR: - the lexer's masked view stays aligned after astral characters; - a dead item is skipped whole whatever commas its generics hold; - paths resolve through imports, re-exports, globs and exported macros, with a memoized per-crate import table, textual macro scope and the extern prelude, cfg-aware (`any(…)` leaves, gate macros, custom cfgs presumed off); - include!, include_str!/include_bytes! and #[doc = include_str!] are carried; #[path] on an inline module is honoured; - --cargo-target decides target cfgs from rustc (run from the home dir, $RUSTC honoured, no auto-install); - --cargo-manifests carries manifests, the lockfile, the cargo config and build scripts, each as written (as #182 carries --manifests files): a file that isn't UTF-8 text is refused, not altered; - vendored crates are held to their package through symlinks and #[path]. On the strict TOML reader of #179: Cargo.toml, Cargo.lock and the cargo config are read by table path (`[package] build`, `edition.workspace`, target-specific dependency tables, every `[patch]` spelling). A Cargo or Foundry file that exists but isn't TOML stops the build, naming the file and line; a dependency's foundry.toml the loader can't read is no longer skipped with a warning but stops the build too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nbp2vC4jLPbGN8cmorxAL
…plate mods - Refuse a Rust source or include_str! file that isn't UTF-8 (and a non-UTF-8 manifest); carry an include_bytes! file as base64. - Compile units: code built for the host (build scripts, build-dependencies, proc-macro crates) is scanned against the host's cfgs and, under resolver 2, the host's own feature context; a crate built both ways is followed as both. Features are on, maybe (only an undecided target table enables them) or off. - --cargo-manifests carries a vendored crate's Cargo.toml and .cargo-checksum.json, and a project package's configs up to the bundle root; the vendored directory follows the replace-with chain whatever its name. - Custom cfgs a build script prints or rustflags set are not presumed off; cfg_x! gates take the cfg their macro_rules! definition writes. - Ambiguous path edges record a cfg-keyed map of their candidates instead of the first written; candidates the build rules out come last; a child module under a custom or dead cfg gives way to an import of its name. - Macro scope: a template's bare calls are made where it is invoked; a mod a template declares belongs to each in-package invoking module and is found beside it (libc's prelude!() -> src/types.rs), else beside the definition. - `extern crate self as x;` is in the extern prelude; a declared dependency is reported missing whatever a glob into a missing crate may provide. - Vendored versions come from the lock, else the one that fits, else a warning and no guess; out-of-root path/[patch] deps never bind to vendored copies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nbp2vC4jLPbGN8cmorxAL
evalCfg is three-valued per leaf, so an item's cfg joined with its enclosing
blocks' -- zerocopy's `#[cfg(any(test, kani))] mod tests { #[cfg(not(kani))]
mod compatibility { use rand::…; } }`, i.e. all(any(test, kani), not(kani)) --
stayed undecided and its `use rand` was reported as a missing crate. When an
unknown leaf occurs more than once (up to six of them), the predicate is
evaluated under each assignment and decided when every one agrees.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nbp2vC4jLPbGN8cmorxAL
…that lead out A path's segment was looked up in the module's imports first and its own definitions only when no import answered, so an import leading out of the bundle (tokio's `pub(crate) use std::sync::atomic::AtomicU64;` in one `imp` variant) hid the mutex-based `AtomicU64` the other variant defines, and a dead import (`cfg_taskdump!`'s) beat the live fn under `cfg_not_taskdump!`. The module's items are now candidates right after its named imports, ahead of globs, and an import leading out of the bundle is an answer under its cfgs rather than a last resort: `imp::AtomicU64` maps both variant files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nbp2vC4jLPbGN8cmorxAL
…ps in the tree pass The cfg-keyed alternatives and the build-deadness check added in review 7 rebuilt the same cfg texts and re-parsed the same leaf predicates for every candidate: libc's tree pass went from 227ms to 351ms. A leaf's verdict is kept per interned build, an alternative's cfg text per asker set, and a file's package per tree pass (asked for every import and item). libc 282ms, tokio 492ms -> 464ms (589afce: 444ms); output unchanged on the corpus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nbp2vC4jLPbGN8cmorxAL
…-table deps, resolver regressions - buildRustBundle: a crate root the tree pass wants but the walk refused is asked for once, so the walk/tree loop ends; every crate the bundle lacks is reported (refused roots included) whether or not a vendor dir exists, the `cargo vendor` hint only without one. - Cargo: each dependency table is its own dependency (version, path, package, workspace per table), and a file follows the one its target uses (build script, test target, normal); a Cargo.lock pin is taken only when the requirement allows it, else the lock is reported out of date; a [patch] is used only when its version fits, and [patch] in .cargo/config.toml (nearest first, ahead of the manifest's) is read; a path dependency or patch outside the bundle root, or naming no package, is reported. - Resolver: a local module under a custom cfg gives way only to the module's own `use` or item, never to a glob; a macro call is looked up among macros (a `fn m`, `mod m` or an import of a module/crate is no `m!`); a template's calls and includes stay with it past a nested `macro_rules!`; cfg values keep their spacing inside quotes, so `"a b"` and `"ab"` are not merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nbp2vC4jLPbGN8cmorxAL
…th cargo's resolver where it can - Cargo.toml, Cargo.lock and the cargo config's [patch] are read by @preventive/lockfile 1.0.0-alpha.3 (parseCargoManifest, parseCargoLock, parseCargoConfig): what cargo refuses, or what the reader can't tell how cargo reads, stops the build naming the file; a Cargo.lock older than version 3 is refused. Version requirements are matched by its rust-semver.js. The loader's own manifest and lock readers and semver matcher are gone. - With a lockfile, --cargo-target and every locked package in-tree (path packages inside the bundle root, the rest vendored with checksums), features come from cargo's resolver: linkCargo over the manifests, then resolveCargoFeatures for `cargo build -p <the entries' packages>`; a lockfile out of date or a vendored copy whose checksum isn't the lockfile's stops the build. An unknown host is resolved both ways (its target tables maybe). - Otherwise the manifests are replayed as before, with fixes: a git dependency takes a git checkout's vendored copy and a registry one a registry's (by .cargo-checksum.json and the lockfile's sources); `dep/feat` turns on the package's feature of that name, written or implicit; a proc-macro entry is resolved and scanned for the host; prereleases follow the semver crate; the lockfile is the entries' workspace's, below the bundle root too. - cfg(true), cfg(false) and raw identifiers in cfgs. - Tests: cargo's recorded builds of @preventive/lockfile's workspace fixture (PreventiveMeasures/libraries#56, MIT) through createCargoContext, the refusals, the fallback, and each fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nbp2vC4jLPbGN8cmorxAL
- scanRustItems: one cfg verdict per predicate per scan, and the open scopes' conjunction kept as they open and close, rather than joined and evaluated again at every token (a 220 KB file inside one #[cfg] block: 588 ms -> 36 ms; with two compile units, 1.3 s -> 28 ms). - cfg leaves: leafFalse evaluates a leaf by key and value (evalCfgKey) instead of rebuilding its text for evalCfg; leavesOfCfgs uses cfgLeaves; joinCfgs where it was inlined; one cache helper for the per-asker verdicts. - One walk driver (collectRustBundle) for buildRustBundle and loadRust: build scripts, compile units and the wantedRoots loop live in rust.js; the link-out-of-package check (withinRealDir), boundaryOf and addUnits are shared rather than copied into bundle.js. - cargo.js: each path dependency's directory resolved once, against the workspace root its inheritance was read from (depSpec gone); requests carry kind and target (no `kind@target` parsing); resolver 1's single context folded into nodeKey; one [patch] source list; the git/registry flag stored with the checksum; tableOf's TOML reused for the manifest; build script and build files memoized per package; one error-naming helper shared with toml.js; internals no longer exported. - rust.js: the macro-template helpers shared by the walk and the tree pass (keyed by the package's own template set, not its identity); providedFrom's per-module file set computed once; resolveModDecl's unused cfg branch removed. - The Rust-only flag checks in stasis.js and bundle.js table-driven. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nbp2vC4jLPbGN8cmorxAL
…tes, proc-macro roots, out-of-root workspaces Crate resolution: - A dependency name is resolved from the tables the asking code links: per crate root the file is compiled in (a build script's modules and a file it shares with the lib through #[path] take the [build-dependencies]; tests, benches and examples the dev ones too), and only tables for the platforms the code is compiled for. Tables that may each apply (no target to decide them, or two roles) give cfg-keyed alternatives, each carried. Both on the replay and on cargo's resolver. - A crate's lib-name fallback resolves only dependencies that may have that lib name: `u8::MAX` no longer resolves every declared dependency and warns about versions. - A declared dependency the bundle lacks is reported when only an expression names it (`serde_json::to_string(…)`). - Macros of other crates get edges: `use dep::mac;` (an alias, a prelude glob too), `dep::mac!(…)` and `#[macro_use] extern crate`. Cargo: - The replay seeds a proc-macro root in the target context too, as cargo activates a member it is asked to build (fixes a regression from f0287e0). - A member bundled from its own directory reads its workspace root above the bundle root (never bundled) for what it inherits and the resolver, when that root's members take it. - [patch] tables apply to their own source only: a git URL's not to the crates.io dependency. - The vendor directory comes from the cargo config nearest the entries (a nested workspace's own). - `stasis bundle` says when the features come from the replay and why; a vendored copy the lockfile doesn't list no longer forces the replay. EXODUS_STASIS_DEBUG labels the mode and prints both contexts. - A vendored file whose bytes aren't those .cargo-checksum.json lists stops the bundle, as cargo refuses to build it. cfgs and resolution: - `r#true` / `r#false` are custom cfgs, not the literals, in every reader. - A child module under a custom cfg gives way to a glob of its name, as any doubtful candidate does. - An include in a nested macro_rules! is that macro's, resolved where it is invoked. - A refused or missing crate root is warned about once. - Dense cycles of cfg-gated globs no longer blow up: leaf keys are cached, absorbed alternatives dropped, and alternatives capped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nbp2vC4jLPbGN8cmorxAL
…s/fixtures/rust-bundle.json.br
`includes` and `cargo-recorded` (30 files, 44 KB) become one brotli-compressed
JSON file (10 KB): project name -> project-relative path -> the file's text, or
`{ base64 }` for one that isn't UTF-8. tests/rust-fixtures.helper.js writes a
project out once per test process, to a temporary directory removed when the
process exits, and `rustFixture(name)` gives its path; `node
tests/rust-fixtures.helper.js unpack|pack <dir>` edits them. The fixtures
main already holds stay as they are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nbp2vC4jLPbGN8cmorxAL
60ea71e to
8e6e4e0
Compare
…eft after #175 and #178 (#203) * fix(bundle): every path a .gitmodules gives a submodule is a dependency A .gitmodules that gives a submodule's `path` twice (the key repeated, or a second `[submodule "x"]` section) is one the library refuses, so it is read submodule by submodule. That reader kept the first value. git reads the last when it reads the checkout's .gitmodules, so the submodule it checks out sits at the second path. stasis took that directory for the project's own, trusted a link planted in it to .env, and bundled the file. The lenient reader now keeps the last `path`, `url` and `branch`, as git does for the checkout. A path or url starting with `-` is skipped, since git ignores those. Every other path given to the submodule is also taken as a dependency, unnamed, with a warning. git reads the first of two values when it reads .gitmodules from a commit, so neither directory may be treated as the project's own code. The library refuses every duplicate, so its strict path never reads one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * fix(vfs-bundle): a Vfs host reads a path as the OS does The Vfs host normalized every path with path.resolve before it resolved links, so a `..` after a symlink was taken textually. The OS, diskHost and forge take it from where the link leads. A Solidity bundle built from a Vfs (`stasis github-bundle`, `buildVfsBundle` with soldeer) read a root `extends = "cfg/link/../base.toml"`, or a `libs` entry with `..` after a link, from a different file than forge does, and carried that file. A dependency's extends through its own link was refused, because the ownership walk and the host's realpath disagreed. stat, readFile, readdir, readlink and realpath now take the path as spelled. realpath walks it one component at a time: a `.` or `..` is taken in the real directory before it, which must be a directory (ENOTDIR otherwise, as the OS gives), and a link target is resolved the same way. realpath also honors a trailing `/`, as realpath(3) does. Node's own lookups, findPackageJSON and resolve, still normalize their paths first, as Node does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * fix(bundle): read cargo's configs, workspace root and lockfile from the one directory cargo runs in The Cargo context assumed several working directories at once: the vendor directory came from the configs above the first entry's file, the [patch] tables from the configs above the workspace root, the rustflags from the bundle root's config alone. A member's .cargo/config.toml could so be taken for its [source] and ignored for its [patch] (cargo run in the member builds the patch's fork; the bundle bound the vendored copy), and a member's rustflags `--cfg` was presumed off. - One directory is taken as where cargo runs: the package directory of the first entry that isn't a vendored crate's (`cargo build` there, which finds its workspace root above), else the bundle root. The configs cargo reads from there -- that directory's and each one above it, above the bundle root too (read, never bundled; not $CARGO_HOME's) -- give the vendor directory, the [patch] tables, the rustflags, `cargo metadata`'s cwd and the configs --cargo-manifests carries. - [source] tables are merged key by key, the nearest config first, as cargo merges them: a nested workspace's `[source.vendored-sources] directory` beside the bundle root's `replace-with` is the nested one's directory, each path relative to the directory holding the `.cargo` of the config writing it. - The workspace root of a package is the nearest [workspace] above it that doesn't `exclude` it (cargo passes an excluding one over), whatever its `members` say: cargo either takes the package as a member there (a member's path dependency is one) or refuses to build it. A member bundled from its own directory below such a root above the bundle root now gets that root's resolver and inheritance when it is a member only as a path dependency. - The [patch] of the build's workspace root applies to every package of the build, an outer root's above the bundle root too (its paths relative to it). A lockfile beside a member whose workspace root lies above the bundle root isn't the build's: none is read, and cargo's resolver doesn't run. - --cargo-manifests carries the build's lockfile and configs only, not a path dependency's own Cargo.lock or .cargo/config.toml, which cargo never reads building the entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * fix(bundle): check a vendored Cargo.toml against .cargo-checksum.json before resolving with it A vendored file the bundle carries was checked against the checksums `cargo vendor` listed, but a vendored crate's Cargo.toml was only read: without --cargo-manifests it isn't carried, and an edited one (a feature turned on by default, a dependency added) drove the feature resolution and so the bundled files, where cargo refuses to build the crate ("the listed checksum of … Cargo.toml has changed"). Every vendored package the build takes in -- the copy a dependency resolves to on the replay, the lockfile's copies on cargo's resolver, the one vendored crate of a name for a file no manifest claims -- has its Cargo.toml checked first, once per package; one that isn't the listed file stops the bundle. Copies nothing resolves to stay unchecked, as cargo never reads them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * fix(bundle): report every dependency the build links that the bundle lacks, however the code names it The missing-crate report listed the crates the code names that don't resolve in-tree. A dependency whose lib name differs from its manifest key (`md-5`, used as `md5::compute`) and isn't vendored was never named as the package's, since its lib name lives in the manifest the bundle lacks, and so went unreported; so did a linked dependency no code names. The replay records each active dependency table -- the build's platforms (a maybe one too), an optional one once turned on -- that no package in-tree answers, and `cargo metadata` the dependencies it couldn't place in-tree. `stasis bundle` adds those of the bundled packages to the report, as `md-5 (a dependency of app 0.1.0)`, unless the code's name is already listed: dev-dependencies only for a test, bench or example entry, build-dependencies only with --cargo-manifests and a build script. Cargo's resolver needs every locked package in-tree, so there is nothing to add there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * fix(bundle): keep code a build compiles where cargo metadata's features or a build's cfgs can't be told apart Two presumptions dropped or ranked away code a build compiles: - --cargo took cargo metadata's one feature set per package -- the union over every build of the workspace, normal, dev and build dependencies on every platform -- as on for certain in the target's build and the host's alike, so `#[cfg(not(feature = "h"))]` code dropped out of the target build of a crate that only its build-dependency use turns `h` on for (cargo compiles it there). Each of those features is now on only maybe; one outside the union stays off. - A custom cfg is presumed off unless the package's build may set it. A build script printing a name it formats in part (`cargo::rustc-cfg=os_{}`) was read as setting `os_` alone, and one calling a build-dependency that prints the cfgs (cfg_aliases' `cfg_aliases!`, as nix and wgpu use it) as setting none -- so `#[cfg(os_linux)]` / `#[cfg(linux_like)]` candidates lost to their negation as certain. A name formatted in part now makes every custom cfg of the package undecided, and so does a build-dependency, or what it depends on in turn for the host, whose lib writes `rustc-cfg` outside full-line comments -- or that the bundle root lacks, which may for all the loader knows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * fix(bundle): keep an any(…) past 16 alternatives undecided, never certain anyOf returned no leaves for an `any(…)` of more than MAX_ALTERNATIVES alternatives -- the cap that keeps a dense cycle of cfg-gated globs from blowing up -- and no leaves means "holds always": the candidate under it became certain and every other one was dropped, without a warning. A cfg listing 17 platforms one way and the rest the other (cap17) mapped `crate::T` to the 17-platform file only, and a module 17 cfg-gated glob paths reach (cap2) hid the `#[cfg(target_os = "linux")] pub use lin::T;` that a linux build compiles. Past the cap, and for an `any` with an undecided alternative, the result is now one undecided leaf: no build decides it, no asker holds it (one under it included), it is no custom cfg and exclusive with nothing, its negation too. A candidate under it is only maybe the answer, so the edge is the cfg-keyed map of every candidate. Folding an `any` with such an alternative into one keeps the sets along a dense cycle few: cygen N=16 and the 14-module dense cycle resolve in tens of ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * fix(bundle): a local module gives way to a glob only when none of its files may be there walkPath let a child module give way to what else its module has of the name -- a glob's `imp` included -- when the module's first file was under a cfg the build rules out or a custom cfg it presumably lacks. A module with a fallback is there in every build all the same: `#[cfg_attr(loom, path = "loom_imp.rs")] mod imp;` is imp.rs without loom, and shadows the `imp` a `use other::*` brings in (cm1, a regression of review 10, which rustc confirms: the glob's `imp::X` lacks the method called). The same with a target: tokio's `cfg_has_atomic_u64! { mod imp; }` first, ruled out, and `cfg_not_has_atomic_u64! { mod imp; }` live (cm3). The module's files are now gathered per crate root, and it gives way only when every one of them is ruled out or doubtful: a `#[cfg(loom)] mod imp` alone still yields to the glob (cm2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * fix(bundle): resolve another crate's macro as its root does, and a path's lead to the crate Review 10 took another crate's macro from the first `#[macro_export]` definition of the name in that crate, whatever cfg its file was mounted under, and never followed a re-export: serde's docsrs-only copy of serde_core's macros (src/core/macros.rs, `#[cfg(docsrs)]`) answered serde_json's `forward_to_deserialize_any!`, which every other build gets through `pub use serde_core::forward_to_deserialize_any` (mx1 is the same in small; cargo builds it from inner). And `use helper::{helper, mk}` lost `mk!`: the path's lead resolved to the import of the macro `helper`, and the walk ended at the crate without looking in it. A macro of another crate (`use dep::mac;`, `dep::mac!(…)`, a `#[macro_use] extern crate`'s, a lead the module binds to the crate) is now looked up from that crate's root as a path there is, in the macro namespace, the cfgs judged by its own build: the crate root's `#[macro_export]` definitions are bindings of the name under their files' cfgs, ranked with its imports (a `pub use inner::m;` followed on into `inner`), and several that may each apply give the cfg-keyed map, for a bare call too. An import followed as a macro to a file that defines no macro of the name (`pub use util::helper;` of a fn) binds none. In the corpus, serde_json's three `forward_to_deserialize_any!` edges move to serde_core/src/macros.rs, and serde_derive gains `parse_macro_input!` and `format_ident!` (`use quote::{format_ident, quote}`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * fix(bundle): declare a template's mod and includes where rustc expands them A `mod` or include a `macro_rules!` body holds is made where the macro is invoked, and the loader placed it right only for a bare call from a file of the defining package, outside inline modules. Otherwise it stayed beside the definition -- carrying a file the build never reads and missing the one it does -- or was dropped silently: - invoked by path: `crate::decl!()` from src/sub.rs, or `$crate::decl!()` in another macro's body, declares src/sub/inner.rs (tm1, tm6), and `crate::embed!()`'s `include_str!("data.txt")` reads src/sub/data.txt (ti1, ti2). A path invocation now counts as one of the macro of its last segment, as a bare one does. - from another crate: `#[macro_use] extern crate defs; decl!();` declares the app's `inner` (tm3). A `#[macro_export]`ed macro's template `mod` is hosted in every crate invoking it; the test that kept it beside the definition asserted what rustc doesn't do, and now asserts what it does. - inline modules: invoked inside `pub mod outer { decl!(); }` the module is outer's (src/outer/inner.rs, tm9); defined inside `mod defs { … }` the definition's inline modules are no part of it (src/inner.rs, tm10). Each invocation keeps the inline modules around it, each template `mod` the depth of its definition. - nested templates: a `mod` (nt2) and the calls (nt3) in a `macro_rules!` that another body defines are that inner macro's, made where it is invoked, as its includes already were. Every case is checked against cargo: each fixture builds only from the file the loader now carries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * fix(bundle): follow a path through every module a segment may name The cfg-keyed map of candidates was recorded only for a path's last segment: a module the path went on through took the first candidate, so without a target `crate::imp::X` beside `#[cfg(windows)] use windows as imp;` / `#[cfg(unix)] use unix as imp;` was windows.rs alone, while a linux build compiles unix.rs (al2); likewise a module two cfg-gated globs bring in (al3), and `#[cfg(not(unix))] mod sys;` beside `#[cfg(unix)] use fallback as sys;`, where the module always won (al6). Several candidates of which a module is first now carry their branches, and walkPath walks the rest of the path through each, the files they lead to the edge's alternatives under the key of the module they go through; a single-file module under cfgs the asking file doesn't hold is one branch beside what else its module binds the name to (a `mod` with cfg variants, or under a custom cfg, stays the module). In the corpus, mio's `sys::udp::bind`, tokio's `os_impl::ctrl_c` and hashbrown's `self::imp::Group` now map each platform's file; under the linux target hashbrown keeps sse2.rs and the generic fallback a build without sse2 compiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * fix(bundle): resolve a dead file as it would compile, a contradictory one to every candidate An asking file's cfg set is its own leaves plus the target's cfgs. For a file the target rules out -- tokio's atomic_u64_as_mutex.rs and its static_*.rs submodules under a 64-bit target -- the two contradict, every candidate looked compatible, and the first written won: `super::AtomicU64` resolved to atomic_u64_native.rs, a sibling variant, not as_mutex, the module `super` names there (au2). A file whose own cfgs contradict each other (static_once_cell.rs, mounted under `not(all(test, loom))` and `all(loom, test)`) had every candidate ruled out, and fell back to the module's first file (au3). A file the target rules out now asks under its own cfgs, as where it is compiled; one whose own cfgs contradict each other is compiled nowhere, so every candidate is compatible with it and none certain, and its paths map them all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * refactor(bundle): one helper for a path a cargo config or manifest writes projectRel takes a path relative to an absolute directory, as cargo does, for the configs' directories, their [source] and [patch] paths and the outer workspace's inherited paths, which joined an absolute path instead of resolving it. The outer workspace keeps its label as `file`, and the root's [patch] reads its paths and file off the manifest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * refactor(bundle): one workspace root for the lockfile, cargo's resolver, the feature resolver and [patch] buildPackage, memoized, is the run directory's package taken once the vendor directory is known; buildWorkspaceRoot is its root, and whether that lies above the bundle root is read in one place, buildLockPath. The feature resolver comes from that root too: a vendored crate an entry is in no longer sets it (cargo is never run there). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * refactor(bundle): one record of a dependency the build lacks, from the replay and from cargo metadata Both producers keep `{ dir, key, name, kinds }`, so lackingDependencies reads them in one loop; the packages whose dev-dependencies are linked are worked out once, for the replay and the report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * refactor(bundle): one scanner for the cfgs a build script prints, itself or through a crate it calls cfgsPrinted reads build scripts and the libs they call alike: full-line comments stripped (only once the raw text mentions a cfg print), a fixed `rustc-cfg=<name>` that name, a formatted one, the directive written apart from its name or autocfg any. So a helper printing one fixed name makes only that name maybe. The crates a build script links for the host are resolved before any lib is read: one the bundle lacks is "any" without I/O. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm * perf(bundle): check a vendored Cargo.toml once, where its manifest is read readManifest checks a vendored copy's Cargo.toml against its .cargo-checksum.json from the bytes tableOf read, before anything uses it; only the vendor directory's listings read copies unchecked, as cargo checks only the copies it builds. A checked file isn't hashed again, so --cargo-manifests no longer re-reads and re-hashes it, and a vendored entry's manifest is checked like a resolved one's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XK2nLruNc3U23JzQ3fxNm --------- Co-authored-by: Claude <noreply@anthropic.com>
Follow-ups to the Rust loader from #173, all found by diffing bundles across that PR's commits, by reviews of the result, and by surveying 44 crates from crates.io (45 with mio, added for section 15) for constructs the loader did not model. The review rounds through section 17 are squashed into one commit on top of main, rebased onto #179 (the strict TOML reader, section 16), #180, #181, #182 (manifests carried as written, section 17), #183 (
@preventive/lockfile's TOML parser, section 19) and main at #193 (section 21) and #201 (section 25). Section 18's round follows as four commits of its own; sections 20, 22, 23, 24 and 26 follow as one commit each.1. Lexer offsets drift after an emoji in a string
The chunked lexer that landed with the simplification pass blanks a string or comment with a
u-flag regex. That regex sees a two-unit surrogate pair (an emoji) as one character and emits one space for it, so the masked view comes out shorter than the source from that point on. The scanner finds items on the masked view and slices their text out of the code view at the same offsets, which only agree while the two views have equal length.Real case, zerocopy 0.7.35
src/lib.rs: test data on line 7790 holds"❤️🧡💛💚💙💜"(five astral chars), and line 8110, inside#[cfg(kani)] mod proofs { … }, hasWith the views out of step by five units, the
usebody was sliced five units early, parsed as the single garbage pathuse, and the bundle lost these three edges:Fix:
lexRustblanks a range line by line with as many spaces as the line has UTF-16 units, so both views keep the source's length whatever characters a string or comment holds.2. Dead items leaking through commas in their generics
The bounded dead-item skip from 2770be4 ended at the first top-level comma, meant for a field, variant or match arm. A comma inside a generic list or a rustfmt-style
whereclause is at top level too, so of a feature-gatedonly
fn deser_sampler<'dewas skipped and the body was scanned as live code (rand); same forimpl<'de, A: Array> Deserialize<'de> for SmallVec<A>(smallvec) andpub struct Builder<'a, T>(tokio). That recordeduse serdeedges nothing live justifies and, because the walk follows crate references, pulled whole dependency trees into the bundle: smallvec's dead serde impl dragged in serde, serde_derive, syn, quote and proc-macro2. Two smaller leaks sat beside it: the attributes of a dead item (#[derive(serde::Serialize)]) and a#[cfg_attr(<pred>, …)]whose predicate can't hold were never blanked from the path scan.Fix:
fn,impl,struct,enum,trait,type,const,static,mod,use,extern,macro_rules,unsafe,async,let) runs to its;or{ … }body and never ends at a comma. Weak keywords a field may be named after (default,union) are left out.<right after a name or::up to its>; the>of->/=>excepted), somap: HashMap<K, V>,is one field.cfg_attrwhose predicate can't hold applies nothing at all (no path, nomacro_export, noinclude_str!), so the paths in either's text are dead too.3. Paths resolved through imports, re-exports and exported macros
A path edge was resolved to the deepest module prefix that names a file, with no notion of items,
pub useor macros. Through a crate's__privatere-export module or its#[macro_export]macros that prefix is justcrate, so every$crate::…path in bitflags'macro_rules!bodies pointed at lib.rs:Change: the scanner also records each
useimport with its visibility, eachmacro_rules!definition with#[macro_export], and the inlinemod x { … }blocks; the tree registers inline modules; a path resolves module by module, and where a segment names no module it may be an import of the module reached, a#[macro_export]macro of the crate, or else an item of the module reached, whose file is the target. Sections 6, 11, 12, 13 and 15 have the rules as they stand after the reviews.bitflags' external.rs now keeps
mod serde,crate::serde::serializeandcrate::serde::deserializeon external/serde.rs and, with serde vendored,use serde; the macro self edges are gone.4. Review follow-ups
Loader:
#[cfg]-gated item is skipped whatever token starts it, not only a word: a{ … }block statement, a match arm whose pattern is a literal,&x,(a, b)or[a, ..], a tuple-struct field type, a*xstatement. A"json" => serde_json::to_string(..)arm with the json feature off no longer bundles serde_json, and acfg_if!branch whose cfg can't hold (if #[cfg(feature = "wasm_js")] { mod wasm_js; }, hashbrown'sfeature = "nightly"lsx branch, ahash'sfeature = "std"hash_map/hash_set) is dead code now.#[path]and a plainmodkeeps the plain mount's<stem>/dir as a fallback for its ownmoddeclarations (R5).resolveModDecldrops cfg_attr path variants whose predicate can't hold whenfeatures/test/targetare passed; without them it trusts the scanner's build, as the loader's own calls do.Cargo:
--cargo-featuresname applies to every root package that has it, not only the first.[workspace]), else the bundle root's own; a lock further up belongs to another project and no longer makescargo metadatarun--locked.kindskeys such asnormal@cfg(unix)), so it neither turns the plain request optional nor strips its defaults.[patch.crates-io.foo]sub-tables andbar.path = "…"dotted keys are read like the inline-table form.dep?/xasked for with--cargo-featureswas applied once, before the fixed-point loop, whendefaulthad not yet activateddep, so it did nothing and said nothing (the manifest form always worked, being re-applied every pass). The requested features now re-apply on every pass too; the unknown-name warning still fires once.>=1.*is>=1,>1.*is>=2.0.0); only a bare1.*is a tilde range.5. Files rustc reads that the bundle left out, and edges it didn't record
include!/include_str!/include_bytes!with a literal path.include!("generated/consts.rs")splices Rust source the bundle never carried; the asset macros name files it never carried either, most often a crate's#[doc = include_str!("../README.md")]. The scanner records all three, in items and inside attributes (a live item's#[doc = include_str!(…)]too, a dead item's not), and the walk follows them relative to the including file: Rust source as a file of its own, assets asresource/resource:base64, never scanned. Edges:include <path>,include_str <path>,include_bytes <path>. A literal naming no file warns; build output underconcat!(env!("OUT_DIR"), …)can't be followed.macro_rules!invoked by name and defined in another file of the crate (libc'ss!from macros.rs, tokio'sready!, syn'sToken!) is a dependency of the invoking file: edge<name>!. A path-qualified invocation ($crate::name!,serde_json::json!) is a path, not a bare invocation.#[cfg(…)]. A file whose inner cfg can never hold is compiled empty (log's serde.rs behindfeature = "serde_core", tokio's io/uring files behindfeature = "fs"): carried, scanned as empty, so nothing in it is followed or reported. The same for an inline module opening with such an attribute..cargo/config.tomlnames under[source.vendored-sources] directory, elsevendor; thecargoecosystem tag and thecargo vendorhint follow it.6. The path resolver, after a second review
The re-export follower of section 3 resolved every path by recursion over the imports of each module it passed, capped at depth 8 and guarded per resolution. The review found two regressions against ordinary code and a list of smaller ones.
A glob into the sysroot or another crate claimed every name.
use std::io::prelude::*,use rayon::prelude::*, a fn-bodyuse core::arch::x86_64::*, or ause super::*when the parent has such a glob: any name looked up in that module was taken for the glob's, which hid the file's crate edges and unresolved reports. Rule now: a glob provides a name only when the module it names is one of this crate and that module has the name, as a child module or through auseof its own (so a glob does follow a named re-export of a plain item to the file holding it:crate::Itemthroughpub use a::*and a'spub use crate::q::Itemlands on q.rs). A glob the loader can't see into claims nothing.Time grew with the square of the file count. A synthetic 800-module crate glob-re-exported from its root took 33s; libc alone 12s. The resolver now walks the module tree and asks, per (module, name, how much of the module's imports the asking module may see), what the module's imports make of the name, computed once per crate and shared by every path (
provided). Glob targets are collected once per module as a transitive closure, settled closures standing in for further walking; import cycles read as nothing while in progress, and only entries computed on top of such a read are discarded, so a later query gets a full answer. libc: 27ms; tokio: 61ms; the synthetic crate: 0.6s.The rest of the list:
mod imp { }under one cfg no longer shadows amod imp;file under another in the module tree:crate::imp::fpoints at imp.rs.#[cfg_attr(unix, path = …)]beside#[cfg_attr(windows, path = …)]) no longer get an empty-keyed edge to their sibling; a path anchored on a file's ownusefollows that file's import, not the sibling variant's, and a via edge names the file whose import was followed.#[macro_export]macro is named only by a path's final segment at the crate root:use log::debugis the cratelog, and reported when it isn't vendored, even when the crate defines alog!macro. An invoked path ($crate::span!) names the macro before a module of that name (tracing), and at the root the macro wins over a glob (pub use facade::*beside$crate::helper!).#[macro_export(local_inner_macros)]and amacro_exportundercfg_attrcount (serde_core).macro_rules! helper { … } pub(crate) use helper;resolves to the macro, not to a crate of that name: no more falseuse cfg_ifedge to vendor/cfg-if, nohelperreported unresolved.pub(self),pub(super)andpub(in …)re-exports reach only their scope; a plainuseonly the module and its descendants.extern crate serde_core as s;is followed likeuse ::serde_core as s(s::Valuerecords serde_core), andextern crate self as me;likeuse crate as me.super::ToTokensfrom quote's ext.rs no longer addssuper→ lib.rs); every file of the crate hangs off the root anyway.useitems bind one name (cfg variants; serde's trait and derive-macro re-exports ofSerialize), the first the bundle can follow wins; one into a crate that isn't in-tree binds the name to its own file as far as the bundle knows, so serde's$crate::__private::Resultrecords lib.rs, the file whoselibmodule re-exportscore::result, withcrate::__private→ private/mod.rs as the module it went through.resolveUsePathis gone.7.
--cargo-target=<triple|host>: target cfgsTarget cfgs were always undecided, so a bundle carried every platform's code: libc's 376 files, tokio's windows and every getrandom backend, and reported crates only other targets depend on (
windows_sys,unsupported_target) as unresolved.stasis bundle --cargo-target=<triple|host>asksrustc --print cfg --target <triple>(host: the running rustc's triple, fromrustc -vV) for the target's cfg set and decides target leaves against it:unix,windows,target_os,target_family,target_arch,target_env,target_vendor,target_abi,target_pointer_width,target_endian,target_has_atomic.target_featureandtarget_thread_localstay undecided (a build may add features; both vary with the toolchain and its flags), as do profile cfgs (debug_assertions,panic) and custom ones. Acfg_attr(…, path)variant whose predicate holds is the#[path]itself and supersedes the default file. Target-specific dependency tables count only for the target (build-dependency tables always: build scripts compile for the host), and with--cargothe triple goes tocargo metadata --filter-platform. rustc is$RUSTCwhen set, as cargo honours it — a target only another toolchain knows, such as Solana'ssbf-solana-solana, needs that toolchain's rustc (RUSTC=~/.cache/solana/<release>/platform-tools/rust/bin/rustc) — elserustcfrom PATH; sections 12 and 13 have where it runs from. Only rustc's built-in knowledge of the target is needed, not its standard library. Without the flag nothing changes.evalCfgmoves to cargo.js (the manifest replay needs it too); a Cargo context takestargetas a triple/hostor a ready{ triple, cfgs }, which the tests use to stay rustc-free (the rustc-dependent tests skip without one).8.
--cargo-manifests: manifests, lockfile, cargo config and build scriptsThe Rust counterpart of
--package-json: the bundle also carries each bundled package'sCargo.tomland the workspaceCargo.tomlabove it, that workspace's (or, outside any, the package's own)Cargo.lockand cargo config —.cargo/configbefore.cargo/config.toml, the order cargo reads them; each as written, see section 17 — when they lie inside the bundle root, all asresourcefiles in their package's bucket (the workspace bucket for the root-level ones), and each package's build script ([package] build = "…", elsebuild.rs;build = falsemeans none), walked like a crate root of its own and compiled for the host, so its modules and the in-tree[build-dependencies]it reaches come along as Rust code, and their build scripts in turn, until no package is new. Nothing runs.9.
#[path]on an inline module (solana-program)solana-program's
message/mod.rsdeclaresAn inline module's own
#[path]names the directory itsmoddeclarations resolve under, in the module name's stead; an empty one puts them beside the file. The loader used the module name, looked formessage/non_bpf_modules/account_keys.rs, found nothing and — the module being cfg-gated — silently left the four files out of every bundle; with--cargo-target=hostthe cfg is decidably on and the missing modules were fatal. The scanner now records the directories an inline-module chain stands for andmodresolution uses them; solana-program bundles with and without a target, the four files included.10.
usewords that aren't imports; every macro-emittedmodvarianttokio's bundle reported
let,true,input,cursor,precise_capture_begin,allow_crate_root_in_path,pathandstatic_macroas unresolved crates.Token[use]and serde_derive'squote! { use #path as _serde; }. Anyuseword was taken for an item and its "tree" read up to the next;. Ausenow counts only when a use tree follows it (a name,::,{, or$cratein amacro_rules!template); one interpolating a metavariable (use $m::X;,#path; a raw identifier'sr#is not one) is a template and skipped. Aquote!/quote_spanned!/parse_quote!body is generated code for another crate and is skipped whole, so serde_derive no longer carries serde's 22 files (pulled in byquote! { extern crate serde as _serde; }) nor 480#private::…edges, and a$trait::fmt(…)in a template is a metavariable's path, not a module's.static_macro: tokio'scfg_has_atomic_u64! { #[path = "native.rs"] mod imp; }besidecfg_not_has_atomic_u64! { #[path = "as_mutex.rs"] mod imp; }. Both declarations were keyed*and the second file was dropped from the module tree, so its own paths went unanchored. A declaration a macro emits is keyed by the macro (cfg_has_atomic_u64!), acfg_if!branch's declarations by the branch's cfg (if #[cfg(unix)] { mod imp; }→unix; a#[cfg]on a block or inline module now gates everything declared inside), and a second file under an already used key gets it numbered (*#2) rather than lost.The remaining reports on tokio's default-feature bundle (
io_uring,backtrace, bytes'extra_platforms) are accurate: the files are bundled because tokio'scfg_io_uring!-style wrapper macros hide the#[cfg(feature = …)]that would exclude them, and they name optional dependencies that aren't vendored. Seeing through such cfg-wrapper macros would be the next improvement.11. A third review: vendored crates as untrusted input, and the resolver's blind spots
Security. A
cargo vendored crate is input the project author didn't write, and--cargo-manifestsmade three ways for it to carry the project's own files into a bundle:include_str!("../../../.git/config"),include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../.env")),#[path = "../../../secret.rs"] mod x;and[package] build = "../../.env". A vendored package's includes,#[path]targets and build script must now lie inside its own package directory; anything else is refused with a warning (amodso refused is a missing module, so fatal unless cfg-gated). The project's own code may still include anything under the bundle root. The cargo config carried with--cargo-manifestskeeps only the tables that describe a build ([source.*],[build],[target.*],[patch.*],[profile.*],[alias]);[http],[net],[registries],[env],[credential-alias]and the rest may hold tokens and proxies and are dropped.--cargo-targetgot a 60-second limit andRUSTUP_AUTO_INSTALL=0, so an uninstalled toolchain is an error rather than a download (where it runs from changed again in sections 12 and 13). An include target is checked to be a regular file before it is read (a FIFO hung the bundle).Resolver regressions found in section 6's rewrite. A glob skipped the items a module defines (libc's
crate::sigset_tfell through to a later platform's re-export; mio's sys/windows lost its edges): the tree pass now indexes every module-levelstruct/enum/union/trait/type/fn/const/static(anexternblock's fns too, not animpl's associated items or a fn's local ones) per module, with its visibility, and a glob brings them in, the module's own tree file first when a module is spread over cfg variants; a privatemodis not glob-importable. A one-segment macro name hijackeduse log(the crate) and an exported macro beat a named re-export for an ordinary callcrate::helper(): an invocation names the macro first, any other path the import or module first. Names a sysroot or other-crate glob may provide (use syn::*; … punctuated::Punctuated) are no longer reported as unresolved crates. A bare macro call after a single:(a:helper!()) keeps its edge. Of the partly-fixed items: inlinemod imp { mod inner; }beside a same-namemod imp;file now joins one module tree (the tree is built in two rounds per file);self::…paths and glob-provided names in a second platform file resolve through that file's own imports first;extern crate … as sat the crate root is in the extern prelude, visible from every file; theelsebranch of a deadif #[cfg(…)]is live again.a::self::bstays declined (rustc rejects it).Correctness. Several
#[path]/#[cfg_attr(…, path)]on onemod: the first whose predicate holds decides and ends the list, and the default lookup is off (it used to be "any true path wins"). Build scripts compile for the host: no target cfg is decided in them and every[target.'cfg(…)'.build-dependencies]table counts. A false#[cfg]inside a macro invocation body (m! { #[cfg(never)] … }) no longer empties the file. A file named by bothmod/include!andinclude_str!is Rust, not a resource, whichever is met first. An include inside amacro_rules!body resolves relative to each invoking file, as rustc expands it (verified against rustc). Include arguments: escapes in plain strings, raw strings andconcat!(env!("CARGO_MANIFEST_DIR"), "/…")are read; a non-literal argument other thanOUT_DIRis warned about instead of silently dropped or misread..cargo/configis read before.cargo/config.toml, as cargo does; a workspace nested below the bundle root keeps its own Cargo.lock and config instead of the root's.target_featureandtarget_thread_localare undecided.12. A fourth review: a crash, the toolchain choice, symlinks, and cfg-aware resolution
Blockers.
use log;oruse x::{self, …}in a module spread over platform files (#[cfg_attr(unix, path = …)] mod sys;) overflowed the stack: the same-file lookup asked its own module for the name, which is that very import, outside the memo; it goes through it now.--cargo-targetran rustc from the bundle root (after a round of running it from a temp dir), and a rustup proxy picks its toolchain from therust-toolchain(.toml)files of the working directory and its parents — a file that may name apathto any binary, so the project being bundled (or anyone able to write to/tmp) chose what ran. It now runs from the user's home directory (else the filesystem root), never from the project nor a temp dir;$RUSTCstill wins, andRUSTUP_TOOLCHAINis the way to pin a project's toolchain deliberately. A vendored crate could still read outside its package through a symlink (a linked file or directory of modules, a linked manifest, a linked include target: the check compared paths as text), through an inline module's#[path = "../../.."](left unnormalized, so a file was carried under a name likevendor/evil/src/../../../x.rs), and through a[lib] pathoutside the package: every file of a vendored crate must now really lie in its package (real paths, manifests included), the inline path is normalized and held to the package like any other, and an outside[lib] pathis refused. Credentials in the URLs of carried config tables (registry = "sparse+https://user:token@…",[patch] git = "https://token@…") are stripped. The fourth blocker, a failing test inrust-loader.test.js, did not reproduce on Node 24; section 13 has what it was.Resolver. The written-order glob rule of section 11 resolved a
unixfile'scrate::c_intthrough libc'sfuchsiabranch (1,837 edges fromunixmodules intofuchsia, 478 intowindows), and a module spread over platform files mixed its variants (mio'ssys::Wakerfrom an epoll file went to the poll selector's; tokio'simp::StaticAtomicU64to the other variant). Each file now carries the cfg leaves of everymodon the way down to it (plus avariantleaf when amodhas several files), each import and defined item its own cfg's on top, andcfg_if!branches after the first are keyed by the earlier cfgs not holding (all(not(unix), windows)); a path skips candidates whose leaves can't hold with its own file's (a leaf and its negation, two values of a single-valued key such astarget_os, two variants of onemod,windowsagainstunix), and a file under no such cfg gets the first in written order. So libc's unix files stay in unix, mio's epoll file gets the epoll branch'sWaker, tokio's variant files get their own variant's items. The candidates for a (module, name) are listed once, lazily (a closure holds hundreds of modules and the first few usually answer), and shared by every asker; an import among them is followed when first picked. Glob closures computed short of an import cycle are recomputed once every entry is in place and the paths resolved again, so the answer no longer depends on which side of a cycle was asked first — the one shape found where it did (a: pub use crate::b::*; pub use crate::c::*/b: pub use crate::a::inner::*/c: pub mod inner { pub struct X; }, askinga::Xfirst) is a test now; two randomized checks of glob/re-export webs against a brute-force reference (48,000 asks, inline modules and cycles included) found no other, at either commit. A glob into an in-tree crate explains only the names that crate's root has (tracing'suse tracing_core::*;no longer hidestracing_attributes), a glob onto an enum or into the sysroot none.crate::helper()andcrate::helper!()get separate edges (crate::helper!). A one-segment path names an exported macro when no crate has the name (anyhow'spub use anyhow as format_err;), and::s::…uses the root'sextern crate … as salias. Barename!follows rustc's textual scope (checked against rustc 1.94): a macro defined after themodmounting a file is not in that file's scope, a nested#[macro_use] modreaches its own parent module only, exported macros are in bare scope in the root file and wherever they areused or glob-imported (use super::*;from the root,use crate::combinator::dispatch;), and nowhere else. Amodwhose every cfg names one file is a flat edge again (libc'smod primitives). The items a module defines leave outmacro_rules!templates' items, lazy_static'sstatic ref, and the bodies ofextern "C" fns, and ause $crate::…in a template is the invoking module's.quote!-family bodies are skipped however the macro is named (quote::quote!too), unless the file defines amacro_rules!of that name itself. The scanner's impl/trait/extern block lookup is a stack, not a scan over every block.Corrections to section 11. A
modinside aninclude!d file resolves beside the included file, not the includer (include!("gen/list.rs")withmod bar;→gen/bar.rs, its submodules undergen/bar/), as rustc does — checked against rustc 1.94, both directions; it is still the includer's child module. A deadlet … = if … else …and a dead match arm now take theirelsebranches with them, and under--cargo-targetthecfg_if!branches after one whose cfg holds are dead.#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))], the common form, is read.Still open at this point (section 13 takes these up). The synthetic all-to-all glob crate remained quadratic: 200/400/800/1600 modules took 142ms/347ms/0.96s/3.5s; 800 modules with 5 globs 269ms, 1600 479ms, linear. Real crates cost about 3x what they did at
1dfe173(libc 55ms → 192ms, tokio 108ms → 334ms; the whole 44-crate corpus 0.5s → 1.4s).a::self::bstays declined.13. A fifth review: the same import in two platform files, namespaces,
any(…)cfgs, source orderBlockers. Two platform files of one module importing the same name (
use log;in u.rs and w.rs;use libc;in both branches of acfg_if!) still recursed without end: section 12's guard was per candidate, so following one file's import asked the module for the name and found the other file's. The import being followed is now marked on the import itself, and its answers are kept per asker's cfg set. Ause super::*; pub use sibling::*;written in a platform file recursed too: resolving the file's own glob sources asked for the file's own candidate list while it was being built; it reads as nothing until built. Afn log() {}besideuse log::info, or a*const libc::c_charin a signature, hid the crate: the scanner records every defined item's namespace (fn/const/static are values; a*const Tis a pointer type, not aconstitem) and a segment with more path after it is looked up in the type namespace only, so no value item ever leads a path. Results depended on the order the sources were listed in (the first variant file scanned woncrate::sys::Thing): each module's files are now indexed in tree order, the variants of amodas declared, whatever order the caller lists the files in (a test runs every rotation of the sources). The failing test of section 12 was a stale expectation:scanRustItemshad given themod impinside#[cfg(unix)] mod sys { … }itsunixcfg since section 10, the test still expectednull, and Node 24's legacyassert.deepEqual(unlike Node 22's) passes'unix'againstnullinside an array — so CI on Node 24 was green while a Node 22 container failed it. The expectation is'unix'now.Wrong edges. Every item of the review's list, checked on the corpus:
mod linux_l4re_sharedin both the linux and the l4re branch) got no cfg at all; it is under any of them now — anany(…)of their predicates.any(…)predicate whose alternatives each decide something is one leaf, and so is a negatedall(…): the leaf is exclusive with a set none of its alternatives can hold with (libc'sany(target_os = "linux", target_os = "l4re", target_os = "android", target_os = "emscripten")branch against a file undertarget_os = "aix"), and holds when one alternative does. Before, anany(…)contributed nothing and its whole subtree was compatible with every file.mod primitives, glob re-exported under a dozen cfgs) kept the first path's leaves; it is under either path's now.target_osleaf its file was mounted under) comes before one merely not ruled out; a file under no cfg keeps written order.src/unix/<os>subtree pointing into another — go from 668 to 12 (the twelve arearch/<cpu>files, which every libc environment shares, pointing intouclibc).cfg_if!corrupted the chain around it (its else-negations leaked into the outer branches): chains are kept per block depth.#[macro_use] mod's macros always beat the file's own): the later definition of a name now shadows the earlier, whichever kind each is, so serde'stri!, defined by the root'scrate_root!()after#[macro_use] mod crate_root;, is the root's (private/de.rs and private/ser.rs'stri!edges move from core/crate_root.rs to lib.rs).modormacro_rules!amacro_rules!template declares took its scope from the definition; it stands where the macro is first invoked in the file now (serde'scrate_root! { … macro_rules! tri … pub mod de; … }), with the template order deciding between items of one invocation.use x;in a child module matched a root-exported macro of that name and hid the crate; only a path written at the crate root names it (anyhow'spub use anyhow as format_err;in lib.rs still does).use tokio::sync::*; use mpsc::…reportedmpsc. The glob's path is walked inside the other crate and the module it names there is asked — what it has, what itspubglobs bring in, what a glob of its own into a third crate may.quote!-family skip missed amacro_rules! quotedefined in another file of the package (syn'sparse_quote!): a package defining such a macro in any of its files has the bodies of that name scanned as macro input throughout, by rescanning only the files that skipped one.Performance. With the checks above, the corpus first ran 2.2x slower than the previous commit. Leaf lists are now prepared once for the exclusivity check (keys, negations, single-valued values, Windows/unix standing), closures are indexed by module so a name's candidates come from the modules having it rather than from every module of the closure, unions of leaf sets are cached per pair, and the template rescan touches only the files that skipped a body. The 44-crate corpus runs at parity with
0accd58(1.34s against 1.36s; libc 149ms against 185ms, tokio 315ms against 321ms); the synthetic 800-file crate with 4 globs takes 355ms and scales linearly (200/400/800: 78/152/355ms); 800 files each glob re-exported from the root take 0.99s, the same as before. Both randomized reference checks (51,600 asks) still find no mismatch.Low priority. Carried
Cargo.tomlandCargo.locklose the credentials in their URLs — auser:token@userinfo and any query parameter named like a secret (?access_token=…), the rest of the query (?branch=main) and the fragment kept; the cargo config filter does the same and drops a[target.*]table'srunner(a command forcargo runto wrap the binary in, not a build's description). rustc runs from the filesystem root when the home directory is the bundle root or lies inside it (a project bundled from$HOME, a CI job whose HOME is its checkout), so the project's toolchain file never chooses.Still open. Without a target, which
cfg_if!branch a file under no cfg means stays a written-order guess (hashbrown'simpis the sse2 branch, mio'sWakerthe pipe one);--cargo-targetdecides it.a::self::bstays declined.14. After the rebase onto #177: one URL credential scrubber
The two URL credential scrubbers that landed side by side — cargo's for carried Cargo manifests and config (section 13), #177's for carried Solidity manifests — are one (
stripUrlCredentials: a URL's userinfo and secret-named query parameters go, the rest of the query and the fragment stay; section 15 has the SSH rule), used by both. The move of the shared TOML reader intoloaders/toml.jsthat was here is #179 now, on its own.15. A sixth review: gates the loader can't see into, cached answers across a cycle's reset, imported values, custom cfgs
Regressions. Section 13's "prefer the candidate whose cfgs hold" rule took a candidate under no leaves for certain, and a
moddeclared inside acfg_<x>!macro body carried none, so mio's unix wakers resolvedcrate::sys::Selectorto the windows selector (no target) or the shell one (a Linux target). Two causes: the scanner dropped a#[cfg(…)]placed on a macro invocation (#[cfg(windows)] cfg_os_poll! { mod windows; … }), which now gates everything the body declares; and the body of acfg_<x>!macro (tokio'scfg_io_uring!, mio'scfg_os_poll!) is now a gate leaf of its own,cfg_not_<x>!its negation, so what the two declare is never taken together and neither counts as certainly compiled. mio (1.0.4, added to the corpus) now resolves the eventfd waker'sSelectorto epoll.rs, the kqueue waker's to kqueue.rs, the shell waker's to shell/selector.rs, and windows/selector.rs'scrate::sys::Eventsto windows/event.rs. The answers imports had given on top of a glob closure short of an import cycle survived the cycle's reset (apub use child::run;at the root asked before the cycle throughchildwas closed): they are dropped with the glob sources, and the A1 shape through a named root re-export is a test.Still open from the last round, now fixed.
use crate::util::log;of afn log) stood for the crate inlog::info!, and the crate edge was lost. The path now resolves to nothing in the type namespace, the lead names the crate, and — since the walk from disk had also takenlogfor the fn and never loaded the crate —buildRustTreereports such crate roots aswantedRoots, whichbuildRustBundleloads and builds again with, until none is new.modamacro_rules!template declares took its textual scope from the declaring file even when that file never invokes the macro: it now stands at the invocation in the first file (tree order) that does, so serde'score/de/value.rsgets itsforward_to_deserialize_any!edge (crate_root.rs declarespub mod de;insidecrate_root!, invoked in lib.rs after#[macro_use] mod macros;).macro_rules!written later in the file doesn't shadow what an earlier#[macro_use] modhanded an earlier call (the scanner records each name's call offsets).quote!'s bodies as templates when the defining file came later: the walk keeps each package's template definitions and rescans the files that skipped one, so amodsuch a body declares is loaded.use serde::*may provide anything) walked the whole glob closure per (module, lead); the globs leading out of a crate are indexed once, gathered once per module, and the per-lead check is a lookup.Ranking under a target and custom cfgs (the second commit). With
--cargo-target, every file compiled for the target now asks under the target's cfgs too (unix,target_os = "linux", …; not a build script's, compiled for the host): a candidate the target rules out is out for every file, and one whoseany(…)the target satisfies is certain. And a positive cfg neither rustc nor cargo sets (loom,docsrs,tokio_unstable,mio_unsupported_force_poll_poll) is a--cfga default build lacks: a candidate under one is doubtful, taken only after a compatible candidate under none, and one under its negation (not(loom)) counts as certain; anany(…)is doubtful when each alternative is, or can't hold with the asking file; a gate leaf and amodvariant's are neither. Items a module defines in several files rank the same way (own tree file first within a rank). So mio'ssys::Wakerunder a Linux target is the eventfd waker (written order had put the poll selector's first; without a target it still does, as section 13 says), getrandom'sbackends::fill_inneris thegetrandombackend rather than thegetrandom_backend = "custom"one, and libc'scrate::kinfo_procfrom freebsdlike/mod.rs is dragonfly's rather than thefreebsd15-gated copy.Low priority. The HOME-in-bundle-root check compares real paths (a linked home or root); a quoted
"runner"key goes from a carried[target.*]table too; an SSH URL (git+ssh://git@github.com/…, a lockfile's usual form) keeps its user — the account the key logs into — and loses only a password after it.Performance. The corpus runs within 7% of
494e9f0(1.58–1.62s against 1.48–1.50s), libc 250ms; both randomized reference checks find no mismatch (51,600 asks); the synthetic 800-file crate with 5 globs and two unresolved leads per module 440–480ms against 370–430ms. The all-to-all glob crate stays quadratic — 400/800/1600 files: 0.46/1.0/6.6s against 0.38/1.0/4.4s — because every module's glob closure holds every module: N closures of N entries, inherent to per-module closures (sharing them across a glob cycle would need a different representation; real crates' closures are small, and the corpus is at parity).Still open.
use serde::*(a glob into a crate that isn't in-tree) still hides a missing crate the same file names, by design: the loader can't see what such a glob brings in. Without a target, acfg_if!branch ormodvariant under anany(…)of platforms stays a written-order guess.a::self::bstays declined.doc/file-formats.mddescribes every rule above.16. After the rebase onto #179: the strict TOML reader
#179 replaced the line-based TOML reading with a strict TOML 1.0 reader (
tomlEntries, entries by table path). This PR's Cargo readers now sit on it:parseCargoManifestreads this PR's additions by table path:[package] build,edition = { workspace = true }/edition.workspace = true,[workspace.package] edition, target-specific dependency tables as their own request (normal@cfg(unix), now without the quotes the header spelled), and every[patch]spelling. The dotted-segment splittertablePartsis gone. Dependency kinds are aMap, so a table namedconstructoris not a kind.vendorDirOffinds[source.vendored-sources] directoryin any spelling, the inline one included.Cargo.toml:3: duplicate key "package.name",.cargo/config.toml:2: invalid value "third_party".No try/catch wraps a TOML read. A file that is missing is still fine. A Cargo.toml, Cargo.lock, cargo config or foundry.toml that exists but isn't TOML stops the build. That includes a dependency's foundry.toml: the Solidity loader used to skip a dependency config it couldn't load, with a warning. It now stops the build, for a TOML error and for the refusals too (an
extendsoutside the dependency, nested inheritance, a key collision). forge itself skips a nested config it can't load, so a dependency forge tolerates can now stop a bundle. The doc says so. Since the rebase onto #178 (section 25), main does this itself for a dependency config that isn't TOML, and the Solidity half of this change is dropped. A config forge refuses for its settings is skipped with a warning, as on main.17. After the rebase onto #182: every carried file as written
#182 dropped the cleanup of what
--manifestscarries: a carried file is the one the build used, byte for byte, never an edited copy.--cargo-manifestsfollows it. The Cargo.toml files, the Cargo.lock and the cargo config are carried as written, or refused:[http],[net],[registries]and[env]tables are carried too, and so is a target'srunner.stripUrlCredentialsof sections 12 to 15 is gone. The credentials in agit = …URL or a lockfile'ssourcestay.Rust manifest is not valid UTF-8: Cargo.lock) rather than decoded with replacement characters.toml.jsis main's again: the per-entrysourcetext added for the filter is gone.What decides which files may be carried is unchanged: nothing outside the bundle root, and a vendored crate's files only from its own package. The doc and the usage text say the carried files may hold credentials. They advise keeping registry tokens in
~/.cargo/credentials.tomlorCARGO_REGISTRIES_<NAME>_TOKEN, or not passing--cargo-manifests.18. A seventh review: host code, build files, cfgs a build sets, gate definitions, variant maps, vendored versions, template
modsThis round is four commits on top of section 17's:
0b9422c: the review's findings.eb1fbaf: deciding a cfg in which an unknown leaf repeats.2d56881: a module's own items ranked with its imports.03d2421: caches for the tree pass.The numbers follow the review's.
Silently incorrect, now fixed.
Non-UTF-8 files. A Rust source (a module, a build script, an
include!d file) or aninclude_str!file that isn't UTF-8 now stops the build and names the file; rustc refuses such a file too. It used to be carried with U+FFFD in place of its bytes. Aninclude_bytes!file may hold any bytes, and is carried asresource:base64when it isn't UTF-8.Host-built code. Each file records the compile units it is built as, written
<features>|<platform>:target|target;target|host;wantedRootsincluded):host|host.Host code is judged against the host's cfgs, which the loader knows only with
--cargo-target=host; with another target, no platform cfg is decided in it. A crate built both ways (a dependency that is also a build-dependency) is scanned under both units, and an item in it is dead only when it is dead in each.Build files under
--cargo-manifests. A vendored crate carries itsCargo.tomland.cargo-checksum.json. TheCargo.lockand.cargo/configit was published with are no longer carried (the corpus's 30 extra files). A project package carries the cargo config of every directory from its own up to the bundle root, so a nested workspace also gets the root.cargo/config.toml.cfgs a build sets. A custom cfg is no longer presumed off when the package's build script prints it (
cargo:rustc-cfg=name, orcargo::) or a rustflags--cfgsets it, so itsnot(…)branch no longer wins by default. The rustflags are read from:buildandtarget.<…>rustflags;RUSTFLAGS,CARGO_ENCODED_RUSTFLAGSandCARGO_BUILD_RUSTFLAGS.A build script that formats a cfg name or uses
autocfgmakes every custom cfg of its package undecided.cfg_x!/cfg_not_x!. A gate's cfg is read from its package'smacro_rules!definition ($( #[cfg(feature = "rt")] $item )*, the same in every arm), not inferred from its name. A gate whose definition can't be read is opaque: never certain, and never exclusive with another gate. That covers a gate with no definition in the package, arms that differ or write no cfg, and two definitions that differ.Macro scope.
modis only ever placed in its own package (see 16).use serde::*. A glob into a missing crate no longer hides a crate the package declares. The lead is that dependency (rustc rejects the ambiguity, E0659), and it is reported.Separately,
eb1fbafdecides a cfg in which an unknown leaf occurs more than once, when every value of that leaf gives the same result. zerocopy's#[cfg(any(test, kani))] mod tests { #[cfg(not(kani))] mod compatibility { use rand::…; } }reduces toall(any(test, kani), not(kani)), which never holds, so itsuse randis dead code rather than a missing crate.Wrong variant files.
{platform: file}shape, or a single file when they all agree. A candidate under a cfg the build rules out (a feature that is off) comes last.mod deyields to thepub use serde_core::deof every other build.2d56881: a module's own items now rank with its named imports, ahead of glob imports, and an import leading out of the bundle counts as an answer under its cfgs.imp::AtomicU64(the review'simp::StaticAtomicU64, in the vendored version) maps both variant files: std'sAtomicU64, re-exported in atomic_u64_native.rs undertarget_has_atomic = "64", and the mutex-based one atomic_u64_as_mutex.rs defines under the negation.crate::trace::trace_leafis the fn defined undercfg_not_taskdump!, not the import undercfg_taskdump!, which is off.sys::Wakerwithout a target maps each platform's waker.Vendored version guessing. A dependency resolves to the version its
Cargo.lockrecords, else to the one vendored version its requirement allows. The loader warns and doesn't follow the dependency when:No vendored version of rand satisfies zerocopy 0.8.59's requirement 0.8.7 (vendored: 0.10.3));Cargo would build none of those copies. A package's own name for a crate is also its manifest's word: a name the manifest doesn't declare is no crate of the package. winnow's
pub use bytes::Bytes;is its ownmod bytes, not the vendoredbytescrate.Out-of-root
[patch]/ path dependencies are not in-tree, and no longer bind to a vendored crate of the same name instead.Features.
--cargo-targetsays it applies (a build-dependency's table is judged against the host), and not when it says it doesn't.resolver, else its edition). It used to come from the bundle root's manifest, which a root holding only vendored crates lacks. So a 2021-edition entry's dev-dependencies no longer count: serde_json'sserdewithderiveis a dev-dependency only.Macro assets. An include in a macro that another macro's body calls resolves against the outermost call site, and two same-named macros keep both bodies' assets.
Vendored source directory. It is the directory the
replace-withchain fromcrates-ioleads to, whatever its name. When the configured directory doesn't exist, the loader warns instead of using a stalevendor/beside it.Loud.
moddeclared in amacro_rules!body is now declared in each module of its package that invokes the macro, and its file is found beside that module's file, as rustc expands it. It carries the cfgs under which the invoking file mounts the definition's file.macro_rules! decl { () => { pub mod gm1; } }invoked in a.rs declarescrate::a::gm1(src/a/gm1.rs), so the root'suse a::*; use gm1::X;resolves.crate::m!()) keeps itsmods beside the definition, and the walk from disk loads those last.prelude!(), defined in macros.rs and invoked in lib.rs, declaresmod types;, which rustc reads as src/types.rs. The loader used to look for src/macros/types.rs and silently leave the file out.extern crate self as x;now puts the crate itself in the extern prelude, sox::…resolves from every module (syn's ownuse syn::parse::ParseStream).Performance (
03d2421). The tree pass now caches three things: a leaf's verdict per build, an alternative's cfg text per asker set, and a file's package.274fe99;589afcebefore the rebase onto fix(bundle): read TOML with @preventive/lockfile's parser #183): libc 282 ms (was 227 ms), tokio 464 ms (444 ms), mio 287 ms (240 ms). The extra time goes to the added candidates, definitions and alternatives. Before the caches, libc took 351 ms.evalCfg's repeated-leaf pass costs about 2 ms of that, over the corpus's 10,686#[cfg]s.Still open.
moddeclared in a template takes the inline modules of the definition, not of the invocation. In every case seen, both sit at a file's top level.doc/file-formats.mddescribes every rule above.19. After the rebase onto #183:
@preventive/lockfile's TOML parser#183 replaced #179's reader with
@preventive/lockfile's strict TOML parser, which returns a tree of tables. This PR's Cargo readers were ported to it in the commits that introduced them:parseCargoManifestreads the tree:[package] build, an inheritedversionoredition,[workspace.package] edition,[lib] proc-macro, and target-specific dependency tables as requests of their own (normal@cfg(unix)), however a table is spelled.vendorDirOfreads each[source.<name>]table'sreplace-withanddirectory.rustflagsCfgsOfreads therustflagsof[build]and of every[target.<…>]..cargo/config.toml: expected a value, found "third_party" at line 2. The tests follow them.20. An eighth review: a refused crate root, unreported missing crates, Cargo per dependency table, four resolver regressions
One commit on top of section 19's:
a401644.Blocking, now fixed.
src/lib.rslinks out of the bundle root. That root was asked for on every pass, so the loop never ended. Now a root is asked for once; the walk has already said why it refused it.vendor/dir existed. Every crate the bundle lacks is now reported, with or without a vendor dir:[stasis] 3 crates referenced but not in the bundle: far, linked (vendor/linked/src/lib.rs), missing. A refused root is listed with its path. Thecargo vendorhint still appears only when there is no vendor dir. The loader now also warns about eachpathdependency outside the bundle root, eachpaththat names no[package], and each[patch]outside the bundle root; before, these were dropped silently.mod de) gives way only to its module's ownuseor item of that name. What a glob brings in never shadows a module that is there.fn manswered the lookup form!()besideuse crate::macros::*. A macro call is now looked up among macros only: an item the module defines, a module (a childmod mtoo), or an import of a module or crate is nom!. The resolver'stypesOnlyflag became a namespace (typeormacro), carried through imports.macro_rules!took over its template's calls. The calls and includes after an innermacro_rules! inner { … }were recorded on the inner macro, so invoking the outer one lost itsinclude_str!asset andhelper!edge. They now stay with the top-level template.all(my = "a b", not(my = "ab"))came out false and dropped its module.normalizeCfgalso collapsed spaces inside quotes. Now both keep a value exactly as written; only spacing outside the quotes is ignored.Older silent Cargo bugs, now fixed.
packageandworkspacenow belong to its table, not to its name. Sorand = "0.7"under[dependencies]besiderand = "0.8"under[build-dependencies], or afake = { package = "rand", version = "0.8" }dev-dependency, are separate crates, as cargo has it. A file follows the table its target uses: the build script uses the build-dependency, a test target the normal or dev one, anything else the normal one.Cargo.lockversion is now taken only if the requirement allows it. A lock that pins only versions the requirement doesn't allow gets the warningCargo.lock pins … which … doesn't allow: the lock is out of date, and the loader goes by the requirement instead.[patch].[patch]in.cargo/config.tomlis now read: from the package's directory up to the bundle root, nearest first and ahead of the manifest's. Itspathis relative to the directory that holds.cargo.21. After the rebases onto #192 and #193
Main's #174 and #185 to #193 needed no change here beyond one merge in
bundle.js: #189'shostparameter now sits beside this PR'scargoTargetandcargoManifestsinclassifyEntries. #192 and #193 bump@preventive/lockfileto 1.0.0-alpha.3 and then alpha.4. alpha.3 refactored the TOML parser with the same rules and messages; alpha.4 changes nothing stasis imports.22.
@preventive/lockfile's Cargo reader, and cargo's resolver where it can runOne commit on top of section 21's:
6a55380.Reading. @preventive/lockfile (alpha.3 from #192, alpha.4 since #193) now reads
Cargo.toml,Cargo.lockand the cargo config's[patch], and itsrust-semver.jsmatches version requirements. What it refuses stops the build, naming the file:dep?/xon a dependency that no table makes optional;[replace];Cargo.lockolder than version 3.The loader's own manifest and lock readers, its semver matcher, its implicit-feature builder and its config
[patch]reader are gone.Cargo's resolver. Cargo's resolver runs when all of these hold:
Cargo.lock;--cargo-targetis given;.cargo-checksum.json;Features then come from
linkCargoandresolveCargoFeatures: the lockfile's graph laid over the manifests, and whatcargo build -p <the entries' packages>turns on. The build stops on a lockfile out of date with the manifests, a vendored copy whose checksum isn't the lockfile's, or a feature asked of a package that lacks it. When the target isn't the host, the host is unknown, so the resolver runs twice: once with the host's target-specific tables off and once with them on, and what only they turn on is on maybe.The replay runs otherwise (no lockfile, no target, or a locked package not vendored). It has these fixes, from the comparison with PreventiveMeasures/libraries#56:
.cargo-checksum.json(a git copy has no package checksum) and by the lockfile's sources. Before,itoa = "1"could land on a vendored git copy, and a git dependency without a version on any copy.serde/derivenow also turns on the package's feature namedserde, whether written or implicit. Before, only an implicit one was turned on.1no longer takes1.1.0-beta.1.<bundle root>/Cargo.lockwas read.Also new:
cfg(true),cfg(false)and raw identifiers (r#unix) in cfgs.23. A simplification pass
One commit on top of section 22's:
a558b7d. What is bundled doesn't change, except where noted below.Speed.
scanRustItemsnow evaluates each cfg predicate once per scan. Before, it joined and evaluated the conjunction of the open#[cfg]scopes again at every token inside them. A 220 KB file inside one#[cfg]block scans in 36 ms instead of 588 ms, and in 28 ms instead of 1.3 s when it is compiled as two units.One walk.
buildRustBundleandloadRustshare one driver,collectRustBundle. It walks the build scripts, tracks the compile units, and loads the crate roots the tree pass asks for until none is new. That loop used to live inbundle.js, soloadRustskipped it and scanned host-built files again as target code.bundle.jsalso called its own copies of three helpers that are now shared: the check that a vendored file doesn't link out of its package,boundaryOfandaddUnits.cargo.js:
kind@targetapart again.[patch]source list, whether a vendored copy came from git or a registry, and each package's build script and build files are worked out once.toml.js, names the file in a TOML or lockfile error.rust.js:
evalCfg.resolveModDecl's cfg filter is gone. No caller used it, and the scanner already drops those variants.24. A tenth review: platform tables, build-script crates, proc-macro roots, out-of-root workspaces
One commit on top of section 23's:
2dd0aa4. Each item was reproduced first with a fixture, and checked against what cargo and rustc 1.94 do.Crate resolution:
[target.'cfg(windows)'.dependencies] foo = "2"above[target.'cfg(unix)'.dependencies] foo = "1"gave 2.0.0 on Linux; it now gives 1.0.0, on both paths and for build-dependency tables too. Without a target, both tables may apply, so both versions are carried and the edge is a cfg-keyed map of them.#[path], now resolve from the[build-dependencies]; the shared file gets both versions.u8::MAXresolved every declared dependency, dev-only and other platforms' included, and warned about their versions.serde_json::to_string(…),serde_json::json!).use dep::mac; mac!(), including an alias and a prelude'spub use dep::macbehind a glob, wherever indepthe macro is defined;dep::mac!();#[macro_use] extern crate dep.Cargo:
6a55380: with entriesappandpm,shared/src/noth.rswas carried although cargo never compiles it.memberstake it. The root is never bundled, and its lockfile is not read. Before,edition.workspace = truewas refused and the resolver came from the member's own edition.[patch]sources. A[patch."https://github.com/…"]no longer applies to the crates.io dependency of the same name on the replay, in either the manifest or the config form. This bug was already ona401644.vendor/.stasis bundlenow says when the features come from the replay and why, for example[stasis] Rust features from a replay of the manifests, not cargo's resolver: no --cargo-target. A vendored copy the lockfile doesn't list no longer forces the replay for lack of a checksum file.EXODUS_STASIS_DEBUGlabels the mode and prints both contexts..cargo-checksum.jsonlists stops the bundle, as cargo refuses to build it.cfgs and resolution:
r#trueandr#falseare now custom cfgs in every reader, not the literals.#[cfg(loom)] mod imp;besideuse crate::other::*;now resolvesimp::gtosrc/other/imp.rs.macro_rules!defined inside another's body belongs to that inner macro, resolved where it is invoked. Before, when the two were invoked in different directories, the asset was missing from the bundle.a ∨ (a ∧ b)isa), and leaf sets are capped at 16 alternatives. A ring of modules each globbing both neighbours took 2.35 s at N=8 and 16.7 s at N=9; it now takes 0.02 to 0.04 s. A complete glob graph at N=5, which took over 150 s, now takes 0.04 s.Not changed: the review items that are by design. A manifest key cargo only warns about,
[replace], and lockfiles older than version 3 are still refused. An out-of-date lock still stops cargo's resolver.--cargo-manifestsstill carries.cargo-checksum.jsonwithout the files it lists that the bundle doesn't reach, and still doesn't carryrust-toolchain.toml.25. After the rebases onto #198, #178 and #201
Main's #191 and #194 to #198 needed two merges, both in the first commit:
foundry.js, feat(vfs-bundle): Solidity bundles from soldeer.lock; post-merge fixes to the vfs-bundle export #194 gaveloadNestedConfigahostparameter. It kept that parameter beside this PR's section-16 change, until fix(bundle): Solidity loader — decide file ownership by real path; invalid configs are errors #178 replaced both.bundle.js, feat(vfs-bundle): Solidity bundles from soldeer.lock; post-merge fixes to the vfs-bundle export #194 and vfs-bundle: add buildGitHubBundle (a GitHub repo at a commit, via @preventive/upstream) #196 movedbuildBundle's JS path intobuildJs. The Rust line keeps this PR'scargoTargetandcargoManifests.Main's #172, #197, #199 and #178 needed no change to the Rust loaders. #178 makes a Solidity dependency config that isn't TOML an error itself, so this PR's own change to
foundry.js(section 16) and its Solidity test are dropped:foundry.js, its tests anddoc/file-formats.md's Solidity text are main's. The bundle-cmd comment now names the.gitmodulesreader beside the TOML and Cargo ones.Main's #201 gave
classifyEntriesafetchedparameter, so main is green again; it sits beside this PR'scargoTargetandcargoManifests. That is the one merge, in the first commit. The Rust loaders, their tests and the fixture archive are byte-identical to the pre-rebase head.26. The Rust loader's new fixture projects in one file
One commit on top of section 24's:
60ea71e. The two fixture projects this PR adds,includesandcargo-recorded, were 30 files (44 KB). They are now one brotli-compressed JSON file,tests/fixtures/rust-bundle.json.br(10 KB). It maps project name to project-relative path to the file's text, or to{ base64 }for a file that isn't UTF-8.rustFixture(name)intests/rust-fixtures.helper.jswrites a project out once per test process and gives its path. It writes to a temporary directory that is removed when the process exits.node tests/rust-fixtures.helper.js unpack <dir>andpack <dir>edit them. An unpack gives back the original files byte for byte, checked withdiff -r.tests/fixtures/rust-bundle/stay as they are.Verification
New tests for every item above, including a bitflags-shaped tree, a serde-shaped one, one for the module an import was followed from, a typenum/libc-shaped one, dead items of every starting token, a dual-mounted file, a reused sources map, an
includesfixture (include!, the two asset macros, a README via#[doc = …], a#[cfg(test)]-dead include, a bare macro invocation, a#[cfg]-gated module, a build script with a module of its own, a Cargo.lock), one test per item of section 6 (glob rule, several globs and the first followable import, inline-vs-file module and cfg variants, macros by final segment/invocation/at the root, macrouses, restricted visibility,extern crate … as, no via edge to the root), target-cfg evaluation and scanning with an injected cfg set, target-specific dependency tables, a target bundle, real-rustc cfg queries, a stand-in$RUSTCfor a target the local rustc doesn't know (which now also records where it ran from and that auto-install was off), the solana-shaped inline module,usewords in macro input and templates, cfg-gated blocks and macro-keyedmodvariants, throwaway Cargo projects for the multi-root, target-table, inherited-edition, both weak-feature, configured-vendor-dir and manifests/build-script cases, for section 11 a vendored crate reaching for.git/config,.envand a#[path]outside its package (refused, warned, fatal when ungated), a nested workspace's lock and config, a build script under--cargo-targetkeeping its Windows module and build-dependency, a file bothmodded andinclude_str!ed, import-cycle probes, glob-provided defined items, private mods and associated items behind a glob, the fn-vs-macro anduse logcases, the extern prelude,self::in a cfg variant,modin aninclude!d file,#[cfg]in a macro body, every include form and its warning, a macro-body include per invoker, for section 12 theuse log;crash, the libc/mio/tokio platform shapes, the anyhow macro, the defined-items exclusions,quote!by any name, the flattened variant map, the cycle shape above, deadlet/arm else branches andcfg_if!after a holding cfg, textual macro scope (definition order, nested#[macro_use]), the#[doc]manifest include, symlinked files/directories/includes and an outside[lib] pathin a vendored crate, and theinclude!d module's directory, for section 13 the same import in two platform files and in bothcfg_if!branches, a glob in a platform file beside its parent's,fn log/*const libc::c_charbeside the crates (andcrate::net::log()still reaching the fn), every rotation of the sources giving one answer, a file mounted under two cfgs and anany(…)branch against anothertarget_os, a module reached by two glob paths, every hop of a chain, nestedcfg_if!chains, macro shadowing in both orders, a template'smodandmacro_rules!at the invocation, the root-only one-segment macro,use tokio::sync::*; use mpsc::…(and a name the crate lacks still reported), a package-definedquote!in another file (and a template in another package still skipped), and the$RUSTCstub run with HOME at and inside the bundle root, and for section 15 a mio-shaped tree (a#[cfg]on acfg_os_poll!invocation,cfg_not_os_poll!against it, the unix wakers'Selector, and the same shape with and without a Linux target forsys::Waker), the A1 cycle through a named root re-export, a fn/static imported by name beside the crate of that name (in the tree and end to end, the crate loaded on the tree's request), a module reached again under another cfg with a module behind it, a template'smodinvoked from another file, a call before the file's own later definition (with the recorded call offsets), a package-definedquote!met a wave after the file that invokes it,loomagainstnot(loom)and anany(loom, aix)kept without a target, and HOME through links either way. The tests of the config filter and the URL scrubber went with them in section 17.Section 18 adds tests for:
include_bytes!file carried as base64;--cargo-target=host;--cargo-manifestscarries for vendored and nested packages, and a renamed vendored source directory;cargo:rustc-cfgand rustflags cfgs;use serde::*;[patch]dependencies;modplaced in the module invoking the macro (on disk and in the tree), a template invoked only by path, andextern crate selffrom child modules;Section 20 adds tests for:
[patch]that fits or doesn't (from the manifest and from the cargo config), and path dependencies outside the root or naming no package;docsrsagainst a glob, and against a nameduseundernot(docsrs);m!()beside afn m, a childmod m, and a crate-rootmod m, each with the macro brought in by a glob;macro_rules!inside before itsinclude_str!andhelper!calls;"a b"against"ab"and"a b"against"a b", inevalCfgand in the tree.On the section-19 head, each of the Cargo and resolver tests fails, and the loop test hangs.
Section 22 adds tests for:
createCargoContext, including cargo's refusal of one. The fixture is copied, MIT, as thecargo-recordedproject oftests/fixtures/rust-bundle.json.br;[patch]and the proc-macro built for the host;On the section-21 head, each of the resolver and replay tests fails.
Differential runs against the previous head over the corpus of current crates from crates.io (serde, syn, tokio, libc, mio, regex, hashbrown, winnow, toml_edit, zerocopy, bincode, ahash and others, ~2400 source files):
usein zerocopy and a spurious unresolved cratesein regex and regex-syntax.crate::BinOp::Sub→ syn's op.rs), relative paths through a module's own imports gain edges. False unresolved reports disappear; real missing crates such asthiserror_implstay reported.cfg_if!branch needs a feature that is off (getrandom/src/backends/wasm_js.rs,hashbrown/src/control/group/lsx.rs); false unresolved reports disappear.<name>!edges and 15 include edges appear; six files join the bundles (four READMEs, borsh's crate-level doc, winnow's example parser embedded byinclude_str!); 14 edges from two#[cfg]-gated files go, with log's falseserde_corereport.use quote::quote;no longer hides the cratequotefrom its own file;use crate::io; io::Errorlands on io/mod.rs; via edges for tokio'scrate::loom), 581 move to the file holding the import that binds the name (libc'scrate::c_int→ primitives.rs, tokio'scrate::loom::sync::Mutex→ loom/std/mod.rs), 88 go: via edges to crate roots, duplicatename!edges for path-qualified invocations, and wrong edges from tokio's windows.rs to unix/mod.rs. Two false unresolved reports (die; serde_with'sforeach_map/foreach_seq/foreach_set, allmacro_rules!+pub(crate) use) disappear. Timing: libc 12.3s → 27ms, tokio 96ms → 61ms; synthetic 800 modules all glob-re-exported from the root 33s → 0.6s, 1600 modules 2.1s; 800 modules with 5 globs 143ms, linear.--cargo-targetidentical; withhoston Linux, 4448 files → 2725 (libc 376 → 48, tokio 838 → 508, getrandom 410 → 70), and the platform-only unresolved reports (windows_sys,unsupported_target,target_arch_not_implemented,__errno) go.--cargo-manifestsidentical; with it, tokio gains its packages' manifests and the build scripts of libc, serde, proc-macro2 and quote, no warnings.#[path]); solana-program 1.18.18 as a vendored dependency bundles with 118 files (was 111, the fournon_bpf_modulesfiles and their submodules missing) and with--cargo-target=host113, where it failed before._andserde_corefrom quote templates, serde's 22 files from serde_derive's bundle, 480#private::…edges, and memchr's and zerocopy's metavariable paths ($memchrty::new,$trait::fmt). New: tokio'satomic_u64_as_mutex.rsand itsstatic_macrovariants in the module tree with their edges, memchr'scrate::arch::x86_64::avx2::memchrmodule paths, syn'smac::parse_delimiter-style paths that a swallowedusebody had hidden.crate::c_int-style paths from lib.rs to the platform file whoses! { … }defines it, first in root-glob order; typenum'scrate::IsEqualand friends from lib.rs to the defining file), 817 go (self edges: a libc or typenum file's path to an item it defines itself;name!edges to a same-namemacro_rules!in a sibling file — borsh, indexmap, memchr, proc-macro2 and serde each define their own copies, which the invoking file's textual scope never held), 13 appear (a:helper!()-style calls, extern-prelude aliases).crate::c_int-style paths from the first platform in written order to its own platform's file), 612 go (libc: a platform file's path to an item its own platform defines, a self edge now; elsewhere the old spelling of an invoked path,crate::__private::stringify→crate::__private::stringify!, and sevenforward_to_deserialize_any!edges in serde's docsrs-only inlined copy of serde_core, whose textual scope rustc's rules don't reach), 97 appear (the!spellings). libc's false__errnoreport goes.tri!edges. Of libc's 4649 changed edges, every sampled one moves from another platform's file to the asking file's own platform (aix/mod.rs'scrate::passwdfrom bsd/mod.rs to aix's own, a self edge now dropped; gnu/b32/arm'scrate::off_tfrom musl/mod.rs to gnu/b32/mod.rs; apple'scrate::sockaddrfrom linux_like/mod.rs to bsd/mod.rs) or to the layer above it (crate::uint64_tfrom lib.rs to primitives.rs); cross-platform edges 668 → 12.Selector, windows/selector.rs'sEventsand event flags to windows/event.rs,sys::Wakerfrom waker.rs off the windows waker), serde'sforward_to_deserialize_any!edge in core/de/value.rs appearing, getrandom'sbackends::fill_innerto the default backend, libc'scrate::kinfo_procfrom freebsdlike/mod.rs to dragonfly's; with a Linux target, mio'ssys::Wakeris the eventfd waker and every unix waker'sSelectorepoll's.extends.--cargo-manifestsbundle carries a manifest and lockfile with tokens in their URLs and a config with[registries]and arunnerbyte for byte, and refuses a lockfile that isn't UTF-8.589afcebefore the rebase onto fix(bundle): read TOML with @preventive/lockfile's parser #183):1.2.0), zerocopy −31 (rand), and num_enum_derive's syn.bytesmodule; toml_edit −136 and proc-macro-crate −137 through winnow.trace_leafandIoDriverMetricsreaching the definitions that are live;check_keyword_matches!, and lib.rs'smod de/mod serplaced by the template;chacha20,r_efi,extra_platforms,serde_coreand the platform-only names go with the dropped crates. Three appear:mio(tokio's unsatisfied requirement),syn(num_enum_derive's), andrustc_std_workspace_core(libc's optional dependency, reported in tokio's bundle, where libc's features are unknown).parseCargoManifestandparseCargoLockgive the same result before and after on all 120 Cargo.toml and Cargo.lock files of the corpus and the fixtures.resolveModDeclassertions for the removed filter are gone.cfg_if::cfg_if!,log::debug!,Token!,format_ident!,forward_to_deserialize_any!,zerocopy::transmute!.getrandom,rustversion,wasi,socket2,signal_hook_registry,zmij.serde_deriveandnum_enum_derivehave their features in the target context too.cfg(windows)cfg-ifof the cargo-recorded fixture is no crate of a Linux build;Full suite (
exodus-test, as on main since Use exodus-test runner and disable NODE_PATH extension #180, Node 24.21), after the rebase onto fix(bundle): read package.json as Node does; remapping errors never quote; green main after #178 #201: 2341 pass, 0 fail, 3 skipped (the cargo/rustc-dependent ones);oxlintclean. fix(bundle): read package.json as Node does; remapping errors never quote; green main after #178 #201 fixed thetests/vfs-bundle-github.test.jsfailure that main had after fix(bundle): Solidity loader — decide file ownership by real path; invalid configs are errors #178.🤖 Generated with Claude Code
https://claude.ai/code/session_015nbp2vC4jLPbGN8cmorxAL