use std::collections::hash_map::DefaultHasher;
use std::fmt::Write as _;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::process::Command;
fn git(args: &[&str]) -> Option<Vec<u8>> {
Command::new("git")
.args(args)
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| o.stdout)
}
fn dirty_hash() -> Option<String> {
let status = git(&["status", "--porcelain"])?;
if status.is_empty() {
return None; }
let diff = git(&["diff", "HEAD"]).unwrap_or_default();
let mut hasher = DefaultHasher::new();
status.hash(&mut hasher);
diff.hash(&mut hasher);
Some(format!("{:08x}", hasher.finish() & 0xffff_ffff))
}
fn agent_files(dir: &Path) -> Vec<(String, PathBuf)> {
let mut out = Vec::new();
collect_files(dir, dir, &mut out);
out.sort_by(|a, b| a.0.cmp(&b.0));
out
}
fn collect_files(root: &Path, dir: &Path, out: &mut Vec<(String, PathBuf)>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_files(root, &path, out);
} else if let Ok(rel) = path.strip_prefix(root) {
let rel = rel
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/");
out.push((rel, path));
}
}
}
fn manifest_version(manifest: &str, path: &Path) -> String {
manifest
.lines()
.find_map(|line| {
let line = line.trim();
let rest = line.strip_prefix("version")?.trim_start();
let rest = rest.strip_prefix('=')?.trim();
rest.strip_prefix('"')?.split('"').next().map(str::to_owned)
})
.unwrap_or_else(|| {
panic!(
"no top-level `version = \"...\"` in {} -- every bundled \
blueprint needs one for `lev setup` to plan installs",
path.display()
)
})
}
fn write_bundled_agents(out_dir: &Path, agents_dir: &Path) {
let mut src = String::from(
"// @generated by build.rs -- do not edit.\n\
/// One blueprint embedded in the binary.\n\
#[derive(Debug)]\n\
pub struct BundledAgent {\n\
\x20 /// Blueprint name, from the manifest's `[agent] name`.\n\
\x20 pub name: &'static str,\n\
\x20 /// Manifest `version`, used to plan install vs. update.\n\
\x20 pub version: &'static str,\n\
\x20 /// `(path relative to the agent dir, file contents)`, sorted.\n\
\x20 pub files: &'static [(&'static str, &'static str)],\n\
}\n\n\
/// Every blueprint shipped with this binary, sorted by name.\n\
pub static BUNDLED_AGENTS: &[BundledAgent] = &[\n",
);
let mut dirs: Vec<PathBuf> = std::fs::read_dir(agents_dir)
.into_iter()
.flatten()
.flatten()
.map(|e| e.path())
.filter(|p| p.is_dir())
.collect();
dirs.sort();
for dir in dirs {
let manifest_path = dir.join("agent.leviath");
let Ok(manifest) = std::fs::read_to_string(&manifest_path) else {
continue;
};
let name = dir
.file_name()
.and_then(|n| n.to_str())
.expect("agent directory names are valid UTF-8")
.to_string();
let version = manifest_version(&manifest, &manifest_path);
writeln!(
src,
" BundledAgent {{\n name: {name:?},\n version: {version:?},\n files: &[",
)
.expect("writing to a String cannot fail");
for (rel, abs) in agent_files(&dir) {
writeln!(
src,
" ({rel:?}, include_str!({:?})),",
abs.display().to_string()
)
.expect("writing to a String cannot fail");
}
src.push_str(" ],\n },\n");
}
src.push_str("];\n");
std::fs::write(out_dir.join("bundled_agents.rs"), src)
.expect("failed to write the generated bundled_agents.rs");
}
fn main() {
let hash = git(&["rev-parse", "--short", "HEAD"])
.and_then(|o| String::from_utf8(o).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let build = match hash {
Some(hash) => match dirty_hash() {
Some(dirty) => format!("{hash}-dirty-{dirty}"),
None => hash,
},
None => format!("v{}", env!("CARGO_PKG_VERSION")),
};
println!("cargo:rustc-env=LEVIATH_BUILD={build}");
let manifest_dir =
PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("cargo sets CARGO_MANIFEST_DIR"));
let agents_dir = manifest_dir.join("agents");
let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("cargo sets OUT_DIR"));
write_bundled_agents(&out_dir, &agents_dir);
println!("cargo:rerun-if-changed=.leviath-build-always-rerun");
}