1use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5
6use crate::model::TargetTriple;
7use crate::error::CrossBuildError;
8
9pub struct CargoConfigGenerator {
11 config: toml::Table,
12 target: TargetTriple,
13 workspace_root: PathBuf,
14}
15
16impl CargoConfigGenerator {
17 pub fn new(target: TargetTriple, workspace_root: PathBuf) -> Self {
19 Self {
20 config: toml::Table::new(),
21 target,
22 workspace_root,
23 }
24 }
25
26 pub fn with_linker(mut self, linker_path: impl Into<PathBuf>, flavor: &str, args: Vec<String>) -> Self {
28 let target_key = format!("target.{}", self.target.as_str());
29 let target_table = self.get_or_create_target_table(&target_key);
30
31 target_table.insert("linker".to_string(), toml::Value::String(linker_path.into().to_string_lossy().into_owned()));
32 target_table.insert("linker-flavor".to_string(), toml::Value::String(flavor.to_string()));
33
34 if !args.is_empty() {
35 target_table.insert("linker-args".to_string(), toml::Value::Array(
36 args.into_iter().map(toml::Value::String).collect()
37 ));
38 }
39
40 self
41 }
42
43 pub fn with_runner(mut self, runner: impl Into<String>) -> Self {
45 let target_key = format!("target.{}", self.target.as_str());
46 let target_table = self.get_or_create_target_table(&target_key);
47 target_table.insert("runner".to_string(), toml::Value::String(runner.into()));
48 self
49 }
50
51 pub fn with_rustflags(mut self, flags: Vec<String>) -> Self {
53 let target_key = format!("target.{}", self.target.as_str());
54 let target_table = self.get_or_create_target_table(&target_key);
55 target_table.insert("rustflags".to_string(), toml::Value::Array(
56 flags.into_iter().map(toml::Value::String).collect()
57 ));
58 self
59 }
60
61 pub fn with_env(mut self, env: BTreeMap<String, String>) -> Self {
63 if env.is_empty() {
64 return self;
65 }
66
67 let target_key = format!("target.{}", self.target.as_str());
68 let target_table = self.get_or_create_target_table(&target_key);
69 let mut env_table = toml::Table::new();
70 for (k, v) in env {
71 env_table.insert(k, toml::Value::String(v));
72 }
73 target_table.insert("env".to_string(), toml::Value::Table(env_table));
74 self
75 }
76
77 pub fn with_sysroot(mut self, sysroot: impl Into<PathBuf>) -> Self {
79 let target_key = format!("target.{}", self.target.as_str());
80 let target_table = self.get_or_create_target_table(&target_key);
81 target_table.insert("sysroot".to_string(), toml::Value::String(sysroot.into().to_string_lossy().into_owned()));
82 self
83 }
84
85 pub fn with_custom_section(mut self, key: String, value: toml::Value) -> Self {
87 let target_key = format!("target.{}", self.target.as_str());
88 let target_table = self.get_or_create_target_table(&target_key);
89 target_table.insert(key, value);
90 self
91 }
92
93 pub fn with_build_target(mut self, target: &crate::model::TargetTriple) -> Self {
95 let mut build_table = toml::Table::new();
96 build_table.insert("target".to_string(), toml::Value::String(target.as_str().to_string()));
97 self.config.insert("build".to_string(), toml::Value::Table(build_table));
98 self
99 }
100
101 fn get_or_create_target_table(&mut self, key: &str) -> &mut toml::Table {
103 self.config
104 .entry(key.to_string())
105 .or_insert_with(|| toml::Value::Table(toml::Table::new()))
106 .as_table_mut()
107 .expect("target entry should be a table")
108 }
109
110 pub fn build(self) -> toml::Table {
112 self.config
113 }
114
115 pub fn write(self) -> Result<PathBuf, CrossBuildError> {
117 let cargo_dir = self.workspace_root.join(".cargo");
118 std::fs::create_dir_all(&cargo_dir).map_err(|e| CrossBuildError::Io {
119 path: Some(cargo_dir.clone()),
120 source: e,
121 })?;
122
123 let config_path = cargo_dir.join("config.toml");
124 let config_str = toml::to_string_pretty(&self.config)
125 .map_err(|e| CrossBuildError::configuration(e.to_string()))?;
126
127 std::fs::write(&config_path, config_str).map_err(|e| CrossBuildError::Io {
128 path: Some(config_path.clone()),
129 source: e,
130 })?;
131
132 Ok(config_path)
133 }
134
135 pub fn write_to(self, path: impl AsRef<Path>) -> Result<(), CrossBuildError> {
137 let config_str = toml::to_string_pretty(&self.config)
138 .map_err(|e| CrossBuildError::configuration(e.to_string()))?;
139
140 let path_ref = path.as_ref().to_path_buf();
141 std::fs::write(&path_ref, config_str).map_err(|e| CrossBuildError::Io {
142 path: Some(path_ref),
143 source: e,
144 })?;
145
146 Ok(())
147 }
148}
149
150pub fn merge_cargo_configs(configs: Vec<toml::Table>) -> toml::Table {
152 let mut merged = toml::Table::new();
153
154 for config in configs {
155 for (key, value) in config {
156 if let Some(existing) = merged.get_mut(&key) {
157 merge_values(existing, value);
158 } else {
159 merged.insert(key, value);
160 }
161 }
162 }
163
164 merged
165}
166
167fn merge_values(existing: &mut toml::Value, new: toml::Value) {
169 match (existing, new) {
170 (toml::Value::Table(existing_table), toml::Value::Table(new_table)) => {
171 for (key, value) in new_table {
172 if let Some(existing_value) = existing_table.get_mut(&key) {
173 merge_values(existing_value, value);
174 } else {
175 existing_table.insert(key, value);
176 }
177 }
178 }
179 (existing, new) => {
180 *existing = new;
181 }
182 }
183}
184
185pub fn create_cross_config(
187 target: &crate::model::TargetTriple,
188 linker_path: Option<PathBuf>,
189 sysroot: Option<PathBuf>,
190 runner: Option<String>,
191 rustflags: Vec<String>,
192 env: BTreeMap<String, String>,
193 workspace_root: &Path,
194) -> Result<toml::Table, CrossBuildError> {
195 let mut generator = CargoConfigGenerator::new(target.clone(), workspace_root.to_path_buf());
196
197 if let Some(linker) = linker_path {
198 let flavor = match target.family() {
199 crate::model::TargetFamily::Windows => "msvc",
200 crate::model::TargetFamily::Wasm => "wasm-ld",
201 crate::model::TargetFamily::MacOs => "ld64",
202 _ => "gcc",
203 };
204 generator = generator.with_linker(linker, flavor, Vec::new());
205 }
206
207 if let Some(sysroot) = sysroot {
208 generator = generator.with_sysroot(sysroot);
209 }
210
211 if let Some(runner) = runner {
212 generator = generator.with_runner(runner);
213 }
214
215 if !rustflags.is_empty() {
216 generator = generator.with_rustflags(rustflags);
217 }
218
219 if !env.is_empty() {
220 generator = generator.with_env(env);
221 }
222
223 generator = generator.with_build_target(target);
224
225 Ok(generator.build())
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::model::TargetTriple;
232 use std::path::PathBuf;
233
234 #[test]
235 fn generates_basic_config() {
236 let target = TargetTriple::parse("x86_64-unknown-linux-gnu").unwrap();
237 let config = CargoConfigGenerator::new(target, PathBuf::from("/tmp"))
238 .with_linker("/usr/bin/ld.lld", "gcc", vec![])
239 .with_sysroot("/opt/sysroot")
240 .with_runner("qemu-x86_64")
241 .with_rustflags(vec!["-C".into(), "linker=clang".into()])
242 .build();
243
244 assert!(config.contains_key("target.x86_64-unknown-linux-gnu"));
245 }
246
247 #[test]
248 fn generates_runner_config() {
249 let target = TargetTriple::parse("wasm32-wasi").unwrap();
250 let config = CargoConfigGenerator::new(target, PathBuf::from("/tmp"))
251 .with_runner("wasmtime")
252 .build();
253
254 let target_table = config.get("target.wasm32-wasi").unwrap().as_table().unwrap();
255 assert_eq!(target_table.get("runner").unwrap().as_str().unwrap(), "wasmtime");
256 }
257
258 #[test]
259 fn merges_configs() {
260 let mut config1 = toml::Table::new();
261 let mut target1 = toml::Table::new();
262 target1.insert("linker".to_string(), toml::Value::String("ld.lld".to_string()));
263 config1.insert("target.x86_64-unknown-linux-gnu".to_string(), toml::Value::Table(target1));
264
265 let mut config2 = toml::Table::new();
266 let mut target2 = toml::Table::new();
267 target2.insert("runner".to_string(), toml::Value::String("qemu".to_string()));
268 config2.insert("target.x86_64-unknown-linux-gnu".to_string(), toml::Value::Table(target2));
269
270 let merged = merge_cargo_configs(vec![config1, config2]);
271 let target = merged.get("target.x86_64-unknown-linux-gnu").unwrap().as_table().unwrap();
272 assert_eq!(target.get("linker").unwrap().as_str().unwrap(), "ld.lld");
273 assert_eq!(target.get("runner").unwrap().as_str().unwrap(), "qemu");
274 }
275
276 #[test]
277 fn creates_cross_config() {
278 let target = crate::model::TargetTriple::parse("x86_64-unknown-linux-gnu").unwrap();
279 let config = create_cross_config(
280 &target,
281 Some(std::path::PathBuf::from("/usr/bin/ld.lld")),
282 Some(std::path::PathBuf::from("/opt/sysroot")),
283 Some("qemu-x86_64".to_string()),
284 vec!["-C".into(), "linker=clang".into()],
285 std::collections::BTreeMap::new(),
286 &PathBuf::from("/workspace"),
287 ).unwrap();
288
289 assert!(config.contains_key("target.x86_64-unknown-linux-gnu"));
290 }
291}