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#[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 let complete_resolution = self.registry.resolve_all(&target_triple, &host, &request)?;
54
55 let cache_manager = CacheManager::new(self.cache_policy.clone(), &workspace_root)?;
57
58 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 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 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 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 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 for arg in &request.cargo_args {
139 command.push_arg(arg.clone());
140 }
141
142 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 let cache_key = cache_manager.policy().cache_key(&workspace_root, &target_triple);
159
160 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
204fn build_provider_actions(
206 resolution: &crate::registry::CompleteResolution,
207 _target: &TargetTriple,
208) -> Vec<ProviderAction> {
209 let mut actions = Vec::new();
210
211 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 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 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 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}