aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core/src/dependency_resolver/pool.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart-core/src/dependency_resolver/pool.rs')
-rw-r--r--crates/mozart-core/src/dependency_resolver/pool.rs427
1 files changed, 427 insertions, 0 deletions
diff --git a/crates/mozart-core/src/dependency_resolver/pool.rs b/crates/mozart-core/src/dependency_resolver/pool.rs
new file mode 100644
index 0000000..8a63c05
--- /dev/null
+++ b/crates/mozart-core/src/dependency_resolver/pool.rs
@@ -0,0 +1,427 @@
+use indexmap::IndexMap;
+use mozart_semver::VersionConstraint;
+use std::fmt;
+
+/// Unique identifier for a package in the pool. 1-based.
+pub type PackageId = u32;
+
+/// A SAT literal. Positive = install package, negative = don't install.
+/// The absolute value is the PackageId.
+pub type Literal = i32;
+
+/// Returns the PackageId from a literal.
+#[inline]
+pub fn literal_to_package_id(literal: Literal) -> PackageId {
+ literal.unsigned_abs()
+}
+
+/// A link from a package to another package name with a version constraint.
+#[derive(Debug, Clone)]
+pub struct PoolLink {
+ /// The target package name.
+ pub target: String,
+ /// The version constraint string (e.g. "^1.0").
+ pub constraint: String,
+ /// The source package name (the one declaring this link).
+ pub source: String,
+}
+
+impl fmt::Display for PoolLink {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{} {} {}", self.source, self.target, self.constraint)
+ }
+}
+
+/// A package entry in the pool. This is the SAT solver's view of a package.
+#[derive(Debug, Clone)]
+pub struct PoolPackage {
+ /// 1-based package ID assigned by the pool.
+ pub id: PackageId,
+ /// Normalized package name (e.g. "monolog/monolog").
+ pub name: String,
+ /// Normalized version string (e.g. "1.0.0.0").
+ pub version: String,
+ /// Pretty version string (e.g. "1.0.0").
+ pub pretty_version: String,
+ /// Package requirements.
+ pub requires: Vec<PoolLink>,
+ /// Packages this replaces.
+ pub replaces: Vec<PoolLink>,
+ /// Packages this provides.
+ pub provides: Vec<PoolLink>,
+ /// Packages this conflicts with.
+ pub conflicts: Vec<PoolLink>,
+ /// Whether this is a fixed/locked package.
+ pub is_fixed: bool,
+ /// If `Some`, this package is an `AliasPackage` whose target is the
+ /// other pool entry with the given ID. Composer creates these for
+ /// `extra.branch-alias` entries (dev branch → numeric alias). When set,
+ /// the rule generator emits `PackageAlias`/`PackageInverseAlias` rules
+ /// instead of regular requires; same-name conflict rules also skip
+ /// alias packages.
+ pub is_alias_of: Option<PackageId>,
+}
+
+impl PoolPackage {
+ /// Returns all names this package is known by (own name + provides + replaces targets).
+ pub fn names(&self) -> Vec<&str> {
+ let mut names = vec![self.name.as_str()];
+ for link in &self.provides {
+ names.push(link.target.as_str());
+ }
+ for link in &self.replaces {
+ names.push(link.target.as_str());
+ }
+ names
+ }
+
+ /// Names that drive same-name conflict resolution — own name plus
+ /// `replace` targets. `provide` targets are excluded because two packages
+ /// providing different versions of the same virtual name may legitimately
+ /// coexist; `replace` declares the replacing package fully supplants the
+ /// replaced one. Mirrors Composer's `BasePackage::getNames(false)`.
+ pub fn conflict_names(&self) -> Vec<&str> {
+ let mut names = vec![self.name.as_str()];
+ for link in &self.replaces {
+ names.push(link.target.as_str());
+ }
+ names
+ }
+}
+
+impl fmt::Display for PoolPackage {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{} {}", self.name, self.pretty_version)
+ }
+}
+
+/// Input for building a Pool. Users of the crate provide these.
+#[derive(Debug, Clone)]
+pub struct PoolPackageInput {
+ pub name: String,
+ pub version: String,
+ pub pretty_version: String,
+ pub requires: Vec<PoolLink>,
+ pub replaces: Vec<PoolLink>,
+ pub provides: Vec<PoolLink>,
+ pub conflicts: Vec<PoolLink>,
+ pub is_fixed: bool,
+ /// When `Some`, the value is the **normalized** version of another input
+ /// in this build batch with the same `name`; the pool will resolve it to
+ /// that input's [`PackageId`] in [`PoolPackage::is_alias_of`]. Used by
+ /// the registry layer to materialize Composer's `AliasPackage` for
+ /// `extra.branch-alias` entries.
+ pub is_alias_of: Option<String>,
+}
+
+/// The package pool: contains all candidate packages for dependency resolution.
+/// Packages are assigned sequential 1-based IDs.
+///
+/// Port of Composer's Pool.php.
+pub struct Pool {
+ /// All packages, indexed by (id - 1).
+ packages: Vec<PoolPackage>,
+ /// Index: package name → list of package IDs providing that name.
+ package_by_name: IndexMap<String, Vec<PackageId>>,
+ /// Cache for what_provides results.
+ provider_cache: IndexMap<(String, String), Vec<PackageId>>,
+ /// Packages that are fixed/locked but unacceptable (e.g. failed stability).
+ unacceptable_fixed_packages: Vec<PackageId>,
+}
+
+impl Pool {
+ /// Create a new pool from a list of package inputs.
+ pub fn new(inputs: Vec<PoolPackageInput>, unacceptable_fixed_ids: Vec<PackageId>) -> Self {
+ let mut packages: Vec<PoolPackage> = Vec::with_capacity(inputs.len());
+ let mut package_by_name: IndexMap<String, Vec<PackageId>> = IndexMap::new();
+ // Collect alias links (alias_idx, target_name, target_normalized) for
+ // a second pass once every input has a stable ID.
+ let mut pending_aliases: Vec<(usize, String, String)> = Vec::new();
+
+ for (idx, input) in inputs.into_iter().enumerate() {
+ let id = (idx as PackageId) + 1;
+ if let Some(target) = input.is_alias_of.clone() {
+ pending_aliases.push((idx, input.name.clone(), target));
+ }
+ let pkg = PoolPackage {
+ id,
+ name: input.name,
+ version: input.version,
+ pretty_version: input.pretty_version,
+ requires: input.requires,
+ replaces: input.replaces,
+ provides: input.provides,
+ conflicts: input.conflicts,
+ is_fixed: input.is_fixed,
+ is_alias_of: None,
+ };
+
+ // Index by all names this package provides
+ for name in pkg.names() {
+ package_by_name
+ .entry(name.to_string())
+ .or_default()
+ .push(id);
+ }
+
+ packages.push(pkg);
+ }
+
+ // Resolve alias targets: for each alias input, find the matching
+ // (name, normalized version) entry and store its ID. Mirrors the
+ // post-construction wiring Composer does in
+ // `RepositorySet::createAliasPackage` / `addPackage`.
+ for (alias_idx, name, target_normalized) in pending_aliases {
+ if let Some(ids) = package_by_name.get(&name) {
+ let target_id = ids.iter().copied().find(|&id| {
+ let candidate = &packages[(id - 1) as usize];
+ !candidate.name.is_empty()
+ && candidate.name == name
+ && candidate.version == target_normalized
+ && candidate.is_alias_of.is_none()
+ });
+ if let Some(tid) = target_id {
+ packages[alias_idx].is_alias_of = Some(tid);
+ }
+ }
+ }
+
+ Pool {
+ packages,
+ package_by_name,
+ provider_cache: IndexMap::new(),
+ unacceptable_fixed_packages: unacceptable_fixed_ids,
+ }
+ }
+
+ /// Returns the number of packages in the pool.
+ pub fn len(&self) -> usize {
+ self.packages.len()
+ }
+
+ /// Returns true if the pool has no packages.
+ pub fn is_empty(&self) -> bool {
+ self.packages.is_empty()
+ }
+
+ /// Look up a package by its 1-based ID.
+ pub fn package_by_id(&self, id: PackageId) -> &PoolPackage {
+ &self.packages[(id - 1) as usize]
+ }
+
+ /// All packages in the pool.
+ pub fn packages(&self) -> &[PoolPackage] {
+ &self.packages
+ }
+
+ /// Convert a literal to its package reference.
+ pub fn literal_to_package(&self, literal: Literal) -> &PoolPackage {
+ self.package_by_id(literal_to_package_id(literal))
+ }
+
+ /// Format a literal as a human-readable string.
+ pub fn literal_to_pretty_string(&self, literal: Literal) -> String {
+ let pkg = self.literal_to_package(literal);
+ let prefix = if literal > 0 {
+ "install"
+ } else {
+ "don't install"
+ };
+ format!("{prefix} {} {}", pkg.name, pkg.pretty_version)
+ }
+
+ /// Find all packages matching a name and optional constraint.
+ /// Results are cached.
+ pub fn what_provides(&mut self, name: &str, constraint: Option<&str>) -> Vec<PackageId> {
+ let key = (name.to_string(), constraint.unwrap_or("").to_string());
+ if let Some(cached) = self.provider_cache.get(&key) {
+ return cached.clone();
+ }
+
+ let result = self.compute_what_provides(name, constraint);
+ self.provider_cache.insert(key, result.clone());
+ result
+ }
+
+ fn compute_what_provides(&self, name: &str, constraint: Option<&str>) -> Vec<PackageId> {
+ let Some(candidate_ids) = self.package_by_name.get(name) else {
+ return vec![];
+ };
+
+ let parsed_constraint = constraint.and_then(|c| VersionConstraint::parse(c).ok());
+
+ let mut matches = Vec::new();
+ for &id in candidate_ids {
+ let pkg = self.package_by_id(id);
+ if self.matches_package(pkg, name, parsed_constraint.as_ref()) {
+ matches.push(id);
+ }
+ }
+ matches
+ }
+
+ /// Check if a candidate package matches a name and optional constraint.
+ /// Handles provides and replaces.
+ fn matches_package(
+ &self,
+ candidate: &PoolPackage,
+ name: &str,
+ constraint: Option<&VersionConstraint>,
+ ) -> bool {
+ if candidate.name == name {
+ return match constraint {
+ None => true,
+ Some(vc) => {
+ // Try the normalized version first; fall back to the
+ // pretty version. Composer normalizes both sides of a
+ // constraint match to a single string form (e.g.
+ // `dev-master` → `9999999-dev`), so a query for
+ // `dev-master` matches a package whose pretty version
+ // is `dev-master` even when the pool stores its
+ // version field in a different normalized shape (e.g.
+ // the four-segment `9999999.9999999.9999999.9999999-dev`
+ // expansion Mozart uses internally for default-branch
+ // and root-alias entries). The pretty fallback bridges
+ // that gap without forcing the pool to commit to a
+ // single normalization.
+ if let Ok(v) = mozart_semver::Version::parse(&candidate.version)
+ && vc.matches(&v)
+ {
+ return true;
+ }
+ if let Ok(pv) = mozart_semver::Version::parse(&candidate.pretty_version)
+ && vc.matches(&pv)
+ {
+ return true;
+ }
+ false
+ }
+ };
+ }
+
+ // Check provides. A package may declare more than one provide link
+ // for the same target (e.g. an `AliasPackage` carries the base's link
+ // and an extra link tagged at the alias's own version), so keep
+ // iterating once a target name matches but the constraint doesn't —
+ // a later link may still satisfy.
+ for link in &candidate.provides {
+ if link.target != name {
+ continue;
+ }
+ match constraint {
+ None => return true,
+ Some(vc) => {
+ if let Ok(provide_vc) = VersionConstraint::parse(&link.constraint)
+ && constraints_intersect(vc, &provide_vc)
+ {
+ return true;
+ }
+ }
+ }
+ }
+
+ for link in &candidate.replaces {
+ if link.target != name {
+ continue;
+ }
+ match constraint {
+ None => return true,
+ Some(vc) => {
+ if let Ok(replace_vc) = VersionConstraint::parse(&link.constraint)
+ && constraints_intersect(vc, &replace_vc)
+ {
+ return true;
+ }
+ }
+ }
+ }
+
+ false
+ }
+
+ /// Check if a package is in the unacceptable fixed list.
+ pub fn is_unacceptable_fixed_package(&self, id: PackageId) -> bool {
+ self.unacceptable_fixed_packages.contains(&id)
+ }
+}
+
+impl fmt::Display for Pool {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ writeln!(f, "Pool:")?;
+ for pkg in &self.packages {
+ writeln!(f, " {:>6}: {} {}", pkg.id, pkg.name, pkg.pretty_version)?;
+ }
+ Ok(())
+ }
+}
+
+/// Whether the request constraint and the provide/replace link constraint
+/// share at least one satisfying version. Mirrors Composer's
+/// `ConstraintInterface::matches` semantics: a provide/replace link only
+/// makes the candidate a viable provider for those versions of the target
+/// that fall in the link's constraint.
+fn constraints_intersect(a: &VersionConstraint, b: &VersionConstraint) -> bool {
+ a.intersects(b)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn make_input(name: &str, version: &str) -> PoolPackageInput {
+ PoolPackageInput {
+ name: name.to_string(),
+ version: version.to_string(),
+ pretty_version: version.to_string(),
+ requires: vec![],
+ replaces: vec![],
+ provides: vec![],
+ conflicts: vec![],
+ is_fixed: false,
+ is_alias_of: None,
+ }
+ }
+
+ #[test]
+ fn test_pool_basic() {
+ let mut pool = Pool::new(
+ vec![
+ make_input("a/a", "1.0.0.0"),
+ make_input("a/a", "2.0.0.0"),
+ make_input("b/b", "1.0.0.0"),
+ ],
+ vec![],
+ );
+
+ assert_eq!(pool.len(), 3);
+ assert_eq!(pool.package_by_id(1).name, "a/a");
+ assert_eq!(pool.package_by_id(2).name, "a/a");
+ assert_eq!(pool.package_by_id(3).name, "b/b");
+
+ let providers = pool.what_provides("a/a", None);
+ assert_eq!(providers, vec![1, 2]);
+ }
+
+ #[test]
+ fn test_literal_to_package() {
+ let pool = Pool::new(
+ vec![make_input("a/a", "1.0.0.0"), make_input("b/b", "1.0.0.0")],
+ vec![],
+ );
+
+ assert_eq!(pool.literal_to_package(1).name, "a/a");
+ assert_eq!(pool.literal_to_package(-1).name, "a/a");
+ assert_eq!(pool.literal_to_package(2).name, "b/b");
+ assert_eq!(pool.literal_to_package(-2).name, "b/b");
+ }
+
+ #[test]
+ fn test_literal_pretty_string() {
+ let pool = Pool::new(vec![make_input("a/a", "1.0.0.0")], vec![]);
+ assert_eq!(pool.literal_to_pretty_string(1), "install a/a 1.0.0.0");
+ assert_eq!(
+ pool.literal_to_pretty_string(-1),
+ "don't install a/a 1.0.0.0"
+ );
+ }
+}