Skip to content

Commit 93e029b

Browse files
EyeCantCUclaude
andcommitted
feat: detect platform compatibility from wheel tags and image libc
Stop hardcoding platform tag lists per architecture. Instead, parse wheel platform tags dynamically by checking the machine suffix and prefix (musllinux_, manylinux, linux_). Detect the image libc from /etc/os-release (ID=alpine → musl, otherwise glibc) and only accept wheels matching the correct libc. Replace the scoring system with simple binary-over-pure-python preference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cbd3302 commit 93e029b

8 files changed

Lines changed: 183 additions & 196 deletions

File tree

internal/cli/lock.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ func LockCmd(ctx context.Context, output string, archs []types.Architecture, opt
254254
return fmt.Errorf("unknown ecosystem: %s", name)
255255
}
256256
for _, arch := range archs {
257-
resolved, err := installer.Resolve(ctx, ecoConfig, arch, auth.DefaultAuthenticators)
257+
resolved, err := installer.Resolve(ctx, ecoConfig, arch, "glibc", auth.DefaultAuthenticators)
258258
if err != nil {
259259
return fmt.Errorf("resolving %s packages for %s: %w", name, arch, err)
260260
}

pkg/ecosystem/ecosystem.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"context"
1919
"fmt"
2020
"maps"
21+
"strings"
2122
"sync"
2223

2324
"chainguard.dev/apko/pkg/apk/auth"
@@ -53,7 +54,8 @@ type Installer interface {
5354
// Name returns the ecosystem name (e.g., "python").
5455
Name() string
5556
// Resolve resolves the requested packages to specific versions and URLs.
56-
Resolve(ctx context.Context, config types.EcosystemConfig, arch types.Architecture, a auth.Authenticator) ([]ResolvedPackage, error)
57+
// libc is "musl" or "glibc", detected from the image filesystem.
58+
Resolve(ctx context.Context, config types.EcosystemConfig, arch types.Architecture, libc string, a auth.Authenticator) ([]ResolvedPackage, error)
5759
// Install extracts resolved packages into the filesystem.
5860
// Returns environment variables that should be set in the image configuration.
5961
Install(ctx context.Context, fs apkfs.FullFS, packages []ResolvedPackage, config types.EcosystemConfig, a auth.Authenticator) (map[string]string, error)
@@ -108,6 +110,21 @@ func Get(name string) (Installer, bool) {
108110
return factory(), true
109111
}
110112

113+
// detectLibc checks /etc/os-release to determine the image's libc.
114+
// Alpine uses musl; everything else uses glibc.
115+
func detectLibc(fs apkfs.FullFS) string {
116+
data, err := fs.ReadFile("etc/os-release")
117+
if err != nil {
118+
return "glibc"
119+
}
120+
for line := range strings.SplitSeq(string(data), "\n") {
121+
if line == "ID=alpine" {
122+
return "musl"
123+
}
124+
}
125+
return "glibc"
126+
}
127+
111128
// OwnerTagger is implemented by filesystems that support tagging files
112129
// with an owner name for layering purposes.
113130
type OwnerTagger interface {
@@ -125,12 +142,14 @@ func InstallAll(ctx context.Context, fs apkfs.FullFS, ecosystems map[string]type
125142
env := map[string]string{}
126143
var installed []ResolvedPackage
127144

145+
libc := detectLibc(fs)
146+
128147
for name, config := range ecosystems {
129148
installer, ok := Get(name)
130149
if !ok {
131150
return nil, nil, fmt.Errorf("unknown ecosystem: %s", name)
132151
}
133-
resolved, err := installer.Resolve(ctx, config, arch, a)
152+
resolved, err := installer.Resolve(ctx, config, arch, libc, a)
134153
if err != nil {
135154
return nil, nil, fmt.Errorf("resolving %s packages: %w", name, err)
136155
}

pkg/ecosystem/python/platform.go

Lines changed: 51 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -16,69 +16,46 @@ package python
1616

1717
import (
1818
"fmt"
19-
"slices"
2019
"strings"
2120

2221
"chainguard.dev/apko/pkg/build/types"
2322
)
2423

25-
// platformTags returns the list of compatible wheel platform tags for the
26-
// given architecture, ordered from most specific to least specific.
27-
func platformTags(arch types.Architecture) []string {
28-
switch arch {
29-
case types.ParseArchitecture("amd64"):
30-
return []string{
31-
"manylinux_2_17_x86_64",
32-
"manylinux2014_x86_64",
33-
"manylinux_2_5_x86_64",
34-
"manylinux1_x86_64",
35-
"linux_x86_64",
36-
}
37-
case types.ParseArchitecture("arm64"):
38-
return []string{
39-
"manylinux_2_17_aarch64",
40-
"manylinux2014_aarch64",
41-
"linux_aarch64",
42-
}
43-
case types.ParseArchitecture("arm/v7"):
44-
return []string{
45-
"manylinux_2_17_armv7l",
46-
"manylinux2014_armv7l",
47-
"linux_armv7l",
48-
}
49-
case types.ParseArchitecture("arm/v6"):
50-
return []string{
51-
"manylinux_2_17_armv6l",
52-
"linux_armv6l",
53-
}
54-
case types.ParseArchitecture("386"):
55-
return []string{
56-
"manylinux_2_17_i686",
57-
"manylinux2014_i686",
58-
"manylinux_2_5_i686",
59-
"manylinux1_i686",
60-
"linux_i686",
61-
}
62-
case types.ParseArchitecture("ppc64le"):
63-
return []string{
64-
"manylinux_2_17_ppc64le",
65-
"manylinux2014_ppc64le",
66-
"linux_ppc64le",
67-
}
68-
case types.ParseArchitecture("s390x"):
69-
return []string{
70-
"manylinux_2_17_s390x",
71-
"manylinux2014_s390x",
72-
"linux_s390x",
73-
}
74-
case types.ParseArchitecture("riscv64"):
75-
return []string{
76-
"manylinux_2_17_riscv64",
77-
"linux_riscv64",
78-
}
79-
default:
80-
return []string{"any"}
24+
// archToMachine maps OCI architecture strings to the Python/Linux machine
25+
// string used in wheel platform tags.
26+
var archToMachine = map[types.Architecture]string{
27+
types.ParseArchitecture("amd64"): "x86_64",
28+
types.ParseArchitecture("arm64"): "aarch64",
29+
types.ParseArchitecture("arm/v7"): "armv7l",
30+
types.ParseArchitecture("arm/v6"): "armv6l",
31+
types.ParseArchitecture("386"): "i686",
32+
types.ParseArchitecture("ppc64le"): "ppc64le",
33+
types.ParseArchitecture("s390x"): "s390x",
34+
types.ParseArchitecture("riscv64"): "riscv64",
35+
types.ParseArchitecture("loong64"): "loongarch64",
36+
}
37+
38+
// isLinuxPlatformTag checks whether a single platform tag (e.g.
39+
// "musllinux_1_2_x86_64") targets the given machine architecture and
40+
// is compatible with the image's libc. musl images only accept musllinux
41+
// wheels; glibc images only accept manylinux wheels.
42+
func isLinuxPlatformTag(tag, machine string, libc string) bool {
43+
if !strings.HasSuffix(tag, "_"+machine) {
44+
return false
8145
}
46+
if tag == "linux_"+machine {
47+
return true
48+
}
49+
if libc == "musl" {
50+
return strings.HasPrefix(tag, "musllinux_")
51+
}
52+
return strings.HasPrefix(tag, "manylinux")
53+
}
54+
55+
// isBinaryWheel returns true if the wheel targets a specific platform
56+
// (not pure-python "any").
57+
func isBinaryWheel(w wheelFileParts) bool {
58+
return w.PlatformTag != "any"
8259
}
8360

8461
// wheelFileParts holds the parsed components of a wheel filename per PEP 427.
@@ -124,8 +101,8 @@ func parseWheelFilename(filename string) (wheelFileParts, error) {
124101
}
125102

126103
// isCompatibleWheel checks whether a wheel file is compatible with the given
127-
// Python version and architecture.
128-
func isCompatibleWheel(w wheelFileParts, pythonVersion string, arch types.Architecture) bool {
104+
// Python version, architecture, and libc.
105+
func isCompatibleWheel(w wheelFileParts, pythonVersion string, arch types.Architecture, libc string) bool {
129106
// Check python tag compatibility
130107
if !isCompatiblePythonTag(w.PythonTag, pythonVersion) {
131108
return false
@@ -137,7 +114,7 @@ func isCompatibleWheel(w wheelFileParts, pythonVersion string, arch types.Archit
137114
}
138115

139116
// Check platform compatibility
140-
return isCompatiblePlatform(w.PlatformTag, arch)
117+
return isCompatiblePlatform(w.PlatformTag, arch, libc)
141118
}
142119

143120
// isCompatiblePythonTag checks if the wheel's python tag is compatible.
@@ -166,56 +143,29 @@ func isCompatibleABI(tag, pythonVersion string) bool {
166143
return false
167144
}
168145

169-
// isCompatiblePlatform checks if the wheel's platform tag is compatible.
170-
func isCompatiblePlatform(tag string, arch types.Architecture) bool {
146+
// isCompatiblePlatform checks if the wheel's platform tag is compatible
147+
// with the given architecture and libc, without version limits.
148+
func isCompatiblePlatform(tag string, arch types.Architecture, libc string) bool {
171149
if tag == "any" {
172150
return true
173151
}
174-
compatible := platformTags(arch)
152+
machine, ok := archToMachine[arch]
153+
if !ok {
154+
return false
155+
}
175156
for t := range strings.SplitSeq(tag, ".") {
176-
if slices.Contains(compatible, t) {
157+
if isLinuxPlatformTag(t, machine, libc) {
177158
return true
178159
}
179160
}
180161
return false
181162
}
182163

183-
// wheelScore returns a priority score for the wheel. Higher is better.
184-
// Binary wheels for the exact platform are preferred over pure-Python wheels.
185-
func wheelScore(w wheelFileParts, pythonVersion string, arch types.Architecture) int {
186-
score := 0
187-
188-
// Prefer exact CPython tag over generic py3
189-
cpTag := "cp" + strings.ReplaceAll(pythonVersion, ".", "")
190-
for t := range strings.SplitSeq(w.PythonTag, ".") {
191-
if t == cpTag {
192-
score += 100
193-
break
194-
}
195-
}
196-
197-
// Prefer specific ABI over none/abi3
198-
for t := range strings.SplitSeq(w.ABITag, ".") {
199-
switch t {
200-
case cpTag:
201-
score += 50
202-
case "abi3":
203-
score += 25
204-
}
205-
}
206-
207-
// Prefer specific platform over any
208-
if w.PlatformTag != "any" {
209-
platTags := platformTags(arch)
210-
for i, pt := range platTags {
211-
for pp := range strings.SplitSeq(w.PlatformTag, ".") {
212-
if pp == pt {
213-
// More specific platforms (earlier in list) get higher scores
214-
score += 10 * (len(platTags) - i)
215-
}
216-
}
217-
}
164+
// isBetterWheel returns true if candidate is a better choice than current.
165+
// Prefers binary wheels over pure-python.
166+
func isBetterWheel(current, candidate wheelFileParts) bool {
167+
if !isBinaryWheel(current) && isBinaryWheel(candidate) {
168+
return true
218169
}
219-
220-
return score
170+
return false
221171
}

0 commit comments

Comments
 (0)