Skip to main content

crossbuild_core/
planner.rs

1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use crate::{
5    cache::{CacheManager, CachePolicy},
6    cargo_config::CargoConfigGenerator,
7    config::CrossBuildConfig,
8    error::CrossBuildError,
9    model::{
10        BuildPlan, BuildRequest, CommandLine,
11        PlanStep, Profile, ProviderAction, TargetTriple,
12    },
13    platform::{assess_target, detect_host},
14    registry::ProviderRegistry,
15};
16
17/// Resolves requests into executable build plans.
18#[derive(Debug)]
19pub struct Planner {
20    pub(crate) registry: ProviderRegistry,
21    cache_policy: CachePolicy,
22}
23
24impl Planner {
25    pub fn new() -> Self {
26        Self {
27            registry: ProviderRegistry::new(),
28            cache_policy: CachePolicy::default(),
29        }
30    }
31
32    pub fn with_cache_policy(mut self, policy: CachePolicy) -> Self {
33        self.cache_policy = policy;
34        self
35    }
36
37    pub fn plan(
38        &self,
39        request: BuildRequest,
40        config: &CrossBuildConfig,
41    ) -> Result<BuildPlan, CrossBuildError> {
42        let target_triple = request.target_triple.clone();
43        let manifest_path = normalize_manifest_path(&request.manifest_path)?;
44        let host = detect_host()?;
45        let target = assess_target(&target_triple, &host);
46
47        let workspace_root = manifest_path
48            .parent()
49            .map(Path::to_path_buf)
50            .unwrap_or_else(|| PathBuf::from("."));
51
52        // Resolve all providers
53        let complete_resolution = self.registry.resolve_all(&target_triple, &host, &request)?;
54
55        // Create cache manager
56        let cache_manager = CacheManager::new(self.cache_policy.clone(), &workspace_root)?;
57
58        // Generate cargo config
59        let cargo_config = CargoConfigGenerator::new(target_triple.clone(), workspace_root.clone())
60            .with_linker(
61                complete_resolution.linker.linker_path.clone(),
62                complete_resolution.linker.flavor.cargo_name(),
63                complete_resolution.linker.linker_args.clone(),
64            )
65            .with_build_target(&target_triple);
66
67        let cargo_config = if let Some(sysroot) = &complete_resolution.sysroot {
68            if !sysroot.sysroot_path.as_os_str().is_empty() {
69                cargo_config.with_sysroot(sysroot.sysroot_path.clone())
70            } else {
71                cargo_config
72            }
73        } else {
74            cargo_config
75        };
76
77        let cargo_config = if !complete_resolution.toolchain.rustflags.is_empty() {
78            cargo_config.with_rustflags(complete_resolution.toolchain.rustflags.clone())
79        } else {
80            cargo_config
81        };
82
83        let cargo_config_table = cargo_config.build();
84
85        // Merge environment from all providers
86        let mut env = BTreeMap::new();
87        env.insert(
88            "CARGO_TARGET_DIR".to_string(),
89            config
90                .target_dir_for(&workspace_root)
91                .to_string_lossy()
92                .into_owned(),
93        );
94        env.extend(config.extra_env.clone());
95        env.extend(complete_resolution.merged_env());
96
97        // Build command
98        let mut command = CommandLine::new(config.cargo_program_str(), &workspace_root);
99        command.env = env;
100        command.push_arg("build");
101        command.push_arg("--manifest-path");
102        command.push_arg(manifest_path.to_string_lossy().into_owned());
103        command.push_arg("--target");
104        command.push_arg(target_triple.as_str());
105
106        // Add profile flag
107        match request.profile {
108            Profile::Release => {
109                command.push_arg("--release");
110            }
111            Profile::Dev => {}
112            Profile::Custom(name) => {
113                command.push_arg("--profile");
114                command.push_arg(name);
115            }
116        }
117
118        // Add features
119        if !request.features.is_empty() {
120            command.push_arg("--features");
121            command.push_arg(request.features.join(","));
122        }
123
124        if request.no_default_features {
125            command.push_arg("--no-default-features");
126        }
127
128        if request.workspace {
129            command.push_arg("--workspace");
130        }
131
132        for exclude in &request.exclude {
133            command.push_arg("--exclude");
134            command.push_arg(exclude);
135        }
136
137        // Add cargo args
138        for arg in &request.cargo_args {
139            command.push_arg(arg.clone());
140        }
141
142        // Build steps
143        let steps = vec![
144            PlanStep::ValidateManifest { path: manifest_path.clone() },
145            PlanStep::ValidateTarget { target: target_triple.clone() },
146            PlanStep::DetectHost,
147            PlanStep::ResolveProviders,
148            PlanStep::PrepareEnvironment,
149            PlanStep::GenerateCargoConfig,
150            PlanStep::ResolveLinker,
151            PlanStep::PrepareCache,
152            PlanStep::InvokeCargo,
153            PlanStep::CaptureDiagnostics,
154            PlanStep::VerifyArtifacts,
155        ];
156
157        // Generate cache key
158        let cache_key = cache_manager.policy().cache_key(&workspace_root, &target_triple);
159
160        // Build provider actions from the complete resolution
161        let provider_actions = build_provider_actions(&complete_resolution, &target_triple);
162
163        Ok(BuildPlan {
164            request: BuildRequest {
165                manifest_path,
166                ..request
167            },
168            host,
169            target,
170            command,
171            steps,
172            provider_actions,
173            cargo_config: Some(cargo_config_table),
174            cache_key,
175        })
176    }
177}
178
179fn normalize_manifest_path(path: &Path) -> Result<PathBuf, CrossBuildError> {
180    let metadata = std::fs::metadata(path).map_err(|source| CrossBuildError::Io {
181        path: Some(path.to_path_buf()),
182        source,
183    })?;
184
185    if metadata.is_dir() {
186        let candidate = path.join("Cargo.toml");
187        if candidate.is_file() {
188            return Ok(candidate);
189        }
190        return Err(CrossBuildError::ManifestNotFound {
191            searched_from: path.to_path_buf(),
192        });
193    }
194
195    if path.file_name().and_then(|name| name.to_str()) != Some("Cargo.toml") {
196        return Err(CrossBuildError::ManifestNotCargoToml {
197            path: path.to_path_buf(),
198        });
199    }
200
201    Ok(path.to_path_buf())
202}
203
204/// Builds provider action entries from the complete resolution.
205fn build_provider_actions(
206    resolution: &crate::registry::CompleteResolution,
207    _target: &TargetTriple,
208) -> Vec<ProviderAction> {
209    let mut actions = Vec::new();
210
211    // Toolchain action
212    actions.push(ProviderAction {
213        provider_name: "toolchain".to_string(),
214        notes: resolution.toolchain.notes.clone(),
215        env: resolution.toolchain.env.clone(),
216        cargo_config: resolution.toolchain.cargo_config.clone(),
217    });
218
219    // Sysroot action (if present)
220    if let Some(ref sysroot) = resolution.sysroot {
221        actions.push(ProviderAction {
222            provider_name: "sysroot".to_string(),
223            notes: sysroot.notes.clone(),
224            env: sysroot.env.clone(),
225            cargo_config: sysroot.cargo_config.clone(),
226        });
227    }
228
229    // Linker action
230    actions.push(ProviderAction {
231        provider_name: "linker".to_string(),
232        notes: resolution.linker.notes.clone(),
233        env: resolution.linker.env.clone(),
234        cargo_config: resolution.linker.cargo_config.clone(),
235    });
236
237    actions
238}
239
240impl Default for Planner {
241    fn default() -> Self {
242        Self::new()
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use crate::model::{BuildRequest, TargetTriple};
250    use crate::provider::{
251        BuiltinToolchainProvider, RustupToolchainProvider, ZigToolchainProvider,
252        NoSysrootProvider,
253    };
254    use tempfile::tempdir;
255
256    /// Test-only linker provider that always resolves.
257    struct TestLinkerProvider;
258
259    impl crate::provider::LinkerProvider for TestLinkerProvider {
260        fn name(&self) -> &'static str { "test-linker" }
261        fn priority(&self) -> i32 { 0 }
262        fn can_provide(&self, _target: &TargetTriple, _host: &crate::model::HostInfo) -> bool { true }
263        fn resolve(
264            &self,
265            _target: &TargetTriple,
266            _host: &crate::model::HostInfo,
267            _request: &BuildRequest,
268        ) -> Result<crate::provider::LinkerResolution, CrossBuildError> {
269            Ok(crate::provider::LinkerResolution::new(
270                std::path::PathBuf::from("cc"),
271                crate::provider::LinkerFlavor::Gnu,
272            ))
273        }
274    }
275
276    #[test]
277    fn planner_creation() {
278        let planner = Planner::new();
279        assert!(format!("{:?}", planner).contains("Planner"));
280    }
281
282    #[test]
283    fn planner_resolves_native_target() {
284        let mut planner = Planner::new();
285        planner.registry.register_toolchain(Box::new(BuiltinToolchainProvider));
286        planner.registry.register_toolchain(Box::new(RustupToolchainProvider));
287        planner.registry.register_toolchain(Box::new(ZigToolchainProvider));
288        planner.registry.register_sysroot(Box::new(NoSysrootProvider));
289        planner.registry.register_linker(Box::new(TestLinkerProvider));
290
291        let dir = tempdir().unwrap();
292        let manifest = dir.path().join("Cargo.toml");
293        std::fs::write(&manifest, r#"
294[package]
295name = "test"
296version = "0.1.0"
297edition = "2021"
298"#).unwrap();
299
300        let host = crate::platform::detect_host().unwrap();
301        let target = TargetTriple::parse(&host.host_triple.triple).unwrap();
302        let request = BuildRequest::new(manifest, target);
303        let config = CrossBuildConfig::default();
304
305        let plan = planner.plan(request, &config).unwrap();
306        assert!(!plan.is_cross_compilation());
307    }
308}