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    pub fn ensure_edition(&mut self) -> bool {
357        if self.embedded.is_none() {
358            return false;
359        }
360
361        let root = self.data.as_table_mut();
362        let package = root.entry("package").or_insert_with(|| {
363            let mut t = toml_edit::Table::new();
364            t.set_position(Some(-1));
365            t.into()
366        });
367        let Some(package) = package.as_table_like_mut() else {
368            return false;
369        };
370
371        let mut changed = false;
372        package.entry("edition").or_insert_with(|| {
373            changed = true;
374            crate::core::features::Edition::LATEST_STABLE
375                .to_string()
376                .into()
377        });
378
379        changed
380    }
381
382    /// Add entry to a Cargo.toml.
383    pub fn insert_into_table(
384        &mut self,
385        table_path: &[String],
386        dep: &Dependency,
387        gctx: &GlobalContext,
388        workspace_root: &Path,
389        unstable_features: &Features,
390    ) -> CargoResult<()> {
391        let crate_root = self
392            .path
393            .parent()
394            .expect("manifest path is absolute")
395            .to_owned();
396        let dep_key = dep.toml_key();
397
398        let table = self
399            .get_table_mut(table_path)
400            .expect("manifest validated, path should be to a table");
401        if let Some((mut dep_key, dep_item)) = table
402            .as_table_like_mut()
403            .unwrap()
404            .get_key_value_mut(dep_key)
405        {
406            dep.update_toml(
407                gctx,
408                workspace_root,
409                &crate_root,
410                unstable_features,
411                &mut dep_key,
412                dep_item,
413            )?;
414            if let Some(table) = dep_item.as_inline_table_mut() {
415                // So long as we don't have `Cargo.toml` auto-formatting and inline-tables can only
416                // be on one line, there isn't really much in the way of interesting formatting to
417                // include (no comments), so let's just wipe it clean
418                table.fmt();
419            }
420        } else {
421            let new_dependency =
422                dep.to_toml(gctx, workspace_root, &crate_root, unstable_features)?;
423            table[dep_key] = new_dependency;
424        }
425
426        Ok(())
427    }
428
429    /// Remove entry from a Cargo.toml.
430    pub fn remove_from_table(
431        &mut self,
432        table_path: &[String],
433        name: &str,
434    ) -> Result<(), MissingDependencyError> {
435        let parent_table = self
436            .get_table_mut(table_path)
437            .expect("manifest validated, path should be to a table");
438
439        match parent_table.get_mut(name).filter(|t| !t.is_none()) {
440            Some(dep) => {
441                // remove the dependency
442                *dep = toml_edit::Item::None;
443
444                // remove table if empty
445                if parent_table.as_table_like().unwrap().is_empty() {
446                    *parent_table = toml_edit::Item::None;
447                }
448            }
449            None => {
450                let names = parent_table
451                    .as_table_like()
452                    .map(|t| t.iter())
453                    .into_iter()
454                    .flatten();
455                let alt_name = closest(name, names.map(|(k, _)| k), |k| k).map(|n| n.to_owned());
456
457                // Search in other tables.
458                let sections = self.get_sections();
459                let found_table_path = sections.iter().find_map(|(t, i)| {
460                    let table_path: Vec<String> =
461                        t.to_table().iter().map(|s| s.to_string()).collect();
462                    i.get(name).is_some().then(|| table_path)
463                });
464
465                return Err(MissingDependencyError {
466                    expected_name: name.to_owned(),
467                    expected_path: table_path.to_owned(),
468                    alt_name: alt_name,
469                    alt_path: found_table_path,
470                });
471            }
472        }
473
474        Ok(())
475    }
476
477    /// Allow mutating dependencies, wherever they live.
478    /// Copied from cargo-edit.
479    pub fn get_dependency_tables_mut(
480        &mut self,
481    ) -> impl Iterator<Item = &mut dyn toml_edit::TableLike> + '_ {
482        let root = self.data.as_table_mut();
483        root.iter_mut().flat_map(|(k, v)| {
484            if DepTable::KINDS
485                .iter()
486                .any(|dt| dt.kind.kind_table() == k.get())
487            {
488                v.as_table_like_mut().into_iter().collect::<Vec<_>>()
489            } else if k == "workspace" {
490                v.as_table_like_mut()
491                    .unwrap()
492                    .iter_mut()
493                    .filter_map(|(k, v)| {
494                        if k.get() == "dependencies" {
495                            v.as_table_like_mut()
496                        } else {
497                            None
498                        }
499                    })
500                    .collect::<Vec<_>>()
501            } else if k == "target" {
502                v.as_table_like_mut()
503                    .unwrap()
504                    .iter_mut()
505                    .flat_map(|(_, v)| {
506                        v.as_table_like_mut().into_iter().flat_map(|v| {
507                            v.iter_mut().filter_map(|(k, v)| {
508                                if DepTable::KINDS
509                                    .iter()
510                                    .any(|dt| dt.kind.kind_table() == k.get())
511                                {
512                                    v.as_table_like_mut()
513                                } else {
514                                    None
515                                }
516                            })
517                        })
518                    })
519                    .collect::<Vec<_>>()
520            } else {
521                Vec::new()
522            }
523        })
524    }
525
526    /// Remove references to `dep_key` if its no longer present.
527    pub fn gc_dep(&mut self, dep_key: &str) {
528        let explicit_dep_activation = self.is_explicit_dep_activation(dep_key);
529        let status = self.dep_status(dep_key);
530
531        if let Some(toml_edit::Item::Table(feature_table)) =
532            self.data.as_table_mut().get_mut("features")
533        {
534            for (_feature, mut feature_values) in feature_table.iter_mut() {
535                if let toml_edit::Item::Value(toml_edit::Value::Array(feature_values)) =
536                    &mut feature_values
537                {
538                    fix_feature_activations(
539                        feature_values,
540                        dep_key,
541                        status,
542                        explicit_dep_activation,
543                    );
544                }
545            }
546        }
547    }
548
549    pub fn is_explicit_dep_activation(&self, dep_key: &str) -> bool {
550        if let Some(toml_edit::Item::Table(feature_table)) = self.data.as_table().get("features") {
551            for values in feature_table
552                .iter()
553                .map(|(_, a)| a)
554                .filter_map(|i| i.as_value())
555                .filter_map(|v| v.as_array())
556            {
557                for value in values.iter().filter_map(|v| v.as_str()) {
558                    let value = FeatureValue::new(value.into());
559                    if let FeatureValue::Dep { dep_name } = &value {
560                        if dep_name.as_str() == dep_key {
561                            return true;
562                        }
563                    }
564                }
565            }
566        }
567
568        false
569    }
570
571    fn dep_status(&self, dep_key: &str) -> DependencyStatus {
572        let mut status = DependencyStatus::None;
573        for (_, tbl) in self.get_sections() {
574            if let toml_edit::Item::Table(tbl) = tbl {
575                if let Some(dep_item) = tbl.get(dep_key) {
576                    let optional = dep_item
577                        .get("optional")
578                        .and_then(|i| i.as_value())
579                        .and_then(|i| i.as_bool())
580                        .unwrap_or(false);
581                    if optional {
582                        return DependencyStatus::Optional;
583                    } else {
584                        status = DependencyStatus::Required;
585                    }
586                }
587            }
588        }
589        status
590    }
591}
592
593impl std::fmt::Display for LocalManifest {
594    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
595        self.manifest.fmt(f)
596    }
597}
598
599/// Edit location for an embedded manifest
600#[derive(Clone, Debug)]
601pub enum Embedded {
602    /// Manifest is implicit
603    ///
604    /// This is the insert location for a frontmatter
605    Implicit(usize),
606    /// Manifest is explicit in a frontmatter
607    ///
608    /// This is the span of the frontmatter body
609    Explicit(std::ops::Range<usize>),
610}
611
612impl Embedded {
613    fn start() -> Self {
614        Self::Implicit(0)
615    }
616
617    fn after(after: std::ops::Range<usize>) -> Self {
618        Self::Implicit(after.end)
619    }
620
621    fn exists(exists: std::ops::Range<usize>) -> Self {
622        Self::Explicit(exists)
623    }
624}
625
626#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
627enum DependencyStatus {
628    None,
629    Optional,
630    Required,
631}
632
633fn fix_feature_activations(
634    feature_values: &mut toml_edit::Array,
635    dep_key: &str,
636    status: DependencyStatus,
637    explicit_dep_activation: bool,
638) {
639    let remove_list: Vec<usize> = feature_values
640        .iter()
641        .enumerate()
642        .filter_map(|(idx, value)| value.as_str().map(|s| (idx, s)))
643        .filter_map(|(idx, value)| {
644            let parsed_value = FeatureValue::new(value.into());
645            match status {
646                DependencyStatus::None => match (parsed_value, explicit_dep_activation) {
647                    (FeatureValue::Feature(dep_name), false)
648                    | (FeatureValue::Dep { dep_name }, _)
649                    | (FeatureValue::DepFeature { dep_name, .. }, _) => dep_name == dep_key,
650                    _ => false,
651                },
652                DependencyStatus::Optional => false,
653                DependencyStatus::Required => match (parsed_value, explicit_dep_activation) {
654                    (FeatureValue::Feature(dep_name), false)
655                    | (FeatureValue::Dep { dep_name }, _) => dep_name == dep_key,
656                    (FeatureValue::Feature(_), true) | (FeatureValue::DepFeature { .. }, _) => {
657                        false
658                    }
659                },
660            }
661            .then(|| idx)
662        })
663        .collect();
664
665    // Remove found idx in revers order so we don't invalidate the idx.
666    for idx in remove_list.iter().rev() {
667        remove_array_index(feature_values, *idx);
668    }
669
670    if status == DependencyStatus::Required {
671        for value in feature_values.iter_mut() {
672            let parsed_value = if let Some(value) = value.as_str() {
673                FeatureValue::new(value.into())
674            } else {
675                continue;
676            };
677            if let FeatureValue::DepFeature {
678                dep_name,
679                dep_feature,
680                weak,
681            } = parsed_value
682            {
683                if dep_name == dep_key && weak {
684                    let mut new_value = toml_edit::Value::from(format!("{dep_name}/{dep_feature}"));
685                    *new_value.decor_mut() = value.decor().clone();
686                    *value = new_value;
687                }
688            }
689        }
690    }
691}
692
693pub fn str_or_1_len_table(item: &toml_edit::Item) -> bool {
694    item.is_str() || item.as_table_like().map(|t| t.len() == 1).unwrap_or(false)
695}
696
697fn parse_manifest_err() -> anyhow::Error {
698    anyhow::format_err!("unable to parse external Cargo.toml")
699}
700
701#[derive(Debug)]
702pub struct MissingDependencyError {
703    pub expected_name: String,
704    pub expected_path: Vec<String>,
705    pub alt_path: Option<Vec<String>>,
706    pub alt_name: Option<String>,
707}
708
709impl std::fmt::Display for MissingDependencyError {
710    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
711        let expected_name = &self.expected_name;
712        let expected_path = self.expected_path.join(".");
713        write!(
714            fmt,
715            "the dependency `{expected_name}` could not be found in `{expected_path}`"
716        )?;
717        if let Some(alt_path) = &self.alt_path {
718            let alt_path = alt_path.join(".");
719            write!(
720                fmt,
721                "\n\nhelp: a dependency with the same name exists in `{alt_path}`"
722            )?;
723        } else if let Some(alt_name) = &self.alt_name {
724            write!(
725                fmt,
726                "\n\nhelp: a dependency with a similar name exists: `{alt_name}`"
727            )?;
728        }
729        Ok(())
730    }
731}
732
733impl std::error::Error for MissingDependencyError {}
734
735fn remove_array_index(array: &mut toml_edit::Array, index: usize) {
736    let value = array.remove(index);
737
738    // Captures all lines before leading whitespace
739    let prefix_lines = value
740        .decor()
741        .prefix()
742        .and_then(|p| p.as_str().expect("spans removed").rsplit_once('\n'))
743        .map(|(lines, _current)| lines);
744    // Captures all lines after trailing whitespace, before the next comma
745    let suffix_lines = value
746        .decor()
747        .suffix()
748        .and_then(|p| p.as_str().expect("spans removed").split_once('\n'))
749        .map(|(_current, lines)| lines);
750    let mut merged_lines = String::new();
751    if let Some(prefix_lines) = prefix_lines {
752        merged_lines.push_str(prefix_lines);
753        merged_lines.push('\n');
754    }
755    if let Some(suffix_lines) = suffix_lines {
756        merged_lines.push_str(suffix_lines);
757        merged_lines.push('\n');
758    }
759
760    let next_index = index; // Since `index` was removed, that effectively auto-advances us
761    if let Some(next) = array.get_mut(next_index) {
762        let next_decor = next.decor_mut();
763        let next_prefix = next_decor
764            .prefix()
765            .map(|s| s.as_str().expect("spans removed"))
766            .unwrap_or_default();
767        merged_lines.push_str(next_prefix);
768        next_decor.set_prefix(merged_lines);
769    } else {
770        let trailing = array.trailing().as_str().expect("spans removed");
771        merged_lines.push_str(trailing);
772        array.set_trailing(merged_lines);
773    }
774}