1use std::path::Path;
8
9use zccache_core::NormalizedPath;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct ExternCrate {
14 pub name: String,
16 pub path: NormalizedPath,
18}
19
20#[derive(Debug, Clone)]
22pub struct RustcParsedArgs {
23 pub source_file: NormalizedPath,
25
26 pub crate_name: Option<String>,
29 pub crate_types: Vec<String>,
31 pub edition: Option<String>,
33 pub emit_types: Vec<String>,
35 pub cfgs: Vec<String>,
37 pub check_cfgs: Vec<String>,
39 pub codegen_flags: Vec<String>,
46 pub target: Option<String>,
48 pub cap_lints: Option<String>,
50 pub externs: Vec<ExternCrate>,
52 pub lint_flags: Vec<String>,
54 pub unknown_flags: Vec<String>,
56
57 pub out_dir: Option<NormalizedPath>,
60 pub extra_filename: Option<String>,
62 pub cargo_metadata: Option<String>,
64 pub incremental_dir: Option<NormalizedPath>,
66 pub error_format: Option<String>,
68 pub json_format: Option<String>,
70 pub color: Option<String>,
72 pub diagnostic_width: Option<String>,
74 pub search_paths: Vec<NormalizedPath>,
76 pub remap_path_prefixes: Vec<String>,
78 pub sysroot: Option<NormalizedPath>,
80 pub output_file: Option<NormalizedPath>,
82}
83
84const EXCLUDED_CODEGEN: &[&str] = &[
88 "incremental",
89 "linker",
90 "link-arg",
91 "link-args",
92 "save-temps",
93 "remark",
94];
95
96pub fn parse_rustc_args(args: &[String], cwd: &Path) -> RustcParsedArgs {
101 let mut result = RustcParsedArgs {
102 source_file: NormalizedPath::new(""),
103 crate_name: None,
104 crate_types: Vec::new(),
105 edition: None,
106 emit_types: Vec::new(),
107 cfgs: Vec::new(),
108 check_cfgs: Vec::new(),
109 codegen_flags: Vec::new(),
110 target: None,
111 cap_lints: None,
112 externs: Vec::new(),
113 lint_flags: Vec::new(),
114 unknown_flags: Vec::new(),
115 out_dir: None,
116 extra_filename: None,
117 cargo_metadata: None,
118 incremental_dir: None,
119 error_format: None,
120 json_format: None,
121 color: None,
122 diagnostic_width: None,
123 search_paths: Vec::new(),
124 remap_path_prefixes: Vec::new(),
125 sysroot: None,
126 output_file: None,
127 };
128
129 let mut i = 0;
130 while i < args.len() {
131 let arg = &args[i];
132
133 if let Some(val) = take_option(arg, "--edition", args.get(i + 1), &mut i) {
135 result.edition = Some(val);
136 continue;
137 }
138
139 if let Some(val) = take_option(arg, "--crate-type", args.get(i + 1), &mut i) {
142 result
143 .crate_types
144 .extend(val.split(',').map(|s| s.to_string()));
145 continue;
146 }
147
148 if let Some(val) = take_option(arg, "--crate-name", args.get(i + 1), &mut i) {
150 result.crate_name = Some(val);
151 continue;
152 }
153
154 if let Some(val) = take_option(arg, "--emit", args.get(i + 1), &mut i) {
156 for part in val.split(',') {
157 let emit_type = part.split('=').next().unwrap_or(part).to_string();
159 if !result.emit_types.contains(&emit_type) {
160 result.emit_types.push(emit_type);
161 }
162 }
163 continue;
164 }
165
166 if let Some(val) = take_option(arg, "--target", args.get(i + 1), &mut i) {
168 result.target = Some(val);
169 continue;
170 }
171
172 if let Some(val) = take_option(arg, "--cap-lints", args.get(i + 1), &mut i) {
174 result.cap_lints = Some(val);
175 continue;
176 }
177
178 if let Some(val) = take_option(arg, "--cfg", args.get(i + 1), &mut i) {
180 result.cfgs.push(val);
181 continue;
182 }
183
184 if let Some(val) = take_option(arg, "--check-cfg", args.get(i + 1), &mut i) {
186 result.check_cfgs.push(val);
187 continue;
188 }
189
190 if let Some(val) = take_option(arg, "--extern", args.get(i + 1), &mut i) {
192 if let Some((name, path)) = val.split_once('=') {
193 let actual_name = name.strip_prefix("noprelude:").unwrap_or(name);
195 result.externs.push(ExternCrate {
196 name: actual_name.to_string(),
197 path: resolve_path(path, cwd),
198 });
199 }
200 continue;
202 }
203
204 if let Some(val) = take_option(arg, "--out-dir", args.get(i + 1), &mut i) {
206 result.out_dir = Some(resolve_path(&val, cwd));
207 continue;
208 }
209
210 if let Some(val) = take_option(arg, "--error-format", args.get(i + 1), &mut i) {
212 result.error_format = Some(val);
213 continue;
214 }
215
216 if let Some(val) = take_option(arg, "--json", args.get(i + 1), &mut i) {
218 result.json_format = Some(val);
219 continue;
220 }
221
222 if let Some(val) = take_option(arg, "--color", args.get(i + 1), &mut i) {
224 result.color = Some(val);
225 continue;
226 }
227
228 if let Some(val) = take_option(arg, "--diagnostic-width", args.get(i + 1), &mut i) {
230 result.diagnostic_width = Some(val);
231 continue;
232 }
233
234 if let Some(val) = take_option(arg, "--sysroot", args.get(i + 1), &mut i) {
236 result.sysroot = Some(resolve_path(&val, cwd));
237 continue;
238 }
239
240 if let Some(val) = take_option(arg, "--remap-path-prefix", args.get(i + 1), &mut i) {
242 result.remap_path_prefixes.push(val);
243 continue;
244 }
245
246 if let Some(_val) = take_option(arg, "--env-set", args.get(i + 1), &mut i) {
248 continue;
249 }
250
251 if arg == "-o" {
253 if let Some(next) = args.get(i + 1) {
254 result.output_file = Some(resolve_path(next, cwd));
255 i += 2;
256 continue;
257 }
258 }
259
260 if arg == "-L" {
262 if let Some(next) = args.get(i + 1) {
263 let path_str = next.split_once('=').map(|(_, p)| p).unwrap_or(next);
265 result.search_paths.push(resolve_path(path_str, cwd));
266 i += 2;
267 continue;
268 }
269 } else if let Some(rest) = arg.strip_prefix("-L") {
270 if !rest.is_empty() {
271 let path_str = rest.split_once('=').map(|(_, p)| p).unwrap_or(rest);
272 result.search_paths.push(resolve_path(path_str, cwd));
273 i += 1;
274 continue;
275 }
276 }
277
278 if arg == "-C" || arg == "--codegen" {
280 if let Some(next) = args.get(i + 1) {
281 handle_codegen_option(next, cwd, &mut result);
282 i += 2;
283 continue;
284 }
285 } else if let Some(rest) = arg.strip_prefix("-C") {
286 if !rest.is_empty() {
287 handle_codegen_option(rest, cwd, &mut result);
288 i += 1;
289 continue;
290 }
291 }
292
293 if matches!(arg.as_str(), "-A" | "-W" | "-D" | "-F") {
295 if let Some(next) = args.get(i + 1) {
296 result.lint_flags.push(format!("{arg} {next}"));
297 i += 2;
298 continue;
299 }
300 }
301
302 if arg == "-Z" {
304 if let Some(next) = args.get(i + 1) {
305 result.unknown_flags.push(format!("-Z {next}"));
306 i += 2;
307 continue;
308 }
309 }
310
311 if arg.starts_with('-') {
313 result.unknown_flags.push(arg.clone());
314 i += 1;
315 continue;
316 }
317
318 if arg.ends_with(".rs") {
320 result.source_file = resolve_path(arg, cwd);
321 }
322
323 i += 1;
324 }
325
326 result.cfgs.sort();
328 result.check_cfgs.sort();
329 result.codegen_flags.sort();
330 result.lint_flags.sort();
331 result.unknown_flags.sort();
332
333 result
334}
335
336fn take_option(arg: &str, flag: &str, next: Option<&String>, i: &mut usize) -> Option<String> {
339 if arg == flag {
340 if let Some(next_val) = next {
341 *i += 2;
342 return Some(next_val.clone());
343 }
344 } else if let Some(val) = arg.strip_prefix(&format!("{flag}=")) {
345 *i += 1;
346 return Some(val.to_string());
347 }
348 None
349}
350
351fn handle_codegen_option(opt: &str, cwd: &Path, result: &mut RustcParsedArgs) {
353 let (key, value) = opt.split_once('=').unwrap_or((opt, ""));
354
355 if key == "metadata" {
357 result.cargo_metadata = Some(value.to_string());
358 return;
359 }
360 if key == "extra-filename" {
361 result.extra_filename = Some(value.to_string());
362 return;
363 }
364 if key == "incremental" {
365 result.incremental_dir = Some(resolve_path(value, cwd));
366 return;
367 }
368 if EXCLUDED_CODEGEN.contains(&key) {
369 return;
370 }
371
372 result.codegen_flags.push(opt.to_string());
374}
375
376fn resolve_path(path: &str, cwd: &Path) -> NormalizedPath {
378 let p = Path::new(path);
379 if p.is_absolute() {
380 NormalizedPath::new(p)
381 } else {
382 NormalizedPath::new(cwd.join(p))
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use zccache_core::NormalizedPath;
389
390 use super::*;
391
392 fn args(s: &[&str]) -> Vec<String> {
393 s.iter().map(|x| x.to_string()).collect()
394 }
395
396 fn cwd() -> NormalizedPath {
397 NormalizedPath::from("/project")
398 }
399
400 #[test]
401 fn basic_parse_source_file() {
402 let parsed = parse_rustc_args(&args(&["src/lib.rs"]), &cwd());
403 assert_eq!(
404 parsed.source_file,
405 NormalizedPath::from("/project/src/lib.rs")
406 );
407 }
408
409 #[test]
410 fn parse_edition() {
411 let parsed = parse_rustc_args(&args(&["--edition", "2021", "src/lib.rs"]), &cwd());
412 assert_eq!(parsed.edition.as_deref(), Some("2021"));
413 }
414
415 #[test]
416 fn parse_edition_equals_form() {
417 let parsed = parse_rustc_args(&args(&["--edition=2021", "src/lib.rs"]), &cwd());
418 assert_eq!(parsed.edition.as_deref(), Some("2021"));
419 }
420
421 #[test]
422 fn parse_crate_type() {
423 let parsed = parse_rustc_args(
424 &args(&["--crate-type", "lib", "--crate-type", "rlib", "src/lib.rs"]),
425 &cwd(),
426 );
427 assert_eq!(parsed.crate_types, vec!["lib", "rlib"]);
428 }
429
430 #[test]
431 fn parse_crate_name() {
432 let parsed = parse_rustc_args(&args(&["--crate-name", "mylib", "src/lib.rs"]), &cwd());
433 assert_eq!(parsed.crate_name.as_deref(), Some("mylib"));
434 }
435
436 #[test]
437 fn parse_emit_types() {
438 let parsed = parse_rustc_args(
439 &args(&["--emit=dep-info,metadata,link", "src/lib.rs"]),
440 &cwd(),
441 );
442 assert_eq!(parsed.emit_types, vec!["dep-info", "metadata", "link"]);
443 }
444
445 #[test]
446 fn parse_emit_with_paths() {
447 let parsed = parse_rustc_args(
449 &args(&["--emit=dep-info=/tmp/deps.d,metadata,link", "src/lib.rs"]),
450 &cwd(),
451 );
452 assert_eq!(parsed.emit_types, vec!["dep-info", "metadata", "link"]);
453 }
454
455 #[test]
456 fn parse_cfg_values() {
457 let parsed = parse_rustc_args(
458 &args(&["--cfg", "feature=\"derive\"", "--cfg", "unix", "src/lib.rs"]),
459 &cwd(),
460 );
461 assert_eq!(parsed.cfgs, vec!["feature=\"derive\"", "unix"]);
463 }
464
465 #[test]
466 fn parse_codegen_flags() {
467 let parsed = parse_rustc_args(
468 &args(&["-C", "opt-level=2", "-C", "debuginfo=2", "src/lib.rs"]),
469 &cwd(),
470 );
471 assert!(parsed.codegen_flags.contains(&"debuginfo=2".to_string()));
473 assert!(parsed.codegen_flags.contains(&"opt-level=2".to_string()));
474 }
475
476 #[test]
477 fn parse_codegen_concatenated() {
478 let parsed = parse_rustc_args(&args(&["-Copt-level=3", "src/lib.rs"]), &cwd());
479 assert!(parsed.codegen_flags.contains(&"opt-level=3".to_string()));
480 }
481
482 #[test]
483 fn excluded_codegen_not_in_cache_key() {
484 let parsed = parse_rustc_args(
485 &args(&[
486 "-C",
487 "metadata=abc123",
488 "-C",
489 "extra-filename=-abc123",
490 "-C",
491 "incremental=/tmp/incr",
492 "-C",
493 "linker=cc",
494 "src/lib.rs",
495 ]),
496 &cwd(),
497 );
498 assert!(parsed.codegen_flags.is_empty());
500 assert_eq!(parsed.cargo_metadata.as_deref(), Some("abc123"));
502 assert_eq!(parsed.extra_filename.as_deref(), Some("-abc123"));
503 assert_eq!(
504 parsed.incremental_dir,
505 Some(NormalizedPath::from("/tmp/incr"))
506 );
507 }
508
509 #[test]
510 fn parse_extern_crates() {
511 let parsed = parse_rustc_args(
512 &args(&[
513 "--extern",
514 "serde=/target/deps/libserde.rlib",
515 "--extern",
516 "log=/target/deps/liblog.rmeta",
517 "src/lib.rs",
518 ]),
519 &cwd(),
520 );
521 assert_eq!(parsed.externs.len(), 2);
522 assert_eq!(parsed.externs[0].name, "serde");
523 assert_eq!(
524 parsed.externs[0].path,
525 NormalizedPath::from("/target/deps/libserde.rlib")
526 );
527 assert_eq!(parsed.externs[1].name, "log");
528 }
529
530 #[test]
531 fn parse_extern_noprelude() {
532 let parsed = parse_rustc_args(
533 &args(&[
534 "--extern",
535 "noprelude:core=/path/libcore.rlib",
536 "src/lib.rs",
537 ]),
538 &cwd(),
539 );
540 assert_eq!(parsed.externs[0].name, "core");
541 }
542
543 #[test]
544 fn search_paths_excluded_from_cache_key() {
545 let parsed = parse_rustc_args(
546 &args(&[
547 "-L",
548 "dependency=/target/deps",
549 "-L",
550 "native=/usr/lib",
551 "src/lib.rs",
552 ]),
553 &cwd(),
554 );
555 assert_eq!(parsed.search_paths.len(), 2);
556 assert!(parsed.codegen_flags.is_empty());
558 assert!(parsed.unknown_flags.is_empty());
559 }
560
561 #[test]
562 fn out_dir_excluded_from_cache_key() {
563 let parsed = parse_rustc_args(
564 &args(&["--out-dir", "/target/debug/deps", "src/lib.rs"]),
565 &cwd(),
566 );
567 assert_eq!(
568 parsed.out_dir,
569 Some(NormalizedPath::from("/target/debug/deps"))
570 );
571 assert!(parsed.unknown_flags.is_empty());
572 }
573
574 #[test]
575 fn cosmetic_flags_excluded() {
576 let parsed = parse_rustc_args(
577 &args(&[
578 "--error-format=json",
579 "--json=diagnostic-rendered-ansi",
580 "--color=always",
581 "--diagnostic-width=80",
582 "src/lib.rs",
583 ]),
584 &cwd(),
585 );
586 assert_eq!(parsed.error_format.as_deref(), Some("json"));
587 assert_eq!(
588 parsed.json_format.as_deref(),
589 Some("diagnostic-rendered-ansi")
590 );
591 assert_eq!(parsed.color.as_deref(), Some("always"));
592 assert_eq!(parsed.diagnostic_width.as_deref(), Some("80"));
593 assert!(parsed.unknown_flags.is_empty());
595 }
596
597 #[test]
598 fn parse_target() {
599 let parsed = parse_rustc_args(
600 &args(&["--target", "x86_64-unknown-linux-gnu", "src/lib.rs"]),
601 &cwd(),
602 );
603 assert_eq!(parsed.target.as_deref(), Some("x86_64-unknown-linux-gnu"));
604 }
605
606 #[test]
607 fn parse_cap_lints() {
608 let parsed = parse_rustc_args(&args(&["--cap-lints", "allow", "src/lib.rs"]), &cwd());
609 assert_eq!(parsed.cap_lints.as_deref(), Some("allow"));
610 }
611
612 #[test]
613 fn parse_lint_flags() {
614 let parsed = parse_rustc_args(
615 &args(&[
616 "-A",
617 "dead_code",
618 "-W",
619 "unused",
620 "-D",
621 "warnings",
622 "src/lib.rs",
623 ]),
624 &cwd(),
625 );
626 assert_eq!(parsed.lint_flags.len(), 3);
627 assert!(parsed.lint_flags.contains(&"-A dead_code".to_string()));
628 assert!(parsed.lint_flags.contains(&"-D warnings".to_string()));
629 assert!(parsed.lint_flags.contains(&"-W unused".to_string()));
630 }
631
632 #[test]
633 fn parse_output_file() {
634 let parsed = parse_rustc_args(&args(&["-o", "libfoo.rlib", "src/lib.rs"]), &cwd());
635 assert_eq!(
636 parsed.output_file,
637 Some(NormalizedPath::from("/project/libfoo.rlib"))
638 );
639 }
640
641 #[test]
642 fn full_cargo_invocation() {
643 let parsed = parse_rustc_args(
644 &args(&[
645 "--edition",
646 "2021",
647 "--crate-type",
648 "lib",
649 "--crate-name",
650 "serde",
651 "--emit=dep-info,metadata,link",
652 "-C",
653 "opt-level=2",
654 "-C",
655 "metadata=abc123def",
656 "-C",
657 "extra-filename=-abc123def",
658 "--out-dir",
659 "/target/release/deps",
660 "-L",
661 "dependency=/target/release/deps",
662 "--extern",
663 "serde_derive=/target/release/deps/libserde_derive-xyz.so",
664 "--cap-lints",
665 "allow",
666 "--cfg",
667 "feature=\"derive\"",
668 "--cfg",
669 "feature=\"std\"",
670 "--error-format=json",
671 "--json=diagnostic-rendered-ansi,artifacts,future-incompat",
672 "--diagnostic-width=211",
673 "-C",
674 "linker=cc",
675 "src/lib.rs",
676 ]),
677 &cwd(),
678 );
679
680 assert_eq!(parsed.edition.as_deref(), Some("2021"));
682 assert_eq!(parsed.crate_types, vec!["lib"]);
683 assert_eq!(parsed.crate_name.as_deref(), Some("serde"));
684 assert_eq!(parsed.emit_types, vec!["dep-info", "metadata", "link"]);
685 assert!(parsed.codegen_flags.contains(&"opt-level=2".to_string()));
686 assert_eq!(parsed.cap_lints.as_deref(), Some("allow"));
687 assert!(parsed.cfgs.contains(&"feature=\"derive\"".to_string()));
688 assert!(parsed.cfgs.contains(&"feature=\"std\"".to_string()));
689 assert_eq!(parsed.externs.len(), 1);
690 assert_eq!(parsed.externs[0].name, "serde_derive");
691
692 assert_eq!(parsed.cargo_metadata.as_deref(), Some("abc123def"));
694 assert_eq!(parsed.extra_filename.as_deref(), Some("-abc123def"));
695 assert_eq!(parsed.error_format.as_deref(), Some("json"));
696 assert!(parsed.search_paths.len() == 1);
697 assert!(parsed.unknown_flags.is_empty());
698 }
699
700 #[test]
701 fn z_flag_with_value_captured() {
702 let parsed = parse_rustc_args(
703 &args(&["-Z", "macro-backtrace", "--crate-type", "lib", "src/lib.rs"]),
704 &cwd(),
705 );
706 assert!(
708 parsed
709 .unknown_flags
710 .contains(&"-Z macro-backtrace".to_string()),
711 "got: {:?}",
712 parsed.unknown_flags
713 );
714 }
715
716 #[test]
717 fn z_flag_different_values_different_keys() {
718 let parsed1 = parse_rustc_args(&args(&["-Z", "query-threads=4", "src/lib.rs"]), &cwd());
719 let parsed2 = parse_rustc_args(&args(&["-Z", "query-threads=8", "src/lib.rs"]), &cwd());
720 assert_ne!(parsed1.unknown_flags, parsed2.unknown_flags);
721 }
722
723 #[test]
724 fn comma_separated_crate_types_split() {
725 let parsed = parse_rustc_args(&args(&["--crate-type", "lib,rlib", "src/lib.rs"]), &cwd());
726 assert_eq!(parsed.crate_types, vec!["lib", "rlib"]);
727 }
728
729 #[test]
730 fn relative_paths_resolved_against_cwd() {
731 let parsed = parse_rustc_args(&args(&["src/lib.rs"]), &cwd());
732 assert_eq!(
733 parsed.source_file,
734 NormalizedPath::from("/project/src/lib.rs")
735 );
736 }
737
738 #[test]
739 fn absolute_paths_unchanged() {
740 let parsed = parse_rustc_args(&args(&["/absolute/src/lib.rs"]), &cwd());
741 assert_eq!(
742 parsed.source_file,
743 NormalizedPath::from("/absolute/src/lib.rs")
744 );
745 }
746
747 #[test]
748 fn check_cfg_parsed() {
749 let parsed = parse_rustc_args(
750 &args(&["--check-cfg", "cfg(feature, values(\"std\"))", "src/lib.rs"]),
751 &cwd(),
752 );
753 assert_eq!(parsed.check_cfgs.len(), 1);
754 }
755
756 #[test]
757 fn sysroot_parsed() {
758 let parsed = parse_rustc_args(
759 &args(&[
760 "--sysroot",
761 "/home/user/.rustup/toolchains/stable",
762 "src/lib.rs",
763 ]),
764 &cwd(),
765 );
766 assert_eq!(
767 parsed.sysroot,
768 Some(NormalizedPath::from("/home/user/.rustup/toolchains/stable"))
769 );
770 }
771
772 #[test]
773 fn remap_path_prefix_parsed() {
774 let parsed = parse_rustc_args(
775 &args(&["--remap-path-prefix", "/home/user=/anon", "src/lib.rs"]),
776 &cwd(),
777 );
778 assert_eq!(parsed.remap_path_prefixes, vec!["/home/user=/anon"]);
779 }
780
781 #[test]
782 fn remap_path_prefix_equals_form_parsed() {
783 let parsed = parse_rustc_args(
784 &args(&["--remap-path-prefix=/home/user=/anon", "src/lib.rs"]),
785 &cwd(),
786 );
787 assert_eq!(parsed.remap_path_prefixes, vec!["/home/user=/anon"]);
788 }
789}