use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::OnceLock;
use std::time::{Duration, Instant};
fn bootstrap_target_dir() -> PathBuf {
let mut dir = std::env::current_exe().expect("test binary path");
dir.pop(); dir.pop(); dir.pop(); dir.join("kache-test-bootstrap")
}
fn reuse_outer_build() -> bool {
std::env::var_os("KACHE_TEST_USE_CARGO_BIN_EXE").is_some_and(|value| value == "1")
}
static KACHE_BIN: OnceLock<Result<PathBuf, String>> = OnceLock::new();
pub fn build_kache() {
let _ = kache_binary();
}
pub fn kache_binary() -> PathBuf {
match KACHE_BIN.get_or_init(bootstrap_kache) {
Ok(path) => path.clone(),
Err(message) => panic!("{message}"),
}
}
fn bootstrap_kache() -> Result<PathBuf, String> {
if reuse_outer_build() {
return Ok(PathBuf::from(env!("CARGO_BIN_EXE_kache")));
}
let target_dir = bootstrap_target_dir();
let output = std::process::Command::new("cargo")
.args([
"build",
"--bin",
"kache",
"--target-dir",
target_dir.to_str().expect("bootstrap target dir is utf-8"),
"--config",
"build.rustc-wrapper=\"\"",
])
.env_remove("RUSTC_WRAPPER")
.env_remove("CARGO_BUILD_RUSTC_WRAPPER")
.output()
.map_err(|e| format!("failed to spawn cargo to build kache: {e}"))?;
if !output.status.success() {
return Err(format!(
"kache build failed ({}) in {}\nstdout: {}\nstderr: {}",
output.status,
target_dir.display(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
));
}
let mut bin = target_dir.join("debug").join("kache");
if cfg!(windows) {
bin.set_extension("exe");
}
if !bin.is_file() {
return Err(format!(
"kache build reported success but {} is missing",
bin.display()
));
}
Ok(bin)
}
#[allow(dead_code)]
pub const SCAN_MACRO: &str = r#"extern crate proc_macro;
#[proc_macro]
pub fn scan(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let assets = std::path::Path::new(&manifest_dir).join("assets");
let mut files: Vec<std::path::PathBuf> = std::fs::read_dir(assets)
.map(|entries| entries.flatten().map(|entry| entry.path()).collect())
.unwrap_or_default();
files.sort();
let mut code = String::from("pub const ASSETS: &[&str] = &[");
for file in files {
code.push_str(&format!("include_str!({:?}),", file.display().to_string()));
}
code.push_str("];");
code.parse().unwrap()
}
"#;
pub fn isolated_config_path(cache_dir: &Path) -> PathBuf {
cache_dir.join("config.toml")
}
#[allow(dead_code)]
pub fn hermetic_command(
program: impl AsRef<OsStr>,
cache_dir: &Path,
config: Option<&Path>,
) -> Command {
let mut command = Command::new(program);
command
.env("KACHE_CACHE_DIR", cache_dir)
.env("KACHE_RUNTIME_DIR", cache_dir)
.env_remove("KACHE_SOCKET_PATH")
.env_remove("KACHE_EVENT_ROOT")
.env_remove("KACHE_ACTIVE")
.env_remove("RUSTC_WRAPPER")
.env_remove("CARGO_BUILD_RUSTC_WRAPPER")
.env_remove("OUT_DIR")
.env("KACHE_HOST_CONFIG", "");
match config {
Some(path) => command.env("KACHE_CONFIG", path),
None => command.env_remove("KACHE_CONFIG"),
};
command
}
#[allow(dead_code)]
pub fn scratch_dir() -> PathBuf {
let dir = match std::env::var_os("KACHE_TEST_SCRATCH_DIR") {
Some(dir) => PathBuf::from(dir),
None => bootstrap_target_dir().with_file_name("kache-test-scratch"),
};
std::fs::create_dir_all(&dir).expect("creating scratch dir");
dir
}
#[allow(dead_code)]
pub fn settle_writes(dirs: &[&Path]) {
use kache_store::file_hash::{
FINE_STAMP_WINDOW_NS, metadata_ctime_ns, metadata_mtime_ns, stamp_clock_ns,
stamp_window_ns, wall_clock_ns,
};
fn collect(path: &Path, stamps: &mut Vec<i64>) {
let Ok(link) = std::fs::symlink_metadata(path) else {
return;
};
if link.is_dir() {
for entry in std::fs::read_dir(path).into_iter().flatten().flatten() {
collect(&entry.path(), stamps);
}
} else if let Ok(metadata) = std::fs::metadata(path) {
stamps.extend([metadata_mtime_ns(&metadata), metadata_ctime_ns(&metadata)]);
}
}
let now = wall_clock_ns();
let mut stamps = Vec::new();
for dir in dirs {
collect(dir, &mut stamps);
}
let settled_at = stamps
.into_iter()
.filter(|&stamp| stamp <= now)
.map(|stamp| stamp.saturating_add(stamp_window_ns(stamp)))
.fold(
stamp_clock_ns().saturating_add(FINE_STAMP_WINDOW_NS),
i64::max,
);
while stamp_clock_ns() <= settled_at {
std::thread::sleep(Duration::from_millis(1));
}
}
fn daemon_run_lock_path(runtime_dir: &Path) -> PathBuf {
runtime_dir.join("daemon.run.lock")
}
fn daemon_run_lock_is_held(runtime_dir: &Path) -> bool {
let file = match std::fs::OpenOptions::new()
.write(true)
.open(daemon_run_lock_path(runtime_dir))
{
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return false,
Err(_) => return true,
};
match file.try_lock() {
Ok(()) => {
let _ = file.unlock();
false
}
Err(_) => true,
}
}
#[allow(dead_code)]
pub fn wait_for_daemon_exit(runtime_dir: &Path, timeout: Duration) -> Result<(), String> {
let deadline = Instant::now() + timeout;
while daemon_run_lock_is_held(runtime_dir) {
if Instant::now() >= deadline {
return Err(format!(
"daemon lifetime lock {} remained held after its drain phase",
daemon_run_lock_path(runtime_dir).display()
));
}
std::thread::sleep(Duration::from_millis(100));
}
Ok(())
}
#[allow(dead_code)]
pub fn stop_daemon(stop: &mut Command, runtime_dir: &Path) {
if !daemon_run_lock_is_held(runtime_dir) {
return;
}
let spawned = stop
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
if let Ok(mut child) = spawned {
let deadline = Instant::now() + Duration::from_secs(45);
loop {
match child.try_wait() {
Ok(None) if Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(50));
}
Ok(Some(_)) => break,
Ok(None) | Err(_) => {
let _ = child.kill();
let _ = child.wait();
break;
}
}
}
}
let _ = wait_for_daemon_exit(runtime_dir, Duration::from_secs(45));
}