Skip to main content

crossbuild_core/
provider.rs

1//! Provider implementations for toolchain, sysroot, and linker resolution.
2
3use 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
11/// A toolchain provider supplies the compiler toolchain for a target.
12pub trait ToolchainProvider: Send + Sync {
13    /// Returns the unique name of this provider.
14    fn name(&self) -> &'static str;
15
16    /// Returns the priority of this provider (higher = preferred).
17    fn priority(&self) -> i32 {
18        0
19    }
20
21    /// Checks if this provider can handle the given target on this host.
22    fn can_provide(&self, target: &TargetTriple, host: &HostInfo) -> bool;
23
24    /// Resolves the toolchain for the target, returning environment variables
25    /// and configuration needed to use it.
26    fn resolve(
27        &self,
28        target: &TargetTriple,
29        host: &HostInfo,
30        request: &BuildRequest,
31    ) -> Result<ToolchainResolution, CrossBuildError>;
32
33    /// Returns the toolchain hint this provider satisfies.
34    fn hint(&self) -> ToolchainHint;
35}
36
37/// Resolution result from a toolchain provider.
38#[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
104/// A sysroot provider supplies the target sysroot (libc, libstd, crt objects).
105pub trait SysrootProvider: Send + Sync {
106    /// Returns the unique name of this provider.
107    fn name(&self) -> &'static str;
108
109    /// Returns the priority of this provider (higher = preferred).
110    fn priority(&self) -> i32 {
111        0
112    }
113
114    /// Checks if this provider can handle the given target on this host.
115    fn can_provide(&self, target: &TargetTriple, host: &HostInfo) -> bool;
116
117    /// Resolves the sysroot for the target.
118    fn resolve(
119        &self,
120        target: &TargetTriple,
121        host: &HostInfo,
122        request: &BuildRequest,
123    ) -> Result<SysrootResolution, CrossBuildError>;
124}
125
126/// Resolution result from a sysroot provider.
127#[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
168/// A linker provider supplies the appropriate linker for a target.
169pub trait LinkerProvider: Send + Sync {
170    /// Returns the unique name of this provider.
171    fn name(&self) -> &'static str;
172
173    /// Returns the priority of this provider (higher = preferred).
174    fn priority(&self) -> i32 {
175        0
176    }
177
178    /// Checks if this provider can handle the given target on this host.
179    fn can_provide(&self, target: &TargetTriple, host: &HostInfo) -> bool;
180
181    /// Resolves the linker for the target.
182    fn resolve(
183        &self,
184        target: &TargetTriple,
185        host: &HostInfo,
186        request: &BuildRequest,
187    ) -> Result<LinkerResolution, CrossBuildError>;
188}
189
190/// Resolution result from a linker provider.
191#[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/// Linker flavor for cargo configuration.
235#[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/// Provider action returned to the build plan.
259#[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
267/// Trait for targets that can convert to zig target string.
268trait 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
337/// Rustup-based toolchain provider.
338pub 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        // Can provide if target is available via rustup
351        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            // Cross-compilation with rustup target
372            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        // Add target specification if not native
395        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
409/// Zig-based toolchain provider.
410pub 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        // Zig can target most platforms
423        !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        // Add linker flags for zig
449        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
462/// Built-in toolchain provider (host native only).
463pub 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
495/// Rustup-based sysroot provider.
496pub 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; // Native builds don't need sysroot
510        }
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        // Add linker search paths
553        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
562/// Zig-based sysroot provider.
563pub 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        // Zig provides sysroots for many targets
576        !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        // Zig doesn't have a separate sysroot - it uses its internal libc
590        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
600/// No sysroot needed (wasm, bare metal).
601pub 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)]
634/// Checks if a target is available via rustup.
635fn is_rustup_target_available(target: &TargetTriple) -> bool {
636    // Check common rustup targets
637    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
662/// Finds the default rustup toolchain.
663fn 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    // Read the default toolchain
672    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    // Fallback: find first stable toolchain
685    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}