Skip to main content

cargo_pgo/
build.rs

1use crate::get_default_target;
2use cargo_metadata::{Artifact, Message, MessageIter, TargetKind};
3use std::collections::HashMap;
4use std::fmt::Write as WriteFmt;
5use std::io::{BufReader, Write};
6use std::path::PathBuf;
7use std::process::{Child, ChildStdout, Command, Stdio};
8
9#[derive(Debug, Default)]
10pub struct CargoArgs {
11    pub filtered: Vec<String>,
12    pub contains_target: bool,
13    pub contains_profile: bool,
14    pub target_dir: Option<PathBuf>,
15}
16
17enum ReleaseMode {
18    AddRelease,
19    NoRelease,
20}
21
22pub struct RunningCargo {
23    child: Child,
24    message_iter: MessageIter<BufReader<ChildStdout>>,
25}
26
27#[derive(Debug, Copy, Clone, clap::ValueEnum)]
28pub enum CargoCommand {
29    Build,
30    Test,
31    Run,
32    Bench,
33}
34
35impl CargoCommand {
36    pub fn to_str(&self) -> &str {
37        match self {
38            CargoCommand::Build => "build",
39            CargoCommand::Test => "test",
40            CargoCommand::Run => "run",
41            CargoCommand::Bench => "bench",
42        }
43    }
44}
45
46impl RunningCargo {
47    pub fn messages(&mut self) -> &mut MessageIter<BufReader<ChildStdout>> {
48        &mut self.message_iter
49    }
50
51    pub fn check_status(mut self) -> anyhow::Result<()> {
52        let status = self.child.wait()?;
53        if !status.success() {
54            return Err(anyhow::anyhow!(
55                "Cargo finished with an error ({})",
56                status.code().unwrap_or(-1),
57            ));
58        }
59        Ok(())
60    }
61}
62
63/// Start a `cargo` command in release mode with the provided RUSTFLAGS and Cargo arguments.
64pub fn cargo_command_with_rustflags(
65    command: CargoCommand,
66    rustflags: Vec<String>,
67    cargo_args: Vec<String>,
68) -> anyhow::Result<RunningCargo> {
69    let mut env = HashMap::default();
70
71    // We have to handle RUSTFLAGS in a special way, to make sure that we don't override users
72    // values from .cargo/config.toml.
73
74    // The `--config` flag is only supported in Rust 1.63+.
75    let supports_config_flag = rustc_version::version()? >= semver::Version::new(1, 63, 0);
76    let serialized_rustflags = rustflags.join(" ");
77
78    let mut final_cargo_args = vec![];
79
80    match (supports_config_flag, std::env::var("RUSTFLAGS")) {
81        (_, Ok(mut existing_rustflags)) => {
82            // If RUSTFLAGS was defined explicitly, we want to append to it. These RUSTFLAGS will
83            // override everything else, so we don't need to care about .cargo/config.toml, but we
84            // have to add ourselves to it, so that we do use the PGO flags
85            write!(&mut existing_rustflags, " {serialized_rustflags}")?;
86            env.insert("RUSTFLAGS".to_string(), existing_rustflags);
87        }
88        (false, _) => {
89            // If there's no RUSTFLAGS, and Cargo doesn't support `--config` yet, just use
90            // RUSTFLAGS.
91            env.insert("RUSTFLAGS".to_string(), serialized_rustflags);
92        }
93        (true, _) => {
94            // If there's no RUSTFLAGS, and Cargo supports `--config`, use it.
95            // The user might have some flags in their config.toml file(s), and
96            // `--config target.*.rustflags` will append to it instead of overwriting it.
97            // We use target RUSTFLAGS instead of e.g. build rustflags, because it has a higher
98            // precedence. See https://github.com/Kobzol/cargo-pgo/issues/56 for more details.
99            final_cargo_args.push("--config".to_string());
100
101            // `cfg(all())` should match any target.
102            let mut flags = String::from("target.'cfg(all())'.rustflags=[");
103            for (index, flag) in rustflags.into_iter().enumerate() {
104                if index > 0 {
105                    flags.push(',');
106                }
107                flags.push_str(&format!("'{flag}'"));
108            }
109            flags.push(']');
110            final_cargo_args.push(flags);
111        }
112    }
113
114    let release_mode = match command {
115        CargoCommand::Bench => ReleaseMode::NoRelease,
116        _ => ReleaseMode::AddRelease,
117    };
118
119    // We need to add any new Cargo arguments to the beginning of `cargo_args`, because it could
120    // end with `--`, in which case the added arguments would be sent to e.g. the executed binary
121    // instead.
122    final_cargo_args.extend(cargo_args);
123
124    let mut child = cargo_command(command, final_cargo_args, env, release_mode)?;
125    let stdout = child.stdout.take().unwrap();
126    Ok(RunningCargo {
127        child,
128        message_iter: Message::parse_stream(BufReader::new(stdout)),
129    })
130}
131
132/// Spawn `cargo` command in release mode with the provided env variables and Cargo arguments.
133fn cargo_command(
134    cargo_cmd: CargoCommand,
135    cargo_args: Vec<String>,
136    env: HashMap<String, String>,
137    release_mode: ReleaseMode,
138) -> anyhow::Result<Child> {
139    let parsed_args = parse_cargo_args(cargo_args);
140
141    let mut command = Command::new("cargo");
142    command.args([
143        cargo_cmd.to_str(),
144        "--message-format",
145        "json-diagnostic-rendered-ansi",
146    ]);
147    command.stdin(Stdio::inherit());
148    command.stdout(Stdio::piped());
149    command.stderr(Stdio::inherit());
150
151    match release_mode {
152        ReleaseMode::AddRelease => {
153            if !parsed_args.contains_profile {
154                command.arg("--release");
155            }
156        }
157        ReleaseMode::NoRelease => {}
158    }
159
160    // --target is passed to avoid instrumenting build scripts
161    // See https://doc.rust-lang.org/rustc/profile-guided-optimization.html#a-complete-cargo-workflow
162    if !parsed_args.contains_target {
163        let default_target = get_default_target().map_err(|error| {
164            anyhow::anyhow!(
165                "Unable to find default target triple for your platform: {:?}",
166                error
167            )
168        })?;
169        command.args(["--target", &default_target]);
170    }
171
172    for arg in parsed_args.filtered {
173        command.arg(arg);
174    }
175    for (key, value) in env {
176        command.env(key, value);
177    }
178    log::debug!("Executing cargo command: {:?}", command);
179    Ok(command.spawn()?)
180}
181
182pub fn parse_cargo_args(cargo_args: Vec<String>) -> CargoArgs {
183    let mut args = CargoArgs::default();
184
185    let mut iterator = cargo_args.into_iter();
186    while let Some(arg) = iterator.next() {
187        match arg.as_str() {
188            "--" => {
189                // After this, the user program arguments start, we should ignore these
190                args.filtered.push("--".to_string());
191                args.filtered.extend(iterator);
192                break;
193            }
194            // Skip `--release`, we will pass it by ourselves.
195            "--release" => {
196                log::warn!(
197                    "Do not pass `--release` manually, it will be added automatically by `cargo-pgo`"
198                );
199            }
200            _ => {
201                if get_key_value("--message-format", arg.as_str(), &mut iterator).is_some() {
202                    // Skip `--message-format`, we need it to be JSON.
203                    log::warn!(
204                        "Do not pass `--message-format` manually, it will be added automatically by `cargo-pgo`"
205                    );
206                } else if let Some(value) = get_key_value("--target", arg.as_str(), &mut iterator) {
207                    // Check if `--target` was passed
208                    args.contains_target = true;
209                    args.filtered.push("--target".to_string());
210                    if let Some(value) = value {
211                        args.filtered.push(value);
212                    }
213                } else if let Some(value) = get_key_value("--profile", arg.as_str(), &mut iterator)
214                {
215                    // Check if `--profile` was passed
216                    args.contains_profile = true;
217                    args.filtered.push("--profile".to_string());
218                    if let Some(value) = value {
219                        args.filtered.push(value);
220                    }
221                } else if let Some(value) =
222                    get_key_value("--target-dir", arg.as_str(), &mut iterator)
223                {
224                    // Extract `--target-dir`
225                    args.target_dir = value.clone().map(PathBuf::from);
226                    args.filtered.push("--target-dir".to_string());
227                    if let Some(value) = value {
228                        args.filtered.push(value);
229                    }
230                } else {
231                    args.filtered.push(arg);
232                }
233            }
234        }
235    }
236    args
237}
238
239/// Parses a `--key=<value>` or `--key <value>` key/value CLI argument pair.
240fn get_key_value<Iter: Iterator<Item = String>>(
241    key: &str,
242    arg: &str,
243    iter: &mut Iter,
244) -> Option<Option<String>> {
245    // A different argument was passed, nothing to be seen here
246    if !arg.starts_with(key) {
247        return None;
248    }
249    // --key was passed exactly, we should extract the value from the following argument
250    if arg == key {
251        let value = iter.next();
252        return Some(value);
253    }
254
255    // --key<suffix> was passed, let's try to split it into --key=value
256    if let Some((parsed_key, value)) = arg.split_once('=') {
257        // if --keyfoo=value was passed, ignore it
258        if parsed_key == key {
259            return Some(Some(value.to_string()));
260        }
261    }
262
263    None
264}
265
266pub fn handle_metadata_message(message: Message) {
267    let stdout = std::io::stdout();
268    let mut stdout = stdout.lock();
269    write_metadata_message(&mut stdout, message);
270    stdout.flush().unwrap();
271}
272
273fn write_metadata_message<W: Write>(mut stream: W, message: Message) {
274    match message {
275        Message::TextLine(line) => {
276            log::debug!("TextLine {}", line);
277            writeln!(stream, "{line}").unwrap();
278        }
279        Message::CompilerMessage(message) => {
280            log::debug!("CompilerMessage {}", message);
281            write!(
282                stream,
283                "{}",
284                message.message.rendered.unwrap_or(message.message.message)
285            )
286            .unwrap();
287        }
288        _ => {
289            log::debug!("Metadata output: {:?}", message);
290        }
291    }
292}
293
294/// Returns a user-friendly name of an artifact kind.
295pub fn get_artifact_kind(artifact: &Artifact) -> &str {
296    for kind in &artifact.target.kind {
297        match kind {
298            TargetKind::Bin => {
299                return "binary";
300            }
301            TargetKind::Bench => {
302                return "benchmark";
303            }
304            TargetKind::Example => {
305                return "example";
306            }
307            _ => {}
308        }
309    }
310    "artifact"
311}
312
313#[cfg(test)]
314mod tests {
315    use crate::build::{get_key_value, parse_cargo_args};
316    use std::path::PathBuf;
317
318    #[test]
319    fn parse_cargo_args_filter_release() {
320        let args = parse_cargo_args(vec![
321            "foo".to_string(),
322            "--release".to_string(),
323            "--bar".to_string(),
324        ]);
325        assert_eq!(args.filtered, vec!["foo".to_string(), "--bar".to_string()]);
326    }
327
328    #[test]
329    fn parse_cargo_args_filter_message_format() {
330        let args = parse_cargo_args(vec![
331            "foo".to_string(),
332            "--message-format".to_string(),
333            "json".to_string(),
334            "bar".to_string(),
335        ]);
336        assert_eq!(args.filtered, vec!["foo".to_string(), "bar".to_string()]);
337    }
338
339    #[test]
340    fn parse_cargo_args_filter_message_format_equals() {
341        let args = parse_cargo_args(vec![
342            "foo".to_string(),
343            "--message-format=json".to_string(),
344            "bar".to_string(),
345        ]);
346        assert_eq!(args.filtered, vec!["foo".to_string(), "bar".to_string()]);
347    }
348
349    #[test]
350    fn parse_cargo_args_find_target() {
351        let args = parse_cargo_args(vec![
352            "--target".to_string(),
353            "x64".to_string(),
354            "bar".to_string(),
355        ]);
356        assert_eq!(
357            args.filtered,
358            vec!["--target".to_string(), "x64".to_string(), "bar".to_string()]
359        );
360        assert!(args.contains_target);
361    }
362
363    #[test]
364    fn parse_cargo_args_find_target_equals() {
365        let args = parse_cargo_args(vec!["--target=x64".to_string(), "bar".to_string()]);
366        assert_eq!(
367            args.filtered,
368            vec!["--target".to_string(), "x64".to_string(), "bar".to_string()]
369        );
370        assert!(args.contains_target);
371    }
372
373    #[test]
374    fn parse_cargo_args_target_dir() {
375        let args = parse_cargo_args(vec![
376            "--target-dir".to_string(),
377            "/tmp/foo".to_string(),
378            "bar".to_string(),
379        ]);
380        assert_eq!(
381            args.filtered,
382            vec![
383                "--target-dir".to_string(),
384                "/tmp/foo".to_string(),
385                "bar".to_string()
386            ]
387        );
388        assert_eq!(args.target_dir, Some(PathBuf::from("/tmp/foo")));
389    }
390
391    #[test]
392    fn parse_cargo_args_target_dir_equals() {
393        let args = parse_cargo_args(vec!["--target-dir=/tmp/foo".to_string(), "bar".to_string()]);
394        assert_eq!(
395            args.filtered,
396            vec![
397                "--target-dir".to_string(),
398                "/tmp/foo".to_string(),
399                "bar".to_string()
400            ]
401        );
402        assert_eq!(args.target_dir, Some(PathBuf::from("/tmp/foo")));
403    }
404
405    #[test]
406    fn parse_cargo_args_profile() {
407        let args = parse_cargo_args(vec!["--profile".to_string(), "dev".to_string()]);
408        assert_eq!(
409            args.filtered,
410            vec!["--profile".to_string(), "dev".to_string(),]
411        );
412        assert!(args.contains_profile);
413    }
414
415    #[test]
416    fn parse_cargo_args_respect_user_args() {
417        let args = parse_cargo_args(vec![
418            "-v".to_string(),
419            "--".to_string(),
420            "--release".to_string(),
421            "--profile".to_string(),
422            "dev".to_string(),
423        ]);
424        assert_eq!(
425            args.filtered,
426            vec![
427                "-v".to_string(),
428                "--".to_string(),
429                "--release".to_string(),
430                "--profile".to_string(),
431                "dev".to_string()
432            ]
433        );
434        assert!(!args.contains_profile);
435    }
436
437    #[test]
438    fn get_key_value_wrong_key() {
439        assert_eq!(
440            get_key_value("--foo", "--bar", &mut std::iter::empty()),
441            None
442        );
443    }
444
445    #[test]
446    fn get_key_value_exact_key_missing_value() {
447        assert_eq!(
448            get_key_value("--foo", "--foo", &mut std::iter::empty()),
449            Some(None)
450        );
451    }
452
453    #[test]
454    fn get_key_value_exact_key_value() {
455        assert_eq!(
456            get_key_value("--foo", "--foo", &mut vec!["bar".to_string()].into_iter()),
457            Some(Some("bar".to_string()))
458        );
459    }
460
461    #[test]
462    fn get_key_value_equals_wrong_prefix() {
463        assert_eq!(
464            get_key_value("--foo", "--foox=bar", &mut std::iter::empty()),
465            None
466        );
467    }
468
469    #[test]
470    fn get_key_value_equals() {
471        assert_eq!(
472            get_key_value("--foo", "--foo=bar", &mut std::iter::empty()),
473            Some(Some("bar".to_string()))
474        );
475    }
476}