codegenrs/
lib.rs

1//! # codegenrs
2//!
3//! > **Moving code-gen our of `build.rs`**
4//!
5//! ## About
6//!
7//! `codegenrs` makes it easy to get rid of code-gen in `build.rs`, reducing your
8//! and dependents' build times.  This is done by:
9//! - Creating a child `[[bin]]` crate that does code-gen using `codegenrs`
10//! - Do one-time code-gen and commit it
11//! - Run the `--check` step in your CI to ensure your code-gen is neither out of
12//!   date or been human edited.
13//!
14//!## Usage
15//!
16//!`imperative` example:
17//! - output: [`wordlist_codegen.rs`](https://github.com/crate-ci/imperative/blob/master/src/wordlist_codegen.rs)
18//! - generator: [`imperative-codegen`](https://github.com/crate-ci/imperative/tree/master/tests/codegen.rs)
19
20#![cfg_attr(docsrs, feature(doc_auto_cfg))]
21#![warn(clippy::print_stderr)]
22#![warn(clippy::print_stdout)]
23
24use std::io::Write;
25
26#[cfg(feature = "clap")]
27use clap::Args;
28
29/// CLI arguments to `flatten` into your args
30///
31/// ## Example
32///
33/// ```rust
34/// #[derive(clap::Parser)]
35/// struct Args{
36///    #[arg(short('i'), long)]
37///    input: std::path::PathBuf,
38///    #[command(flatten)]
39///    codegen: codegenrs::CodeGenArgs,
40/// }
41/// ```
42#[cfg(feature = "clap")]
43#[derive(Debug, Args)]
44pub struct CodeGenArgs {
45    #[arg(short('o'), long)]
46    output: std::path::PathBuf,
47
48    #[arg(long)]
49    check: bool,
50}
51
52#[cfg(feature = "clap")]
53impl CodeGenArgs {
54    /// Write or verify code-genned text.
55    pub fn write_str(&self, content: &str) -> Result<(), Box<dyn std::error::Error>> {
56        write_str(content, &self.output, self.check)
57    }
58}
59
60/// Write or verify code-genned text.
61///
62/// See `CodeGenArgs` for `clap` integration.
63pub fn write_str(
64    content: &str,
65    output: &std::path::Path,
66    check: bool,
67) -> Result<(), Box<dyn std::error::Error>> {
68    if check {
69        let content: String = normalize_line_endings::normalized(content.chars()).collect();
70
71        let actual = std::fs::read_to_string(output)?;
72        let actual: String = normalize_line_endings::normalized(actual.chars()).collect();
73
74        if content != actual {
75            // `difference` will allocation a `Vec` with  N*M elements.
76            let allocation = content.lines().count() * actual.lines().count();
77            if 1_000_000_000 < allocation {
78                return Err(Box::new(CodeGenError {
79                    message: format!("{} out of sync (too big to diff)", output.display()),
80                }));
81            } else {
82                let changeset = difference::Changeset::new(&actual, &content, "\n");
83                assert_ne!(changeset.distance, 0);
84                return Err(Box::new(CodeGenError {
85                    message: format!("{} out of sync:\n{changeset}", output.display()),
86                }));
87            }
88        }
89    } else {
90        let mut file = std::io::BufWriter::new(std::fs::File::create(output)?);
91        write!(file, "{content}")?;
92    }
93
94    Ok(())
95}
96
97/// CLI arguments to `flatten` into your args
98///
99/// ## Example
100///
101/// ```rust
102/// #[derive(clap::Parser)]
103/// struct Args{
104///    #[arg(short('i'), long)]
105///    input: std::path::PathBuf,
106///    #[command(flatten)]
107///    codegen: codegenrs::CodeGenArgs,
108///    #[command(flatten)]
109///    rustfmt: codegenrs::RustfmtArgs,
110/// }
111/// ```
112#[cfg(feature = "clap")]
113#[derive(Debug, Args)]
114pub struct RustfmtArgs {
115    #[arg(long)]
116    rustfmt_config: Option<std::path::PathBuf>,
117}
118
119#[cfg(feature = "clap")]
120impl RustfmtArgs {
121    /// Write or verify code-genned text.
122    pub fn reformat(
123        &self,
124        text: impl std::fmt::Display,
125    ) -> Result<String, Box<dyn std::error::Error>> {
126        rustfmt(text, self.rustfmt_config.as_deref())
127    }
128}
129
130/// Run `rustfmt` on an in-memory string
131pub fn rustfmt(
132    text: impl std::fmt::Display,
133    config: Option<&std::path::Path>,
134) -> Result<String, Box<dyn std::error::Error>> {
135    let mut rustfmt = std::process::Command::new("rustfmt");
136    rustfmt
137        .stdin(std::process::Stdio::piped())
138        .stdout(std::process::Stdio::piped());
139    if let Some(config) = config {
140        rustfmt.arg("--config-path").arg(config);
141    }
142    let mut rustfmt = rustfmt
143        .spawn()
144        .map_err(|err| format!("could not run `rustfmt`: {err}"))?;
145    write!(
146        rustfmt
147            .stdin
148            .take()
149            .expect("rustfmt was configured with stdin"),
150        "{text}"
151    )?;
152    let output = rustfmt.wait_with_output()?;
153    let stdout = String::from_utf8(output.stdout)?;
154    Ok(stdout)
155}
156
157#[derive(Clone, Debug)]
158struct CodeGenError {
159    message: String,
160}
161
162impl std::error::Error for CodeGenError {}
163
164impl std::fmt::Display for CodeGenError {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        self.message.fmt(f)
167    }
168}