use std::{
collections::{BTreeSet, HashSet},
path::Path,
};
use anyhow::{Context as _, Result, bail, format_err};
use crate::{fs, metadata::Metadata, restore, term};
type ParseResult<T> = Result<T, &'static str>;
pub(crate) struct Manifest {
raw: String,
doc: toml_edit::DocumentMut,
pub(crate) package: Package,
}
impl Manifest {
pub(crate) fn new(path: &Path, metadata_cargo_version: u32) -> Result<Self> {
let raw = fs::read_to_string(path)?;
let doc: toml_edit::DocumentMut = raw
.parse()
.with_context(|| format!("failed to parse manifest `{}` as toml", path.display()))?;
let package = Package::from_table(&doc, metadata_cargo_version).map_err(|s| {
format_err!("failed to parse `{s}` field from manifest `{}`", path.display())
})?;
Ok(Self { raw, doc, package })
}
}
pub(crate) struct Package {
pub(crate) publish: Option<bool>,
}
impl Package {
fn from_table(doc: &toml_edit::DocumentMut, metadata_cargo_version: u32) -> ParseResult<Self> {
let package = doc.get("package").and_then(toml_edit::Item::as_table).ok_or("package")?;
Ok(Self {
publish: if metadata_cargo_version >= 39 {
None } else {
Some(match package.get("publish") {
None => true,
Some(toml_edit::Item::Value(toml_edit::Value::Boolean(b))) => *b.value(),
Some(toml_edit::Item::Value(toml_edit::Value::Array(a))) => !a.is_empty(),
Some(_) => return Err("publish"),
})
},
})
}
}
pub(crate) fn with(
metadata: &Metadata,
no_dev_deps: bool,
no_private: bool,
restore_lockfile: bool,
f: impl FnOnce() -> Result<()>,
) -> Result<()> {
let restore = restore::Manager::new();
let workspace_root = &metadata.workspace_root;
let root_manifest = &workspace_root.join("Cargo.toml");
let mut root_crate = None;
let mut private_crates = BTreeSet::new();
for &id in &metadata.workspace_members {
let package = &metadata[id];
let manifest_path = &*package.manifest_path;
let is_root = manifest_path == root_manifest;
let mut manifest = None;
let is_private = if metadata.cargo_version >= 39 {
!package.publish
} else {
let m = Manifest::new(manifest_path, metadata.cargo_version)?;
let is_private = !m.package.publish.unwrap();
manifest = Some(m);
is_private
};
if is_private && no_private {
if is_root {
bail!("--no-private is not supported yet with workspace with private root crate");
}
private_crates.insert(manifest_path);
} else if is_root && no_private {
root_crate = Some(manifest);
} else if no_dev_deps {
let manifest = match manifest {
Some(manifest) => manifest,
None => Manifest::new(manifest_path, metadata.cargo_version)?,
};
let mut doc = manifest.doc;
if term::verbose() {
info!("removing dev-dependencies from {}", manifest_path.display());
}
remove_dev_deps(&mut doc);
restore.register(manifest.raw, manifest_path);
fs::write(manifest_path, doc.to_string())?;
}
}
let has_root_crate = root_crate.is_some();
if no_private && (no_dev_deps && has_root_crate || !private_crates.is_empty()) {
let manifest_path = root_manifest;
let (mut doc, orig) = match root_crate {
Some(Some(manifest)) => (manifest.doc, manifest.raw),
_ => {
let orig = fs::read_to_string(manifest_path)?;
(
orig.parse().with_context(|| {
format!("failed to parse manifest `{}` as toml", manifest_path.display())
})?,
orig,
)
}
};
if no_dev_deps && has_root_crate {
if term::verbose() {
info!("removing dev-dependencies from {}", manifest_path.display());
}
remove_dev_deps(&mut doc);
}
if !private_crates.is_empty() {
if term::verbose() {
info!("removing private crates from {}", manifest_path.display());
}
remove_private_crates(&mut doc, workspace_root, private_crates);
}
restore.register(orig, manifest_path);
fs::write(manifest_path, doc.to_string())?;
}
if restore_lockfile {
let lockfile = &workspace_root.join("Cargo.lock");
if lockfile.exists() {
restore.register(fs::read(lockfile)?, lockfile);
}
}
f()?;
restore.restore_all();
Ok(())
}
fn remove_dev_deps(doc: &mut toml_edit::DocumentMut) {
let mut keeping_features = HashSet::new();
let mut collect_features = |table: &dyn toml_edit::TableLike| {
for key in ["dependencies", "build-dependencies"] {
if let Some(table) = table.get(key).and_then(toml_edit::Item::as_table_like) {
keeping_features.reserve(table.len());
for (name, _) in table.iter() {
keeping_features.insert(name.to_owned());
}
}
}
};
let table = doc.as_table();
collect_features(table);
if let Some(table) = table.get("target").and_then(toml_edit::Item::as_table_like) {
for (_, val) in table.iter() {
if let Some(table) = val.as_table_like() {
collect_features(table);
}
}
}
let table = doc.as_table_mut();
let mut removing_features = HashSet::new();
let mut remove_dev_deps = |table: &mut dyn toml_edit::TableLike| {
let removed = table.remove("dev-dependencies");
if let Some(table) = removed.as_ref().and_then(toml_edit::Item::as_table_like) {
for (name, _) in table.iter() {
if !keeping_features.contains(name) {
removing_features.insert(name.to_owned());
}
}
}
};
remove_dev_deps(table);
if let Some(table) = table.get_mut("target").and_then(toml_edit::Item::as_table_like_mut) {
for (_, val) in table.iter_mut() {
if let Some(table) = val.as_table_like_mut() {
remove_dev_deps(table);
}
}
}
drop(keeping_features);
if let Some(table) = table.get_mut("features").and_then(toml_edit::Item::as_table_like_mut) {
let mut indices = vec![];
for (_, val) in table.iter_mut() {
if let Some(array) = val.as_array_mut() {
for (i, v) in array.iter().enumerate() {
if let Some(v) = v.as_str() {
if let Some((name, _)) = v.split_once('/') {
if removing_features.contains(name) {
indices.push(i);
}
}
}
}
for i in indices.drain(..).rev() {
array.remove(i);
}
}
}
}
}
fn remove_private_crates(
doc: &mut toml_edit::DocumentMut,
workspace_root: &Path,
mut private_crates: BTreeSet<&Path>,
) {
let table = doc.as_table_mut();
if let Some(workspace) = table.get_mut("workspace").and_then(toml_edit::Item::as_table_like_mut)
{
if let Some(members) = workspace.get_mut("members").and_then(toml_edit::Item::as_array_mut)
{
let mut i = 0;
while i < members.len() {
if let Some(member) = members.get(i).and_then(toml_edit::Value::as_str) {
let manifest_path = workspace_root.join(member).join("Cargo.toml");
if let Some(p) = private_crates.iter().find_map(|p| {
same_file::is_same_file(p, &manifest_path)
.ok()
.and_then(|v| if v { Some(*p) } else { None })
}) {
members.remove(i);
private_crates.remove(p);
continue;
}
}
i += 1;
}
}
if private_crates.is_empty() {
return;
}
if let Some(exclude) = workspace.get_mut("exclude").and_then(toml_edit::Item::as_array_mut)
{
for private_crate in private_crates {
exclude.push(private_crate.parent().unwrap().to_str().unwrap());
}
} else {
workspace.insert(
"exclude",
toml_edit::Item::Value(toml_edit::Value::Array(
private_crates
.iter()
.map(|p| {
toml_edit::Value::String(toml_edit::Formatted::new(
p.parent().unwrap().to_str().unwrap().to_owned(),
))
})
.collect::<toml_edit::Array>(),
)),
);
}
}
}
#[cfg(test)]
mod tests {
use super::remove_dev_deps;
macro_rules! test {
($name:ident, $input:expr, $expected:expr) => {
#[test]
fn $name() {
let mut doc: toml_edit::DocumentMut = $input.parse().unwrap();
remove_dev_deps(&mut doc);
assert_eq!($expected, doc.to_string());
}
};
}
test!(
a,
"\
[package]
[dependencies]
[[example]]
[dev-dependencies.serde]
[dev-dependencies]",
"\
[package]
[dependencies]
[[example]]
"
);
test!(
b,
"\
[package]
[dependencies]
[[example]]
[dev-dependencies.serde]
[dev-dependencies]
",
"\
[package]
[dependencies]
[[example]]
"
);
test!(
c,
"\
[dev-dependencies]
foo = { features = [] }
bar = \"0.1\"
",
"\
"
);
test!(
d,
"\
[dev-dependencies.foo]
features = []
[dev-dependencies]
bar = { features = [], a = [] }
[dependencies]
bar = { features = [], a = [] }
",
"
[dependencies]
bar = { features = [], a = [] }
"
);
test!(
many_lines,
"\
[package]\n\n
[dev-dependencies.serde]
[dev-dependencies]
",
"\
[package]
"
);
test!(
target_deps1,
"\
[package]
[target.'cfg(unix)'.dev-dependencies]
[dependencies]
",
"\
[package]
[dependencies]
"
);
test!(
target_deps2,
"\
[package]
[target.'cfg(unix)'.dev-dependencies]
foo = \"0.1\"
[target.'cfg(unix)'.dev-dependencies.bar]
[dev-dependencies]
foo = \"0.1\"
[target.'cfg(unix)'.dependencies]
foo = \"0.1\"
",
"\
[package]
[target.'cfg(unix)'.dependencies]
foo = \"0.1\"
"
);
test!(
target_deps3,
"\
[package]
[target.'cfg(unix)'.dependencies]
[dev-dependencies]
",
"\
[package]
[target.'cfg(unix)'.dependencies]
"
);
test!(
target_deps4,
"\
[package]
[target.'cfg(unix)'.dev-dependencies]
",
"\
[package]
"
);
test!(
not_table_multi_line,
"\
[package]
foo = [
['dev-dependencies'],
[\"dev-dependencies\"]
]
",
"\
[package]
foo = [
['dev-dependencies'],
[\"dev-dependencies\"]
]
"
);
test!(
dep_future,
r#"
[features]
f1 = ["d1/f", "d2/f", "d4/f"]
f2 = ["d3/f", "d2/f", "d1/f"]
f3 = ["d2/f"]
[dependencies]
d1 = "1"
[target.'cfg(unix)'.dependencies]
d3 = "1"
[dev-dependencies]
d1 = "1"
d2 = "1"
[target.'cfg(unix)'.dev-dependencies]
d4 = "1"
"#,
r#"
[features]
f1 = ["d1/f"]
f2 = ["d3/f", "d1/f"]
f3 = []
[dependencies]
d1 = "1"
[target.'cfg(unix)'.dependencies]
d3 = "1"
"#
);
}