use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use jacquard_lexicon::codegen::{CodeGenerator, CodegenMode};
use jacquard_lexicon::corpus::LexiconCorpus;
use walkdir::WalkDir;
const LEXICONS_SUBDIR: &str = "lexicons";
const STAGED_SUBDIR: &str = "lexicons-staged";
const GENERATED_SUBDIR: &str = "_lex";
const GENERATED_ROOT_FILE: &str = "_lex_root.rs";
const SKIPPED_SEGMENTS: [&str; 2] = ["temp", "legacy"];
fn main() -> Result<()> {
let lexicons_dir = match std::env::var("TANGLED_LEXICONS_DIR") {
Ok(dir) => PathBuf::from(dir),
Err(_) => {
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?);
manifest_dir
.parent()
.and_then(Path::parent)
.and_then(Path::parent)
.context("resolve workspace root from manifest dir")?
.join(LEXICONS_SUBDIR)
}
};
println!("cargo:rerun-if-env-changed=TANGLED_LEXICONS_DIR");
println!("cargo:rerun-if-changed={}", lexicons_dir.display());
println!("cargo:rerun-if-changed=build.rs");
let out_dir = PathBuf::from(std::env::var("OUT_DIR")?);
let staged = out_dir.join(STAGED_SUBDIR);
if staged.exists() {
std::fs::remove_dir_all(&staged).context("clean staged lexicons")?;
}
stage_lexicons(&lexicons_dir, &staged)?;
let corpus = LexiconCorpus::load_from_dir(&staged)
.map_err(|e| anyhow::anyhow!("load lexicon corpus: {e:?}"))?;
let generated = out_dir.join(GENERATED_SUBDIR);
if generated.exists() {
std::fs::remove_dir_all(&generated).context("clean generated dir")?;
}
std::fs::create_dir_all(&generated).context("create generated dir")?;
let codegen = CodeGenerator::with_mode(&corpus, "crate", CodegenMode::Pretty);
codegen
.write_to_disk(&generated)
.map_err(|e| anyhow::anyhow!("write generated code: {e:?}"))?;
write_generated_root(&generated, &out_dir.join(GENERATED_ROOT_FILE))?;
Ok(())
}
/// absolute #[path] so the generated `mod x;` files resolve inside OUT_DIR, not next to lib.rs
fn write_generated_root(generated: &Path, path: &Path) -> Result<()> {
let source = format!(
"#[path = {:?}]\nmod generated;\npub use generated::*;\n",
generated.join("lib.rs")
);
std::fs::write(path, source).context("write generated root")
}
fn stage_lexicons(src: &Path, dst: &Path) -> Result<()> {
WalkDir::new(src)
.into_iter()
.filter_map(std::result::Result::ok)
.filter(|entry| entry.file_type().is_file())
.filter(|entry| {
entry
.path()
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
})
.filter(|entry| {
!entry.path().components().any(|component| {
SKIPPED_SEGMENTS
.iter()
.any(|skip| component.as_os_str() == *skip)
})
})
.try_for_each(|entry| -> Result<()> {
let staged_namespace =
std::fs::read(entry.path()).context("read lexicon for staging")?;
let staged_namespace = serde_json::from_slice::<serde_json::Value>(&staged_namespace)
.context("parse lexicon for staging")?;
let staged_namespace = staged_namespace["id"].as_str().unwrap_or_default();
if !staged_namespace.starts_with("com.atproto.")
&& !staged_namespace.starts_with("org.tangled.")
{
return Ok(());
}
let rel = entry.path().strip_prefix(src).context("strip src prefix")?;
let target = dst.join(rel);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).context("create staged parent")?;
}
std::fs::copy(entry.path(), &target).context("copy lexicon file")?;
Ok(())
})
}