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, skill_resources) = 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, authored_skill_resources) = 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, &skill_resources, &predicates, &templates]
.into_iter()
.flatten(),
),
snapshot(
&authored,
[
&authored_roles,
&authored_skills,
&authored_skill_resources,
&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_resource_array(
&mut generated,
"EMBEDDED_SKILL_RESOURCES",
&content,
&skill_resources,
);
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>, Vec<PathBuf>) {
let mut skills = Vec::new();
let mut resources = 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() && !metadata.file_type().is_symlink(),
"skill source is not a regular file: {}",
skill.display()
);
skills.push(skill);
for resource_directory in ["assets", "references", "scripts"] {
let resource_root = path.join(resource_directory);
let metadata = match fs::symlink_metadata(&resource_root) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => panic!(
"cannot inspect skill resource category {}: {error}",
resource_root.display()
),
};
assert!(
metadata.file_type().is_dir() && !metadata.file_type().is_symlink(),
"skill resource category is not a regular directory: {}",
resource_root.display()
);
for resource in read_directory(&resource_root) {
let resource_path = resource.path();
let resource_type = resource.file_type().expect("inspect skill resource");
assert!(
resource_type.is_file() && !resource_type.is_symlink(),
"skill resource must be one regular file below its category: {}",
resource_path.display()
);
resources.push(resource_path);
}
}
for child in read_directory(&path) {
let child_path = child.path();
let child_name = child.file_name();
let child_name = child_name.to_str().expect("UTF-8 skill entry");
assert!(
child_name == "SKILL.md"
|| ["assets", "references", "scripts"].contains(&child_name),
"unexpected skill entry: {}",
child_path.display()
);
}
}
skills.sort();
resources.sort();
(skills, resources)
}
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");
}
fn emit_resource_array(output: &mut String, name: &str, content: &Path, files: &[PathBuf]) {
writeln!(
output,
"pub(crate) const {name}: &[(&str, &str, bool)] = &["
)
.expect("write resource array");
let source_root = content.parent().expect("content parent");
for path in files {
let logical = path
.strip_prefix(source_root)
.expect("canonical resource 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()));
let executable = path
.parent()
.and_then(Path::file_name)
.and_then(|value| value.to_str())
== Some("scripts");
writeln!(output, " ({logical:?}, {raw:?}, {executable}),")
.expect("write embedded resource");
}
writeln!(output, "];\n").expect("finish resource array");
}