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::{table, value, Array, Document, Formatted, Value};
7
8#[derive(Debug, Clone)]
10pub struct CargoConfig {
11 path: PathBuf,
12 document: Document,
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::<Document>()
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 Some((key, value)) = rustflag.split_once('=') else {
82 return None;
83 };
84 Some((key.to_string(), value.to_string()))
85 })
86 .collect();
87
88 if let Some(array) = flags.as_array_mut() {
90 for item in array.iter_mut() {
93 if let Some(val) = item.as_str() {
94 if let Some((key, _)) = val.split_once('=') {
95 if let Some(new_value) = flag_map.get(key) {
96 let decor = item.decor().clone();
97 let mut new_value =
98 Value::String(Formatted::new(format!("{key}={new_value}")));
99 *new_value.decor_mut() = decor;
100 *item = new_value;
101 }
102 }
103 }
104 }
105
106 let existing_flags: HashSet<String> = array
107 .iter()
108 .filter_map(|v| v.as_str())
109 .map(|s| s.to_string())
110 .collect();
111 for arg in rustflags {
112 if !existing_flags.contains(&arg) {
113 array.push(Value::String(Formatted::new(arg)));
114 }
115 }
116 } else if let Some(val) = flags.as_value_mut().filter(|v| v.is_str()) {
117 let flattened_flags = rustflags.join(" ");
118 let mut original_value = val.as_str().unwrap_or_default().to_string();
119 if !original_value.ends_with(' ') && !original_value.is_empty() {
120 original_value.push(' ');
121 }
122 original_value.push_str(&flattened_flags);
123 let decor = val.decor().clone();
124 *val = Value::String(Formatted::new(original_value));
125 *val.decor_mut() = decor;
126 } else {
127 return Err(anyhow::anyhow!(
128 "build.rustflags in config.toml is not a string or an array"
129 ));
130 }
131
132 Ok(self)
133 }
134
135 pub fn write(self) -> anyhow::Result<()> {
136 std::fs::create_dir_all(self.path.parent().expect("Missing config.toml parent"))
137 .context("Cannot create config.toml parent directory")?;
138 std::fs::write(&self.path, self.document.to_string())
139 .context("Cannot write config.toml manifest")?;
140 Ok(())
141 }
142}
143
144pub fn config_path_from_manifest_path(manifest_path: &Path) -> PathBuf {
145 manifest_path
146 .parent()
147 .map(|p| p.join(".cargo").join("config.toml"))
148 .expect("Manifest path has no parent")
149}
150
151#[cfg(test)]
152mod tests {
153 use std::str::FromStr;
154
155 use toml_edit::Document;
156
157 use crate::template::TemplateBuilder;
158 use crate::workspace::manifest::BuiltinProfile;
159 use crate::{CargoConfig, Template, TemplateItemId, TomlValue};
160
161 #[test]
162 fn create_rustflags() {
163 let template = create_template(&[(TemplateItemId::TargetCpuInstructionSet, "native")]);
164 let config = create_empty_config().apply_template(&template).unwrap();
165 insta::assert_snapshot!(config.get_text(), @r###"
166 [build]
167 rustflags = ["-Ctarget-cpu=native"]
168 "###);
169 }
170
171 #[test]
172 fn append_to_array_rustflags() {
173 let template = create_template(&[(TemplateItemId::TargetCpuInstructionSet, "native")]);
174 let config = create_config(
175 r#"
176[build]
177rustflags = ["-Cbar=foo"]
178"#,
179 );
180 let config = config.apply_template(&template).unwrap();
181 insta::assert_snapshot!(config.get_text(), @r###"
182 [build]
183 rustflags = ["-Cbar=foo", "-Ctarget-cpu=native"]
184 "###);
185 }
186
187 #[test]
188 fn ignore_existing_entry() {
189 let template = create_template(&[(TemplateItemId::TargetCpuInstructionSet, "foo")]);
190 let config = create_config(
191 r#"
192[build]
193rustflags = ["-Ctarget-cpu=foo"]
194"#,
195 );
196 let config = config.apply_template(&template).unwrap();
197 insta::assert_snapshot!(config.get_text(), @r###"
198 [build]
199 rustflags = ["-Ctarget-cpu=foo"]
200 "###);
201 }
202
203 #[test]
204 fn append_to_empty_string_rustflags() {
205 let template = create_template(&[(TemplateItemId::TargetCpuInstructionSet, "native")]);
206 let config = create_config(
207 r#"
208[build]
209rustflags = ""
210"#,
211 );
212 let config = config.apply_template(&template).unwrap();
213 insta::assert_snapshot!(config.get_text(), @r###"
214 [build]
215 rustflags = "-Ctarget-cpu=native"
216 "###);
217 }
218
219 #[test]
220 fn append_to_string_rustflags() {
221 let template = create_template(&[(TemplateItemId::TargetCpuInstructionSet, "native")]);
222 let config = create_config(
223 r#"
224[build]
225rustflags = "-Cfoo=bar"
226"#,
227 );
228 let config = config.apply_template(&template).unwrap();
229 insta::assert_snapshot!(config.get_text(), @r###"
230 [build]
231 rustflags = "-Cfoo=bar -Ctarget-cpu=native"
232 "###);
233 }
234
235 #[test]
236 fn append_to_string_rustflags_keep_formatting() {
237 let template = create_template(&[(TemplateItemId::TargetCpuInstructionSet, "native")]);
238 let config = create_config(
239 r#"
240[build]
241rustflags = "-Cfoo=bar" # Foo
242"#,
243 );
244 let config = config.apply_template(&template).unwrap();
245 insta::assert_snapshot!(config.get_text(), @r###"
246 [build]
247 rustflags = "-Cfoo=bar -Ctarget-cpu=native" # Foo
248 "###);
249 }
250
251 #[test]
252 fn replace_rustflag_value() {
253 let template = create_template(&[(TemplateItemId::TargetCpuInstructionSet, "native")]);
254 let config = create_config(
255 r#"
256[build]
257rustflags = [
258 # Foo
259 "-Ctarget-cpu=foo", # Foo
260 "-Cbar=baz", # Foo
261]
262"#,
263 );
264 let config = config.apply_template(&template).unwrap();
265 insta::assert_snapshot!(config.get_text(), @r###"
266
267 [build]
268 rustflags = [
269 # Foo
270 "-Ctarget-cpu=native", # Foo
271 "-Cbar=baz", # Foo
272 ]
273 "###);
274 }
275
276 fn create_template(items: &[(TemplateItemId, &str)]) -> Template {
277 let mut builder = TemplateBuilder::new(BuiltinProfile::Release);
278 for (id, value) in items {
279 builder = builder.item(*id, TomlValue::String(value.to_string()));
280 }
281 builder.build()
282 }
283
284 fn create_config(text: &str) -> CargoConfig {
285 CargoConfig {
286 path: Default::default(),
287 document: Document::from_str(text).unwrap(),
288 }
289 }
290
291 fn create_empty_config() -> CargoConfig {
292 CargoConfig {
293 path: Default::default(),
294 document: Default::default(),
295 }
296 }
297}