1use std::collections::BTreeMap;
2use std::fmt::{self, Display, Formatter};
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8
9use crate::CrossBuildError;
10
11#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
13pub enum TargetParseError {
14 #[error("empty target triple")]
15 Empty,
16 #[error("target triple contains whitespace")]
17 Whitespace,
18 #[error("invalid target triple format: expected at least {expected_min} components, got {actual} for `{triple}`")]
19 InvalidFormat { triple: String, expected_min: usize, actual: usize },
20 #[error("unknown architecture: {0}")]
21 UnknownArchitecture(String),
22 #[error("unknown vendor: {0}")]
23 UnknownVendor(String),
24 #[error("unknown operating system: {0}")]
25 UnknownOs(String),
26 #[error("unknown abi: {0}")]
27 UnknownAbi(String),
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub enum Architecture {
33 X86_64,
34 AArch64,
35 X86,
36 Arm,
37 Arm64,
38 RiscV64,
39 PowerPC64,
40 S390x,
41 Mips64,
42 LoongArch64,
43 Wasm32,
44 Wasm64,
45 Other(String),
46}
47
48impl Architecture {
49 pub fn parse(s: &str) -> Result<Self, TargetParseError> {
50 Ok(match s.to_lowercase().as_str() {
51 "x86_64" | "amd64" => Architecture::X86_64,
52 "aarch64" | "arm64" => Architecture::AArch64,
53 "i686" | "i586" | "i386" | "x86" => Architecture::X86,
54 "arm" | "armv7" | "armv7a" | "armv7hf" => Architecture::Arm,
55 "riscv64" | "riscv64gc" => Architecture::RiscV64,
56 "powerpc64le" | "ppc64le" => Architecture::PowerPC64,
57 "s390x" => Architecture::S390x,
58 "mips64" | "mips64el" => Architecture::Mips64,
59 "loongarch64" => Architecture::LoongArch64,
60 "wasm32" => Architecture::Wasm32,
61 "wasm64" => Architecture::Wasm64,
62 other => Architecture::Other(other.to_string()),
63 })
64 }
65
66 pub fn pointer_width(&self) -> u8 {
67 match self {
68 Architecture::X86_64
69 | Architecture::AArch64
70 | Architecture::Arm64
71 | Architecture::RiscV64
72 | Architecture::PowerPC64
73 | Architecture::S390x
74 | Architecture::Mips64
75 | Architecture::LoongArch64
76 | Architecture::Wasm64 => 64,
77 Architecture::X86
78 | Architecture::Arm
79 | Architecture::Wasm32 => 32,
80 Architecture::Other(_) => 64,
81 }
82 }
83
84 pub fn endianness(&self) -> Endianness {
85 match self {
86 Architecture::X86_64 | Architecture::X86 | Architecture::Wasm32 | Architecture::Wasm64 => Endianness::Little,
87 Architecture::AArch64 | Architecture::Arm | Architecture::Arm64 | Architecture::RiscV64 => Endianness::Little,
88 Architecture::PowerPC64 => Endianness::Little,
89 Architecture::S390x => Endianness::Big,
90 Architecture::Mips64 => Endianness::Big,
91 Architecture::LoongArch64 => Endianness::Little,
92 Architecture::Other(_) => Endianness::Little,
93 }
94 }
95
96 pub fn name(&self) -> &str {
97 match self {
98 Architecture::X86_64 => "x86_64",
99 Architecture::AArch64 => "aarch64",
100 Architecture::X86 => "i686",
101 Architecture::Arm => "arm",
102 Architecture::Arm64 => "aarch64",
103 Architecture::RiscV64 => "riscv64",
104 Architecture::PowerPC64 => "powerpc64le",
105 Architecture::S390x => "s390x",
106 Architecture::Mips64 => "mips64",
107 Architecture::LoongArch64 => "loongarch64",
108 Architecture::Wasm32 => "wasm32",
109 Architecture::Wasm64 => "wasm64",
110 Architecture::Other(name) => name,
111 }
112 }
113}
114
115impl Display for Architecture {
116 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
117 f.write_str(self.name())
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123pub enum Endianness {
124 Little,
125 Big,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130pub enum Vendor {
131 Unknown,
132 Pc,
133 Apple,
134 Linux,
135 Uwp,
136 Fuchsia,
137 PlayStation,
138 Nintendo,
139 Sony,
140 Microsoft,
141 Other,
142}
143
144impl Vendor {
145 pub fn parse(s: &str) -> Result<Self, TargetParseError> {
146 Ok(match s.to_lowercase().as_str() {
147 "unknown" => Vendor::Unknown,
148 "pc" => Vendor::Pc,
149 "apple" => Vendor::Apple,
150 "linux" => Vendor::Linux,
151 "uwp" => Vendor::Uwp,
152 "fuchsia" => Vendor::Fuchsia,
153 "playstation" => Vendor::PlayStation,
154 "nintendo" => Vendor::Nintendo,
155 "sony" => Vendor::Sony,
156 "microsoft" => Vendor::Microsoft,
157 _ => Vendor::Other,
158 })
159 }
160
161 pub fn name(&self) -> &str {
162 match self {
163 Vendor::Unknown => "unknown",
164 Vendor::Pc => "pc",
165 Vendor::Apple => "apple",
166 Vendor::Linux => "linux",
167 Vendor::Uwp => "uwp",
168 Vendor::Fuchsia => "fuchsia",
169 Vendor::PlayStation => "playstation",
170 Vendor::Nintendo => "nintendo",
171 Vendor::Sony => "sony",
172 Vendor::Microsoft => "microsoft",
173 Vendor::Other => "unknown",
174 }
175 }
176}
177
178impl Display for Vendor {
179 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
180 f.write_str(self.name())
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Hash)]
186pub enum OperatingSystem {
187 None,
188 Linux,
189 Windows,
190 MacOs,
191 FreeBSD,
192 NetBSD,
193 OpenBSD,
194 DragonflyBSD,
195 Solaris,
196 Illumos,
197 Android,
198 Ios,
199 TvOS,
200 WatchOS,
201 Wasm,
202 Wasi,
203 Uefi,
204 Redox,
205 Heron,
206 Fuchsia,
207 Zos,
208 Other(String),
209}
210
211impl OperatingSystem {
212 pub fn parse(s: &str) -> Result<Self, TargetParseError> {
213 Ok(match s.to_lowercase().as_str() {
214 "none" => OperatingSystem::None,
215 "linux" => OperatingSystem::Linux,
216 "windows" => OperatingSystem::Windows,
217 "darwin" | "macos" => OperatingSystem::MacOs,
218 "freebsd" => OperatingSystem::FreeBSD,
219 "openbsd" => OperatingSystem::OpenBSD,
220 "netbsd" => OperatingSystem::NetBSD,
221 "dragonflybsd" | "dragonfly" => OperatingSystem::DragonflyBSD,
222 "solaris" => OperatingSystem::Solaris,
223 "illumos" => OperatingSystem::Illumos,
224 "android" => OperatingSystem::Android,
225 "ios" => OperatingSystem::Ios,
226 "tvos" => OperatingSystem::TvOS,
227 "watchos" => OperatingSystem::WatchOS,
228 "wasm" => OperatingSystem::Wasm,
229 "wasi" => OperatingSystem::Wasi,
230 "fuchsia" => OperatingSystem::Fuchsia,
231 "redox" => OperatingSystem::Redox,
232 "heron" => OperatingSystem::Heron,
233 "zos" => OperatingSystem::Zos,
234 other => OperatingSystem::Other(other.to_string()),
235 })
236 }
237
238 pub fn name(&self) -> &str {
239 match self {
240 OperatingSystem::None => "none",
241 OperatingSystem::Linux => "linux",
242 OperatingSystem::Windows => "windows",
243 OperatingSystem::MacOs => "darwin",
244 OperatingSystem::FreeBSD => "freebsd",
245 OperatingSystem::OpenBSD => "openbsd",
246 OperatingSystem::NetBSD => "netbsd",
247 OperatingSystem::DragonflyBSD => "dragonflybsd",
248 OperatingSystem::Solaris => "solaris",
249 OperatingSystem::Illumos => "illumos",
250 OperatingSystem::Android => "android",
251 OperatingSystem::Ios => "ios",
252 OperatingSystem::TvOS => "tvos",
253 OperatingSystem::WatchOS => "watchos",
254 OperatingSystem::Wasm => "wasi",
255 OperatingSystem::Wasi => "wasi",
256 OperatingSystem::Uefi => "uefi",
257 OperatingSystem::Redox => "redox",
258 OperatingSystem::Heron => "heron",
259 OperatingSystem::Fuchsia => "fuchsia",
260 OperatingSystem::Zos => "zos",
261 OperatingSystem::Other(s) => s,
262 }
263 }
264
265 pub fn is_unix_like(&self) -> bool {
266 matches!(
267 self,
268 OperatingSystem::Linux
269 | OperatingSystem::MacOs
270 | OperatingSystem::FreeBSD
271 | OperatingSystem::OpenBSD
272 | OperatingSystem::NetBSD
273 | OperatingSystem::DragonflyBSD
274 | OperatingSystem::Solaris
275 | OperatingSystem::Illumos
276 | OperatingSystem::Android
277 | OperatingSystem::Redox
278 | OperatingSystem::Fuchsia
279 )
280 }
281}
282
283impl Display for OperatingSystem {
284 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
285 f.write_str(self.name())
286 }
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
291pub enum Abi {
292 None,
293 Gnu,
294 Musl,
295 Msvc,
296 Uwp,
297 Wasm32,
298 Wasm64,
299 Eabi,
300 Eabihf,
301 Android,
302 Simulator,
303}
304
305impl Abi {
306 pub fn parse(s: &str) -> Result<Self, TargetParseError> {
307 Ok(match s.to_lowercase().as_str() {
308 "none" => Abi::None,
309 "gnu" => Abi::Gnu,
310 "musl" => Abi::Musl,
311 "msvc" => Abi::Msvc,
312 "uwp" => Abi::Uwp,
313 "wasm32" => Abi::Wasm32,
314 "wasm64" => Abi::Wasm64,
315 "eabi" => Abi::Eabi,
316 "eabihf" => Abi::Eabihf,
317 "android" => Abi::Android,
318 "simulator" => Abi::Simulator,
319 other => return Err(TargetParseError::UnknownAbi(other.to_string())),
320 })
321 }
322
323 pub fn default_for_os(os: &OperatingSystem) -> Self {
324 match os {
325 OperatingSystem::Linux => Abi::Gnu,
326 OperatingSystem::Windows => Abi::Msvc,
327 OperatingSystem::MacOs => Abi::None,
328 OperatingSystem::Wasm => Abi::Wasm32,
329 OperatingSystem::Wasi => Abi::None,
330 OperatingSystem::Android => Abi::Android,
331 OperatingSystem::FreeBSD
332 | OperatingSystem::OpenBSD
333 | OperatingSystem::NetBSD
334 | OperatingSystem::DragonflyBSD => Abi::Gnu,
335 _ => Abi::None,
336 }
337 }
338
339 pub fn name(&self) -> &str {
340 match self {
341 Abi::None => "none",
342 Abi::Gnu => "gnu",
343 Abi::Musl => "musl",
344 Abi::Msvc => "msvc",
345 Abi::Uwp => "uwp",
346 Abi::Wasm32 => "wasm32",
347 Abi::Wasm64 => "wasm64",
348 Abi::Eabi => "eabi",
349 Abi::Eabihf => "eabihf",
350 Abi::Android => "android",
351 Abi::Simulator => "simulator",
352 }
353 }
354}
355
356impl Display for Abi {
357 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
358 f.write_str(self.name())
359 }
360}
361
362impl Default for Abi {
363 fn default() -> Self {
364 Abi::None
365 }
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
370pub enum TargetFamily {
371 Windows,
372 Linux,
373 MacOs,
374 Wasm,
375 BareMetal,
376 Other,
377}
378
379impl TargetFamily {
380 fn from_os_abi(os: &OperatingSystem, abi: &Abi) -> Self {
381 match (os, abi) {
382 (OperatingSystem::Windows, _) => TargetFamily::Windows,
383 (OperatingSystem::Linux, _) => TargetFamily::Linux,
384 (OperatingSystem::MacOs, _) => TargetFamily::MacOs,
385 (OperatingSystem::Wasm, _) | (OperatingSystem::Wasi, _) => TargetFamily::Wasm,
386 (OperatingSystem::None, _) => TargetFamily::BareMetal,
387 _ => TargetFamily::Other,
388 }
389 }
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, Hash)]
394pub struct TargetTriple {
395 pub triple: String,
396 pub arch: Architecture,
397 pub vendor: Vendor,
398 pub os: OperatingSystem,
399 pub abi: Abi,
400 pub family: TargetFamily,
401}
402
403impl TargetTriple {
404 pub fn parse(triple: &str) -> Result<Self, TargetParseError> {
405 let trimmed = triple.trim();
406 if trimmed.is_empty() {
407 return Err(TargetParseError::Empty);
408 }
409
410 if trimmed.chars().any(char::is_whitespace) {
411 return Err(TargetParseError::Whitespace);
412 }
413
414 let parts: Vec<&str> = trimmed.split('-').collect();
415 if parts.len() < 2 {
416 return Err(TargetParseError::InvalidFormat {
417 triple: trimmed.to_string(),
418 expected_min: 2,
419 actual: parts.len(),
420 });
421 }
422
423 let arch = Architecture::parse(parts[0])?;
424
425 let (vendor, os, abi, family) = if parts.len() == 2 {
426 let os = OperatingSystem::parse(parts[1])?;
428 let abi = Abi::default_for_os(&os);
429 let family = TargetFamily::from_os_abi(&os, &abi);
430 (Vendor::Unknown, os, abi, family)
431 } else if parts.len() == 3 {
432 let vendor = Vendor::parse(parts[1])?;
434 let os = OperatingSystem::parse(parts[2])?;
435 let abi = Abi::default_for_os(&os);
436 let family = TargetFamily::from_os_abi(&os, &abi);
437 (vendor, os, abi, family)
438 } else {
439 let vendor = Vendor::parse(parts[1])?;
440 let os = OperatingSystem::parse(parts[2])?;
441 let abi = Abi::parse(parts[3])?;
442 let family = TargetFamily::from_os_abi(&os, &abi);
443 (vendor, os, abi, family)
444 };
445
446 Ok(Self {
447 triple: trimmed.to_string(),
448 arch,
449 vendor,
450 os,
451 abi,
452 family,
453 })
454 }
455
456 pub fn as_str(&self) -> &str {
457 &self.triple
458 }
459
460 pub fn is_windows(&self) -> bool {
461 matches!(self.family, TargetFamily::Windows)
462 }
463
464 pub fn is_linux(&self) -> bool {
465 matches!(self.family, TargetFamily::Linux)
466 }
467
468 pub fn is_macos(&self) -> bool {
469 matches!(self.family, TargetFamily::MacOs)
470 }
471
472 pub fn is_wasm(&self) -> bool {
473 matches!(self.family, TargetFamily::Wasm)
474 }
475
476 pub fn is_bare_metal(&self) -> bool {
477 matches!(self.family, TargetFamily::BareMetal)
478 }
479
480 pub fn pointer_width(&self) -> u8 {
481 self.arch.pointer_width()
482 }
483
484 pub fn endianness(&self) -> Endianness {
485 self.arch.endianness()
486 }
487}
488
489impl FromStr for TargetTriple {
490 type Err = TargetParseError;
491
492 fn from_str(s: &str) -> Result<Self, Self::Err> {
493 Self::parse(s)
494 }
495}
496
497impl Display for TargetTriple {
498 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
499 f.write_str(&self.triple)
500 }
501}
502
503impl PartialOrd for TargetTriple {
504 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
505 Some(self.cmp(other))
506 }
507}
508
509impl Ord for TargetTriple {
510 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
511 self.triple.cmp(&other.triple)
512 }
513}
514
515impl TargetTriple {
517 pub fn family(&self) -> TargetFamily {
518 self.family
519 }
520}
521
522pub trait TargetFamilyExt {
524 fn family(&self) -> TargetFamily;
525}
526
527impl TargetFamilyExt for TargetTriple {
528 fn family(&self) -> TargetFamily {
529 self.family
530 }
531}
532
533pub struct KnownTargets;
535
536impl KnownTargets {
537 pub fn tier1() -> &'static [&'static str] {
539 &[
540 "x86_64-unknown-linux-gnu",
541 "x86_64-pc-windows-msvc",
542 "aarch64-unknown-linux-gnu",
543 "aarch64-apple-darwin",
544 "x86_64-apple-darwin",
545 ]
546 }
547
548 pub fn tier2() -> &'static [&'static str] {
550 &[
551 "x86_64-unknown-linux-musl",
552 "aarch64-unknown-linux-musl",
553 "x86_64-unknown-freebsd",
554 "aarch64-unknown-freebsd",
555 "x86_64-unknown-netbsd",
556 "aarch64-unknown-netbsd",
557 "x86_64-unknown-openbsd",
558 "x86_64-unknown-illumos",
559 "powerpc64le-unknown-linux-gnu",
560 "s390x-unknown-linux-gnu",
561 "riscv64gc-unknown-linux-gnu",
562 "x86_64-pc-windows-gnu",
563 "i686-pc-windows-msvc",
564 "i686-pc-windows-gnu",
565 "aarch64-pc-windows-msvc",
566 "wasm32-wasi",
567 "wasm32-unknown-unknown",
568 "wasm32-unknown-emscripten",
569 ]
570 }
571
572 pub fn is_tier1(target: &str) -> bool {
574 Self::tier1().contains(&target)
575 }
576
577 pub fn is_tier2(target: &str) -> bool {
579 Self::tier2().contains(&target)
580 }
581
582 pub fn all_known() -> Vec<&'static str> {
584 let mut targets = Vec::new();
585 targets.extend_from_slice(Self::tier1());
586 targets.extend_from_slice(Self::tier2());
587 targets
588 }
589}
590
591#[derive(Debug, Clone, Copy, PartialEq, Eq)]
593pub enum ExecutionMode {
594 DryRun,
595 Execute,
596}
597
598#[derive(Debug, Clone, Copy, PartialEq, Eq)]
600pub enum Profile {
601 Dev,
602 Release,
603 Custom(&'static str),
604}
605
606impl Default for Profile {
607 fn default() -> Self {
608 Profile::Dev
609 }
610}
611
612impl Display for Profile {
613 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
614 match self {
615 Profile::Dev => f.write_str("dev"),
616 Profile::Release => f.write_str("release"),
617 Profile::Custom(name) => f.write_str(name),
618 }
619 }
620}
621
622#[derive(Debug, Clone, PartialEq, Eq)]
624pub struct BuildRequest {
625 pub manifest_path: PathBuf,
626 pub target_triple: TargetTriple,
627 pub cargo_args: Vec<String>,
628 pub execution_mode: ExecutionMode,
629 pub verbose: bool,
630 pub profile: Profile,
631 pub features: Vec<String>,
632 pub no_default_features: bool,
633 pub workspace: bool,
634 pub exclude: Vec<String>,
635}
636
637impl BuildRequest {
638 pub fn new(manifest_path: PathBuf, target_triple: TargetTriple) -> Self {
639 Self {
640 manifest_path,
641 target_triple,
642 cargo_args: Vec::new(),
643 execution_mode: ExecutionMode::Execute,
644 verbose: false,
645 profile: Profile::default(),
646 features: Vec::new(),
647 no_default_features: false,
648 workspace: false,
649 exclude: Vec::new(),
650 }
651 }
652
653 pub fn with_cargo_args(mut self, args: Vec<String>) -> Self {
654 self.cargo_args = args;
655 self
656 }
657
658 pub fn with_execution_mode(mut self, mode: ExecutionMode) -> Self {
659 self.execution_mode = mode;
660 self
661 }
662
663 pub fn with_verbose(mut self, verbose: bool) -> Self {
664 self.verbose = verbose;
665 self
666 }
667
668 pub fn with_profile(mut self, profile: Profile) -> Self {
669 self.profile = profile;
670 self
671 }
672
673 pub fn with_features(mut self, features: Vec<String>) -> Self {
674 self.features = features;
675 self
676 }
677
678 pub fn with_no_default_features(mut self, no_default_features: bool) -> Self {
679 self.no_default_features = no_default_features;
680 self
681 }
682
683 pub fn with_workspace(mut self, workspace: bool) -> Self {
684 self.workspace = workspace;
685 self
686 }
687
688 pub fn with_exclude(mut self, exclude: Vec<String>) -> Self {
689 self.exclude = exclude;
690 self
691 }
692}
693
694#[derive(Debug, Clone, PartialEq, Eq)]
696pub struct HostInfo {
697 pub host_triple: TargetTriple,
698 pub os: OperatingSystem,
699 pub arch: Architecture,
700 pub rustc_version: Option<String>,
701 pub cargo_version: Option<String>,
702 pub target_dir: PathBuf,
703}
704
705impl HostInfo {
706 pub fn detect() -> Result<Self, HostDetectError> {
707 let host_triple_str = Self::detect_host_triple_from_rustc()?;
708 let host_triple = TargetTriple::parse(&host_triple_str)
709 .map_err(|e| HostDetectError::ParseError(e.to_string()))?;
710
711 let rustc_version = Self::detect_rustc_version();
712 let cargo_version = Self::detect_cargo_version();
713 let target_dir = std::env::current_dir()
714 .unwrap_or_else(|_| PathBuf::from("."))
715 .join("target")
716 .join("crossbuild");
717
718 Ok(Self {
719 os: host_triple.os.clone(),
720 arch: host_triple.arch.clone(),
721 host_triple,
722 rustc_version,
723 cargo_version,
724 target_dir,
725 })
726 }
727
728 fn detect_host_triple_from_rustc() -> Result<String, HostDetectError> {
729 let output = std::process::Command::new("rustc")
730 .arg("-vV")
731 .output()
732 .map_err(|_| HostDetectError::RustcNotFound)?;
733
734 let stdout = String::from_utf8(output.stdout)
735 .map_err(|_| HostDetectError::ParseError("invalid UTF-8".to_string()))?;
736
737 for line in stdout.lines() {
738 if line.starts_with("host: ") {
739 return Ok(line.strip_prefix("host: ").unwrap().to_string());
740 }
741 }
742
743 Err(HostDetectError::ParseError("host triple not found in rustc -vV output".to_string()))
744 }
745
746 fn detect_rustc_version() -> Option<String> {
747 std::process::Command::new("rustc")
748 .arg("--version")
749 .output()
750 .ok()
751 .and_then(|o| String::from_utf8(o.stdout).ok())
752 .map(|s| s.trim().to_string())
753 }
754
755 fn detect_cargo_version() -> Option<String> {
756 std::process::Command::new("cargo")
757 .arg("--version")
758 .output()
759 .ok()
760 .and_then(|o| String::from_utf8(o.stdout).ok())
761 .map(|s| s.trim().to_string())
762 }
763}
764
765#[derive(Debug, Clone, PartialEq, Eq)]
767pub enum HostDetectError {
768 ParseError(String),
769 RustcNotFound,
770 CargoNotFound,
771}
772
773impl Display for HostDetectError {
774 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
775 match self {
776 HostDetectError::ParseError(e) => write!(f, "failed to parse host triple: {e}"),
777 HostDetectError::RustcNotFound => f.write_str("rustc not found in PATH"),
778 HostDetectError::CargoNotFound => f.write_str("cargo not found in PATH"),
779 }
780 }
781}
782
783impl std::error::Error for HostDetectError {}
784
785#[derive(Debug, Clone, PartialEq, Eq)]
787pub struct TargetInfo {
788 pub triple: TargetTriple,
789 pub is_native: bool,
790 pub requires_cross: bool,
791 pub supported: TargetSupport,
792 pub toolchain_hint: ToolchainHint,
793 pub sysroot_hint: SysrootHint,
794 pub linker_hint: LinkerHint,
795}
796
797#[derive(Debug, Clone, Copy, PartialEq, Eq)]
799pub enum TargetSupport {
800 Tier1,
802 Tier2,
804 Tier3,
806 Unsupported,
808}
809
810#[derive(Debug, Clone, Copy, PartialEq, Eq)]
812pub enum ToolchainHint {
813 Rustup,
814 Zig,
815 CrossDocker,
816 Custom,
817}
818
819#[derive(Debug, Clone, Copy, PartialEq, Eq)]
821pub enum SysrootHint {
822 Rustup,
823 Zig,
824 Custom,
825 None,
826}
827
828#[derive(Debug, Clone, Copy, PartialEq, Eq)]
830pub enum LinkerHint {
831 SystemDefault,
832 Lld,
833 Mold,
834 Zig,
835 MSVC,
836 Custom,
837}
838
839#[derive(Debug, Clone, PartialEq, Eq)]
841pub struct CommandLine {
842 pub program: String,
843 pub args: Vec<String>,
844 pub env: BTreeMap<String, String>,
845 pub current_dir: PathBuf,
846}
847
848impl CommandLine {
849 pub fn new(program: impl Into<String>, current_dir: impl Into<PathBuf>) -> Self {
850 Self {
851 program: program.into(),
852 args: Vec::new(),
853 env: BTreeMap::new(),
854 current_dir: current_dir.into(),
855 }
856 }
857
858 pub fn push_arg(&mut self, arg: impl Into<String>) {
859 self.args.push(arg.into());
860 }
861
862 pub fn extend_args(&mut self, args: impl IntoIterator<Item = impl Into<String>>) {
863 self.args.extend(args.into_iter().map(Into::into));
864 }
865
866 pub fn set_env(&mut self, key: impl Into<String>, value: impl Into<String>) {
867 self.env.insert(key.into(), value.into());
868 }
869
870 pub fn extend_env(&mut self, env: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>) {
871 self.env.extend(env.into_iter().map(|(k, v)| (k.into(), v.into())));
872 }
873}
874
875impl Display for CommandLine {
876 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
877 write!(f, "{}", self.program)?;
878 for arg in &self.args {
879 write!(f, " ")?;
880 if arg.contains(' ') || arg.contains('"') {
881 write!(f, "{:?}", arg)?;
882 } else {
883 write!(f, "{arg}")?;
884 }
885 }
886 Ok(())
887 }
888}
889
890#[derive(Debug, Clone, PartialEq)]
892pub struct ProviderAction {
893 pub provider_name: String,
894 pub notes: Vec<String>,
895 pub env: BTreeMap<String, String>,
896 pub cargo_config: Option<toml::Table>,
897}
898
899#[derive(Debug, Clone, PartialEq)]
901pub struct CargoConfigSnippet {
902 pub target_section: Option<String>,
903 pub config: toml::Table,
904}
905
906#[derive(Debug, Clone, PartialEq, Eq)]
908pub enum PlanStep {
909 ValidateManifest { path: PathBuf },
910 ValidateTarget { target: TargetTriple },
911 DetectHost,
912 ResolveProviders,
913 PrepareEnvironment,
914 GenerateCargoConfig,
915 ResolveLinker,
916 PrepareCache,
917 InvokeCargo,
918 CaptureDiagnostics,
919 VerifyArtifacts,
920}
921
922#[derive(Debug, Clone, PartialEq)]
924pub struct BuildPlan {
925 pub request: BuildRequest,
926 pub host: HostInfo,
927 pub target: TargetInfo,
928 pub command: CommandLine,
929 pub steps: Vec<PlanStep>,
930 pub provider_actions: Vec<ProviderAction>,
931 pub cargo_config: Option<toml::Table>,
932 pub cache_key: String,
933}
934
935impl BuildPlan {
936 pub fn manifest_directory(&self) -> &Path {
937 self.request
938 .manifest_path
939 .parent()
940 .unwrap_or_else(|| Path::new("."))
941 }
942
943 pub fn target_triple(&self) -> &TargetTriple {
944 &self.request.target_triple
945 }
946
947 pub fn is_cross_compilation(&self) -> bool {
948 self.target.requires_cross
949 }
950}
951
952#[derive(Debug, Clone, PartialEq)]
954pub struct ExecutionReport {
955 pub plan: BuildPlan,
956 pub run: RunReport,
957}
958
959#[derive(Debug, Clone, PartialEq, Eq)]
961pub struct RunReport {
962 pub executed: bool,
963 pub command: String,
964 pub working_directory: PathBuf,
965 pub exit_code: Option<i32>,
966 pub duration_ms: u64,
967}
968
969#[derive(Debug, Clone, PartialEq, Eq)]
971pub struct ValidationPlan {
972 pub label: String,
973 pub target: TargetTriple,
974 pub test_command: Vec<String>,
975 pub expected_artifacts: Vec<PathBuf>,
976 pub requires_release: bool,
977 pub env: BTreeMap<String, String>,
978 pub timeout_secs: u64,
979}
980
981impl ValidationPlan {
982 pub fn new(label: impl Into<String>, target: TargetTriple) -> Self {
983 Self {
984 label: label.into(),
985 target,
986 test_command: vec!["cargo".into(), "test".into()],
987 expected_artifacts: Vec::new(),
988 requires_release: false,
989 env: BTreeMap::new(),
990 timeout_secs: 300,
991 }
992 }
993
994 pub fn with_test_command(mut self, cmd: Vec<String>) -> Self {
995 self.test_command = cmd;
996 self
997 }
998
999 pub fn with_artifacts(mut self, artifacts: Vec<PathBuf>) -> Self {
1000 self.expected_artifacts = artifacts;
1001 self
1002 }
1003
1004 pub fn with_release_mode(mut self, requires_release: bool) -> Self {
1005 self.requires_release = requires_release;
1006 self
1007 }
1008
1009 pub fn with_env(mut self, env: BTreeMap<String, String>) -> Self {
1010 self.env = env;
1011 self
1012 }
1013
1014 pub fn with_timeout(mut self, secs: u64) -> Self {
1015 self.timeout_secs = secs;
1016 self
1017 }
1018
1019 pub fn requires_release_mode(&self) -> bool {
1020 self.requires_release
1021 }
1022}
1023
1024pub struct StandardValidations;
1026
1027impl StandardValidations {
1028 pub fn library(target: TargetTriple) -> ValidationPlan {
1030 ValidationPlan::new("library", target)
1031 .with_test_command(vec!["cargo".into(), "test".into(), "--lib".into()])
1032 .with_artifacts(vec![
1033 PathBuf::from("libtest.rlib"),
1034 PathBuf::from("deps"),
1035 ])
1036 }
1037
1038 pub fn binary(target: TargetTriple) -> ValidationPlan {
1040 ValidationPlan::new("binary", target)
1041 .with_test_command(vec!["cargo".into(), "test".into(), "--bin".into(), "main".into()])
1042 .with_artifacts(vec![PathBuf::from("main")])
1043 }
1044
1045 pub fn mixed(target: TargetTriple) -> ValidationPlan {
1047 ValidationPlan::new("mixed", target)
1048 .with_test_command(vec!["cargo".into(), "test".into()])
1049 .with_artifacts(vec![
1050 PathBuf::from("libtest.rlib"),
1051 PathBuf::from("main"),
1052 ])
1053 }
1054
1055 pub fn no_std(target: TargetTriple) -> ValidationPlan {
1057 ValidationPlan::new("no_std", target)
1058 .with_test_command(vec!["cargo".into(), "build".into()])
1059 .with_artifacts(vec![PathBuf::from("libnostd.rlib")])
1060 .with_env({
1061 let mut env = BTreeMap::new();
1062 env.insert("RUSTFLAGS".to_string(), "--cfg=no_std".to_string());
1063 env
1064 })
1065 }
1066
1067 pub fn all(target: TargetTriple) -> Vec<ValidationPlan> {
1069 vec![
1070 Self::library(target.clone()),
1071 Self::binary(target.clone()),
1072 Self::mixed(target.clone()),
1073 ]
1074 }
1075}
1076
1077#[derive(Debug, Clone, PartialEq, Eq)]
1079pub struct WrapperPlan {
1080 pub wrapper: String,
1081 pub target: String,
1082}
1083
1084impl WrapperPlan {
1085 pub fn new(wrapper: impl Into<String>, target: impl Into<String>) -> Self {
1086 Self {
1087 wrapper: wrapper.into(),
1088 target: target.into(),
1089 }
1090 }
1091
1092 pub fn invocation(&self) -> String {
1093 format!("{} {}", self.wrapper, self.target)
1094 }
1095}
1096
1097#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1099pub struct CachePolicy {
1100 pub root: PathBuf,
1101 pub max_size_bytes: Option<u64>,
1102 pub max_age: Option<Duration>,
1103 pub compress: bool,
1104}
1105
1106impl Default for CachePolicy {
1107 fn default() -> Self {
1108 Self {
1109 root: PathBuf::from("target").join("crossbuild-cache"),
1110 max_size_bytes: Some(10 * 1024 * 1024 * 1024), max_age: Some(Duration::from_secs(30 * 24 * 60 * 60)), compress: true,
1113 }
1114 }
1115}
1116
1117impl CachePolicy {
1118 pub fn new(root: impl Into<PathBuf>) -> Self {
1119 Self {
1120 root: root.into(),
1121 ..Default::default()
1122 }
1123 }
1124
1125 pub fn with_max_size(mut self, bytes: u64) -> Self {
1126 self.max_size_bytes = Some(bytes);
1127 self
1128 }
1129
1130 pub fn with_max_age(mut self, age: Duration) -> Self {
1131 self.max_age = Some(age);
1132 self
1133 }
1134
1135 pub fn with_compression(mut self, compress: bool) -> Self {
1136 self.compress = compress;
1137 self
1138 }
1139
1140 pub fn absolute_root(&self, workspace_root: &Path) -> PathBuf {
1141 if self.root.is_absolute() {
1142 self.root.clone()
1143 } else {
1144 workspace_root.join(&self.root)
1145 }
1146 }
1147
1148 pub fn cache_key(&self, workspace_root: &Path, target: &TargetTriple) -> String {
1149 let workspace_label = workspace_root
1150 .to_string_lossy()
1151 .replace(['\\', '/', ':'], "_");
1152 format!("{}::{}", workspace_label, target.triple)
1153 }
1154
1155 pub fn download_dir(&self, workspace_root: &Path) -> PathBuf {
1156 self.absolute_root(workspace_root).join("downloads")
1157 }
1158
1159 pub fn sysroot_dir(&self, workspace_root: &Path) -> PathBuf {
1160 self.absolute_root(workspace_root).join("sysroots")
1161 }
1162
1163 pub fn toolchain_dir(&self, workspace_root: &Path) -> PathBuf {
1164 self.absolute_root(workspace_root).join("toolchains")
1165 }
1166
1167 pub fn build_dir(&self, workspace_root: &Path, target: &TargetTriple) -> PathBuf {
1168 self.absolute_root(workspace_root)
1169 .join("builds")
1170 .join(&target.triple)
1171 }
1172
1173 pub fn metadata_path(&self, workspace_root: &Path) -> PathBuf {
1174 self.absolute_root(workspace_root).join("metadata.json")
1175 }
1176}
1177
1178#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1180pub struct CacheMetadata {
1181 pub entries: BTreeMap<String, CacheEntry>,
1182 pub total_size_bytes: u64,
1183 pub last_cleanup: u64,
1184}
1185
1186impl Default for CacheMetadata {
1187 fn default() -> Self {
1188 Self {
1189 entries: BTreeMap::new(),
1190 total_size_bytes: 0,
1191 last_cleanup: SystemTime::now()
1192 .duration_since(UNIX_EPOCH)
1193 .unwrap_or_default()
1194 .as_secs(),
1195 }
1196 }
1197}
1198
1199#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1201pub struct CacheEntry {
1202 pub key: String,
1203 pub path: PathBuf,
1204 pub size_bytes: u64,
1205 pub created: u64,
1206 pub last_accessed: u64,
1207 pub access_count: u64,
1208 pub entry_type: CacheEntryType,
1209 pub metadata: BTreeMap<String, String>,
1210}
1211
1212#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
1213pub enum CacheEntryType {
1214 Download,
1215 Sysroot,
1216 Toolchain,
1217 BuildArtifact,
1218 Other,
1219}
1220
1221pub struct CacheManager {
1223 policy: CachePolicy,
1224 workspace_root: PathBuf,
1225 metadata: CacheMetadata,
1226}
1227
1228impl CacheManager {
1229 pub fn new(policy: CachePolicy, workspace_root: impl AsRef<Path>) -> Result<Self, CrossBuildError> {
1231 let workspace_root = workspace_root.as_ref().to_path_buf();
1232 let root = policy.absolute_root(&workspace_root);
1233 fs::create_dir_all(&root).map_err(|source| CrossBuildError::Io {
1234 path: Some(root),
1235 source,
1236 })?;
1237
1238 fs::create_dir_all(policy.download_dir(&workspace_root)).map_err(|source| CrossBuildError::Io {
1239 path: Some(policy.download_dir(&workspace_root)),
1240 source,
1241 })?;
1242 fs::create_dir_all(policy.sysroot_dir(&workspace_root)).map_err(|source| CrossBuildError::Io {
1243 path: Some(policy.sysroot_dir(&workspace_root)),
1244 source,
1245 })?;
1246 fs::create_dir_all(policy.toolchain_dir(&workspace_root)).map_err(|source| CrossBuildError::Io {
1247 path: Some(policy.toolchain_dir(&workspace_root)),
1248 source,
1249 })?;
1250
1251 let metadata_path = policy.metadata_path(&workspace_root);
1252 let metadata = if metadata_path.exists() {
1253 let content = fs::read_to_string(&metadata_path).map_err(|source| CrossBuildError::Io {
1254 path: Some(metadata_path),
1255 source,
1256 })?;
1257 serde_json::from_str(&content).unwrap_or_default()
1258 } else {
1259 CacheMetadata::default()
1260 };
1261
1262 Ok(Self {
1263 policy,
1264 workspace_root,
1265 metadata,
1266 })
1267 }
1268
1269 pub fn get_download(&self, url: &str, checksum: Option<&str>) -> Option<PathBuf> {
1271 let key = self.download_key(url, checksum);
1272 self.metadata.entries.get(&key).and_then(|entry| {
1273 if entry.path.exists() {
1274 Some(entry.path.clone())
1275 } else {
1276 None
1277 }
1278 })
1279 }
1280
1281 pub fn store_download(
1283 &mut self,
1284 url: &str,
1285 checksum: Option<&str>,
1286 source_path: &Path,
1287 ) -> Result<PathBuf, CrossBuildError> {
1288 let key = self.download_key(url, checksum);
1289 let dest = self.policy.download_dir(&self.workspace_root).join(&key);
1290
1291 fs::copy(source_path, &dest).map_err(|source| CrossBuildError::Io {
1292 path: Some(dest.clone()),
1293 source,
1294 })?;
1295
1296 let size = fs::metadata(&dest).map(|m| m.len()).unwrap_or(0);
1297 let now = current_timestamp();
1298
1299 self.metadata.entries.insert(
1300 key.clone(),
1301 CacheEntry {
1302 key: key.clone(),
1303 path: dest.clone(),
1304 size_bytes: size,
1305 created: now,
1306 last_accessed: now,
1307 access_count: 1,
1308 entry_type: CacheEntryType::Download,
1309 metadata: {
1310 let mut m = BTreeMap::new();
1311 m.insert("url".to_string(), url.to_string());
1312 if let Some(cs) = checksum {
1313 m.insert("checksum".to_string(), cs.to_string());
1314 }
1315 m
1316 },
1317 },
1318 );
1319 self.metadata.total_size_bytes += size;
1320 self.save_metadata()?;
1321
1322 Ok(dest)
1323 }
1324
1325 pub fn get_sysroot(&self, target: &TargetTriple, provider: &str) -> Option<PathBuf> {
1327 let key = self.sysroot_key(target, provider);
1328 self.metadata.entries.get(&key).and_then(|entry| {
1329 if entry.path.exists() {
1330 Some(entry.path.clone())
1331 } else {
1332 None
1333 }
1334 })
1335 }
1336
1337 pub fn store_sysroot(
1339 &mut self,
1340 target: &TargetTriple,
1341 provider: &str,
1342 source_path: &Path,
1343 ) -> Result<PathBuf, CrossBuildError> {
1344 let key = self.sysroot_key(target, provider);
1345 let dest = self.policy.sysroot_dir(&self.workspace_root).join(&key);
1346
1347 if source_path.is_dir() {
1348 copy_dir(source_path, &dest)?;
1349 } else {
1350 fs::copy(source_path, &dest).map_err(|source| CrossBuildError::Io {
1351 path: Some(dest.clone()),
1352 source,
1353 })?;
1354 }
1355
1356 let size = dir_size(&dest).unwrap_or(0);
1357 let now = current_timestamp();
1358
1359 self.metadata.entries.insert(
1360 key.clone(),
1361 CacheEntry {
1362 key: key.clone(),
1363 path: dest.clone(),
1364 size_bytes: size,
1365 created: now,
1366 last_accessed: now,
1367 access_count: 1,
1368 entry_type: CacheEntryType::Sysroot,
1369 metadata: {
1370 let mut m = BTreeMap::new();
1371 m.insert("target".to_string(), target.triple.clone());
1372 m.insert("provider".to_string(), provider.to_string());
1373 m
1374 },
1375 },
1376 );
1377 self.metadata.total_size_bytes += size;
1378 self.save_metadata()?;
1379
1380 Ok(dest)
1381 }
1382
1383 pub fn build_dir(&self, target: &TargetTriple) -> PathBuf {
1385 self.policy.build_dir(&self.workspace_root, target)
1386 }
1387
1388 pub fn cleanup(&mut self) -> Result<CleanupReport, CrossBuildError> {
1390 let mut report = CleanupReport::default();
1391 let now = current_timestamp();
1392
1393 if let Some(max_age) = self.policy.max_age {
1395 let cutoff = now - max_age.as_secs();
1396 let expired: Vec<_> = self.metadata.entries
1397 .iter()
1398 .filter(|(_, entry)| entry.last_accessed < cutoff)
1399 .map(|(k, _)| k.clone())
1400 .collect();
1401
1402 for key in expired {
1403 if let Some(entry) = self.metadata.entries.remove(&key) {
1404 if entry.path.exists() {
1405 remove_entry(&entry.path)?;
1406 }
1407 report.removed_entries += 1;
1408 report.freed_bytes += entry.size_bytes;
1409 self.metadata.total_size_bytes = self.metadata.total_size_bytes.saturating_sub(entry.size_bytes);
1410 }
1411 }
1412 }
1413
1414 if let Some(max_size) = self.policy.max_size_bytes {
1416 if self.metadata.total_size_bytes > max_size {
1417 let mut entries: Vec<_> = self.metadata.entries
1419 .iter()
1420 .map(|(k, v)| (k.clone(), v.last_accessed, v.size_bytes, v.path.clone()))
1421 .collect();
1422 entries.sort_by_key(|(_, last_accessed, _, _)| *last_accessed);
1423
1424 for (key, _, _size, path) in entries {
1425 if self.metadata.total_size_bytes <= max_size {
1426 break;
1427 }
1428 if let Some(entry) = self.metadata.entries.remove(&key) {
1429 if path.exists() {
1430 remove_entry(&path)?;
1431 }
1432 report.removed_entries += 1;
1433 report.freed_bytes += entry.size_bytes;
1434 self.metadata.total_size_bytes = self.metadata.total_size_bytes.saturating_sub(entry.size_bytes);
1435 }
1436 }
1437 }
1438 }
1439
1440 self.metadata.last_cleanup = now;
1441 self.save_metadata()?;
1442
1443 Ok(report)
1444 }
1445
1446 fn download_key(&self, url: &str, checksum: Option<&str>) -> String {
1447 use sha2::{Digest, Sha256};
1448 let mut hasher = Sha256::new();
1449 hasher.update(url.as_bytes());
1450 if let Some(cs) = checksum {
1451 hasher.update(cs.as_bytes());
1452 }
1453 hex::encode(hasher.finalize())[..16].to_string()
1454 }
1455
1456 fn sysroot_key(&self, target: &TargetTriple, provider: &str) -> String {
1457 use sha2::{Digest, Sha256};
1458 let mut hasher = Sha256::new();
1459 hasher.update(target.triple.as_bytes());
1460 hasher.update(provider.as_bytes());
1461 format!("sysroot-{}", &hex::encode(hasher.finalize())[..16])
1462 }
1463
1464 pub fn policy(&self) -> &CachePolicy {
1466 &self.policy
1467 }
1468
1469 pub fn stats(&self) -> CacheStats {
1471 let mut by_type = BTreeMap::new();
1472 for entry in self.metadata.entries.values() {
1473 *by_type.entry(entry.entry_type).or_insert(0) += 1;
1474 }
1475 CacheStats {
1476 total_entries: self.metadata.entries.len(),
1477 total_size_bytes: self.metadata.total_size_bytes,
1478 by_type,
1479 root: self.policy.absolute_root(&self.workspace_root),
1480 }
1481 }
1482
1483 fn save_metadata(&self) -> Result<(), CrossBuildError> {
1484 let path = self.policy.metadata_path(&self.workspace_root);
1485 let content = serde_json::to_string_pretty(&self.metadata)
1486 .map_err(|e| CrossBuildError::configuration(e.to_string()))?;
1487 fs::write(&path, content).map_err(|source| CrossBuildError::Io {
1488 path: Some(path),
1489 source,
1490 })
1491 }
1492}
1493
1494#[derive(Debug, Clone)]
1496pub struct CacheStats {
1497 pub total_entries: usize,
1498 pub total_size_bytes: u64,
1499 pub by_type: BTreeMap<CacheEntryType, usize>,
1500 pub root: PathBuf,
1501}
1502
1503#[derive(Debug, Default, Clone)]
1505pub struct CleanupReport {
1506 pub removed_entries: usize,
1507 pub freed_bytes: u64,
1508}
1509
1510fn current_timestamp() -> u64 {
1511 SystemTime::now()
1512 .duration_since(UNIX_EPOCH)
1513 .unwrap_or_default()
1514 .as_secs()
1515}
1516
1517fn copy_dir(src: &Path, dest: &Path) -> Result<(), CrossBuildError> {
1518 fs::create_dir_all(dest).map_err(|source| CrossBuildError::Io {
1519 path: Some(dest.to_path_buf()),
1520 source,
1521 })?;
1522
1523 for entry in fs::read_dir(src).map_err(|source| CrossBuildError::Io {
1524 path: Some(src.to_path_buf()),
1525 source,
1526 })? {
1527 let entry = entry.map_err(|source| CrossBuildError::Io {
1528 path: Some(src.to_path_buf()),
1529 source,
1530 })?;
1531 let src_path = entry.path();
1532 let dest_path = dest.join(entry.file_name());
1533
1534 if src_path.is_dir() {
1535 copy_dir(&src_path, &dest_path)?;
1536 } else {
1537 fs::copy(&src_path, &dest_path).map_err(|source| CrossBuildError::Io {
1538 path: Some(dest_path),
1539 source,
1540 })?;
1541 }
1542 }
1543 Ok(())
1544}
1545
1546fn dir_size(path: &Path) -> Result<u64, CrossBuildError> {
1547 let mut size = 0;
1548 for entry in fs::read_dir(path).map_err(|source| CrossBuildError::Io {
1549 path: Some(path.to_path_buf()),
1550 source,
1551 })? {
1552 let entry = entry.map_err(|source| CrossBuildError::Io {
1553 path: Some(path.to_path_buf()),
1554 source,
1555 })?;
1556 let path = entry.path();
1557 if path.is_dir() {
1558 size += dir_size(&path)?;
1559 } else {
1560 size += fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
1561 }
1562 }
1563 Ok(size)
1564}
1565
1566fn remove_entry(path: &Path) -> Result<(), CrossBuildError> {
1567 if path.is_dir() {
1568 fs::remove_dir_all(path).map_err(|source| CrossBuildError::Io {
1569 path: Some(path.to_path_buf()),
1570 source,
1571 })
1572 } else {
1573 fs::remove_file(path).map_err(|source| CrossBuildError::Io {
1574 path: Some(path.to_path_buf()),
1575 source,
1576 })
1577 }
1578}
1579
1580#[cfg(test)]
1581mod tests {
1582 use super::*;
1583 use crate::model::TargetTriple;
1584 use tempfile::tempdir;
1585
1586 #[test]
1587 fn cache_policy_default() {
1588 let policy = CachePolicy::default();
1589 assert_eq!(policy.root, PathBuf::from("target").join("crossbuild-cache"));
1590 assert_eq!(policy.max_size_bytes, Some(10 * 1024 * 1024 * 1024));
1591 }
1592
1593 #[test]
1594 fn cache_key_generation() {
1595 let policy = CachePolicy::default();
1596 let workspace = PathBuf::from("/home/user/project");
1597 let target = TargetTriple::parse("x86_64-unknown-linux-gnu").unwrap();
1598
1599 let key = policy.cache_key(&workspace, &target);
1600 assert!(key.contains("home_user_project"));
1601 assert!(key.contains("x86_64-unknown-linux-gnu"));
1602 }
1603
1604 #[test]
1605 fn cache_manager_creation() {
1606 let dir = tempdir().unwrap();
1607 let policy = CachePolicy::new(dir.path().join("cache"));
1608 let manager = CacheManager::new(policy, dir.path()).unwrap();
1609
1610 let stats = manager.stats();
1611 assert_eq!(stats.total_entries, 0);
1612 assert_eq!(stats.total_size_bytes, 0);
1613 }
1614
1615 #[test]
1616 fn download_caching() {
1617 let dir = tempdir().unwrap();
1618 let policy = CachePolicy::new(dir.path().join("cache"));
1619 let mut manager = CacheManager::new(policy, dir.path()).unwrap();
1620
1621 let source = dir.path().join("test-download");
1623 fs::write(&source, b"test content").unwrap();
1624
1625 let cached = manager.store_download(
1627 "https://example.com/file",
1628 Some("sha256:abc123"),
1629 &source,
1630 ).unwrap();
1631
1632 assert!(cached.exists());
1633
1634 let retrieved = manager.get_download("https://example.com/file", Some("sha256:abc123"));
1636 assert_eq!(retrieved, Some(cached));
1637
1638 let retrieved2 = manager.get_download("https://example.com/file", Some("sha256:different"));
1640 assert_eq!(retrieved2, None);
1641 }
1642
1643 #[test]
1644 fn sysroot_caching() {
1645 let dir = tempdir().unwrap();
1646 let policy = CachePolicy::new(dir.path().join("cache"));
1647 let mut manager = CacheManager::new(policy, dir.path()).unwrap();
1648
1649 let target = TargetTriple::parse("x86_64-unknown-linux-gnu").unwrap();
1650
1651 let sysroot = dir.path().join("sysroot");
1653 fs::create_dir_all(sysroot.join("lib")).unwrap();
1654 fs::write(sysroot.join("lib").join("libc.so"), b"fake").unwrap();
1655
1656 let cached = manager.store_sysroot(&target, "rustup", &sysroot).unwrap();
1658
1659 assert!(cached.exists());
1660 assert!(cached.join("lib").join("libc.so").exists());
1661
1662 let retrieved = manager.get_sysroot(&target, "rustup");
1664 assert_eq!(retrieved, Some(cached));
1665 }
1666
1667 #[test]
1668 fn cleanup_removes_old_entries() {
1669 let dir = tempdir().unwrap();
1670 let policy = CachePolicy::new(dir.path().join("cache"))
1671 .with_max_age(Duration::from_secs(60)); let mut manager = CacheManager::new(policy, dir.path()).unwrap();
1673
1674 let target = TargetTriple::parse("x86_64-unknown-linux-gnu").unwrap();
1675
1676 let sysroot = dir.path().join("sysroot");
1678 fs::create_dir_all(sysroot.join("lib")).unwrap();
1679 fs::write(sysroot.join("lib").join("libc.so"), b"fake").unwrap();
1680
1681 let cached = manager.store_sysroot(&target, "rustup", &sysroot).unwrap();
1682
1683 if let Some(entry) = manager.metadata.entries.get_mut(&manager.sysroot_key(&target, "rustup")) {
1685 entry.last_accessed = current_timestamp() - 120; }
1687 manager.save_metadata().unwrap();
1688
1689 let report = manager.cleanup().unwrap();
1691 assert_eq!(report.removed_entries, 1);
1692 assert!(!cached.exists());
1693 }
1694}