use crate::cargo::manifest_analyzer::DepKind;
use crate::cargo::manifest_ops;
use crate::cargo::unify_types::{TransitivePin, UnifiedDep};
use crate::error::{RailError, RailResult, ResultExt};
use crate::toml::format::TomlFormatter;
use std::path::Path;
#[derive(Debug)]
pub struct ManifestWriter {
formatter: TomlFormatter,
}
impl Default for ManifestWriter {
fn default() -> Self {
Self::new()
}
}
impl ManifestWriter {
pub fn new() -> Self {
Self {
formatter: TomlFormatter::new(),
}
}
pub fn write_workspace_deps(&self, workspace_toml_path: &Path, deps: &[UnifiedDep]) -> RailResult<()> {
let mut doc = manifest_ops::read_toml_file(workspace_toml_path)?;
manifest_ops::ensure_section(&mut doc, "workspace").context("Failed to create [workspace] section")?;
let deps_table = manifest_ops::get_or_create_table(&mut doc, "workspace.dependencies")
.context("Failed to create [workspace.dependencies]")?;
for dep in deps {
let entry = manifest_ops::build_dep_entry(dep);
manifest_ops::insert_dependency(deps_table, &dep.name, entry).context("Failed to insert dependency")?;
}
self.formatter.format_manifest(&mut doc)?;
manifest_ops::write_toml_file(workspace_toml_path, &doc)?;
Ok(())
}
pub fn update_member<S: AsRef<str>>(
&self,
member_toml_path: &Path,
dep_name: &str,
dep_kind: DepKind,
target: Option<&str>,
local_features: Option<&[S]>,
is_optional: bool,
) -> RailResult<()> {
let mut doc = manifest_ops::read_toml_file(member_toml_path)?;
let kind_section = self.dep_kind_to_section(dep_kind);
let entry = manifest_ops::build_workspace_dep_entry(local_features, is_optional);
if let Some(target_cfg) = target {
manifest_ops::insert_target_dependency(&mut doc, target_cfg, kind_section, dep_name, entry)
.context("Failed to insert target-specific workspace dependency")?;
} else {
let deps = manifest_ops::get_or_create_table(&mut doc, kind_section)
.context("Failed to get dependencies section")?;
manifest_ops::insert_dependency(deps, dep_name, entry).context("Failed to insert workspace dependency")?;
}
self.formatter.format_manifest(&mut doc)?;
manifest_ops::write_toml_file(member_toml_path, &doc)?;
Ok(())
}
pub fn add_transitive_pins(&self, host_toml_path: &Path, transitives: &[TransitivePin]) -> RailResult<()> {
let mut doc = manifest_ops::read_toml_file(host_toml_path)?;
let dev_deps = manifest_ops::get_or_create_table(&mut doc, "dev-dependencies")
.context("Failed to create [dev-dependencies]")?;
for pin in transitives {
let entry = manifest_ops::build_transitive_entry(&pin.features);
manifest_ops::insert_dependency(dev_deps, &pin.name, entry)
.context("Failed to insert transitive dependency")?;
}
self.formatter.format_manifest(&mut doc)?;
manifest_ops::write_toml_file(host_toml_path, &doc)?;
Ok(())
}
pub fn write_transitive_workspace_deps(
&self,
workspace_toml_path: &Path,
transitives: &[TransitivePin],
) -> RailResult<()> {
let mut doc = manifest_ops::read_toml_file(workspace_toml_path)?;
manifest_ops::ensure_section(&mut doc, "workspace").context("Failed to create [workspace] section")?;
let deps_table = manifest_ops::get_or_create_table(&mut doc, "workspace.dependencies")
.context("Failed to create [workspace.dependencies]")?;
for pin in transitives {
let entry = manifest_ops::build_versioned_dep_entry(&pin.version, &pin.features);
manifest_ops::insert_dependency(deps_table, &pin.name, entry)
.context("Failed to insert transitive to workspace.dependencies")?;
}
self.formatter.format_manifest(&mut doc)?;
manifest_ops::write_toml_file(workspace_toml_path, &doc)?;
Ok(())
}
fn dep_kind_to_section(&self, dep_kind: DepKind) -> &'static str {
manifest_ops::dep_kind_to_section(dep_kind)
}
pub fn write_workspace_msrv(&self, workspace_toml_path: &Path, msrv: &semver::Version) -> RailResult<()> {
let mut doc = manifest_ops::read_toml_file(workspace_toml_path)?;
manifest_ops::ensure_section(&mut doc, "workspace").context("Failed to create [workspace] section")?;
let ws_package = manifest_ops::get_or_create_table(&mut doc, "workspace.package")
.context("Failed to create [workspace.package]")?;
let msrv_str = format!("{}.{}.{}", msrv.major, msrv.minor, msrv.patch);
ws_package.insert("rust-version", toml_edit::value(&msrv_str));
self.formatter.format_manifest(&mut doc)?;
manifest_ops::write_toml_file(workspace_toml_path, &doc)?;
Ok(())
}
pub fn enforce_member_msrv_inheritance(&self, member_toml_path: &Path) -> RailResult<()> {
let mut doc = manifest_ops::read_toml_file(member_toml_path)?;
let Some(pkg) = doc.get_mut("package").and_then(|p| p.as_table_like_mut()) else {
return Ok(());
};
let mut tbl = toml_edit::InlineTable::new();
tbl.insert("workspace", true.into());
pkg.insert("rust-version", toml_edit::value(toml_edit::Value::InlineTable(tbl)));
self.formatter.format_manifest(&mut doc)?;
manifest_ops::write_toml_file(member_toml_path, &doc)?;
Ok(())
}
pub fn inherit_member_package_field(&self, member_toml_path: &Path, field: &str) -> RailResult<()> {
if !crate::cargo::manifest_analyzer::package_field_is_automatically_inheritable(field) {
return Err(RailError::message(format!(
"package field '{field}' is not authorized for automatic workspace inheritance"
)));
}
let mut doc = manifest_ops::read_toml_file(member_toml_path)?;
let package = doc
.get_mut("package")
.and_then(|item| item.as_table_like_mut())
.ok_or_else(|| RailError::message(format!("'{}' has no [package] table", member_toml_path.display())))?;
let decor = package
.get(field)
.and_then(toml_edit::Item::as_value)
.map(|value| value.decor().clone())
.ok_or_else(|| {
RailError::message(format!(
"package field '{field}' disappeared from '{}' before mutation",
member_toml_path.display()
))
})?;
let mut table = toml_edit::InlineTable::new();
table.insert("workspace", true.into());
let mut value = toml_edit::Value::InlineTable(table);
*value.decor_mut() = decor;
package.insert(field, toml_edit::value(value));
self.formatter.format_manifest(&mut doc)?;
manifest_ops::write_toml_file(member_toml_path, &doc)
}
pub fn remove_dep(
&self,
member_toml_path: &Path,
dep_name: &str,
dep_kind: DepKind,
target: Option<&str>,
) -> RailResult<()> {
let mut doc = manifest_ops::read_toml_file(member_toml_path)?;
let kind_section = self.dep_kind_to_section(dep_kind);
if let Some(target_cfg) = target {
manifest_ops::remove_target_dependency(&mut doc, target_cfg, kind_section, dep_name)
.context("Failed to remove target-specific dependency")?;
} else {
if let Some(deps) = doc.get_mut(kind_section).and_then(|d| d.as_table_like_mut()) {
deps.remove(dep_name);
}
}
self.formatter.format_manifest(&mut doc)?;
manifest_ops::write_toml_file(member_toml_path, &doc)?;
Ok(())
}
pub fn remove_feature(&self, member_toml_path: &Path, feature_name: &str) -> RailResult<()> {
let mut doc = manifest_ops::read_toml_file(member_toml_path)?;
if let Some(features) = doc.get_mut("features").and_then(|f| f.as_table_like_mut()) {
features.remove(feature_name);
}
self.formatter.format_manifest(&mut doc)?;
manifest_ops::write_toml_file(member_toml_path, &doc)?;
Ok(())
}
pub fn add_features<S: AsRef<str>>(
&self,
member_toml_path: &Path,
dep_name: &str,
dep_kind: DepKind,
target: Option<&str>,
features_to_add: &[S],
) -> RailResult<()> {
let mut doc = manifest_ops::read_toml_file(member_toml_path)?;
let kind_section = self.dep_kind_to_section(dep_kind);
let dep_item = if let Some(target_cfg) = target {
doc.get_mut("target")
.and_then(|item| item.get_mut(target_cfg))
.and_then(|item| item.get_mut(kind_section))
.and_then(|item| item.get_mut(dep_name))
} else {
doc.get_mut(kind_section)
.and_then(|t| t.as_table_mut())
.and_then(|t| t.get_mut(dep_name))
};
let Some(dep_item) = dep_item else {
return Ok(());
};
let mut existing_features: Vec<String> = manifest_ops::extract_features(dep_item).unwrap_or_default();
for feature in features_to_add {
let feat_str = feature.as_ref();
if !existing_features.iter().any(|f| f == feat_str) {
existing_features.push(feat_str.to_string());
}
}
existing_features.sort();
if let Some(version) = dep_item.as_str() {
let mut inline_table = toml_edit::InlineTable::new();
inline_table.insert("version", toml_edit::Value::from(version));
inline_table.insert("features", manifest_ops::build_feature_array(&existing_features));
*dep_item = toml_edit::Item::Value(toml_edit::Value::InlineTable(inline_table));
} else {
manifest_ops::set_features(dep_item, &existing_features)?;
}
self.formatter.format_manifest(&mut doc)?;
manifest_ops::write_toml_file(member_toml_path, &doc)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_features_uses_literal_target_key_and_preserves_other_dependencies() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("Cargo.toml");
std::fs::write(
&path,
r#"[target.'thumbv8m.main-none-eabi'.dependencies]
serde = "1.0"
keep = "2.0"
[target.'cfg(unix)'.dependencies]
serde = "3.0"
"#,
)
.unwrap();
ManifestWriter::new()
.add_features(
&path,
"serde",
DepKind::Normal,
Some("thumbv8m.main-none-eabi"),
&["derive"],
)
.unwrap();
let doc: toml_edit::DocumentMut = std::fs::read_to_string(&path).unwrap().parse().unwrap();
let deps = &doc["target"]["thumbv8m.main-none-eabi"]["dependencies"];
assert_eq!(deps["serde"]["version"].as_str(), Some("1.0"));
assert_eq!(
deps["serde"]["features"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect::<Vec<_>>(),
["derive"]
);
assert_eq!(deps["keep"].as_str(), Some("2.0"));
assert_eq!(
doc["target"]["cfg(unix)"]["dependencies"]["serde"].as_str(),
Some("3.0")
);
assert_eq!(doc["target"].as_table().unwrap().len(), 2);
}
}