use super::{
compile::{
C_SRCS, CPP_SRCS, collect_sources, find_sources, test_source_roots,
},
deps::local_dep_fingerprint,
diag::ReadPath,
proj_parse::{Dependency, Language, Project},
};
use miette::Result;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs,
hash::{DefaultHasher, Hash, Hasher},
path::{Path, PathBuf},
time::UNIX_EPOCH,
};
const CACHE_FILE: &str = ".conjure/build/fingerprint.kdl";
#[derive(Serialize, Deserialize)]
struct FileEntry {
size: u64,
mtime: i64,
hash: u64,
}
#[derive(Serialize, Deserialize, Default)]
pub struct FileCache {
files: HashMap<PathBuf, FileEntry>,
}
impl FileCache {
pub fn load(base: &Path) -> Self {
fs::read_to_string(base.join(CACHE_FILE))
.ok()
.and_then(|s| kdl::de::from_str(&s).ok())
.unwrap_or_default()
}
pub fn save(&self, base: &Path) {
let path = base.join(CACHE_FILE);
let _ = fs::create_dir_all(path.parent().unwrap());
if let Ok(mut doc) = kdl::se::to_document(self) {
let cfg = kdl::FormatConfigBuilder::new().indent(" ").build();
doc.autoformat_config(&cfg);
let _ = fs::write(path, super::proj_write::fix_braces(&doc.to_string()));
}
}
}
fn mtime_nanos(md: &fs::Metadata) -> i64 {
md.modified()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map_or(0, |d| d.as_nanos() as i64)
}
fn hash_file(path: &Path) -> Result<u64> {
let bytes = fs::read(path).map_err(|source| ReadPath {
path: path.to_path_buf(),
source,
})?;
let mut h = DefaultHasher::new();
bytes.hash(&mut h);
Ok(h.finish())
}
fn file_fingerprint(
cache: &mut FileCache,
paths: &[PathBuf],
) -> Result<Vec<String>> {
let mut out = Vec::with_capacity(paths.len());
for path in paths {
let md = fs::metadata(path).map_err(|source| ReadPath {
path: path.to_path_buf(),
source,
})?;
let size = md.len();
let mtime = mtime_nanos(&md);
let hash = match cache.files.get(path) {
Some(entry) if entry.size == size && entry.mtime == mtime => entry.hash,
_ => {
let hash = hash_file(path)?;
cache
.files
.insert(path.clone(), FileEntry { size, mtime, hash });
hash
}
};
out.push(format!("{}:{hash:016x}", path.display()));
}
out.sort();
Ok(out)
}
pub fn dir_fingerprint(dir: &Path, cache: &mut FileCache) -> Result<String> {
let files = collect_sources(dir, &[".".to_string()], &[])?;
Ok(file_fingerprint(cache, &files)?.join(","))
}
pub fn fingerprint(
project: &Project,
dir: &Path,
profile_name: &str,
dep_commits: &[&str],
cache: &mut FileCache,
) -> Result<String> {
let exts = match project.language {
Language::C => C_SRCS,
Language::Cpp => CPP_SRCS,
};
let compile = project
.compile
.as_ref()
.ok_or_else(|| miette::miette!("no compile section"))?;
let mut items = vec![profile_name.to_string()];
if let Some(cc) = &compile.cc {
items.push(cc.clone());
}
items.push(format!("ty:{:?}", project.ty));
items.push(format!("link:{:?}", project.link));
if let Some(l) = &compile.linker {
items.push(l.clone());
}
if let Some(std) = &compile.standard {
items.push(std.clone());
}
if let Some(arch) = compile.arch {
items.push(format!("arch:{arch:?}"));
}
if let Some(inc) = &compile.include {
items.extend(inc.list());
}
if let Some(f) = &compile.c_flags {
items.push(format!("{:?}", f));
}
if let Some(f) = &compile.ld_flags {
items.push(format!("{:?}", f));
}
items.extend(dep_commits.iter().map(|s| s.to_string()));
let roots = compile.src_roots();
let mut sources = collect_sources(dir, &roots, exts)?;
let excludes = test_source_roots(project, dir);
if !excludes.is_empty() {
sources.retain(|src| !excludes.iter().any(|e| src.starts_with(e)));
}
items.extend(file_fingerprint(cache, &sources)?);
if let Some(include) = &compile.include {
for inc in include.list() {
let mut files = vec![];
find_sources(&dir.join(inc), &[], &mut files)?;
items.extend(file_fingerprint(cache, &files)?);
}
}
if let Some(deps) = &project.dependencies {
let mut entries: Vec<(&String, &Dependency)> = deps.iter().collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
for (name, dep) in entries {
items.push(format!(
"{name}:cfg:{:?}:{:?}:{:?}:{:?}",
dep.build, dep.include, dep.pkg_config, dep.src
));
if dep.local.is_some() {
items.push(format!(
"{name}:{}",
local_dep_fingerprint(project, dir, profile_name, name, dep, cache)?
));
}
}
}
Ok(items.join("|"))
}
#[cfg(test)]
mod tests {
use super::{FileCache, fingerprint};
use crate::conjure::proj_parse::{
Arch, Compile, Dependency, Flags, Language, Project,
};
use std::collections::HashMap;
fn local_dep(path: &str) -> Dependency {
Dependency {
remote: None,
local: Some(path.into()),
transport: None,
build: None,
include: None,
src: None,
pkg_config: None,
r#ref: None,
}
}
#[test]
fn fingerprint_is_order_independent_for_deps() {
let base =
std::env::temp_dir().join(format!("conjure_fp_{}", std::process::id()));
std::fs::create_dir_all(base.join("src")).unwrap();
std::fs::write(base.join("src/main.c"), "").unwrap();
std::fs::create_dir_all(base.join("deps/a/src")).unwrap();
std::fs::create_dir_all(base.join("deps/b/src")).unwrap();
std::fs::write(base.join("deps/a/src/a.c"), "").unwrap();
std::fs::write(base.join("deps/b/src/b.c"), "").unwrap();
let mut a = Project {
name: "x".into(),
language: Language::C,
compile: Some(Default::default()),
..Default::default()
};
let mut b = a.clone();
let mut m1 = HashMap::new();
m1.insert("a".to_string(), local_dep("deps/a"));
m1.insert("b".to_string(), local_dep("deps/b"));
a.dependencies = Some(m1);
let mut m2 = HashMap::new();
m2.insert("b".to_string(), local_dep("deps/b"));
m2.insert("a".to_string(), local_dep("deps/a"));
b.dependencies = Some(m2);
assert_eq!(
fingerprint(&a, &base, "default", &[], &mut FileCache::default())
.unwrap(),
fingerprint(&b, &base, "default", &[], &mut FileCache::default())
.unwrap()
);
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn file_cache_avoids_rereading_unchanged_files() {
let base =
std::env::temp_dir().join(format!("conjure_fc_{}", std::process::id()));
let src = base.join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("main.c"), "int main(void){return 0;}").unwrap();
let mut project = Project {
name: "x".into(),
language: Language::C,
compile: Some(Default::default()),
..Default::default()
};
project.compile.as_mut().unwrap().cc = Some("cc".into());
let mut cache = FileCache::default();
let first =
fingerprint(&project, &base, "default", &[], &mut cache).unwrap();
assert_eq!(cache.files.len(), 1, "one source cached");
let second =
fingerprint(&project, &base, "default", &[], &mut cache).unwrap();
assert_eq!(first, second);
std::fs::write(src.join("main.c"), "int main(void){return 42;}").unwrap();
let third =
fingerprint(&project, &base, "default", &[], &mut cache).unwrap();
assert_ne!(first, third);
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn fingerprint_includes_single_file_sources() {
let base =
std::env::temp_dir().join(format!("conjure_fs1_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
std::fs::write(base.join("a.c"), "int a;").unwrap();
std::fs::write(base.join("b.c"), "int b;").unwrap();
let mut project = Project {
name: "x".into(),
language: Language::C,
compile: Some(Default::default()),
..Default::default()
};
project.compile.as_mut().unwrap().src =
Some(Flags::Append(vec!["a.c".into()]));
let mut cache = FileCache::default();
let first =
fingerprint(&project, &base, "default", &[], &mut cache).unwrap();
std::fs::write(base.join("b.c"), "int b = 1;").unwrap();
let second =
fingerprint(&project, &base, "default", &[], &mut cache).unwrap();
assert_eq!(first, second);
std::fs::write(base.join("a.c"), "int a = 1;").unwrap();
let third =
fingerprint(&project, &base, "default", &[], &mut cache).unwrap();
assert_ne!(first, third);
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn fingerprint_covers_optional_fields() {
let base =
std::env::temp_dir().join(format!("conjure_fpo_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(base.join("src")).unwrap();
std::fs::create_dir_all(base.join("include")).unwrap();
std::fs::write(base.join("src/main.c"), "int main(void){return 0;}")
.unwrap();
std::fs::write(base.join("include/x.h"), "#pragma once").unwrap();
let compile = Compile {
cc: Some("cc".into()),
linker: Some("gcc".into()),
standard: Some("c11".into()),
arch: Some(Arch::X86_64),
include: Some(Flags::Append(vec!["include".into()])),
c_flags: Some(Flags::Append(vec!["-Wall".into()])),
ld_flags: Some(Flags::Append(vec!["-flto".into()])),
..Default::default()
};
let full = Project {
name: "x".into(),
language: Language::C,
compile: Some(compile),
..Default::default()
};
let fp =
fingerprint(&full, &base, "default", &[], &mut FileCache::default())
.unwrap();
assert!(!fp.is_empty());
let bare = Project {
name: "x".into(),
language: Language::C,
compile: Some(Compile::default()),
..Default::default()
};
fingerprint(&bare, &base, "default", &[], &mut FileCache::default())
.unwrap();
let _ = std::fs::remove_dir_all(&base);
}
}