Skip to main content

cargo_wizard/workspace/
config.rs

1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use crate::{Template, TemplateItemId, TomlValue};
5use anyhow::Context;
6use toml_edit::{Array, DocumentMut, Formatted, Value, table, value};
7
8/// Config stored in `.cargo/config.toml` file.
9#[derive(Debug, Clone)]
10pub struct CargoConfig {
11    path: PathBuf,
12    document: DocumentMut,
13}
14
15impl CargoConfig {
16    pub fn empty_from_manifest(manifest_path: &Path) -> Self {
17        Self {
18            path: config_path_from_manifest_path(manifest_path),
19            document: Default::default(),
20        }
21    }
22
23    pub fn from_path(path: &Path) -> anyhow::Result<Self> {
24        let config = std::fs::read_to_string(path).context("Cannot read config.toml file")?;
25        let document = config
26            .parse::<DocumentMut>()
27            .context("Cannot parse config.toml file")?;
28
29        Ok(Self {
30            document,
31            path: path.to_path_buf(),
32        })
33    }
34
35    pub fn get_text(&self) -> String {
36        self.document.to_string()
37    }
38
39    pub fn apply_template(mut self, template: &Template) -> anyhow::Result<Self> {
40        let rustflags: Vec<String> = template
41            .iter_items()
42            .filter_map(|(id, value)| {
43                let value = match value {
44                    TomlValue::String(value) => value.clone(),
45                    TomlValue::Int(value) => value.to_string(),
46                    TomlValue::Bool(value) => value.to_string(),
47                };
48                match id {
49                    TemplateItemId::TargetCpuInstructionSet => {
50                        Some(format!("-Ctarget-cpu={value}"))
51                    }
52                    TemplateItemId::FrontendThreads => Some(format!("-Zthreads={value}")),
53                    TemplateItemId::Linker => Some(format!("-Clink-arg=-fuse-ld={value}")),
54                    TemplateItemId::DebugInfo
55                    | TemplateItemId::Strip
56                    | TemplateItemId::Lto
57                    | TemplateItemId::CodegenUnits
58                    | TemplateItemId::Panic
59                    | TemplateItemId::OptimizationLevel
60                    | TemplateItemId::CodegenBackend
61                    | TemplateItemId::Incremental
62                    | TemplateItemId::SplitDebugInfo => None,
63                }
64            })
65            .collect();
66        if rustflags.is_empty() {
67            return Ok(self);
68        }
69
70        let build = self
71            .document
72            .entry("build")
73            .or_insert(table())
74            .as_table_mut()
75            .ok_or_else(|| anyhow::anyhow!("The build item in config.toml is not a table"))?;
76        let flags = build.entry("rustflags").or_insert(value(Array::new()));
77
78        let flag_map: HashMap<_, _> = rustflags
79            .iter()
80            .filter_map(|rustflag| {
81                let (key, value) = rustflag.split_once('=')?;
82                Some((key.to_string(), value.to_string()))
83            })
84            .collect();
85
86        // build.rustflags can be either a string or an array of strings
87        if let Some(array) = flags.as_array_mut() {
88            // Find flags with the same key (e.g. -Ckey=val) and replace their values, to avoid
89            // duplicating the keys.
90            for item in array.iter_mut() {
91                if let Some(val) = item.as_str()
92                    && let Some((key, _)) = val.split_once('=')
93                    && let Some(new_value) = flag_map.get(key)
94                {
95                    let decor = item.decor().clone();
96                    let mut new_value = Value::String(Formatted::new(format!("{key}={new_value}")));
97                    *new_value.decor_mut() = decor;
98                    *item = new_value;
99                }
100            }
101
102            let existing_flags: HashSet<String> = array
103                .iter()
104                .filter_map(|v| v.as_str())
105                .map(|s| s.to_string())
106                .collect();
107            for arg in rustflags {
108                if !existing_flags.contains(&arg) {
109                    array.push(Value::String(Formatted::new(arg)));
110                }
111            }
112        } else if let Some(val) = flags.as_value_mut().filter(|v| v.is_str()) {
113            let flattened_flags = rustflags.join(" ");
114            let mut original_value = val.as_str().unwrap_or_default().to_string();
115            if !original_value.ends_with(' ') && !original_value.is_empty() {
116                original_value.push(' ');
117            }
118            original_value.push_str(&flattened_flags);
119            let decor = val.decor().clone();
120            *val = Value::String(Formatted::new(original_value));
121            *val.decor_mut() = decor;
122        } else {
123            return Err(anyhow::anyhow!(
124                "build.rustflags in config.toml is not a string or an array"
125            ));
126        }
127
128        Ok(self)
129    }
130
131    pub fn write(self) -> anyhow::Result<()> {
132        std::fs::create_dir_all(self.path.parent().expect("Missing config.toml parent"))
133            .context("Cannot create config.toml parent directory")?;
134        std::fs::write(&self.path, self.document.to_string())
135            .context("Cannot write config.toml manifest")?;
136        Ok(())
137    }
138}
139
140pub fn config_path_from_manifest_path(manifest_path: &Path) -> PathBuf {
141    manifest_path
142        .parent()
143        .map(|p| p.join(".cargo").join("config.toml"))
144        .expect("Manifest path has no parent")
145}
146
147#[cfg(test)]
148mod tests {
149    use std::str::FromStr;
150
151    use toml_edit::DocumentMut;
152
153    use crate::template::TemplateBuilder;
154    use crate::workspace::manifest::BuiltinProfile;
155    use crate::{CargoConfig, Template, TemplateItemId, TomlValue};
156
157    #[test]
158    fn create_rustflags() {
159        let template = create_template(&[(TemplateItemId::TargetCpuInstructionSet, "native")]);
160        let config = create_empty_config().apply_template(&template).unwrap();
161        insta::assert_snapshot!(config.get_text(), @r###"
162        [build]
163        rustflags = ["-Ctarget-cpu=native"]
164        "###);
165    }
166
167    #[test]
168    fn append_to_array_rustflags() {
169        let template = create_template(&[(TemplateItemId::TargetCpuInstructionSet, "native")]);
170        let config = create_config(
171            r#"
172[build]
173rustflags = ["-Cbar=foo"]
174"#,
175        );
176        let config = config.apply_template(&template).unwrap();
177        insta::assert_snapshot!(config.get_text(), @r###"
178    [build]
179    rustflags = ["-Cbar=foo", "-Ctarget-cpu=native"]
180    "###);
181    }
182
183    #[test]
184    fn ignore_existing_entry() {
185        let template = create_template(&[(TemplateItemId::TargetCpuInstructionSet, "foo")]);
186        let config = create_config(
187            r#"
188[build]
189rustflags = ["-Ctarget-cpu=foo"]
190"#,
191        );
192        let config = config.apply_template(&template).unwrap();
193        insta::assert_snapshot!(config.get_text(), @r###"
194        [build]
195        rustflags = ["-Ctarget-cpu=foo"]
196        "###);
197    }
198
199    #[test]
200    fn append_to_empty_string_rustflags() {
201        let template = create_template(&[(TemplateItemId::TargetCpuInstructionSet, "native")]);
202        let config = create_config(
203            r#"
204[build]
205rustflags = ""
206"#,
207        );
208        let config = config.apply_template(&template).unwrap();
209        insta::assert_snapshot!(config.get_text(), @r###"
210            [build]
211            rustflags = "-Ctarget-cpu=native"
212            "###);
213    }
214
215    #[test]
216    fn append_to_string_rustflags() {
217        let template = create_template(&[(TemplateItemId::TargetCpuInstructionSet, "native")]);
218        let config = create_config(
219            r#"
220[build]
221rustflags = "-Cfoo=bar"
222"#,
223        );
224        let config = config.apply_template(&template).unwrap();
225        insta::assert_snapshot!(config.get_text(), @r###"
226        [build]
227        rustflags = "-Cfoo=bar -Ctarget-cpu=native"
228        "###);
229    }
230
231    #[test]
232    fn append_to_string_rustflags_keep_formatting() {
233        let template = create_template(&[(TemplateItemId::TargetCpuInstructionSet, "native")]);
234        let config = create_config(
235            r#"
236[build]
237rustflags = "-Cfoo=bar" # Foo
238"#,
239        );
240        let config = config.apply_template(&template).unwrap();
241        insta::assert_snapshot!(config.get_text(), @r###"
242        [build]
243        rustflags = "-Cfoo=bar -Ctarget-cpu=native" # Foo
244        "###);
245    }
246
247    #[test]
248    fn replace_rustflag_value() {
249        let template = create_template(&[(TemplateItemId::TargetCpuInstructionSet, "native")]);
250        let config = create_config(
251            r#"
252[build]
253rustflags = [
254    # Foo
255    "-Ctarget-cpu=foo", # Foo
256    "-Cbar=baz", # Foo
257]
258"#,
259        );
260        let config = config.apply_template(&template).unwrap();
261        insta::assert_snapshot!(config.get_text(), @r###"
262
263        [build]
264        rustflags = [
265            # Foo
266            "-Ctarget-cpu=native", # Foo
267            "-Cbar=baz", # Foo
268        ]
269        "###);
270    }
271
272    fn create_template(items: &[(TemplateItemId, &str)]) -> Template {
273        let mut builder = TemplateBuilder::new(BuiltinProfile::Release);
274        for (id, value) in items {
275            builder = builder.item(*id, TomlValue::String(value.to_string()));
276        }
277        builder.build()
278    }
279
280    fn create_config(text: &str) -> CargoConfig {
281        CargoConfig {
282            path: Default::default(),
283            document: DocumentMut::from_str(text).unwrap(),
284        }
285    }
286
287    fn create_empty_config() -> CargoConfig {
288        CargoConfig {
289            path: Default::default(),
290            document: Default::default(),
291        }
292    }
293}