blob: e859af2153a3c7862dc81184ffd322f695b89cca [file] [log] [blame]
//! Parsing and editing of manifest files.
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::str;
use anyhow::Context as _;
use super::dependency::Dependency;
use crate::core::dependency::DepKind;
use crate::core::FeatureValue;
use crate::util::interning::InternedString;
use crate::CargoResult;
/// Dependency table to add deps to.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DepTable {
kind: DepKind,
target: Option<String>,
}
impl DepTable {
const KINDS: &'static [Self] = &[
Self::new().set_kind(DepKind::Normal),
Self::new().set_kind(DepKind::Development),
Self::new().set_kind(DepKind::Build),
];
/// Reference to a Dependency Table.
pub const fn new() -> Self {
Self {
kind: DepKind::Normal,
target: None,
}
}
/// Choose the type of dependency.
pub const fn set_kind(mut self, kind: DepKind) -> Self {
self.kind = kind;
self
}
/// Choose the platform for the dependency.
pub fn set_target(mut self, target: impl Into<String>) -> Self {
self.target = Some(target.into());
self
}
/// Type of dependency.
pub fn kind(&self) -> DepKind {
self.kind
}
/// Platform for the dependency.
pub fn target(&self) -> Option<&str> {
self.target.as_deref()
}
/// Keys to the table.
pub fn to_table(&self) -> Vec<&str> {
if let Some(target) = &self.target {
vec!["target", target, self.kind.kind_table()]
} else {
vec![self.kind.kind_table()]
}
}
}
impl Default for DepTable {
fn default() -> Self {
Self::new()
}
}
impl From<DepKind> for DepTable {
fn from(other: DepKind) -> Self {
Self::new().set_kind(other)
}
}
/// An editable Cargo manifest.
#[derive(Debug, Clone)]
pub struct Manifest {
/// Manifest contents as TOML data.
pub data: toml_edit::Document,
}
impl Manifest {
/// Get the manifest's package name.
pub fn package_name(&self) -> CargoResult<&str> {
self.data
.as_table()
.get("package")
.and_then(|m| m.get("name"))
.and_then(|m| m.as_str())
.ok_or_else(parse_manifest_err)
}
/// Get the specified table from the manifest.
pub fn get_table<'a>(&'a self, table_path: &[String]) -> CargoResult<&'a toml_edit::Item> {
/// Descend into a manifest until the required table is found.
fn descend<'a>(
input: &'a toml_edit::Item,
path: &[String],
) -> CargoResult<&'a toml_edit::Item> {
if let Some(segment) = path.get(0) {
let value = input
.get(&segment)
.ok_or_else(|| non_existent_table_err(segment))?;
if value.is_table_like() {
descend(value, &path[1..])
} else {
Err(non_existent_table_err(segment))
}
} else {
Ok(input)
}
}
descend(self.data.as_item(), table_path)
}
/// Get the specified table from the manifest.
pub fn get_table_mut<'a>(
&'a mut self,
table_path: &[String],
) -> CargoResult<&'a mut toml_edit::Item> {
/// Descend into a manifest until the required table is found.
fn descend<'a>(
input: &'a mut toml_edit::Item,
path: &[String],
) -> CargoResult<&'a mut toml_edit::Item> {
if let Some(segment) = path.get(0) {
let mut default_table = toml_edit::Table::new();
default_table.set_implicit(true);
let value = input[&segment].or_insert(toml_edit::Item::Table(default_table));
if value.is_table_like() {
descend(value, &path[1..])
} else {
Err(non_existent_table_err(segment))
}
} else {
Ok(input)
}
}
descend(self.data.as_item_mut(), table_path)
}
/// Get all sections in the manifest that exist and might contain
/// dependencies. The returned items are always `Table` or
/// `InlineTable`.
pub fn get_sections(&self) -> Vec<(DepTable, toml_edit::Item)> {
let mut sections = Vec::new();
for table in DepTable::KINDS {
let dependency_type = table.kind.kind_table();
// Dependencies can be in the three standard sections...
if self
.data
.get(dependency_type)
.map(|t| t.is_table_like())
.unwrap_or(false)
{
sections.push((table.clone(), self.data[dependency_type].clone()))
}
// ... and in `target.<target>.(build-/dev-)dependencies`.
let target_sections = self
.data
.as_table()
.get("target")
.and_then(toml_edit::Item::as_table_like)
.into_iter()
.flat_map(toml_edit::TableLike::iter)
.filter_map(|(target_name, target_table)| {
let dependency_table = target_table.get(dependency_type)?;
dependency_table.as_table_like().map(|_| {
(
table.clone().set_target(target_name),
dependency_table.clone(),
)
})
});
sections.extend(target_sections);
}
sections
}
pub fn get_legacy_sections(&self) -> Vec<String> {
let mut result = Vec::new();
for dependency_type in ["dev_dependencies", "build_dependencies"] {
if self.data.contains_key(dependency_type) {
result.push(dependency_type.to_owned());
}
// ... and in `target.<target>.(build-/dev-)dependencies`.
result.extend(
self.data
.as_table()
.get("target")
.and_then(toml_edit::Item::as_table_like)
.into_iter()
.flat_map(toml_edit::TableLike::iter)
.filter_map(|(target_name, target_table)| {
if target_table.as_table_like()?.contains_key(dependency_type) {
Some(format!("target.{target_name}.{dependency_type}"))
} else {
None
}
}),
);
}
result
}
}
impl str::FromStr for Manifest {
type Err = anyhow::Error;
/// Read manifest data from string
fn from_str(input: &str) -> ::std::result::Result<Self, Self::Err> {
let d: toml_edit::Document = input.parse().context("Manifest not valid TOML")?;
Ok(Manifest { data: d })
}
}
impl std::fmt::Display for Manifest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.data.fmt(f)
}
}
/// An editable Cargo manifest that is available locally.
#[derive(Debug, Clone)]
pub struct LocalManifest {
/// Path to the manifest.
pub path: PathBuf,
/// Manifest contents.
pub manifest: Manifest,
}
impl Deref for LocalManifest {
type Target = Manifest;
fn deref(&self) -> &Manifest {
&self.manifest
}
}
impl DerefMut for LocalManifest {
fn deref_mut(&mut self) -> &mut Manifest {
&mut self.manifest
}
}
impl LocalManifest {
/// Construct the `LocalManifest` corresponding to the `Path` provided..
pub fn try_new(path: &Path) -> CargoResult<Self> {
if !path.is_absolute() {
anyhow::bail!("can only edit absolute paths, got {}", path.display());
}
let data = cargo_util::paths::read(&path)?;
let manifest = data.parse().context("Unable to parse Cargo.toml")?;
Ok(LocalManifest {
manifest,
path: path.to_owned(),
})
}
/// Write changes back to the file.
pub fn write(&self) -> CargoResult<()> {
if !self.manifest.data.contains_key("package")
&& !self.manifest.data.contains_key("project")
{
if self.manifest.data.contains_key("workspace") {
anyhow::bail!(
"found virtual manifest at {}, but this command requires running against an \
actual package in this workspace.",
self.path.display()
);
} else {
anyhow::bail!(
"missing expected `package` or `project` fields in {}",
self.path.display()
);
}
}
let s = self.manifest.data.to_string();
let new_contents_bytes = s.as_bytes();
cargo_util::paths::write_atomic(&self.path, new_contents_bytes)
}
/// Lookup a dependency.
pub fn get_dependency_versions<'s>(
&'s self,
dep_key: &'s str,
) -> impl Iterator<Item = (DepTable, CargoResult<Dependency>)> + 's {
let crate_root = self.path.parent().expect("manifest path is absolute");
self.get_sections()
.into_iter()
.filter_map(move |(table_path, table)| {
let table = table.into_table().ok()?;
Some(
table
.into_iter()
.filter_map(|(key, item)| {
if key.as_str() == dep_key {
Some((table_path.clone(), key, item))
} else {
None
}
})
.collect::<Vec<_>>(),
)
})
.flatten()
.map(move |(table_path, dep_key, dep_item)| {
let dep = Dependency::from_toml(crate_root, &dep_key, &dep_item);
(table_path, dep)
})
}
/// Add entry to a Cargo.toml.
pub fn insert_into_table(
&mut self,
table_path: &[String],
dep: &Dependency,
) -> CargoResult<()> {
let crate_root = self
.path
.parent()
.expect("manifest path is absolute")
.to_owned();
let dep_key = dep.toml_key();
let table = self.get_table_mut(table_path)?;
if let Some((mut dep_key, dep_item)) = table
.as_table_like_mut()
.unwrap()
.get_key_value_mut(dep_key)
{
dep.update_toml(&crate_root, &mut dep_key, dep_item);
if let Some(table) = dep_item.as_inline_table_mut() {
// So long as we don't have `Cargo.toml` auto-formatting and inline-tables can only
// be on one line, there isn't really much in the way of interesting formatting to
// include (no comments), so let's just wipe it clean
table.fmt();
}
} else {
let new_dependency = dep.to_toml(&crate_root);
table[dep_key] = new_dependency;
}
Ok(())
}
/// Remove entry from a Cargo.toml.
pub fn remove_from_table(&mut self, table_path: &[String], name: &str) -> CargoResult<()> {
let parent_table = self.get_table_mut(table_path)?;
match parent_table.get_mut(name).filter(|t| !t.is_none()) {
Some(dep) => {
// remove the dependency
*dep = toml_edit::Item::None;
// remove table if empty
if parent_table.as_table_like().unwrap().is_empty() {
*parent_table = toml_edit::Item::None;
}
}
None => {
// Search in other tables.
let sections = self.get_sections();
let found_table_path = sections.iter().find_map(|(t, i)| {
let table_path: Vec<String> =
t.to_table().iter().map(|s| s.to_string()).collect();
i.get(name).is_some().then(|| table_path.join("."))
});
return Err(non_existent_dependency_err(
name,
table_path.join("."),
found_table_path,
));
}
}
Ok(())
}
/// Remove references to `dep_key` if its no longer present.
pub fn gc_dep(&mut self, dep_key: &str) {
let explicit_dep_activation = self.is_explicit_dep_activation(dep_key);
let status = self.dep_status(dep_key);
if let Some(toml_edit::Item::Table(feature_table)) =
self.data.as_table_mut().get_mut("features")
{
for (_feature, mut feature_values) in feature_table.iter_mut() {
if let toml_edit::Item::Value(toml_edit::Value::Array(feature_values)) =
&mut feature_values
{
fix_feature_activations(
feature_values,
dep_key,
status,
explicit_dep_activation,
);
}
}
}
}
fn is_explicit_dep_activation(&self, dep_key: &str) -> bool {
if let Some(toml_edit::Item::Table(feature_table)) = self.data.as_table().get("features") {
for values in feature_table
.iter()
.map(|(_, a)| a)
.filter_map(|i| i.as_value())
.filter_map(|v| v.as_array())
{
for value in values.iter().filter_map(|v| v.as_str()) {
let value = FeatureValue::new(InternedString::new(value));
if let FeatureValue::Dep { dep_name } = &value {
if dep_name.as_str() == dep_key {
return true;
}
}
}
}
}
false
}
fn dep_status(&self, dep_key: &str) -> DependencyStatus {
let mut status = DependencyStatus::None;
for (_, tbl) in self.get_sections() {
if let toml_edit::Item::Table(tbl) = tbl {
if let Some(dep_item) = tbl.get(dep_key) {
let optional = dep_item
.get("optional")
.and_then(|i| i.as_value())
.and_then(|i| i.as_bool())
.unwrap_or(false);
if optional {
return DependencyStatus::Optional;
} else {
status = DependencyStatus::Required;
}
}
}
}
status
}
}
impl std::fmt::Display for LocalManifest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.manifest.fmt(f)
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum DependencyStatus {
None,
Optional,
Required,
}
fn fix_feature_activations(
feature_values: &mut toml_edit::Array,
dep_key: &str,
status: DependencyStatus,
explicit_dep_activation: bool,
) {
let remove_list: Vec<usize> = feature_values
.iter()
.enumerate()
.filter_map(|(idx, value)| value.as_str().map(|s| (idx, s)))
.filter_map(|(idx, value)| {
let parsed_value = FeatureValue::new(InternedString::new(value));
match status {
DependencyStatus::None => match (parsed_value, explicit_dep_activation) {
(FeatureValue::Feature(dep_name), false)
| (FeatureValue::Dep { dep_name }, _)
| (FeatureValue::DepFeature { dep_name, .. }, _) => dep_name == dep_key,
_ => false,
},
DependencyStatus::Optional => false,
DependencyStatus::Required => match (parsed_value, explicit_dep_activation) {
(FeatureValue::Feature(dep_name), false)
| (FeatureValue::Dep { dep_name }, _) => dep_name == dep_key,
(FeatureValue::Feature(_), true) | (FeatureValue::DepFeature { .. }, _) => {
false
}
},
}
.then(|| idx)
})
.collect();
// Remove found idx in revers order so we don't invalidate the idx.
for idx in remove_list.iter().rev() {
remove_array_index(feature_values, *idx);
}
if status == DependencyStatus::Required {
for value in feature_values.iter_mut() {
let parsed_value = if let Some(value) = value.as_str() {
FeatureValue::new(InternedString::new(value))
} else {
continue;
};
if let FeatureValue::DepFeature {
dep_name,
dep_feature,
weak,
} = parsed_value
{
if dep_name == dep_key && weak {
let mut new_value = toml_edit::Value::from(format!("{dep_name}/{dep_feature}"));
*new_value.decor_mut() = value.decor().clone();
*value = new_value;
}
}
}
}
}
pub fn str_or_1_len_table(item: &toml_edit::Item) -> bool {
item.is_str() || item.as_table_like().map(|t| t.len() == 1).unwrap_or(false)
}
fn parse_manifest_err() -> anyhow::Error {
anyhow::format_err!("unable to parse external Cargo.toml")
}
fn non_existent_table_err(table: impl std::fmt::Display) -> anyhow::Error {
anyhow::format_err!("the table `{table}` could not be found.")
}
fn non_existent_dependency_err(
name: impl std::fmt::Display,
search_table: impl std::fmt::Display,
found_table: Option<impl std::fmt::Display>,
) -> anyhow::Error {
let mut msg = format!("the dependency `{name}` could not be found in `{search_table}`");
if let Some(found_table) = found_table {
msg.push_str(&format!("; it is present in `{found_table}`",));
}
anyhow::format_err!(msg)
}
fn remove_array_index(array: &mut toml_edit::Array, index: usize) {
let value = array.remove(index);
// Captures all lines before leading whitespace
let prefix_lines = value
.decor()
.prefix()
.and_then(|p| p.as_str().expect("spans removed").rsplit_once('\n'))
.map(|(lines, _current)| lines);
// Captures all lines after trailing whitespace, before the next comma
let suffix_lines = value
.decor()
.suffix()
.and_then(|p| p.as_str().expect("spans removed").split_once('\n'))
.map(|(_current, lines)| lines);
let mut merged_lines = String::new();
if let Some(prefix_lines) = prefix_lines {
merged_lines.push_str(prefix_lines);
merged_lines.push('\n');
}
if let Some(suffix_lines) = suffix_lines {
merged_lines.push_str(suffix_lines);
merged_lines.push('\n');
}
let next_index = index; // Since `index` was removed, that effectively auto-advances us
if let Some(next) = array.get_mut(next_index) {
let next_decor = next.decor_mut();
let next_prefix = next_decor
.prefix()
.map(|s| s.as_str().expect("spans removed"))
.unwrap_or_default();
merged_lines.push_str(next_prefix);
next_decor.set_prefix(merged_lines);
} else {
let trailing = array.trailing().as_str().expect("spans removed");
merged_lines.push_str(trailing);
array.set_trailing(merged_lines);
}
}