Theme

alex/volcanic

Automated vulkan bindings

BRANCH main
main
SNAPSHOT Improved loading api and made ready for publish
DOWNLOAD
Download file

root / codegen / src / output.rs

use crate::Result;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};

pub fn format_rust(source: &str) -> Result<String> {
    let mut child = Command::new("rustfmt")
        .args([
            "--edition",
            "2024",
            "--emit",
            "stdout",
            "--config",
            "skip_children=true",
        ])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;
    let mut stdin = child.stdin.take().expect("piped rustfmt stdin");
    let output = std::thread::scope(|scope| -> Result<_> {
        // Drain stdout while feeding large generated files to rustfmt
        let writer = scope.spawn(move || stdin.write_all(source.as_bytes()));
        let output = child.wait_with_output()?;
        let write_result = writer.join().expect("rustfmt writer panicked");
        if !output.status.success() {
            return Err(format!(
                "rustfmt failed: {}",
                String::from_utf8_lossy(&output.stderr)
            )
            .into());
        }
        write_result?;
        Ok(output)
    })?;
    Ok(String::from_utf8(output.stdout)?)
}

pub fn write_outputs(outputs: &[(PathBuf, String)], check: bool) -> Result<()> {
    let mut stale = Vec::new();
    for (path, expected) in outputs {
        let current = match std::fs::read_to_string(path) {
            Ok(current) => Some(current),
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
            Err(error) => return Err(error.into()),
        };
        if current.as_deref() == Some(expected) {
            continue;
        }
        if check {
            stale.push(path.display().to_string());
        } else {
            std::fs::write(path, expected)?;
        }
    }
    if stale.is_empty() {
        Ok(())
    } else {
        Err(format!("generated files are stale: {}", stale.join(", ")).into())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn check_reports_stale_files_without_writing() {
        let path = std::env::temp_dir().join(format!(
            "volcanic-output-test-{}-{}",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        let mut file = std::fs::File::create_new(&path).unwrap();
        struct Cleanup(PathBuf);
        impl Drop for Cleanup {
            fn drop(&mut self) {
                let _ = std::fs::remove_file(&self.0);
            }
        }
        let _cleanup = Cleanup(path.clone());
        file.write_all(b"old").unwrap();
        drop(file);
        let outputs = [(path.clone(), "new".to_string())];
        let error = write_outputs(&outputs, true).unwrap_err();
        assert!(error.to_string().contains("generated files are stale"));
        assert_eq!(std::fs::read_to_string(&path).unwrap(), "old");
        write_outputs(&outputs, false).unwrap();
        assert_eq!(std::fs::read_to_string(&path).unwrap(), "new");
        write_outputs(&outputs, true).unwrap();
    }

    #[test]
    fn formatting_rejects_invalid_generated_rust() {
        assert!(format_rust("fn invalid(").is_err());
    }
}