cargo_wizard/workspace/
config.rs1use 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#[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 if let Some(array) = flags.as_array_mut() {
88 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}