cargo_kit/workspace/
manifest.rs1use std::path::{Path, PathBuf};
2
3use anyhow::Context;
4use toml_edit::{table, value, Array, DocumentMut as Document, Item, Value};
5
6use crate::template::{dev_profile, release_profile, TemplateItemId};
7use crate::{Template, TomlValue};
8
9pub fn resolve_manifest_path() -> anyhow::Result<PathBuf> {
11 let cmd = cargo_metadata::MetadataCommand::new();
12 let metadata = cmd
13 .exec()
14 .map_err(|error| anyhow::anyhow!("Cannot get cargo metadata: {:?}", error))?;
15 let manifest_path = metadata
16 .workspace_root
17 .into_std_path_buf()
18 .join("Cargo.toml");
19 Ok(manifest_path)
20}
21
22#[derive(Clone, Copy, Debug)]
23pub enum BuiltinProfile {
24 Dev,
25 Release,
26}
27
28impl BuiltinProfile {
29 fn name(&self) -> &str {
30 match self {
31 BuiltinProfile::Dev => "dev",
32 BuiltinProfile::Release => "release",
33 }
34 }
35}
36
37#[derive(Clone, Debug)]
38pub enum Profile {
39 Builtin(BuiltinProfile),
40 Custom(String),
41}
42
43impl Profile {
44 pub fn dev() -> Self {
45 Self::Builtin(BuiltinProfile::Dev)
46 }
47
48 pub fn release() -> Self {
49 Self::Builtin(BuiltinProfile::Release)
50 }
51
52 pub fn name(&self) -> &str {
53 match self {
54 Profile::Builtin(builtin) => builtin.name(),
55 Profile::Custom(name) => name.as_str(),
56 }
57 }
58
59 pub fn is_builtin(&self) -> bool {
60 matches!(self, Profile::Builtin(_))
61 }
62}
63
64#[warn(deprecated)]
66#[derive(Clone)]
67pub struct CargoManifest {
68 path: PathBuf,
69 document: Document,
70}
71
72impl CargoManifest {
73 pub fn from_path(path: &Path) -> anyhow::Result<Self> {
74 let manifest = std::fs::read_to_string(path)
75 .with_context(|| format!("Cannot read Cargo.toml manifest from {}", path.display()))?;
76 let document = manifest
77 .parse::<Document>()
78 .with_context(|| format!("Cannot parse Cargo.toml manifest from {}", path.display()))?;
79 Ok(Self {
80 document,
81 path: path.to_path_buf(),
82 })
83 }
84
85 pub fn get_profiles(&self) -> Vec<String> {
86 self.document
87 .get("profile")
88 .and_then(|p| p.as_table_like())
89 .map(|t| t.iter().map(|(name, _)| name.to_string()).collect())
90 .unwrap_or_default()
91 }
92
93 pub fn get_text(&self) -> String {
94 self.document.to_string()
95 }
96
97 pub fn apply_template(
98 mut self,
99 profile: &Profile,
100 template: &Template,
101 ) -> anyhow::Result<Self> {
102 let profiles_table = self
103 .document
104 .entry("profile")
105 .or_insert(table())
106 .as_table_mut()
107 .ok_or_else(|| anyhow::anyhow!("The profile item in Cargo.toml is not a table"))?;
108 profiles_table.set_dotted(true);
109
110 let profile_table = profiles_table
111 .entry(profile.name())
112 .or_insert(table())
113 .as_table_mut()
114 .ok_or_else(|| {
115 anyhow::anyhow!(
116 "The profile.{} table in Cargo.toml is not a table",
117 profile.name()
118 )
119 })?;
120
121 let base_template = if profile.is_builtin() {
126 Some(match template.inherits() {
127 BuiltinProfile::Dev => dev_profile().build(),
128 BuiltinProfile::Release => release_profile().build(),
129 })
130 } else {
131 None
132 };
133 let mut values: Vec<_> = template
134 .iter_items()
135 .filter_map(|(id, value)| {
136 let Some(name) = id_to_item_name(id) else {
137 return None;
138 };
139
140 let existing_value = profile_table.get(name).and_then(|item| {
142 if let Some(value) = item.as_bool() {
143 Some(TomlValue::Bool(value))
144 } else if let Some(value) = item.as_integer() {
145 Some(TomlValue::Int(value))
146 } else if let Some(value) = item.as_str() {
147 Some(TomlValue::String(value.to_string()))
148 } else {
149 None
150 }
151 });
152 let default_item = base_template.as_ref().and_then(|t| t.get_item(id).cloned());
155
156 let base_item = existing_value.or(default_item);
159 if let Some(base_value) = base_item {
160 if &base_value == value {
161 return None;
162 }
163 };
164
165 Some(TableItem {
166 name: name.to_string(),
167 value: value.clone(),
168 })
169 })
170 .collect();
171
172 if !profile.is_builtin() {
173 values.insert(0, TableItem::string("inherits", template.inherits().name()));
175 }
176
177 for entry in values {
178 let mut new_value = entry.value.to_toml_value();
179
180 if let Some(existing_item) = profile_table.get_mut(&entry.name) {
181 if let Some(value) = existing_item.as_value() {
182 *new_value.decor_mut() = value.decor().clone();
183 }
184 *existing_item = value(new_value);
185 } else {
186 profile_table.insert(&entry.name, value(new_value));
187 }
188 }
189
190 if template.get_item(TemplateItemId::CodegenBackend).is_some() {
192 if let Some(features) = self
193 .document
194 .entry("cargo-features")
195 .or_insert(Item::Value(Value::Array(Array::new())))
196 .as_array_mut()
197 {
198 if !features
199 .iter()
200 .any(|v| v.as_str() == Some("codegen-backend"))
201 {
202 features.push("codegen-backend");
203 }
204 }
205 }
206
207 Ok(self)
208 }
209
210 pub fn write(self) -> anyhow::Result<()> {
211 std::fs::write(self.path, self.document.to_string())
212 .context("Cannot write Cargo.toml manifest")?;
213 Ok(())
214 }
215}
216
217fn id_to_item_name(id: TemplateItemId) -> Option<&'static str> {
218 match id {
219 TemplateItemId::DebugInfo => Some("debug"),
220 TemplateItemId::SplitDebugInfo => Some("split-debuginfo"),
221 TemplateItemId::Strip => Some("strip"),
222 TemplateItemId::Lto => Some("lto"),
223 TemplateItemId::CodegenUnits => Some("codegen-units"),
224 TemplateItemId::Panic => Some("panic"),
225 TemplateItemId::OptimizationLevel => Some("opt-level"),
226 TemplateItemId::CodegenBackend => Some("codegen-backend"),
227 TemplateItemId::Incremental => Some("incremental"),
228 TemplateItemId::TargetCpuInstructionSet
229 | TemplateItemId::FrontendThreads
230 | TemplateItemId::Linker => None,
231 }
232}
233
234#[derive(Clone, Debug)]
235struct TableItem {
236 name: String,
237 value: TomlValue,
238}
239
240impl TableItem {
241 fn string(name: &str, value: &str) -> Self {
242 Self {
243 name: name.to_string(),
244 value: TomlValue::String(value.to_string()),
245 }
246 }
247}