use std::collections::BTreeMap;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::io;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use cargo_metadata::PackageId;
use cargo_metadata::TargetKind;
use cargo_metadata::semver::Version;
use sha2::Digest as _;
use crate::constants::CARGO_CONFIG;
use crate::constants::CARGO_CONFIG_TOML;
use crate::constants::CARGO_LOCK;
use crate::constants::CARGO_TOML;
use crate::constants::DOT_CARGO_DIR;
use crate::constants::RUST_TOOLCHAIN;
use crate::constants::RUST_TOOLCHAIN_TOML;
use crate::project::AbsolutePath;
#[derive(Debug, Default)]
pub(crate) struct WorkspaceMetadataStore {
pub by_root: HashMap<AbsolutePath, WorkspaceMetadata>,
pub dispatch_generations: HashMap<AbsolutePath, u64>,
}
impl WorkspaceMetadataStore {
pub fn new() -> Self { Self::default() }
pub fn get(&self, workspace_root: &AbsolutePath) -> Option<&WorkspaceMetadata> {
self.by_root.get(workspace_root)
}
pub fn containing_workspace_root(&self, path: &AbsolutePath) -> Option<&AbsolutePath> {
let mut cursor: Option<&Path> = Some(path.as_path());
while let Some(p) = cursor {
if let Some((root, _)) = self.by_root.iter().find(|(root, _)| root.as_path() == p) {
return Some(root);
}
cursor = p.parent();
}
None
}
pub fn resolved_target_dir(&self, path: &AbsolutePath) -> Option<&AbsolutePath> {
let root = self.containing_workspace_root(path)?;
self.by_root.get(root).map(|snap| &snap.target_directory)
}
pub fn package_for_path(&self, path: &AbsolutePath) -> Option<&PackageRecord> {
let root = self.containing_workspace_root(path)?;
let snap = self.by_root.get(root)?;
let expected_manifest = path.as_path().join(CARGO_TOML);
snap.packages
.values()
.find(|pkg| pkg.manifest_path.as_path() == expected_manifest)
}
pub fn upsert(&mut self, workspace_metadata: WorkspaceMetadata) {
self.by_root.insert(
workspace_metadata.workspace_root.clone(),
workspace_metadata,
);
}
pub fn set_out_of_tree_target_bytes(
&mut self,
workspace_root: &AbsolutePath,
target_dir: &AbsolutePath,
bytes: u64,
) -> bool {
let Some(snap) = self.by_root.get_mut(workspace_root) else {
return false;
};
if snap.target_directory != *target_dir {
return false;
}
snap.out_of_tree_target_bytes = Some(bytes);
true
}
pub fn next_generation(&mut self, workspace_root: &AbsolutePath) -> u64 {
let slot = self
.dispatch_generations
.entry(workspace_root.clone())
.or_default();
*slot = slot.saturating_add(1);
*slot
}
pub fn is_current_generation(&self, workspace_root: &AbsolutePath, generation: u64) -> bool {
self.dispatch_generations.get(workspace_root).copied() == Some(generation)
}
}
#[derive(Clone, Debug)]
pub(crate) struct WorkspaceMetadata {
pub workspace_root: AbsolutePath,
pub target_directory: AbsolutePath,
pub packages: HashMap<PackageId, PackageRecord>,
pub fingerprint: ManifestFingerprint,
pub out_of_tree_target_bytes: Option<u64>,
}
#[derive(Clone, Debug)]
pub(crate) struct PackageRecord {
pub name: String,
pub version: Version,
pub edition: String,
pub description: Option<String>,
pub license: Option<String>,
pub homepage: Option<String>,
pub repository: Option<String>,
pub manifest_path: AbsolutePath,
pub targets: Vec<TargetRecord>,
pub publish: PublishPolicy,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum PublishPolicy {
Any,
Never,
Registries(Vec<String>),
}
impl PublishPolicy {
pub fn from_cargo_publish(raw: Option<&[String]>) -> Self {
match raw {
None => Self::Any,
Some([]) => Self::Never,
Some(list) => Self::Registries(list.to_vec()),
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct TargetRecord {
pub name: String,
pub kinds: Vec<TargetKind>,
pub src_path: AbsolutePath,
pub required_features: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct ManifestFingerprint {
pub manifest: FileStamp,
pub lockfile: Option<FileStamp>,
pub rust_toolchain: Option<FileStamp>,
pub configs: BTreeMap<PathBuf, Option<FileStamp>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct FileStamp {
pub content_hash: [u8; 32],
}
impl FileStamp {
pub fn from_path(path: &Path) -> io::Result<Self> {
let bytes = fs::read(path)?;
let mut hasher = sha2::Sha256::new();
hasher.update(&bytes);
let digest = hasher.finalize();
let mut content_hash = [0_u8; 32];
content_hash.copy_from_slice(&digest);
Ok(Self { content_hash })
}
}
impl ManifestFingerprint {
pub fn capture(workspace_root: &Path) -> io::Result<Self> {
let manifest = FileStamp::from_path(&workspace_root.join(CARGO_TOML))?;
let lockfile = optional_stamp(&workspace_root.join(CARGO_LOCK))?;
let rust_toolchain = toolchain_stamp(workspace_root)?;
let configs = capture_config_chain(workspace_root)?;
Ok(Self {
manifest,
lockfile,
rust_toolchain,
configs,
})
}
}
fn optional_stamp(path: &Path) -> io::Result<Option<FileStamp>> {
match FileStamp::from_path(path) {
Ok(stamp) => Ok(Some(stamp)),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(None),
Err(err) => Err(err),
}
}
fn toolchain_stamp(workspace_root: &Path) -> io::Result<Option<FileStamp>> {
for ancestor in workspace_root.ancestors() {
for name in [RUST_TOOLCHAIN_TOML, RUST_TOOLCHAIN] {
let candidate = ancestor.join(name);
if let Some(stamp) = optional_stamp(&candidate)? {
return Ok(Some(stamp));
}
}
}
Ok(None)
}
fn capture_config_chain(workspace_root: &Path) -> io::Result<BTreeMap<PathBuf, Option<FileStamp>>> {
let cargo_home = resolve_cargo_home();
let mut configs = BTreeMap::new();
let mut visited: Option<PathBuf> = None;
for ancestor in workspace_root.ancestors() {
for name in [CARGO_CONFIG_TOML, CARGO_CONFIG] {
let candidate = ancestor.join(DOT_CARGO_DIR).join(name);
configs.insert(candidate.clone(), optional_stamp(&candidate)?);
}
visited = Some(ancestor.to_path_buf());
if cargo_home.as_ref().is_some_and(|home| ancestor == home) {
return Ok(configs);
}
}
if let Some(home) = cargo_home
&& visited.as_deref() != Some(home.as_path())
{
for name in [CARGO_CONFIG_TOML, CARGO_CONFIG] {
let candidate = home.join(name);
configs.insert(candidate.clone(), optional_stamp(&candidate)?);
}
}
Ok(configs)
}
fn resolve_cargo_home() -> Option<PathBuf> {
if let Ok(home) = env::var("CARGO_HOME")
&& !home.is_empty()
{
return Some(PathBuf::from(home));
}
dirs::home_dir().map(|home| home.join(DOT_CARGO_DIR))
}
#[cfg(test)]
#[allow(
clippy::expect_used,
reason = "tests should panic on unexpected values"
)]
#[allow(
clippy::unwrap_used,
reason = "tests should panic on unexpected values"
)]
mod tests {
use std::fs;
use tempfile::TempDir;
use super::*;
fn write_file(path: &Path, body: &[u8]) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, body).unwrap();
}
#[test]
fn equality_is_content_hash_only() {
let a = FileStamp {
content_hash: [7; 32],
};
let b = FileStamp {
content_hash: [7; 32],
};
assert_eq!(a, b, "same hash → equal");
let c = FileStamp {
content_hash: [8; 32],
};
assert_ne!(a, c, "different hash → unequal");
}
#[test]
fn same_bytes_produce_same_content_hash() {
let tmp = TempDir::new().unwrap();
let first_path = tmp.path().join("a.toml");
let second_path = tmp.path().join("b.toml");
write_file(&first_path, b"payload");
write_file(&second_path, b"payload");
let first = FileStamp::from_path(&first_path).unwrap();
let second = FileStamp::from_path(&second_path).unwrap();
assert_eq!(
first.content_hash, second.content_hash,
"identical bytes hash identically"
);
}
#[test]
fn identical_bytes_written_via_rename_stay_equal() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("file.toml");
write_file(&path, b"payload");
let first = FileStamp::from_path(&path).unwrap();
let tmp_path = tmp.path().join("file.toml.new");
write_file(&tmp_path, b"payload");
fs::rename(&tmp_path, &path).unwrap();
let second = FileStamp::from_path(&path).unwrap();
assert_eq!(
first, second,
"same bytes → same stamp, even after rename-save"
);
}
#[test]
fn config_chain_records_absent_files_as_none() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("ws");
write_file(&workspace.join("Cargo.toml"), b"[package]\nname=\"x\"\n");
let fp = ManifestFingerprint::capture(&workspace).unwrap();
assert!(
fp.configs.values().any(Option::is_none),
"absent configs are recorded as None, not omitted"
);
}
#[test]
fn config_chain_none_to_some_invalidates() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("ws");
write_file(&workspace.join("Cargo.toml"), b"[package]\nname=\"x\"\n");
let before = ManifestFingerprint::capture(&workspace).unwrap();
write_file(&workspace.join(".cargo/config.toml"), b"[build]\n");
let after = ManifestFingerprint::capture(&workspace).unwrap();
assert_ne!(
before, after,
"None → Some on a tracked config slot invalidates"
);
}
#[test]
fn next_generation_is_strictly_monotonic() {
let mut store = WorkspaceMetadataStore::new();
let root = AbsolutePath::from(PathBuf::from("/ws"));
let g1 = store.next_generation(&root);
let g2 = store.next_generation(&root);
let g3 = store.next_generation(&root);
assert!(g1 < g2 && g2 < g3);
}
#[test]
fn is_current_generation_rejects_stale_stamps() {
let mut store = WorkspaceMetadataStore::new();
let root = AbsolutePath::from(PathBuf::from("/ws"));
let gen_a = store.next_generation(&root);
assert!(store.is_current_generation(&root, gen_a));
let _ = store.next_generation(&root);
assert!(
!store.is_current_generation(&root, gen_a),
"older generation no longer current after a new dispatch"
);
}
fn fake_metadata(
workspace_root: AbsolutePath,
target_directory: AbsolutePath,
) -> WorkspaceMetadata {
WorkspaceMetadata {
workspace_root,
target_directory,
packages: std::collections::HashMap::new(),
fingerprint: ManifestFingerprint {
manifest: FileStamp {
content_hash: [0_u8; 32],
},
lockfile: None,
rust_toolchain: None,
configs: BTreeMap::new(),
},
out_of_tree_target_bytes: None,
}
}
#[test]
fn resolved_target_dir_is_none_without_metadata() {
let store = WorkspaceMetadataStore::new();
let path = AbsolutePath::from(PathBuf::from("/ws/src/lib.rs"));
assert!(
store.resolved_target_dir(&path).is_none(),
"no metadata → None; callers fall back to <project>/target"
);
}
#[test]
fn resolved_target_dir_returns_target_for_workspace_root() {
let mut store = WorkspaceMetadataStore::new();
let root = AbsolutePath::from(PathBuf::from("/ws"));
let target = AbsolutePath::from(PathBuf::from("/tmp/out-of-tree-target"));
store.upsert(fake_metadata(root.clone(), target.clone()));
assert_eq!(
store.resolved_target_dir(&root).cloned(),
Some(target),
"exact-match workspace root resolves its own target_directory"
);
}
#[test]
fn resolved_target_dir_walks_ancestors_from_member_or_worktree_paths() {
let mut store = WorkspaceMetadataStore::new();
let root = AbsolutePath::from(PathBuf::from("/ws"));
let target = AbsolutePath::from(PathBuf::from("/tmp/out-of-tree-target"));
store.upsert(fake_metadata(root, target.clone()));
let member = AbsolutePath::from(PathBuf::from("/ws/crates/core/src/lib.rs"));
assert_eq!(
store.resolved_target_dir(&member).cloned(),
Some(target),
"member paths resolve via ancestor walk up to the workspace root"
);
}
fn fake_package_record(name: &str, manifest_path: AbsolutePath) -> (PackageId, PackageRecord) {
let id = PackageId {
repr: format!("{name}-test-id"),
};
let record = PackageRecord {
name: name.into(),
version: Version::new(0, 1, 0),
edition: "2021".into(),
description: None,
license: Some("MIT".into()),
homepage: None,
repository: Some(format!("https://example.test/{name}")),
manifest_path,
targets: Vec::new(),
publish: PublishPolicy::Any,
};
(id, record)
}
#[test]
fn package_for_path_is_none_without_metadata() {
let store = WorkspaceMetadataStore::new();
let path = AbsolutePath::from(PathBuf::from("/ws"));
assert!(store.package_for_path(&path).is_none());
}
#[test]
fn package_for_path_matches_standalone_package_at_its_root() {
let mut store = WorkspaceMetadataStore::new();
let root = AbsolutePath::from(PathBuf::from("/ws"));
let target = AbsolutePath::from(PathBuf::from("/ws/target"));
let mut snap = fake_metadata(root.clone(), target);
let (pkg_id, pkg) =
fake_package_record("demo", AbsolutePath::from(PathBuf::from("/ws/Cargo.toml")));
snap.packages.insert(pkg_id, pkg);
store.upsert(snap);
let found = store.package_for_path(&root).expect("package found");
assert_eq!(found.name, "demo");
assert_eq!(found.license.as_deref(), Some("MIT"));
}
#[test]
fn package_for_path_matches_workspace_member() {
let mut store = WorkspaceMetadataStore::new();
let root = AbsolutePath::from(PathBuf::from("/ws"));
let target = AbsolutePath::from(PathBuf::from("/ws/target"));
let mut snap = fake_metadata(root, target);
let member_root = AbsolutePath::from(PathBuf::from("/ws/crates/core"));
let (pkg_id, pkg) = fake_package_record(
"core",
AbsolutePath::from(PathBuf::from("/ws/crates/core/Cargo.toml")),
);
snap.packages.insert(pkg_id, pkg);
store.upsert(snap);
let found = store
.package_for_path(&member_root)
.expect("member resolves via its own manifest_path");
assert_eq!(found.name, "core");
}
#[test]
fn package_for_path_returns_none_when_metadata_has_no_matching_package() {
let mut store = WorkspaceMetadataStore::new();
let root = AbsolutePath::from(PathBuf::from("/ws"));
let target = AbsolutePath::from(PathBuf::from("/ws/target"));
store.upsert(fake_metadata(root, target));
let phantom_member = AbsolutePath::from(PathBuf::from("/ws/crates/never"));
assert!(store.package_for_path(&phantom_member).is_none());
}
#[test]
fn set_out_of_tree_target_bytes_stamps_matching_metadata() {
let mut store = WorkspaceMetadataStore::new();
let root = AbsolutePath::from(PathBuf::from("/ws"));
let target = AbsolutePath::from(PathBuf::from("/elsewhere/target"));
store.upsert(fake_metadata(root.clone(), target.clone()));
let applied = store.set_out_of_tree_target_bytes(&root, &target, 42_000);
assert!(applied, "matching target_directory accepts the stamp");
assert_eq!(
store.get(&root).and_then(|s| s.out_of_tree_target_bytes),
Some(42_000)
);
}
#[test]
fn set_out_of_tree_target_bytes_declines_stale_target_dir() {
let mut store = WorkspaceMetadataStore::new();
let root = AbsolutePath::from(PathBuf::from("/ws"));
let stale_target = AbsolutePath::from(PathBuf::from("/old/target"));
let current_target = AbsolutePath::from(PathBuf::from("/new/target"));
store.upsert(fake_metadata(root.clone(), current_target));
let applied = store.set_out_of_tree_target_bytes(&root, &stale_target, 999);
assert!(
!applied,
"stale target_dir is discarded; a fresh walk is already in flight"
);
assert!(
store
.get(&root)
.and_then(|s| s.out_of_tree_target_bytes)
.is_none()
);
}
#[test]
fn set_out_of_tree_target_bytes_noop_without_metadata() {
let mut store = WorkspaceMetadataStore::new();
let root = AbsolutePath::from(PathBuf::from("/ws"));
let target = AbsolutePath::from(PathBuf::from("/elsewhere/target"));
let applied = store.set_out_of_tree_target_bytes(&root, &target, 1);
assert!(!applied, "no metadata → nothing to stamp");
}
}