1use std::collections::{BTreeMap, BTreeSet};
64use std::path::Path;
65
66use codehelion_helper_protocol::compile_commands::resolve_in_directory;
67
68pub const EXCLUDED: [&str; 8] = [
73 "-c",
74 "-M",
75 "-MM",
76 "-MD",
77 "-MMD",
78 "-MP",
79 "-fcolor-diagnostics",
80 "-fno-color-diagnostics",
81];
82
83pub const EXCLUDED_WITH_VALUE: [&str; 4] = ["-o", "-MF", "-MT", "-MQ"];
89
90#[must_use]
93pub fn content_hash(text: &str) -> String {
94 blake3::hash(text.as_bytes()).to_hex().to_string()
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct Setting {
103 pub name: &'static str,
105 pub shape: Shape,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
115pub enum Shape {
116 Given(String),
118 Resolved(Option<String>),
120 Ordered(Vec<String>),
122}
123
124impl Shape {
125 #[must_use]
130 pub fn values(&self) -> Vec<&str> {
131 match self {
132 Self::Given(value) => vec![value.as_str()],
133 Self::Resolved(value) => value.as_deref().into_iter().collect(),
134 Self::Ordered(values) => values.iter().map(String::as_str).collect(),
135 }
136 }
137}
138
139fn given(name: &'static str, value: &str) -> Setting {
140 Setting {
141 name,
142 shape: Shape::Given(value.to_string()),
143 }
144}
145
146fn resolved(name: &'static str, value: Option<&str>) -> Setting {
147 Setting {
148 name,
149 shape: Shape::Resolved(value.map(ToString::to_string)),
150 }
151}
152
153fn ordered(name: &'static str, values: &[String]) -> Setting {
154 Setting {
155 name,
156 shape: Shape::Ordered(values.to_vec()),
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Default)]
162pub struct CppBuild {
163 pub compiler: String,
165 pub compiler_version: Option<String>,
167 pub linker: Option<String>,
169 pub macros: Vec<String>,
172 pub include_paths: Vec<String>,
174 pub flags: Vec<String>,
176 pub database_hash: Option<String>,
178 pub post_processing_tools: Vec<String>,
183}
184
185impl CppBuild {
186 #[must_use]
191 pub fn from_command(arguments: &[String], file: &Path) -> Self {
192 Self::from_command_in_directory(arguments, file, None)
193 }
194
195 #[must_use]
204 pub fn from_command_in_directory(
205 arguments: &[String],
206 file: &Path,
207 directory: Option<&Path>,
208 ) -> Self {
209 let mut build = Self {
210 compiler: arguments.first().cloned().unwrap_or_default(),
211 ..Self::default()
212 };
213 let mut macros = Vec::new();
214 let mut index = 1;
215 while index < arguments.len() {
216 let argument = arguments[index].as_str();
217 index += 1;
218 if source_argument_matches(argument, file, directory) {
219 continue;
220 }
221 if EXCLUDED_WITH_VALUE.contains(&argument) {
222 index += 1;
223 continue;
224 }
225 if EXCLUDED.contains(&argument) || argument.starts_with("-fdiagnostics-color") {
226 continue;
227 }
228 match separated(argument, arguments.get(index).map(String::as_str)) {
229 Some(Separated::Macro(setting, consumed)) => {
230 macros.push(setting);
231 index += usize::from(consumed);
232 }
233 Some(Separated::Include(path, consumed)) => {
234 build.include_paths.push(resolved_path(&path, directory));
235 index += usize::from(consumed);
236 }
237 Some(Separated::Reads(option, path, consumed)) => {
238 build.flags.push(option.to_string());
239 build.flags.push(resolved_path(&path, directory));
240 index += usize::from(consumed);
241 }
242 None => build.flags.push(argument.to_string()),
243 }
244 }
245 build.macros = last_mention_wins(macros);
246 build
247 }
248
249 #[must_use]
251 pub fn defines(&self) -> Vec<&str> {
252 self.macros
253 .iter()
254 .filter_map(|setting| setting.strip_prefix("-D"))
255 .collect()
256 }
257
258 #[must_use]
260 pub fn settings(&self) -> Vec<Setting> {
261 vec![
262 given("compiler", &self.compiler),
263 resolved("compiler_version", self.compiler_version.as_deref()),
264 resolved("linker", self.linker.as_deref()),
265 ordered("macros", &self.macros),
266 ordered("includes", &self.include_paths),
267 ordered("flags", &self.flags),
268 resolved("database", self.database_hash.as_deref()),
269 ordered("post_processing_tools", &self.post_processing_tools),
270 ]
271 }
272}
273
274fn source_argument_matches(argument: &str, file: &Path, directory: Option<&Path>) -> bool {
275 let resolved = resolve_in_directory(directory, Path::new(argument));
276 normalize_path(&resolved) == normalize_path(file)
277}
278
279fn resolved_path(path: &str, directory: Option<&Path>) -> String {
288 let path = Path::new(path);
289 if path.as_os_str().is_empty() {
293 return String::new();
294 }
295 if path.is_relative() && directory.is_none() {
296 return path.display().to_string();
299 }
300 normalize_path(&resolve_in_directory(directory, path))
301 .display()
302 .to_string()
303}
304
305fn normalize_path(path: &Path) -> std::path::PathBuf {
306 crate::paths::canonical(path).unwrap_or_else(|_| path.to_path_buf())
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Default)]
315pub struct RustBuild {
316 pub target: String,
318 pub features: Vec<String>,
325 pub cfgs: Vec<String>,
327 pub compiler_version: String,
329 pub opt_level: String,
331 pub lto: String,
333 pub codegen_units: Option<u32>,
335 pub panic: String,
337 pub lockfile_hash: Option<String>,
340 pub build_command_hash: Option<String>,
342 pub post_processing_tools: Vec<String>,
347 pub permitted_execution: Vec<String>,
358}
359
360impl RustBuild {
361 #[must_use]
363 pub fn normalized(mut self) -> Self {
364 self.features = self
365 .features
366 .into_iter()
367 .collect::<BTreeSet<_>>()
368 .into_iter()
369 .collect();
370 self.cfgs = self
371 .cfgs
372 .into_iter()
373 .collect::<BTreeSet<_>>()
374 .into_iter()
375 .collect();
376 self
377 }
378
379 #[must_use]
381 pub fn settings(&self) -> Vec<Setting> {
382 vec![
383 given("target", &self.target),
384 ordered("features", &self.features),
385 ordered("cfgs", &self.cfgs),
386 given("compiler_version", &self.compiler_version),
387 given("opt_level", &self.opt_level),
388 given("lto", &self.lto),
389 resolved(
390 "codegen_units",
391 self.codegen_units.map(|units| units.to_string()).as_deref(),
392 ),
393 given("panic", &self.panic),
394 resolved("lockfile", self.lockfile_hash.as_deref()),
395 resolved("build_command", self.build_command_hash.as_deref()),
396 ordered("post_processing_tools", &self.post_processing_tools),
397 ordered("permitted_execution", &self.permitted_execution),
398 ]
399 }
400}
401
402#[derive(Debug, Clone, PartialEq, Eq)]
404pub enum BuildConfiguration {
405 Rust(Box<RustBuild>),
407 Cpp(Box<CppBuild>),
409}
410
411impl BuildConfiguration {
412 #[must_use]
418 pub const fn language(&self) -> &'static str {
419 match self {
420 Self::Rust(_) => "rust",
421 Self::Cpp(_) => "cpp",
422 }
423 }
424
425 #[must_use]
427 pub fn settings(&self) -> Vec<Setting> {
428 match self {
429 Self::Rust(build) => build.settings(),
430 Self::Cpp(build) => build.settings(),
431 }
432 }
433
434 #[must_use]
439 pub fn canonical(&self) -> String {
440 let mut out = String::new();
441 scalar(&mut out, "language", self.language());
442 for setting in self.settings() {
443 match &setting.shape {
444 Shape::Given(value) => scalar(&mut out, setting.name, value),
445 Shape::Resolved(value) => optional(&mut out, setting.name, value.as_deref()),
446 Shape::Ordered(values) => list(&mut out, setting.name, values),
447 }
448 }
449 out
450 }
451
452 #[must_use]
454 pub fn fingerprint(&self) -> String {
455 blake3::hash(self.canonical().as_bytes())
456 .to_hex()
457 .to_string()
458 }
459}
460
461const READS_PATH: [&str; 9] = [
468 "--sysroot",
469 "-F",
470 "-idirafter",
471 "-iframework",
472 "-imacros",
473 "-include",
474 "-iquote",
475 "-isysroot",
476 "-isystem",
477];
478
479enum Separated {
481 Macro(String, bool),
483 Include(String, bool),
485 Reads(&'static str, String, bool),
488}
489
490fn separated(argument: &str, next: Option<&str>) -> Option<Separated> {
498 for prefix in ["-D", "-U"] {
499 if let Some(rest) = argument.strip_prefix(prefix) {
500 return Some(if rest.is_empty() {
501 Separated::Macro(format!("{prefix}{}", next.unwrap_or_default()), true)
502 } else {
503 Separated::Macro(argument.to_string(), false)
504 });
505 }
506 }
507 if let Some(rest) = argument.strip_prefix("-I") {
508 return Some(if rest.is_empty() {
509 Separated::Include(next.unwrap_or_default().to_string(), true)
510 } else {
511 Separated::Include(rest.to_string(), false)
512 });
513 }
514 if let Some(rest) = argument.strip_prefix("--sysroot=") {
515 return Some(Separated::Reads("--sysroot", rest.to_string(), false));
516 }
517 if let Some(rest) = argument.strip_prefix("-F")
518 && !rest.is_empty()
519 {
520 return Some(Separated::Reads("-F", rest.to_string(), false));
521 }
522 READS_PATH
523 .into_iter()
524 .find(|option| *option == argument)
525 .map(|option| Separated::Reads(option, next.unwrap_or_default().to_string(), true))
526}
527
528fn last_mention_wins(settings: Vec<String>) -> Vec<String> {
534 let mut latest: BTreeMap<String, String> = BTreeMap::new();
535 for setting in settings {
536 let name = setting
537 .trim_start_matches("-D")
538 .trim_start_matches("-U")
539 .split('=')
540 .next()
541 .unwrap_or_default()
542 .to_string();
543 latest.insert(name, setting);
544 }
545 latest.into_values().collect()
546}
547
548fn scalar(out: &mut String, name: &str, value: &str) {
549 out.push_str(name);
550 out.push('=');
551 push_sized(out, value);
552 out.push(';');
553}
554
555fn optional(out: &mut String, name: &str, value: Option<&str>) {
556 out.push_str(name);
557 out.push('=');
558 match value {
559 Some(value) => {
560 out.push_str("some");
561 push_sized(out, value);
562 }
563 None => out.push_str("none"),
565 }
566 out.push(';');
567}
568
569fn list(out: &mut String, name: &str, values: &[String]) {
570 out.push_str(name);
571 out.push('=');
572 out.push_str(&values.len().to_string());
573 out.push('[');
574 for value in values {
575 push_sized(out, value);
576 }
577 out.push_str("];");
578}
579
580fn push_sized(out: &mut String, value: &str) {
581 out.push_str(&value.len().to_string());
582 out.push(':');
583 out.push_str(value);
584}
585
586#[cfg(test)]
587#[allow(clippy::unwrap_used, clippy::expect_used)]
588mod tests {
589 use super::*;
590
591 fn command(arguments: &[&str]) -> Vec<String> {
592 arguments.iter().map(|a| (*a).to_string()).collect()
593 }
594
595 fn cpp(arguments: &[&str], file: &str) -> CppBuild {
596 CppBuild::from_command(&command(arguments), Path::new(file))
597 }
598
599 fn cpp_in(arguments: &[&str], file: &str, directory: &str) -> CppBuild {
600 CppBuild::from_command_in_directory(
601 &command(arguments),
602 Path::new(file),
603 Some(Path::new(directory)),
604 )
605 }
606
607 fn spelled(directory: &str, path: &str) -> String {
608 Path::new(directory).join(path).display().to_string()
609 }
610
611 #[test]
612 fn the_compiler_and_what_it_was_told_are_read_off_the_command() {
613 let build = cpp(
614 &[
615 "clang++",
616 "-std=c++17",
617 "-DACCUM_WIDTH=64",
618 "-I/w/include",
619 "-c",
620 "-o",
621 "wide.o",
622 "/w/src/wide.cpp",
623 ],
624 "/w/src/wide.cpp",
625 );
626 assert_eq!(build.compiler, "clang++");
627 assert_eq!(build.macros, vec!["-DACCUM_WIDTH=64"]);
628 assert_eq!(build.include_paths, vec!["/w/include"]);
629 assert_eq!(build.flags, vec!["-std=c++17"]);
630 assert_eq!(build.defines(), vec!["ACCUM_WIDTH=64"]);
631 }
632
633 #[test]
636 fn the_object_path_does_not_become_part_of_the_identity() {
637 let narrow = cpp(&["cc", "-O2", "-o", "a/narrow.o", "-c", "/w/a.c"], "/w/a.c");
638 let wide = cpp(&["cc", "-O2", "-o", "b/wide.o", "-c", "/w/a.c"], "/w/a.c");
639 assert_eq!(narrow, wide);
640 assert!(narrow.flags.iter().all(|flag| !flag.contains("narrow.o")));
641 }
642
643 #[test]
644 fn dependency_bookkeeping_and_diagnostic_colour_are_not_identity() {
645 let plain = cpp(&["cc", "-O2", "/w/a.c"], "/w/a.c");
646 let noisy = cpp(
647 &[
648 "cc",
649 "-O2",
650 "-MD",
651 "-MF",
652 "a.d",
653 "-MT",
654 "a.o",
655 "-fcolor-diagnostics",
656 "-fdiagnostics-color=always",
657 "/w/a.c",
658 ],
659 "/w/a.c",
660 );
661 assert_eq!(plain, noisy);
662 }
663
664 #[test]
668 fn an_unrecognised_flag_counts_towards_identity() {
669 let plain = cpp(&["cc", "/w/a.c"], "/w/a.c");
670 let odd = cpp(&["cc", "-fsomething-nobody-here-knows", "/w/a.c"], "/w/a.c");
671 assert_ne!(plain, odd);
672 assert_eq!(odd.flags, vec!["-fsomething-nobody-here-knows"]);
673 }
674
675 #[test]
676 fn the_separated_spellings_mean_the_same_as_the_joined_ones() {
677 let joined = cpp(&["cc", "-DWIDTH=64", "-I/w/inc", "/w/a.c"], "/w/a.c");
678 let separated = cpp(
679 &["cc", "-D", "WIDTH=64", "-I", "/w/inc", "/w/a.c"],
680 "/w/a.c",
681 );
682 assert_eq!(joined, separated);
683 }
684
685 #[test]
688 fn macros_are_sorted_but_the_last_mention_still_decides() {
689 let one = cpp(&["cc", "-DB=2", "-DA=1", "/w/a.c"], "/w/a.c");
690 let other = cpp(&["cc", "-DA=1", "-DB=2", "/w/a.c"], "/w/a.c");
691 assert_eq!(one, other);
692 assert_eq!(one.macros, vec!["-DA=1", "-DB=2"]);
693
694 let redefined = cpp(&["cc", "-DA=1", "-DA=2", "/w/a.c"], "/w/a.c");
695 assert_eq!(redefined.macros, vec!["-DA=2"]);
696 }
697
698 #[test]
701 fn defining_then_undefining_is_not_the_same_as_the_reverse() {
702 let defined_last = cpp(&["cc", "-UA", "-DA=1", "/w/a.c"], "/w/a.c");
703 let undefined_last = cpp(&["cc", "-DA=1", "-UA", "/w/a.c"], "/w/a.c");
704 assert_ne!(defined_last, undefined_last);
705 assert_eq!(defined_last.macros, vec!["-DA=1"]);
706 assert_eq!(undefined_last.macros, vec!["-UA"]);
707 }
708
709 #[test]
712 fn include_order_is_meaning_and_is_kept() {
713 let vendor_first = cpp(&["cc", "-I/vendor", "-I/local", "/w/a.c"], "/w/a.c");
714 let local_first = cpp(&["cc", "-I/local", "-I/vendor", "/w/a.c"], "/w/a.c");
715 assert_ne!(vendor_first, local_first);
716 assert_eq!(vendor_first.include_paths, vec!["/vendor", "/local"]);
717 }
718
719 #[test]
725 fn one_relative_include_from_two_directories_is_two_builds() {
726 let unit = ["clang++", "-Iinclude", "-c", "a.cpp"];
727 let one = cpp_in(&unit, "/w/one/a.cpp", "/w/one");
728 let other = cpp_in(&unit, "/w/two/a.cpp", "/w/two");
729 assert_eq!(one.include_paths, [spelled("/w/one", "include")]);
730 assert_eq!(other.include_paths, [spelled("/w/two", "include")]);
731 assert_ne!(
732 BuildConfiguration::Cpp(Box::new(one)).fingerprint(),
733 BuildConfiguration::Cpp(Box::new(other)).fingerprint()
734 );
735
736 assert_eq!(
739 cpp_in(&unit, "/w/one/a.cpp", "/w/one"),
740 cpp_in(&unit, "/w/one/a.cpp", "/w/one")
741 );
742 }
743
744 #[test]
748 fn a_relative_path_in_any_reading_option_is_resolved_where_the_command_ran() {
749 for option in ["--sysroot", "-isystem", "-iquote", "-idirafter", "-include"] {
750 let unit = ["cc", option, "sdk/thing", "-c", "a.c"];
751 let one = cpp_in(&unit, "/w/one/a.c", "/w/one");
752 let other = cpp_in(&unit, "/w/two/a.c", "/w/two");
753 assert_eq!(
754 one.flags,
755 [option.to_string(), spelled("/w/one", "sdk/thing")],
756 "{option}"
757 );
758 assert_ne!(one, other, "{option}");
759 }
760 }
761
762 #[test]
765 fn the_joined_and_separated_spellings_of_a_path_option_are_one_build() {
766 let joined = cpp_in(&["cc", "--sysroot=/sdk", "-F/sdk/f", "a.c"], "/w/a.c", "/w");
767 let apart = cpp_in(
768 &["cc", "--sysroot", "/sdk", "-F", "/sdk/f", "a.c"],
769 "/w/a.c",
770 "/w",
771 );
772 assert_eq!(joined, apart);
773 }
774
775 #[test]
778 fn two_routes_to_one_include_directory_are_one_build() {
779 let project = tempfile::tempdir().unwrap();
780 let include = project.path().join("include");
781 std::fs::create_dir(&include).unwrap();
782 let build_directory = project.path().join("build");
783 std::fs::create_dir(&build_directory).unwrap();
784 let unit = project.path().join("a.c");
785
786 let relative = CppBuild::from_command_in_directory(
787 &command(&["cc", "-I../include"]),
788 &unit,
789 Some(&build_directory),
790 );
791 let joined = format!("-I{}", include.display());
792 let absolute = CppBuild::from_command_in_directory(
793 &command(&["cc", &joined]),
794 &unit,
795 Some(&build_directory),
796 );
797 assert_eq!(relative, absolute);
798 }
799
800 #[test]
804 fn a_relative_path_with_no_directory_stands_as_it_was_written() {
805 let build = cpp(&["cc", "-Iinclude", "-isystem", "sdk", "/w/a.c"], "/w/a.c");
806 assert_eq!(build.include_paths, ["include"]);
807 assert_eq!(build.flags, ["-isystem", "sdk"]);
808 }
809
810 #[test]
813 fn punctuation_inside_an_argument_cannot_forge_another_argument() {
814 let one = cpp(&["cc", "-Dpair=a,b", "/w/a.c"], "/w/a.c");
815 let two = cpp(&["cc", "-Dpair=a", "-Db", "/w/a.c"], "/w/a.c");
816 let one = BuildConfiguration::Cpp(Box::new(one));
817 let two = BuildConfiguration::Cpp(Box::new(two));
818 assert_ne!(one.canonical(), two.canonical());
819 assert_ne!(one.fingerprint(), two.fingerprint());
820 }
821
822 #[test]
823 fn an_absent_value_is_not_an_empty_one() {
824 let absent = BuildConfiguration::Cpp(Box::new(CppBuild {
825 compiler: "cc".into(),
826 compiler_version: None,
827 ..CppBuild::default()
828 }));
829 let empty = BuildConfiguration::Cpp(Box::new(CppBuild {
830 compiler: "cc".into(),
831 compiler_version: Some(String::new()),
832 ..CppBuild::default()
833 }));
834 assert_ne!(absent.fingerprint(), empty.fingerprint());
835 }
836
837 #[test]
838 fn the_fingerprint_is_a_function_of_the_configuration_alone() {
839 let build = || {
840 BuildConfiguration::Cpp(Box::new(cpp(
841 &["clang++", "-std=c++17", "-DA=1", "/w/a.c"],
842 "/w/a.c",
843 )))
844 };
845 assert_eq!(build().fingerprint(), build().fingerprint());
846 }
847
848 #[test]
849 fn rust_features_are_a_set_and_are_ordered_like_one() {
850 let one = RustBuild {
851 features: vec!["wide".into(), "serde".into(), "wide".into()],
852 ..RustBuild::default()
853 }
854 .normalized();
855 let other = RustBuild {
856 features: vec!["serde".into(), "wide".into()],
857 ..RustBuild::default()
858 }
859 .normalized();
860 assert_eq!(one, other);
861 assert_eq!(one.features, vec!["serde", "wide"]);
862 }
863
864 #[test]
867 fn a_different_lockfile_is_a_different_build() {
868 let base = RustBuild {
869 target: "aarch64-apple-darwin".into(),
870 compiler_version: "rustc 1.85.0".into(),
871 lockfile_hash: Some(content_hash("one")),
872 ..RustBuild::default()
873 };
874 let moved = RustBuild {
875 lockfile_hash: Some(content_hash("another")),
876 ..base.clone()
877 };
878 assert_ne!(
879 BuildConfiguration::Rust(Box::new(base)).fingerprint(),
880 BuildConfiguration::Rust(Box::new(moved)).fingerprint()
881 );
882 }
883
884 #[test]
887 fn the_two_languages_are_in_different_identity_spaces() {
888 let rust = BuildConfiguration::Rust(Box::default());
889 let cpp = BuildConfiguration::Cpp(Box::default());
890 assert_ne!(rust.fingerprint(), cpp.fingerprint());
891 }
892
893 #[test]
898 fn the_encoding_of_a_configuration_is_fixed() {
899 let build = BuildConfiguration::Cpp(Box::new(CppBuild {
900 compiler: "cc".into(),
901 macros: vec!["-DA=1".into()],
902 include_paths: vec!["/inc".into()],
903 ..CppBuild::default()
904 }));
905 assert_eq!(
906 build.canonical(),
907 "language=3:cpp;compiler=2:cc;compiler_version=none;linker=none;\
908 macros=1[5:-DA=1];includes=1[4:/inc];flags=0[];database=none;\
909 post_processing_tools=0[];"
910 );
911 let build = BuildConfiguration::Rust(Box::new(RustBuild {
912 features: vec!["ledger/std".into()],
913 cfgs: vec!["unix".into()],
914 compiler_version: "rust-analyzer 0.0.344".into(),
915 permitted_execution: vec!["build-script".into()],
916 ..RustBuild::default()
917 }));
918 assert_eq!(
919 build.canonical(),
920 "language=4:rust;target=0:;features=1[10:ledger/std];cfgs=1[4:unix];\
921 compiler_version=21:rust-analyzer 0.0.344;opt_level=0:;lto=0:;\
922 codegen_units=none;panic=0:;lockfile=none;build_command=none;\
923 post_processing_tools=0[];permitted_execution=1[12:build-script];"
924 );
925 }
926
927 #[test]
932 fn every_field_that_moves_the_identity_is_one_of_the_settings() {
933 let cpp = |change: fn(&mut CppBuild)| {
934 let mut build = CppBuild {
935 compiler: "cc".into(),
936 compiler_version: Some("18".into()),
937 linker: Some("ld".into()),
938 macros: vec!["-DA=1".into()],
939 include_paths: vec!["/inc".into()],
940 flags: vec!["-O2".into()],
941 database_hash: Some("db".into()),
942 post_processing_tools: vec!["strip".into()],
943 };
944 change(&mut build);
945 BuildConfiguration::Cpp(Box::new(build))
946 };
947 let changes: [fn(&mut CppBuild); 8] = [
948 |b| b.compiler = "c++".into(),
949 |b| b.compiler_version = None,
950 |b| b.linker = Some("lld".into()),
951 |b| b.macros.push("-DB=2".into()),
952 |b| b.include_paths.clear(),
953 |b| b.flags = vec!["-O0".into()],
954 |b| b.database_hash = None,
955 |b| b.post_processing_tools.push("objcopy".into()),
956 ];
957 let base = cpp(|_| {});
958 for change in changes {
959 let moved = cpp(change);
960 assert_ne!(base.fingerprint(), moved.fingerprint());
961 assert_ne!(base.settings(), moved.settings());
962 }
963
964 let rust = |change: fn(&mut RustBuild)| {
965 let mut build = RustBuild {
966 target: "aarch64-apple-darwin".into(),
967 features: vec!["serde".into()],
968 cfgs: vec!["unix".into()],
969 compiler_version: "rustc 1.85.0".into(),
970 opt_level: "3".into(),
971 lto: "thin".into(),
972 codegen_units: Some(16),
973 panic: "unwind".into(),
974 lockfile_hash: Some("lock".into()),
975 build_command_hash: Some("cmd".into()),
976 post_processing_tools: vec!["strip".into()],
977 permitted_execution: Vec::new(),
978 };
979 change(&mut build);
980 BuildConfiguration::Rust(Box::new(build))
981 };
982 let changes: [fn(&mut RustBuild); 12] = [
983 |b| b.target = "x86_64-unknown-linux-gnu".into(),
984 |b| b.features.clear(),
985 |b| b.cfgs.push("windows".into()),
986 |b| b.compiler_version = "rustc 1.86.0".into(),
987 |b| b.opt_level = "0".into(),
988 |b| b.lto = "fat".into(),
989 |b| b.codegen_units = None,
990 |b| b.panic = "abort".into(),
991 |b| b.lockfile_hash = None,
992 |b| b.build_command_hash = Some("other".into()),
993 |b| b.post_processing_tools.push("objcopy".into()),
994 |b| b.permitted_execution = vec!["build-script".into()],
995 ];
996 let base = rust(|_| {});
997 for change in changes {
998 let moved = rust(change);
999 assert_ne!(base.fingerprint(), moved.fingerprint());
1000 assert_ne!(base.settings(), moved.settings());
1001 }
1002 }
1003
1004 #[test]
1007 fn an_unresolved_setting_records_nothing_and_an_empty_one_records_a_value() {
1008 assert!(
1009 Shape::Resolved(None).values().is_empty(),
1010 "{:?}",
1011 Shape::Resolved(None).values()
1012 );
1013 assert_eq!(Shape::Resolved(Some(String::new())).values(), vec![""]);
1014 assert_eq!(Shape::Given("cc".into()).values(), vec!["cc"]);
1015 assert_eq!(
1016 Shape::Ordered(vec!["/a".into(), "/b".into()]).values(),
1017 vec!["/a", "/b"]
1018 );
1019 }
1020}