cargo_edit/
manifest.rs

1use std::fs;
2use std::ops::{Deref, DerefMut};
3use std::path::{Path, PathBuf};
4use std::{env, str};
5
6use semver::Version;
7
8use super::errors::*;
9use super::metadata::find_manifest_path;
10
11#[derive(PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Debug, Copy)]
12pub enum DepKind {
13    Normal,
14    Development,
15    Build,
16}
17
18/// Dependency table to add dep to
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct DepTable {
21    kind: DepKind,
22    target: Option<String>,
23}
24
25impl DepTable {
26    const KINDS: &'static [Self] = &[
27        Self::new().set_kind(DepKind::Normal),
28        Self::new().set_kind(DepKind::Development),
29        Self::new().set_kind(DepKind::Build),
30    ];
31
32    /// Reference to a Dependency Table
33    pub(crate) const fn new() -> Self {
34        Self {
35            kind: DepKind::Normal,
36            target: None,
37        }
38    }
39
40    /// Choose the type of dependency
41    pub(crate) const fn set_kind(mut self, kind: DepKind) -> Self {
42        self.kind = kind;
43        self
44    }
45
46    /// Choose the platform for the dependency
47    pub(crate) fn set_target(mut self, target: impl Into<String>) -> Self {
48        self.target = Some(target.into());
49        self
50    }
51
52    fn kind_table(&self) -> &str {
53        match self.kind {
54            DepKind::Normal => "dependencies",
55            DepKind::Development => "dev-dependencies",
56            DepKind::Build => "build-dependencies",
57        }
58    }
59}
60
61impl Default for DepTable {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67impl From<DepKind> for DepTable {
68    fn from(other: DepKind) -> Self {
69        Self::new().set_kind(other)
70    }
71}
72
73/// A Cargo manifest
74#[derive(Debug, Clone)]
75pub struct Manifest {
76    /// Manifest contents as TOML data
77    pub data: toml_edit::DocumentMut,
78}
79
80impl Manifest {
81    /// Get the specified table from the manifest.
82    ///
83    /// If there is no table at the specified path, then a non-existent table
84    /// error will be returned.
85    pub(crate) fn get_table_mut<'a>(
86        &'a mut self,
87        table_path: &[String],
88    ) -> CargoResult<&'a mut toml_edit::Item> {
89        self.get_table_mut_internal(table_path, false)
90    }
91
92    /// Get all sections in the manifest that exist and might contain dependencies.
93    /// The returned items are always `Table` or `InlineTable`.
94    pub(crate) fn get_sections(&self) -> Vec<(DepTable, toml_edit::Item)> {
95        let mut sections = Vec::new();
96
97        for table in DepTable::KINDS {
98            let dependency_type = table.kind_table();
99            // Dependencies can be in the three standard sections...
100            if self
101                .data
102                .get(dependency_type)
103                .map(|t| t.is_table_like())
104                .unwrap_or(false)
105            {
106                sections.push((table.clone(), self.data[dependency_type].clone()))
107            }
108
109            // ... and in `target.<target>.(build-/dev-)dependencies`.
110            let target_sections = self
111                .data
112                .as_table()
113                .get("target")
114                .and_then(toml_edit::Item::as_table_like)
115                .into_iter()
116                .flat_map(toml_edit::TableLike::iter)
117                .filter_map(|(target_name, target_table)| {
118                    let dependency_table = target_table.get(dependency_type)?;
119                    dependency_table.as_table_like().map(|_| {
120                        (
121                            table.clone().set_target(target_name),
122                            dependency_table.clone(),
123                        )
124                    })
125                });
126
127            sections.extend(target_sections);
128        }
129
130        sections
131    }
132
133    fn get_table_mut_internal<'a>(
134        &'a mut self,
135        table_path: &[String],
136        insert_if_not_exists: bool,
137    ) -> CargoResult<&'a mut toml_edit::Item> {
138        /// Descend into a manifest until the required table is found.
139        fn descend<'a>(
140            input: &'a mut toml_edit::Item,
141            path: &[String],
142            insert_if_not_exists: bool,
143        ) -> CargoResult<&'a mut toml_edit::Item> {
144            if let Some(segment) = path.first() {
145                let value = if insert_if_not_exists {
146                    input[&segment].or_insert(toml_edit::table())
147                } else {
148                    input
149                        .get_mut(segment)
150                        .ok_or_else(|| non_existent_table_err(segment))?
151                };
152
153                if value.is_table_like() {
154                    descend(value, &path[1..], insert_if_not_exists)
155                } else {
156                    Err(non_existent_table_err(segment))
157                }
158            } else {
159                Ok(input)
160            }
161        }
162
163        descend(self.data.as_item_mut(), table_path, insert_if_not_exists)
164    }
165}
166
167impl str::FromStr for Manifest {
168    type Err = anyhow::Error;
169
170    /// Read manifest data from string
171    fn from_str(input: &str) -> ::std::result::Result<Self, Self::Err> {
172        let d: toml_edit::DocumentMut = input.parse().context("Manifest not valid TOML")?;
173
174        Ok(Manifest { data: d })
175    }
176}
177
178impl std::fmt::Display for Manifest {
179    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180        let s = self.data.to_string();
181        s.fmt(f)
182    }
183}
184
185/// A Cargo manifest that is available locally.
186#[derive(Debug)]
187pub struct LocalManifest {
188    /// Path to the manifest
189    pub path: PathBuf,
190    /// Manifest contents
191    pub manifest: Manifest,
192}
193
194impl Deref for LocalManifest {
195    type Target = Manifest;
196
197    fn deref(&self) -> &Manifest {
198        &self.manifest
199    }
200}
201
202impl DerefMut for LocalManifest {
203    fn deref_mut(&mut self) -> &mut Manifest {
204        &mut self.manifest
205    }
206}
207
208impl LocalManifest {
209    /// Construct a `LocalManifest`. If no path is provided, make an educated guess as to which one
210    /// the user means.
211    pub fn find(path: Option<&Path>) -> CargoResult<Self> {
212        let path = dunce::canonicalize(find(path)?)?;
213        Self::try_new(&path)
214    }
215
216    /// Construct the `LocalManifest` corresponding to the `Path` provided.
217    pub fn try_new(path: &Path) -> CargoResult<Self> {
218        if !path.is_absolute() {
219            anyhow::bail!("can only edit absolute paths, got {}", path.display());
220        }
221        let data = fs::read_to_string(path).with_context(|| "Failed to read manifest contents")?;
222        let manifest = data.parse().context("Unable to parse Cargo.toml")?;
223        Ok(LocalManifest {
224            manifest,
225            path: path.to_owned(),
226        })
227    }
228
229    /// Write changes back to the file
230    pub fn write(&self) -> CargoResult<()> {
231        let s = self.manifest.data.to_string();
232        let new_contents_bytes = s.as_bytes();
233
234        fs::write(&self.path, new_contents_bytes).context("Failed to write updated Cargo.toml")
235    }
236
237    /// Remove entry from a Cargo.toml.
238    ///
239    /// # Examples
240    ///
241    /// ```
242    ///   use cargo_edit::{Dependency, LocalManifest, Manifest, RegistrySource};
243    ///   use toml_edit;
244    ///
245    ///   let root = std::path::PathBuf::from("/").canonicalize().unwrap();
246    ///   let path = root.join("Cargo.toml");
247    ///   let manifest: toml_edit::Document = "
248    ///   [dependencies]
249    ///   cargo-edit = '0.1.0'
250    ///   ".parse().unwrap();
251    ///   let mut manifest = LocalManifest { path, manifest: Manifest { data: manifest } };
252    ///   assert!(manifest.remove_from_table(&["dependencies".to_owned()], "cargo-edit").is_ok());
253    ///   assert!(manifest.remove_from_table(&["dependencies".to_owned()], "cargo-edit").is_err());
254    ///   assert!(!manifest.data.contains_key("dependencies"));
255    /// ```
256    pub fn remove_from_table(&mut self, table_path: &[String], name: &str) -> CargoResult<()> {
257        let parent_table = self.get_table_mut(table_path)?;
258
259        {
260            let dep = parent_table
261                .get_mut(name)
262                .filter(|t| !t.is_none())
263                .ok_or_else(|| non_existent_dependency_err(name, table_path.join(".")))?;
264            // remove the dependency
265            *dep = toml_edit::Item::None;
266        }
267
268        // remove table if empty
269        if parent_table.as_table_like().unwrap().is_empty() {
270            *parent_table = toml_edit::Item::None;
271        }
272
273        Ok(())
274    }
275
276    /// Allow mutating depedencies, wherever they live
277    pub fn get_dependency_tables_mut(
278        &mut self,
279    ) -> impl Iterator<Item = &mut dyn toml_edit::TableLike> + '_ {
280        let root = self.data.as_table_mut();
281        root.iter_mut().flat_map(|(k, v)| {
282            if DepTable::KINDS
283                .iter()
284                .any(|kind| kind.kind_table() == k.get())
285            {
286                v.as_table_like_mut().into_iter().collect::<Vec<_>>()
287            } else if k == "workspace" {
288                v.as_table_like_mut()
289                    .unwrap()
290                    .iter_mut()
291                    .filter_map(|(k, v)| {
292                        if k.get() == "dependencies" {
293                            v.as_table_like_mut()
294                        } else {
295                            None
296                        }
297                    })
298                    .collect::<Vec<_>>()
299            } else if k == "target" {
300                v.as_table_like_mut()
301                    .unwrap()
302                    .iter_mut()
303                    .flat_map(|(_, v)| {
304                        v.as_table_like_mut().into_iter().flat_map(|v| {
305                            v.iter_mut().filter_map(|(k, v)| {
306                                if DepTable::KINDS
307                                    .iter()
308                                    .any(|kind| kind.kind_table() == k.get())
309                                {
310                                    v.as_table_like_mut()
311                                } else {
312                                    None
313                                }
314                            })
315                        })
316                    })
317                    .collect::<Vec<_>>()
318            } else {
319                Vec::new()
320            }
321        })
322    }
323
324    /// Iterates mutably over the `[workspace.dependencies]`.
325    pub fn get_workspace_dependency_table_mut(&mut self) -> Option<&mut dyn toml_edit::TableLike> {
326        self.data
327            .get_mut("workspace")?
328            .get_mut("dependencies")?
329            .as_table_like_mut()
330    }
331
332    /// Override the manifest's version
333    pub fn set_package_version(&mut self, version: &Version) {
334        self.data["package"]["version"] = toml_edit::value(version.to_string());
335    }
336
337    /// `true` if the package inherits the workspace version
338    pub fn version_is_inherited(&self) -> bool {
339        fn inherits_workspace_version_impl(this: &Manifest) -> Option<bool> {
340            this.data
341                .get("package")?
342                .get("version")?
343                .get("workspace")?
344                .as_bool()
345        }
346
347        inherits_workspace_version_impl(self).unwrap_or(false)
348    }
349
350    /// Get the current workspace version, if any.
351    pub fn get_workspace_version(&self) -> Option<Version> {
352        let version = self
353            .data
354            .get("workspace")?
355            .get("package")?
356            .get("version")?
357            .as_str()?;
358        Version::parse(version).ok()
359    }
360
361    /// Override the workspace's version.
362    pub fn set_workspace_version(&mut self, version: &Version) {
363        self.data["workspace"]["package"]["version"] = toml_edit::value(version.to_string());
364    }
365
366    /// Remove references to `dep_key` if its no longer present
367    pub fn gc_dep(&mut self, dep_key: &str) {
368        let status = self.dep_feature(dep_key);
369        if matches!(status, FeatureStatus::None | FeatureStatus::DepFeature) {
370            if let toml_edit::Item::Table(feature_table) = &mut self.data.as_table_mut()["features"]
371            {
372                for (_feature, mut activated_crates) in feature_table.iter_mut() {
373                    if let toml_edit::Item::Value(toml_edit::Value::Array(feature_activations)) =
374                        &mut activated_crates
375                    {
376                        remove_feature_activation(feature_activations, dep_key, status);
377                    }
378                }
379            }
380        }
381    }
382
383    fn dep_feature(&self, dep_key: &str) -> FeatureStatus {
384        let mut status = FeatureStatus::None;
385        for (_, tbl) in self.get_sections() {
386            if let toml_edit::Item::Table(tbl) = tbl {
387                if let Some(dep_item) = tbl.get(dep_key) {
388                    let optional = dep_item.get("optional");
389                    let optional = optional.and_then(|i| i.as_value());
390                    let optional = optional.and_then(|i| i.as_bool());
391                    let optional = optional.unwrap_or(false);
392                    if optional {
393                        return FeatureStatus::Feature;
394                    } else {
395                        status = FeatureStatus::DepFeature;
396                    }
397                }
398            }
399        }
400        status
401    }
402}
403
404#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
405enum FeatureStatus {
406    None,
407    DepFeature,
408    Feature,
409}
410
411fn remove_feature_activation(
412    feature_activations: &mut toml_edit::Array,
413    dep: &str,
414    status: FeatureStatus,
415) {
416    let dep_feature: &str = &format!("{dep}/",);
417
418    let remove_list: Vec<usize> = feature_activations
419        .iter()
420        .enumerate()
421        .filter_map(|(idx, feature_activation)| {
422            if let toml_edit::Value::String(feature_activation) = feature_activation {
423                let activation = feature_activation.value();
424                #[allow(clippy::unnecessary_lazy_evaluations)] // requires 1.62
425                match status {
426                    FeatureStatus::None => activation == dep || activation.starts_with(dep_feature),
427                    FeatureStatus::DepFeature => activation == dep,
428                    FeatureStatus::Feature => false,
429                }
430                .then(|| idx)
431            } else {
432                None
433            }
434        })
435        .collect();
436
437    // Remove found idx in revers order so we don't invalidate the idx.
438    for idx in remove_list.iter().rev() {
439        feature_activations.remove(*idx);
440    }
441}
442
443/// If a manifest is specified, return that one, otherise perform a manifest search starting from
444/// the current directory.
445/// If a manifest is specified, return that one. If a path is specified, perform a manifest search
446/// starting from there. If nothing is specified, start searching from the current directory
447/// (`cwd`).
448pub fn find(specified: Option<&Path>) -> CargoResult<PathBuf> {
449    match specified {
450        Some(path)
451            if fs::metadata(path)
452                .with_context(|| "Failed to get cargo file metadata")?
453                .is_file() =>
454        {
455            Ok(path.to_owned())
456        }
457        Some(path) => find_manifest_path(path),
458        None => find_manifest_path(
459            &env::current_dir().with_context(|| "Failed to get current directory")?,
460        ),
461    }
462}
463
464/// Get a dependency's version from its entry in the dependency table
465pub fn get_dep_version(dep_item: &toml_edit::Item) -> CargoResult<&str> {
466    if let Some(req) = dep_item.as_str() {
467        Ok(req)
468    } else if dep_item.is_table_like() {
469        let version = dep_item
470            .get("version")
471            .ok_or_else(|| anyhow::format_err!("Missing version field"))?;
472        version
473            .as_str()
474            .ok_or_else(|| anyhow::format_err!("Expect version to be a string"))
475    } else {
476        anyhow::bail!("Invalid dependency type");
477    }
478}
479
480/// Set a dependency's version in its entry in the dependency table
481pub fn set_dep_version(dep_item: &mut toml_edit::Item, new_version: &str) -> CargoResult<()> {
482    if dep_item.is_str() {
483        overwrite_value(dep_item, new_version);
484    } else if let Some(table) = dep_item.as_table_like_mut() {
485        let version = table
486            .get_mut("version")
487            .ok_or_else(|| anyhow::format_err!("Missing version field"))?;
488        overwrite_value(version, new_version);
489    } else {
490        anyhow::bail!("Invalid dependency type");
491    }
492    Ok(())
493}
494
495/// Overwrite a value while preserving the original formatting
496fn overwrite_value(item: &mut toml_edit::Item, value: impl Into<toml_edit::Value>) {
497    let mut value = value.into();
498
499    let existing_decor = item
500        .as_value()
501        .map(|v| v.decor().clone())
502        .unwrap_or_default();
503
504    *value.decor_mut() = existing_decor;
505
506    *item = toml_edit::Item::Value(value);
507}
508
509pub fn str_or_1_len_table(item: &toml_edit::Item) -> bool {
510    item.is_str() || item.as_table_like().map(|t| t.len() == 1).unwrap_or(false)
511}