use std::collections::{BTreeMap, HashMap};
use anyhow::{Error, Result, anyhow};
use cargo_metadata::{Node, PackageId, camino::Utf8PathBuf};
use serde::{Deserialize, Serialize};
use crate::utils::{UnwrapOrExit, get_cache_path};
const CACHE_VERSION: u32 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Fingerprint([u8; 32]);
impl Serialize for Fingerprint {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&hex::encode(self.0))
}
}
impl<'de> Deserialize<'de> for Fingerprint {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let bytes = hex::decode(s).map_err(serde::de::Error::custom)?;
if bytes.len() != 32 {
return Err(serde::de::Error::custom("fingerprint must be 32 bytes"));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(Fingerprint(arr))
}
}
pub trait BuckalHash {
fn fingerprint(&self) -> Fingerprint;
}
impl BuckalHash for Node {
fn fingerprint(&self) -> Fingerprint {
let encoded = bincode::serde::encode_to_vec(self, bincode::config::standard())
.expect("Serialization failed");
Fingerprint(blake3::hash(&encoded).into())
}
}
pub trait PackageIdExt {
fn resolve(&self, workspace_root: &Utf8PathBuf) -> Self;
fn canonicalize(&self, workspace_root: &Utf8PathBuf) -> Self;
}
impl PackageIdExt for PackageId {
fn resolve(&self, workspace_root: &Utf8PathBuf) -> Self {
if self.repr.starts_with("path+file://($WORKSPACE)") {
PackageId {
repr: self
.repr
.clone()
.replace("($WORKSPACE)", workspace_root.as_str()),
}
} else {
self.clone()
}
}
fn canonicalize(&self, workspace_root: &Utf8PathBuf) -> Self {
if self
.repr
.starts_with(format!("path+file://{}", workspace_root.as_str()).as_str())
{
PackageId {
repr: self
.repr
.clone()
.replace(workspace_root.as_str(), "($WORKSPACE)"),
}
} else {
self.clone()
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct BuckalCache {
fingerprints: BTreeMap<PackageId, Fingerprint>,
version: u32,
}
impl BuckalCache {
pub fn new(resolve: &HashMap<PackageId, Node>, workspace_root: &Utf8PathBuf) -> Self {
let fingerprints = resolve
.iter()
.map(|(id, node)| (id.canonicalize(workspace_root), node.fingerprint()))
.collect();
Self {
fingerprints,
version: CACHE_VERSION,
}
}
pub fn new_empty() -> Self {
Self {
fingerprints: BTreeMap::new(),
version: CACHE_VERSION,
}
}
pub fn load() -> Result<Self, Error> {
let cache_path = get_cache_path().unwrap_or_exit_ctx("failed to get cache path");
if !cache_path.exists() {
return Err(anyhow!("Cache file does not exist"));
}
let content = std::fs::read_to_string(&cache_path)?;
let cache = toml::from_str::<BuckalCache>(&content)
.map_err(|e| anyhow!("Failed to parse cache file: {}", e))?;
if cache.version != CACHE_VERSION {
return Err(anyhow!(
"Cache version mismatch (found {}, expected {})",
cache.version,
CACHE_VERSION
));
}
Ok(cache)
}
pub fn save(&self) {
let cache_path = get_cache_path().unwrap_or_exit();
let content = toml::to_string_pretty(self).unwrap_or_exit();
let comment = "# @generated by `cargo buckal`\n# Not intended for manual editing.";
std::fs::write(cache_path, format!("{}\n{}", comment, content)).unwrap_or_exit();
}
pub fn diff(&self, other: &BuckalCache, workspace_root: &Utf8PathBuf) -> BuckalChange {
let mut _diff = BuckalChange::default();
for (id, fp) in &self.fingerprints {
if let Some(other_fp) = other.fingerprints.get(id) {
if fp != other_fp {
_diff
.changes
.insert(id.resolve(workspace_root), ChangeType::Changed);
}
} else {
_diff
.changes
.insert(id.resolve(workspace_root), ChangeType::Added);
}
}
for id in other.fingerprints.keys() {
if !self.fingerprints.contains_key(id) {
_diff
.changes
.insert(id.resolve(workspace_root), ChangeType::Removed);
}
}
_diff
}
}
#[derive(Debug, Default)]
pub struct BuckalChange {
pub changes: BTreeMap<PackageId, ChangeType>,
}
#[derive(Debug)]
pub enum ChangeType {
Added,
Removed,
Changed,
}