Skip to main content

cargo/util/toml_mut/
manifest.rs

1//! Parsing and editing of manifest files.
2
3use std::ops::{Deref, DerefMut};
4use std::path::{Path, PathBuf};
5use std::str;
6
7use anyhow::Context as _;
8
9use super::dependency::Dependency;
10use crate::core::dependency::DepKind;
11use crate::core::{FeatureValue, Features, Workspace};
12use crate::util::closest;
13use crate::util::frontmatter::ScriptSource;
14use crate::util::toml::is_embedded;
15use crate::{CargoResult, GlobalContext};
16
17/// Dependency table to add deps to.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct DepTable {
20    kind: DepKind,
21    target: Option<String>,
22}
23
24impl DepTable {
25    const KINDS: &'static [Self] = &[
26        Self::new().set_kind(DepKind::Normal),
27        Self::new().set_kind(DepKind::Development),
28        Self::new().set_kind(DepKind::Build),
29    ];
30
31    /// Reference to a Dependency Table.
32    pub const fn new() -> Self {
33        Self {
34            kind: DepKind::Normal,
35            target: None,
36        }
37    }
38
39    /// Choose the type of dependency.
40    pub const fn set_kind(mut self, kind: DepKind) -> Self {
41        self.kind = kind;
42        self
43    }
44
45    /// Choose the platform for the dependency.
46    pub fn set_target(mut self, target: impl Into<String>) -> Self {
47        self.target = Some(target.into());
48        self
49    }
50
51    /// Type of dependency.
52    pub fn kind(&self) -> DepKind {
53        self.kind
54    }
55
56    /// Platform for the dependency.
57    pub fn target(&self) -> Option<&str> {
58        self.target.as_deref()
59    }
60
61    /// Keys to the table.
62    pub fn to_table(&self) -> Vec<&str> {
63        if let Some(target) = &self.target {
64            vec!["target", target, self.kind.kind_table()]
65        } else {
66            vec![self.kind.kind_table()]
67        }
68    }
69}
70
71impl Default for DepTable {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl From<DepKind> for DepTable {
78    fn from(other: DepKind) -> Self {
79        Self::new().set_kind(other)
80    }
81}
82
83/// An editable Cargo manifest.
84#[derive(Debug, Clone)]
85pub struct Manifest {
86    /// Manifest contents as TOML data.
87    pub data: toml_edit::DocumentMut,
88}
89
90impl Manifest {
91    /// Get the manifest's package name.
92    pub fn package_name(&self) -> CargoResult<&str> {
93        self.data
94            .as_table()
95            .get("package")
96            .and_then(|m| m.get("name"))
97            .and_then(|m| m.as_str())
98            .ok_or_else(parse_manifest_err)
99    }
100
101    /// Get the specified table from the manifest.
102    pub fn get_table<'a>(&'a self, table_path: &[String]) -> Option<&'a toml_edit::Item> {
103        /// Descend into a manifest until the required table is found.
104        fn descend<'a>(input: &'a toml_edit::Item, path: &[String]) -> Option<&'a toml_edit::Item> {
105            if let Some(segment) = path.get(0) {
106                let value = input.get(&segment)?;
107
108                if value.is_table_like() {
109                    descend(value, &path[1..])
110                } else {
111                    None
112                }
113            } else {
114                Some(input)
115            }
116        }
117
118        descend(self.data.as_item(), table_path)
119    }
120
121    /// Get the specified table from the manifest.
122    pub fn get_table_mut<'a>(
123        &'a mut self,
124        table_path: &[String],
125    ) -> Option<&'a mut toml_edit::Item> {
126        /// Descend into a manifest until the required table is found.
127        fn descend<'a>(
128            input: &'a mut toml_edit::Item,
129            path: &[String],
130        ) -> Option<&'a mut toml_edit::Item> {
131            if let Some(segment) = path.get(0) {
132                let mut default_table = toml_edit::Table::new();
133                default_table.set_implicit(true);
134                let value = input[&segment].or_insert(toml_edit::Item::Table(default_table));
135
136                if value.is_table_like() {
137                    descend(value, &path[1..])
138                } else {
139                    None
140                }
141            } else {
142                Some(input)
143            }
144        }
145
146        descend(self.data.as_item_mut(), table_path)
147    }
148
149    /// Get all sections in the manifest that exist and might contain
150    /// dependencies. The returned items are always `Table` or
151    /// `InlineTable`.
152    pub fn get_sections(&self) -> Vec<(DepTable, toml_edit::Item)> {
153        let mut sections = Vec::new();
154
155        for table in DepTable::KINDS {
156            let dependency_type = table.kind.kind_table();
157            // Dependencies can be in the three standard sections...
158            if self
159                .data
160                .get(dependency_type)
161                .map(|t| t.is_table_like())
162                .unwrap_or(false)
163            {
164                sections.push((table.clone(), self.data[dependency_type].clone()))
165            }
166
167            // ... and in `target.<target>.(build-/dev-)dependencies`.
168            let target_sections = self
169                .data
170                .as_table()
171                .get("target")
172                .and_then(toml_edit::Item::as_table_like)
173                .into_iter()
174                .flat_map(toml_edit::TableLike::iter)
175                .filter_map(|(target_name, target_table)| {
176                    let dependency_table = target_table.get(dependency_type)?;
177                    dependency_table.as_table_like().map(|_| {
178                        (
179                            table.clone().set_target(target_name),
180                            dependency_table.clone(),
181                        )
182                    })
183                });
184
185            sections.extend(target_sections);
186        }
187
188        sections
189    }
190
191    pub fn get_legacy_sections(&self) -> Vec<String> {
192        let mut result = Vec::new();
193
194        for dependency_type in ["dev_dependencies", "build_dependencies"] {
195            if self.data.contains_key(dependency_type) {
196                result.push(dependency_type.to_owned());
197            }
198
199            // ... and in `target.<target>.(build-/dev-)dependencies`.
200            result.extend(
201                self.data
202                    .as_table()
203                    .get("target")
204                    .and_then(toml_edit::Item::as_table_like)
205                    .into_iter()
206                    .flat_map(toml_edit::TableLike::iter)
207                    .filter_map(|(target_name, target_table)| {
208                        if target_table.as_table_like()?.contains_key(dependency_type) {
209                            Some(format!("target.{target_name}.{dependency_type}"))
210                        } else {
211                            None
212                        }
213                    }),
214            );
215        }
216        result
217    }
218}
219
220impl str::FromStr for Manifest {
221    type Err = anyhow::Error;
222
223    /// Read manifest data from string
224    fn from_str(input: &str) -> ::std::result::Result<Self, Self::Err> {
225        let d: toml_edit::DocumentMut = input.parse().context("Manifest not valid TOML")?;
226
227        Ok(Manifest { data: d })
228    }
229}
230
231impl std::fmt::Display for Manifest {
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        self.data.fmt(f)
234    }
235}
236
237/// An editable Cargo manifest that is available locally.
238#[derive(Debug, Clone)]
239pub struct LocalManifest {
240    /// Path to the manifest.
241    pub path: PathBuf,
242    /// Manifest contents.
243    pub manifest: Manifest,
244    /// The raw, unparsed package file
245    pub raw: String,
246    /// Edit location for an embedded manifest, if relevant
247    pub embedded: Option<Embedded>,
248}
249
250impl Deref for LocalManifest {
251    type Target = Manifest;
252
253    fn deref(&self) -> &Manifest {
254        &self.manifest
255    }
256}
257
258impl DerefMut for LocalManifest {
259    fn deref_mut(&mut self) -> &mut Manifest {
260        &mut self.manifest
261    }
262}
263
264impl LocalManifest {
265    /// Construct the `LocalManifest` corresponding to the `Path` provided..
266    pub fn try_new(path: &Path) -> CargoResult<Self> {
267        if !path.is_absolute() {
268            anyhow::bail!("can only edit absolute paths, got {}", path.display());
269        }
270        let raw = cargo_util::paths::read(&path)?;
271        let mut data = raw.clone();
272        let mut embedded = None;
273        if is_embedded(path) {
274            let source = ScriptSource::parse(&data)?;
275            if let Some(frontmatter) = source.frontmatter_span() {
276                embedded = Some(Embedded::exists(frontmatter));
277                data = source.frontmatter().unwrap().to_owned();
278            } else if let Some(shebang) = source.shebang_span() {
279                embedded = Some(Embedded::after(shebang));
280                data = String::new();
281            } else {
282                embedded = Some(Embedded::start());
283                data = String::new();
284            }
285        }
286        let manifest = data.parse().context("Unable to parse Cargo.toml")?;
287        Ok(LocalManifest {
288            manifest,
289            path: path.to_owned(),
290            raw,
291            embedded,
292        })
293    }
294
295    /// Write changes back to the file.
296    pub fn write(&self) -> CargoResult<()> {
297        let mut manifest = self.manifest.data.to_string();
298        let raw = match self.embedded.as_ref() {
299            Some(Embedded::Implicit(start)) => {
300                if !manifest.ends_with("\n") {
301                    manifest.push_str("\n");
302                }
303                let fence = "---\n";
304                let prefix = &self.raw[0..*start];
305                let suffix = &self.raw[*start..];
306                let empty_line = if prefix.is_empty() { "\n" } else { "" };
307                format!("{prefix}{fence}{manifest}{fence}{empty_line}{suffix}")
308            }
309            Some(Embedded::Explicit(span)) => {
310                if !manifest.ends_with("\n") {
311                    manifest.push_str("\n");
312                }
313                let prefix = &self.raw[0..span.start];
314                let suffix = &self.raw[span.end..];
315                format!("{prefix}{manifest}{suffix}")
316            }
317            None => manifest,
318        };
319        let new_contents_bytes = raw.as_bytes();
320
321        cargo_util::paths::write_atomic(&self.path, new_contents_bytes)
322    }
323
324    /// Lookup a dependency.
325    pub fn get_dependencies<'s>(
326        &'s self,
327        ws: &'s Workspace<'_>,
328        unstable_features: &'s Features,
329    ) -> impl Iterator<Item = (String, DepTable, CargoResult<Dependency>)> + 's {
330        let crate_root = self.path.parent().expect("manifest path is absolute");
331        self.get_sections()
332            .into_iter()
333            .filter_map(move |(table_path, table)| {
334                let table = table.into_table().ok()?;
335                Some(
336                    table
337                        .into_iter()
338                        .map(|(key, item)| (table_path.clone(), key, item))
339                        .collect::<Vec<_>>(),
340                )
341            })
342            .flatten()
343            .map(move |(table_path, dep_key, dep_item)| {
344                let dep = Dependency::from_toml(
345                    ws.gctx(),
346                    ws.root(),
347                    crate_root,
348                    unstable_features,
349                    &dep_key,
350                    &dep_item,
351                );
352                (dep_key, table_path, dep)
353            })
354    }
355
356    /// Add entry to a Cargo.toml.
357    pub fn insert_into_table(
358        &mut self,
359        table_path: &[String],
360        dep: &Dependency,
361        gctx: &GlobalContext,
362        workspace_root: &Path,
363        unstable_features: &Features,
364    ) -> CargoResult<()> {
365        let crate_root = self
366            .path
367            .parent()
368            .expect("manifest path is absolute")
369            .to_owned();
370        let dep_key = dep.toml_key();
371
372        let table = self
373            .get_table_mut(table_path)
374            .expect("manifest validated, path should be to a table");
375        if let Some((mut dep_key, dep_item)) = table
376            .as_table_like_mut()
377            .unwrap()
378            .get_key_value_mut(dep_key)
379        {
380            dep.update_toml(
381                gctx,
382                workspace_root,
383                &crate_root,
384                unstable_features,
385                &mut dep_key,
386                dep_item,
387            )?;
388            if let Some(table) = dep_item.as_inline_table_mut() {
389                // So long as we don't have `Cargo.toml` auto-formatting and inline-tables can only
390                // be on one line, there isn't really much in the way of interesting formatting to
391                // include (no comments), so let's just wipe it clean
392                table.fmt();
393            }
394        } else {
395            let new_dependency =
396                dep.to_toml(gctx, workspace_root, &crate_root, unstable_features)?;
397            table[dep_key] = new_dependency;
398        }
399
400        Ok(())
401    }
402
403    /// Remove entry from a Cargo.toml.
404    pub fn remove_from_table(
405        &mut self,
406        table_path: &[String],
407        name: &str,
408    ) -> Result<(), MissingDependencyError> {
409        let parent_table = self
410            .get_table_mut(table_path)
411            .expect("manifest validated, path should be to a table");
412
413        match parent_table.get_mut(name).filter(|t| !t.is_none()) {
414            Some(dep) => {
415                // remove the dependency
416                *dep = toml_edit::Item::None;
417
418                // remove table if empty
419                if parent_table.as_table_like().unwrap().is_empty() {
420                    *parent_table = toml_edit::Item::None;
421                }
422            }
423            None => {
424                let names = parent_table
425                    .as_table_like()
426                    .map(|t| t.iter())
427                    .into_iter()
428                    .flatten();
429                let alt_name = closest(name, names.map(|(k, _)| k), |k| k).map(|n| n.to_owned());
430
431                // Search in other tables.
432                let sections = self.get_sections();
433                let found_table_path = sections.iter().find_map(|(t, i)| {
434                    let table_path: Vec<String> =
435                        t.to_table().iter().map(|s| s.to_string()).collect();
436                    i.get(name).is_some().then(|| table_path)
437                });
438
439                return Err(MissingDependencyError {
440                    expected_name: name.to_owned(),
441                    expected_path: table_path.to_owned(),
442                    alt_name: alt_name,
443                    alt_path: found_table_path,
444                });
445            }
446        }
447
448        Ok(())
449    }
450
451    /// Allow mutating dependencies, wherever they live.
452    /// Copied from cargo-edit.
453    pub fn get_dependency_tables_mut(
454        &mut self,
455    ) -> impl Iterator<Item = &mut dyn toml_edit::TableLike> + '_ {
456        let root = self.data.as_table_mut();
457        root.iter_mut().flat_map(|(k, v)| {
458            if DepTable::KINDS
459                .iter()
460                .any(|dt| dt.kind.kind_table() == k.get())
461            {
462                v.as_table_like_mut().into_iter().collect::<Vec<_>>()
463            } else if k == "workspace" {
464                v.as_table_like_mut()
465                    .unwrap()
466                    .iter_mut()
467                    .filter_map(|(k, v)| {
468                        if k.get() == "dependencies" {
469                            v.as_table_like_mut()
470                        } else {
471                            None
472                        }
473                    })
474                    .collect::<Vec<_>>()
475            } else if k == "target" {
476                v.as_table_like_mut()
477                    .unwrap()
478                    .iter_mut()
479                    .flat_map(|(_, v)| {
480                        v.as_table_like_mut().into_iter().flat_map(|v| {
481                            v.iter_mut().filter_map(|(k, v)| {
482                                if DepTable::KINDS
483                                    .iter()
484                                    .any(|dt| dt.kind.kind_table() == k.get())
485                                {
486                                    v.as_table_like_mut()
487                                } else {
488                                    None
489                                }
490                            })
491                        })
492                    })
493                    .collect::<Vec<_>>()
494            } else {
495                Vec::new()
496            }
497        })
498    }
499
500    /// Remove references to `dep_key` if its no longer present.
501    pub fn gc_dep(&mut self, dep_key: &str) {
502        let explicit_dep_activation = self.is_explicit_dep_activation(dep_key);
503        let status = self.dep_status(dep_key);
504
505        if let Some(toml_edit::Item::Table(feature_table)) =
506            self.data.as_table_mut().get_mut("features")
507        {
508            for (_feature, mut feature_values) in feature_table.iter_mut() {
509                if let toml_edit::Item::Value(toml_edit::Value::Array(feature_values)) =
510                    &mut feature_values
511                {
512                    fix_feature_activations(
513                        feature_values,
514                        dep_key,
515                        status,
516                        explicit_dep_activation,
517                    );
518                }
519            }
520        }
521    }
522
523    pub fn is_explicit_dep_activation(&self, dep_key: &str) -> bool {
524        if let Some(toml_edit::Item::Table(feature_table)) = self.data.as_table().get("features") {
525            for values in feature_table
526                .iter()
527                .map(|(_, a)| a)
528                .filter_map(|i| i.as_value())
529                .filter_map(|v| v.as_array())
530            {
531                for value in values.iter().filter_map(|v| v.as_str()) {
532                    let value = FeatureValue::new(value.into());
533                    if let FeatureValue::Dep { dep_name } = &value {
534                        if dep_name.as_str() == dep_key {
535                            return true;
536                        }
537                    }
538                }
539            }
540        }
541
542        false
543    }
544
545    fn dep_status(&self, dep_key: &str) -> DependencyStatus {
546        let mut status = DependencyStatus::None;
547        for (_, tbl) in self.get_sections() {
548            if let toml_edit::Item::Table(tbl) = tbl {
549                if let Some(dep_item) = tbl.get(dep_key) {
550                    let optional = dep_item
551                        .get("optional")
552                        .and_then(|i| i.as_value())
553                        .and_then(|i| i.as_bool())
554                        .unwrap_or(false);
555                    if optional {
556                        return DependencyStatus::Optional;
557                    } else {
558                        status = DependencyStatus::Required;
559                    }
560                }
561            }
562        }
563        status
564    }
565}
566
567impl std::fmt::Display for LocalManifest {
568    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
569        self.manifest.fmt(f)
570    }
571}
572
573/// Edit location for an embedded manifest
574#[derive(Clone, Debug)]
575pub enum Embedded {
576    /// Manifest is implicit
577    ///
578    /// This is the insert location for a frontmatter
579    Implicit(usize),
580    /// Manifest is explicit in a frontmatter
581    ///
582    /// This is the span of the frontmatter body
583    Explicit(std::ops::Range<usize>),
584}
585
586impl Embedded {
587    fn start() -> Self {
588        Self::Implicit(0)
589    }
590
591    fn after(after: std::ops::Range<usize>) -> Self {
592        Self::Implicit(after.end)
593    }
594
595    fn exists(exists: std::ops::Range<usize>) -> Self {
596        Self::Explicit(exists)
597    }
598}
599
600#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
601enum DependencyStatus {
602    None,
603    Optional,
604    Required,
605}
606
607fn fix_feature_activations(
608    feature_values: &mut toml_edit::Array,
609    dep_key: &str,
610    status: DependencyStatus,
611    explicit_dep_activation: bool,
612) {
613    let remove_list: Vec<usize> = feature_values
614        .iter()
615        .enumerate()
616        .filter_map(|(idx, value)| value.as_str().map(|s| (idx, s)))
617        .filter_map(|(idx, value)| {
618            let parsed_value = FeatureValue::new(value.into());
619            match status {
620                DependencyStatus::None => match (parsed_value, explicit_dep_activation) {
621                    (FeatureValue::Feature(dep_name), false)
622                    | (FeatureValue::Dep { dep_name }, _)
623                    | (FeatureValue::DepFeature { dep_name, .. }, _) => dep_name == dep_key,
624                    _ => false,
625                },
626                DependencyStatus::Optional => false,
627                DependencyStatus::Required => match (parsed_value, explicit_dep_activation) {
628                    (FeatureValue::Feature(dep_name), false)
629                    | (FeatureValue::Dep { dep_name }, _) => dep_name == dep_key,
630                    (FeatureValue::Feature(_), true) | (FeatureValue::DepFeature { .. }, _) => {
631                        false
632                    }
633                },
634            }
635            .then(|| idx)
636        })
637        .collect();
638
639    // Remove found idx in revers order so we don't invalidate the idx.
640    for idx in remove_list.iter().rev() {
641        remove_array_index(feature_values, *idx);
642    }
643
644    if status == DependencyStatus::Required {
645        for value in feature_values.iter_mut() {
646            let parsed_value = if let Some(value) = value.as_str() {
647                FeatureValue::new(value.into())
648            } else {
649                continue;
650            };
651            if let FeatureValue::DepFeature {
652                dep_name,
653                dep_feature,
654                weak,
655            } = parsed_value
656            {
657                if dep_name == dep_key && weak {
658                    let mut new_value = toml_edit::Value::from(format!("{dep_name}/{dep_feature}"));
659                    *new_value.decor_mut() = value.decor().clone();
660                    *value = new_value;
661                }
662            }
663        }
664    }
665}
666
667pub fn str_or_1_len_table(item: &toml_edit::Item) -> bool {
668    item.is_str() || item.as_table_like().map(|t| t.len() == 1).unwrap_or(false)
669}
670
671fn parse_manifest_err() -> anyhow::Error {
672    anyhow::format_err!("unable to parse external Cargo.toml")
673}
674
675#[derive(Debug)]
676pub struct MissingDependencyError {
677    pub expected_name: String,
678    pub expected_path: Vec<String>,
679    pub alt_path: Option<Vec<String>>,
680    pub alt_name: Option<String>,
681}
682
683impl std::fmt::Display for MissingDependencyError {
684    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685        let expected_name = &self.expected_name;
686        let expected_path = self.expected_path.join(".");
687        write!(
688            fmt,
689            "the dependency `{expected_name}` could not be found in `{expected_path}`"
690        )?;
691        if let Some(alt_path) = &self.alt_path {
692            let alt_path = alt_path.join(".");
693            write!(
694                fmt,
695                "\n\nhelp: a dependency with the same name exists in `{alt_path}`"
696            )?;
697        } else if let Some(alt_name) = &self.alt_name {
698            write!(
699                fmt,
700                "\n\nhelp: a dependency with a similar name exists: `{alt_name}`"
701            )?;
702        }
703        Ok(())
704    }
705}
706
707impl std::error::Error for MissingDependencyError {}
708
709fn remove_array_index(array: &mut toml_edit::Array, index: usize) {
710    let value = array.remove(index);
711
712    // Captures all lines before leading whitespace
713    let prefix_lines = value
714        .decor()
715        .prefix()
716        .and_then(|p| p.as_str().expect("spans removed").rsplit_once('\n'))
717        .map(|(lines, _current)| lines);
718    // Captures all lines after trailing whitespace, before the next comma
719    let suffix_lines = value
720        .decor()
721        .suffix()
722        .and_then(|p| p.as_str().expect("spans removed").split_once('\n'))
723        .map(|(_current, lines)| lines);
724    let mut merged_lines = String::new();
725    if let Some(prefix_lines) = prefix_lines {
726        merged_lines.push_str(prefix_lines);
727        merged_lines.push('\n');
728    }
729    if let Some(suffix_lines) = suffix_lines {
730        merged_lines.push_str(suffix_lines);
731        merged_lines.push('\n');
732    }
733
734    let next_index = index; // Since `index` was removed, that effectively auto-advances us
735    if let Some(next) = array.get_mut(next_index) {
736        let next_decor = next.decor_mut();
737        let next_prefix = next_decor
738            .prefix()
739            .map(|s| s.as_str().expect("spans removed"))
740            .unwrap_or_default();
741        merged_lines.push_str(next_prefix);
742        next_decor.set_prefix(merged_lines);
743    } else {
744        let trailing = array.trailing().as_str().expect("spans removed");
745        merged_lines.push_str(trailing);
746        array.set_trailing(merged_lines);
747    }
748}