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}