use std::{
env,
fmt::Write as _,
fs,
path::{Path, PathBuf},
};
fn main() {
println!("cargo:rerun-if-changed=package-content");
let manifest = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("manifest directory"));
let content = manifest.join("package-content/content");
let roles = collect_roles(&content.join("roles"));
let skills = collect_skills(&content.join("skills"));
let predicates = collect_predicates(&content.join("predicates"));
let templates = collect_templates(&content.join("templates"));
assert!(!roles.is_empty(), "canonical content has zero roles");
assert!(!skills.is_empty(), "canonical content has zero skills");
assert!(
!predicates.is_empty(),
"canonical content has zero predicates"
);
let authored = manifest.join("../../content");
if authored.is_dir() {
println!("cargo:rerun-if-changed=../../content");
let authored_roles = collect_roles(&authored.join("roles"));
let authored_skills = collect_skills(&authored.join("skills"));
let authored_predicates = collect_predicates(&authored.join("predicates"));
let authored_templates = collect_templates(&authored.join("templates"));
assert_eq!(
snapshot(
&content,
[&roles, &skills, &predicates, &templates]
.into_iter()
.flatten(),
),
snapshot(
&authored,
[
&authored_roles,
&authored_skills,
&authored_predicates,
&authored_templates,
]
.into_iter()
.flatten(),
),
"generated compiler package content differs from authored root content"
);
}
let mut generated = String::from(
"// Generated by crates/compiler/build.rs from the canonical top-level content tree.\n\
// This file lives in OUT_DIR and must never be hand-edited.\n",
);
emit_array(&mut generated, "EMBEDDED_ROLES", &content, &roles);
emit_array(&mut generated, "EMBEDDED_SKILLS", &content, &skills);
emit_array(&mut generated, "EMBEDDED_PREDICATES", &content, &predicates);
emit_array(&mut generated, "EMBEDDED_TEMPLATES", &content, &templates);
let output = PathBuf::from(env::var_os("OUT_DIR").expect("output directory"))
.join("embedded_content.rs");
fs::write(output, generated).expect("write embedded canonical content");
}
fn collect_templates(directory: &Path) -> Vec<PathBuf> {
let handoff = directory.join("handoff.md");
let metadata = fs::symlink_metadata(&handoff).expect("templates must contain handoff.md");
assert!(
metadata.file_type().is_file() && !metadata.file_type().is_symlink(),
"handoff template is not a regular file: {}",
handoff.display()
);
vec![handoff]
}
fn snapshot<'a>(
content: &Path,
files: impl Iterator<Item = &'a PathBuf>,
) -> Vec<(String, Vec<u8>)> {
let mut snapshot = files
.map(|path| {
let logical = path
.strip_prefix(content)
.expect("content source below root")
.to_string_lossy()
.replace('\\', "/");
let raw = fs::read(path)
.unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
(logical, raw)
})
.collect::<Vec<_>>();
snapshot.sort();
snapshot
}
fn collect_roles(directory: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
for entry in read_directory(directory) {
let path = entry.path();
let kind = entry.file_type().expect("inspect role entry");
assert!(
kind.is_file() && !kind.is_symlink(),
"unexpected role entry: {}",
path.display()
);
assert_eq!(
path.extension().and_then(|value| value.to_str()),
Some("md"),
"role is not Markdown: {}",
path.display()
);
files.push(path);
}
files.sort();
files
}
fn collect_skills(directory: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
for entry in read_directory(directory) {
let path = entry.path();
let kind = entry.file_type().expect("inspect skill entry");
assert!(
kind.is_dir() && !kind.is_symlink(),
"unexpected skill entry: {}",
path.display()
);
let skill = path.join("SKILL.md");
let metadata = fs::symlink_metadata(&skill).expect("skill directory must contain SKILL.md");
assert!(
metadata.file_type().is_file(),
"skill source is not a regular file: {}",
skill.display()
);
files.push(skill);
}
files.sort();
files
}
fn collect_predicates(directory: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
for entry in read_directory(directory) {
let path = entry.path();
let kind = entry.file_type().expect("inspect predicate entry");
assert!(
kind.is_file() && !kind.is_symlink(),
"unexpected predicate entry: {}",
path.display()
);
assert_eq!(
path.extension().and_then(|value| value.to_str()),
Some("toml"),
"predicate is not TOML: {}",
path.display()
);
files.push(path);
}
files.sort();
files
}
fn read_directory(directory: &Path) -> Vec<fs::DirEntry> {
fs::read_dir(directory)
.unwrap_or_else(|error| panic!("cannot read {}: {error}", directory.display()))
.map(|entry| entry.expect("read canonical content entry"))
.collect()
}
fn emit_array(output: &mut String, name: &str, content: &Path, files: &[PathBuf]) {
writeln!(output, "pub(crate) const {name}: &[(&str, &str)] = &[").expect("write array");
let source_root = content.parent().expect("content parent");
for path in files {
let logical = path
.strip_prefix(source_root)
.expect("canonical source below root")
.to_string_lossy()
.replace('\\', "/");
let raw = fs::read_to_string(path)
.unwrap_or_else(|error| panic!("{} is not UTF-8: {error}", path.display()));
writeln!(output, " ({logical:?}, {raw:?}),").expect("write embedded entry");
}
writeln!(output, "];\n").expect("finish array");
}