use std::collections::{BTreeMap, BTreeSet};
use std::convert::Infallible;
use pubgrub::{
Dependencies, DependencyConstraints, DependencyProvider, PackageResolutionStatistics, Ranges,
};
use semver::Version;
use crate::error::PkgError;
use crate::version::{parse_version, parse_version_req, req_to_pubgrub_range};
pub type SemverRanges = Ranges<Version>;
pub type ResolvedDeps = BTreeMap<String, Version>;
pub type UnifiedFeatures = BTreeMap<String, BTreeSet<String>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DepVisibility {
Public,
Private,
}
#[derive(Debug, Clone, Default)]
pub struct PackageVersionMeta {
pub deps: BTreeMap<String, String>,
pub dep_features: BTreeMap<String, Vec<String>>,
pub supported_targets: Option<Vec<String>>,
pub available_features: BTreeMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Default)]
pub struct PackageRegistry {
packages: BTreeMap<String, BTreeMap<Version, PackageVersionMeta>>,
}
impl PackageRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(
&mut self,
name: &str,
version: &str,
deps: BTreeMap<String, String>,
) -> Result<(), PkgError> {
let meta = PackageVersionMeta {
deps,
..Default::default()
};
self.register_with_meta(name, version, meta)
}
pub fn register_with_meta(
&mut self,
name: &str,
version: &str,
meta: PackageVersionMeta,
) -> Result<(), PkgError> {
let ver = parse_version(version)?;
self.packages
.entry(name.to_string())
.or_default()
.insert(ver, meta);
Ok(())
}
#[must_use]
pub fn available_versions(&self, name: &str) -> Vec<&Version> {
self.packages
.get(name)
.map(|versions| versions.keys().collect())
.unwrap_or_default()
}
#[must_use]
pub fn has_package(&self, name: &str) -> bool {
self.packages.contains_key(name)
}
pub fn resolve(
&self,
root_name: &str,
root_version: &str,
direct_deps: &BTreeMap<String, String>,
) -> Result<ResolvedDeps, PkgError> {
self.resolve_internal(root_name, root_version, direct_deps, None)
}
pub fn resolve_for_target(
&self,
root_name: &str,
root_version: &str,
direct_deps: &BTreeMap<String, String>,
target: &str,
) -> Result<ResolvedDeps, PkgError> {
self.resolve_internal(
root_name,
root_version,
direct_deps,
Some(target.to_string()),
)
}
fn resolve_internal(
&self,
root_name: &str,
root_version: &str,
direct_deps: &BTreeMap<String, String>,
active_target: Option<String>,
) -> Result<ResolvedDeps, PkgError> {
let root_ver = parse_version(root_version)?;
let provider = ResolverProvider {
registry: self,
root_package: root_name.to_string(),
root_version: root_ver.clone(),
root_deps: direct_deps.clone(),
active_target,
};
let result = pubgrub::resolve(&provider, root_name.to_string(), root_ver).map_err(|e| {
let msg = format!("{e}");
if msg.contains("No solution") || msg.contains("conflict") {
PkgError::UnresolvableConstraints(vec![msg])
} else {
PkgError::ResolutionFailed(msg)
}
})?;
let mut resolved = BTreeMap::new();
for (pkg, ver) in result {
if pkg != root_name {
resolved.insert(pkg, ver);
}
}
Ok(resolved)
}
#[must_use]
pub fn unify_features(
&self,
root_dep_features: &BTreeMap<String, Vec<String>>,
resolved: &ResolvedDeps,
) -> UnifiedFeatures {
let mut unified: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for (name, feats) in root_dep_features {
if resolved.contains_key(name) && !feats.is_empty() {
unified
.entry(name.clone())
.or_default()
.extend(feats.iter().cloned());
}
}
for (pkg_name, ver) in resolved {
if let Some(versions) = self.packages.get(pkg_name) {
if let Some(meta) = versions.get(ver) {
for (dep_name, feats) in &meta.dep_features {
if resolved.contains_key(dep_name) && !feats.is_empty() {
unified
.entry(dep_name.clone())
.or_default()
.extend(feats.iter().cloned());
}
}
}
}
}
unified
}
}
#[must_use]
pub fn compute_visibility(
direct_dep_names: &BTreeMap<String, String>,
resolved: &ResolvedDeps,
) -> BTreeMap<String, DepVisibility> {
resolved
.keys()
.map(|name| {
let vis = if direct_dep_names.contains_key(name) {
DepVisibility::Public
} else {
DepVisibility::Private
};
(name.clone(), vis)
})
.collect()
}
struct ResolverProvider<'a> {
registry: &'a PackageRegistry,
root_package: String,
root_version: Version,
root_deps: BTreeMap<String, String>,
active_target: Option<String>,
}
impl<'a> DependencyProvider for ResolverProvider<'a> {
type P = String;
type V = Version;
type VS = Ranges<Version>;
type M = String;
type Err = Infallible;
type Priority = u32;
fn prioritize(
&self,
package: &String,
_range: &Ranges<Version>,
_stats: &PackageResolutionStatistics,
) -> Self::Priority {
if package == &self.root_package {
return u32::MAX;
}
let count = self
.registry
.packages
.get(package)
.map(|v| v.len())
.unwrap_or(0);
u32::MAX.saturating_sub(count as u32)
}
fn choose_version(
&self,
package: &String,
range: &Ranges<Version>,
) -> Result<Option<Version>, Infallible> {
if package == &self.root_package {
if range.contains(&self.root_version) {
return Ok(Some(self.root_version.clone()));
}
return Ok(None);
}
let version = self.registry.packages.get(package).and_then(|versions| {
versions
.keys()
.rev()
.find(|v| {
if !range.contains(v) {
return false;
}
if let Some(target) = &self.active_target {
if let Some(meta) = versions.get(v) {
if let Some(supported) = &meta.supported_targets {
return supported.iter().any(|t| t == target);
}
}
}
true
})
.cloned()
});
Ok(version)
}
fn get_dependencies(
&self,
package: &String,
version: &Version,
) -> Result<Dependencies<String, Ranges<Version>, String>, Infallible> {
if package == &self.root_package && version == &self.root_version {
let deps: DependencyConstraints<String, Ranges<Version>> = self
.root_deps
.iter()
.filter_map(|(name, req_str)| {
parse_version_req(req_str)
.ok()
.map(|req| (name.clone(), req_to_pubgrub_range(&req)))
})
.collect();
return Ok(Dependencies::Available(deps));
}
let Some(versions) = self.registry.packages.get(package) else {
return Ok(Dependencies::Unavailable(format!(
"package '{package}' not found in registry"
)));
};
let Some(meta) = versions.get(version) else {
return Ok(Dependencies::Unavailable(format!(
"version {version} of '{package}' not found"
)));
};
let deps: DependencyConstraints<String, Ranges<Version>> = meta
.deps
.iter()
.filter_map(|(name, req_str)| {
parse_version_req(req_str)
.ok()
.map(|req| (name.clone(), req_to_pubgrub_range(&req)))
})
.collect();
Ok(Dependencies::Available(deps))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_deps(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn resolve_simple_deps() {
let mut registry = PackageRegistry::new();
registry.register("foo", "1.0.0", BTreeMap::new()).unwrap();
registry.register("foo", "1.1.0", BTreeMap::new()).unwrap();
registry
.register("bar", "2.0.0", make_deps(&[("foo", "^1.0")]))
.unwrap();
let direct = make_deps(&[("bar", "^2.0")]);
let resolved = registry.resolve("my-app", "0.1.0", &direct).unwrap();
assert!(resolved.contains_key("bar"));
assert!(resolved.contains_key("foo"));
assert_eq!(resolved["bar"], Version::new(2, 0, 0));
assert_eq!(resolved["foo"], Version::new(1, 1, 0));
}
#[test]
fn resolve_transitive_deps() {
let mut registry = PackageRegistry::new();
registry.register("c", "1.0.0", BTreeMap::new()).unwrap();
registry
.register("b", "1.0.0", make_deps(&[("c", "^1.0")]))
.unwrap();
registry
.register("a", "1.0.0", make_deps(&[("b", "^1.0")]))
.unwrap();
let direct = make_deps(&[("a", "^1.0")]);
let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
assert_eq!(resolved.len(), 3);
assert!(resolved.contains_key("a"));
assert!(resolved.contains_key("b"));
assert!(resolved.contains_key("c"));
}
#[test]
fn resolve_conflicting_deps() {
let mut registry = PackageRegistry::new();
registry
.register("shared", "1.0.0", BTreeMap::new())
.unwrap();
registry
.register("a", "1.0.0", make_deps(&[("shared", "^1.0")]))
.unwrap();
registry
.register("b", "1.0.0", make_deps(&[("shared", "^2.0")]))
.unwrap();
let direct = make_deps(&[("a", "^1.0"), ("b", "^1.0")]);
let result = registry.resolve("root", "0.1.0", &direct);
assert!(result.is_err());
}
#[test]
fn resolve_empty_deps() {
let registry = PackageRegistry::new();
let direct = BTreeMap::new();
let resolved = registry.resolve("my-app", "1.0.0", &direct).unwrap();
assert!(resolved.is_empty());
}
#[test]
fn resolve_picks_highest_compatible() {
let mut registry = PackageRegistry::new();
registry.register("foo", "1.0.0", BTreeMap::new()).unwrap();
registry.register("foo", "1.2.0", BTreeMap::new()).unwrap();
registry.register("foo", "1.5.0", BTreeMap::new()).unwrap();
registry.register("foo", "2.0.0", BTreeMap::new()).unwrap();
let direct = make_deps(&[("foo", "^1.0")]);
let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
assert_eq!(resolved["foo"], Version::new(1, 5, 0));
}
#[test]
fn target_filtering_skips_incompatible_packages() {
let mut registry = PackageRegistry::new();
registry
.register_with_meta(
"foo",
"1.0.0",
PackageVersionMeta {
deps: BTreeMap::new(),
supported_targets: Some(vec!["js".into()]),
..Default::default()
},
)
.unwrap();
registry
.register_with_meta(
"foo",
"1.1.0",
PackageVersionMeta {
deps: BTreeMap::new(),
supported_targets: Some(vec!["js".into(), "rust".into()]),
..Default::default()
},
)
.unwrap();
let direct = make_deps(&[("foo", "^1.0")]);
let resolved = registry
.resolve_for_target("root", "0.1.0", &direct, "js")
.unwrap();
assert_eq!(resolved["foo"], Version::new(1, 1, 0));
let resolved = registry
.resolve_for_target("root", "0.1.0", &direct, "rust")
.unwrap();
assert_eq!(resolved["foo"], Version::new(1, 1, 0));
let result = registry.resolve_for_target("root", "0.1.0", &direct, "python");
assert!(result.is_err());
}
#[test]
fn target_filtering_no_targets_means_all() {
let mut registry = PackageRegistry::new();
registry.register("foo", "1.0.0", BTreeMap::new()).unwrap();
let direct = make_deps(&[("foo", "^1.0")]);
let resolved = registry
.resolve_for_target("root", "0.1.0", &direct, "rust")
.unwrap();
assert_eq!(resolved["foo"], Version::new(1, 0, 0));
}
#[test]
fn target_filtering_transitive() {
let mut registry = PackageRegistry::new();
registry
.register_with_meta(
"inner",
"1.0.0",
PackageVersionMeta {
deps: BTreeMap::new(),
supported_targets: Some(vec!["rust".into()]),
..Default::default()
},
)
.unwrap();
registry
.register("outer", "1.0.0", make_deps(&[("inner", "^1.0")]))
.unwrap();
let direct = make_deps(&[("outer", "^1.0")]);
let result = registry.resolve_for_target("root", "0.1.0", &direct, "js");
assert!(result.is_err());
let resolved = registry
.resolve_for_target("root", "0.1.0", &direct, "rust")
.unwrap();
assert!(resolved.contains_key("outer"));
assert!(resolved.contains_key("inner"));
}
#[test]
fn feature_unification_unions_features() {
let mut registry = PackageRegistry::new();
registry
.register_with_meta(
"shared",
"1.0.0",
PackageVersionMeta {
deps: BTreeMap::new(),
available_features: BTreeMap::from([
("json".into(), vec![]),
("xml".into(), vec![]),
("yaml".into(), vec![]),
]),
..Default::default()
},
)
.unwrap();
registry
.register_with_meta(
"a",
"1.0.0",
PackageVersionMeta {
deps: make_deps(&[("shared", "^1.0")]),
dep_features: BTreeMap::from([("shared".into(), vec!["json".into()])]),
..Default::default()
},
)
.unwrap();
registry
.register_with_meta(
"b",
"1.0.0",
PackageVersionMeta {
deps: make_deps(&[("shared", "^1.0")]),
dep_features: BTreeMap::from([(
"shared".into(),
vec!["xml".into(), "yaml".into()],
)]),
..Default::default()
},
)
.unwrap();
let direct = make_deps(&[("a", "^1.0"), ("b", "^1.0")]);
let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
let unified = registry.unify_features(&BTreeMap::new(), &resolved);
let shared_features = &unified["shared"];
assert!(shared_features.contains("json"));
assert!(shared_features.contains("xml"));
assert!(shared_features.contains("yaml"));
assert_eq!(shared_features.len(), 3);
}
#[test]
fn feature_unification_includes_root_features() {
let mut registry = PackageRegistry::new();
registry.register("foo", "1.0.0", BTreeMap::new()).unwrap();
let direct = make_deps(&[("foo", "^1.0")]);
let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
let root_features = BTreeMap::from([("foo".into(), vec!["extra".into(), "debug".into()])]);
let unified = registry.unify_features(&root_features, &resolved);
let foo_features = &unified["foo"];
assert!(foo_features.contains("extra"));
assert!(foo_features.contains("debug"));
}
#[test]
fn feature_unification_empty_when_no_features() {
let mut registry = PackageRegistry::new();
registry.register("foo", "1.0.0", BTreeMap::new()).unwrap();
let direct = make_deps(&[("foo", "^1.0")]);
let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
let unified = registry.unify_features(&BTreeMap::new(), &resolved);
assert!(unified.is_empty());
}
#[test]
fn transitive_deps_are_private() {
let mut registry = PackageRegistry::new();
registry.register("c", "1.0.0", BTreeMap::new()).unwrap();
registry
.register("b", "1.0.0", make_deps(&[("c", "^1.0")]))
.unwrap();
registry
.register("a", "1.0.0", make_deps(&[("b", "^1.0")]))
.unwrap();
let direct = make_deps(&[("a", "^1.0")]);
let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
let visibility = compute_visibility(&direct, &resolved);
assert_eq!(visibility["a"], DepVisibility::Public);
assert_eq!(visibility["b"], DepVisibility::Private);
assert_eq!(visibility["c"], DepVisibility::Private);
}
#[test]
fn direct_deps_are_public() {
let mut registry = PackageRegistry::new();
registry.register("foo", "1.0.0", BTreeMap::new()).unwrap();
registry.register("bar", "1.0.0", BTreeMap::new()).unwrap();
let direct = make_deps(&[("foo", "^1.0"), ("bar", "^1.0")]);
let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
let visibility = compute_visibility(&direct, &resolved);
assert_eq!(visibility["foo"], DepVisibility::Public);
assert_eq!(visibility["bar"], DepVisibility::Public);
}
#[test]
fn visibility_mixed_direct_and_transitive() {
let mut registry = PackageRegistry::new();
registry
.register("shared", "1.0.0", BTreeMap::new())
.unwrap();
registry
.register("lib", "1.0.0", make_deps(&[("shared", "^1.0")]))
.unwrap();
let direct = make_deps(&[("lib", "^1.0"), ("shared", "^1.0")]);
let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
let visibility = compute_visibility(&direct, &resolved);
assert_eq!(visibility["lib"], DepVisibility::Public);
assert_eq!(visibility["shared"], DepVisibility::Public);
}
}