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
63pub 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 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 write!(&mut existing_rustflags, " {serialized_rustflags}")?;
86 env.insert("RUSTFLAGS".to_string(), existing_rustflags);
87 }
88 (false, _) => {
89 env.insert("RUSTFLAGS".to_string(), serialized_rustflags);
92 }
93 (true, _) => {
94 final_cargo_args.push("--config".to_string());
100
101 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 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
132fn 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 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 args.filtered.push("--".to_string());
191 args.filtered.extend(iterator);
192 break;
193 }
194 "--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 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 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 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 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
239fn get_key_value<Iter: Iterator<Item = String>>(
241 key: &str,
242 arg: &str,
243 iter: &mut Iter,
244) -> Option<Option<String>> {
245 if !arg.starts_with(key) {
247 return None;
248 }
249 if arg == key {
251 let value = iter.next();
252 return Some(value);
253 }
254
255 if let Some((parsed_key, value)) = arg.split_once('=') {
257 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
294pub 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}