Skip to main content

crossbuild_core/
cargo_config.rs

1//! Cargo configuration generation for cross-compilation.
2
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5
6use crate::model::TargetTriple;
7use crate::error::CrossBuildError;
8
9/// Generates a .cargo/config.toml for cross-compilation.
10pub struct CargoConfigGenerator {
11    config: toml::Table,
12    target: TargetTriple,
13    workspace_root: PathBuf,
14}
15
16impl CargoConfigGenerator {
17    /// Creates a new generator for the given target.
18    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    /// Sets the linker for the target.
27    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    /// Sets the runner for the target (for running tests).
44    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    /// Sets custom rustflags for the target.
52    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    /// Sets environment variables for the target.
62    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    /// Sets the sysroot for the target.
78    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    /// Adds a custom [target.xxx] section.
86    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    /// Sets the build target.
94    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    /// Gets or creates a target table.
102    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    /// Builds the final config.
111    pub fn build(self) -> toml::Table {
112        self.config
113    }
114
115    /// Writes the config to .cargo/config.toml in the workspace root.
116    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    /// Writes the config to a specific path.
136    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
150/// Merges multiple cargo config snippets.
151pub 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
167/// Merges two toml values.
168fn 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
185/// Creates a standard cross-compilation cargo config.
186pub 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}