cargo_kit/workspace/
manifest.rs

1use 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
9/// Tries to resolve the workspace root manifest (Cargo.toml) path from the current directory.
10pub 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/// Manifest parsed out of a `Cargo.toml` file.
65#[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        // If we're applying the template to a built-in profile (dev or release), we skip the items
122        // that still have the default value.
123        // However, we don't do that for custom profiles based on dev/release, since dev/release
124        // might not actually contain the default values in that case.
125        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                // Check if there is any existing value in the TOML profile table
141                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                // Check if we modify a built-in profile, and if we have a default vaule for this
153                // item in the profile.
154                let default_item = base_template.as_ref().and_then(|t| t.get_item(id).cloned());
155
156                // If we have the same value as the default, and the existing value also matches the
157                // default, skip this item.
158                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            // Add "inherits" to the table
174            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        // Add necessary Cargo features
191        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}