Skip to main content

imping_build/
lib.rs

1//! Generates Rust bindings from imping IR files, from a build script or the
2//! command line.
3//!
4//! [`Builder`] reads the IR file of each build of the target, generates one set
5//! of bindings for them with `imping-codegen` and writes it. Failures are an
6//! [`Error`] naming the file involved.
7//!
8//! # From a build script
9//!
10//! ```no_run
11//! // build.rs
12//! fn main() -> Result<(), imping_build::Error> {
13//!     imping_build::Builder::new()
14//!         .ir("steam", "ir/target.json")
15//!         .generate()
16//! }
17//! ```
18//!
19//! Each IR file gets a label, which names its build in the bindings: here
20//! `target::Build::Steam` and constants such as `RVA_STEAM`. For several
21//! builds, add an `ir` per build; see
22//! [Several builds](imping_codegen#several-builds).
23//!
24//! [`Builder::generate`] writes `$OUT_DIR/imped.rs` for the crate to include:
25//!
26//! ```ignore
27//! include!(concat!(env!("OUT_DIR"), "/imped.rs"));
28//! ```
29//!
30//! # From the command line
31//!
32//! With the `cli` feature, this crate also builds the `imping` tool:
33//!
34//! ```text
35//! cargo install --path crates/imping-build --features cli
36//! imping generate --ir steam=ir/target.json -o src/imped.rs --module-name TargetCore.dll=target
37//! imping generate --ir steam_2021=ir/2021.json --ir steam_2026=ir/2026.json -o src/imped.rs
38//! imping generate --ir steam=ir/target.json --accept-build steam:target.exe=0x64b8f2a1:0xa3c000
39//! imping check --ir steam=ir/target.json
40//! ```
41
42mod error;
43
44#[cfg(feature = "cli")]
45#[doc(hidden)]
46pub mod __cli;
47
48pub use crate::error::Error;
49
50use imping_codegen::{Generator, Warning};
51use imping_ir::Ir;
52use std::path::{Path, PathBuf};
53
54/// The default name of the file [`Builder::generate`] writes to `OUT_DIR`.
55const DEFAULT_OUT_FILE: &str = "imped.rs";
56
57/// Generates one set of bindings from the IR files of one or more builds.
58///
59/// Create one with [`Builder::new`], add each build's IR file with
60/// [`ir`](Self::ir), optionally configure it, then call
61/// [`generate`](Self::generate) from a build script or
62/// [`generate_to`](Self::generate_to) anywhere else.
63///
64/// # Examples
65///
66/// ```no_run
67/// // build.rs, for two builds of `target.exe`, the first also as a packed
68/// // image that keeps its timestamp but grows the image.
69/// fn main() -> Result<(), imping_build::Error> {
70///     imping_build::Builder::new()
71///         .ir("steam_2021", "ir/target-2021.json")
72///         .ir("steam_2026", "ir/target-2026.json")
73///         .accept_build("steam_2021", "target.exe", 0x64b8f2a1, 0xa3c000)
74///         .generate()
75/// }
76/// ```
77#[derive(Debug, Clone)]
78pub struct Builder {
79    /// Each build's label and IR file, in call order.
80    irs: Vec<(String, PathBuf)>,
81    generator: Generator,
82    out_file: PathBuf,
83}
84
85impl Default for Builder {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91impl Builder {
92    /// Creates a builder with no IR files yet.
93    pub fn new() -> Self {
94        Self {
95            irs: Vec::new(),
96            generator: Generator::new(),
97            out_file: PathBuf::from(DEFAULT_OUT_FILE),
98        }
99    }
100
101    /// Adds the IR file of the build labelled `label`, such as `steam_2021`.
102    ///
103    /// The label names the build in the bindings: a `Build::Steam2021` variant
104    /// and constants such as `RVA_STEAM_2021`. Labels are lowercase letters,
105    /// digits and single underscores, starting with a letter. In a build
106    /// script, a relative path is relative to the crate's root.
107    pub fn ir(mut self, label: impl Into<String>, path: impl Into<PathBuf>) -> Self {
108        self.irs.push((label.into(), path.into()));
109        self
110    }
111
112    /// Names the Rust module generated for the IR module `ir_name`.
113    ///
114    /// See [`imping_codegen::Generator::module_name`] for the default name.
115    pub fn module_name(mut self, ir_name: impl Into<String>, rust_name: impl Into<String>) -> Self {
116        self.generator = self.generator.module_name(ir_name, rust_name);
117        self
118    }
119
120    /// Makes the generated `init()` of the IR module `ir_name` also accept, as
121    /// the build labelled `label`, the image with this timestamp and image
122    /// size.
123    ///
124    /// See [`imping_codegen::Generator::accept_build`] for when that's sound.
125    pub fn accept_build(
126        mut self,
127        label: impl Into<String>,
128        ir_name: impl Into<String>,
129        timestamp: u32,
130        image_size: u32,
131    ) -> Self {
132        self.generator = self
133            .generator
134            .accept_build(label, ir_name, timestamp, image_size);
135        self
136    }
137
138    /// Sets the file name [`generate`](Self::generate) writes in `OUT_DIR`,
139    /// `imped.rs` by default.
140    ///
141    /// Use it to keep several sets of bindings apart.
142    pub fn out_file(mut self, name: impl Into<PathBuf>) -> Self {
143        self.out_file = name.into();
144        self
145    }
146
147    /// Generates the bindings into `$OUT_DIR/imped.rs`, or the name set with
148    /// [`out_file`](Self::out_file).
149    ///
150    /// For build scripts: also prints `cargo:rerun-if-changed` for each IR file
151    /// and one `cargo:warning` per [`Warning`].
152    ///
153    /// # Errors
154    ///
155    /// [`Error::OutDirNotSet`] outside a build script, otherwise as for
156    /// [`generate_to`](Self::generate_to).
157    pub fn generate(&self) -> Result<(), Error> {
158        let out_dir = std::env::var_os("OUT_DIR").ok_or(Error::OutDirNotSet)?;
159        for (_, path) in &self.irs {
160            println!("cargo:rerun-if-changed={}", path.display());
161        }
162        let warnings = self.generate_to(Path::new(&out_dir).join(&self.out_file))?;
163        for warning in warnings {
164            println!("cargo:warning=imping: {warning}");
165        }
166        Ok(())
167    }
168
169    /// Generates the bindings into the file at `path` and returns the
170    /// warnings, without printing anything.
171    ///
172    /// # Errors
173    ///
174    /// [`Error::Read`], [`Error::Parse`], [`Error::Generate`] or
175    /// [`Error::Write`], each naming the file involved.
176    pub fn generate_to(&self, path: impl AsRef<Path>) -> Result<Vec<Warning>, Error> {
177        let path = path.as_ref();
178        let (source, warnings) = self.render()?;
179        std::fs::write(path, source).map_err(|source| Error::Write {
180            path: path.to_owned(),
181            source,
182        })?;
183        Ok(warnings)
184    }
185
186    /// Reads the IR files and generates the bindings, with a `// Source:` line
187    /// per file added to the header.
188    pub(crate) fn render(&self) -> Result<(String, Vec<Warning>), Error> {
189        let mut irs = Vec::with_capacity(self.irs.len());
190        for (_, path) in &self.irs {
191            let text = std::fs::read_to_string(path).map_err(|source| Error::Read {
192                path: path.clone(),
193                source,
194            })?;
195            let ir = Ir::from_json_str(&text).map_err(|source| Error::Parse {
196                path: path.clone(),
197                source,
198            })?;
199            irs.push(ir);
200        }
201        let builds: Vec<(&str, &Ir)> = self
202            .irs
203            .iter()
204            .zip(&irs)
205            .map(|((label, _), ir)| (label.as_str(), ir))
206            .collect();
207        let bindings = self
208            .generator
209            .generate(&builds)
210            .map_err(|source| Error::Generate {
211                path: self
212                    .irs
213                    .first()
214                    .map(|(_, path)| path.clone())
215                    .unwrap_or_default(),
216                source,
217            })?;
218        let warnings = bindings.warnings().to_vec();
219        let source = with_source_lines(bindings.into_source(), &self.irs);
220        Ok((source, warnings))
221    }
222}
223
224/// Adds `// Source: <path> (<label>)` for each IR file after the generated
225/// file's first header line.
226fn with_source_lines(source: String, irs: &[(String, PathBuf)]) -> String {
227    let lines: String = irs
228        .iter()
229        .map(|(label, path)| format!("// Source: {} ({label})\n", path.display()))
230        .collect();
231    match source.find('\n') {
232        Some(end) => {
233            let mut out = String::with_capacity(source.len() + lines.len());
234            out.push_str(&source[..=end]);
235            out.push_str(&lines);
236            out.push_str(&source[end + 1..]);
237            out
238        }
239        None => format!("{source}\n{lines}"),
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::with_source_lines;
246    use std::path::PathBuf;
247
248    #[test]
249    fn the_source_lines_follow_the_first_header_line() {
250        let source =
251            "// Generated by imping-codegen 0.1.0. Do not edit.\n\npub mod game {}\n".to_owned();
252        let irs = [
253            ("steam_2021".to_owned(), PathBuf::from("ir/2021.json")),
254            ("steam_2026".to_owned(), PathBuf::from("ir/2026.json")),
255        ];
256        assert_eq!(
257            with_source_lines(source, &irs),
258            "// Generated by imping-codegen 0.1.0. Do not edit.\n\
259             // Source: ir/2021.json (steam_2021)\n\
260             // Source: ir/2026.json (steam_2026)\n\
261             \n\
262             pub mod game {}\n"
263        );
264    }
265}