thrift_compiler/
lib.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under both the MIT license found in the
5 * LICENSE-MIT file in the root directory of this source tree and the Apache
6 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
7 * of this source tree.
8 */
9
10#![deny(warnings, missing_docs, clippy::all, rustdoc::broken_intra_doc_links)]
11
12//! This crate is a wrapper around
13//! [fbthrift](https://github.com/facebook/fbthrift)'s compiler. Its main usage
14//! is from within [Cargo build
15//! scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html) where
16//! it might be invoked to generate rust code from thrift files.
17
18use std::borrow::Cow;
19use std::env;
20use std::ffi::OsStr;
21use std::ffi::OsString;
22use std::fmt;
23use std::fs;
24use std::path::Path;
25use std::path::PathBuf;
26use std::process::Command;
27
28use anyhow::anyhow;
29use anyhow::ensure;
30use anyhow::Context;
31use anyhow::Result;
32use clap::ValueEnum;
33use serde::Deserialize;
34use which::which;
35
36/// A thrift library 'foo' (say) results in two crates 'foo' and 'foo_types'. We
37/// arrange that the thrift compiler wrapper be invoked from the build of both.
38/// The behavior of the wrapper is sensitive to the invocation context ('foo' vs
39/// 'foo-types') and this type is used to disambiguate.
40#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, ValueEnum)]
41pub enum GenContext {
42    /// 'lib' crate generation context (e.g. 'foo').
43    #[serde(rename = "lib")]
44    Lib,
45    /// 'types' crate generation context (e.g. 'foo_types').
46    #[serde(rename = "types")]
47    Types,
48}
49
50impl fmt::Display for GenContext {
51    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
52        let t = match self {
53            GenContext::Lib => "lib",
54            GenContext::Types => "types",
55        };
56        fmt.write_str(t)
57    }
58}
59
60/// Builder for thrift compiler wrapper.
61pub struct Config {
62    thrift_bin: Option<OsString>,
63    out_dir: PathBuf,
64    gen_context: GenContext,
65    base_path: Option<PathBuf>,
66    crate_map: Option<PathBuf>,
67    types_crate: Option<String>,
68    options: Option<String>,
69    lib_include_srcs: Vec<String>, // src to include in the primary crate
70    types_include_srcs: Vec<String>, // src to include in the -types sub-crate
71}
72
73impl Config {
74    /// Return a new configuration with the required parameters set
75    pub fn new(
76        gen_context: GenContext,
77        thrift_bin: Option<OsString>,
78        out_dir: PathBuf,
79    ) -> Result<Self> {
80        Ok(Self {
81            thrift_bin,
82            out_dir,
83            gen_context,
84            base_path: None,
85            crate_map: None,
86            types_crate: None,
87            options: None,
88            lib_include_srcs: vec![],
89            types_include_srcs: vec![],
90        })
91    }
92
93    /// Return a new configuration with parameters computed based on environment variables set by
94    /// Cargo's build scrip (OUT_DIR mostly). If THRIFT is in the environment, that will be used as
95    /// the Thrift binary. Otherwise, it will be detected in run_compiler.
96    pub fn from_env(gen_context: GenContext) -> Result<Self> {
97        println!("cargo:rerun-if-env-changed=THRIFT");
98
99        let thrift_bin = env::var_os("THRIFT");
100        let out_dir = env::var_os("OUT_DIR")
101            .map(PathBuf::from)
102            .context("OUT_DIR environment variable must be set")?;
103
104        let crate_map = out_dir.join("cratemap");
105        let mut conf = Self::new(gen_context, thrift_bin, out_dir)?;
106
107        if crate_map.is_file() {
108            conf.crate_map(crate_map);
109        }
110
111        Ok(conf)
112    }
113
114    /// Set the base path which is used by the compiler to find thrift files included by input
115    /// thrift files. This is also used to find the compiler.
116    pub fn base_path(&mut self, value: impl Into<PathBuf>) -> &mut Self {
117        self.base_path = Some(value.into());
118        self
119    }
120
121    /// Set the path to file with crate map definition which is used by the
122    /// compiler to infer crate names that will be used in the generated code.
123    /// Please refer to code in
124    /// fbthrift/thrift/compiler/generate/t_mstch_rust_generator.cc
125    /// for the scheme of crate map.
126    pub fn crate_map(&mut self, value: impl Into<PathBuf>) -> &mut Self {
127        self.crate_map = Some(value.into());
128        self
129    }
130
131    /// Set the name of the types sub-crate needed by by the thrift-compiler (to
132    /// be able to generate things like `use ::foo__types`).
133    pub fn types_crate(&mut self, value: impl Into<String>) -> &mut Self {
134        self.types_crate = Some(value.into());
135        self
136    }
137
138    /// Set the options to be passed to `mstch_rust` code generation. Example
139    /// options are `serde`.
140    pub fn options(&mut self, value: impl Into<String>) -> &mut Self {
141        self.options = Some(value.into());
142        self
143    }
144
145    /// Set extra srcs to be available in the generated primary crate.
146    pub fn lib_include_srcs(&mut self, value: Vec<String>) -> &mut Self {
147        self.lib_include_srcs = value;
148        self
149    }
150
151    /// Set extra srcs to be available in the generated types sub-crate.
152    pub fn types_include_srcs(&mut self, value: Vec<String>) -> &mut Self {
153        self.types_include_srcs = value;
154        self
155    }
156
157    /// Run the compiler on the input files. As a result a `lib.rs` file will be
158    /// generated inside the output dir. The contents of the `lib.rs` can vary
159    /// according to the generation context (e.g. for a given thrift library,
160    /// 'foo' say, we invoke the generator for the crate 'foo' and for the crate
161    /// 'foo-types').
162    pub fn run(&self, input_files: impl IntoIterator<Item = impl AsRef<Path>>) -> Result<()> {
163        let thrift_bin = self.resolve_thrift_bin()?;
164
165        let input = name_and_path_from_input(input_files)?;
166        let out = &self.out_dir;
167        fs::create_dir_all(out)?;
168
169        for input in &input {
170            println!("cargo:rerun-if-changed={}", input.1.as_ref().display());
171        }
172        for lib_include_src in &self.lib_include_srcs {
173            println!("cargo:rerun-if-changed={lib_include_src}");
174            fs::copy(lib_include_src, out.join(lib_include_src))?;
175        }
176        for types_include_src in &self.types_include_srcs {
177            println!("cargo:rerun-if-changed={types_include_src}");
178            fs::copy(types_include_src, out.join(types_include_src))?;
179        }
180
181        if let [(_name, file)] = &input[..] {
182            match self.gen_context {
183                GenContext::Lib => {
184                    // The primary crate.
185
186                    self.run_compiler(&thrift_bin, out, file)?;
187
188                    // These files are not of interest here.
189                    fs::remove_file(out.join("consts.rs"))?;
190                    fs::remove_file(out.join("errors.rs"))?;
191                    fs::remove_file(out.join("services.rs"))?;
192                    fs::remove_file(out.join("types.rs"))?;
193
194                    // 'lib.rs' together with the remaining files have the
195                    // content we want.
196                    { /* nothing to do */ }
197                }
198                GenContext::Types => {
199                    // The -types sub-crate.
200
201                    self.run_compiler(&thrift_bin, out, file)?;
202
203                    // These files are not of interest here (for now).
204                    fs::remove_file(out.join("lib.rs"))?;
205                    fs::remove_file(out.join("dependencies.rs"))?;
206                    fs::remove_file(out.join("client.rs"))?;
207                    fs::remove_file(out.join("server.rs"))?;
208                    fs::remove_file(out.join("mock.rs"))?;
209
210                    // 'types.rs' (together with the remaining files) has the
211                    // content we want (but the file needs renaming to
212                    // 'lib.rs').
213                    fs::rename(out.join("types.rs"), out.join("lib.rs"))?;
214                }
215            }
216        } else {
217            match self.gen_context {
218                GenContext::Lib => {
219                    // The primary crate.
220
221                    for (name, file) in &input {
222                        let submod = out.join(name);
223                        fs::create_dir_all(&submod)?;
224                        self.run_compiler(&thrift_bin, &submod, file)?;
225
226                        // These files are not of interest here.
227                        fs::remove_file(submod.join("consts.rs"))?;
228                        fs::remove_file(submod.join("errors.rs"))?;
229                        fs::remove_file(submod.join("services.rs"))?;
230                        fs::remove_file(submod.join("types.rs"))?;
231
232                        // 'lib.rs' (together with the remaining files) has the
233                        // content we want (but the file needs renaming to
234                        // 'mod.rs').
235                        fs::rename(submod.join("lib.rs"), submod.join("mod.rs"))?;
236                    }
237                }
238                GenContext::Types => {
239                    // The -types sub-crate.
240
241                    for (name, file) in &input {
242                        let submod = out.join(name);
243                        fs::create_dir_all(&submod)?;
244                        self.run_compiler(&thrift_bin, &submod, file)?;
245
246                        // These files are not of interest here.
247                        fs::remove_file(submod.join("lib.rs"))?;
248                        fs::remove_file(submod.join("dependencies.rs"))?;
249                        fs::remove_file(submod.join("client.rs"))?;
250                        fs::remove_file(submod.join("server.rs"))?;
251
252                        // 'types.rs' (together with the remaining files) has the
253                        // content we want (but the file needs renaming to
254                        // 'mod.rs').
255                        fs::rename(submod.join("types.rs"), submod.join("mod.rs"))?;
256                    }
257                }
258            }
259
260            let lib = format!(
261                "{}\n",
262                input
263                    .iter()
264                    .map(|(name, _file)| format!("pub mod {};", name.to_string_lossy()))
265                    .collect::<Vec<_>>()
266                    .join("\n")
267            );
268            fs::write(out.join("lib.rs"), lib)?;
269        }
270
271        Ok(())
272    }
273
274    fn resolve_thrift_bin(&self) -> Result<Cow<'_, OsString>> {
275        // Get raw location
276        let mut thrift_bin = if let Some(bin) = self.thrift_bin.as_ref() {
277            Cow::Borrowed(bin)
278        } else {
279            Cow::Owned(self.infer_thrift_binary())
280        };
281        // Resolve based on PATH if needed
282        let thrift_bin_path: &Path = thrift_bin.as_ref().as_ref();
283        if thrift_bin_path.components().count() == 1 {
284            println!("cargo:rerun-if-env-changed=PATH");
285            let new_path = which(thrift_bin.as_ref()).with_context(|| {
286                format!(
287                    "Failed to resolve thrift binary `{}` to an absolute path",
288                    thrift_bin.to_string_lossy()
289                )
290            })?;
291            thrift_bin = Cow::Owned(new_path.into_os_string())
292        }
293        println!("cargo:rerun-if-changed={}", thrift_bin.to_string_lossy());
294        Ok(thrift_bin)
295    }
296
297    fn infer_thrift_binary(&self) -> OsString {
298        if let Some(base) = self.base_path.as_ref() {
299            let mut candidate = base.clone();
300            candidate.push("thrift/facebook/rpm/thrift1");
301            #[cfg(windows)]
302            candidate.set_extension("exe");
303            if Path::new(&candidate).exists() {
304                return candidate.into_os_string();
305            }
306        }
307
308        "thrift1".into()
309    }
310
311    fn run_compiler(
312        &self,
313        thrift_bin: &OsStr,
314        out: impl AsRef<Path>,
315        input: impl AsRef<Path>,
316    ) -> Result<String> {
317        let mut cmd = Command::new(thrift_bin);
318
319        let args = {
320            let mut args = Vec::new();
321
322            if let Some(crate_map) = &self.crate_map {
323                args.push(format!("cratemap={}", crate_map.display()))
324            }
325            if let Some(base_path) = &self.base_path {
326                args.push(format!("include_prefix={}", base_path.display()));
327                cmd.arg("-I");
328                cmd.arg(base_path);
329            }
330            if let Some(types_crate) = &self.types_crate {
331                args.push(format!("types_crate={}", types_crate));
332            }
333            if !self.lib_include_srcs.is_empty() {
334                args.push(format!(
335                    "lib_include_srcs={}",
336                    self.lib_include_srcs.join(":")
337                ));
338            }
339            if !self.types_include_srcs.is_empty() {
340                args.push(format!(
341                    "types_include_srcs={}",
342                    self.types_include_srcs.join(":")
343                ));
344            }
345            if let Some(options) = &self.options {
346                args.push(options.to_owned());
347            }
348            if args.is_empty() {
349                "".to_owned()
350            } else {
351                format!(":{}", args.join(","))
352            }
353        };
354
355        cmd.arg("--gen")
356            .arg(format!("mstch_rust{args}"))
357            .arg("--out")
358            .arg(out.as_ref())
359            .arg(input.as_ref());
360
361        let output = cmd.output().with_context(|| {
362            format!(
363                "Failed to run thrift compiler. Is '{}' executable?",
364                thrift_bin.to_string_lossy()
365            )
366        })?;
367        ensure!(
368            output.status.success(),
369            format!(
370                "Command '{:#?}' failed! Stdout:\n{}\nStderr:\n{}",
371                cmd,
372                String::from_utf8_lossy(&output.stdout),
373                String::from_utf8_lossy(&output.stderr),
374            )
375        );
376
377        let out_file = out.as_ref().join("lib.rs");
378        ensure!(
379            out_file.is_file(),
380            format!(
381                "Thrift has successfully run, but the resulting '{}' file is missing, command: '{:#?}'",
382                out_file.display(),
383                cmd,
384            )
385        );
386
387        fs::read_to_string(&out_file)
388            .with_context(|| format!("Failed to read content of file '{}'", out_file.display()))
389    }
390}
391
392fn name_and_path_from_input<T: AsRef<Path>>(
393    input_files: impl IntoIterator<Item = T>,
394) -> Result<Vec<(OsString, T)>> {
395    input_files
396        .into_iter()
397        .map(|file| {
398            Ok((
399                file.as_ref()
400                    .file_stem()
401                    .ok_or_else(|| {
402                        anyhow!(
403                            "Failed to get file_stem from path {}",
404                            file.as_ref().display()
405                        )
406                    })?
407                    .to_owned(),
408                file,
409            ))
410        })
411        .collect()
412}