Skip to content

apk: repo: prefer same-origin/same-version providers for virtual packages#2098

Draft
aborrero wants to merge 2 commits intochainguard-dev:mainfrom
aborrero:arturo-apk-repo-same-origin-version-preference
Draft

apk: repo: prefer same-origin/same-version providers for virtual packages#2098
aborrero wants to merge 2 commits intochainguard-dev:mainfrom
aborrero:arturo-apk-repo-same-origin-version-preference

Conversation

@aborrero
Copy link
Copy Markdown

Summary

When selecting a provider for a virtual package, the resolver now gives
highest priority to candidates that share both the origin and the exact
package version
with an already-selected package.

This models the common pattern where a single source package (e.g.
postgresql) produces multiple binary packages at the same version (e.g.
postgresql-15=15.3-r1 and libpq-15=15.3-r1). Packages from the same
source build are most likely mutually compatible, so they should be preferred
over a newer release of the same library from the same origin.

The desired provider ordering for a virtual like libpq when
postgresql-15=15.3-r1 is already selected becomes:

libpq-15=15.3-r1  ← same origin + same version   (new tier 2)
libpq-14=…        ← same origin + same version for postgresql-14 if also selected
libpq-18=latest   ← same origin, different version (existing tier 3)
libpq-17=latest
…
libpq-13=latest

Implementation

  • existingOrigins changes from map[string]bool to
    map[string]map[string]bool so that multiple packages from the same
    origin at different versions (e.g. postgresql-14 and postgresql-15
    installed simultaneously) are all tracked correctly.
  • addExistingOrigin() populates the nested map safely, guarding against
    empty-origin packages.
  • cloneExistingOrigins() deep-clones the nested map for snapshots,
    replacing the previous shallow maps.Clone() call.
  • A new priority tier is inserted in comparePackages between the existing
    "exact name+version already installed" and "same origin any version" tiers.

Priority tiers in comparePackages after this change

# Condition
1 Exact package name + version already installed (existing)
2 Same origin + same package version as an installed package (new)
3 Same origin, any version (existing)
4 Version comparison (existing)

Test plan

  • TestSameOriginVersionPreferredOverNewer — unit test via sortPackages confirming the new tier ranks the version-matched candidate first even when a newer version from the same origin exists
  • TestSameOriginVersionEndToEnd — integration test through GetPackagesWithDependencies confirming that when postgresql-15=15.3-r1 is a top-level package, libpq-15=15.3-r1 is selected over libpq-15=15.4-r0
  • Full existing test suite passes unchanged

🤖 Generated with Claude Code

…resolution

We have identified a situation in which apko dependency resolver doesn't correctly
resolve virtual packages competing to satisfy the dependency tree.

Take this scenario as example:
 * an image installs package-A and package-B
 * package-A depends on so:libfoo (unversioned)
 * package-B depends on so:libfoo-15 (versioned)
 * both so:libfoo and so:libfoo-15 can be satisfied by the libfoo-15 package
 * but there is also libfoo-16, which also provides so:libfoo (unversioned)

In this scenario, before this patch, apko resolve will happily install libfoo-16
and thus break installation of package-B, resulting in an image build failure.

This patch introduces a "backtracking" logic that re-evaluates the dependency three
when this case is found. This is achieved with this strategy:

 * by introducing the notion of "retryable" errors in the resolver loop, and
 * snapshotting the resolver state when evaluating candidate packages
 * evaluate package candidates and if a retryable error is found, restore resolver state from the snapshot

This change has been co-authored with Claude Code.

Signed-off-by: Arturo Borrero Gonzalez <arturo.borrero@chainguard.dev>
…ages

When selecting a provider for a virtual package, the resolver now gives
the highest priority to candidates that share both the origin and the
exact package version with an already-selected package.

This models the common pattern where a source package (e.g. postgresql)
produces multiple binary packages at the same version (e.g.
postgresql-15=15.3-r1 and libpq-15=15.3-r1). Packages from the same
source build are most likely to be mutually compatible, so they should
be preferred over a newer release of the same library from the same
origin.

The priority tiers in comparePackages are now:
  1. exact package name+version already installed       (existing)
  2. same origin + same package version as installed    (new)
  3. same origin, any version                           (existing)
  4. version comparison                                 (existing)

Implementation details:
  - existingOrigins changes from map[string]bool to
    map[string]map[string]bool so that multiple versions from the
    same origin (e.g. postgresql-14 and postgresql-15 installed
    simultaneously) are all tracked correctly.
  - addExistingOrigin() is a helper that populates the nested map
    safely, guarding against empty-origin packages.
  - cloneExistingOrigins() deep-clones the nested map for snapshots,
    replacing the previous shallow maps.Clone() call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Signed-off-by: Arturo Borrero Gonzalez <arturo.borrero@chainguard.dev>
if packageProvidesVersion(conflict, constraint.Name, constraint.Version) {
continue
}
return fmt.Errorf("selecting package %s conflicts with %s on %q", pkg.Filename(), conflict.Filename(), constraint.Name)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fun fact, in apk-tools file are deemed to be conflicts if they are different checksums. Othwerise, I believe two packages providing identical filenames is harmless.

@xnox
Copy link
Copy Markdown
Member

xnox commented Feb 26, 2026

Ultimate tests if this:

  • fixes curl/libcurl fiasco
  • glibc fiasco
  • enables auto-selection of libpq without strict inter-dependencies

Todo:

  • see impact on stereo resolution of all build-depends
  • see impact on images art resolve / lock

@aborrero
Copy link
Copy Markdown
Author

@aborrero
Copy link
Copy Markdown
Author

I have rebased the base patch at #2098 so this needs rebase.

@aborrero aborrero marked this pull request as draft February 26, 2026 17:22
@xnox
Copy link
Copy Markdown
Member

xnox commented Feb 27, 2026

This still automatically picks libpq-18 without any capping and pinning.
But I can pick and cap libpq with existing images.cue, existing resolver, and removal of explicit deps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants