1#![deny(warnings, missing_docs, clippy::all, rustdoc::broken_intra_doc_links)]
11
12use 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#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, ValueEnum)]
41pub enum GenContext {
42 #[serde(rename = "lib")]
44 Lib,
45 #[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
60pub 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>, types_include_srcs: Vec<String>, }
72
73impl Config {
74 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 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 pub fn base_path(&mut self, value: impl Into<PathBuf>) -> &mut Self {
117 self.base_path = Some(value.into());
118 self
119 }
120
121 pub fn crate_map(&mut self, value: impl Into<PathBuf>) -> &mut Self {
127 self.crate_map = Some(value.into());
128 self
129 }
130
131 pub fn types_crate(&mut self, value: impl Into<String>) -> &mut Self {
134 self.types_crate = Some(value.into());
135 self
136 }
137
138 pub fn options(&mut self, value: impl Into<String>) -> &mut Self {
141 self.options = Some(value.into());
142 self
143 }
144
145 pub fn lib_include_srcs(&mut self, value: Vec<String>) -> &mut Self {
147 self.lib_include_srcs = value;
148 self
149 }
150
151 pub fn types_include_srcs(&mut self, value: Vec<String>) -> &mut Self {
153 self.types_include_srcs = value;
154 self
155 }
156
157 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 self.run_compiler(&thrift_bin, out, file)?;
187
188 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 { }
197 }
198 GenContext::Types => {
199 self.run_compiler(&thrift_bin, out, file)?;
202
203 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 fs::rename(out.join("types.rs"), out.join("lib.rs"))?;
214 }
215 }
216 } else {
217 match self.gen_context {
218 GenContext::Lib => {
219 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 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 fs::rename(submod.join("lib.rs"), submod.join("mod.rs"))?;
236 }
237 }
238 GenContext::Types => {
239 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 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 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 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 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}