Something went wrong. Try again.
Rust CLI for tangled
Something went wrong. Try again.
5.3 kB · 178 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179//! Workspace automation. Currently one command://!//! ```sh//! cargo xtask codegen [--check]//! ```//!//! Regenerates `crates/tangled-api/src/lexicon/` from the vendored lexicon//! JSON files in `lexicons/`. With `--check`, no files are written; instead//! the command exits non-zero if the committed generated code is out of date//! (or contains stray files).
use xtask::codegen;
use std::collections::BTreeMap;use std::path::{Path, PathBuf};use std::process::Command;
use anyhow::{bail, Context, Result};
fn main() -> Result<()> { let args: Vec<String> = std::env::args().skip(1).collect(); match args.first().map(String::as_str) { Some("codegen") => { let check = args.iter().any(|a| a == "--check"); run_codegen(check) } _ => { eprintln!("usage: cargo xtask codegen [--check]"); std::process::exit(2); } }}
fn workspace_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .parent() .expect("xtask sits directly under the workspace root") .to_path_buf()}
fn run_codegen(check: bool) -> Result<()> { let root = workspace_root(); let lexicons_dir = root.join("lexicons"); let out_dir = root.join("crates/tangled-api/src/lexicon");
let generated = codegen::generate(&lexicons_dir)?; for w in &generated.warnings { eprintln!("warning: {w}"); }
let files = rustfmt_all(generated.files)?;
let existing = read_tree(&out_dir)?; let mut stale = Vec::new(); for (path, content) in &files { if existing.get(path) != Some(content) { stale.push(path.clone()); } } let stray: Vec<_> = existing .keys() .filter(|p| !files.contains_key(*p)) .cloned() .collect();
if check { if stale.is_empty() && stray.is_empty() { println!( "generated code is up to date ({} files)", files.len() ); return Ok(()); } for p in &stale { eprintln!("out of date: {}", out_dir.join(p).display()); } for p in &stray { eprintln!("stray file: {}", out_dir.join(p).display()); } bail!("generated code is out of date; run `cargo xtask codegen`"); }
for (path, content) in &files { let full = out_dir.join(path); if let Some(parent) = full.parent() { std::fs::create_dir_all(parent)?; } if existing.get(path) != Some(content) { std::fs::write(&full, content) .with_context(|| format!("writing {}", full.display()))?; } } for p in &stray { let full = out_dir.join(p); std::fs::remove_file(&full).with_context(|| format!("removing {}", full.display()))?; } remove_empty_dirs(&out_dir)?; println!( "wrote {} files ({} changed, {} removed)", files.len(), stale.len(), stray.len() ); Ok(())}
/// Formats each generated file with rustfmt so output is stable and readable.fn rustfmt_all(files: BTreeMap<PathBuf, String>) -> Result<BTreeMap<PathBuf, String>> { let mut out = BTreeMap::new(); for (path, content) in files { let formatted = rustfmt(&content) .with_context(|| format!("rustfmt failed on generated file {}", path.display()))?; out.insert(path, formatted); } Ok(out)}
fn rustfmt(source: &str) -> Result<String> { use std::io::Write; let mut child = Command::new("rustfmt") .args(["--edition", "2021", "--emit", "stdout"]) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .context("spawning rustfmt (is it installed?)")?; child .stdin .as_mut() .unwrap() .write_all(source.as_bytes())?; let output = child.wait_with_output()?; if !output.status.success() { bail!( "rustfmt exited with {}: {}", output.status, String::from_utf8_lossy(&output.stderr) ); } Ok(String::from_utf8(output.stdout)?)}
fn read_tree(dir: &Path) -> Result<BTreeMap<PathBuf, String>> { let mut out = BTreeMap::new(); if !dir.exists() { return Ok(out); } fn walk(root: &Path, dir: &Path, out: &mut BTreeMap<PathBuf, String>) -> Result<()> { for entry in std::fs::read_dir(dir)? { let path = entry?.path(); if path.is_dir() { walk(root, &path, out)?; } else { let rel = path.strip_prefix(root).unwrap().to_path_buf(); out.insert(rel, std::fs::read_to_string(&path)?); } } Ok(()) } walk(dir, dir, &mut out)?; Ok(out)}
fn remove_empty_dirs(dir: &Path) -> Result<()> { if !dir.exists() { return Ok(()); } for entry in std::fs::read_dir(dir)? { let path = entry?.path(); if path.is_dir() { remove_empty_dirs(&path)?; if std::fs::read_dir(&path)?.next().is_none() { std::fs::remove_dir(&path)?; } } } Ok(())}