crates/mimir-driver/src/lib.rs
//! Shared, rustc-free half of mimir-driver: the toolchain pin, the rustc argv
//! reading, the crate-version map (`MIMIR_CRATE_VERSIONS`), the per-artifact
//! sidecar index, and the per-crate stats record. The `mimir-driver` binary
//! (src/main.rs, `rustc_private`) and `cargo-mimir-dump` both use it.
//!
//! ## Extern crate versions (plan §10 "ADDED")
//!
//! rustc metadata carries a crate's NAME and its StableCrateId, never its
//! package version. Two sources, in order:
//!
//! 1. **Sidecar index (exact).** Under `RUSTC_WRAPPER` every rustc invocation of
//! the build goes through the driver, so when cargo compiles package
//! `serde 1.0.228` the driver writes `$MIMIR_INDEX_DIR/serde-<hash>.json`
//! with `CARGO_PKG_NAME` / `CARGO_PKG_VERSION`, keyed by `--crate-name` +
//! `-C extra-filename` — i.e. by the artifact's file stem
//! (`libserde-<hash>.rmeta`). A dependent crate later asks
//! `tcx.used_crate_source(cnum)` for the exact file rustc loaded and looks
//! that stem up. Two versions of one name have two hashes → two sidecars, no
//! ambiguity, no guessing from argv. The index lives in the SAME target dir
//! as the artifacts, so a warm re-check (cargo skips unchanged deps) still
//! finds them.
//! 2. **The cargo-metadata map (fallback).** `MIMIR_CRATE_VERSIONS` is a JSON
//! file built from ONE `cargo metadata` (cargo-mimir-dump writes it): every
//! package with its lib crate name, version and workspace membership. Used
//! when no sidecar exists (e.g. `RUSTC_WORKSPACE_WRAPPER` mode, where deps are
//! compiled by plain rustc) and the crate name maps to exactly ONE package.
//!
//! When the name is ambiguous (two versions of one crate), a DIRECT dep is
//! still resolved exactly: rustc's argv has `--extern a_old=…/liba-HASH.rmeta`,
//! the loaded file of the crate is `tcx.used_crate_source(cnum)`, and the map
//! carries cargo's resolve graph (`extern_deps`: package → extern name →
//! package@version), so `a_old` → `a 0.0.9`.
//!
//! Otherwise version = `"?"` and the crate is counted in `unresolved_versions`.
//!
//! ## Cargo features (GATED_BY / ENABLES)
//!
//! The same ONE `cargo metadata` carries every package's `[features]` table
//! (implicit `dep:` features of optional dependencies included). The version
//! map keeps it per package ([`FeatureDecl`]), each entry already resolved to
//! the package@version it points at ([`FeatureEnable`]), so the driver writes
//! feature nodes and ENABLES edges without reading a manifest itself. Which
//! features a crate was COMPILED with is not in the map: that is rustc's own
//! `--cfg feature="…"` set, read per compile.
//!
//! [`cfg_scan`] is the second, token-level source for items the build's
//! configuration compiled out (rustc never sees those).
pub mod cfg_scan;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
/// The ONE nightly this driver is built against (== rust-toolchain.toml).
pub const MIMIR_NIGHTLY: &str = "nightly-2026-10-05";
/// `rustc -vV` commit-hash of [`MIMIR_NIGHTLY`].
pub const MIMIR_NIGHTLY_COMMIT: &str = "28221559263a3976766cf305940e80e30cf9ba8a";
/// Env: output directory for the Arrow files. Unset → the driver is a plain rustc.
pub const ENV_OUT: &str = "MIMIR_OUT";
/// Env: path of the JSON crate-version map (from one `cargo metadata`).
pub const ENV_VERSIONS: &str = "MIMIR_CRATE_VERSIONS";
/// Env: sidecar index directory (inside the target dir).
pub const ENV_INDEX: &str = "MIMIR_INDEX_DIR";
/// Env: git SHA written as `valid_from_sha`.
pub const ENV_SHA: &str = "MIMIR_SHA";
/// Env: repo name written in `repo` for local nodes.
pub const ENV_REPO: &str = "MIMIR_REPO";
/// Env: `all` (default) dumps every compiled crate (deps too — the universe
/// ring, once per name@version per target dir); `workspace` dumps only
/// `CARGO_PRIMARY_PACKAGE` crates; `off` dumps nothing — the driver is a
/// plain rustc of its nightly (a dependent of a change compiled only to
/// see that it still builds: `BuildWorkspace { only: Affected }`), the
/// sidecars still written, so the units are the ones a dump would compile.
pub const ENV_SCOPE: &str = "MIMIR_DUMP";
/// Where this compile's dump goes: `MIMIR_OUT`, unless `MIMIR_DUMP=off`.
pub fn dump_out(out: Option<std::ffi::OsString>, scope: Option<&str>) -> Option<PathBuf> {
out.filter(|_| scope != Some("off")).map(PathBuf::from)
}
/// Env: which crates get the token scan ([`cfg_scan`]: compiled-out items
/// and the gates INSIDE items): `local` (default) = the workspace's own
/// crates; `all` = dependencies too (the universe ring, once per
/// name@version); `off` = none. The own gates of the items rustc KEPT,
/// feature nodes and ENABLES are written for every crate regardless.
pub const ENV_CFG_SCAN: &str = "MIMIR_CFG_SCAN";
/// The cfg this driver sets for the crates it compiles from our own sources
/// (`--cfg mimir`), and the tool it registers: `#[cfg_attr(mimir,
/// mimir::table(read, name))]` — the one annotation the table pass reads.
pub const CFG: &str = "mimir";
/// Env: `1` = the toolchain-crate dump (`cargo mimir-dump --std`, i.e. cargo
/// `-Zbuild-std`): every crate the sysroot ships (std, core, alloc and std's
/// private deps) is written under the lang identity (package = crate name,
/// version = rust-analyzer's `https://github.com/rust-lang/rust/library/<crate>`,
/// origin `lang`) — the SAME ids a normal dump gives them as dependencies.
pub const ENV_STD: &str = "MIMIR_STD_DUMP";
/// What the rustc argv says about this invocation.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Invocation {
pub crate_name: Option<String>,
pub crate_types: Vec<String>,
/// `-C extra-filename=` value, e.g. `-1a2b3c4d5e6f7a8b`.
pub extra_filename: Option<String>,
/// `-C metadata=` value: cargo passes it for EVERY unit, also the ones
/// without an extra-filename (an uplifted bin of `cargo build`).
pub metadata: Option<String>,
pub is_test: bool,
pub has_print: bool,
pub has_input: bool,
/// The crate's root file as cargo passed it (relative to rustc's cwd, or
/// absolute): it tells which TARGET of the package this unit is.
pub input: Option<PathBuf>,
/// `--extern NAME=PATH` pairs (the name the crate is known by here).
pub externs: Vec<(String, PathBuf)>,
/// `--cfg` values (features are `feature="x"`).
pub cfgs: Vec<String>,
/// `-C` options that change what the crate IS (`opt-level`,
/// `debug-assertions`, `overflow-checks`, `target-cpu`, `target-feature`, `panic`).
pub codegen: Vec<String>,
/// `--target` (absent: the host).
pub target: Option<String>,
}
impl Invocation {
/// Read rustc's argv (WITHOUT the program name).
pub fn parse(args: &[String]) -> Invocation {
let mut inv = Invocation::default();
let mut i = 0;
while i < args.len() {
let a = args[i].as_str();
let next = args.get(i + 1).map(String::as_str);
let mut take = |flag: &str| -> Option<String> {
if a == flag {
i += 1;
next.map(str::to_string)
} else {
a.strip_prefix(flag).and_then(|r| r.strip_prefix('=')).map(str::to_string)
}
};
if let Some(v) = take("--crate-name") {
inv.crate_name = Some(v);
} else if let Some(v) = take("--crate-type") {
inv.crate_types.extend(v.split(',').map(str::to_string));
} else if let Some(v) = take("--extern") {
if let Some((n, p)) = v.split_once('=') {
// `noprelude:alloc=…` / `priv:x=…` → the bare name
let n = n.rsplit(':').next().unwrap_or(n);
inv.externs.push((n.to_string(), PathBuf::from(p)));
}
} else if let Some(v) = take("--print") {
let _ = v;
inv.has_print = true;
} else if a == "-C" || a == "--codegen" {
if let Some(v) = next {
if let Some(x) = v.strip_prefix("extra-filename=") {
inv.extra_filename = Some(x.to_string());
} else if let Some(x) = v.strip_prefix("metadata=") {
inv.metadata = Some(x.to_string());
} else if SEMANTIC_CODEGEN.iter().any(|k| v.starts_with(k)) {
inv.codegen.push(v.to_string());
}
}
i += 1;
} else if let Some(x) = a.strip_prefix("-Cextra-filename=") {
inv.extra_filename = Some(x.to_string());
} else if let Some(x) = a.strip_prefix("-Cmetadata=") {
inv.metadata = Some(x.to_string());
} else if let Some(x) = a.strip_prefix("-C").filter(|x| SEMANTIC_CODEGEN.iter().any(|k| x.starts_with(k))) {
inv.codegen.push(x.to_string());
} else if let Some(v) = take("--cfg") {
inv.cfgs.push(v);
} else if let Some(v) = take("--target") {
inv.target = Some(v);
} else if a == "--test" {
inv.is_test = true;
} else if a == "-" || (!a.starts_with('-') && (a.ends_with(".rs") || Path::new(a).is_file())) {
inv.has_input = true;
if a != "-" {
inv.input = Some(PathBuf::from(a));
}
} else if a == "-vV" || a == "-V" || a == "--version" {
inv.has_print = true;
} else if matches!(
a,
"-o" | "--out-dir" | "--edition" | "--emit" | "-L"
| "--check-cfg" | "--cap-lints" | "--error-format" | "--json"
| "--diagnostic-width" | "-A" | "-W" | "-D" | "-F" | "--sysroot" | "-Z"
| "--remap-path-prefix" | "--env-set" | "--explain" | "-l"
) {
// flags that take a separate value: skip the value
i += 1;
}
i += 1;
}
inv
}
pub fn is_proc_macro(&self) -> bool {
self.crate_types.iter().any(|t| t == "proc-macro")
}
pub fn is_build_script(&self) -> bool {
self.crate_name.as_deref().is_some_and(|n| n.starts_with("build_script_"))
}
pub fn is_lib(&self) -> bool {
self.crate_types
.iter()
.any(|t| matches!(t.as_str(), "lib" | "rlib" | "dylib" | "cdylib" | "staticlib"))
}
/// The unit's SEMANTIC key: what makes its dump what it is — package,
/// version, source (its manifest dir), crate, target kinds, test harness,
/// every `--cfg` (features included), the `-C` options that change the
/// code, the target. NOT the hashes of its dependencies: two compiles of
/// one crate with one feature set read the same.
pub fn semkey(&self, package: &str, version: &str, manifest_dir: &str) -> String {
let mut cfgs = self.cfgs.clone();
cfgs.sort();
let mut cg = self.codegen.clone();
cg.sort();
let mut types = self.crate_types.clone();
types.sort();
format!(
"{package}@{version}|{manifest_dir}|{}|{}|{}|{}|{}|{}",
self.crate_name.as_deref().unwrap_or(""),
types.join(","),
self.is_test,
cfgs.join(","),
cg.join(","),
self.target.as_deref().unwrap_or("host")
)
}
/// Sidecar / artifact key: `<crate_name><extra_filename>` (= artifact stem without `lib`).
pub fn artifact_key(&self) -> Option<String> {
Some(format!("{}{}", self.crate_name.as_deref()?, self.extra_filename.as_deref()?))
}
/// The unit part of the output file names — unique per rustc invocation
/// of one build:
/// * cargo's `-C extra-filename` hash (what cargo's JSON names the unit by);
/// * else `m<-C metadata>`: cargo omits the extra-filename for an uplifted
/// bin of `cargo build` (release bins), but every unit still has its own
/// metadata hash — two bins with one name + version stay apart;
/// * else `nohash.<pid>`: a rustc run that is not a cargo unit (a build
/// script's feature probe). Consumers skip `nohash*` stems.
pub fn short_hash(&self) -> String {
if let Some(e) = self.extra_filename.as_deref().map(|e| e.trim_start_matches('-')).filter(|s| !s.is_empty()) {
return e.to_string();
}
if let Some(m) = self.metadata.as_deref().filter(|s| !s.is_empty()) {
return format!("m{m}");
}
format!("{NOHASH}.{}", std::process::id())
}
}
/// `-C` options that change what a crate's code is (its cfgs, its bodies).
pub const SEMANTIC_CODEGEN: [&str; 6] = ["opt-level", "debug-assertions", "overflow-checks", "target-cpu", "target-feature", "panic"];
/// A short, stable hex name of a semantic key (FNV-1a 64; the same in every
/// process and every build of this driver).
pub fn semhash(semkey: &str) -> String {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in semkey.as_bytes() {
h ^= u64::from(*b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{h:016x}")
}
/// **The semantic key a dump is known by** across builds: the unit's own
/// ([`Invocation::semkey`]) plus what decides its ROWS that the unit itself
/// does not say —
/// * the package@version every `--extern` resolves to (its sidecar in
/// `index_dir`; an artifact with no sidecar is named by its file stem, so it
/// never equals another): the edges into a dependency carry its version;
/// * its features' optional dependencies as the reading repo's lockfile
/// resolves them (the feature file of the package at `manifest_dir`, next
/// to `versions_file`): `feature=x ENABLES dep@version`.
/// Two units with one key write the same rows (the repo's commit sha aside);
/// a twin whose dependency is at another version keeps its own (measured
/// 2026-10-16 on the 21 nordisk repos: 31% of the units equal on the bare
/// semantic key wrote other rows, most of them for this).
pub fn unit_semkey(inv: &Invocation, package: &str, version: &str, manifest_dir: &str, index_dir: Option<&Path>, versions_file: Option<&Path>) -> String {
let mut key = inv.semkey(package, version, manifest_dir);
let mut deps: Vec<String> = inv
.externs
.iter()
.map(|(name, path)| {
let stem = artifact_key_of(path).unwrap_or_default();
match index_dir.and_then(|d| read_sidecar(d, &stem)) {
Some(s) => format!("{name}={}@{}", s.package, s.version),
None => format!("{name}=#{stem}"),
}
})
.collect();
deps.sort();
deps.dedup();
key.push_str("|deps:");
key.push_str(&deps.join(","));
if let Some(vf) = versions_file {
let mut opt: Vec<String> = VersionMap::load_features_at(vf, package, version, Path::new(manifest_dir))
.iter()
.flat_map(|f| f.enables.iter().filter_map(move |e| Some(format!("{}:{}@{}", f.name, e.dep_package.as_deref()?, e.dep_version.as_deref().unwrap_or("?")))))
.collect();
opt.sort();
opt.dedup();
key.push_str("|opt:");
key.push_str(&opt.join(","));
}
key
}
/// A source file the crate's build script GENERATED (under cargo's
/// `OUT_DIR`, a directory named by the unit's hash) as `OUT_DIR/<rest>`: the
/// same generated file reads the same in every build of the crate, not under
/// one target dir's hash. `None`: not under `out_dir`.
pub fn out_dir_relative(path: &Path, out_dir: Option<&Path>) -> Option<String> {
let rest = path.strip_prefix(out_dir?).ok()?;
Some(format!("OUT_DIR/{}", rest.to_string_lossy()))
}
/// A printed type with every `a::b::C` path cut to its last segment
/// (`std::collections::BTreeMap<u32, crate::Point>` → `BTreeMap<u32, Point>`):
/// the human `ty` column, made from a type printed with FULL paths. The
/// compiler's own trimming is never asked: its `trimmed_def_paths` query makes
/// rustc ICE at the end of a session that emitted no diagnostic (dwarf
/// 2026-10-17: toml_edit and every crate like it).
pub fn shorten_paths(s: &str) -> String {
let b = s.as_bytes();
let is_start = |c: u8| c.is_ascii_alphabetic() || c == b'_' || c >= 0x80;
let is_cont = |c: u8| c.is_ascii_alphanumeric() || c == b'_' || c >= 0x80;
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < b.len() {
if is_start(b[i]) && (i == 0 || !is_cont(b[i - 1])) {
// an identifier, and the `::ident` segments right after it: keep the last
let mut start = i;
let mut end = i;
while end < b.len() && is_cont(b[end]) {
end += 1;
}
while end + 2 < b.len() && &b[end..end + 2] == b"::" && is_start(b[end + 2]) {
start = end + 2;
end = start;
while end < b.len() && is_cont(b[end]) {
end += 1;
}
}
out.push_str(&s[start..end]);
i = end;
} else {
let ch = s[i..].chars().next().unwrap_or(' ');
out.push(ch);
i += ch.len_utf8();
}
}
out
}
/// [`unit_semkey`] of the unit this process compiles, hashed ([`semhash`]):
/// cargo's environment (`CARGO_PKG_NAME`, `CARGO_PKG_VERSION`,
/// `CARGO_MANIFEST_DIR`) and the driver's (`MIMIR_INDEX_DIR`,
/// `MIMIR_CRATE_VERSIONS`). The ONE spelling of the hash a dump is stored
/// under and a ring check looks for.
pub fn unit_semhash_of_env(inv: &Invocation) -> String {
let var = |k: &str| std::env::var(k).unwrap_or_default();
let index = std::env::var_os(ENV_INDEX).map(PathBuf::from);
let versions = std::env::var_os(ENV_VERSIONS).map(PathBuf::from);
semhash(&unit_semkey(inv, &var("CARGO_PKG_NAME"), &var("CARGO_PKG_VERSION"), &var("CARGO_MANIFEST_DIR"), index.as_deref(), versions.as_deref()))
}
/// A file of semantic hashes, one per line: dependency units whose dump is
/// already in the log's ring. Such a unit is compiled (its dependents need its
/// metadata) with its table summaries, but not walked or dumped again; the
/// driver leaves `<stem>.ring` in the out dir instead (holding the hash).
pub const ENV_RING_HAVE: &str = "MIMIR_RING_HAVE";
/// Is `hash` in the ring-have file at `path`?
pub fn ring_has(path: &Path, hash: &str) -> bool {
std::fs::read_to_string(path).is_ok_and(|s| s.lines().any(|l| l.trim() == hash))
}
/// Stem prefix of a dump that is no cargo unit (see [`Invocation::short_hash`]).
pub const NOHASH: &str = "nohash";
/// The crate names the toolchain at `sysroot` ships as rlibs
/// (`lib/rustlib/<target>/lib/lib<crate>-<hash>.rlib`): std, core, alloc,
/// proc_macro, test and std's private deps (hashbrown, gimli …).
pub fn sysroot_crates(sysroot: &Path) -> std::collections::BTreeSet<String> {
let mut out = std::collections::BTreeSet::new();
let Ok(targets) = std::fs::read_dir(sysroot.join("lib/rustlib")) else { return out };
for t in targets.flatten() {
let Ok(libs) = std::fs::read_dir(t.path().join("lib")) else { continue };
for f in libs.flatten() {
let n = f.file_name().to_string_lossy().into_owned();
if let Some(stem) = n.strip_prefix("lib").and_then(|r| r.strip_suffix(".rlib")) {
if let Some((name, _hash)) = stem.rsplit_once('-') {
out.insert(name.to_string());
}
}
}
}
out
}
/// The artifact key of a file rustc loaded (`.../libserde-1a2b.rmeta` → `serde-1a2b`).
pub fn artifact_key_of(path: &Path) -> Option<String> {
let stem = path.file_stem()?.to_str()?;
Some(stem.strip_prefix("lib").unwrap_or(stem).to_string())
}
/// How one entry of a `[features]` value switches something on.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "kebab-case")]
pub enum EnableForm {
/// `"other"` — another feature of the same package.
Feature,
/// `"dep:x"` — the optional dependency `x`.
Dep,
/// `"x/f"` — dependency `x` AND its feature `f`.
DepFeature,
/// `"x?/f"` — feature `f` of `x`, only if `x` is enabled by another route.
WeakDepFeature,
}
/// One entry of a `[features]` value, resolved against the lockfile.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FeatureEnable {
/// The manifest string: `std`, `dep:serde`, `serde/derive`, `serde?/derive`.
pub raw: String,
pub form: EnableForm,
/// The dependency's PACKAGE name (dep forms; the manifest key may be a rename).
pub dep_package: Option<String>,
/// Its version in the lockfile. `None` = not resolvable: no dependency of
/// that key, or an optional dependency the resolve did not activate whose
/// name the lockfile holds in more than one version.
pub dep_version: Option<String>,
/// The feature switched on: this package's ([`EnableForm::Feature`]) or
/// the dependency's (the two `x/f` forms).
pub feature: Option<String>,
}
/// One declared feature of a package.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FeatureDecl {
pub name: String,
/// 1-based line of its key in the manifest's `[features]` table; `None`
/// for an implicit feature (an optional dependency without one).
pub line: Option<u32>,
pub enables: Vec<FeatureEnable>,
}
/// One cargo target of a workspace member.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TargetDecl {
/// Cargo's target kind — `bin`, `example`, `test`, `bench`,
/// `custom-build` — and `lib` for the library in any crate type.
pub kind: String,
/// The target's crate name (`-` → `_`).
pub crate_name: String,
/// Its root file (`src_path`, absolute).
pub src: String,
}
/// One package in the version map.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PackageVersion {
pub name: String,
pub version: String,
/// lib target crate names (`-` → `_`).
pub crate_names: Vec<String>,
/// workspace member?
pub local: bool,
/// Every target of a workspace member (empty for a dependency: only its
/// lib is ever compiled). A unit's target is the one whose root file is
/// rustc's input ([`VersionMap::target_kind`]).
#[serde(default)]
pub targets: Vec<TargetDecl>,
/// The package's `Cargo.toml`.
#[serde(default)]
pub manifest_path: String,
/// Its `[features]`, by name. NOT in the version-map file: every rustc
/// invocation of a build loads that file, and all features of all
/// packages are megabytes — each package's list is its own small file
/// ([`VersionMap::write`] / [`VersionMap::load_features`]).
#[serde(skip)]
pub features: Vec<FeatureDecl>,
}
/// Directory (next to the version-map file) of the per-package feature files.
pub const FEATURES_DIR: &str = "crate-features";
fn features_file(versions_file: &Path, name: &str, version: &str) -> PathBuf {
versions_file.with_file_name(FEATURES_DIR).join(format!("{name}@{}.json", version.replace(['/', '\\', ':'], "_")))
}
/// The feature file of the `name@version` whose manifest is in `manifest_dir`.
fn features_file_at(versions_file: &Path, name: &str, version: &str, manifest_dir: &Path) -> PathBuf {
let dir = semhash(&manifest_dir.to_string_lossy());
versions_file.with_file_name(FEATURES_DIR).join(format!("{name}@{}@{dir}.json", version.replace(['/', '\\', ':'], "_")))
}
/// 1-based line of every key of the `[features]` table in a manifest's text.
/// A line scan, not a TOML parse: the table header alone on its line, keys up
/// to the next `[header]`; multi-line arrays only contribute their first line.
pub fn feature_lines(manifest: &str) -> BTreeMap<String, u32> {
let mut out = BTreeMap::new();
let mut inside = false;
for (i, line) in manifest.lines().enumerate() {
let t = line.trim();
if t.starts_with('[') {
inside = t.split('#').next().map(str::trim) == Some("[features]");
continue;
}
if !inside || t.starts_with('#') {
continue;
}
let Some((key, _)) = t.split_once('=') else { continue };
let key = key.trim().trim_matches(|c| c == '"' || c == '\'');
let simple = !key.is_empty() && key.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '+' | '.'));
if simple {
out.entry(key.to_string()).or_insert(i as u32 + 1);
}
}
out
}
/// `Cargo.lock` as `(name, version)` → its dependencies `(name, version)`.
/// The lockfile is the one place that names EVERY package a feature could
/// pull in — `cargo metadata` leaves out an optional dependency nothing
/// enables. A line scan of cargo's own generated format (`[[package]]`,
/// `name`, `version`, `dependencies = [ "name", "name version", … ]`); a
/// dependency written without a version has exactly one in the file.
pub fn lock_dependencies(lock: &str) -> BTreeMap<(String, String), Vec<(String, Option<String>)>> {
let unquote = |v: &str| v.trim().trim_end_matches(',').trim().trim_matches('"').to_string();
let mut out: BTreeMap<(String, String), Vec<(String, Option<String>)>> = BTreeMap::new();
let mut versions: BTreeMap<String, Vec<String>> = BTreeMap::new();
let (mut name, mut version, mut deps, mut in_deps) = (String::new(), String::new(), Vec::new(), false);
let mut flush = |name: &mut String, version: &mut String, deps: &mut Vec<String>| {
if !name.is_empty() {
versions.entry(name.clone()).or_default().push(version.clone());
out.insert((std::mem::take(name), std::mem::take(version)), std::mem::take(deps).into_iter().map(|d| (d, None)).collect());
}
deps.clear();
};
for line in lock.lines() {
let t = line.trim();
if t == "[[package]]" {
flush(&mut name, &mut version, &mut deps);
in_deps = false;
} else if in_deps {
if t.starts_with(']') {
in_deps = false;
} else if t.starts_with('"') {
deps.push(unquote(t));
}
} else if let Some(v) = t.strip_prefix("name = ") {
name = unquote(v);
} else if let Some(v) = t.strip_prefix("version = ") {
version = unquote(v);
} else if let Some(v) = t.strip_prefix("dependencies = [") {
match v.strip_suffix(']') {
// one line: `dependencies = ["a", "b 1.0"]`
Some(inline) => deps.extend(inline.split(',').map(unquote).filter(|d| !d.is_empty())),
None => in_deps = true,
}
}
}
flush(&mut name, &mut version, &mut deps);
for list in out.values_mut() {
for (dep, ver) in list.iter_mut() {
// `name version (source)` | `name version` | `name`
let mut parts = dep.split(' ');
let n = parts.next().unwrap_or_default().to_string();
*ver = match parts.next() {
Some(v) => Some(v.to_string()),
None => match versions.get(&n).map(Vec::as_slice) {
Some([one]) => Some(one.clone()),
_ => None,
},
};
*dep = n;
}
}
out
}
/// Split one `[features]` value entry into its form, dependency key and feature.
fn split_enable(raw: &str) -> (EnableForm, Option<&str>, Option<&str>) {
if let Some(dep) = raw.strip_prefix("dep:") {
return (EnableForm::Dep, Some(dep), None);
}
match raw.split_once('/') {
Some((dep, f)) => match dep.strip_suffix('?') {
Some(dep) => (EnableForm::WeakDepFeature, Some(dep), Some(f)),
None => (EnableForm::DepFeature, Some(dep), Some(f)),
},
None => (EnableForm::Feature, None, Some(raw)),
}
}
/// `MIMIR_CRATE_VERSIONS` file contents.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct VersionMap {
pub workspace_root: String,
pub repo: String,
pub packages: Vec<PackageVersion>,
/// `"<pkg> <version>"` → extern crate name → (pkg, version), from cargo's
/// resolve graph (direct deps only).
#[serde(default)]
pub extern_deps: BTreeMap<String, BTreeMap<String, (String, String)>>,
}
impl VersionMap {
pub fn load(path: &Path) -> Option<VersionMap> {
serde_json::from_slice(&std::fs::read(path).ok()?).ok()
}
/// Write the map to `path` and each package's features to its own file
/// under [`FEATURES_DIR`] next to it.
pub fn write(&self, path: &Path) -> std::io::Result<()> {
std::fs::write(path, serde_json::to_vec_pretty(self)?)?;
let dir = path.with_file_name(FEATURES_DIR);
std::fs::create_dir_all(&dir)?;
for p in self.packages.iter().filter(|p| !p.features.is_empty()) {
let body = serde_json::to_vec(&p.features)?;
std::fs::write(features_file(path, &p.name, &p.version), &body)?;
// … and by its manifest: two packages of one name and version
// (a path checkout and its published copy) keep their own
if let Some(dir) = Path::new(&p.manifest_path).parent().filter(|d| !d.as_os_str().is_empty()) {
std::fs::write(features_file_at(path, &p.name, &p.version, dir), &body)?;
}
}
Ok(())
}
/// The declared features of `name@version`, written by [`Self::write`]
/// next to the map at `path`. Empty when the package declares none.
pub fn load_features(path: &Path, name: &str, version: &str) -> Vec<FeatureDecl> {
std::fs::read(features_file(path, name, version)).ok().and_then(|b| serde_json::from_slice(&b).ok()).unwrap_or_default()
}
/// The declared features of the `name@version` whose manifest is in
/// `manifest_dir`: one build can hold two packages of one name and version
/// (a path checkout and its published copy), each with its own manifest.
pub fn load_features_at(path: &Path, name: &str, version: &str, manifest_dir: &Path) -> Vec<FeatureDecl> {
match std::fs::read(features_file_at(path, name, version, manifest_dir)) {
Ok(b) => serde_json::from_slice(&b).unwrap_or_default(),
Err(_) => Self::load_features(path, name, version),
}
}
/// The cargo target kind of the unit of `package@version` whose root
/// file is `input` (rustc's input, relative to the cwd or absolute):
/// [`TargetDecl::kind`]. `None` = not a target this map knows (a
/// dependency, a file cargo did not declare).
pub fn target_kind(&self, package: &str, version: &str, input: &Path) -> Option<&str> {
let canon = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
let abs = if input.is_absolute() { input.to_path_buf() } else { std::env::current_dir().ok()?.join(input) };
let input = canon(&abs);
self.packages
.iter()
.filter(|p| p.name == package && p.version == version)
.flat_map(|p| &p.targets)
.find(|t| !t.src.is_empty() && canon(Path::new(&t.src)) == input)
.map(|t| t.kind.as_str())
}
/// Is the unit cargo is compiling — `package@version`, with its manifest
/// in `manifest_dir` (cargo's `CARGO_MANIFEST_DIR`) — a MEMBER of this
/// workspace?
///
/// By its manifest, not by name and version alone: one build can hold a
/// member AND the published copy of that same member (`edda`:
/// `nornir-cli-outcome 0.1.4` is `crates/nornir-cli-outcome` and also comes
/// from the registry through a dependency that names the published
/// crate). The copy is a library; calling it local made the dump step see
/// a "local" dump of a unit cargo calls a dependency. Without a manifest
/// dir (a run outside cargo) name and version are all there is.
pub fn is_member(&self, package: &str, version: &str, manifest_dir: Option<&Path>) -> bool {
let canon = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
self.packages.iter().filter(|p| p.local && p.name == package && p.version == version).any(|p| match (manifest_dir, Path::new(&p.manifest_path).parent()) {
(Some(dir), Some(own)) if !p.manifest_path.is_empty() => canon(dir) == canon(own),
_ => true,
})
}
/// Packages whose lib crate is called `crate_name`.
pub fn by_crate(&self, crate_name: &str) -> Vec<&PackageVersion> {
self.packages
.iter()
.filter(|p| p.crate_names.iter().any(|c| c == crate_name))
.collect()
}
/// Build from `cargo metadata --format-version 1` JSON, reading the
/// workspace's `Cargo.lock` next to it (see [`Self::with_lock`]).
pub fn from_cargo_metadata(meta: &serde_json::Value, repo: &str) -> VersionMap {
let lock = std::fs::read_to_string(Path::new(meta["workspace_root"].as_str().unwrap_or(".")).join("Cargo.lock")).ok();
Self::with_lock(meta, repo, lock.as_deref())
}
/// [`Self::from_cargo_metadata`] with the lockfile's text given: it
/// resolves the dependencies of `[features]` entries that the metadata's
/// resolve does not hold (an optional dependency nothing enables).
pub fn with_lock(meta: &serde_json::Value, repo: &str, lock: Option<&str>) -> VersionMap {
let members: Vec<&str> = meta["workspace_members"]
.as_array()
.map(|a| a.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
let mut packages = Vec::new();
for p in meta["packages"].as_array().into_iter().flatten() {
let id = p["id"].as_str().unwrap_or_default();
let crate_names = p["targets"]
.as_array()
.into_iter()
.flatten()
.filter(|t| {
t["kind"].as_array().into_iter().flatten().any(|k| {
matches!(
k.as_str(),
Some("lib" | "rlib" | "dylib" | "cdylib" | "staticlib" | "proc-macro")
)
})
})
.filter_map(|t| t["name"].as_str().map(|n| n.replace('-', "_")))
.collect();
let local = members.contains(&id);
let targets = p["targets"]
.as_array()
.into_iter()
.flatten()
.filter(|_| local)
.filter_map(|t| {
let kinds: Vec<&str> = t["kind"].as_array().into_iter().flatten().filter_map(|k| k.as_str()).collect();
let kind = if kinds.iter().any(|k| mimir_schema::moniker::LIB_TARGET_KINDS.contains(k)) { "lib" } else { kinds.first().copied()? };
Some(TargetDecl {
kind: kind.to_string(),
crate_name: t["name"].as_str()?.replace('-', "_"),
src: t["src_path"].as_str().unwrap_or_default().to_string(),
})
})
.collect();
packages.push(PackageVersion {
name: p["name"].as_str().unwrap_or_default().to_string(),
version: p["version"].as_str().unwrap_or_default().to_string(),
crate_names,
local,
targets,
manifest_path: p["manifest_path"].as_str().unwrap_or_default().to_string(),
features: Vec::new(),
});
}
// package id → (name, version)
let mut by_id: BTreeMap<&str, (String, String)> = BTreeMap::new();
for p in meta["packages"].as_array().into_iter().flatten() {
by_id.insert(
p["id"].as_str().unwrap_or_default(),
(
p["name"].as_str().unwrap_or_default().to_string(),
p["version"].as_str().unwrap_or_default().to_string(),
),
);
}
// ---- features: every `[features]` entry resolved to package@version
let locked = lock.map(lock_dependencies).unwrap_or_default();
// package id → the dependencies the resolve ACTIVATED: (extern name, package, version)
let mut activated: BTreeMap<&str, Vec<(&str, &str, &str)>> = BTreeMap::new();
for n in meta["resolve"]["nodes"].as_array().into_iter().flatten() {
let Some(id) = n["id"].as_str() else { continue };
for d in n["deps"].as_array().into_iter().flatten() {
if let (Some(name), Some((dn, dv))) = (d["name"].as_str(), d["pkg"].as_str().and_then(|i| by_id.get(i))) {
activated.entry(id).or_default().push((name, dn.as_str(), dv.as_str()));
}
}
}
for (pkg, p) in packages.iter_mut().zip(meta["packages"].as_array().into_iter().flatten()) {
let id = p["id"].as_str().unwrap_or_default();
let me = (pkg.name.clone(), pkg.version.clone());
// manifest dependency key (the rename, else the package name) → package name
let mut dep_keys: BTreeMap<&str, &str> = BTreeMap::new();
for d in p["dependencies"].as_array().into_iter().flatten() {
let Some(name) = d["name"].as_str() else { continue };
dep_keys.entry(d["rename"].as_str().unwrap_or(name)).or_insert(name);
}
let resolve_dep = |key: &str| -> (Option<String>, Option<String>) {
let Some(dep_pkg) = dep_keys.get(key).copied() else { return (None, None) };
let ext = key.replace('-', "_");
let act = activated.get(id).map(Vec::as_slice).unwrap_or_default();
let of_pkg: Vec<&(&str, &str, &str)> = act.iter().filter(|(_, n, _)| *n == dep_pkg).collect();
let version = match of_pkg.as_slice() {
[one] => Some(one.2.to_string()),
// the same package twice (two versions under two keys): the extern name decides
many if !many.is_empty() => many.iter().find(|(e, ..)| *e == ext).map(|x| x.2.to_string()),
// not activated (an optional dependency that is off): the
// lockfile still pins it — exact when this package depends
// on ONE version of that name
_ => {
let mine: Vec<&(String, Option<String>)> =
locked.get(&me).into_iter().flatten().filter(|(n, _)| n == dep_pkg).collect();
match mine.as_slice() {
[(_, v)] => v.clone(),
_ => None,
}
}
};
(Some(dep_pkg.to_string()), version)
};
let lines = std::fs::read_to_string(&pkg.manifest_path).map(|t| feature_lines(&t)).unwrap_or_default();
for (name, vals) in p["features"].as_object().into_iter().flatten() {
let mut enables = Vec::new();
for raw in vals.as_array().into_iter().flatten().filter_map(|v| v.as_str()) {
let (form, dep, feature) = split_enable(raw);
let (dep_package, dep_version) = dep.map(&resolve_dep).unwrap_or((None, None));
enables.push(FeatureEnable { raw: raw.to_string(), form, dep_package, dep_version, feature: feature.map(str::to_string) });
}
pkg.features.push(FeatureDecl { name: name.clone(), line: lines.get(name).copied(), enables });
}
pkg.features.sort_by(|a, b| a.name.cmp(&b.name));
}
let mut extern_deps = BTreeMap::new();
for n in meta["resolve"]["nodes"].as_array().into_iter().flatten() {
let Some((pn, pv)) = n["id"].as_str().and_then(|id| by_id.get(id)) else { continue };
let mut m = BTreeMap::new();
for d in n["deps"].as_array().into_iter().flatten() {
if let (Some(name), Some(dep)) =
(d["name"].as_str(), d["pkg"].as_str().and_then(|id| by_id.get(id)))
{
m.insert(name.to_string(), dep.clone());
}
}
extern_deps.insert(format!("{pn} {pv}"), m);
}
VersionMap {
workspace_root: meta["workspace_root"].as_str().unwrap_or_default().to_string(),
repo: repo.to_string(),
packages,
extern_deps,
}
}
}
/// One sidecar: what cargo told the driver when it compiled one artifact.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Sidecar {
pub package: String,
pub version: String,
pub crate_name: String,
/// `CARGO_PRIMARY_PACKAGE` was set (a selected workspace member).
pub primary: bool,
/// The unit is a workspace member by its manifest
/// ([`VersionMap::is_member`]) — what a crate that loads this artifact
/// reads its origin from. Absent in a sidecar written before the field
/// existed: the reader then falls back to name and version.
#[serde(default)]
pub member: Option<bool>,
/// The unit's `CARGO_MANIFEST_DIR`. `primary` and `member` describe the
/// build that COMPILED the artifact; a target dir shared by several
/// workspaces hands the same artifact to another build, so a reader with
/// a version map decides membership in ITS build by this dir
/// ([`Sidecar::local_in`]).
#[serde(default)]
pub manifest_dir: Option<String>,
}
impl Sidecar {
/// Is this artifact a member of the build `versions` describes? By its
/// manifest dir when the sidecar has one and the reader has a map; else
/// what the compiling build said (`primary` / `member`), else by name and
/// version.
pub fn local_in(&self, versions: Option<&VersionMap>) -> bool {
match (&self.manifest_dir, versions) {
(Some(dir), Some(m)) => m.is_member(&self.package, &self.version, Some(Path::new(dir))),
_ => {
self.primary
|| self.member.unwrap_or_else(|| versions.is_some_and(|m| m.packages.iter().any(|x| x.local && x.name == self.package && x.version == self.version)))
}
}
}
}
pub fn sidecar_path(index_dir: &Path, key: &str) -> PathBuf {
index_dir.join(format!("{key}.json"))
}
pub fn write_sidecar(index_dir: &Path, key: &str, s: &Sidecar) -> std::io::Result<()> {
std::fs::create_dir_all(index_dir)?;
let p = sidecar_path(index_dir, key);
let tmp = p.with_extension(format!("json.tmp{}", std::process::id()));
std::fs::write(&tmp, serde_json::to_vec(s)?)?;
std::fs::rename(tmp, p)
}
pub fn read_sidecar(index_dir: &Path, key: &str) -> Option<Sidecar> {
serde_json::from_slice(&std::fs::read(sidecar_path(index_dir, key)).ok()?).ok()
}
/// What a value in a fn body says about WHICH table it is (see the driver's
/// `walk/tables.rs`).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "snake_case")]
pub enum TableRef {
/// A name the compiler resolved (a literal, a const, a static).
Const(String),
/// Whatever the caller passes as the fn's parameter with this index
/// (`self` is 0).
Param(u32),
/// A runtime value.
Unknown,
}
/// What ONE fn does with the tables its parameters name — the fact a CALLER
/// of that fn needs, and the only thing about table access that crosses a
/// crate boundary ([`TableSidecar`]).
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct TableSummary {
/// parameter index → access bits (`mimir_schema::Access::READ` /
/// `WRITE`): the fn reads / writes the table that parameter names (a
/// table name, an ident, a table handle, a collection of them)
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub effects: BTreeMap<u32, u8>,
/// the tables the fn's RETURN value stands for (a handle it opened)
#[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
pub ret: std::collections::BTreeSet<TableRef>,
}
impl TableSummary {
pub fn is_empty(&self) -> bool {
self.effects.is_empty() && self.ret.is_empty()
}
/// Union `other` in; `true` when that changed something.
pub fn absorb(&mut self, other: &TableSummary) -> bool {
let mut changed = false;
for (p, bits) in &other.effects {
let e = self.effects.entry(*p).or_insert(0);
if *e | bits != *e {
*e |= bits;
changed = true;
}
}
for r in &other.ret {
changed |= self.ret.insert(r.clone());
}
changed
}
}
/// The table summaries of one compiled crate, written next to its sidecar
/// (`<key>.tables.json`) so a dependent crate's compile knows what a call
/// into this crate does with a table argument. Keys are rustc's
/// `DefPathHash` (32 hex digits): the same for the crate that defines an
/// item and for every crate that loads it.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct TableSidecar {
/// [`TABLE_PASS`] of the driver that wrote it. A sidecar of another
/// version is not read: its crate counts as not summarised
/// ([`CrateStats::table_sidecars_stale`]).
#[serde(default)]
pub v: u32,
/// The table pass FAILED on this crate (it panicked): nothing below is
/// to be trusted, and a dependent crate counts it as not summarised.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
/// fns / methods of THIS crate
#[serde(default)]
pub fns: BTreeMap<String, TableSummary>,
/// trait methods (of any crate) an impl in THIS crate implements, with
/// that impl's summary: a call through the trait (`dyn`, a generic) may
/// land in it
#[serde(default)]
pub trait_methods: BTreeMap<String, TableSummary>,
}
/// The version of the table pass's summaries. Bump when what a summary says
/// changes: artifacts cargo keeps (dependencies compiled by an older driver)
/// then count as not summarised until they are compiled again.
pub const TABLE_PASS: u32 = 1;
pub fn table_sidecar_path(index_dir: &Path, key: &str) -> PathBuf {
index_dir.join(format!("{key}.tables.json"))
}
pub fn write_table_sidecar(index_dir: &Path, key: &str, s: &TableSidecar) -> std::io::Result<()> {
std::fs::create_dir_all(index_dir)?;
let p = table_sidecar_path(index_dir, key);
let tmp = p.with_extension(format!("json.tmp{}", std::process::id()));
std::fs::write(&tmp, serde_json::to_vec(s)?)?;
std::fs::rename(tmp, p)
}
/// The sidecar of `key`, if this driver's version of the pass wrote it.
pub fn read_table_sidecar(index_dir: &Path, key: &str) -> Option<TableSidecar> {
let s: TableSidecar = serde_json::from_slice(&std::fs::read(table_sidecar_path(index_dir, key)).ok()?).ok()?;
(s.v == TABLE_PASS).then_some(s)
}
/// One crate a compile saw, with the identity rustc mangles into its symbols.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct CrateId {
pub crate_name: String,
/// `tcx.stable_crate_id(cnum)` as 16 lowercase hex digits. v0 mangling puts
/// it on every path root (`C s<base62(id-2)>_ <name>`), and rustc names the
/// crate's codegen units `<crate>.<hex>-cgu.N` (the DWARF CU name).
pub stable_crate_id: String,
pub package: String,
pub version: String,
pub origin: String,
/// the moniker descriptor prefix of this crate's items
/// (`moniker::target_prefix`): `` `bin=<crate>`: `` … for a non-lib target
/// (bin, integration test, bench, example), empty for a lib
#[serde(default)]
pub prefix: String,
}
/// `StableCrateId` → the 16-hex form [`CrateId::stable_crate_id`] uses.
pub fn stable_crate_id_hex(id: u64) -> String {
format!("{id:016x}")
}
/// Per-crate dump record (`<stem>.stats.json` next to the Arrow files).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CrateStats {
pub crate_name: String,
pub package: String,
pub version: String,
pub hash: String,
pub origin: String,
pub crate_types: Vec<String>,
pub test_harness: bool,
pub nodes: usize,
pub nodes_defined: usize,
/// Semantic edges `(from, to, kind)`.
pub edges: usize,
/// Sum of the edges' `count`: distinct sites.
#[serde(default)]
pub edge_sites: usize,
#[serde(default)]
pub spans: usize,
pub edges_by_kind: BTreeMap<String, usize>,
/// edges whose target lives in another crate
pub cross_crate_edges: usize,
/// edges from a local (workspace) crate to a library / lang crate
pub cross_repo_edges: usize,
/// crates whose version could not be resolved (version "?")
pub unresolved_versions: Vec<String>,
/// how each extern crate's version was resolved: sidecar / map / lang / unresolved
pub version_sources: BTreeMap<String, usize>,
/// locally defined nodes that got a moniker another local node already had
#[serde(default)]
pub rustc_release: String,
pub moniker_collisions: usize,
#[serde(default)]
pub collision_samples: Vec<String>,
pub walk_ms: u128,
pub write_ms: u128,
pub nodes_file: String,
pub edges_file: String,
/// `code_spans` rows: node spans + positioned edge sites.
#[serde(default)]
pub spans_file: String,
/// [`semhash`] of the unit's [`Invocation::semkey`]: a dump with the same
/// one says the same.
#[serde(default)]
pub semhash: String,
/// this crate's StableCrateId (16 hex), see [`CrateId`]
#[serde(default)]
pub stable_crate_id: String,
/// every crate this compile loaded, local first: the exact
/// StableCrateId → package@version map for an artifact built by this compile
#[serde(default)]
pub crate_ids: Vec<CrateId>,
/// `-C symbol-mangling-version` of this compile (`v0` / `legacy` / `hashed`)
#[serde(default)]
pub symbol_mangling: String,
/// rows in `symbols_file` (rustc's symbol name per non-generic local item)
#[serde(default)]
pub symbols: usize,
#[serde(default)]
pub symbols_file: String,
/// symbol-name computations that panicked (item skipped, counted)
#[serde(default)]
pub symbol_errors: usize,
#[serde(default)]
pub symbols_ms: u128,
/// defined nodes per `visibility` value (`pub(in …)` folded into one key)
#[serde(default)]
pub nodes_by_visibility: BTreeMap<String, usize>,
/// defined nodes per `effective_visibility` value
#[serde(default)]
pub nodes_by_effective_visibility: BTreeMap<String, usize>,
/// the nameable API (`direct` / `reexported`) in rustc's `missing_docs`
/// sense: no impl blocks, no items of a trait impl, no generic params
#[serde(default)]
pub api_items: usize,
/// … of which carry a doc comment
#[serde(default)]
pub api_documented: usize,
/// defined nodes that carry a doc comment (any visibility)
#[serde(default)]
pub documented_nodes: usize,
/// feature nodes the package's manifest declares
#[serde(default)]
pub feature_nodes: usize,
/// … of which this compile was built with (`--cfg feature="…"`)
#[serde(default)]
pub features_active: usize,
/// feature nodes only referenced (a dependency's feature; a `feature = "…"`
/// in a cfg that no manifest declares)
#[serde(default)]
pub feature_refs: usize,
/// `[features]` entries whose dependency could not be resolved to a
/// package@version (no ENABLES edge written for them)
#[serde(default)]
pub feature_enables_unresolved: Vec<String>,
/// GATED_BY edges from items the compile kept, by their OWN `#[cfg]`
/// (rustc's cfg evaluation; `partial = false`)
#[serde(default)]
pub gated_by_active: usize,
/// GATED_BY edges with `partial = true`: a feature gates something INSIDE
/// the item (a statement, an expression, a `use`, a macro call, `cfg!`)
#[serde(default)]
pub gated_by_partial: usize,
/// `cfg_gated_out` stub nodes (token scan; rustc never saw these items)
#[serde(default)]
pub gated_out_stubs: usize,
/// GATED_BY edges from those stubs, by their own `#[cfg]`
#[serde(default)]
pub gated_by_stubs: usize,
/// source files the cfg scan read (rustc's own + compiled-out module files)
#[serde(default)]
pub cfg_scan_files: usize,
/// … that did not lex (no stubs from them)
#[serde(default)]
pub cfg_scan_failed: Vec<String>,
/// compiled-out `mod x;` whose file was not found
#[serde(default)]
pub cfg_scan_unresolved_mods: Vec<String>,
/// every `feature = "…"` atom in a cfg context of the scanned files
/// (`#[cfg]`, `#![cfg]`, `cfg!`, the predicate of `#[cfg_attr]`)
#[serde(default)]
pub feature_cfg_mentions: usize,
/// … of which sit on an item's own `#[cfg]` (a GATED_BY site, `partial =
/// false`)
#[serde(default)]
pub feature_cfg_attributed: usize,
/// … of which gate a PART of an item (a GATED_BY site, `partial = true`).
/// `mentions − attributed − partial` is what is NOT an edge: the
/// predicates of `cfg_attr`. GATED_BY is a lower bound by that much (plus
/// the files in `cfg_scan_failed`).
#[serde(default)]
pub feature_cfg_partial: usize,
#[serde(default)]
pub cfg_scan_ms: u128,
/// the table pass ran: the storage engine's crate is this crate or one
/// it (transitively) depends on. `false` = no TOUCHES_TABLE edge can come
/// from this crate, and none was looked for.
#[serde(default)]
pub table_pass: bool,
/// the table pass panicked: this crate's TOUCHES_TABLE edges are missing
#[serde(default)]
pub table_pass_error: Option<String>,
/// places where a table meets a read / write (the sum of the
/// TOUCHES_TABLE edges' `count`, before sites on one line fold)
#[serde(default)]
pub table_sites: usize,
/// … of which name no table the compiler could resolve (edges to the
/// `<unresolved>` table node)
#[serde(default)]
pub table_sites_unresolved: usize,
/// the tables this crate's fns touch by name
#[serde(default)]
pub tables: Vec<String>,
/// fns of this crate with a table summary (a parameter that names a
/// table, or a table handle returned)
#[serde(default)]
pub table_summaries: usize,
/// fn bodies the pass looked at (the callers of a summarised fn, to a
/// fixpoint), and how many looks that took
#[serde(default)]
pub table_bodies: usize,
#[serde(default)]
pub table_body_visits: usize,
/// dependency crates whose table sidecar was read / crates in the closure
#[serde(default)]
pub table_sidecars: (usize, usize),
/// dependency crates (not the toolchain's) with NO sidecar of this
/// driver's [`TABLE_PASS`]: compiled by another driver, or without the
/// sidecar index. What they do with a table is unknown here — the
/// TOUCHES_TABLE edges of this crate are a lower bound until they are
/// compiled again (a fresh target dir).
#[serde(default)]
pub table_sidecars_stale: Vec<String>,
#[serde(default)]
pub tables_ms: u128,
/// `#[mimir::table(…)]` attributes on this crate's own fns / methods
#[serde(default)]
pub table_annotations: usize,
/// … of which say something this driver does not understand (another
/// access word, a name that is no parameter of the fn): NO summary came
/// of them
#[serde(default)]
pub table_annotations_bad: Vec<String>,
/// The MIR fact format this dump carries ([`MIR_FACTS`]); 0 = a dump
/// from a driver before MIR facts (no `mir_body` / `mir_block` nodes).
#[serde(default)]
pub mir_facts: u32,
/// `full` (block nodes + CFG_SUCC edges + the body node), `fingerprint`
/// (the body node only) or `off` — [`ENV_MIR`] and the crate's origin.
#[serde(default)]
pub mir_mode: String,
/// fn / method bodies with MIR facts (closures folded into their fn).
#[serde(default)]
pub mir_bodies: usize,
/// … closure / coroutine bodies among them, folded into their fn.
#[serde(default)]
pub mir_nested_bodies: usize,
/// Basic blocks described (cleanup blocks excluded).
#[serde(default)]
pub mir_blocks: usize,
/// MIR statements written (full mode; storage markers, fake reads and
/// retags excluded).
#[serde(default)]
pub mir_statements: usize,
/// Bodies whose MIR was gone (stolen) before the walk: no facts.
#[serde(default)]
pub mir_missing: usize,
/// Bytes of MIR payload (the `ty` column of `mir_body` / `mir_block`).
#[serde(default)]
pub mir_payload_bytes: usize,
#[serde(default)]
pub mir_ms: u128,
/// The MIR pass failed on this crate (its facts are left out).
#[serde(default)]
pub mir_error: Option<String>,
}
/// The MIR fact format the driver writes (see `walk/mir.rs`): `mir_body` /
/// `mir_block` PART nodes and `CFG_SUCC` edges, the payload in `ty`. Carried
/// per crate in `.stats.json` (`mir_facts`); a dump without it (0) simply has
/// no MIR facts. Bumped only when the payload grammar changes. NOT part of the
/// lake schema ([`mimir_schema::SCHEMA_VERSION`] is unchanged: no column was
/// added) and not of any unit hash, so a new driver forces no re-dump.
pub const MIR_FACTS: u32 = mimir_schema::mir::FACTS_VERSION;
/// Env: which crates get MIR facts. `local` (default) = full facts for the
/// workspace's own crates, block fingerprints only (one `mir_body` node per
/// fn) for dependencies; `all` = full facts everywhere; `fingerprint` =
/// fingerprints everywhere; `off` = none.
pub const ENV_MIR: &str = "MIMIR_MIR";
/// A dumped table read back as rows of strings (null → empty), keyed by column
/// name — for tests and `cargo mimir-dump --cat`.
#[derive(Debug, Clone, Default)]
pub struct Table {
pub columns: Vec<String>,
pub rows: Vec<Vec<String>>,
}
impl Table {
pub fn col(&self, name: &str) -> usize {
self.columns.iter().position(|c| c == name).unwrap_or_else(|| panic!("no column {name}"))
}
}
/// Read every `*.<suffix>.arrow` file in `dir` (suffix = `nodes` / `edges`).
pub fn read_tables(dir: &Path, suffix: &str) -> std::io::Result<Table> {
use arrow_array::Array;
use arrow_array::cast::AsArray;
let mut t = Table::default();
let mut files: Vec<PathBuf> = std::fs::read_dir(dir)?
.flatten()
.map(|e| e.path())
.filter(|p| p.to_string_lossy().ends_with(&format!(".{suffix}.arrow")))
.collect();
files.sort();
for f in files {
let r = arrow_ipc::reader::FileReader::try_new(std::fs::File::open(&f)?, None)
.map_err(std::io::Error::other)?;
let schema = r.schema();
if t.columns.is_empty() {
t.columns = schema.fields().iter().map(|f| f.name().clone()).collect();
}
for b in r {
let b = b.map_err(std::io::Error::other)?;
let cols = b.columns();
for i in 0..b.num_rows() {
let row = cols
.iter()
.map(|c| {
if c.is_null(i) {
return String::new();
}
match c.data_type() {
arrow_schema::DataType::Utf8 => c.as_string::<i32>().value(i).to_string(),
arrow_schema::DataType::UInt32 => {
c.as_primitive::<arrow_array::types::UInt32Type>().value(i).to_string()
}
arrow_schema::DataType::Boolean => c.as_boolean().value(i).to_string(),
_ => "?".into(),
}
})
.collect();
t.rows.push(row);
}
}
}
Ok(t)
}
#[cfg(test)]
mod tests {
/// A target dir shared by several workspaces hands one artifact to many
/// builds: whether a crate is a MEMBER is the reading build's question
/// The semantic key of a unit: its cfgs (features) and code-changing
/// options are in it, in any order; its extra-filename (the hash cargo
/// mixes its dependencies into) is not.
/// `MIMIR_DUMP=off`: no out dir, whatever `MIMIR_OUT` says.
/// A generated file under the unit's OUT_DIR is named without the hash
/// of the dir (`OUT_DIR/private.rs`); anything else is not touched.
/// Paths are cut to their last segment, nothing else is touched.
#[test]
fn printed_types_keep_only_the_last_path_segment() {
let cut = crate::shorten_paths;
assert_eq!(cut("std::collections::BTreeMap<u32, clean::Point>"), "BTreeMap<u32, Point>");
assert_eq!(cut("&'a mut alloc::vec::Vec<(u8, core::option::Option<std::string::String>)>"), "&'a mut Vec<(u8, Option<String>)>");
assert_eq!(cut("<T as core::iter::Iterator>::Item"), "<T as Iterator>::Item");
assert_eq!(cut("dyn std::ops::Fn(u8) -> u8 + Send"), "dyn Fn(u8) -> u8 + Send");
assert_eq!(cut("[u8; 4]"), "[u8; 4]");
assert_eq!(cut("{closure@src/lib.rs:3:9: 3:11}"), "{closure@src/lib.rs:3:9: 3:11}");
assert_eq!(cut("Point"), "Point");
}
#[test]
fn a_generated_file_is_named_from_out_dir() {
let out = std::path::Path::new("/t/debug/build/serde/b82cacc85af76fd3/out");
assert_eq!(crate::out_dir_relative(std::path::Path::new("/t/debug/build/serde/b82cacc85af76fd3/out/private.rs"), Some(out)).as_deref(), Some("OUT_DIR/private.rs"));
assert_eq!(crate::out_dir_relative(std::path::Path::new("/r/serde/src/lib.rs"), Some(out)), None);
assert_eq!(crate::out_dir_relative(std::path::Path::new("/t/debug/build/serde/b82cacc85af76fd3/out/private.rs"), None), None);
}
/// The full semantic key: the same unit with its dependency at ANOTHER
/// version, or a feature's optional dependency resolved to another
/// version, is another key (its rows differ); the same versions under
/// other artifact hashes are one key.
#[test]
fn a_twin_whose_dependency_is_at_another_version_has_another_key() {
let tmp = std::env::temp_dir().join(format!("mimir-unit-semkey-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
let index = tmp.join("index");
let side = |key: &str, version: &str| {
crate::write_sidecar(&index, key, &crate::Sidecar { package: "serde_core".into(), version: version.into(), crate_name: "serde_core".into(), primary: false, member: None, manifest_dir: None }).unwrap();
};
side("serde_core-aaaa", "1.0.228");
side("serde_core-bbbb", "1.0.229");
side("serde_core-cccc", "1.0.229");
let inv = |dep: &str, extra: &str| {
let a: Vec<String> = ["--crate-name", "serde", "--crate-type", "lib", "src/lib.rs", "-C", &format!("extra-filename=-{extra}"), "--extern", &format!("serde_core=/t/deps/libserde_core-{dep}.rmeta")].iter().map(|s| s.to_string()).collect();
Invocation::parse(&a)
};
let key = |i: &Invocation, vf: Option<&std::path::Path>| crate::unit_semkey(i, "serde", "1.0.229", "/r/serde", Some(&index), vf);
assert_ne!(key(&inv("aaaa", "1"), None), key(&inv("bbbb", "2"), None), "serde_core 1.0.228 vs 1.0.229: other edges, another key");
assert_eq!(key(&inv("bbbb", "1"), None), key(&inv("cccc", "2"), None), "the same versions under other hashes: one key");
assert_ne!(key(&inv("dddd", "1"), None), key(&inv("eeee", "2"), None), "no sidecar: never equal");
// the reading repo's lockfile resolves an optional dependency
let features = |dep_version: &str| {
let vf = tmp.join(format!("v{dep_version}/crate-versions.json"));
let map = VersionMap {
packages: vec![PackageVersion {
name: "serde".into(),
version: "1.0.229".into(),
crate_names: vec!["serde".into()],
local: false,
targets: vec![],
manifest_path: "/r/serde/Cargo.toml".into(),
features: vec![FeatureDecl { name: "rc".into(), line: Some(3), enables: vec![FeatureEnable { raw: "dep:arc".into(), form: EnableForm::Dep, dep_package: Some("arc".into()), dep_version: Some(dep_version.into()), feature: None }] }],
}],
..Default::default()
};
std::fs::create_dir_all(vf.parent().unwrap()).unwrap();
map.write(&vf).unwrap();
vf
};
let (v1, v2) = (features("0.1.0"), features("0.2.0"));
assert_ne!(key(&inv("bbbb", "1"), Some(&v1)), key(&inv("bbbb", "1"), Some(&v2)), "an optional dependency at another version");
let _ = std::fs::remove_dir_all(&tmp);
}
/// Two packages of one name and version (a path checkout and its
/// published copy) each keep their own features, found by the manifest.
#[test]
fn features_are_found_by_the_packages_manifest() {
let tmp = std::env::temp_dir().join(format!("mimir-features-at-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
let pkg = |dir: &str, line: u32| PackageVersion {
name: "rotaryengine".into(),
version: "0.1.13".into(),
crate_names: vec!["gatling".into()],
local: false,
targets: vec![],
manifest_path: format!("{dir}/Cargo.toml"),
features: vec![FeatureDecl { name: "testmatrix".into(), line: Some(line), enables: vec![] }],
};
let map = VersionMap { packages: vec![pkg("/clones/zz/gatling", 30), pkg("/registry/rotaryengine-0.1.13", 29)], ..Default::default() };
let vf = tmp.join("crate-versions.json");
std::fs::create_dir_all(&tmp).unwrap();
map.write(&vf).unwrap();
let line = |dir: &str| VersionMap::load_features_at(&vf, "rotaryengine", "0.1.13", std::path::Path::new(dir)).first().and_then(|f| f.line);
assert_eq!(line("/clones/zz/gatling"), Some(30));
assert_eq!(line("/registry/rotaryengine-0.1.13"), Some(29));
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn dump_off_writes_no_dump() {
let out = || Some(std::ffi::OsString::from("/o"));
assert_eq!(crate::dump_out(out(), None), Some(std::path::PathBuf::from("/o")));
assert_eq!(crate::dump_out(out(), Some("workspace")), Some(std::path::PathBuf::from("/o")));
assert_eq!(crate::dump_out(out(), Some("off")), None);
assert_eq!(crate::dump_out(None, None), None);
}
#[test]
fn the_semantic_key_is_the_unit_without_its_dependency_hashes() {
let argv = |extra: &str, cfgs: &[&str]| -> Vec<String> {
let mut v: Vec<String> = ["--crate-name", "serde", "--crate-type", "lib", "src/lib.rs", "-C", &format!("extra-filename=-{extra}"), "-C", "opt-level=0"].iter().map(|s| s.to_string()).collect();
for c in cfgs {
v.push("--cfg".into());
v.push(c.to_string());
}
v
};
let key = |a: Vec<String>| Invocation::parse(&a).semkey("serde", "1.0.0", "/r/serde-1.0.0");
let a = key(argv("aaaa", &["feature=\"std\"", "feature=\"derive\""]));
assert_eq!(a, key(argv("bbbb", &["feature=\"derive\"", "feature=\"std\""])), "another dependency hash, same features: one unit");
assert_ne!(a, key(argv("aaaa", &["feature=\"std\""])), "another feature set: another unit");
assert!(a.contains("opt-level=0") && a.contains("feature=\"derive\""), "{a}");
assert_eq!(semhash(&a), semhash(&a.clone()));
assert_eq!(semhash(&a).len(), 16);
}
/// (its version map, by manifest dir), not what the compiling build said.
#[test]
fn a_sidecar_is_local_by_the_reading_builds_map() {
let pkg = |name: &str, local: bool, manifest: &str| PackageVersion {
name: name.into(),
version: "0.7.0".into(),
crate_names: vec![name.into()],
local,
targets: vec![],
manifest_path: manifest.into(),
features: vec![],
};
let skade_build = VersionMap { workspace_root: "/c/skade".into(), repo: "skade".into(), packages: vec![pkg("skade", true, "/c/skade/skade/Cargo.toml")], ..Default::default() };
let nornir_build = VersionMap { workspace_root: "/c/nornir".into(), repo: "nornir".into(), packages: vec![pkg("skade", false, "/c/skade/skade/Cargo.toml")], ..Default::default() };
// compiled in skade's own build: primary, a member there
let sc = Sidecar { package: "skade".into(), version: "0.7.0".into(), crate_name: "skade".into(), primary: true, member: Some(true), manifest_dir: Some("/c/skade/skade".into()) };
assert!(sc.local_in(Some(&skade_build)));
assert!(!sc.local_in(Some(&nornir_build)), "read in nornir's build it is a dependency");
// a sidecar written before the field: what the compiling build said
let old = Sidecar { manifest_dir: None, ..sc.clone() };
assert!(old.local_in(Some(&nornir_build)));
}
use super::{feature_lines, lock_dependencies, semhash, EnableForm, FeatureDecl, FeatureEnable, Invocation, PackageVersion, Sidecar, VersionMap, NOHASH};
/// A member and the PUBLISHED copy of that member in one build (what
/// `edda` holds of `nornir-cli-outcome 0.1.4`): the unit compiled from the
/// member's own manifest is a member, the one from the registry is not —
/// same name, same version.
///
/// RED WHEN REVERTED: decide by name and version alone and the registry
/// copy is a member too.
#[test]
fn a_member_is_told_from_its_published_copy_by_its_manifest() {
let meta: serde_json::Value = serde_json::from_str(
r#"{
"workspace_root": "/nonexistent/ws",
"workspace_members": ["own-id"],
"packages": [
{"id": "own-id", "name": "outcome", "version": "0.1.4", "manifest_path": "/nonexistent/ws/crates/outcome/Cargo.toml",
"targets": [{"kind": ["lib"], "name": "outcome"}], "dependencies": [], "features": {}},
{"id": "registry-id", "name": "outcome", "version": "0.1.4", "manifest_path": "/nonexistent/registry/outcome-0.1.4/Cargo.toml",
"targets": [{"kind": ["lib"], "name": "outcome"}], "dependencies": [], "features": {}}
],
"resolve": {"nodes": [{"id": "own-id", "deps": []}, {"id": "registry-id", "deps": []}]}
}"#,
)
.unwrap();
let m = VersionMap::from_cargo_metadata(&meta, "ws");
let at = |dir: &str| m.is_member("outcome", "0.1.4", Some(std::path::Path::new(dir)));
assert!(at("/nonexistent/ws/crates/outcome"), "compiled from the member's own manifest");
assert!(!at("/nonexistent/registry/outcome-0.1.4"), "the published copy is a library");
assert!(m.is_member("outcome", "0.1.4", None), "no manifest dir: name and version are all there is");
assert!(!m.is_member("outcome", "0.1.5", Some(std::path::Path::new("/nonexistent/ws/crates/outcome"))));
}
/// What `cargo metadata` prints for: member `b` with deps `a` (0.1.0),
/// `a_old` (= package `a` 0.0.9) and the OPTIONAL `c` (off, so `c` is in
/// neither `packages` nor the resolve — only in the lockfile).
const META: &str = r#"{
"workspace_root": "/nonexistent/ws",
"workspace_members": ["b-id"],
"packages": [
{"id": "a-id", "name": "a", "version": "0.1.0", "manifest_path": "/nonexistent/ws/a/Cargo.toml",
"targets": [{"kind": ["lib"], "name": "a"}], "dependencies": [],
"features": {"default": ["on"], "on": [], "extra": ["on"]}},
{"id": "a-old-id", "name": "a", "version": "0.0.9", "manifest_path": "/nonexistent/ws/a-old/Cargo.toml",
"targets": [{"kind": ["lib"], "name": "a"}], "dependencies": [], "features": {"legacy": []}},
{"id": "b-id", "name": "b", "version": "0.2.0", "manifest_path": "/nonexistent/ws/b/Cargo.toml",
"targets": [{"kind": ["lib"], "name": "b"}, {"kind": ["bin"], "name": "bctl"}],
"dependencies": [
{"name": "a", "rename": null, "optional": false},
{"name": "a", "rename": "a_old", "optional": false},
{"name": "c", "rename": null, "optional": true},
{"name": "gone", "rename": null, "optional": true}],
"features": {"fast": ["a/extra"], "old": ["a_old/legacy"], "with-c": ["dep:c"], "c-turbo": ["c?/turbo"],
"c": ["dep:c"], "lost": ["dep:gone"], "nodep": ["nothing/x"]}}
],
"resolve": {"nodes": [
{"id": "a-id", "deps": []},
{"id": "a-old-id", "deps": []},
{"id": "b-id", "deps": [{"name": "a", "pkg": "a-id"}, {"name": "a_old", "pkg": "a-old-id"}]}
]}
}"#;
const LOCK: &str = "version = 4\n\n[[package]]\nname = \"a\"\nversion = \"0.0.9\"\n\n[[package]]\nname = \"a\"\nversion = \"0.1.0\"\n\n[[package]]\nname = \"b\"\nversion = \"0.2.0\"\ndependencies = [\n \"a 0.0.9\",\n \"a 0.1.0\",\n \"c\",\n]\n\n[[package]]\nname = \"c\"\nversion = \"0.3.0\"\n";
#[test]
fn the_lockfile_names_every_dependency_with_its_version() {
let l = lock_dependencies(LOCK);
assert_eq!(
l[&("b".to_string(), "0.2.0".to_string())],
vec![("a".to_string(), Some("0.0.9".to_string())), ("a".to_string(), Some("0.1.0".to_string())), ("c".to_string(), Some("0.3.0".to_string()))]
);
assert!(l[&("c".to_string(), "0.3.0".to_string())].is_empty());
// a registry entry: `name version (source)`
let l = lock_dependencies("[[package]]\nname = \"x\"\nversion = \"1.0.0\"\ndependencies = [\n \"y 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)\",\n]\n");
assert_eq!(l[&("x".to_string(), "1.0.0".to_string())], vec![("y".to_string(), Some("2.0.0".to_string()))]);
}
#[test]
fn feature_entries_resolve_to_package_and_version() {
let meta: serde_json::Value = serde_json::from_str(META).unwrap();
let m = VersionMap::with_lock(&meta, "fixture", Some(LOCK));
let b = m.packages.iter().find(|p| p.name == "b").unwrap();
let get = |f: &str| {
let e = &b.features.iter().find(|x| x.name == f).unwrap_or_else(|| panic!("no feature {f}")).enables[0];
(e.form, e.dep_package.clone().unwrap_or_default(), e.dep_version.clone(), e.feature.clone().unwrap_or_default())
};
// an ACTIVATED dependency: the resolve graph, the rename told apart by its extern name
assert_eq!(get("fast"), (EnableForm::DepFeature, "a".into(), Some("0.1.0".into()), "extra".into()));
assert_eq!(get("old"), (EnableForm::DepFeature, "a".into(), Some("0.0.9".into()), "legacy".into()));
// an optional dependency that is OFF: not in the metadata at all — the lockfile pins it
assert_eq!(get("with-c"), (EnableForm::Dep, "c".into(), Some("0.3.0".into()), String::new()));
assert_eq!(get("c-turbo"), (EnableForm::WeakDepFeature, "c".into(), Some("0.3.0".into()), "turbo".into()));
// not in the lockfile either: a known package, NO version — never a guess
assert_eq!(get("lost"), (EnableForm::Dep, "gone".into(), None, String::new()));
// no dependency of that key at all
assert_eq!(get("nodep"), (EnableForm::DepFeature, String::new(), None, "x".into()));
let a = m.packages.iter().find(|p| p.name == "a" && p.version == "0.1.0").unwrap();
let extra = a.features.iter().find(|f| f.name == "extra").unwrap();
assert_eq!((extra.enables[0].form, extra.enables[0].feature.as_deref()), (EnableForm::Feature, Some("on")));
// features come out sorted by name (a stable dump)
assert_eq!(a.features.iter().map(|f| f.name.as_str()).collect::<Vec<_>>(), ["default", "extra", "on"]);
// the features travel in per-package files, not in the map every
// rustc invocation loads
let dir = std::env::temp_dir().join(format!("mimir-vmap-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("crate-versions.json");
m.write(&file).unwrap();
let back = VersionMap::load(&file).unwrap();
assert!(back.packages.iter().all(|p| p.features.is_empty()), "the map file carries no features");
assert!(!std::fs::read_to_string(&file).unwrap().contains("dep_version"));
assert_eq!(VersionMap::load_features(&file, "b", "0.2.0"), b.features);
assert_eq!(VersionMap::load_features(&file, "a", "0.0.9").len(), 1, "two versions of one name: two files");
assert!(VersionMap::load_features(&file, "nobody", "1.0.0").is_empty());
std::fs::remove_dir_all(&dir).unwrap();
// WITHOUT the lockfile the off dependency stays unresolved (red-proof
// of the lockfile read)
let no_lock = VersionMap::with_lock(&meta, "fixture", None);
let b = no_lock.packages.iter().find(|p| p.name == "b").unwrap();
assert_eq!(b.features.iter().find(|x| x.name == "with-c").unwrap().enables[0].dep_version, None);
}
#[test]
fn feature_lines_are_the_keys_of_the_features_table_only() {
let toml = "[package]\nname = \"a\"\ndefault = 1\n\n[features] # the table\ndefault = [\"on\"]\n# on = []\non = []\n\"quoted-one\" = [\n \"on\",\n]\n\n[dependencies]\nextra = \"1\"\n";
let l = feature_lines(toml);
assert_eq!(l.get("default"), Some(&6));
assert_eq!(l.get("on"), Some(&8));
assert_eq!(l.get("quoted-one"), Some(&9));
assert_eq!(l.get("extra"), None, "a key of another table is not a feature");
assert_eq!(l.len(), 3);
}
fn argv(s: &str) -> Vec<String> {
s.split_whitespace().map(str::to_string).collect()
}
#[test]
fn the_file_stem_is_unique_per_unit_also_without_an_extra_filename() {
let check = Invocation::parse(&argv("--crate-name bctl --crate-type bin -C metadata=1af1 -C extra-filename=-e793 src/main.rs"));
assert_eq!(check.short_hash(), "e793");
// `cargo build --release`: an uplifted bin has NO extra-filename. Two
// such bins with one crate name + version (two packages, two profiles,
// two targets) used to share the stem `nohash` and overwrite each other.
let a = Invocation::parse(&argv("--crate-name bctl --crate-type bin -C metadata=a763 src/main.rs"));
let b = Invocation::parse(&argv("--crate-name bctl --crate-type bin -Cmetadata=bfd0 src/main.rs"));
assert_eq!((a.short_hash().as_str(), b.short_hash().as_str()), ("ma763", "mbfd0"));
assert_ne!(a.short_hash(), b.short_hash());
// a build-script probe (no cargo unit): skipped by consumers
let probe = Invocation::parse(&argv("--crate-name anyhow --crate-type lib probe.rs"));
assert!(probe.short_hash().starts_with(NOHASH));
}
}