Rust · 29627 bytes Raw Blame History
1 //! Hand-rolled CLI parser. No clap.
2 //!
3 //! Sprint 0 recognizes a minimal set (`-o`, `-e`, `-arch`, positional inputs)
4 //! and errors on anything else with a precise diagnostic. Sprint 19 grows this
5 //! into the full `ld`-compatible surface described in `.docs/sprints/sprint19.md`.
6
7 use std::path::PathBuf;
8
9 use crate::resolve::{levenshtein, UndefinedTreatment};
10 use crate::{FrameworkSpec, IcfMode, LinkOptions, OutputKind, PlatformVersion, ThunkMode};
11
12 const KNOWN_FLAGS: &[&str] = &[
13 "-o",
14 "-e",
15 "-arch",
16 "-l",
17 "-L",
18 "-framework",
19 "-weak_framework",
20 "-ObjC",
21 "-syslibroot",
22 "-platform_version",
23 "-r",
24 "-bundle",
25 "-undefined",
26 "-rpath",
27 "-install_name",
28 "-current_version",
29 "-compatibility_version",
30 "-exported_symbols_list",
31 "-unexported_symbols_list",
32 "-exported_symbol",
33 "-unexported_symbol",
34 "-S",
35 "-no_uuid",
36 "-no_loh",
37 "-thunks=none",
38 "-thunks=safe",
39 "-thunks=all",
40 "-dead_strip",
41 "-icf=safe",
42 "-icf=none",
43 "-icf=all",
44 "-fixup_chains",
45 "-no_fixup_chains",
46 "-map",
47 "-why_live",
48 "-t",
49 "-trace",
50 "-v",
51 "--version",
52 "-h",
53 "--help",
54 "-x",
55 "-dylib",
56 "-all_load",
57 "-force_load",
58 "-j",
59 "--dump",
60 "--dump-archive",
61 "--dump-dylib",
62 "--dump-tbd",
63 ];
64
65 #[derive(Debug)]
66 pub enum ArgsError {
67 /// A flag that takes an argument was supplied without one.
68 MissingValue(String),
69 /// A recognized flag got a value we do not accept.
70 InvalidValue {
71 flag: String,
72 value: String,
73 expected: String,
74 },
75 /// An unrecognized flag.
76 UnknownFlag {
77 flag: String,
78 suggestion: Option<String>,
79 },
80 }
81
82 impl std::fmt::Display for ArgsError {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 match self {
85 ArgsError::MissingValue(flag) => {
86 write!(f, "flag `{flag}` requires a value")
87 }
88 ArgsError::InvalidValue {
89 flag,
90 value,
91 expected,
92 } => {
93 write!(
94 f,
95 "flag `{flag}` got invalid value `{value}` (expected {expected})"
96 )
97 }
98 ArgsError::UnknownFlag { flag, suggestion } => {
99 write!(f, "unknown flag `{flag}`")?;
100 if let Some(suggestion) = suggestion {
101 write!(f, " (did you mean `{suggestion}`?)")?;
102 }
103 write!(f, " (Sprint 19 adds the full `ld` surface)")
104 }
105 }
106 }
107 }
108
109 fn unknown_flag(flag: &str) -> ArgsError {
110 let suggestion = KNOWN_FLAGS
111 .iter()
112 .map(|candidate| (levenshtein(flag, candidate), *candidate))
113 .filter(|(distance, _)| *distance <= 3)
114 .min_by_key(|(distance, candidate)| (*distance, candidate.len()))
115 .map(|(_, candidate)| candidate.to_string());
116 ArgsError::UnknownFlag {
117 flag: flag.to_string(),
118 suggestion,
119 }
120 }
121
122 fn parse_version_component(flag: &str, value: &str) -> Result<u32, ArgsError> {
123 let mut parts = value.split('.');
124 let parse_part = |piece: Option<&str>| -> Result<u32, ArgsError> {
125 let raw = piece.unwrap_or("0");
126 raw.parse::<u32>().map_err(|_| ArgsError::InvalidValue {
127 flag: flag.to_string(),
128 value: value.to_string(),
129 expected: "version like <major>[.<minor>[.<patch>]]".into(),
130 })
131 };
132 let major = parse_part(parts.next())?;
133 let minor = parse_part(parts.next())?;
134 let patch = parse_part(parts.next())?;
135 if parts.next().is_some() {
136 return Err(ArgsError::InvalidValue {
137 flag: flag.to_string(),
138 value: value.to_string(),
139 expected: "version like <major>[.<minor>[.<patch>]]".into(),
140 });
141 }
142 Ok((major << 16) | ((minor & 0xff) << 8) | (patch & 0xff))
143 }
144
145 fn parse_jobs(value: &str) -> Result<usize, ArgsError> {
146 let jobs = value
147 .parse::<usize>()
148 .map_err(|_| ArgsError::InvalidValue {
149 flag: "-j".into(),
150 value: value.to_string(),
151 expected: "positive integer job count".into(),
152 })?;
153 if jobs == 0 {
154 return Err(ArgsError::InvalidValue {
155 flag: "-j".into(),
156 value: value.to_string(),
157 expected: "positive integer job count".into(),
158 });
159 }
160 Ok(jobs)
161 }
162
163 pub fn parse(argv: &[String]) -> Result<LinkOptions, ArgsError> {
164 let normalized = normalize_wl(argv);
165 let mut opts = LinkOptions::default();
166 let mut it = normalized.iter();
167 while let Some(arg) = it.next() {
168 match arg.as_str() {
169 "-o" => {
170 opts.output = Some(PathBuf::from(
171 it.next()
172 .ok_or_else(|| ArgsError::MissingValue("-o".into()))?,
173 ));
174 }
175 "-e" => {
176 opts.entry = Some(
177 it.next()
178 .ok_or_else(|| ArgsError::MissingValue("-e".into()))?
179 .clone(),
180 );
181 }
182 "-arch" => {
183 opts.arch = Some(
184 it.next()
185 .ok_or_else(|| ArgsError::MissingValue("-arch".into()))?
186 .clone(),
187 );
188 }
189 "-l" => {
190 opts.library_names.push(
191 it.next()
192 .ok_or_else(|| ArgsError::MissingValue("-l".into()))?
193 .clone(),
194 );
195 }
196 s if s.starts_with("-l") && s.len() > 2 => {
197 opts.library_names.push(s[2..].to_string());
198 }
199 "-L" => {
200 opts.search_paths.push(PathBuf::from(
201 it.next()
202 .ok_or_else(|| ArgsError::MissingValue("-L".into()))?,
203 ));
204 }
205 "-framework" => {
206 opts.frameworks.push(FrameworkSpec {
207 name: it
208 .next()
209 .ok_or_else(|| ArgsError::MissingValue("-framework".into()))?
210 .clone(),
211 weak: false,
212 });
213 }
214 "-weak_framework" => {
215 opts.frameworks.push(FrameworkSpec {
216 name: it
217 .next()
218 .ok_or_else(|| ArgsError::MissingValue("-weak_framework".into()))?
219 .clone(),
220 weak: true,
221 });
222 }
223 "-ObjC" => {
224 opts.objc_force_load = true;
225 }
226 "-syslibroot" => {
227 opts.syslibroot =
228 Some(PathBuf::from(it.next().ok_or_else(|| {
229 ArgsError::MissingValue("-syslibroot".into())
230 })?));
231 }
232 "-platform_version" => {
233 let platform = it
234 .next()
235 .ok_or_else(|| ArgsError::MissingValue("-platform_version".into()))?;
236 if platform != "macos" {
237 return Err(ArgsError::InvalidValue {
238 flag: "-platform_version".into(),
239 value: platform.clone(),
240 expected: "platform `macos`".into(),
241 });
242 }
243 let minos_raw = it
244 .next()
245 .ok_or_else(|| ArgsError::MissingValue("-platform_version".into()))?;
246 let sdk_raw = it
247 .next()
248 .ok_or_else(|| ArgsError::MissingValue("-platform_version".into()))?;
249 opts.platform_version = Some(PlatformVersion {
250 minos: parse_version_component("-platform_version", minos_raw)?,
251 sdk: parse_version_component("-platform_version", sdk_raw)?,
252 });
253 }
254 "-r" => {
255 opts.relocatable = true;
256 }
257 "-bundle" => {
258 opts.bundle = true;
259 }
260 "-undefined" => {
261 let value = it
262 .next()
263 .ok_or_else(|| ArgsError::MissingValue("-undefined".into()))?;
264 opts.undefined_treatment = match value.as_str() {
265 "error" => UndefinedTreatment::Error,
266 "warning" => UndefinedTreatment::Warning,
267 "suppress" => UndefinedTreatment::Suppress,
268 "dynamic_lookup" => UndefinedTreatment::DynamicLookup,
269 _ => {
270 return Err(ArgsError::InvalidValue {
271 flag: "-undefined".into(),
272 value: value.clone(),
273 expected: "`error`, `warning`, `suppress`, or `dynamic_lookup`".into(),
274 });
275 }
276 };
277 }
278 "-rpath" => {
279 opts.rpaths.push(
280 it.next()
281 .ok_or_else(|| ArgsError::MissingValue("-rpath".into()))?
282 .clone(),
283 );
284 }
285 "-install_name" => {
286 opts.install_name = Some(
287 it.next()
288 .ok_or_else(|| ArgsError::MissingValue("-install_name".into()))?
289 .clone(),
290 );
291 }
292 "-current_version" => {
293 let value = it
294 .next()
295 .ok_or_else(|| ArgsError::MissingValue("-current_version".into()))?;
296 opts.current_version = Some(parse_version_component("-current_version", value)?);
297 }
298 "-compatibility_version" => {
299 let value = it
300 .next()
301 .ok_or_else(|| ArgsError::MissingValue("-compatibility_version".into()))?;
302 opts.compatibility_version =
303 Some(parse_version_component("-compatibility_version", value)?);
304 }
305 "-exported_symbols_list" => {
306 opts.exported_symbols_lists
307 .push(PathBuf::from(it.next().ok_or_else(|| {
308 ArgsError::MissingValue("-exported_symbols_list".into())
309 })?));
310 }
311 "-unexported_symbols_list" => {
312 opts.unexported_symbols_lists.push(PathBuf::from(
313 it.next().ok_or_else(|| {
314 ArgsError::MissingValue("-unexported_symbols_list".into())
315 })?,
316 ));
317 }
318 "-exported_symbol" => {
319 opts.exported_symbols.push(
320 it.next()
321 .ok_or_else(|| ArgsError::MissingValue("-exported_symbol".into()))?
322 .clone(),
323 );
324 }
325 "-unexported_symbol" => {
326 opts.unexported_symbols.push(
327 it.next()
328 .ok_or_else(|| ArgsError::MissingValue("-unexported_symbol".into()))?
329 .clone(),
330 );
331 }
332 "-S" => {
333 opts.strip_debug = true;
334 }
335 "-no_uuid" => {
336 opts.emit_uuid = false;
337 }
338 "-no_loh" => {
339 opts.no_loh = true;
340 }
341 s if s.starts_with("-thunks=") => {
342 opts.thunks = match s {
343 "-thunks=none" => ThunkMode::None,
344 "-thunks=safe" => ThunkMode::Safe,
345 "-thunks=all" => ThunkMode::All,
346 _ => {
347 let value = s.trim_start_matches("-thunks=").to_string();
348 return Err(ArgsError::InvalidValue {
349 flag: "-thunks".into(),
350 value,
351 expected: "`none`, `safe`, or `all`".into(),
352 });
353 }
354 };
355 }
356 "-dead_strip" => {
357 opts.dead_strip = true;
358 }
359 s if s.starts_with("-icf=") => {
360 opts.icf_mode = match s {
361 "-icf=none" => IcfMode::None,
362 "-icf=safe" => IcfMode::Safe,
363 "-icf=all" => IcfMode::All,
364 _ => {
365 let value = s.trim_start_matches("-icf=").to_string();
366 return Err(ArgsError::InvalidValue {
367 flag: "-icf".into(),
368 value,
369 expected: "`safe`, `none`, or `all`".into(),
370 });
371 }
372 };
373 }
374 "-fixup_chains" => {
375 opts.fixup_chains = true;
376 }
377 "-no_fixup_chains" => {
378 opts.fixup_chains = false;
379 }
380 "-map" => {
381 opts.map = Some(PathBuf::from(
382 it.next()
383 .ok_or_else(|| ArgsError::MissingValue("-map".into()))?,
384 ));
385 }
386 "-why_live" => {
387 opts.why_live.push(
388 it.next()
389 .ok_or_else(|| ArgsError::MissingValue("-why_live".into()))?
390 .clone(),
391 );
392 }
393 "-t" | "-trace" => {
394 opts.trace_inputs = true;
395 }
396 "-v" | "--version" => {
397 opts.show_version = true;
398 }
399 "-h" | "--help" => {
400 opts.show_help = true;
401 }
402 "-x" => {
403 opts.strip_locals = true;
404 }
405 "-dylib" => {
406 opts.kind = OutputKind::Dylib;
407 }
408 "-all_load" => {
409 opts.all_load = true;
410 }
411 "-force_load" => {
412 opts.force_load_archives
413 .push(PathBuf::from(it.next().ok_or_else(|| {
414 ArgsError::MissingValue("-force_load".into())
415 })?));
416 }
417 "-j" => {
418 let value = it
419 .next()
420 .ok_or_else(|| ArgsError::MissingValue("-j".into()))?;
421 opts.jobs = Some(parse_jobs(value)?);
422 }
423 "--dump" => {
424 opts.dump = Some(PathBuf::from(
425 it.next()
426 .ok_or_else(|| ArgsError::MissingValue("--dump".into()))?,
427 ));
428 }
429 "--dump-archive" => {
430 opts.dump_archive =
431 Some(PathBuf::from(it.next().ok_or_else(|| {
432 ArgsError::MissingValue("--dump-archive".into())
433 })?));
434 }
435 "--dump-dylib" => {
436 opts.dump_dylib =
437 Some(PathBuf::from(it.next().ok_or_else(|| {
438 ArgsError::MissingValue("--dump-dylib".into())
439 })?));
440 }
441 "--dump-tbd" => {
442 opts.dump_tbd =
443 Some(PathBuf::from(it.next().ok_or_else(|| {
444 ArgsError::MissingValue("--dump-tbd".into())
445 })?));
446 }
447 s if s.starts_with('-') => {
448 return Err(unknown_flag(s));
449 }
450 _ => {
451 opts.inputs.push(PathBuf::from(arg));
452 }
453 }
454 }
455 Ok(opts)
456 }
457
458 fn normalize_wl(argv: &[String]) -> Vec<String> {
459 let mut out = Vec::with_capacity(argv.len());
460 for arg in argv {
461 if let Some(rest) = arg.strip_prefix("-Wl,") {
462 out.extend(
463 rest.split(',')
464 .filter(|piece| !piece.is_empty())
465 .map(ToString::to_string),
466 );
467 } else {
468 out.push(arg.clone());
469 }
470 }
471 out
472 }
473
474 #[cfg(test)]
475 mod tests {
476 use super::*;
477
478 fn argv(words: &[&str]) -> Vec<String> {
479 words.iter().map(|s| s.to_string()).collect()
480 }
481
482 #[test]
483 fn parse_output_and_entry() {
484 let opts = parse(&argv(&["-o", "out", "-e", "_start", "a.o", "b.o"])).unwrap();
485 assert_eq!(opts.output.as_deref(), Some(std::path::Path::new("out")));
486 assert_eq!(opts.entry.as_deref(), Some("_start"));
487 assert_eq!(opts.inputs.len(), 2);
488 }
489
490 #[test]
491 fn dylib_flag_switches_output_kind() {
492 let opts = parse(&argv(&["-dylib", "foo.o"])).unwrap();
493 assert_eq!(opts.kind, OutputKind::Dylib);
494 }
495
496 #[test]
497 fn deferred_output_flags_are_recorded() {
498 let opts = parse(&argv(&["-r", "-bundle", "foo.o"])).unwrap();
499 assert!(opts.relocatable);
500 assert!(opts.bundle);
501 }
502
503 #[test]
504 fn strip_locals_flag_is_recorded() {
505 let opts = parse(&argv(&["-x", "foo.o"])).unwrap();
506 assert!(opts.strip_locals);
507 }
508
509 #[test]
510 fn strip_debug_and_uuid_flags_are_recorded() {
511 let opts = parse(&argv(&["-S", "-no_uuid", "foo.o"])).unwrap();
512 assert!(opts.strip_debug);
513 assert!(!opts.emit_uuid);
514 }
515
516 #[test]
517 fn no_loh_flag_is_recorded() {
518 let opts = parse(&argv(&["-no_loh", "foo.o"])).unwrap();
519 assert!(opts.no_loh);
520 }
521
522 #[test]
523 fn dead_strip_icf_and_fixup_chain_flags_are_recorded() {
524 let opts = parse(&argv(&[
525 "-dead_strip",
526 "-thunks=all",
527 "-icf=safe",
528 "-fixup_chains",
529 "-no_fixup_chains",
530 "-icf=none",
531 "foo.o",
532 ]))
533 .unwrap();
534 assert!(opts.dead_strip);
535 assert_eq!(opts.thunks, ThunkMode::All);
536 assert_eq!(opts.icf_mode, IcfMode::None);
537 assert!(!opts.fixup_chains);
538 }
539
540 #[test]
541 fn thunks_flag_rejects_unknown_modes() {
542 let err = parse(&argv(&["-thunks=clustered", "main.o"])).unwrap_err();
543 assert!(matches!(
544 err,
545 ArgsError::InvalidValue {
546 ref flag,
547 ref value,
548 ..
549 } if flag == "-thunks" && value == "clustered"
550 ));
551 }
552
553 #[test]
554 fn icf_all_flag_is_recorded() {
555 let opts = parse(&argv(&["-icf=all", "foo.o"])).unwrap();
556 assert_eq!(opts.icf_mode, IcfMode::All);
557 }
558
559 #[test]
560 fn icf_flag_rejects_unknown_modes() {
561 let err = parse(&argv(&["-icf=aggressive", "main.o"])).unwrap_err();
562 assert!(matches!(
563 err,
564 ArgsError::InvalidValue {
565 ref flag,
566 ref value,
567 ..
568 } if flag == "-icf" && value == "aggressive"
569 ));
570 }
571
572 #[test]
573 fn l_flag_accepts_separate_value() {
574 let opts = parse(&argv(&["-l", "System", "main.o"])).unwrap();
575 assert_eq!(opts.library_names, vec!["System".to_string()]);
576 assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
577 }
578
579 #[test]
580 fn l_flag_accepts_joined_value() {
581 let opts = parse(&argv(&["-lSystem", "main.o"])).unwrap();
582 assert_eq!(opts.library_names, vec!["System".to_string()]);
583 assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
584 }
585
586 #[test]
587 fn search_path_and_syslibroot_flags_are_recorded() {
588 let opts = parse(&argv(&["-L", "/tmp/lib", "-syslibroot", "/sdk", "main.o"])).unwrap();
589 assert_eq!(opts.search_paths, vec![PathBuf::from("/tmp/lib")]);
590 assert_eq!(opts.syslibroot, Some(PathBuf::from("/sdk")));
591 }
592
593 #[test]
594 fn framework_flags_are_recorded_in_order() {
595 let opts = parse(&argv(&[
596 "-framework",
597 "Foundation",
598 "-weak_framework",
599 "Metal",
600 "main.o",
601 ]))
602 .unwrap();
603 assert_eq!(
604 opts.frameworks,
605 vec![
606 FrameworkSpec {
607 name: "Foundation".into(),
608 weak: false,
609 },
610 FrameworkSpec {
611 name: "Metal".into(),
612 weak: true,
613 }
614 ]
615 );
616 assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
617 }
618
619 #[test]
620 fn objc_flag_is_recorded() {
621 let opts = parse(&argv(&["-ObjC", "main.o"])).unwrap();
622 assert!(opts.objc_force_load);
623 }
624
625 #[test]
626 fn platform_version_flag_is_recorded() {
627 let opts = parse(&argv(&[
628 "-platform_version",
629 "macos",
630 "13.2.1",
631 "14.5",
632 "main.o",
633 ]))
634 .unwrap();
635 let platform = opts.platform_version.expect("platform version");
636 assert_eq!(platform.minos, (13 << 16) | (2 << 8) | 1);
637 assert_eq!(platform.sdk, (14 << 16) | (5 << 8));
638 }
639
640 #[test]
641 fn platform_version_rejects_non_macos_platform() {
642 let err = parse(&argv(&["-platform_version", "ios", "13.0", "13.0"])).unwrap_err();
643 assert!(matches!(
644 err,
645 ArgsError::InvalidValue {
646 ref flag,
647 ref value,
648 ..
649 } if flag == "-platform_version" && value == "ios"
650 ));
651 }
652
653 #[test]
654 fn platform_version_rejects_bad_version() {
655 let err = parse(&argv(&["-platform_version", "macos", "13.bad", "14.0"])).unwrap_err();
656 assert!(matches!(
657 err,
658 ArgsError::InvalidValue {
659 ref flag,
660 ref value,
661 ..
662 } if flag == "-platform_version" && value == "13.bad"
663 ));
664 }
665
666 #[test]
667 fn undefined_flag_records_dynamic_lookup() {
668 let opts = parse(&argv(&["-undefined", "dynamic_lookup", "main.o"])).unwrap();
669 assert_eq!(opts.undefined_treatment, UndefinedTreatment::DynamicLookup);
670 }
671
672 #[test]
673 fn undefined_flag_records_warning_and_suppress() {
674 let warning = parse(&argv(&["-undefined", "warning", "main.o"])).unwrap();
675 assert_eq!(warning.undefined_treatment, UndefinedTreatment::Warning);
676
677 let suppress = parse(&argv(&["-undefined", "suppress", "main.o"])).unwrap();
678 assert_eq!(suppress.undefined_treatment, UndefinedTreatment::Suppress);
679 }
680
681 #[test]
682 fn undefined_flag_rejects_unknown_modes() {
683 let err = parse(&argv(&["-undefined", "bogus", "main.o"])).unwrap_err();
684 assert!(matches!(
685 err,
686 ArgsError::InvalidValue {
687 ref flag,
688 ref value,
689 ..
690 } if flag == "-undefined" && value == "bogus"
691 ));
692 }
693
694 #[test]
695 fn dylib_metadata_flags_are_recorded() {
696 let opts = parse(&argv(&[
697 "-rpath",
698 "@loader_path/../lib",
699 "-install_name",
700 "@rpath/libdemo.dylib",
701 "-current_version",
702 "2.3.4",
703 "-compatibility_version",
704 "1.2",
705 "main.o",
706 ]))
707 .unwrap();
708 assert_eq!(opts.rpaths, vec!["@loader_path/../lib".to_string()]);
709 assert_eq!(opts.install_name.as_deref(), Some("@rpath/libdemo.dylib"));
710 assert_eq!(opts.current_version, Some((2 << 16) | (3 << 8) | 4));
711 assert_eq!(opts.compatibility_version, Some((1 << 16) | (2 << 8)));
712 }
713
714 #[test]
715 fn export_visibility_flags_are_recorded() {
716 let opts = parse(&argv(&[
717 "-exported_symbols_list",
718 "exports.txt",
719 "-unexported_symbols_list",
720 "hidden.txt",
721 "-exported_symbol",
722 "_keep",
723 "-unexported_symbol",
724 "_drop",
725 "main.o",
726 ]))
727 .unwrap();
728 assert_eq!(
729 opts.exported_symbols_lists,
730 vec![PathBuf::from("exports.txt")]
731 );
732 assert_eq!(
733 opts.unexported_symbols_lists,
734 vec![PathBuf::from("hidden.txt")]
735 );
736 assert_eq!(opts.exported_symbols, vec!["_keep".to_string()]);
737 assert_eq!(opts.unexported_symbols, vec!["_drop".to_string()]);
738 }
739
740 #[test]
741 fn map_flag_is_recorded() {
742 let opts = parse(&argv(&["-map", "link.map", "main.o"])).unwrap();
743 assert_eq!(opts.map.as_deref(), Some(std::path::Path::new("link.map")));
744 }
745
746 #[test]
747 fn wl_normalizes_map_like_direct_flag() {
748 let opts = parse(&argv(&["-Wl,-map,link.map", "main.o"])).unwrap();
749 assert_eq!(opts.map.as_deref(), Some(std::path::Path::new("link.map")));
750 assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
751 }
752
753 #[test]
754 fn trace_flags_are_recorded() {
755 let opts = parse(&argv(&["-trace", "main.o"])).unwrap();
756 assert!(opts.trace_inputs);
757 let opts = parse(&argv(&["-t", "main.o"])).unwrap();
758 assert!(opts.trace_inputs);
759 }
760
761 #[test]
762 fn why_live_flag_accumulates_symbols() {
763 let opts = parse(&argv(&[
764 "-why_live",
765 "_helper",
766 "-why_live",
767 "_leaf",
768 "main.o",
769 ]))
770 .unwrap();
771 assert_eq!(
772 opts.why_live,
773 vec!["_helper".to_string(), "_leaf".to_string()]
774 );
775 }
776
777 #[test]
778 fn help_and_version_flags_are_recorded() {
779 let opts = parse(&argv(&["--help"])).unwrap();
780 assert!(opts.show_help);
781 let opts = parse(&argv(&["-h"])).unwrap();
782 assert!(opts.show_help);
783 let opts = parse(&argv(&["--version"])).unwrap();
784 assert!(opts.show_version);
785 let opts = parse(&argv(&["-v"])).unwrap();
786 assert!(opts.show_version);
787 }
788
789 #[test]
790 fn all_load_flag_is_recorded() {
791 let opts = parse(&argv(&["-all_load", "libfoo.a"])).unwrap();
792 assert!(opts.all_load);
793 assert_eq!(opts.inputs, vec![PathBuf::from("libfoo.a")]);
794 }
795
796 #[test]
797 fn force_load_flag_accumulates_archive_paths() {
798 let opts = parse(&argv(&[
799 "-force_load",
800 "liba.a",
801 "-force_load",
802 "libb.a",
803 "main.o",
804 ]))
805 .unwrap();
806 assert_eq!(
807 opts.force_load_archives,
808 vec![PathBuf::from("liba.a"), PathBuf::from("libb.a")]
809 );
810 assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
811 }
812
813 #[test]
814 fn jobs_flag_records_positive_worker_limit() {
815 let opts = parse(&argv(&["-j", "1", "main.o"])).unwrap();
816 assert_eq!(opts.jobs, Some(1));
817 assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
818 }
819
820 #[test]
821 fn jobs_flag_rejects_zero_or_non_numeric_values() {
822 let err = parse(&argv(&["-j", "0"])).unwrap_err();
823 assert!(matches!(
824 err,
825 ArgsError::InvalidValue {
826 ref flag,
827 ref value,
828 ..
829 } if flag == "-j" && value == "0"
830 ));
831 let err = parse(&argv(&["-j", "many"])).unwrap_err();
832 assert!(matches!(
833 err,
834 ArgsError::InvalidValue {
835 ref flag,
836 ref value,
837 ..
838 } if flag == "-j" && value == "many"
839 ));
840 }
841
842 #[test]
843 fn missing_jobs_value_errors() {
844 let err = parse(&argv(&["-j"])).unwrap_err();
845 assert!(matches!(err, ArgsError::MissingValue(ref f) if f == "-j"));
846 }
847
848 #[test]
849 fn missing_force_load_value_errors() {
850 let err = parse(&argv(&["-force_load"])).unwrap_err();
851 assert!(matches!(err, ArgsError::MissingValue(ref f) if f == "-force_load"));
852 }
853
854 #[test]
855 fn missing_l_value_errors() {
856 let err = parse(&argv(&["-l"])).unwrap_err();
857 assert!(matches!(err, ArgsError::MissingValue(ref f) if f == "-l"));
858 }
859
860 #[test]
861 fn missing_output_value_errors() {
862 let err = parse(&argv(&["-o"])).unwrap_err();
863 assert!(matches!(err, ArgsError::MissingValue(ref f) if f == "-o"));
864 }
865
866 #[test]
867 fn unknown_flag_errors() {
868 let err = parse(&argv(&["-nonsense"])).unwrap_err();
869 assert!(matches!(
870 err,
871 ArgsError::UnknownFlag {
872 ref flag,
873 suggestion: None
874 } if flag == "-nonsense"
875 ));
876 }
877
878 #[test]
879 fn unknown_flag_suggests_nearby_match() {
880 let err = parse(&argv(&["-all_lod"])).unwrap_err();
881 assert!(matches!(
882 err,
883 ArgsError::UnknownFlag {
884 ref flag,
885 suggestion: Some(ref suggestion)
886 } if flag == "-all_lod" && suggestion == "-all_load"
887 ));
888 }
889
890 #[test]
891 fn empty_argv_is_ok_with_no_inputs() {
892 let opts = parse(&[]).unwrap();
893 assert!(opts.inputs.is_empty());
894 }
895
896 #[test]
897 fn dump_flag_captures_path() {
898 let opts = parse(&argv(&["--dump", "some.o"])).unwrap();
899 assert_eq!(opts.dump.as_deref(), Some(std::path::Path::new("some.o")));
900 }
901
902 #[test]
903 fn dump_flag_without_value_errors() {
904 let err = parse(&argv(&["--dump"])).unwrap_err();
905 assert!(matches!(err, ArgsError::MissingValue(ref f) if f == "--dump"));
906 }
907
908 #[test]
909 fn dump_archive_flag_captures_path() {
910 let opts = parse(&argv(&["--dump-archive", "libfoo.a"])).unwrap();
911 assert_eq!(
912 opts.dump_archive.as_deref(),
913 Some(std::path::Path::new("libfoo.a"))
914 );
915 }
916 }
917