use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::Path;
use crate::io::{create_dir_all, has_matching_version, read_file, write_file_if_changed};
use crate::paths::{join_path, lockfile_path, parent_directory};
use crate::solution_generator::{GenerateOptions, GenerateResult};
const FINGERPRINTS_DIR: &str = ".fingerprints";
const GENERATE_FINGERPRINT_VERSION: u32 = crate::CACHE_VERSION;
fn canonical_options(opts: &GenerateOptions) -> String {
let mut s = String::new();
s.push_str("platform=");
s.push_str(opts.platform.raw());
s.push('|');
s.push_str("config=");
s.push_str(opts.build_config.raw());
s.push('|');
s.push_str("generator-root=");
s.push_str(&opts.generator_root);
s.push('|');
s.push_str("output-dir=");
match opts.output_dir.as_deref() {
None => s.push_str("<default>"),
Some(d) => s.push_str(d),
}
s.push('|');
s.push_str("extra-refs=");
for (i, r) in opts.extra_refs.iter().enumerate() {
if i > 0 {
s.push(',');
}
s.push_str(&r.name);
s.push('=');
s.push_str(&r.path);
}
s
}
fn options_hash(canonical: &str) -> u64 {
let mut h = DefaultHasher::new();
canonical.hash(&mut h);
h.finish()
}
pub(crate) fn fingerprint_path(project_root: &str, opts: &GenerateOptions) -> (String, String) {
let canonical = canonical_options(opts);
let hash = options_hash(&canonical);
let dir = join_path(
&join_path(project_root, &opts.generator_root),
FINGERPRINTS_DIR,
);
let file = format!("{}/{:016x}", dir, hash);
(canonical, file)
}
#[derive(Debug)]
pub(crate) struct Fingerprint {
pub canonical_options: String,
pub scan_cache_mtime: u128,
pub lockfile_mtime: u128,
pub sln: String,
pub csprojs: Vec<String>,
pub warnings: Vec<String>,
}
impl Fingerprint {
fn serialize(&self) -> String {
let mut s = String::from("# generate-fingerprint — auto-generated, do not edit\n");
s.push_str(&format!("# version: {}\n", GENERATE_FINGERPRINT_VERSION));
s.push_str("options: ");
s.push_str(&self.canonical_options);
s.push('\n');
s.push_str(&format!("scan-cache-mtime: {}\n", self.scan_cache_mtime));
s.push_str(&format!("lockfile-mtime: {}\n", self.lockfile_mtime));
s.push_str("sln: ");
s.push_str(&self.sln);
s.push('\n');
for c in &self.csprojs {
s.push_str("csproj: ");
s.push_str(c);
s.push('\n');
}
for w in &self.warnings {
s.push_str("warning: ");
s.push_str(w);
s.push('\n');
}
s
}
fn deserialize(content: &str) -> Option<Self> {
let mut canonical_options = None;
let mut scan_cache_mtime = None;
let mut lockfile_mtime = None;
let mut sln = None;
let mut csprojs = Vec::new();
let mut warnings = Vec::new();
for line in content.split('\n') {
if line.is_empty() || line.starts_with('#') {
continue;
}
let (k, v) = line.split_once(": ")?;
match k {
"options" => canonical_options = Some(v.to_string()),
"scan-cache-mtime" => scan_cache_mtime = v.parse().ok(),
"lockfile-mtime" => lockfile_mtime = v.parse().ok(),
"sln" => sln = Some(v.to_string()),
"csproj" => csprojs.push(v.to_string()),
"warning" => warnings.push(v.to_string()),
_ => {}
}
}
Some(Fingerprint {
canonical_options: canonical_options?,
scan_cache_mtime: scan_cache_mtime?,
lockfile_mtime: lockfile_mtime?,
sln: sln?,
csprojs,
warnings,
})
}
}
pub(crate) fn try_load_valid(
project_root: &str,
opts: &GenerateOptions,
) -> Option<GenerateResult> {
let (canonical, fp_path) = fingerprint_path(project_root, opts);
let content = read_file(&fp_path).ok()?;
if !has_matching_version(&content, GENERATE_FINGERPRINT_VERSION) {
return None;
}
let fp = Fingerprint::deserialize(&content)?;
if fp.canonical_options != canonical {
return None;
}
let scan_cache = join_path(
&join_path(project_root, &opts.generator_root),
"scan-cache",
);
let lockfile = lockfile_path(project_root, &opts.generator_root);
let scan_mtime = mtime_nanos(&scan_cache)?;
let lock_mtime = mtime_nanos(&lockfile)?;
if scan_mtime != fp.scan_cache_mtime || lock_mtime != fp.lockfile_mtime {
return None;
}
let sln_full = join_path(project_root, &fp.sln);
if !Path::new(&sln_full).exists() {
return None;
}
for c in &fp.csprojs {
if !Path::new(&join_path(project_root, c)).exists() {
return None;
}
}
Some(GenerateResult {
warnings: fp.warnings,
variant_csprojs: fp.csprojs,
variant_sln_path: fp.sln,
})
}
pub(crate) fn write_after_generate(
project_root: &str,
opts: &GenerateOptions,
result: &GenerateResult,
) {
let (canonical, fp_path) = fingerprint_path(project_root, opts);
let scan_cache = join_path(
&join_path(project_root, &opts.generator_root),
"scan-cache",
);
let lockfile = lockfile_path(project_root, &opts.generator_root);
let Some(scan_mtime) = mtime_nanos(&scan_cache) else {
return;
};
let Some(lock_mtime) = mtime_nanos(&lockfile) else {
return;
};
let stable_warnings: Vec<String> = result
.warnings
.iter()
.filter(|w| !w.starts_with("Unresolved: "))
.cloned()
.collect();
let fp = Fingerprint {
canonical_options: canonical,
scan_cache_mtime: scan_mtime,
lockfile_mtime: lock_mtime,
sln: result.variant_sln_path.clone(),
csprojs: result.variant_csprojs.clone(),
warnings: stable_warnings,
};
create_dir_all(parent_directory(&fp_path));
let _ = write_file_if_changed(&fp_path, &fp.serialize());
}
#[cfg(unix)]
fn mtime_nanos(path: &str) -> Option<u128> {
use std::os::unix::fs::MetadataExt;
let m = std::fs::metadata(path).ok()?;
let secs = m.mtime();
let nanos = m.mtime_nsec();
if secs < 0 {
return None;
}
Some((secs as u128) * 1_000_000_000 + (nanos as u128))
}
#[cfg(not(unix))]
fn mtime_nanos(path: &str) -> Option<u128> {
let m = std::fs::metadata(path).ok()?;
let mt = m.modified().ok()?;
let d = mt.duration_since(std::time::SystemTime::UNIX_EPOCH).ok()?;
Some(d.as_nanos())
}