use anyhow::{Context, Result, bail};
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
pub(crate) fn find_user_manifest(project_dir: &Path) -> Result<PathBuf> {
let path = project_dir.join("Cargo.toml");
if path.exists() {
Ok(path)
} else {
bail!("No Cargo.toml found in {}", project_dir.display());
}
}
pub(crate) fn find_installed_bp_names(manifest_content: &str) -> Result<Vec<String>> {
let raw: toml::Value =
toml::from_str(manifest_content).context("Failed to parse Cargo.toml")?;
let build_deps = raw
.get("build-dependencies")
.and_then(|bd| bd.as_table())
.cloned()
.unwrap_or_default();
Ok(build_deps
.keys()
.filter(|k| k.ends_with("-battery-pack") || *k == "battery-pack")
.cloned()
.collect())
}
pub(crate) fn find_workspace_manifest(crate_manifest: &Path) -> Result<Option<PathBuf>> {
let parent = crate_manifest.parent().unwrap_or(Path::new("."));
let parent = if parent.as_os_str().is_empty() {
Path::new(".")
} else {
parent
};
let crate_dir = parent
.canonicalize()
.context("Failed to resolve crate directory")?;
let mut dir = crate_dir.clone();
loop {
let candidate = dir.join("Cargo.toml");
if candidate.exists() && candidate != crate_dir.join("Cargo.toml") {
let content = std::fs::read_to_string(&candidate)?;
if content.contains("[workspace]") {
return Ok(Some(candidate));
}
}
if !dir.pop() {
break;
}
}
Ok(None)
}
pub(crate) fn dep_kind_section(kind: bphelper_manifest::DepKind) -> &'static str {
match kind {
bphelper_manifest::DepKind::Normal => "dependencies",
bphelper_manifest::DepKind::Dev => "dev-dependencies",
bphelper_manifest::DepKind::Build => "build-dependencies",
}
}
pub(crate) fn write_deps_by_kind(
doc: &mut toml_edit::DocumentMut,
crates: &BTreeMap<String, bphelper_manifest::CrateSpec>,
if_missing: bool,
) -> usize {
let mut written = 0;
for (dep_name, dep_spec) in crates {
let section = dep_kind_section(dep_spec.dep_kind);
let table = doc[section].or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
if let Some(table) = table.as_table_mut()
&& (!if_missing || !table.contains_key(dep_name))
{
add_dep_to_table(table, dep_name, dep_spec);
written += 1;
}
}
written
}
pub(crate) fn write_workspace_refs_by_kind(
doc: &mut toml_edit::DocumentMut,
crates: &BTreeMap<String, bphelper_manifest::CrateSpec>,
if_missing: bool,
) -> usize {
let mut written = 0;
for (dep_name, dep_spec) in crates {
let section = dep_kind_section(dep_spec.dep_kind);
let table = doc[section].or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
if let Some(table) = table.as_table_mut()
&& (!if_missing || !table.contains_key(dep_name))
{
let mut dep = toml_edit::InlineTable::new();
dep.insert("workspace", toml_edit::Value::from(true));
table.insert(
dep_name,
toml_edit::Item::Value(toml_edit::Value::InlineTable(dep)),
);
written += 1;
}
}
written
}
pub(crate) fn add_dep_to_table(
table: &mut toml_edit::Table,
name: &str,
spec: &bphelper_manifest::CrateSpec,
) {
if spec.features.is_empty() {
table.insert(name, toml_edit::value(&spec.version));
} else {
let mut dep = toml_edit::InlineTable::new();
dep.insert("version", toml_edit::Value::from(spec.version.as_str()));
let mut features = toml_edit::Array::new();
for feat in &spec.features {
features.push(feat.as_str());
}
dep.insert("features", toml_edit::Value::Array(features));
table.insert(
name,
toml_edit::Item::Value(toml_edit::Value::InlineTable(dep)),
);
}
}
pub(crate) fn should_upgrade_version(current: &str, recommended: &str) -> bool {
match (
semver::Version::parse(current)
.or_else(|_| semver::Version::parse(&format!("{}.0", current)))
.or_else(|_| semver::Version::parse(&format!("{}.0.0", current))),
semver::Version::parse(recommended)
.or_else(|_| semver::Version::parse(&format!("{}.0", recommended)))
.or_else(|_| semver::Version::parse(&format!("{}.0.0", recommended))),
) {
(Ok(cur), Ok(rec)) => rec > cur,
_ => current != recommended,
}
}
pub(crate) fn sync_dep_in_table(
table: &mut toml_edit::Table,
name: &str,
spec: &bphelper_manifest::CrateSpec,
) -> bool {
let Some(existing) = table.get_mut(name) else {
add_dep_to_table(table, name, spec);
return true;
};
let mut changed = false;
match existing {
toml_edit::Item::Value(toml_edit::Value::String(version_str)) => {
let current = version_str.value().to_string();
if !spec.version.is_empty() && should_upgrade_version(¤t, &spec.version) {
*version_str = toml_edit::Formatted::new(spec.version.clone());
changed = true;
}
if !spec.features.is_empty() {
let keep_version = if !spec.version.is_empty()
&& should_upgrade_version(¤t, &spec.version)
{
spec.version.clone()
} else {
current.clone()
};
let patched = bphelper_manifest::CrateSpec {
version: keep_version,
features: spec.features.clone(),
dep_kind: spec.dep_kind,
optional: spec.optional,
};
add_dep_to_table(table, name, &patched);
changed = true;
}
}
toml_edit::Item::Value(toml_edit::Value::InlineTable(inline)) => {
if let Some(toml_edit::Value::String(v)) = inline.get_mut("version")
&& !spec.version.is_empty()
&& should_upgrade_version(v.value(), &spec.version)
{
*v = toml_edit::Formatted::new(spec.version.clone());
changed = true;
}
if !spec.features.is_empty() {
let existing_features: Vec<String> = inline
.get("features")
.and_then(|f| f.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let mut needs_update = false;
let existing_set: BTreeSet<&str> =
existing_features.iter().map(|s| s.as_str()).collect();
let mut all_features = existing_features.clone();
for feat in &spec.features {
if !existing_set.contains(feat.as_str()) {
all_features.push(feat.clone());
needs_update = true;
}
}
if needs_update {
let mut arr = toml_edit::Array::new();
for f in &all_features {
arr.push(f.as_str());
}
inline.insert("features", toml_edit::Value::Array(arr));
changed = true;
}
}
}
toml_edit::Item::Table(tbl) => {
if let Some(toml_edit::Item::Value(toml_edit::Value::String(v))) =
tbl.get_mut("version")
&& !spec.version.is_empty()
&& should_upgrade_version(v.value(), &spec.version)
{
*v = toml_edit::Formatted::new(spec.version.clone());
changed = true;
}
if !spec.features.is_empty() {
let existing_features: Vec<String> = tbl
.get("features")
.and_then(|f| f.as_value())
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let existing_set: BTreeSet<&str> =
existing_features.iter().map(|s| s.as_str()).collect();
let mut all_features = existing_features.clone();
let mut needs_update = false;
for feat in &spec.features {
if !existing_set.contains(feat.as_str()) {
all_features.push(feat.clone());
needs_update = true;
}
}
if needs_update {
let mut arr = toml_edit::Array::new();
for f in &all_features {
arr.push(f.as_str());
}
tbl.insert(
"features",
toml_edit::Item::Value(toml_edit::Value::Array(arr)),
);
changed = true;
}
}
}
_ => {}
}
changed
}
pub(crate) fn read_features_at(
raw: &toml::Value,
prefix: &[&str],
bp_name: &str,
) -> BTreeSet<String> {
let mut node = Some(raw);
for key in prefix {
node = node.and_then(|n| n.get(key));
}
node.and_then(|m| m.get("battery-pack"))
.and_then(|bp| bp.get(bp_name))
.and_then(|entry| entry.get("features"))
.and_then(|sets| sets.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_else(|| BTreeSet::from(["default".to_string()]))
}
pub(crate) fn read_active_features(manifest_content: &str, bp_name: &str) -> BTreeSet<String> {
let raw: toml::Value = match toml::from_str(manifest_content) {
Ok(v) => v,
Err(_) => return BTreeSet::from(["default".to_string()]),
};
read_features_at(&raw, &["package", "metadata"], bp_name)
}
pub(crate) fn read_active_features_ws(ws_content: &str, bp_name: &str) -> BTreeSet<String> {
let raw: toml::Value = match toml::from_str(ws_content) {
Ok(v) => v,
Err(_) => return BTreeSet::from(["default".to_string()]),
};
read_features_at(&raw, &["workspace", "metadata"], bp_name)
}
#[derive(Debug, Clone)]
pub(crate) enum MetadataLocation {
Package,
Workspace { ws_manifest_path: PathBuf },
}
pub(crate) fn resolve_metadata_location(user_manifest_path: &Path) -> Result<MetadataLocation> {
if let Some(ws_path) = find_workspace_manifest(user_manifest_path)? {
let ws_content =
std::fs::read_to_string(&ws_path).context("Failed to read workspace Cargo.toml")?;
let raw: toml::Value =
toml::from_str(&ws_content).context("Failed to parse workspace Cargo.toml")?;
if raw
.get("workspace")
.and_then(|w| w.get("metadata"))
.and_then(|m| m.get("battery-pack"))
.is_some()
{
return Ok(MetadataLocation::Workspace {
ws_manifest_path: ws_path,
});
}
}
Ok(MetadataLocation::Package)
}
pub(crate) fn read_active_features_from(
location: &MetadataLocation,
user_manifest_content: &str,
bp_name: &str,
) -> BTreeSet<String> {
match location {
MetadataLocation::Package => read_active_features(user_manifest_content, bp_name),
MetadataLocation::Workspace { ws_manifest_path } => {
let ws_content = match std::fs::read_to_string(ws_manifest_path) {
Ok(c) => c,
Err(_) => return BTreeSet::from(["default".to_string()]),
};
read_active_features_ws(&ws_content, bp_name)
}
}
}
pub(crate) fn write_bp_features_to_doc(
doc: &mut toml_edit::DocumentMut,
path_prefix: &[&str],
bp_name: &str,
active_features: &BTreeSet<String>,
) {
let mut features_array = toml_edit::Array::new();
for feature in active_features {
features_array.push(feature.as_str());
}
doc[path_prefix[0]].or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
doc[path_prefix[0]][path_prefix[1]].or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
doc[path_prefix[0]][path_prefix[1]]["battery-pack"]
.or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
let mut inline = toml_edit::InlineTable::new();
inline.insert("features", toml_edit::Value::Array(features_array));
let bp_table = doc[path_prefix[0]][path_prefix[1]]["battery-pack"]
.as_table_mut()
.expect("battery-pack table must exist");
bp_table.insert(
bp_name,
toml_edit::Item::Value(toml_edit::Value::InlineTable(inline)),
);
}
pub(crate) fn resolve_battery_pack_manifest(bp_name: &str) -> Result<PathBuf> {
let metadata = cargo_metadata::MetadataCommand::new()
.exec()
.context("Failed to run `cargo metadata`")?;
let package = metadata
.packages
.iter()
.find(|p| p.name == bp_name)
.ok_or_else(|| {
anyhow::anyhow!(
"Battery pack '{}' not found in dependency graph. Is it in [build-dependencies]?",
bp_name
)
})?;
Ok(package.manifest_path.clone().into())
}
#[cfg(test)]
mod tests;