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}