1use std::collections::BTreeMap;
4use std::path::PathBuf;
5
6use crate::{
7 model::{Abi, Architecture, BuildRequest, HostInfo, OperatingSystem, TargetFamily, TargetTriple, ToolchainHint},
8 error::CrossBuildError,
9};
10
11pub trait ToolchainProvider: Send + Sync {
13 fn name(&self) -> &'static str;
15
16 fn priority(&self) -> i32 {
18 0
19 }
20
21 fn can_provide(&self, target: &TargetTriple, host: &HostInfo) -> bool;
23
24 fn resolve(
27 &self,
28 target: &TargetTriple,
29 host: &HostInfo,
30 request: &BuildRequest,
31 ) -> Result<ToolchainResolution, CrossBuildError>;
32
33 fn hint(&self) -> ToolchainHint;
35}
36
37#[derive(Debug, Clone, PartialEq)]
39pub struct ToolchainResolution {
40 pub env: BTreeMap<String, String>,
41 pub cargo_config: Option<toml::Table>,
42 pub notes: Vec<String>,
43 pub rustc_path: Option<PathBuf>,
44 pub cargo_path: Option<PathBuf>,
45 pub target_spec: Option<String>,
46 pub rustflags: Vec<String>,
47}
48
49impl ToolchainResolution {
50 pub fn new() -> Self {
51 Self {
52 env: BTreeMap::new(),
53 cargo_config: None,
54 notes: Vec::new(),
55 rustc_path: None,
56 cargo_path: None,
57 target_spec: None,
58 rustflags: Vec::new(),
59 }
60 }
61
62 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
63 self.env.insert(key.into(), value.into());
64 self
65 }
66
67 pub fn with_cargo_config(mut self, config: toml::Table) -> Self {
68 self.cargo_config = Some(config);
69 self
70 }
71
72 pub fn with_note(mut self, note: impl Into<String>) -> Self {
73 self.notes.push(note.into());
74 self
75 }
76
77 pub fn with_rustc(mut self, path: PathBuf) -> Self {
78 self.rustc_path = Some(path);
79 self
80 }
81
82 pub fn with_cargo(mut self, path: PathBuf) -> Self {
83 self.cargo_path = Some(path);
84 self
85 }
86
87 pub fn with_target_spec(mut self, spec: String) -> Self {
88 self.target_spec = Some(spec);
89 self
90 }
91
92 pub fn with_rustflags(mut self, flags: Vec<String>) -> Self {
93 self.rustflags = flags;
94 self
95 }
96}
97
98impl Default for ToolchainResolution {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104pub trait SysrootProvider: Send + Sync {
106 fn name(&self) -> &'static str;
108
109 fn priority(&self) -> i32 {
111 0
112 }
113
114 fn can_provide(&self, target: &TargetTriple, host: &HostInfo) -> bool;
116
117 fn resolve(
119 &self,
120 target: &TargetTriple,
121 host: &HostInfo,
122 request: &BuildRequest,
123 ) -> Result<SysrootResolution, CrossBuildError>;
124}
125
126#[derive(Debug, Clone, PartialEq)]
128pub struct SysrootResolution {
129 pub sysroot_path: PathBuf,
130 pub env: BTreeMap<String, String>,
131 pub cargo_config: Option<toml::Table>,
132 pub notes: Vec<String>,
133 pub is_builtin: bool,
134}
135
136impl SysrootResolution {
137 pub fn new(sysroot_path: PathBuf) -> Self {
138 Self {
139 sysroot_path,
140 env: BTreeMap::new(),
141 cargo_config: None,
142 notes: Vec::new(),
143 is_builtin: false,
144 }
145 }
146
147 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
148 self.env.insert(key.into(), value.into());
149 self
150 }
151
152 pub fn with_cargo_config(mut self, config: toml::Table) -> Self {
153 self.cargo_config = Some(config);
154 self
155 }
156
157 pub fn with_note(mut self, note: impl Into<String>) -> Self {
158 self.notes.push(note.into());
159 self
160 }
161
162 pub fn with_builtin(mut self, builtin: bool) -> Self {
163 self.is_builtin = builtin;
164 self
165 }
166}
167
168pub trait LinkerProvider: Send + Sync {
170 fn name(&self) -> &'static str;
172
173 fn priority(&self) -> i32 {
175 0
176 }
177
178 fn can_provide(&self, target: &TargetTriple, host: &HostInfo) -> bool;
180
181 fn resolve(
183 &self,
184 target: &TargetTriple,
185 host: &HostInfo,
186 request: &BuildRequest,
187 ) -> Result<LinkerResolution, CrossBuildError>;
188}
189
190#[derive(Debug, Clone, PartialEq)]
192pub struct LinkerResolution {
193 pub linker_path: PathBuf,
194 pub linker_args: Vec<String>,
195 pub env: BTreeMap<String, String>,
196 pub cargo_config: Option<toml::Table>,
197 pub notes: Vec<String>,
198 pub flavor: LinkerFlavor,
199}
200
201impl LinkerResolution {
202 pub fn new(linker_path: PathBuf, flavor: LinkerFlavor) -> Self {
203 Self {
204 linker_path,
205 linker_args: Vec::new(),
206 env: BTreeMap::new(),
207 cargo_config: None,
208 notes: Vec::new(),
209 flavor,
210 }
211 }
212
213 pub fn with_args(mut self, args: Vec<String>) -> Self {
214 self.linker_args = args;
215 self
216 }
217
218 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
219 self.env.insert(key.into(), value.into());
220 self
221 }
222
223 pub fn with_cargo_config(mut self, config: toml::Table) -> Self {
224 self.cargo_config = Some(config);
225 self
226 }
227
228 pub fn with_note(mut self, note: impl Into<String>) -> Self {
229 self.notes.push(note.into());
230 self
231 }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum LinkerFlavor {
237 Gnu,
238 Msvc,
239 Lld,
240 Mold,
241 WasmLld,
242 Darwin,
243}
244
245impl LinkerFlavor {
246 pub fn cargo_name(&self) -> &str {
247 match self {
248 LinkerFlavor::Gnu => "gcc",
249 LinkerFlavor::Msvc => "msvc",
250 LinkerFlavor::Lld => "ld.lld",
251 LinkerFlavor::Mold => "mold",
252 LinkerFlavor::WasmLld => "wasm-ld",
253 LinkerFlavor::Darwin => "ld64",
254 }
255 }
256}
257
258#[derive(Debug, Clone, PartialEq)]
260pub struct ProviderAction {
261 pub provider_name: String,
262 pub notes: Vec<String>,
263 pub env: BTreeMap<String, String>,
264 pub cargo_config: Option<toml::Table>,
265}
266
267trait ZigTarget {
269 fn to_zig_target(&self) -> String;
270}
271
272impl ZigTarget for crate::model::TargetTriple {
273 fn to_zig_target(&self) -> String {
274 let arch = match self.arch {
275 Architecture::X86_64 => "x86_64",
276 Architecture::AArch64 => "aarch64",
277 Architecture::X86 => "x86",
278 Architecture::Arm => "arm",
279 Architecture::Arm64 => "aarch64",
280 Architecture::RiscV64 => "riscv64",
281 Architecture::PowerPC64 => "powerpc64le",
282 Architecture::S390x => "s390x",
283 Architecture::Mips64 => "mips64",
284 Architecture::LoongArch64 => "loongarch64",
285 Architecture::Wasm32 => "wasm32",
286 Architecture::Wasm64 => "wasm64",
287 Architecture::Other(ref s) => s,
288 };
289
290 let os = match self.os {
291 OperatingSystem::Linux => "linux",
292 OperatingSystem::Windows => "windows",
293 OperatingSystem::MacOs => "macos",
294 OperatingSystem::FreeBSD => "freebsd",
295 OperatingSystem::NetBSD => "netbsd",
296 OperatingSystem::OpenBSD => "openbsd",
297 OperatingSystem::DragonflyBSD => "dragonflybsd",
298 OperatingSystem::Solaris => "solaris",
299 OperatingSystem::Illumos => "illumos",
300 OperatingSystem::Android => "android",
301 OperatingSystem::Wasm => "wasi",
302 OperatingSystem::Wasi => "wasi",
303 OperatingSystem::None => "freestanding",
304 OperatingSystem::Uefi => "uefi",
305 OperatingSystem::Ios => "ios",
306 OperatingSystem::TvOS => "tvos",
307 OperatingSystem::WatchOS => "watchos",
308 OperatingSystem::Heron => "heron",
309 OperatingSystem::Zos => "zos",
310 OperatingSystem::Fuchsia => "fuchsia",
311 OperatingSystem::Redox => "redox",
312 OperatingSystem::Other(ref s) => s,
313 };
314
315 let abi = match self.abi {
316 Abi::Gnu => "gnu",
317 Abi::Musl => "musl",
318 Abi::Msvc => "msvc",
319 Abi::Android => "android",
320 Abi::Wasm32 => "wasi",
321 Abi::None => "",
322 Abi::Eabi => "eabi",
323 Abi::Eabihf => "eabihf",
324 Abi::Simulator => "simulator",
325 Abi::Uwp => "uwp",
326 Abi::Wasm64 => "wasi",
327 };
328
329 if abi.is_empty() {
330 format!("{}-{}", arch, os)
331 } else {
332 format!("{}-{}-{}", arch, os, abi)
333 }
334 }
335}
336
337pub struct RustupToolchainProvider;
339
340impl ToolchainProvider for RustupToolchainProvider {
341 fn name(&self) -> &'static str {
342 "rustup"
343 }
344
345 fn priority(&self) -> i32 {
346 100
347 }
348
349 fn can_provide(&self, target: &TargetTriple, host: &HostInfo) -> bool {
350 if target.triple == host.host_triple.triple {
352 return true;
353 }
354 crate::platform::rustup_target_available(target)
355 }
356
357 fn resolve(
358 &self,
359 target: &TargetTriple,
360 host: &HostInfo,
361 _request: &BuildRequest,
362 ) -> Result<ToolchainResolution, CrossBuildError> {
363 let mut resolution = ToolchainResolution::new();
364
365 if target.triple == host.host_triple.triple {
366 resolution = resolution
367 .with_note("Using host toolchain for native build")
368 .with_rustc(which::which("rustc")?)
369 .with_cargo(which::which("cargo")?);
370 } else {
371 let rustup_home = std::env::var("RUSTUP_HOME")
373 .map(PathBuf::from)
374 .or_else(|_| {
375 std::env::var("HOME")
376 .or_else(|_| std::env::var("USERPROFILE"))
377 .map(|h| PathBuf::from(h).join(".rustup"))
378 })
379 .unwrap_or_else(|_| PathBuf::from("/rustup"));
380
381 let toolchain = find_rustup_toolchain(&rustup_home.to_string_lossy())?;
382 let toolchain_path = rustup_home.join("toolchains").join(&toolchain);
383
384 let rustc_path = toolchain_path.join("bin").join("rustc");
385 let cargo_path = toolchain_path.join("bin").join("cargo");
386
387 resolution = resolution
388 .with_note(format!("Using rustup toolchain: {toolchain}"))
389 .with_rustc(rustc_path)
390 .with_cargo(cargo_path)
391 .with_env("RUSTUP_TOOLCHAIN", toolchain);
392 }
393
394 if target.triple != host.host_triple.triple {
396 resolution = resolution
397 .with_target_spec(target.triple.clone())
398 .with_env("CARGO_BUILD_TARGET", target.triple.clone());
399 }
400
401 Ok(resolution)
402 }
403
404 fn hint(&self) -> ToolchainHint {
405 ToolchainHint::Rustup
406 }
407}
408
409pub struct ZigToolchainProvider;
411
412impl ToolchainProvider for ZigToolchainProvider {
413 fn name(&self) -> &'static str {
414 "zig"
415 }
416
417 fn priority(&self) -> i32 {
418 50
419 }
420
421 fn can_provide(&self, target: &TargetTriple, _host: &HostInfo) -> bool {
422 !matches!(target.family(), TargetFamily::Other | TargetFamily::BareMetal)
424 || target.is_wasm()
425 }
426
427 fn resolve(
428 &self,
429 target: &TargetTriple,
430 _host: &HostInfo,
431 _request: &BuildRequest,
432 ) -> Result<ToolchainResolution, CrossBuildError> {
433 let zig_path = which::which("zig").map_err(|_| CrossBuildError::ToolNotFound {
434 tool: "zig".to_string(),
435 })?;
436
437 let target_arg = target.to_zig_target();
438
439 let mut resolution = ToolchainResolution::new()
440 .with_note(format!("Using zig cc for target: {target_arg}"))
441 .with_rustc(zig_path.clone())
442 .with_cargo(which::which("cargo")?)
443 .with_env("CC", format!("zig cc -target {}", target_arg))
444 .with_env("CXX", format!("zig c++ -target {}", target_arg))
445 .with_env("AR", "zig ar")
446 .with_env("CARGO_TARGET_RUNNER", format!("zig cc -target {}", target_arg));
447
448 resolution = resolution.with_env(
450 "CARGO_TARGET_RUSTFLAGS",
451 format!("-C linker=zig cc -target {}", target_arg),
452 );
453
454 Ok(resolution)
455 }
456
457 fn hint(&self) -> ToolchainHint {
458 ToolchainHint::Zig
459 }
460}
461
462pub struct BuiltinToolchainProvider;
464
465impl ToolchainProvider for BuiltinToolchainProvider {
466 fn name(&self) -> &'static str {
467 "builtin"
468 }
469
470 fn priority(&self) -> i32 {
471 200
472 }
473
474 fn can_provide(&self, target: &TargetTriple, host: &HostInfo) -> bool {
475 target.triple == host.host_triple.triple
476 }
477
478 fn resolve(
479 &self,
480 _target: &TargetTriple,
481 _host: &HostInfo,
482 _request: &BuildRequest,
483 ) -> Result<ToolchainResolution, CrossBuildError> {
484 Ok(ToolchainResolution::new()
485 .with_note("Using host toolchain")
486 .with_rustc(which::which("rustc")?)
487 .with_cargo(which::which("cargo")?))
488 }
489
490 fn hint(&self) -> ToolchainHint {
491 ToolchainHint::Rustup
492 }
493}
494
495pub struct RustupSysrootProvider;
497
498impl SysrootProvider for RustupSysrootProvider {
499 fn name(&self) -> &'static str {
500 "rustup"
501 }
502
503 fn priority(&self) -> i32 {
504 100
505 }
506
507 fn can_provide(&self, target: &TargetTriple, host: &HostInfo) -> bool {
508 if target.triple == host.host_triple.triple {
509 return false; }
511 crate::platform::rustup_target_available(target)
512 }
513
514 fn resolve(
515 &self,
516 target: &TargetTriple,
517 host: &HostInfo,
518 _request: &BuildRequest,
519 ) -> Result<SysrootResolution, CrossBuildError> {
520 if target.triple == host.host_triple.triple {
521 return Err(CrossBuildError::SysrootNotNeeded);
522 }
523
524 let rustup_home = std::env::var("RUSTUP_HOME")
525 .map(PathBuf::from)
526 .or_else(|_| {
527 std::env::var("HOME")
528 .or_else(|_| std::env::var("USERPROFILE"))
529 .map(|h| PathBuf::from(h).join(".rustup"))
530 })
531 .unwrap_or_else(|_| PathBuf::from("/rustup"));
532
533 let toolchain = find_rustup_toolchain(&rustup_home.to_string_lossy())?;
534 let sysroot = rustup_home
535 .join("toolchains")
536 .join(&toolchain)
537 .join("lib")
538 .join("rustlib")
539 .join(&target.triple);
540
541 if !sysroot.exists() {
542 return Err(CrossBuildError::SysrootNotFound {
543 target: target.triple.clone(),
544 });
545 }
546
547 let mut resolution = SysrootResolution::new(sysroot.clone())
548 .with_note(format!("Using rustup sysroot from toolchain: {toolchain}"))
549 .with_env("CARGO_SYSROOT", sysroot.to_string_lossy())
550 .with_builtin(true);
551
552 let lib_dir = sysroot.join("lib");
554 if lib_dir.exists() {
555 resolution = resolution.with_env("LIBRARY_PATH", lib_dir.to_string_lossy());
556 }
557
558 Ok(resolution)
559 }
560}
561
562pub struct ZigSysrootProvider;
564
565impl SysrootProvider for ZigSysrootProvider {
566 fn name(&self) -> &'static str {
567 "zig"
568 }
569
570 fn priority(&self) -> i32 {
571 50
572 }
573
574 fn can_provide(&self, target: &TargetTriple, _host: &HostInfo) -> bool {
575 !matches!(target.os, OperatingSystem::None)
577 }
578
579 fn resolve(
580 &self,
581 target: &TargetTriple,
582 _host: &HostInfo,
583 _request: &BuildRequest,
584 ) -> Result<SysrootResolution, CrossBuildError> {
585 let _ = which::which("zig").map_err(|_| CrossBuildError::ToolNotFound {
586 tool: "zig".to_string(),
587 })?;
588
589 let sysroot = std::env::temp_dir().join("zig-sysroot").join(&target.triple);
591
592 let resolution = SysrootResolution::new(sysroot)
593 .with_note("Using zig's built-in libc/sysroot")
594 .with_env("ZIG_SYSROOT", "1");
595
596 Ok(resolution)
597 }
598}
599
600pub struct NoSysrootProvider;
602
603impl SysrootProvider for NoSysrootProvider {
604 fn name(&self) -> &'static str {
605 "none"
606 }
607
608 fn priority(&self) -> i32 {
609 200
610 }
611
612 fn can_provide(&self, target: &TargetTriple, host: &HostInfo) -> bool {
613 target.triple == host.host_triple.triple
614 || target.is_wasm()
615 || target.is_bare_metal()
616 }
617
618 fn resolve(
619 &self,
620 target: &TargetTriple,
621 host: &HostInfo,
622 _request: &BuildRequest,
623 ) -> Result<SysrootResolution, CrossBuildError> {
624 if target.triple == host.host_triple.triple {
625 return Err(CrossBuildError::SysrootNotNeeded);
626 }
627
628 Ok(SysrootResolution::new(PathBuf::new())
629 .with_note("No sysroot required for this target"))
630 }
631}
632
633#[allow(dead_code)]
634fn is_rustup_target_available(target: &TargetTriple) -> bool {
636 const RUSTUP_TARGETS: &[&str] = &[
638 "x86_64-unknown-linux-gnu",
639 "x86_64-unknown-linux-musl",
640 "aarch64-unknown-linux-gnu",
641 "aarch64-unknown-linux-musl",
642 "x86_64-pc-windows-msvc",
643 "x86_64-pc-windows-gnu",
644 "aarch64-pc-windows-msvc",
645 "i686-pc-windows-msvc",
646 "i686-pc-windows-gnu",
647 "x86_64-apple-darwin",
648 "aarch64-apple-darwin",
649 "wasm32-wasi",
650 "wasm32-unknown-unknown",
651 "wasm32-unknown-emscripten",
652 "x86_64-unknown-freebsd",
653 "aarch64-unknown-freebsd",
654 "powerpc64le-unknown-linux-gnu",
655 "s390x-unknown-linux-gnu",
656 "riscv64gc-unknown-linux-gnu",
657 ];
658
659 RUSTUP_TARGETS.contains(&target.triple.as_str())
660}
661
662fn find_rustup_toolchain(rustup_home: &str) -> Result<String, CrossBuildError> {
664 let toolchains_dir = PathBuf::from(rustup_home).join("toolchains");
665 if !toolchains_dir.exists() {
666 return Err(CrossBuildError::SysrootNotFound {
667 target: "rustup".to_string(),
668 });
669 }
670
671 let default_file = PathBuf::from(rustup_home).join("settings").join("default-toolchain");
673 if default_file.exists() {
674 let content = std::fs::read_to_string(&default_file)
675 .map_err(|_| CrossBuildError::SysrootNotFound {
676 target: "rustup".to_string(),
677 })?;
678 let toolchain = content.trim().to_string();
679 if toolchains_dir.join(&toolchain).exists() {
680 return Ok(toolchain);
681 }
682 }
683
684 for entry in std::fs::read_dir(&toolchains_dir).map_err(|_| CrossBuildError::SysrootNotFound {
686 target: "rustup".to_string(),
687 })? {
688 let entry = entry.map_err(|_| CrossBuildError::SysrootNotFound {
689 target: "rustup".to_string(),
690 })?;
691 let name = entry.file_name().to_string_lossy().to_string();
692 if name.contains("stable") || name.contains("1.") {
693 return Ok(name);
694 }
695 }
696
697 Err(CrossBuildError::SysrootNotFound {
698 target: "rustup".to_string(),
699 })
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705 use crate::model::TargetTriple;
706
707 #[test]
708 fn zig_target_conversion() {
709 let targets = [
710 ("x86_64-unknown-linux-gnu", "x86_64-linux-gnu"),
711 ("aarch64-unknown-linux-musl", "aarch64-linux-musl"),
712 ("x86_64-pc-windows-msvc", "x86_64-windows-msvc"),
713 ("wasm32-wasi", "wasm32-wasi"),
714 ];
715
716 for (input, expected) in targets {
717 let target = TargetTriple::parse(input).unwrap();
718 assert_eq!(target.to_zig_target(), expected);
719 }
720 }
721
722 #[test]
723 fn rustup_provider_native() {
724 let provider = RustupToolchainProvider;
725 let host = crate::model::HostInfo::detect().unwrap();
726 let target = TargetTriple::parse(&host.host_triple.triple).unwrap();
727 assert!(provider.can_provide(&target, &host));
728 }
729}