1use std::{
2 ffi::OsString,
3 path::{Path, PathBuf},
4 process::Command,
5};
6
7#[derive(Debug, Clone)]
8/// A command, its args and its environment. Used for
9/// the main command, the dependency builder and the cfg-reader.
10pub struct CommandBuilder {
11 /// Path to the binary.
12 pub program: PathBuf,
13 /// Arguments to the binary.
14 pub args: Vec<OsString>,
15 /// A flag to prefix before the path to where output files should be written.
16 pub out_dir_flag: Option<OsString>,
17 /// A flag to set as the last flag in the command, so the `build` caller can
18 /// append the filename themselves.
19 pub input_file_flag: Option<OsString>,
20 /// Environment variables passed to the binary that is executed.
21 /// The environment variable is removed if the second tuple field is `None`
22 pub envs: Vec<(OsString, Option<OsString>)>,
23}
24
25impl CommandBuilder {
26 /// Uses the `CARGO` env var or just a program named `cargo` and the argument `build`.
27 pub fn cargo() -> Self {
28 Self {
29 program: PathBuf::from(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())),
30 args: vec!["build".into()],
31 out_dir_flag: Some("--target-dir".into()),
32 input_file_flag: Some("--manifest-path".into()),
33 envs: vec![],
34 }
35 }
36
37 /// Uses the `RUSTC` env var or just a program named `rustc` and the argument `--error-format=json`.
38 ///
39 /// Take care to only append unless you actually meant to overwrite the defaults.
40 /// Overwriting the defaults may make `//~ ERROR` style comments stop working.
41 pub fn rustc() -> Self {
42 Self {
43 program: PathBuf::from(std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into())),
44 args: vec!["--error-format=json".into()],
45 out_dir_flag: Some("--out-dir".into()),
46 input_file_flag: None,
47 envs: vec![],
48 }
49 }
50
51 /// Same as [`CommandBuilder::rustc`], but with arguments for obtaining the cfgs.
52 pub fn cfgs() -> Self {
53 Self {
54 args: vec!["--print".into(), "cfg".into()],
55 ..Self::rustc()
56 }
57 }
58
59 /// Build a `CommandBuilder` for a command without any argumemnts.
60 /// You can still add arguments later.
61 pub fn cmd(cmd: impl Into<PathBuf>) -> Self {
62 Self {
63 program: cmd.into(),
64 args: vec![],
65 out_dir_flag: None,
66 input_file_flag: None,
67 envs: vec![],
68 }
69 }
70
71 /// Render the command like you'd use it on a command line.
72 pub fn display(&self) -> impl std::fmt::Display + '_ {
73 struct Display<'a>(&'a CommandBuilder);
74 impl std::fmt::Display for Display<'_> {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 for (var, val) in &self.0.envs {
77 if let Some(val) = val {
78 write!(f, "{var:?}={val:?} ")?;
79 }
80 }
81 self.0.program.display().fmt(f)?;
82 for arg in &self.0.args {
83 write!(f, " {arg:?}")?;
84 }
85 if let Some(flag) = &self.0.out_dir_flag {
86 write!(f, " {flag:?} OUT_DIR")?;
87 }
88 if let Some(flag) = &self.0.input_file_flag {
89 write!(f, " {flag:?}")?;
90 }
91 Ok(())
92 }
93 }
94 Display(self)
95 }
96
97 /// Create a command with the given settings.
98 pub fn build(&self, out_dir: &Path) -> Command {
99 let mut cmd = Command::new(&self.program);
100 cmd.args(self.args.iter());
101 if let Some(flag) = &self.out_dir_flag {
102 cmd.arg(flag).arg(out_dir);
103 }
104 if let Some(flag) = &self.input_file_flag {
105 cmd.arg(flag);
106 }
107 self.apply_env(&mut cmd);
108 cmd
109 }
110
111 pub(crate) fn apply_env(&self, cmd: &mut Command) {
112 for (var, val) in self.envs.iter() {
113 if let Some(val) = val {
114 cmd.env(var, val);
115 } else {
116 cmd.env_remove(var);
117 }
118 }
119 }
120}
121