Skip to main content

nickel_lang_package/
resolve.rs

1use std::{collections::HashMap, path::PathBuf};
2
3use nickel_lang_core::{cache::normalize_path, identifier::Ident, package::PackageMap};
4use pubgrub::{DefaultStringReporter, DependencyProvider, Reporter as _};
5
6use crate::{
7    Dependency, IndexDependency, ManifestFile, PreciseGitPkg, PreciseIndexPkg, PrecisePkg,
8    UnversionedPrecisePkg,
9    config::Config,
10    error::{Error, IoResultExt as _},
11    index::{self, PackageIndex, Shared},
12    lock::{LockFile, LockPrecisePkg},
13    snapshot::Snapshot,
14    version::{SemVer, VersionReq},
15};
16
17pub type ResolveError = pubgrub::PubGrubError<PackageRegistry>;
18
19pub fn print_resolve_error(f: &mut std::fmt::Formatter<'_>, e: &ResolveError) -> std::fmt::Result {
20    match e {
21        pubgrub::PubGrubError::NoSolution(derivation_tree) => {
22            let mut tree = derivation_tree.clone();
23            tree.collapse_no_versions();
24            write!(f, "{}", DefaultStringReporter::report(&tree))
25        }
26        pubgrub::PubGrubError::ErrorRetrievingDependencies {
27            package: _,
28            version: _,
29            source,
30        } => write!(f, "{source}"),
31        pubgrub::PubGrubError::ErrorChoosingVersion { package: _, source } => write!(f, "{source}"),
32        // We don't override should_cancel, so it can't trigger an error
33        pubgrub::PubGrubError::ErrorInShouldCancel(_) => unreachable!(),
34    }
35}
36
37pub struct PackageRegistry {
38    // The packages whose versions were locked in a lockfile; we'll try to prefer using
39    // those same versions. We won't absolutely insist on it, because if the manifest
40    // changed (or some path-dependency changed) then the old locked versions might not
41    // resolve anymore.
42    previously_locked: HashMap<Package, SemVer>,
43    index: PackageIndex<Shared>,
44    snapshot: Snapshot,
45}
46
47#[derive(Debug, Clone, Eq, PartialEq, Hash)]
48pub enum Package {
49    Root,
50    Git(PreciseGitPkg),
51    Path(PathBuf),
52    Index(Bucket),
53}
54
55impl Package {
56    fn unversioned_or_index(self) -> Result<UnversionedPrecisePkg, Bucket> {
57        match self {
58            Package::Root => Ok(UnversionedPrecisePkg::Path(PathBuf::new())),
59            Package::Path(p) => Ok(UnversionedPrecisePkg::Path(p)),
60            Package::Git(g) => Ok(UnversionedPrecisePkg::Git(g)),
61            Package::Index(bucket) => Err(bucket),
62        }
63    }
64}
65
66#[derive(Debug, Clone, Eq, PartialEq, Hash)]
67pub struct Bucket {
68    pub id: index::Id,
69    pub version: BucketVersion,
70}
71
72/// A bucket version represents a collection of compatible semver versions.
73#[derive(Debug, Clone, Eq, PartialEq, Hash)]
74pub enum BucketVersion {
75    /// A collection of versions all having the same major version number.
76    /// (For example, 1.x.y)
77    Major(u64),
78    /// A collection of versions all having major version zero, and the same minor version number.
79    /// (For example, 0.2.x)
80    Minor(u64),
81    /// An exact prerelease version.
82    ///
83    /// The `pre` field of the version must be non-empty.
84    Prerelease(SemVer),
85}
86
87impl From<VersionReq> for BucketVersion {
88    fn from(req: VersionReq) -> Self {
89        match req {
90            VersionReq::Compatible(prefix) => {
91                BucketVersion::major_minor(prefix.major, prefix.minor.unwrap_or_default())
92            }
93            VersionReq::Exact(v) => v.into(),
94        }
95    }
96}
97
98impl From<SemVer> for BucketVersion {
99    fn from(v: SemVer) -> Self {
100        if v.pre.is_empty() {
101            BucketVersion::major_minor(v.major, v.minor)
102        } else {
103            BucketVersion::Prerelease(v.clone())
104        }
105    }
106}
107
108impl BucketVersion {
109    pub fn major_minor(major: u64, minor: u64) -> Self {
110        if major == 0 {
111            BucketVersion::Minor(minor)
112        } else {
113            BucketVersion::Major(major)
114        }
115    }
116
117    pub fn contains(&self, semver: &SemVer) -> bool {
118        match self {
119            BucketVersion::Major(v) => *v == semver.major && semver.pre.is_empty(),
120            BucketVersion::Minor(v) => {
121                semver.major == 0 && semver.minor == *v && semver.pre.is_empty()
122            }
123            BucketVersion::Prerelease(v) => v == semver,
124        }
125    }
126}
127
128impl std::fmt::Display for BucketVersion {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        match self {
131            BucketVersion::Major(v) => write!(f, "{v}"),
132            BucketVersion::Minor(v) => write!(f, "0.{v}"),
133            BucketVersion::Prerelease(v) => write!(f, "{v}"),
134        }
135    }
136}
137
138impl std::fmt::Display for Package {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        match self {
141            Package::Root => {
142                write!(f, "top-level package")
143            }
144            Package::Git(PreciseGitPkg { url, id, path }) => {
145                write!(f, "{url}@{id}/{}", path.display())
146            }
147            Package::Path(path) => {
148                write!(f, "'Path {}", path.display())
149            }
150            Package::Index(b) => {
151                write!(f, "{}", b.id)
152            }
153        }
154    }
155}
156
157/// Pubgrub's advice for package resolution priority heuristics is
158/// that we should first resolve (i.e. assign highest priority to)
159///
160/// - packages whose version is already known, and
161/// - packages that have lots of conflicts.
162#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
163pub struct Priority {
164    pub single_version: bool,
165    pub conflicts: u32,
166}
167
168// Like `collect`, but if it encounters a duplicate entry then the resulting ranges are intersected.
169fn collect_intersections(
170    deps: impl Iterator<Item = (Package, pubgrub::Ranges<SemVer>)>,
171) -> pubgrub::Map<Package, pubgrub::Ranges<SemVer>> {
172    let mut ret = pubgrub::Map::default();
173    for (pkg, ranges) in deps {
174        ret.entry(pkg)
175            .and_modify(|e: &mut pubgrub::Ranges<_>| *e = e.intersection(&ranges))
176            .or_insert(ranges);
177    }
178
179    // TODO: if a package depends on conflicting versions of a package, this will say
180    // that it depends on an empty set of versions. Which is sort of correct, but the
181    // error message is confusing. Fortunately, you only hit this case if you have
182    // two conflicting constraints *in the same manifest*. If the conflicts some from
183    // other parts of the dependency tree you get a reasonable error.
184    ret
185}
186
187impl PackageRegistry {
188    /// Read git or path dependencies from the snapshot.
189    ///
190    /// `pkg` must not be an index package.
191    ///
192    /// # Panics
193    ///
194    /// Panics if `pkg` was not part of the snapshot.
195    fn snapshot_dependencies(
196        &self,
197        pkg: &UnversionedPrecisePkg,
198    ) -> pubgrub::Map<Package, pubgrub::Ranges<SemVer>> {
199        let index_deps = self
200            .snapshot
201            .index_deps(pkg)
202            .map(index_dep_package_and_range);
203        let other_deps = self
204            .snapshot
205            .sorted_unversioned_dependencies(pkg)
206            .into_iter()
207            .map(|(_name, _dep, pkg)| {
208                let p = match pkg {
209                    UnversionedPrecisePkg::Git(g) => Package::Git(g),
210                    UnversionedPrecisePkg::Path(path) => Package::Path(path),
211                };
212                (p, pubgrub::Ranges::full())
213            });
214
215        collect_intersections(index_deps.chain(other_deps))
216    }
217}
218
219impl DependencyProvider for PackageRegistry {
220    type P = Package;
221    type V = SemVer;
222    type VS = pubgrub::Ranges<SemVer>;
223    type Priority = Priority;
224    type M = String;
225    type Err = crate::Error;
226
227    fn prioritize(
228        &self,
229        package: &Self::P,
230        _range: &Self::VS,
231        package_conflicts_counts: &pubgrub::PackageResolutionStatistics,
232    ) -> Self::Priority {
233        let single_version = match package {
234            Package::Git { .. } | Package::Path { .. } | Package::Root => true,
235            Package::Index(Bucket {
236                version: BucketVersion::Prerelease(_),
237                ..
238            }) => true,
239            // We could be more accurate here, by actually looking at `range` and
240            // checking if it defines a single version.
241            Package::Index(_) => false,
242        };
243        Priority {
244            single_version,
245            conflicts: package_conflicts_counts.conflict_count(),
246        }
247    }
248
249    fn choose_version(
250        &self,
251        package: &Self::P,
252        range: &Self::VS,
253    ) -> Result<Option<Self::V>, Self::Err> {
254        let check_version = |v| {
255            if range.contains(v) {
256                Ok(Some(v.clone()))
257            } else {
258                Ok(None)
259            }
260        };
261
262        if let Some(locked_version) = self.previously_locked.get(package)
263            && range.contains(locked_version)
264        {
265            return Ok(Some(locked_version.clone()));
266        }
267
268        match package {
269            Package::Git(g) => check_version(
270                &self
271                    .snapshot
272                    .manifest(&UnversionedPrecisePkg::Git(g.clone()))
273                    .version,
274            ),
275            Package::Path(path) => check_version(
276                // TODO: less cloning
277                &self
278                    .snapshot
279                    .manifest(&UnversionedPrecisePkg::Path(path.clone()))
280                    .version,
281            ),
282            Package::Index(bucket) => {
283                if let BucketVersion::Prerelease(v) = &bucket.version {
284                    if self.index.has_version(&bucket.id, v)? {
285                        Ok(Some(v.clone()))
286                    } else {
287                        Ok(None)
288                    }
289                } else {
290                    // `available_versions` are sorted in increasing order, so this will return
291                    // the smallest version that's in the bucket and the constrained range.
292                    let min_version = self
293                        .index
294                        .available_versions(&bucket.id)?
295                        .find(|v| bucket.version.contains(v) && range.contains(v));
296                    Ok(min_version)
297                }
298            }
299            Package::Root => check_version(
300                &self
301                    .snapshot
302                    .manifest(&UnversionedPrecisePkg::Path(PathBuf::new()))
303                    .version,
304            ),
305        }
306    }
307
308    fn get_dependencies(
309        &self,
310        package: &Self::P,
311        version: &Self::V,
312    ) -> Result<pubgrub::Dependencies<Self::P, Self::VS, Self::M>, Self::Err> {
313        let deps: pubgrub::Map<_, _> = match package.clone().unversioned_or_index() {
314            Ok(uv) => self.snapshot_dependencies(&uv),
315            Err(bucket) => {
316                let index_package = self.index.package(&bucket.id, version)?;
317                collect_intersections(
318                    index_package
319                        .dependencies
320                        .values()
321                        .map(index_dep_package_and_range),
322                )
323            }
324        };
325
326        Ok(pubgrub::Dependencies::Available(deps))
327    }
328}
329
330fn index_dep_package_and_range(dep: &IndexDependency) -> (Package, pubgrub::Ranges<SemVer>) {
331    let p = Package::Index(Bucket {
332        id: dep.id.clone(),
333        version: dep.version.clone().into(),
334    });
335    let range = match &dep.version {
336        VersionReq::Compatible(v) => {
337            let lower_bound = SemVer::new(
338                v.major,
339                v.minor.unwrap_or_default(),
340                v.patch.unwrap_or_default(),
341            );
342            pubgrub::Ranges::higher_than(lower_bound)
343        }
344        VersionReq::Exact(req) => pubgrub::Ranges::singleton(req.clone()),
345    };
346
347    (p, range)
348}
349
350/// Stores the result of resolving version constraints to exact versions.
351#[derive(Debug)]
352pub struct Resolution {
353    pub config: Config,
354    /// The snapshot (of path and git packages) that was used to construct
355    /// this resolution. Note that path and git packages are not "resolved";
356    /// they have fixed versions. The snapshot is only used to collect index
357    /// dependencies of git and path packages.
358    pub snapshot: Snapshot,
359    pub index: PackageIndex<Shared>,
360    /// All the index packages in the dependency tree.
361    ///
362    /// Each package id can resolve to multiple versions, but those versions should all fall
363    /// into disjoint semantic-version buckets.
364    pub index_packages: HashMap<index::Id, Vec<SemVer>>,
365}
366
367/// Resolve a package's dependencies, from scratch.
368pub fn resolve(
369    manifest: &ManifestFile,
370    snapshot: Snapshot,
371    index: PackageIndex<Shared>,
372    config: Config,
373) -> Result<Resolution, Error> {
374    resolve_with_lock(manifest, &LockFile::default(), snapshot, index, config)
375}
376
377/// Resolve a package's dependencies, giving preference to the versions that were previously
378/// locked.
379///
380/// The only guarantee we give is that if the lock file already contains a complete and valid
381/// dependency graph then it will be kept. If the lock file is incomplete, or any part of
382/// it is invalid then we will try to preserve the valid parts but make no guarantees.
383pub fn resolve_with_lock(
384    manifest: &ManifestFile,
385    lock: &LockFile,
386    snapshot: Snapshot,
387    index: PackageIndex<Shared>,
388    config: Config,
389) -> Result<Resolution, Error> {
390    let version = manifest.version.clone();
391    let registry = PackageRegistry {
392        previously_locked: lock
393            .packages
394            .values()
395            .filter_map(|entry| {
396                let LockPrecisePkg::Index { id, version } = &entry.precise else {
397                    return None;
398                };
399
400                let pkg = Package::Index(Bucket {
401                    id: id.clone(),
402                    version: version.clone().into(),
403                });
404
405                Some((pkg, version.clone()))
406            })
407            .collect(),
408        index,
409        snapshot,
410    };
411
412    let deps = pubgrub::resolve(&registry, Package::Root, version)
413        .map_err(|e| Error::Resolution(Box::new(e)))?;
414
415    let mut index_packages: HashMap<index::Id, Vec<SemVer>> = HashMap::new();
416    for (pkg, version) in deps {
417        if let Package::Index(bucket) = pkg {
418            index_packages.entry(bucket.id).or_default().push(version);
419        }
420    }
421    for list in index_packages.values_mut() {
422        list.sort();
423        list.dedup();
424    }
425
426    Ok(Resolution {
427        config,
428        snapshot: registry.snapshot,
429        index: registry.index,
430        index_packages,
431    })
432}
433
434/// Builds a resolution by just copying out all the versions of index
435/// dependencies that we find in the lock file.
436///
437/// The resolution returned here is not guaranteed to be a *valid* resolution if
438/// the lock file is out-of-date relative to the snapshot: there could be some
439/// packages in the snapshot's dependency tree that aren't mentioned in the lock
440/// file and will be missing from the resolution.
441pub fn copy_from_lock(
442    lock: &LockFile,
443    snapshot: Snapshot,
444    index: PackageIndex<Shared>,
445    config: Config,
446) -> Result<Resolution, Error> {
447    let mut index_packages: HashMap<index::Id, Vec<SemVer>> = HashMap::new();
448
449    for entry in lock.packages.values() {
450        if let LockPrecisePkg::Index { id, version } = &entry.precise {
451            index_packages
452                .entry(id.clone())
453                .or_default()
454                .push(version.clone());
455        }
456    }
457    for list in index_packages.values_mut() {
458        list.sort();
459        list.dedup();
460    }
461
462    Ok(Resolution {
463        config,
464        snapshot,
465        index,
466        index_packages,
467    })
468}
469
470impl Resolution {
471    /// Finds the resolved version of this index dependency.
472    ///
473    /// # Panics
474    ///
475    /// Panics if the dependency was not part of the dependency tree that this resolution
476    /// was generated for.
477    fn index_dep_version(&self, dep: &IndexDependency) -> &SemVer {
478        // unwrap: we can assume `dep` was part of the resolved dependency tree
479        self.index_packages
480            .get(&dep.id)
481            .unwrap()
482            .iter()
483            // We take the first matching version. Once version resolution is
484            // done and we start checking for version conflicts, there will be
485            // guaranteed to be only one.
486            .find(|v| dep.version.matches(v))
487            .unwrap()
488    }
489
490    /// Finds the precise resolved version of this dependency.
491    ///
492    /// # Panics
493    ///
494    /// Panics if the dependency was not part of the dependency tree that this resolution
495    /// was generated for.
496    pub fn precise(&self, dep: &Dependency) -> PrecisePkg {
497        match dep {
498            Dependency::Git(git) => PrecisePkg::Git(PreciseGitPkg {
499                url: git.url.clone(),
500                id: self.snapshot.git[git],
501                path: git.path.clone(),
502            }),
503            Dependency::Path(path) => PrecisePkg::Path(path.to_owned()),
504            Dependency::Index(idx) => {
505                let version = self.index_dep_version(idx).clone();
506                PrecisePkg::Index(PreciseIndexPkg {
507                    id: idx.id.clone(),
508                    version,
509                })
510            }
511        }
512    }
513
514    /// Returns the dependencies of a package.
515    ///
516    /// # Panics
517    ///
518    /// Panics if the package was not part of the dependency tree that this resolution
519    /// was generated for.
520    pub fn sorted_dependencies(
521        &self,
522        pkg: &PrecisePkg,
523    ) -> Result<Vec<(Ident, Dependency, PrecisePkg)>, Error> {
524        match pkg.clone().unversioned_or_index() {
525            Ok(uv) => {
526                let mut deps: Vec<_> = self
527                    .snapshot
528                    .sorted_unversioned_dependencies(&uv)
529                    .into_iter()
530                    .map(|(i, d, p)| (i, d, p.into()))
531                    .collect();
532                let index_deps =
533                    self.snapshot
534                        .manifest(&uv)
535                        .dependencies
536                        .iter()
537                        .filter_map(|(id, dep)| {
538                            if matches!(dep, Dependency::Index(_)) {
539                                Some((*id, dep.clone(), self.precise(dep)))
540                            } else {
541                                None
542                            }
543                        });
544                deps.extend(index_deps);
545                deps.sort_by(|(name0, _, _), (name1, _, _)| name0.label().cmp(name1.label()));
546                deps.dedup();
547                Ok(deps)
548            }
549            Err(PreciseIndexPkg { id, version }) => {
550                let pkg = self.index.package(&id, &version)?;
551                let mut ret: Vec<_> = pkg
552                    .dependencies
553                    .iter()
554                    .map(|(id, index_dep)| {
555                        let semver = self.index_packages[&index_dep.id]
556                            .iter()
557                            .find(|v| index_dep.version.matches(v))
558                            .unwrap();
559                        (
560                            *id,
561                            Dependency::Index(index_dep.clone()),
562                            PrecisePkg::Index(PreciseIndexPkg {
563                                id: index_dep.id.clone(),
564                                version: semver.clone(),
565                            }),
566                        )
567                    })
568                    .collect();
569                ret.sort_by(|(name0, _, _), (name1, _, _)| name0.label().cmp(name1.label()));
570                Ok(ret)
571            }
572        }
573    }
574
575    /// Returns a package map containing the entire dependency tree.
576    pub fn package_map(&self, manifest: &ManifestFile) -> Result<PackageMap, Error> {
577        let parent_dir = manifest.parent_dir.clone();
578        let manifest_dir = normalize_path(&parent_dir).with_path(&parent_dir)?;
579        let config = &self.config;
580
581        let mut all: Vec<PrecisePkg> = self
582            .snapshot
583            .all_packages()
584            .map(|p| p.clone().into())
585            .collect();
586        all.extend(self.index_packages.iter().flat_map(|(id, versions)| {
587            versions.iter().map(|v| {
588                PrecisePkg::Index(PreciseIndexPkg {
589                    id: id.clone(),
590                    version: v.clone(),
591                })
592            })
593        }));
594        all.sort();
595        all.dedup();
596
597        let mut packages = HashMap::new();
598        for p in &all {
599            let p_path = p
600                .clone()
601                .with_abs_path(&manifest_dir)
602                .local_path(config, &self.index)?;
603            let root_path = &manifest_dir;
604            for (dep_id, _, dep_precise) in self.sorted_dependencies(p)? {
605                packages.insert(
606                    (p_path.clone(), dep_id),
607                    dep_precise
608                        .with_abs_path(root_path)
609                        .local_path(config, &self.index)?,
610                );
611            }
612        }
613
614        Ok(PackageMap {
615            // Copy over dependencies of the root, making paths absolute.
616            top_level: manifest
617                .dependencies
618                .iter()
619                .map(|(name, source)| {
620                    Ok((
621                        *name,
622                        self.precise(source)
623                            .with_abs_path(&manifest_dir)
624                            .local_path(config, &self.index)?,
625                    ))
626                })
627                .collect::<Result<_, Error>>()?,
628
629            packages,
630        })
631    }
632
633    /// Returns all the dependencies of a package, along with their package-local names.
634    pub fn dependencies(&self, pkg: &PrecisePkg) -> Result<HashMap<Ident, PrecisePkg>, Error> {
635        let ret = match pkg.clone().unversioned_or_index() {
636            Ok(uv) => {
637                let manifest = self.snapshot.manifest(&uv);
638                manifest
639                    .dependencies
640                    .iter()
641                    .map(move |(dep_name, dep)| {
642                        let pkg = match dep.clone().as_unversioned() {
643                            Some(dep) => self.snapshot.dependency(&uv, &dep).clone().into(),
644                            None => {
645                                // Since the realization contains all the unversioned deps, if we didn't
646                                // find our dep then it must be an index dep.
647                                self.precise(dep)
648                            }
649                        };
650                        (*dep_name, pkg)
651                    })
652                    .collect()
653            }
654            Err(PreciseIndexPkg { id, version }) => {
655                let index_pkg = self.index.package(&id, &version)?;
656                index_pkg
657                    .dependencies
658                    .into_iter()
659                    .map(move |(dep_name, dep)| {
660                        let precise_dep = self.precise(&Dependency::Index(IndexDependency {
661                            id: dep.id.clone(),
662                            version: dep.version.clone(),
663                        }));
664                        (dep_name, precise_dep)
665                    })
666                    .collect()
667            }
668        };
669        Ok(ret)
670    }
671
672    /// Returns all the resolved packages in the dependency tree.
673    pub fn all_packages(&self) -> Vec<PrecisePkg> {
674        let mut ret: Vec<_> = self
675            .snapshot
676            .all_packages()
677            .map(|p| p.clone().into())
678            .collect();
679        ret.extend(self.index_packages.iter().flat_map(|(id, vs)| {
680            vs.iter().map(|v| {
681                PrecisePkg::Index(PreciseIndexPkg {
682                    id: id.clone(),
683                    version: v.clone(),
684                })
685            })
686        }));
687        ret.sort();
688        ret.dedup();
689        ret
690    }
691}