use std::fmt::Write as _;
use std::fs;
use std::path::Path;
#[path = "src/domain/manifest.rs"]
pub mod manifest;
#[path = "src/generator/schema_model.rs"]
pub mod schema_model;
#[path = "src/generator/emit.rs"]
pub mod emit;
use manifest::{Manifest, hex_digest, normalize_line_endings};
fn main() {
let spec_root = Path::new("spec");
let out_dir_var = std::env::var("OUT_DIR").unwrap_or_else(|_| fail("OUT_DIR is unset"));
let out_dir = Path::new(&out_dir_var);
println!("cargo:rerun-if-changed=build.rs");
let manifest_path = spec_root.join("MANIFEST.toml");
println!("cargo:rerun-if-changed={}", manifest_path.display());
let source = match fs::read_to_string(&manifest_path) {
Ok(text) => text,
Err(err) => fail(&format!(
"could not read {}: {err}",
manifest_path.display()
)),
};
let manifest = match Manifest::parse(&source) {
Ok(parsed) => parsed,
Err(err) => fail(&format!("{err}")),
};
for name in manifest.revision_names() {
println!("cargo:rerun-if-changed=spec/{name}");
}
let cited_dir = spec_root.join(manifest::CITED_DOCUMENT_DIR);
println!("cargo:rerun-if-changed={}", cited_dir.display());
for document in manifest.rfc_documents.values() {
println!(
"cargo:rerun-if-changed={}",
cited_dir.join(&document.file).display()
);
}
for (name, spec) in &manifest.revisions {
if spec.is_floating() && !spec.tag.is_empty() {
fail(&format!(
"revision `{name}` is marked floating but also records tag `{}`; a floating \
revision has no upstream tag",
spec.tag
));
}
if !spec.is_floating() && spec.tag != *name {
fail(&format!(
"revision `{name}` records tag `{}`, which does not match its directory name",
spec.tag
));
}
for required in manifest::RevisionSpec::SCHEMA_FILES {
if !spec.files.contains_key(required) {
fail(&format!(
"revision `{name}` does not record {required}; every revision must vendor both \
schema representations"
));
}
}
for file in spec.files.keys() {
if let Err(err) = spec.upstream_path(file) {
fail(&format!("revision `{name}`: {err}"));
}
}
}
if let Err(err) = manifest.verify(
|revision, file| {
let path = spec_root.join(revision).join(file);
fs::read(&path)
.map(|bytes| hex_digest(&normalize_line_endings(&bytes)))
.map_err(|err| format!("{}: {err}", path.display()))
},
|revision| {
let dir = spec_root.join(revision);
let Ok(entries) = fs::read_dir(&dir) else {
return Vec::new();
};
let mut names: Vec<String> = entries
.filter_map(Result::ok)
.filter(|entry| entry.file_type().is_ok_and(|kind| kind.is_file()))
.filter_map(|entry| entry.file_name().into_string().ok())
.collect();
names.sort();
names
},
) {
fail(&format!("{err}"));
}
verify_cited_documents(&manifest, &cited_dir);
generate_wire_types(&manifest, spec_root, out_dir);
}
fn verify_cited_documents(manifest: &Manifest, cited_dir: &Path) {
if let Err(err) = manifest.verify_rfc_documents(
|file| {
let path = cited_dir.join(file);
fs::read(&path)
.map(|bytes| hex_digest(&normalize_line_endings(&bytes)))
.map_err(|err| format!("{}: {err}", path.display()))
},
|| {
let Ok(entries) = fs::read_dir(cited_dir) else {
return Vec::new();
};
let mut names: Vec<String> = entries
.filter_map(Result::ok)
.filter(|entry| entry.file_type().is_ok_and(|kind| kind.is_file()))
.filter_map(|entry| entry.file_name().into_string().ok())
.collect();
names.sort();
names
},
) {
fail(&format!("{err}"));
}
}
fn generate_wire_types(manifest: &Manifest, spec_root: &Path, out_dir: &Path) {
let mut index = String::with_capacity(4096);
let () = index.push_str("// GENERATED — do not edit. Produced by `build.rs`.\n");
let () = index.push_str("//\n");
let () = index.push_str("// One module per vendored revision. Every one is compiled\n");
let () = index.push_str("// unconditionally: which revision a process speaks is decided at\n");
let () =
index.push_str("// runtime by negotiation, never at compile time by a feature flag.\n");
let () = index.push_str("//\n");
let () = index.push_str("// Each module exposes the same top-level type names, so the codec\n");
let () = index.push_str("// layer can instantiate one implementation per revision against a\n");
let () = index.push_str("// uniform set of names.\n\n");
for revision in manifest.revision_names() {
let path = spec_root.join(revision).join("schema.json");
let text = match fs::read_to_string(&path) {
Ok(text) => text,
Err(err) => fail(&format!("could not read {}: {err}", path.display())),
};
let parsed = match schema_model::parse(revision, &text) {
Ok(parsed) => parsed,
Err(err) => fail(&format!("{err}")),
};
let generated = emit::emit(&parsed);
if !generated.dangling_refs.is_empty() {
fail(&format!(
"{revision}: `$ref` to definitions that do not exist: {:?}",
generated.dangling_refs
));
}
let module = emit::module_ident(revision);
let target = out_dir.join(format!("{module}.rs"));
if let Err(err) = fs::write(&target, &generated.source) {
fail(&format!("could not write {}: {err}", target.display()));
}
let _ = writeln!(index, "pub mod {module};");
}
let index_path = out_dir.join("wire.rs");
if let Err(err) = fs::write(&index_path, index) {
fail(&format!("could not write {}: {err}", index_path.display()));
}
}
fn fail(message: &str) -> ! {
eprintln!("error: vendored spec tree is invalid: {message}");
eprintln!();
eprintln!(" A revision is re-downloaded with `cargo xtask refresh-spec <revision>`,");
eprintln!(" and a cited document with `cargo xtask refresh-cited <document>`.");
eprintln!(" Either rewrites the manifest's digests; review `git diff` afterwards.");
std::process::exit(1);
}