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