1use std::{borrow::Cow, path::PathBuf, sync::Arc};
2
3use cargo_metadata::Target;
4use snafu::ResultExt;
5
6use crate::{
7 Result,
8 cache::Cache,
9 cargo::{CargoMetadataOptions, CargoRunner, Metadata},
10 config::Config,
11 crate_resolver::ResolvedSource,
12 cratespec::CrateSpec,
13 downloader::DownloadedCrate,
14 error,
15 target::TargetTriple,
16};
17
18#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
20pub enum BuildTarget {
21 #[default]
24 DefaultBin,
25
26 Bin(String),
28
29 Example(String),
31}
32
33#[derive(Clone, Debug, Default)]
36pub struct BuildOverrides {
37 pub features: Option<Vec<String>>,
41
42 pub all_features: bool,
44
45 pub no_default_features: bool,
47
48 pub profile: Option<String>,
50
51 pub target: Option<String>,
53
54 pub jobs: Option<usize>,
56
57 pub ignore_rust_version: bool,
59
60 pub target_selection: BuildTarget,
62
63 pub toolchain: Option<String>,
65}
66
67#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
96pub struct BuildOptions {
97 pub(crate) features: Vec<String>,
99
100 pub(crate) all_features: bool,
102
103 pub(crate) no_default_features: bool,
105
106 pub(crate) profile: Option<String>,
111
112 pub(crate) target: Option<TargetTriple>,
114
115 pub(crate) locked: bool,
117
118 pub(crate) offline: bool,
120
121 pub(crate) jobs: Option<usize>,
125
126 pub(crate) ignore_rust_version: bool,
128
129 pub(crate) build_target: BuildTarget,
131
132 pub(crate) toolchain: Option<String>,
136}
137
138impl Default for BuildOptions {
139 fn default() -> Self {
140 Self {
141 features: Vec::new(),
142 all_features: false,
143 no_default_features: false,
144 profile: None,
145 target: None,
146 locked: true,
147 offline: false,
148 jobs: None,
149 ignore_rust_version: false,
150 build_target: BuildTarget::default(),
151 toolchain: None,
152 }
153 }
154}
155
156impl BuildOptions {
157 pub fn features(&self) -> &[String] {
159 &self.features
160 }
161
162 pub fn all_features(&self) -> bool {
164 self.all_features
165 }
166
167 pub fn no_default_features(&self) -> bool {
169 self.no_default_features
170 }
171
172 pub fn profile(&self) -> Option<&str> {
174 self.profile.as_deref()
175 }
176
177 pub fn target(&self) -> Option<&str> {
179 self.target.as_ref().map(TargetTriple::as_str)
180 }
181
182 pub fn locked(&self) -> bool {
184 self.locked
185 }
186
187 pub fn offline(&self) -> bool {
189 self.offline
190 }
191
192 pub fn jobs(&self) -> Option<usize> {
194 self.jobs
195 }
196
197 pub fn ignore_rust_version(&self) -> bool {
199 self.ignore_rust_version
200 }
201
202 pub fn build_target(&self) -> &BuildTarget {
204 &self.build_target
205 }
206
207 pub fn toolchain(&self) -> Option<&str> {
209 self.toolchain.as_deref()
210 }
211
212 pub fn load(config: &Config, overrides: &BuildOverrides) -> Result<Self> {
222 let target = overrides.target.clone().map(TargetTriple::from_owned);
226
227 Ok(BuildOptions {
228 locked: config.locked,
230 offline: config.offline,
231
232 toolchain: overrides.toolchain.clone().or_else(|| config.toolchain.clone()),
234
235 features: overrides.features.clone().unwrap_or_default(),
237 all_features: overrides.all_features,
238 no_default_features: overrides.no_default_features,
239 profile: overrides.profile.clone(),
240 target,
241 jobs: overrides.jobs,
242 ignore_rust_version: overrides.ignore_rust_version,
243 build_target: overrides.target_selection.clone(),
244 })
245 }
246
247 pub fn load_for_crate(
253 config: &Config,
254 overrides: &BuildOverrides,
255 crate_spec: &CrateSpec,
256 ) -> Result<Self> {
257 let mut options = Self::load(config, overrides)?;
260
261 if !options.all_features {
272 if let Some(crate_name) = crate_spec.configured_tool_name() {
273 if let Some(tool_config) = config.tools.get(crate_name) {
274 if overrides.features.is_none() {
275 if let Some(features) = tool_config.features() {
276 options.features = features.to_vec();
277 }
278 }
279
280 if !tool_config.default_features() {
281 options.no_default_features = true;
282 }
283 }
284 }
285 }
286
287 Ok(options)
288 }
289
290 pub(crate) fn target_platform(&self) -> &TargetTriple {
293 match &self.target {
294 Some(target) => target,
295 None => TargetTriple::host(),
296 }
297 }
298}
299
300pub trait CrateBuilder {
301 fn list_targets(
310 &self,
311 krate: &DownloadedCrate,
312 options: &BuildOptions,
313 ) -> Result<(Option<Target>, Vec<Target>, Vec<Target>)>;
314
315 fn build(&self, krate: &DownloadedCrate, options: &BuildOptions) -> Result<(PathBuf, BuildTarget)>;
325}
326
327pub(crate) fn create_builder(
328 config: Config,
329 cache: Cache,
330 cargo_runner: Arc<dyn CargoRunner>,
331) -> impl CrateBuilder {
332 RealCrateBuilder {
333 config,
334 cache,
335 cargo_runner,
336 }
337}
338
339struct RealCrateBuilder {
341 config: Config,
342 cache: Cache,
343 cargo_runner: Arc<dyn CargoRunner>,
344}
345
346impl CrateBuilder for RealCrateBuilder {
347 fn list_targets(
348 &self,
349 krate: &DownloadedCrate,
350 options: &BuildOptions,
351 ) -> Result<(Option<Target>, Vec<Target>, Vec<Target>)> {
352 let metadata = self
353 .cargo_runner
354 .metadata(&krate.crate_path, &CargoMetadataOptions::from(options))?;
355
356 Self::list_targets_internal(krate, &metadata)
357 }
358
359 fn build(&self, krate: &DownloadedCrate, options: &BuildOptions) -> Result<(PathBuf, BuildTarget)> {
360 let metadata = self
364 .cargo_runner
365 .metadata(&krate.crate_path, &CargoMetadataOptions::from(options))?;
366
367 let options: Cow<'_, BuildOptions> = if matches!(options.build_target, BuildTarget::DefaultBin) {
371 Cow::Owned(BuildOptions {
372 build_target: Self::resolve_binary_target(krate, options, &metadata)?,
373 ..options.clone()
374 })
375 } else {
376 Cow::Borrowed(options)
377 };
378
379 let built_target = options.build_target.clone();
383
384 if matches!(krate.resolved.source, ResolvedSource::LocalDir { .. }) {
391 let (binary_path, _sbom) = self.build_uncached(krate, options.as_ref(), &metadata)?;
392 return Ok((binary_path, built_target));
393 }
394
395 let binary_path = self
396 .cache
397 .get_or_build_binary(&krate.resolved, options.as_ref(), || {
398 self.build_uncached(krate, options.as_ref(), &metadata)
399 })?;
400 Ok((binary_path, built_target))
401 }
402}
403
404impl RealCrateBuilder {
405 fn list_targets_internal(
410 krate: &DownloadedCrate,
411 metadata: &Metadata,
412 ) -> Result<(Option<Target>, Vec<Target>, Vec<Target>)> {
413 let package = metadata
415 .packages
416 .iter()
417 .find(|p| p.name.as_str() == krate.resolved.name)
418 .ok_or_else(|| {
419 error::PackageNotFoundInWorkspaceSnafu {
420 name: krate.resolved.name.clone(),
421 available: metadata
422 .packages
423 .iter()
424 .map(|p| p.name.to_string())
425 .collect::<Vec<_>>(),
426 }
427 .build()
428 })?;
429
430 let bin_targets: Vec<_> = package
433 .targets
434 .iter()
435 .filter(|t| {
436 t.kind
437 .iter()
438 .any(|k| matches!(k, cargo_metadata::TargetKind::Bin))
439 })
440 .cloned()
441 .collect();
442 let example_targets: Vec<_> = package
443 .targets
444 .iter()
445 .filter(|t| {
446 t.kind
447 .iter()
448 .any(|k| matches!(k, cargo_metadata::TargetKind::Example))
449 })
450 .cloned()
451 .collect();
452
453 let default = package.default_run.as_ref().and_then(|default_run| {
455 bin_targets
456 .iter()
457 .find(|t| t.name == default_run.as_str())
458 .cloned()
459 });
460
461 Ok((default, bin_targets, example_targets))
462 }
463
464 fn resolve_binary_target(
472 krate: &DownloadedCrate,
473 options: &BuildOptions,
474 metadata: &Metadata,
475 ) -> Result<BuildTarget> {
476 let (default, bins, examples) = Self::list_targets_internal(krate, metadata)?;
477
478 let build_target = if matches!(options.build_target, BuildTarget::DefaultBin) {
480 if let Some(default) = default {
481 BuildTarget::Bin(default.name.clone())
482 } else {
483 BuildTarget::DefaultBin
484 }
485 } else {
486 options.build_target.clone()
487 };
488
489 match build_target {
499 BuildTarget::DefaultBin => {
500 match bins.len() {
502 0 => {
503 error::NoPackageBinariesSnafu {
505 krate: krate.resolved.name.clone(),
506 }
507 .fail()
508 }
509 1 => {
510 Ok(BuildTarget::Bin(bins[0].name.clone()))
512 }
513 _ => {
514 error::AmbiguousBinaryTargetSnafu {
516 package: krate.resolved.name.clone(),
517 available: bins.iter().map(|t| t.name.clone()).collect::<Vec<_>>(),
518 }
519 .fail()
520 }
521 }
522 }
523 BuildTarget::Bin(ref name) => {
524 if bins.iter().any(|t| t.name == *name) {
526 Ok(build_target)
527 } else {
528 error::RunnableTargetNotFoundSnafu {
529 kind: "binary",
530 package: krate.resolved.name.clone(),
531 target: name.clone(),
532 available: bins.iter().map(|t| t.name.clone()).collect::<Vec<_>>(),
533 }
534 .fail()
535 }
536 }
537 BuildTarget::Example(ref name) => {
538 if examples.iter().any(|t| t.name == *name) {
540 Ok(build_target)
541 } else {
542 error::RunnableTargetNotFoundSnafu {
543 kind: "example",
544 package: krate.resolved.name.clone(),
545 target: name.clone(),
546 available: bins.iter().map(|t| t.name.clone()).collect::<Vec<_>>(),
547 }
548 .fail()
549 }
550 }
551 }
552 }
553
554 fn build_uncached(
585 &self,
586 krate: &DownloadedCrate,
587 options: &BuildOptions,
588 metadata: &Metadata,
589 ) -> Result<(PathBuf, crate::sbom::CycloneDx)> {
590 let build_dir = self.prepare_build_dir(krate, options)?;
591
592 let package_name = Self::resolve_package_name(metadata, &krate.resolved.name)?;
593
594 let binary_path = self
595 .cargo_runner
596 .build(&build_dir, package_name.as_deref(), options)?;
597
598 let metadata = self
606 .cargo_runner
607 .metadata(&build_dir, &CargoMetadataOptions::from(options))?;
608
609 let sbom = crate::sbom::generate_sbom(&metadata, &krate.resolved, options)?;
611
612 Ok((binary_path, sbom))
613 }
614
615 fn prepare_build_dir(&self, krate: &DownloadedCrate, options: &BuildOptions) -> Result<PathBuf> {
628 if let ResolvedSource::LocalDir { .. } = krate.resolved.source {
629 return Ok(krate.crate_path.clone());
630 }
631
632 std::fs::create_dir_all(&self.config.build_dir).with_context(|_| error::IoSnafu {
633 path: self.config.build_dir.clone(),
634 })?;
635
636 let temp_dir = tempfile::Builder::new()
637 .prefix(&format!("cgx-build-{}", &krate.resolved.name))
638 .tempdir_in(&self.config.build_dir)
639 .with_context(|_| error::TempDirInCreationSnafu {
640 parent: self.config.build_dir.clone(),
641 })?;
642
643 let temp_path = temp_dir.path().to_path_buf();
644 crate::helpers::copy_source_tree(&krate.crate_path, &temp_path)?;
645
646 if !options.locked {
649 let lock_path = temp_path.join("Cargo.lock");
650 if lock_path.exists() {
651 std::fs::remove_file(&lock_path).with_context(|_| error::IoSnafu { path: lock_path })?;
652 }
653 }
654
655 let _ = temp_dir.keep();
656 Ok(temp_path)
657 }
658
659 fn resolve_package_name(metadata: &Metadata, crate_name: &str) -> Result<Option<String>> {
667 let workspace_members: Vec<_> = metadata
668 .workspace_packages()
669 .iter()
670 .map(|p| p.name.as_str())
671 .collect();
672
673 match workspace_members.len() {
674 0 | 1 => Ok(None),
675 _ => {
676 if workspace_members.iter().any(|name| *name == crate_name) {
677 Ok(Some(crate_name.to_string()))
678 } else {
679 error::PackageNotFoundInWorkspaceSnafu {
680 name: crate_name.to_string(),
681 available: workspace_members
682 .into_iter()
683 .map(String::from)
684 .collect::<Vec<_>>(),
685 }
686 .fail()
687 }
688 }
689 }
690 }
691}
692
693#[cfg(test)]
694mod tests {
695 use std::{fs, path::Path};
696
697 use assert_matches::assert_matches;
698 use semver::Version;
699
700 use super::*;
701 use crate::{
702 cargo::create_cargo_runner,
703 crate_resolver::{ResolvedCrate, ResolvedSource},
704 error::Error,
705 testdata::CrateTestCase,
706 };
707
708 fn test_builder() -> (RealCrateBuilder, tempfile::TempDir) {
709 crate::logging::init_test_logging();
710
711 let (temp_dir, config) = crate::config::create_test_env();
712
713 fs::create_dir_all(&config.cache_dir).unwrap();
714 fs::create_dir_all(&config.bin_dir).unwrap();
715 fs::create_dir_all(&config.build_dir).unwrap();
716
717 let cache = Cache::new(config.clone(), crate::messages::MessageReporter::null());
718 let cargo_runner =
719 Arc::new(create_cargo_runner(config.clone(), crate::messages::MessageReporter::null()).unwrap());
720
721 let builder = RealCrateBuilder {
722 config,
723 cache,
724 cargo_runner,
725 };
726
727 (builder, temp_dir)
728 }
729
730 #[derive(Debug, Clone)]
732 enum FakeSourceType {
733 Registry { version: String },
734 Git { url: String, rev: String },
735 LocalDir,
736 }
737
738 fn fake_downloaded_crate(
741 tc: &CrateTestCase,
742 source_type: FakeSourceType,
743 package_name: Option<&str>,
744 ) -> DownloadedCrate {
745 let (resolved_source, crate_path) = match &source_type {
746 FakeSourceType::Registry { .. } => {
747 let path = if let Some(pkg) = package_name {
749 tc.path().join(pkg)
750 } else {
751 tc.path().to_path_buf()
752 };
753 (ResolvedSource::CratesIo, path)
754 }
755 FakeSourceType::Git { url, rev } => {
756 (
758 ResolvedSource::Git {
759 repo: url.clone(),
760 commit: rev.clone(),
761 },
762 tc.path().to_path_buf(),
763 )
764 }
765 FakeSourceType::LocalDir => {
766 let path = tc.path().to_path_buf();
768 (ResolvedSource::LocalDir { path: path.clone() }, path)
769 }
770 };
771
772 let name = package_name.unwrap_or(tc.name).to_string();
773 let version = match &source_type {
774 FakeSourceType::Registry { version } => Version::parse(version).unwrap(),
775 _ => Version::parse("0.1.0").unwrap(),
776 };
777
778 DownloadedCrate {
779 resolved: ResolvedCrate {
780 name,
781 version,
782 source: resolved_source,
783 },
784 crate_path,
785 }
786 }
787
788 fn read_sbom_for_binary(binary_path: &Path) -> PathBuf {
790 binary_path.parent().unwrap().join("sbom.cyclonedx.json")
792 }
793
794 fn expected_bin_name(base_name: &str) -> String {
798 format!("{}{}", base_name, std::env::consts::EXE_SUFFIX)
799 }
800
801 fn assert_cache_hit(path1: &Path, path2: &Path) {
803 assert_eq!(
804 path1,
805 path2,
806 "Cache hit expected: paths should be identical\n path1: {}\n path2: {}",
807 path1.display(),
808 path2.display()
809 );
810
811 let mtime1 = fs::metadata(path1).unwrap().modified().unwrap();
812 let mtime2 = fs::metadata(path2).unwrap().modified().unwrap();
813
814 assert_eq!(
815 mtime1,
816 mtime2,
817 "Cache hit expected: modification times should be identical\n path1: {}\n path2: {}",
818 path1.display(),
819 path2.display()
820 );
821 }
822
823 fn assert_cache_miss(path1: &Path, path2: &Path) {
825 let paths_differ = path1 != path2;
826 let mtimes_differ = if path1.exists() && path2.exists() {
827 let mtime1 = fs::metadata(path1).unwrap().modified().unwrap();
828 let mtime2 = fs::metadata(path2).unwrap().modified().unwrap();
829 mtime1 != mtime2
830 } else {
831 true
832 };
833
834 assert!(
835 paths_differ || mtimes_differ,
836 "Cache miss expected: paths or mtimes should differ\n path1: {}\n path2: {}\n paths_differ: \
837 {}\n mtimes_differ: {}",
838 path1.display(),
839 path2.display(),
840 paths_differ,
841 mtimes_differ
842 );
843 }
844
845 #[derive(Debug)]
847 struct TimestampOutput {
848 build_timestamp: String,
849 features: Vec<String>,
850 }
851
852 fn run_timestamp_binary(path: &Path) -> TimestampOutput {
854 let output = std::process::Command::new(path)
855 .output()
856 .unwrap_or_else(|e| panic!("Failed to execute timestamp binary at {}: {}", path.display(), e));
857
858 assert!(
859 output.status.success(),
860 "Timestamp binary failed with status {}: {}",
861 output.status,
862 String::from_utf8_lossy(&output.stderr)
863 );
864
865 let stdout = String::from_utf8_lossy(&output.stdout);
866
867 let mut build_timestamp = None;
868 let mut features = Vec::new();
869
870 for line in stdout.lines() {
871 if let Some(ts) = line.strip_prefix("Built at: ") {
872 build_timestamp = Some(ts.to_string());
873 }
874 if let Some(feat_str) = line.strip_prefix("Features enabled: ") {
875 if feat_str != "none" {
876 features = feat_str.split(", ").map(|s| s.to_string()).collect();
877 }
878 }
879 }
880
881 TimestampOutput {
882 build_timestamp: build_timestamp.expect("No 'Built at:' line in timestamp output"),
883 features,
884 }
885 }
886
887 fn assert_cache_hit_by_timestamp(output1: &TimestampOutput, output2: &TimestampOutput) {
889 assert_eq!(
890 output1.build_timestamp, output2.build_timestamp,
891 "Cache hit expected: build timestamps should match\n ts1: {}\n ts2: {}",
892 output1.build_timestamp, output2.build_timestamp
893 );
894 }
895
896 fn assert_cache_miss_by_timestamp(output1: &TimestampOutput, output2: &TimestampOutput) {
898 assert_ne!(
899 output1.build_timestamp, output2.build_timestamp,
900 "Cache miss expected: build timestamps should differ\n ts1: {}\n ts2: {}",
901 output1.build_timestamp, output2.build_timestamp
902 );
903 }
904
905 mod smoke_tests {
906 use super::*;
907
908 #[test]
909 fn builds_all_testcases_with_bins() {
910 let (builder, _temp) = test_builder();
911 let cargo_runner =
912 create_cargo_runner(Config::default(), crate::messages::MessageReporter::null()).unwrap();
913
914 for tc in CrateTestCase::all() {
915 let metadata_opts = CargoMetadataOptions::default();
916 let metadata = cargo_runner.metadata(tc.path(), &metadata_opts).unwrap();
917
918 let workspace_pkgs = metadata.workspace_packages();
919 let buildable_packages: Vec<_> = workspace_pkgs
920 .iter()
921 .filter(|pkg| {
922 pkg.targets.iter().any(|t| {
923 t.kind
924 .iter()
925 .any(|k| matches!(k, cargo_metadata::TargetKind::Bin))
926 })
927 })
928 .collect();
929
930 if buildable_packages.is_empty() {
931 continue;
932 }
933
934 for pkg in buildable_packages {
935 let krate = fake_downloaded_crate(
936 &tc,
937 FakeSourceType::Registry {
938 version: "1.0.0".to_string(),
939 },
940 Some(&pkg.name),
941 );
942
943 let options = BuildOptions {
944 profile: Some("dev".to_string()),
945 ..Default::default()
946 };
947
948 let result = builder.build(&krate, &options);
949
950 if let Ok((binary, _target)) = result {
951 assert!(binary.exists(), "Binary missing for {}/{}", tc.name, pkg.name);
952
953 let binary_name = binary.file_name().unwrap().to_str().unwrap();
954
955 let bin_targets: Vec<_> = pkg
957 .targets
958 .iter()
959 .filter(|t| {
960 t.kind
961 .iter()
962 .any(|k| matches!(k, cargo_metadata::TargetKind::Bin))
963 })
964 .collect();
965
966 let expected_name = if bin_targets.len() == 1 {
967 bin_targets[0].name.as_str()
969 } else if let Some(ref default_run) = pkg.default_run {
970 default_run.as_str()
972 } else {
973 panic!(
975 "Build succeeded for {}/{} but should have failed due to ambiguous binary \
976 target",
977 tc.name, pkg.name
978 );
979 };
980
981 assert_eq!(
982 binary_name,
983 expected_bin_name(expected_name),
984 "Wrong binary name for {}/{}: expected '{}', got '{}'",
985 tc.name,
986 pkg.name,
987 expected_name,
988 binary_name
989 );
990 }
991 }
992 }
993 }
994
995 #[test]
996 fn simple_bin_no_deps_from_registry() {
997 let (builder, _temp) = test_builder();
998 let tc = CrateTestCase::simple_bin_no_deps();
999 let krate = fake_downloaded_crate(
1000 &tc,
1001 FakeSourceType::Registry {
1002 version: "1.0.0".to_string(),
1003 },
1004 None,
1005 );
1006
1007 let options = BuildOptions {
1008 profile: Some("dev".to_string()),
1009 ..Default::default()
1010 };
1011
1012 let (binary, _target) = builder.build(&krate, &options).unwrap();
1013
1014 assert!(binary.exists());
1015 assert!(binary.is_file());
1016 assert!(binary.starts_with(&builder.config.bin_dir));
1017
1018 let binary_name = binary.file_name().unwrap().to_str().unwrap();
1019 assert_eq!(binary_name, expected_bin_name("simple-bin-no-deps"));
1020 }
1021 }
1022
1023 mod binary_selection {
1024 use super::*;
1025
1026 #[test]
1027 fn default_bin_selected_automatically() {
1028 let (builder, _temp) = test_builder();
1029 let tc = CrateTestCase::single_crate_multiple_bins_with_default();
1030 let krate = fake_downloaded_crate(
1031 &tc,
1032 FakeSourceType::Registry {
1033 version: "1.0.0".to_string(),
1034 },
1035 None,
1036 );
1037
1038 let options = BuildOptions {
1039 profile: Some("dev".to_string()),
1040 build_target: BuildTarget::DefaultBin,
1041 ..Default::default()
1042 };
1043
1044 let (binary, target) = builder.build(&krate, &options).unwrap();
1045 assert!(binary.exists());
1046 let binary_name = binary.file_name().unwrap().to_str().unwrap();
1047 assert_eq!(
1048 binary_name,
1049 expected_bin_name("bin1"),
1050 "Should build bin1 or the crate's default binary, got: {}",
1051 binary_name
1052 );
1053
1054 assert_eq!(target, BuildTarget::Bin("bin1".to_string()));
1056 }
1057
1058 #[test]
1059 fn explicit_bin_overrides_default() {
1060 let (builder, _temp) = test_builder();
1061 let tc = CrateTestCase::single_crate_multiple_bins_with_default();
1062 let krate = fake_downloaded_crate(
1063 &tc,
1064 FakeSourceType::Registry {
1065 version: "1.0.0".to_string(),
1066 },
1067 None,
1068 );
1069
1070 let options = BuildOptions {
1071 profile: Some("dev".to_string()),
1072 build_target: BuildTarget::Bin("bin2".to_string()),
1073 ..Default::default()
1074 };
1075
1076 let (binary, _target) = builder.build(&krate, &options).unwrap();
1077 assert!(binary.exists());
1078 let binary_name = binary.file_name().unwrap().to_str().unwrap();
1079 assert_eq!(binary_name, expected_bin_name("bin2"));
1080 }
1081
1082 #[test]
1083 fn multiple_bins_without_default_fails() {
1084 let (builder, _temp) = test_builder();
1085 let tc = CrateTestCase::single_crate_multiple_bins();
1086 let krate = fake_downloaded_crate(
1087 &tc,
1088 FakeSourceType::Registry {
1089 version: "1.0.0".to_string(),
1090 },
1091 None,
1092 );
1093
1094 let options = BuildOptions {
1095 profile: Some("dev".to_string()),
1096 ..Default::default()
1097 };
1098
1099 let result = builder.build(&krate, &options);
1100
1101 assert_matches!(
1102 result,
1103 Err(Error::AmbiguousBinaryTarget { ref package, ref available })
1104 if package == "single-crate-multiple-bins"
1105 && available.len() == 2
1106 && available.contains(&"bin1".to_string())
1107 && available.contains(&"bin2".to_string())
1108 );
1109 }
1110 }
1111
1112 mod workspace_handling {
1113 use super::*;
1114
1115 #[test]
1116 fn workspace_with_correct_package_succeeds() {
1117 let (builder, _temp) = test_builder();
1118 let tc = CrateTestCase::workspace_multiple_bin_crates();
1119 let krate = fake_downloaded_crate(
1120 &tc,
1121 FakeSourceType::Git {
1122 url: "https://github.com/example/test.git".to_string(),
1123 rev: "abc123".to_string(),
1124 },
1125 Some("bin1"),
1126 );
1127
1128 let options = BuildOptions {
1129 profile: Some("dev".to_string()),
1130 ..Default::default()
1131 };
1132
1133 let (binary, _target) = builder.build(&krate, &options).unwrap();
1134 assert!(binary.exists());
1135
1136 let binary_name = binary.file_name().unwrap().to_str().unwrap();
1137 assert_eq!(binary_name, expected_bin_name("bin1"));
1138 }
1139
1140 #[test]
1141 fn workspace_with_wrong_package_fails() {
1142 let (builder, _temp) = test_builder();
1143 let tc = CrateTestCase::workspace_multiple_bin_crates();
1144
1145 let krate = DownloadedCrate {
1146 resolved: ResolvedCrate {
1147 name: "nonexistent-package".to_string(),
1148 version: Version::parse("1.0.0").unwrap(),
1149 source: ResolvedSource::CratesIo,
1150 },
1151 crate_path: tc.path().to_path_buf(),
1152 };
1153
1154 let options = BuildOptions {
1155 profile: Some("dev".to_string()),
1156 ..Default::default()
1157 };
1158
1159 let result = builder.build(&krate, &options);
1160
1161 assert_matches!(
1162 result,
1163 Err(Error::PackageNotFoundInWorkspace { ref name, ref available })
1164 if name == "nonexistent-package" && !available.is_empty()
1165 );
1166 }
1167 }
1168
1169 mod cache_functional {
1170 use super::*;
1171
1172 #[test]
1173 fn identical_builds_hit_cache() {
1174 let (builder, _temp) = test_builder();
1175 let tc = CrateTestCase::timestamp();
1176
1177 let krate1 = fake_downloaded_crate(
1178 &tc,
1179 FakeSourceType::Registry {
1180 version: "1.0.0".to_string(),
1181 },
1182 None,
1183 );
1184 let options = BuildOptions {
1185 profile: Some("dev".to_string()),
1186 ..Default::default()
1187 };
1188
1189 let (binary1, _target) = builder.build(&krate1, &options).unwrap();
1190 let binary1_name = binary1.file_name().unwrap().to_str().unwrap();
1191 assert_eq!(binary1_name, expected_bin_name("timestamp"));
1192 let output1 = run_timestamp_binary(&binary1);
1193
1194 std::thread::sleep(std::time::Duration::from_millis(100));
1195
1196 let krate2 = fake_downloaded_crate(
1197 &tc,
1198 FakeSourceType::Registry {
1199 version: "1.0.0".to_string(),
1200 },
1201 None,
1202 );
1203
1204 let (binary2, _target) = builder.build(&krate2, &options).unwrap();
1205 let binary2_name = binary2.file_name().unwrap().to_str().unwrap();
1206 assert_eq!(binary2_name, expected_bin_name("timestamp"));
1207 let output2 = run_timestamp_binary(&binary2);
1208
1209 assert_cache_hit_by_timestamp(&output1, &output2);
1210 assert_cache_hit(&binary1, &binary2);
1211 }
1212
1213 #[test]
1214 fn different_profile_cache_miss() {
1215 let (builder, _temp) = test_builder();
1216 let tc = CrateTestCase::timestamp();
1217
1218 let krate1 = fake_downloaded_crate(
1219 &tc,
1220 FakeSourceType::Registry {
1221 version: "1.0.0".to_string(),
1222 },
1223 None,
1224 );
1225 let options1 = BuildOptions {
1226 profile: Some("dev".to_string()),
1227 ..Default::default()
1228 };
1229 let (binary1, _target) = builder.build(&krate1, &options1).unwrap();
1230 let binary1_name = binary1.file_name().unwrap().to_str().unwrap();
1231 assert_eq!(binary1_name, expected_bin_name("timestamp"));
1232 let output1 = run_timestamp_binary(&binary1);
1233
1234 let krate2 = fake_downloaded_crate(
1235 &tc,
1236 FakeSourceType::Registry {
1237 version: "1.0.0".to_string(),
1238 },
1239 None,
1240 );
1241 let options2 = BuildOptions {
1242 profile: Some("release".to_string()),
1243 ..Default::default()
1244 };
1245 let (binary2, _target) = builder.build(&krate2, &options2).unwrap();
1246 let binary2_name = binary2.file_name().unwrap().to_str().unwrap();
1247 assert_eq!(binary2_name, expected_bin_name("timestamp"));
1248 let output2 = run_timestamp_binary(&binary2);
1249
1250 assert_cache_miss_by_timestamp(&output1, &output2);
1251 assert_cache_miss(&binary1, &binary2);
1252 }
1253
1254 #[test]
1255 fn different_target_cache_miss() {
1256 let (builder, _temp) = test_builder();
1257 let tc = CrateTestCase::simple_bin_no_deps();
1258
1259 let krate1 = fake_downloaded_crate(
1260 &tc,
1261 FakeSourceType::Registry {
1262 version: "1.0.0".to_string(),
1263 },
1264 None,
1265 );
1266 let options1 = BuildOptions {
1267 profile: Some("dev".to_string()),
1268 target: None,
1269 ..Default::default()
1270 };
1271 let (binary1, _target) = builder.build(&krate1, &options1).unwrap();
1272 let binary1_name = binary1.file_name().unwrap().to_str().unwrap();
1273 assert_eq!(binary1_name, expected_bin_name("simple-bin-no-deps"));
1274
1275 let krate2 = fake_downloaded_crate(
1276 &tc,
1277 FakeSourceType::Registry {
1278 version: "1.0.0".to_string(),
1279 },
1280 None,
1281 );
1282 let options2 = BuildOptions {
1283 profile: Some("dev".to_string()),
1284 target: Some(TargetTriple::host().clone()),
1285 ..Default::default()
1286 };
1287 let (binary2, _target) = builder.build(&krate2, &options2).unwrap();
1288 let binary2_name = binary2.file_name().unwrap().to_str().unwrap();
1289 assert_eq!(binary2_name, expected_bin_name("simple-bin-no-deps"));
1290
1291 assert_cache_miss(&binary1, &binary2);
1292 }
1293 }
1294
1295 mod dependency_resolution {
1296 use super::*;
1297 use crate::sbom::tests::get_sbom_component_version;
1298
1299 #[test]
1300 fn locked_vs_unlocked_produces_different_cache_entries() {
1301 let (builder, _temp) = test_builder();
1302 let tc = CrateTestCase::stale_serde();
1303
1304 let krate1 = fake_downloaded_crate(
1305 &tc,
1306 FakeSourceType::Registry {
1307 version: "1.0.0".to_string(),
1308 },
1309 None,
1310 );
1311 let options1 = BuildOptions {
1312 profile: Some("dev".to_string()),
1313 locked: true,
1314 ..Default::default()
1315 };
1316 let (binary1, _target) = builder.build(&krate1, &options1).unwrap();
1317 let binary1_name = binary1.file_name().unwrap().to_str().unwrap();
1318 assert_eq!(binary1_name, expected_bin_name("stale-serde"));
1319 let sbom1 = read_sbom_for_binary(&binary1);
1320
1321 assert_eq!(
1322 get_sbom_component_version(&sbom1, "serde"),
1323 Some("1.0.5".to_string()),
1324 "With --locked, should use old serde from Cargo.lock"
1325 );
1326
1327 let krate2 = fake_downloaded_crate(
1328 &tc,
1329 FakeSourceType::Registry {
1330 version: "1.0.0".to_string(),
1331 },
1332 None,
1333 );
1334 let options2 = BuildOptions {
1335 profile: Some("dev".to_string()),
1336 locked: false,
1337 ..Default::default()
1338 };
1339 let (binary2, _target) = builder.build(&krate2, &options2).unwrap();
1340 let binary2_name = binary2.file_name().unwrap().to_str().unwrap();
1341 assert_eq!(binary2_name, expected_bin_name("stale-serde"));
1342 let sbom2 = read_sbom_for_binary(&binary2);
1343
1344 let version = get_sbom_component_version(&sbom2, "serde").unwrap();
1345 assert_ne!(
1346 version, "1.0.5",
1347 "Without --locked, should resolve to newer serde"
1348 );
1349 assert!(version.starts_with("1.0."), "Should still be serde 1.0.x");
1350
1351 crate::sbom::tests::assert_sboms_ne(&sbom1, &sbom2);
1352 assert_cache_miss(&binary1, &binary2);
1353 }
1354
1355 #[test]
1356 fn same_locked_flag_produces_cache_hit() {
1357 let (builder, _temp) = test_builder();
1358 let tc = CrateTestCase::stale_serde();
1359
1360 let krate1 = fake_downloaded_crate(
1361 &tc,
1362 FakeSourceType::Registry {
1363 version: "1.0.0".to_string(),
1364 },
1365 None,
1366 );
1367 let options = BuildOptions {
1368 profile: Some("dev".to_string()),
1369 locked: true,
1370 ..Default::default()
1371 };
1372
1373 let (binary1, _target) = builder.build(&krate1, &options).unwrap();
1374 let binary1_name = binary1.file_name().unwrap().to_str().unwrap();
1375 assert_eq!(binary1_name, expected_bin_name("stale-serde"));
1376
1377 let krate2 = fake_downloaded_crate(
1378 &tc,
1379 FakeSourceType::Registry {
1380 version: "1.0.0".to_string(),
1381 },
1382 None,
1383 );
1384
1385 let (binary2, _target) = builder.build(&krate2, &options).unwrap();
1386 let binary2_name = binary2.file_name().unwrap().to_str().unwrap();
1387 assert_eq!(binary2_name, expected_bin_name("stale-serde"));
1388
1389 assert_cache_hit(&binary1, &binary2);
1390 }
1391
1392 #[test]
1393 fn different_features_different_dependencies() {
1394 let (builder, _temp) = test_builder();
1395 let tc = CrateTestCase::timestamp();
1396
1397 let krate1 = fake_downloaded_crate(
1398 &tc,
1399 FakeSourceType::Registry {
1400 version: "1.0.0".to_string(),
1401 },
1402 None,
1403 );
1404 let options1 = BuildOptions {
1405 profile: Some("dev".to_string()),
1406 ..Default::default()
1407 };
1408 let (binary1, _target) = builder.build(&krate1, &options1).unwrap();
1409 let binary1_name = binary1.file_name().unwrap().to_str().unwrap();
1410 assert_eq!(binary1_name, expected_bin_name("timestamp"));
1411 let sbom1 = read_sbom_for_binary(&binary1);
1412 let output1 = run_timestamp_binary(&binary1);
1413
1414 let krate2 = fake_downloaded_crate(
1415 &tc,
1416 FakeSourceType::Registry {
1417 version: "1.0.0".to_string(),
1418 },
1419 None,
1420 );
1421 let options2 = BuildOptions {
1422 profile: Some("dev".to_string()),
1423 features: vec!["frobnulator".to_string()],
1424 no_default_features: true,
1425 ..Default::default()
1426 };
1427 let (binary2, _target) = builder.build(&krate2, &options2).unwrap();
1428 let binary2_name = binary2.file_name().unwrap().to_str().unwrap();
1429 assert_eq!(binary2_name, expected_bin_name("timestamp"));
1430 let sbom2 = read_sbom_for_binary(&binary2);
1431 let output2 = run_timestamp_binary(&binary2);
1432
1433 assert!(output1.features.contains(&"gonkolator".to_string()));
1434 assert!(output2.features.contains(&"frobnulator".to_string()));
1435
1436 crate::sbom::tests::assert_sboms_ne(&sbom1, &sbom2);
1437 assert_cache_miss_by_timestamp(&output1, &output2);
1438 }
1439
1440 #[test]
1441 fn all_features_includes_all_dependencies() {
1442 let (builder, _temp) = test_builder();
1443 let tc = CrateTestCase::timestamp();
1444
1445 let krate = fake_downloaded_crate(
1446 &tc,
1447 FakeSourceType::Registry {
1448 version: "1.0.0".to_string(),
1449 },
1450 None,
1451 );
1452 let options = BuildOptions {
1453 profile: Some("dev".to_string()),
1454 all_features: true,
1455 ..Default::default()
1456 };
1457
1458 let (binary, _target) = builder.build(&krate, &options).unwrap();
1459 let binary_name = binary.file_name().unwrap().to_str().unwrap();
1460 assert_eq!(binary_name, expected_bin_name("timestamp"));
1461 let output = run_timestamp_binary(&binary);
1462
1463 assert!(
1464 output.features.contains(&"gonkolator".to_string()),
1465 "Should have gonkolator"
1466 );
1467 assert!(
1468 output.features.contains(&"frobnulator".to_string()),
1469 "Should have frobnulator"
1470 );
1471 }
1472
1473 #[test]
1474 fn default_is_locked_true() {
1475 let (builder, _temp) = test_builder();
1476 let tc = CrateTestCase::stale_serde();
1477
1478 let krate = fake_downloaded_crate(
1479 &tc,
1480 FakeSourceType::Registry {
1481 version: "1.0.0".to_string(),
1482 },
1483 None,
1484 );
1485 let options = BuildOptions::default();
1486
1487 let (binary, _target) = builder.build(&krate, &options).unwrap();
1488 let sbom = read_sbom_for_binary(&binary);
1489
1490 assert_eq!(
1491 get_sbom_component_version(&sbom, "serde"),
1492 Some("1.0.5".to_string()),
1493 "Default (locked=true) should honor Cargo.lock"
1494 );
1495 }
1496
1497 #[test]
1498 fn frozen_honors_cargo_lock_and_is_offline() {
1499 let (builder, _temp) = test_builder();
1500 let tc = CrateTestCase::stale_serde();
1501
1502 let krate = fake_downloaded_crate(
1503 &tc,
1504 FakeSourceType::Registry {
1505 version: "1.0.0".to_string(),
1506 },
1507 None,
1508 );
1509 let options = BuildOptions {
1510 profile: Some("dev".to_string()),
1511 locked: true,
1512 offline: true,
1513 ..Default::default()
1514 };
1515
1516 let (binary, _target) = builder.build(&krate, &options).unwrap();
1517 let sbom = read_sbom_for_binary(&binary);
1518
1519 assert_eq!(
1520 get_sbom_component_version(&sbom, "serde"),
1521 Some("1.0.5".to_string()),
1522 "Frozen should honor Cargo.lock"
1523 );
1524
1525 assert!(options.offline, "Frozen should set offline mode");
1526 }
1527 }
1528
1529 mod source_types {
1530 use super::*;
1531
1532 #[test]
1533 fn local_dir_never_cached() {
1534 let (builder, _temp) = test_builder();
1535 let tc = CrateTestCase::simple_bin_no_deps();
1536
1537 let krate = fake_downloaded_crate(&tc, FakeSourceType::LocalDir, None);
1538
1539 let options = BuildOptions {
1540 profile: Some("dev".to_string()),
1541 ..Default::default()
1542 };
1543
1544 let (binary, _target) = builder.build(&krate, &options).unwrap();
1545
1546 assert!(!binary.starts_with(&builder.config.bin_dir));
1547 assert!(binary.starts_with(tc.path()));
1548
1549 let binary_name = binary.file_name().unwrap().to_str().unwrap();
1550 assert_eq!(binary_name, expected_bin_name("simple-bin-no-deps"));
1551
1552 let sbom_path = read_sbom_for_binary(&binary);
1553 assert!(!sbom_path.exists());
1554 }
1555
1556 #[test]
1557 fn registry_source_cached_with_sbom() {
1558 let (builder, _temp) = test_builder();
1559 let tc = CrateTestCase::simple_bin_no_deps();
1560
1561 let krate1 = fake_downloaded_crate(
1562 &tc,
1563 FakeSourceType::Registry {
1564 version: "1.0.0".to_string(),
1565 },
1566 None,
1567 );
1568 let options = BuildOptions {
1569 profile: Some("dev".to_string()),
1570 ..Default::default()
1571 };
1572
1573 let (binary1, _target) = builder.build(&krate1, &options).unwrap();
1574
1575 assert!(binary1.starts_with(&builder.config.bin_dir));
1576
1577 let binary1_name = binary1.file_name().unwrap().to_str().unwrap();
1578 assert_eq!(binary1_name, expected_bin_name("simple-bin-no-deps"));
1579
1580 let sbom_path = read_sbom_for_binary(&binary1);
1581 assert!(sbom_path.exists());
1582
1583 let krate2 = fake_downloaded_crate(
1584 &tc,
1585 FakeSourceType::Registry {
1586 version: "1.0.0".to_string(),
1587 },
1588 None,
1589 );
1590 let (binary2, _target) = builder.build(&krate2, &options).unwrap();
1591 let binary2_name = binary2.file_name().unwrap().to_str().unwrap();
1592 assert_eq!(binary2_name, expected_bin_name("simple-bin-no-deps"));
1593
1594 assert_cache_hit(&binary1, &binary2);
1595 }
1596
1597 #[test]
1598 fn git_source_cached_with_sbom() {
1599 let (builder, _temp) = test_builder();
1600 let tc = CrateTestCase::simple_bin_no_deps();
1601
1602 let krate1 = fake_downloaded_crate(
1603 &tc,
1604 FakeSourceType::Git {
1605 url: "https://github.com/example/test.git".to_string(),
1606 rev: "abc123".to_string(),
1607 },
1608 None,
1609 );
1610 let options = BuildOptions {
1611 profile: Some("dev".to_string()),
1612 ..Default::default()
1613 };
1614
1615 let (binary1, _target) = builder.build(&krate1, &options).unwrap();
1616
1617 assert!(binary1.starts_with(&builder.config.bin_dir));
1618
1619 let binary1_name = binary1.file_name().unwrap().to_str().unwrap();
1620 assert_eq!(binary1_name, expected_bin_name("simple-bin-no-deps"));
1621
1622 let sbom_path = read_sbom_for_binary(&binary1);
1623 assert!(sbom_path.exists());
1624
1625 let krate2 = fake_downloaded_crate(
1626 &tc,
1627 FakeSourceType::Git {
1628 url: "https://github.com/example/test.git".to_string(),
1629 rev: "abc123".to_string(),
1630 },
1631 None,
1632 );
1633 let (binary2, _target) = builder.build(&krate2, &options).unwrap();
1634 let binary2_name = binary2.file_name().unwrap().to_str().unwrap();
1635 assert_eq!(binary2_name, expected_bin_name("simple-bin-no-deps"));
1636
1637 assert_cache_hit(&binary1, &binary2);
1638 }
1639 }
1640
1641 mod proc_macro_detection {
1642 use super::*;
1643
1644 #[test]
1645 fn proc_macro_marked_as_build_dep() {
1646 let (builder, _temp) = test_builder();
1647 let tc = CrateTestCase::proc_macro_dep();
1648
1649 let krate = fake_downloaded_crate(
1650 &tc,
1651 FakeSourceType::Registry {
1652 version: "1.0.0".to_string(),
1653 },
1654 None,
1655 );
1656 let options = BuildOptions {
1657 profile: Some("dev".to_string()),
1658 ..Default::default()
1659 };
1660
1661 let (binary, _target) = builder.build(&krate, &options).unwrap();
1662 let binary_name = binary.file_name().unwrap().to_str().unwrap();
1663 assert_eq!(binary_name, expected_bin_name("proc-macro-dep"));
1664
1665 let sbom_path = read_sbom_for_binary(&binary);
1666
1667 let json_str = fs::read_to_string(&sbom_path).unwrap();
1668 let bom: serde_cyclonedx::cyclonedx::v_1_4::CycloneDx = serde_json::from_str(&json_str).unwrap();
1669
1670 let components = bom.components.unwrap();
1671 let serde_derive = components
1672 .iter()
1673 .find(|c| c.name.as_str() == "serde_derive")
1674 .expect("serde_derive should be in components");
1675
1676 if let Some(ref props) = serde_derive.properties {
1677 let has_build_kind = props.iter().any(|p| {
1678 p.name.as_deref() == Some("cdx:rustc:dependency_kind")
1679 && p.value.as_deref() == Some("build")
1680 });
1681 assert!(has_build_kind, "proc-macro should be marked as build dependency");
1682 } else {
1683 panic!("proc-macro should have dependency_kind property");
1684 }
1685 }
1686 }
1687
1688 mod build_options {
1689 use super::*;
1690 use crate::cli::Cli;
1691
1692 mod features_parsing {
1693 use super::*;
1694
1695 #[test]
1697 fn empty_features_string() {
1698 let config = Config::default();
1699 let cli = Cli::parse_from_test_args(["--features", "", "tool"]);
1700 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1701
1702 assert!(options.features.is_empty());
1703 }
1704
1705 #[test]
1707 fn single_feature() {
1708 let config = Config::default();
1709 let cli = Cli::parse_from_test_args(["--features", "feat1", "tool"]);
1710 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1711
1712 assert_eq!(options.features, vec!["feat1"]);
1713 }
1714
1715 #[test]
1717 fn comma_separated_features() {
1718 let config = Config::default();
1719 let cli = Cli::parse_from_test_args(["--features", "feat1,feat2", "tool"]);
1720 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1721
1722 assert_eq!(options.features, vec!["feat1", "feat2"]);
1723 }
1724
1725 #[test]
1727 fn space_separated_features() {
1728 let config = Config::default();
1729 let cli = Cli::parse_from_test_args(["--features", "feat1 feat2", "tool"]);
1730 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1731
1732 assert_eq!(options.features, vec!["feat1", "feat2"]);
1733 }
1734
1735 #[test]
1737 fn mixed_separator_features() {
1738 let config = Config::default();
1739 let cli = Cli::parse_from_test_args(["--features", "feat1, feat2 feat3", "tool"]);
1740 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1741
1742 assert_eq!(options.features, vec!["feat1", "feat2", "feat3"]);
1743 }
1744
1745 #[test]
1747 fn whitespace_handling() {
1748 let config = Config::default();
1749 let cli = Cli::parse_from_test_args(["--features", " feat1 , feat2 ", "tool"]);
1750 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1751
1752 assert_eq!(options.features, vec!["feat1", "feat2"]);
1753 }
1754
1755 #[test]
1757 fn no_features_flag() {
1758 let config = Config::default();
1759 let cli = Cli::parse_from_test_args(["tool"]);
1760 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1761
1762 assert!(options.features.is_empty());
1763 }
1764 }
1765
1766 mod profile_selection {
1767 use super::*;
1768
1769 #[test]
1771 fn debug_flag_maps_to_dev() {
1772 let config = Config::default();
1773 let cli = Cli::parse_from_test_args(["--debug", "tool"]);
1774 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1775
1776 assert_eq!(options.profile, Some("dev".to_string()));
1777 }
1778
1779 #[test]
1781 fn explicit_profile() {
1782 let config = Config::default();
1783 let cli = Cli::parse_from_test_args(["--profile", "custom", "tool"]);
1784 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1785
1786 assert_eq!(options.profile, Some("custom".to_string()));
1787 }
1788
1789 #[test]
1791 fn no_profile_specified() {
1792 let config = Config::default();
1793 let cli = Cli::parse_from_test_args(["tool"]);
1794 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1795
1796 assert_eq!(options.profile, None);
1797 }
1798 }
1799
1800 mod build_target_selection {
1801 use super::*;
1802
1803 #[test]
1805 fn default_bin_when_no_flags() {
1806 let config = Config::default();
1807 let cli = Cli::parse_from_test_args(["tool"]);
1808 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1809
1810 assert_eq!(options.build_target, BuildTarget::DefaultBin);
1811 }
1812
1813 #[test]
1815 fn explicit_bin() {
1816 let config = Config::default();
1817 let cli = Cli::parse_from_test_args(["--bin", "foo", "tool"]);
1818 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1819
1820 assert_eq!(options.build_target, BuildTarget::Bin("foo".to_string()));
1821 }
1822
1823 #[test]
1825 fn explicit_example() {
1826 let config = Config::default();
1827 let cli = Cli::parse_from_test_args(["--example", "bar", "tool"]);
1828 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1829
1830 assert_eq!(options.build_target, BuildTarget::Example("bar".to_string()));
1831 }
1832 }
1833
1834 mod locked_offline_from_config {
1835 use super::*;
1836
1837 #[test]
1842 fn reads_default_locked_true() {
1843 let config = Config::default();
1844 let cli = Cli::parse_from_test_args(["tool"]);
1845 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1846
1847 assert!(options.locked, "Should read locked=true from default Config");
1848 assert!(!options.offline, "Should read offline=false from default Config");
1849 }
1850
1851 #[test]
1852 fn reads_config_locked_false() {
1853 let config = Config {
1854 locked: false,
1855 ..Default::default()
1856 };
1857 let cli = Cli::parse_from_test_args(["tool"]);
1858 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1859
1860 assert!(!options.locked, "Should read locked=false from Config");
1861 }
1862
1863 #[test]
1864 fn reads_config_offline_true() {
1865 let config = Config {
1866 offline: true,
1867 ..Default::default()
1868 };
1869 let cli = Cli::parse_from_test_args(["tool"]);
1870 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1871
1872 assert!(options.offline, "Should read offline=true from Config");
1873 }
1874
1875 #[test]
1876 fn reads_config_both_values() {
1877 let config = Config {
1878 locked: false,
1879 offline: true,
1880 ..Default::default()
1881 };
1882 let cli = Cli::parse_from_test_args(["tool"]);
1883 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1884
1885 assert!(!options.locked, "Should read locked=false from Config");
1886 assert!(options.offline, "Should read offline=true from Config");
1887 }
1888 }
1889
1890 mod toolchain_from_config {
1891 use super::*;
1892
1893 #[test]
1899 fn reads_default_none() {
1900 let config = Config::default();
1901 let cli = Cli::parse_from_test_args(["tool"]);
1902 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1903
1904 assert_eq!(
1905 options.toolchain, None,
1906 "Should read toolchain=None from default Config"
1907 );
1908 }
1909
1910 #[test]
1911 fn reads_config_toolchain() {
1912 let config = Config {
1913 toolchain: Some("stable".to_string()),
1914 ..Default::default()
1915 };
1916 let cli = Cli::parse_from_test_args(["tool"]);
1917 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1918
1919 assert_eq!(
1920 options.toolchain,
1921 Some("stable".to_string()),
1922 "Should read toolchain from Config"
1923 );
1924 }
1925
1926 #[test]
1927 fn reads_config_nightly() {
1928 let config = Config {
1929 toolchain: Some("nightly".to_string()),
1930 ..Default::default()
1931 };
1932 let cli = Cli::parse_from_test_args(["tool"]);
1933 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1934
1935 assert_eq!(
1936 options.toolchain,
1937 Some("nightly".to_string()),
1938 "Should read toolchain from Config"
1939 );
1940 }
1941 }
1942
1943 mod direct_passthrough {
1944 use super::*;
1945
1946 #[test]
1948 fn all_features() {
1949 let config = Config::default();
1950 let cli = Cli::parse_from_test_args(["--all-features", "tool"]);
1951 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1952
1953 assert!(options.all_features);
1954 }
1955
1956 #[test]
1958 fn no_default_features() {
1959 let config = Config::default();
1960 let cli = Cli::parse_from_test_args(["--no-default-features", "tool"]);
1961 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1962
1963 assert!(options.no_default_features);
1964 }
1965
1966 #[test]
1968 fn target() {
1969 let config = Config::default();
1970 let cli = Cli::parse_from_test_args(["--target", "x86_64-unknown-linux-gnu", "tool"]);
1971 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1972
1973 assert_eq!(
1974 options.target.as_ref().map(TargetTriple::as_str),
1975 Some("x86_64-unknown-linux-gnu")
1976 );
1977 }
1978
1979 #[test]
1983 fn target_unparsable_by_target_lexicon() {
1984 let config = Config::default();
1985 let cli = Cli::parse_from_test_args(["--target", "arm64ec-pc-windows-msvc", "tool"]);
1986 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
1987
1988 assert_eq!(
1989 options.target.as_ref().map(TargetTriple::as_str),
1990 Some("arm64ec-pc-windows-msvc")
1991 );
1992 }
1993
1994 #[test]
1996 fn jobs() {
1997 let config = Config::default();
1998 let cli = Cli::parse_from_test_args(["--jobs", "4", "tool"]);
1999 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
2000
2001 assert_eq!(options.jobs, Some(4));
2002 }
2003
2004 #[test]
2006 fn ignore_rust_version() {
2007 let config = Config::default();
2008 let cli = Cli::parse_from_test_args(["--ignore-rust-version", "tool"]);
2009 let options = BuildOptions::load(&config, &cli.crate_args().to_build_overrides()).unwrap();
2010
2011 assert!(options.ignore_rust_version);
2012 }
2013 }
2014 }
2015}