use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use std::path::{Path, PathBuf};
const DEPENDENCY_TABLES: [&str; 3] = ["dependencies", "dev-dependencies", "build-dependencies"];
fn manifest_base_dir(config: &ResolvedCrateConfig) -> Option<PathBuf> {
config.workspace_root.clone().or_else(|| std::env::current_dir().ok())
}
pub(crate) fn apply_version_floors(files: &mut [GeneratedFile], config: &ResolvedCrateConfig) {
for file in files.iter_mut() {
if file.path.file_name().and_then(|name| name.to_str()) != Some("Cargo.toml") {
continue;
}
let path = file.path.clone();
file.content = floor_manifest(&file.content, config, &path);
}
}
pub(crate) fn floor_manifest(rendered: &str, config: &ResolvedCrateConfig, relative_path: &Path) -> String {
let Some(base_dir) = manifest_base_dir(config) else {
return rendered.to_string();
};
let Ok(existing) = std::fs::read_to_string(base_dir.join(relative_path)) else {
return rendered.to_string();
};
floor_against(rendered, &existing)
}
pub(crate) fn floor_against(rendered: &str, existing: &str) -> String {
let (Ok(mut generated_doc), Ok(existing_doc)) = (
rendered.parse::<toml_edit::DocumentMut>(),
existing.parse::<toml_edit::DocumentMut>(),
) else {
return rendered.to_string();
};
let mut raised = 0usize;
for table in DEPENDENCY_TABLES {
raised += raise_table(generated_doc.get_mut(table), existing_doc.get(table));
}
raised += raise_target_tables(&mut generated_doc, &existing_doc);
if raised == 0 {
return rendered.to_string();
}
generated_doc.to_string()
}
fn raise_target_tables(generated_doc: &mut toml_edit::DocumentMut, existing_doc: &toml_edit::DocumentMut) -> usize {
let Some(target_item) = generated_doc.get_mut("target") else {
return 0;
};
let Some(targets) = target_item.as_table_like_mut() else {
return 0;
};
let target_keys: Vec<String> = targets.iter().map(|(key, _)| key.to_string()).collect();
let existing_targets = existing_doc.get("target").and_then(|item| item.as_table_like());
let mut raised = 0usize;
for target_key in target_keys {
let existing_target = existing_targets.and_then(|tables| tables.get(&target_key));
let Some(generated_target) = targets.get_mut(&target_key) else {
continue;
};
let Some(generated_target) = generated_target.as_table_like_mut() else {
continue;
};
for table in DEPENDENCY_TABLES {
let existing_table = existing_target
.and_then(|item| item.as_table_like())
.and_then(|tables| tables.get(table));
raised += raise_table(generated_target.get_mut(table), existing_table);
}
}
raised
}
fn raise_table(generated: Option<&mut toml_edit::Item>, existing: Option<&toml_edit::Item>) -> usize {
let (Some(generated), Some(existing)) = (generated, existing) else {
return 0;
};
let (Some(generated), Some(existing)) = (generated.as_table_like_mut(), existing.as_table_like()) else {
return 0;
};
let names: Vec<String> = generated.iter().map(|(name, _)| name.to_string()).collect();
let mut raised = 0usize;
for name in names {
let Some(committed) = existing.get(&name).and_then(version_requirement) else {
continue;
};
let Some(entry) = generated.get_mut(&name) else {
continue;
};
let Some(emitted) = version_requirement(entry) else {
continue;
};
let (Some(committed_floor), Some(emitted_floor)) = (requirement_floor(&committed), requirement_floor(&emitted))
else {
continue;
};
if committed_floor <= emitted_floor {
continue;
}
if set_version_requirement(entry, &committed) {
tracing::debug!(
dependency = %name,
emitted = %emitted,
committed = %committed,
"raised an emitted dependency requirement to the one the committed manifest declares"
);
raised += 1;
}
}
raised
}
fn version_requirement(entry: &toml_edit::Item) -> Option<String> {
if let Some(requirement) = entry.as_str() {
return Some(requirement.to_string());
}
entry.as_table_like()?.get("version")?.as_str().map(str::to_string)
}
fn set_version_requirement(entry: &mut toml_edit::Item, requirement: &str) -> bool {
if entry.is_str() {
let Some(value) = entry.as_value_mut() else {
return false;
};
replace_preserving_decor(value, requirement);
return true;
}
let Some(table) = entry.as_table_like_mut() else {
return false;
};
let Some(version) = table.get_mut("version") else {
return false;
};
let Some(version) = version.as_value_mut() else {
return false;
};
replace_preserving_decor(version, requirement);
true
}
fn replace_preserving_decor(value: &mut toml_edit::Value, requirement: &str) {
let decor = value.decor().clone();
*value = toml_edit::Value::from(requirement);
*value.decor_mut() = decor;
}
fn requirement_floor(requirement: &str) -> Option<semver::Version> {
let parsed = semver::VersionReq::parse(requirement).ok()?;
let mut floor: Option<semver::Version> = None;
for comparator in &parsed.comparators {
let bounded_below = matches!(
comparator.op,
semver::Op::Exact | semver::Op::Greater | semver::Op::GreaterEq | semver::Op::Caret | semver::Op::Tilde
);
if !bounded_below {
continue;
}
let candidate = semver::Version {
major: comparator.major,
minor: comparator.minor.unwrap_or(0),
patch: comparator.patch.unwrap_or(0),
pre: comparator.pre.clone(),
build: semver::BuildMetadata::EMPTY,
};
if floor.as_ref().is_none_or(|current| candidate > *current) {
floor = Some(candidate);
}
}
floor
}
#[cfg(test)]
mod tests;