Skip to content

Commit bfd6776

Browse files
authored
Tweak solver's same-origin heuristic (#2208)
Previously, we'd always prefer providers of constraints that come from the same origin as a package we'd already selected. This made it very difficult to move a package to a different origin, as the heuristic would select the older version of an APK that matched the same origin. This change tracks not just origins we've already selected, but the version of that origin, so we don't end up stuck on an older version. Signed-off-by: Jon Johnson <jon.johnson@chainguard.dev>
1 parent 0e4728d commit bfd6776

3 files changed

Lines changed: 56 additions & 14 deletions

File tree

pkg/apk/apk/repo.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -569,11 +569,11 @@ func (p *PkgResolver) GetPackagesWithDependencies(ctx context.Context, packages
569569
func (p *PkgResolver) GetPackageWithDependencies(ctx context.Context, pkgName string, existing map[string]*RepositoryPackage, dq map[*RepositoryPackage]string) (*RepositoryPackage, []*RepositoryPackage, []string, error) {
570570
parents := make(map[string]bool)
571571
localExisting := make(map[string]*RepositoryPackage, len(existing))
572-
existingOrigins := map[string]bool{}
572+
existingOrigins := map[string]string{}
573573
for k, v := range existing {
574574
localExisting[k] = v
575575
if v != nil && v.Origin != "" {
576-
existingOrigins[v.Origin] = true
576+
existingOrigins[v.Origin] = v.Version
577577
}
578578
}
579579

@@ -712,7 +712,7 @@ func (p *PkgResolver) resolvePackage(pkgName string, dq map[*RepositoryPackage]s
712712
// It might change the order of install.
713713
// In other words, this _should_ be a DAG (acyclical), but because the packages
714714
// are just listing dependencies in text, it might be cyclical. We need to be careful of that.
715-
func (p *PkgResolver) getPackageDependencies(ctx context.Context, pkg *RepositoryPackage, allowPin string, parents map[string]bool, existing map[string]*RepositoryPackage, existingOrigins map[string]bool, dq map[*RepositoryPackage]string) (dependencies []*RepositoryPackage, conflicts []string, err error) {
715+
func (p *PkgResolver) getPackageDependencies(ctx context.Context, pkg *RepositoryPackage, allowPin string, parents map[string]bool, existing map[string]*RepositoryPackage, existingOrigins map[string]string, dq map[*RepositoryPackage]string) (dependencies []*RepositoryPackage, conflicts []string, err error) {
716716
if err := ctx.Err(); err != nil {
717717
return nil, nil, context.Cause(ctx)
718718
}
@@ -902,7 +902,9 @@ func (p *PkgResolver) getPackageDependencies(ctx context.Context, pkg *Repositor
902902
conflicts = append(conflicts, confs...)
903903
for _, dep := range subDeps {
904904
existing[dep.Name] = dep
905-
existingOrigins[dep.Origin] = true
905+
if dep.Origin != "" {
906+
existingOrigins[dep.Origin] = dep.Version
907+
}
906908
}
907909
}
908910
return dependencies, conflicts, nil
@@ -944,11 +946,11 @@ func cachedResolvePackageNameVersionPin(pkgName string) ParsedConstraint {
944946
// For example, if the original search was for package "a", then pkgs may contain some that
945947
// are named "a", but others that provided "a". In that case, we should look not at the
946948
// version of the package, but the version of "a" that the package provides.
947-
func (p *PkgResolver) sortPackages(pkgs []*repositoryPackage, compare *RepositoryPackage, name string, existing map[string]*RepositoryPackage, existingOrigins map[string]bool, pin string) {
949+
func (p *PkgResolver) sortPackages(pkgs []*repositoryPackage, compare *RepositoryPackage, name string, existing map[string]*RepositoryPackage, existingOrigins map[string]string, pin string) {
948950
slices.SortFunc(pkgs, p.comparePackages(compare, name, existing, existingOrigins, pin))
949951
}
950952

951-
func (p *PkgResolver) comparePackages(compare *RepositoryPackage, name string, existing map[string]*RepositoryPackage, existingOrigins map[string]bool, pin string) func(a, b *repositoryPackage) int { //nolint:gocyclo
953+
func (p *PkgResolver) comparePackages(compare *RepositoryPackage, name string, existing map[string]*RepositoryPackage, existingOrigins map[string]string, pin string) func(a, b *repositoryPackage) int { //nolint:gocyclo
952954
return func(a, b *repositoryPackage) int {
953955
// determine versions
954956
iVersionStr := p.getDepVersionForName(a, name)
@@ -990,9 +992,12 @@ func (p *PkgResolver) comparePackages(compare *RepositoryPackage, name string, e
990992
}
991993
// both matched, so keep looking
992994

993-
// see if an origin already is installed
994-
iOriginMatched := existingOrigins[a.Origin]
995-
jOriginMatched := existingOrigins[b.Origin]
995+
// Prefer a candidate whose origin we've already pulled in, but only at
996+
// the version we pulled in. Otherwise this heuristic would keep us on
997+
// an older version of an origin (e.g. when a package moves origins at
998+
// a newer version, or an old binary lingers in the index after rebuild).
999+
iOriginMatched := a.Origin != "" && existingOrigins[a.Origin] == a.Version
1000+
jOriginMatched := b.Origin != "" && existingOrigins[b.Origin] == b.Version
9961001
if iOriginMatched && !jOriginMatched {
9971002
return -1
9981003
}
@@ -1052,7 +1057,7 @@ func (p *PkgResolver) comparePackages(compare *RepositoryPackage, name string, e
10521057
}
10531058
}
10541059

1055-
func (p *PkgResolver) bestPackage(pkgs []*repositoryPackage, compare *RepositoryPackage, name string, existing map[string]*RepositoryPackage, existingOrigins map[string]bool, pin string) *repositoryPackage {
1060+
func (p *PkgResolver) bestPackage(pkgs []*repositoryPackage, compare *RepositoryPackage, name string, existing map[string]*RepositoryPackage, existingOrigins map[string]string, pin string) *repositoryPackage {
10561061
if len(pkgs) == 0 {
10571062
return nil
10581063
}

pkg/apk/apk/repo_test.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,7 @@ func TestSortPackages(t *testing.T) {
764764
pkgs []*RepositoryPackage
765765
pkg *RepositoryPackage
766766
existing = map[string]*RepositoryPackage{}
767-
existingOrigins = map[string]bool{}
767+
existingOrigins = map[string]string{}
768768
)
769769
for _, pkg := range tt.pkgs {
770770
// we cheat and use the InstalledSize for the preferred order, so that it gets carried around.
@@ -778,7 +778,7 @@ func TestSortPackages(t *testing.T) {
778778
}
779779
for _, pkg := range tt.existing {
780780
existing[pkg.pkg.Name] = NewRepositoryPackage(pkg.pkg, &RepositoryWithIndex{Repository: &Repository{URI: pkg.repo}})
781-
existingOrigins[pkg.pkg.Origin] = true
781+
existingOrigins[pkg.pkg.Origin] = pkg.pkg.Version
782782
}
783783
namedPkgs := testNamedPackageFromPackages(pkgs)
784784
pr := NewPkgResolver(context.Background(), []NamedIndex{})
@@ -900,6 +900,43 @@ func TestHigherProvidedVersion(t *testing.T) {
900900
}
901901
}
902902

903+
// When an origin is bumped to a new version that no longer ships one of its
904+
// old subpackages, but the old subpackage still lingers in the index providing
905+
// some so:, the resolver must not prefer that stale package over a fresh
906+
// provider in a different origin just because we already pulled the origin at
907+
// a different version.
908+
//
909+
// As a contrived example, if we want to drop libcrypt1 from glibc's origin,
910+
// it was difficult because we would prefer the libcrypt.so.1 provider due to
911+
// that same-origin heuristic. This tests that the heuristic does not activate
912+
// if the origins match but the versions don't.
913+
func TestProviderAcrossOriginVersionBump(t *testing.T) {
914+
repo := Repository{}
915+
index := repo.WithIndex(&APKIndex{
916+
Packages: []*Package{
917+
{Name: "glibc", Version: "1", Origin: "glibc"},
918+
{Name: "libcrypt1", Version: "1", Origin: "glibc",
919+
Provides: []string{"so:libcrypt.so.1=1"}},
920+
{Name: "glibc", Version: "2", Origin: "glibc"},
921+
{Name: "libxcrypt", Version: "2", Origin: "libxcrypt",
922+
Provides: []string{"so:libcrypt.so.1=1"}},
923+
{Name: "consumer", Version: "1",
924+
Dependencies: []string{"so:libcrypt.so.1"}},
925+
},
926+
})
927+
resolver := NewPkgResolver(context.Background(), testNamedRepositoryFromIndexes([]*RepositoryWithIndex{index}))
928+
pkgs, _, err := resolver.GetPackagesWithDependencies(context.Background(), []string{"consumer", "glibc=2"}, nil)
929+
require.NoError(t, err)
930+
931+
got := make([]string, 0, len(pkgs))
932+
for _, p := range pkgs {
933+
got = append(got, p.Filename())
934+
}
935+
require.NotContains(t, got, "libcrypt1-1.apk", "should not pull stale libcrypt1 from old glibc origin")
936+
require.Contains(t, got, "libxcrypt-2.apk", "should select libxcrypt for so:libcrypt.so.1")
937+
require.Contains(t, got, "glibc-2.apk")
938+
}
939+
903940
func TestConstrains(t *testing.T) {
904941
providers := map[string][]string{
905942
"ld-linux=2.38-r10": {"so:ld-linux-aarch64.so.1=1.0"},

pkg/apk/apk/version_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -910,10 +910,10 @@ func TestResolveVersion(t *testing.T) {
910910
found := filterPackages(pkgs, map[*RepositoryPackage]string{}, withVersion(tt.version, tt.compare), withPreferPin(tt.pin), withInstalledPackage(tt.installed))
911911
// add the existing in, if any
912912
existing := make(map[string]*RepositoryPackage)
913-
existingOrigins := make(map[string]bool)
913+
existingOrigins := make(map[string]string)
914914
if tt.installed != nil {
915915
existing[tt.installed.Name] = tt.installed
916-
existingOrigins[tt.installed.Origin] = true
916+
existingOrigins[tt.installed.Origin] = tt.installed.Version
917917
}
918918
pkg := pr.bestPackage(found, nil, "", existing, existingOrigins, tt.pin)
919919
if tt.want == "" {

0 commit comments

Comments
 (0)