Skip to main content

bock_pkg/
resolver.rs

1//! Dependency resolver using the PubGrub algorithm.
2//!
3//! Wraps the `pubgrub` crate to resolve Bock package dependencies from
4//! a registry of known packages and their version constraints.
5
6use std::collections::{BTreeMap, BTreeSet};
7use std::convert::Infallible;
8
9use pubgrub::{
10    Dependencies, DependencyConstraints, DependencyProvider, PackageResolutionStatistics, Ranges,
11};
12use semver::Version;
13
14use crate::error::PkgError;
15use crate::version::{parse_version, parse_version_req, req_to_pubgrub_range};
16
17/// Type alias for pubgrub version ranges over semver versions.
18pub type SemverRanges = Ranges<Version>;
19
20/// A resolved set of packages and their selected versions.
21pub type ResolvedDeps = BTreeMap<String, Version>;
22
23/// Unified feature sets: package name → set of enabled features.
24pub type UnifiedFeatures = BTreeMap<String, BTreeSet<String>>;
25
26/// Visibility of a resolved dependency.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum DepVisibility {
29    /// Visible to dependents (direct dependency).
30    Public,
31    /// Not visible to dependents (transitive dependency, private by default).
32    Private,
33}
34
35/// Metadata for a specific version of a package in the registry.
36#[derive(Debug, Clone, Default)]
37pub struct PackageVersionMeta {
38    /// Dependencies: name → version requirement string.
39    pub deps: BTreeMap<String, String>,
40    /// Features requested for each dependency: dep_name → feature list.
41    pub dep_features: BTreeMap<String, Vec<String>>,
42    /// Targets this version supports. `None` means all targets are supported.
43    pub supported_targets: Option<Vec<String>>,
44    /// Features declared by this package version: feature_name → implied deps/features.
45    pub available_features: BTreeMap<String, Vec<String>>,
46}
47
48/// A registry of known packages and their versions/dependencies.
49///
50/// This serves as the `DependencyProvider` for pubgrub resolution.
51/// In v1, packages are registered in-memory (from lockfiles, local dirs, etc.).
52#[derive(Debug, Clone, Default)]
53pub struct PackageRegistry {
54    /// Map of package name → available versions (sorted) and their metadata.
55    packages: BTreeMap<String, BTreeMap<Version, PackageVersionMeta>>,
56}
57
58impl PackageRegistry {
59    /// Create a new empty registry.
60    #[must_use]
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Register a package version with its dependencies.
66    ///
67    /// Dependencies are given as `name → version_req` pairs (e.g., `"^1.0"`).
68    pub fn register(
69        &mut self,
70        name: &str,
71        version: &str,
72        deps: BTreeMap<String, String>,
73    ) -> Result<(), PkgError> {
74        let meta = PackageVersionMeta {
75            deps,
76            ..Default::default()
77        };
78        self.register_with_meta(name, version, meta)
79    }
80
81    /// Register a package version with full metadata including targets and features.
82    pub fn register_with_meta(
83        &mut self,
84        name: &str,
85        version: &str,
86        meta: PackageVersionMeta,
87    ) -> Result<(), PkgError> {
88        let ver = parse_version(version)?;
89        self.packages
90            .entry(name.to_string())
91            .or_default()
92            .insert(ver, meta);
93        Ok(())
94    }
95
96    /// List all available versions for a package.
97    #[must_use]
98    pub fn available_versions(&self, name: &str) -> Vec<&Version> {
99        self.packages
100            .get(name)
101            .map(|versions| versions.keys().collect())
102            .unwrap_or_default()
103    }
104
105    /// Check if a package exists in the registry.
106    #[must_use]
107    pub fn has_package(&self, name: &str) -> bool {
108        self.packages.contains_key(name)
109    }
110
111    /// Resolve dependencies starting from a root package with given direct dependencies.
112    ///
113    /// Returns the resolved set of packages and their versions.
114    pub fn resolve(
115        &self,
116        root_name: &str,
117        root_version: &str,
118        direct_deps: &BTreeMap<String, String>,
119    ) -> Result<ResolvedDeps, PkgError> {
120        self.resolve_internal(root_name, root_version, direct_deps, None)
121    }
122
123    /// Resolve dependencies for a specific build target.
124    ///
125    /// Packages whose `supported_targets` don't include `target` are skipped
126    /// during version selection.
127    pub fn resolve_for_target(
128        &self,
129        root_name: &str,
130        root_version: &str,
131        direct_deps: &BTreeMap<String, String>,
132        target: &str,
133    ) -> Result<ResolvedDeps, PkgError> {
134        self.resolve_internal(
135            root_name,
136            root_version,
137            direct_deps,
138            Some(target.to_string()),
139        )
140    }
141
142    fn resolve_internal(
143        &self,
144        root_name: &str,
145        root_version: &str,
146        direct_deps: &BTreeMap<String, String>,
147        active_target: Option<String>,
148    ) -> Result<ResolvedDeps, PkgError> {
149        let root_ver = parse_version(root_version)?;
150
151        // Create a provider that includes the root package
152        let provider = ResolverProvider {
153            registry: self,
154            root_package: root_name.to_string(),
155            root_version: root_ver.clone(),
156            root_deps: direct_deps.clone(),
157            active_target,
158        };
159
160        let result = pubgrub::resolve(&provider, root_name.to_string(), root_ver).map_err(|e| {
161            let msg = format!("{e}");
162            // Check if this is a "no solution" type error
163            if msg.contains("No solution") || msg.contains("conflict") {
164                PkgError::UnresolvableConstraints(vec![msg])
165            } else {
166                PkgError::ResolutionFailed(msg)
167            }
168        })?;
169
170        // Convert to BTreeMap, excluding the root package
171        let mut resolved = BTreeMap::new();
172        for (pkg, ver) in result {
173            if pkg != root_name {
174                resolved.insert(pkg, ver);
175            }
176        }
177
178        Ok(resolved)
179    }
180
181    /// Unify features requested for each dependency across the dependency graph.
182    ///
183    /// When multiple paths in the dep graph request different features of the
184    /// same package, the union of all requested feature sets is returned.
185    #[must_use]
186    pub fn unify_features(
187        &self,
188        root_dep_features: &BTreeMap<String, Vec<String>>,
189        resolved: &ResolvedDeps,
190    ) -> UnifiedFeatures {
191        let mut unified: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
192
193        // Collect features requested by the root package's deps
194        for (name, feats) in root_dep_features {
195            if resolved.contains_key(name) && !feats.is_empty() {
196                unified
197                    .entry(name.clone())
198                    .or_default()
199                    .extend(feats.iter().cloned());
200            }
201        }
202
203        // Collect features requested by each resolved package's transitive deps
204        for (pkg_name, ver) in resolved {
205            if let Some(versions) = self.packages.get(pkg_name) {
206                if let Some(meta) = versions.get(ver) {
207                    for (dep_name, feats) in &meta.dep_features {
208                        if resolved.contains_key(dep_name) && !feats.is_empty() {
209                            unified
210                                .entry(dep_name.clone())
211                                .or_default()
212                                .extend(feats.iter().cloned());
213                        }
214                    }
215                }
216            }
217        }
218
219        unified
220    }
221}
222
223/// Compute visibility of resolved dependencies.
224///
225/// Direct dependencies are [`DepVisibility::Public`]. Transitive dependencies
226/// (not directly listed in the root's dependency set) are [`DepVisibility::Private`]
227/// by default.
228#[must_use]
229pub fn compute_visibility(
230    direct_dep_names: &BTreeMap<String, String>,
231    resolved: &ResolvedDeps,
232) -> BTreeMap<String, DepVisibility> {
233    resolved
234        .keys()
235        .map(|name| {
236            let vis = if direct_dep_names.contains_key(name) {
237                DepVisibility::Public
238            } else {
239                DepVisibility::Private
240            };
241            (name.clone(), vis)
242        })
243        .collect()
244}
245
246/// Internal provider that implements pubgrub's `DependencyProvider`.
247struct ResolverProvider<'a> {
248    registry: &'a PackageRegistry,
249    root_package: String,
250    root_version: Version,
251    root_deps: BTreeMap<String, String>,
252    active_target: Option<String>,
253}
254
255impl<'a> DependencyProvider for ResolverProvider<'a> {
256    type P = String;
257    type V = Version;
258    type VS = Ranges<Version>;
259    type M = String;
260    type Err = Infallible;
261    type Priority = u32;
262
263    fn prioritize(
264        &self,
265        package: &String,
266        _range: &Ranges<Version>,
267        _stats: &PackageResolutionStatistics,
268    ) -> Self::Priority {
269        // Higher priority for packages with fewer versions (more constrained first)
270        if package == &self.root_package {
271            return u32::MAX;
272        }
273        let count = self
274            .registry
275            .packages
276            .get(package)
277            .map(|v| v.len())
278            .unwrap_or(0);
279        // Invert: fewer versions = higher priority
280        u32::MAX.saturating_sub(count as u32)
281    }
282
283    fn choose_version(
284        &self,
285        package: &String,
286        range: &Ranges<Version>,
287    ) -> Result<Option<Version>, Infallible> {
288        if package == &self.root_package {
289            if range.contains(&self.root_version) {
290                return Ok(Some(self.root_version.clone()));
291            }
292            return Ok(None);
293        }
294
295        // Choose the highest version that matches the range and target
296        let version = self.registry.packages.get(package).and_then(|versions| {
297            versions
298                .keys()
299                .rev()
300                .find(|v| {
301                    if !range.contains(v) {
302                        return false;
303                    }
304                    // M-090: Skip versions incompatible with the active target
305                    if let Some(target) = &self.active_target {
306                        if let Some(meta) = versions.get(v) {
307                            if let Some(supported) = &meta.supported_targets {
308                                return supported.iter().any(|t| t == target);
309                            }
310                        }
311                    }
312                    true
313                })
314                .cloned()
315        });
316
317        Ok(version)
318    }
319
320    fn get_dependencies(
321        &self,
322        package: &String,
323        version: &Version,
324    ) -> Result<Dependencies<String, Ranges<Version>, String>, Infallible> {
325        if package == &self.root_package && version == &self.root_version {
326            // Return the root's direct dependencies
327            let deps: DependencyConstraints<String, Ranges<Version>> = self
328                .root_deps
329                .iter()
330                .filter_map(|(name, req_str)| {
331                    parse_version_req(req_str)
332                        .ok()
333                        .map(|req| (name.clone(), req_to_pubgrub_range(&req)))
334                })
335                .collect();
336            return Ok(Dependencies::Available(deps));
337        }
338
339        let Some(versions) = self.registry.packages.get(package) else {
340            return Ok(Dependencies::Unavailable(format!(
341                "package '{package}' not found in registry"
342            )));
343        };
344
345        let Some(meta) = versions.get(version) else {
346            return Ok(Dependencies::Unavailable(format!(
347                "version {version} of '{package}' not found"
348            )));
349        };
350
351        let deps: DependencyConstraints<String, Ranges<Version>> = meta
352            .deps
353            .iter()
354            .filter_map(|(name, req_str)| {
355                parse_version_req(req_str)
356                    .ok()
357                    .map(|req| (name.clone(), req_to_pubgrub_range(&req)))
358            })
359            .collect();
360
361        Ok(Dependencies::Available(deps))
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    fn make_deps(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
370        pairs
371            .iter()
372            .map(|(k, v)| (k.to_string(), v.to_string()))
373            .collect()
374    }
375
376    #[test]
377    fn resolve_simple_deps() {
378        let mut registry = PackageRegistry::new();
379        registry.register("foo", "1.0.0", BTreeMap::new()).unwrap();
380        registry.register("foo", "1.1.0", BTreeMap::new()).unwrap();
381        registry
382            .register("bar", "2.0.0", make_deps(&[("foo", "^1.0")]))
383            .unwrap();
384
385        let direct = make_deps(&[("bar", "^2.0")]);
386        let resolved = registry.resolve("my-app", "0.1.0", &direct).unwrap();
387
388        assert!(resolved.contains_key("bar"));
389        assert!(resolved.contains_key("foo"));
390        assert_eq!(resolved["bar"], Version::new(2, 0, 0));
391        // Should pick highest compatible foo version
392        assert_eq!(resolved["foo"], Version::new(1, 1, 0));
393    }
394
395    #[test]
396    fn resolve_transitive_deps() {
397        let mut registry = PackageRegistry::new();
398        registry.register("c", "1.0.0", BTreeMap::new()).unwrap();
399        registry
400            .register("b", "1.0.0", make_deps(&[("c", "^1.0")]))
401            .unwrap();
402        registry
403            .register("a", "1.0.0", make_deps(&[("b", "^1.0")]))
404            .unwrap();
405
406        let direct = make_deps(&[("a", "^1.0")]);
407        let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
408
409        assert_eq!(resolved.len(), 3);
410        assert!(resolved.contains_key("a"));
411        assert!(resolved.contains_key("b"));
412        assert!(resolved.contains_key("c"));
413    }
414
415    #[test]
416    fn resolve_conflicting_deps() {
417        let mut registry = PackageRegistry::new();
418        // Only version 1.0.0 of shared exists
419        registry
420            .register("shared", "1.0.0", BTreeMap::new())
421            .unwrap();
422        // a needs shared ^1.0 (ok)
423        registry
424            .register("a", "1.0.0", make_deps(&[("shared", "^1.0")]))
425            .unwrap();
426        // b needs shared ^2.0 (conflict — no 2.x available)
427        registry
428            .register("b", "1.0.0", make_deps(&[("shared", "^2.0")]))
429            .unwrap();
430
431        let direct = make_deps(&[("a", "^1.0"), ("b", "^1.0")]);
432        let result = registry.resolve("root", "0.1.0", &direct);
433        assert!(result.is_err());
434    }
435
436    #[test]
437    fn resolve_empty_deps() {
438        let registry = PackageRegistry::new();
439        let direct = BTreeMap::new();
440        let resolved = registry.resolve("my-app", "1.0.0", &direct).unwrap();
441        assert!(resolved.is_empty());
442    }
443
444    #[test]
445    fn resolve_picks_highest_compatible() {
446        let mut registry = PackageRegistry::new();
447        registry.register("foo", "1.0.0", BTreeMap::new()).unwrap();
448        registry.register("foo", "1.2.0", BTreeMap::new()).unwrap();
449        registry.register("foo", "1.5.0", BTreeMap::new()).unwrap();
450        registry.register("foo", "2.0.0", BTreeMap::new()).unwrap();
451
452        let direct = make_deps(&[("foo", "^1.0")]);
453        let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
454        assert_eq!(resolved["foo"], Version::new(1, 5, 0));
455    }
456
457    // --- M-090: Target filtering ---
458
459    #[test]
460    fn target_filtering_skips_incompatible_packages() {
461        let mut registry = PackageRegistry::new();
462
463        // foo 1.0.0 supports only js
464        registry
465            .register_with_meta(
466                "foo",
467                "1.0.0",
468                PackageVersionMeta {
469                    deps: BTreeMap::new(),
470                    supported_targets: Some(vec!["js".into()]),
471                    ..Default::default()
472                },
473            )
474            .unwrap();
475
476        // foo 1.1.0 supports js and rust
477        registry
478            .register_with_meta(
479                "foo",
480                "1.1.0",
481                PackageVersionMeta {
482                    deps: BTreeMap::new(),
483                    supported_targets: Some(vec!["js".into(), "rust".into()]),
484                    ..Default::default()
485                },
486            )
487            .unwrap();
488
489        let direct = make_deps(&[("foo", "^1.0")]);
490
491        // Resolving for js should pick 1.1.0 (highest compatible)
492        let resolved = registry
493            .resolve_for_target("root", "0.1.0", &direct, "js")
494            .unwrap();
495        assert_eq!(resolved["foo"], Version::new(1, 1, 0));
496
497        // Resolving for rust should pick 1.1.0 (only compatible version)
498        let resolved = registry
499            .resolve_for_target("root", "0.1.0", &direct, "rust")
500            .unwrap();
501        assert_eq!(resolved["foo"], Version::new(1, 1, 0));
502
503        // Resolving for python should fail (no compatible version)
504        let result = registry.resolve_for_target("root", "0.1.0", &direct, "python");
505        assert!(result.is_err());
506    }
507
508    #[test]
509    fn target_filtering_no_targets_means_all() {
510        let mut registry = PackageRegistry::new();
511
512        // foo has no target restriction
513        registry.register("foo", "1.0.0", BTreeMap::new()).unwrap();
514
515        let direct = make_deps(&[("foo", "^1.0")]);
516
517        // Should resolve for any target
518        let resolved = registry
519            .resolve_for_target("root", "0.1.0", &direct, "rust")
520            .unwrap();
521        assert_eq!(resolved["foo"], Version::new(1, 0, 0));
522    }
523
524    #[test]
525    fn target_filtering_transitive() {
526        let mut registry = PackageRegistry::new();
527
528        // "inner" only supports rust
529        registry
530            .register_with_meta(
531                "inner",
532                "1.0.0",
533                PackageVersionMeta {
534                    deps: BTreeMap::new(),
535                    supported_targets: Some(vec!["rust".into()]),
536                    ..Default::default()
537                },
538            )
539            .unwrap();
540
541        // "outer" depends on inner, supports all targets
542        registry
543            .register("outer", "1.0.0", make_deps(&[("inner", "^1.0")]))
544            .unwrap();
545
546        let direct = make_deps(&[("outer", "^1.0")]);
547
548        // Resolving for js should fail because inner doesn't support js
549        let result = registry.resolve_for_target("root", "0.1.0", &direct, "js");
550        assert!(result.is_err());
551
552        // Resolving for rust should succeed
553        let resolved = registry
554            .resolve_for_target("root", "0.1.0", &direct, "rust")
555            .unwrap();
556        assert!(resolved.contains_key("outer"));
557        assert!(resolved.contains_key("inner"));
558    }
559
560    // --- M-091: Feature unification ---
561
562    #[test]
563    fn feature_unification_unions_features() {
564        let mut registry = PackageRegistry::new();
565
566        // "shared" has features available
567        registry
568            .register_with_meta(
569                "shared",
570                "1.0.0",
571                PackageVersionMeta {
572                    deps: BTreeMap::new(),
573                    available_features: BTreeMap::from([
574                        ("json".into(), vec![]),
575                        ("xml".into(), vec![]),
576                        ("yaml".into(), vec![]),
577                    ]),
578                    ..Default::default()
579                },
580            )
581            .unwrap();
582
583        // "a" depends on shared with feature "json"
584        registry
585            .register_with_meta(
586                "a",
587                "1.0.0",
588                PackageVersionMeta {
589                    deps: make_deps(&[("shared", "^1.0")]),
590                    dep_features: BTreeMap::from([("shared".into(), vec!["json".into()])]),
591                    ..Default::default()
592                },
593            )
594            .unwrap();
595
596        // "b" depends on shared with features "xml" and "yaml"
597        registry
598            .register_with_meta(
599                "b",
600                "1.0.0",
601                PackageVersionMeta {
602                    deps: make_deps(&[("shared", "^1.0")]),
603                    dep_features: BTreeMap::from([(
604                        "shared".into(),
605                        vec!["xml".into(), "yaml".into()],
606                    )]),
607                    ..Default::default()
608                },
609            )
610            .unwrap();
611
612        let direct = make_deps(&[("a", "^1.0"), ("b", "^1.0")]);
613        let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
614
615        let unified = registry.unify_features(&BTreeMap::new(), &resolved);
616
617        let shared_features = &unified["shared"];
618        assert!(shared_features.contains("json"));
619        assert!(shared_features.contains("xml"));
620        assert!(shared_features.contains("yaml"));
621        assert_eq!(shared_features.len(), 3);
622    }
623
624    #[test]
625    fn feature_unification_includes_root_features() {
626        let mut registry = PackageRegistry::new();
627        registry.register("foo", "1.0.0", BTreeMap::new()).unwrap();
628
629        let direct = make_deps(&[("foo", "^1.0")]);
630        let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
631
632        let root_features = BTreeMap::from([("foo".into(), vec!["extra".into(), "debug".into()])]);
633        let unified = registry.unify_features(&root_features, &resolved);
634
635        let foo_features = &unified["foo"];
636        assert!(foo_features.contains("extra"));
637        assert!(foo_features.contains("debug"));
638    }
639
640    #[test]
641    fn feature_unification_empty_when_no_features() {
642        let mut registry = PackageRegistry::new();
643        registry.register("foo", "1.0.0", BTreeMap::new()).unwrap();
644
645        let direct = make_deps(&[("foo", "^1.0")]);
646        let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
647
648        let unified = registry.unify_features(&BTreeMap::new(), &resolved);
649        assert!(unified.is_empty());
650    }
651
652    // --- M-094: Transitive deps private ---
653
654    #[test]
655    fn transitive_deps_are_private() {
656        let mut registry = PackageRegistry::new();
657        registry.register("c", "1.0.0", BTreeMap::new()).unwrap();
658        registry
659            .register("b", "1.0.0", make_deps(&[("c", "^1.0")]))
660            .unwrap();
661        registry
662            .register("a", "1.0.0", make_deps(&[("b", "^1.0")]))
663            .unwrap();
664
665        let direct = make_deps(&[("a", "^1.0")]);
666        let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
667
668        let visibility = compute_visibility(&direct, &resolved);
669
670        // Direct dep is public
671        assert_eq!(visibility["a"], DepVisibility::Public);
672        // Transitive deps are private
673        assert_eq!(visibility["b"], DepVisibility::Private);
674        assert_eq!(visibility["c"], DepVisibility::Private);
675    }
676
677    #[test]
678    fn direct_deps_are_public() {
679        let mut registry = PackageRegistry::new();
680        registry.register("foo", "1.0.0", BTreeMap::new()).unwrap();
681        registry.register("bar", "1.0.0", BTreeMap::new()).unwrap();
682
683        let direct = make_deps(&[("foo", "^1.0"), ("bar", "^1.0")]);
684        let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
685
686        let visibility = compute_visibility(&direct, &resolved);
687
688        assert_eq!(visibility["foo"], DepVisibility::Public);
689        assert_eq!(visibility["bar"], DepVisibility::Public);
690    }
691
692    #[test]
693    fn visibility_mixed_direct_and_transitive() {
694        let mut registry = PackageRegistry::new();
695        // "shared" is both a direct dep and pulled transitively
696        registry
697            .register("shared", "1.0.0", BTreeMap::new())
698            .unwrap();
699        registry
700            .register("lib", "1.0.0", make_deps(&[("shared", "^1.0")]))
701            .unwrap();
702
703        // Root depends on both "lib" and "shared" directly
704        let direct = make_deps(&[("lib", "^1.0"), ("shared", "^1.0")]);
705        let resolved = registry.resolve("root", "0.1.0", &direct).unwrap();
706
707        let visibility = compute_visibility(&direct, &resolved);
708
709        // Both are direct deps, so both are public
710        assert_eq!(visibility["lib"], DepVisibility::Public);
711        assert_eq!(visibility["shared"], DepVisibility::Public);
712    }
713}