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_exempting(rendered, &existing, &explicitly_pinned_crates(config))
}
fn explicitly_pinned_crates(config: &ResolvedCrateConfig) -> std::collections::HashSet<String> {
crate::core::managed_versions::MANAGED_LANGUAGES
.iter()
.flat_map(|language| config.dependency_version_overrides(*language).keys())
.filter_map(|key| key.strip_prefix("cargo:"))
.map(str::to_string)
.collect()
}
#[cfg(test)]
pub(crate) fn floor_against(rendered: &str, existing: &str) -> String {
floor_against_exempting(rendered, existing, &std::collections::HashSet::new())
}
pub(crate) fn floor_against_exempting(
rendered: &str,
existing: &str,
exempt: &std::collections::HashSet<String>,
) -> 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), exempt);
}
raised += raise_target_tables(&mut generated_doc, &existing_doc, exempt);
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,
exempt: &std::collections::HashSet<String>,
) -> 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, exempt);
}
}
raised
}
fn raise_table(
generated: Option<&mut toml_edit::Item>,
existing: Option<&toml_edit::Item>,
exempt: &std::collections::HashSet<String>,
) -> 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 {
if exempt.contains(&name) {
tracing::debug!(
dependency = %name,
"leaving an emitted dependency requirement alone: it is pinned explicitly in \
[crates.<lang>.dependency_versions]"
);
continue;
}
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;