flatbuffers_build/lib.rs
1#![warn(clippy::all, clippy::pedantic, clippy::cargo)]
2
3//! This crate provides a set of functions to facilitate compiling flatbuffers to Rust from within
4//! Rust. This is particularly helpful for use in `build.rs` scripts. Please note that for
5//! compatiblity this crate will only support a single version of the `flatc` compiler. Please
6//! check what version that is against whatever version is installed on your system.That said, due
7//! to flatbuffers' versioning policy, it could be ok to mix patch and even minor versions.
8//!
9//! ## Usage
10//!
11//! If you're not sure where to start, take a look at [`BuilderOptions`]. Please also look at the
12//! [`flatbuffers-build-example`](https://github.com/rdelfin/flatbuffers-build/tree/main/flatbuffers-build-example)
13//! folder in the repo for an example. However, we'll explain the full functionality here.
14//!
15//! As an example, imagine a crate with the following folder structure:
16//! ```bash
17//! ├── build.rs
18//! ├── Cargo.toml
19//! ├── schemas
20//! │ ├── example.fbs
21//! │ └── weapon.fbs
22//! └── src
23//! └── main.rs
24//! ```
25//! In order to compile and use the code generated from both `example.fbs` and `weapon.fbs`, first
26//! you need to add `flatbuffers-build` to your build dependencies, as well as a matching version
27//! of `flatbuffers`:
28//! ```toml
29//! # Cargo.toml
30//! # [...]
31//! [dependencies]
32//! flatbuffers = "=25.2.10"
33//!
34//! [build-dependencies]
35//! flatbuffers-build = "=25.2.10"
36//! # [...]
37//! ```
38//!
39//! You can then have a very simple `build.rs` as follows:
40//! ```no_run
41//! use flatbuffers_build::BuilderOptions;
42//!
43//! BuilderOptions::new_with_files(["schemas/weapon.fbs", "schemas/example.fbs"])
44//! .compile()
45//! .expect("flatbuffer compilation failed");
46//! ```
47//!
48//! Note here that `weapon.fbs` and `example.fbs` are based on the schemas provided by
49//! `flatbuffers` as an example. The namespace is `MyGame.Sample` and it contains multiple tables
50//! and structs, including a `Monster` table.
51//!
52//! This will just compile the flatbuffers and drop them in `${OUT_DIR}/flatbuffers`
53//! You can then use them in `lib.rs` like so:
54//!
55//! ```rust,ignore
56//! #[allow(warnings)]
57//! mod gen_flatbuffers {
58//! include!(concat!(env!("OUT_DIR"), "/flatbuffers/mod.rs"));
59//! }
60//!
61//! use gen_flatbuffers::my_game::sample::Monster;
62//!
63//! fn some_fn() {
64//! // Make use of `Monster`
65//! }
66//! ```
67//!
68//!
69//! ## On file ordering
70//!
71//! Unfortunately due to a quirk in the `flatc` compiler the order you provide the `fbs` files does
72//! matter. From some experimentation, the guidance is to always list files _after_ their
73//! dependencies. Otherwise, the resulting `mod.rs` will be unusable. As an example, we have a
74//! `weapon.fbs` and `example.fbs`. Since the latter has an `include` directive for `weapon.fbs`,
75//! it should go after in the list. If you were to put `example.fbs` _before_ `weapon.fbs`, you'd
76//! end up only being able to import the contents of `weapon.fbs` and with compilation errors if
77//! you tried to use any other components.
78
79use std::{
80 ffi::{OsStr, OsString},
81 path::{Path, PathBuf},
82 process::Command,
83};
84
85const FLATC_VERSION_PREFIX: &str = "flatc version ";
86const FLATC_BUILD_PATH: Option<&str> = option_env!("FLATC_PATH");
87
88/// Version of `flatc` supported by this library. Make sure this matches exactly with the `flatc`
89/// binary you're using and the version of the `flatbuffers` rust library.
90pub const SUPPORTED_FLATC_VERSION: &str = "25.2.10";
91
92/// Primary error type returned when you compile your flatbuffer specifications to Rust.
93#[derive(thiserror::Error, Debug)]
94pub enum Error {
95 /// Returned when `flatc` returns with an non-zero status code for a reason not covered
96 /// elsewhere in this enum.
97 #[error("flatc exited unexpectedly with status code {status_code:?}\n-- stdout:\n{stdout}\n-- stderr:\n{stderr}\n")]
98 FlatcErrorCode {
99 /// Status code returned by `flatc` (none if program was terminated by a signal).
100 status_code: Option<i32>,
101 /// Standard output stream contents of the program
102 stdout: String,
103 /// Standard error stream contents of the program
104 stderr: String,
105 },
106 /// Returned if `flatc --version` generates output we cannot parse. Usually means that the
107 /// binary requested is not, in fact, flatc.
108 #[error("flatc returned invalid output for --version: {0}")]
109 InvalidFlatcOutput(String),
110 /// Returned if the version of `flatc` does not match the supported version. Please refer to
111 /// [`SUPPORTED_FLATC_VERSION`] for that.
112 #[error("flatc version '{0}' is unsupported by this version of the library. Please match your library with your flatc version")]
113 UnsupportedFlatcVersion(String),
114 /// Returned if we fail to spawn a process with `flatc`. Usually means the supplied path to
115 /// flatc does not exist.
116 #[error("flatc failed to spawn: {0}")]
117 FlatcSpawnFailure(#[source] std::io::Error),
118 /// Returned if you failed to set either the output path or the `OUT_DIR` environment variable.
119 #[error(
120 "output directory was not set. Either call .set_output_path() or set the `OUT_DIR` env var"
121 )]
122 OutputDirNotSet,
123}
124
125/// Alias for a Result that uses [`Error`] as the default error type.
126pub type Result<T = (), E = Error> = std::result::Result<T, E>;
127
128/// Builder for options to the flatc compiler options. When consumed using
129/// [`BuilderOptions::compile`], this generates rust code from the flatbuffer definition files
130/// provided. The basic usage for this struct looks something like this:
131/// ```no_run
132/// use flatbuffers_build::BuilderOptions;
133///
134/// BuilderOptions::new_with_files(["some_file.fbs", "some_other_file.fbs"])
135/// .compile()
136/// .expect("flatbuffer compilation failed");
137/// ```
138///
139/// This struct operates as a builder pattern, so you can do things like set the `flatc` path:
140/// ```no_run
141/// # use flatbuffers_build::BuilderOptions;
142/// BuilderOptions::new_with_files(["some_file.fbs", "some_other_file.fbs"])
143/// .set_compiler("/some/path/to/flatc")
144/// .compile()
145/// .expect("flatbuffer compilation failed");
146/// ```
147///
148/// Consult the functions bellow for more details.
149#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct BuilderOptions {
151 files: Vec<PathBuf>,
152 compiler: Option<String>,
153 output_path: Option<PathBuf>,
154 supress_buildrs_directives: bool,
155 gen_object_api: bool,
156 additional_flatc_args: Vec<OsString>,
157}
158
159impl BuilderOptions {
160 /// Create a new builder for the compiler options. We purely initialise with an iterable of
161 /// files to compile. To actually build, refer to the [`Self::compile`] function. Note that the
162 /// order of the files is actually important, as incorrect ordering will result in incorrect
163 /// generated code with missing components. You should always put dependencies of other files
164 /// earlier in the list. In other words, if `schema_a.fbs` imports `schema_b.fbs`, then you'd
165 /// want to call this with:
166 ///
167 /// ```rust
168 /// # use flatbuffers_build::BuilderOptions;
169 /// BuilderOptions::new_with_files(["schema_b.fbs", "schema_a.fbs"]);
170 /// ```
171 ///
172 /// # Arguments
173 /// * `files` - An iterable of files that should be compiled into rust code. No glob resolution
174 /// happens here, and all paths MUST match to real files, either as absolute paths
175 /// or relative to the current working directory.
176 #[must_use]
177 pub fn new_with_files<P: AsRef<Path>, I: IntoIterator<Item = P>>(files: I) -> Self {
178 BuilderOptions {
179 files: files.into_iter().map(|f| f.as_ref().into()).collect(),
180 compiler: None,
181 output_path: None,
182 supress_buildrs_directives: false,
183 gen_object_api: false,
184 additional_flatc_args: Vec::new(),
185 }
186 }
187
188 /// Set the path of the `flatc` binary to use as a compiler. If no such path is provided, we
189 /// will default to first using whatever's set in the `FLATC_PATH` environment variable, or if
190 /// that's not set, we will let the system resolve using standard `PATH` resolution.
191 ///
192 /// # Arguments
193 /// * `compiler` - Path to the compiler to run. This can also be a name that we should resolve
194 /// using standard `PATH` resolution.
195 #[must_use]
196 pub fn set_compiler<S: AsRef<str>>(self, compiler: S) -> Self {
197 BuilderOptions {
198 compiler: Some(compiler.as_ref().into()),
199 ..self
200 }
201 }
202
203 /// Call this to set the output directory of the protobufs. If you don't set this, we will
204 /// default to writing to `${OUT_DIR}/flatbuffers`.
205 ///
206 /// # Arguments
207 /// * `output_path` - The directory to write the files to.
208 #[must_use]
209 pub fn set_output_path<P: AsRef<Path>>(self, output_path: P) -> Self {
210 BuilderOptions {
211 output_path: Some(output_path.as_ref().into()),
212 ..self
213 }
214 }
215
216 /// Set this if you're not running from a `build.rs` script and don't want us to print the
217 /// build.rs instructions/directives that we would otherwise print in stdout.
218 #[must_use]
219 pub fn supress_buildrs_directives(self) -> Self {
220 BuilderOptions {
221 supress_buildrs_directives: true,
222 ..self
223 }
224 }
225
226 /// Generate an additional object-based API. This API is more convenient for object construction
227 /// and mutation than the base API, at the cost of efficiency (object allocation).
228 /// Recommended only to be used if other options are insufficient.
229 #[must_use]
230 pub fn gen_object_api(self) -> Self {
231 BuilderOptions {
232 gen_object_api: true,
233 ..self
234 }
235 }
236
237 /// Use this to add additional arguments to pass to flatc. We will strive to provide explicit
238 /// functions to set these arguments, but this lets you add any missing functionality yourself.
239 /// We guarantee to add these arguments in order, right before the output file and input file
240 /// arguments on the `flatc` invocation, regardless of what other arguments we've passed in.
241 /// It's on users to make sure flags don't conflict with those passed by other functions.
242 #[must_use]
243 pub fn add_flatc_arguments<S: AsRef<str>>(mut self, args: &[S]) -> Self {
244 self.additional_flatc_args
245 .extend(args.iter().map(|s| s.as_ref().into()));
246 self
247 }
248
249 /// Call this function to trigger compilation. Will write the compiled protobufs to the
250 /// specified directory, or to `${OUT_DIR}/flatbuffers` by default.
251 ///
252 /// # Errors
253 /// Will fail if any error happens during compilation, including:
254 /// - Invalid protoc files
255 /// - Unsupported flatc version
256 /// - flatc exiting with a non-zero error code
257 ///
258 /// For more details, see [`Error`].
259 pub fn compile(self) -> Result {
260 compile(self)
261 }
262}
263
264fn compile(builder_options: BuilderOptions) -> Result {
265 let files_str: Vec<_> = builder_options
266 .files
267 .iter()
268 .map(|p| p.clone().into_os_string())
269 .collect();
270 let compiler = builder_options.compiler.unwrap_or_else(|| {
271 if let Some(build_flatc) = FLATC_BUILD_PATH {
272 build_flatc.to_owned()
273 } else {
274 std::env::var("FLATC_PATH").unwrap_or("flatc".into())
275 }
276 });
277 let output_path = builder_options.output_path.map_or_else(
278 || {
279 std::env::var_os("OUT_DIR")
280 .ok_or(Error::OutputDirNotSet)
281 .map(|mut s| {
282 s.push(OsString::from("/flatbuffers"));
283 s
284 })
285 },
286 |p| Ok(p.into_os_string()),
287 )?;
288
289 confirm_flatc_version(&compiler)?;
290
291 let mut args = vec![
292 OsString::from("--rust"),
293 OsString::from("--rust-module-root-file"),
294 ];
295
296 if builder_options.gen_object_api {
297 args.push(OsString::from("--gen-object-api"));
298 }
299
300 args.extend_from_slice(&builder_options.additional_flatc_args[..]);
301
302 args.extend(vec![OsString::from("-o"), output_path.clone()]);
303 args.extend(files_str);
304 run_flatc(&compiler, &args)?;
305
306 if !builder_options.supress_buildrs_directives {
307 for file in builder_options.files {
308 println!("cargo:rerun-if-changed={}", file.display());
309 }
310 }
311 Ok(())
312}
313
314fn confirm_flatc_version(compiler: &str) -> Result {
315 // Output shows up in stdout
316 let output = run_flatc(compiler, ["--version"])?;
317 if output.stdout.starts_with(FLATC_VERSION_PREFIX) {
318 let version_str = output.stdout[FLATC_VERSION_PREFIX.len()..].trim_end();
319 if version_str == SUPPORTED_FLATC_VERSION {
320 Ok(())
321 } else {
322 Err(Error::UnsupportedFlatcVersion(version_str.into()))
323 }
324 } else {
325 Err(Error::InvalidFlatcOutput(output.stdout))
326 }
327}
328
329struct ProgramOutput {
330 pub stdout: String,
331 pub _stderr: String,
332}
333
334fn run_flatc<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
335 compiler: &str,
336 args: I,
337) -> Result<ProgramOutput> {
338 let output = Command::new(compiler)
339 .args(args)
340 .output()
341 .map_err(Error::FlatcSpawnFailure)?;
342 let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
343 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
344 if output.status.success() {
345 Ok(ProgramOutput {
346 stdout,
347 _stderr: stderr,
348 })
349 } else {
350 Err(Error::FlatcErrorCode {
351 status_code: output.status.code(),
352 stdout,
353 stderr,
354 })
355 }
356}