Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ interaction uses `oras-go` v2 with Docker's credential store (ADR-4).

| Namespace | Features | Source |
|---|---|---|
| `x86_64` | `level` = `v4`/`v3`/`v2`/`v1` (preference-ordered, all supported levels emitted) | CPUID via `golang.org/x/sys/cpu` |
| `aarch64` | `arch` = `v8` | runtime GOARCH |
| `x86_64` | `level` = `v4`/`v3`/`v2`/`v1` (preference-ordered, all supported levels emitted); plus `<cpu-flag>` = `on` per supported flag in the WheelNext x86_64 provider feature list (`avx2`, `sha_ni`, …) | CPUID via `golang.org/x/sys/cpu`; `/proc/cpuinfo` flags (Linux) |
| `aarch64` | `version` = `9.0a`/`8.5a`/`8.4a`/…/`8a` (preference-ordered, all supported versions emitted; WheelNext aarch64 provider value scheme); plus `<cpu-flag>` = `on` per supported flag in the WheelNext aarch64 provider feature list (`sve2`, `i8mm`, …) | HWCAP via `golang.org/x/sys/cpu`; `/proc/cpuinfo` Features (Linux) |
| `nvidia` | `cuda_version_lower_bound` = all known CUDA versions ≤ detected, descending | `nvidia-smi` header parse |
| (mock) | anything | `--properties-file` / `DOCKER_VARIANT_PROPERTIES_FILE` |

Expand Down
124 changes: 121 additions & 3 deletions pkg/providers/aarch64.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ package providers
import (
"context"
"runtime"

"golang.org/x/sys/cpu"
)

// aarch64Provider reports a minimal ARM64 architecture property. Finer
// detection (v9, SVE, …) is future work; "v8" is the baseline every Go
// aarch64Provider reports the Arm architecture versions supported by the
// host CPU, as the feature "version" with values "9.0a" > "8.4a" > … > "8a"
// (all supported versions are emitted, preference-ordered), plus one
// property per supported CPU flag ("sve2" = "on", …) from the WheelNext
// reference feature list. The value scheme follows the WheelNext aarch64
// provider plugin: "armv9.0a" → "9.0a", with "8a" as the baseline every Go
// arm64 binary can assume.
type aarch64Provider struct{}

Expand All @@ -16,5 +22,117 @@ func (aarch64Provider) Detect(ctx context.Context) (map[string][]string, error)
if runtime.GOARCH != "arm64" {
return nil, nil
}
return map[string][]string{"arch": {"v8"}}, nil
features := onFeatures(aarch64RefFeatures, hostCPUFlags())
features["version"] = supportedVersions(hostARMFeatures())
return features, nil
}

// aarch64RefFeatures mirrors all_features of the WheelNext aarch64 provider
// plugin (wheelnext/provider-variant-aarch64), preference-ordered; the names
// are the Linux hwcap vocabulary used by archspec. Upstream's "btiecv" and
// "asimdrdmatomics" entries are concatenations of the archspec features
// listed separately here.
var aarch64RefFeatures = []string{
// neoverse_n2/v2 (armv9.0) features
"sve2",
"flagm2",
"frint",
"sb",
// m2 (armv8.5) features
"bti",
"ecv",
// m1 (armv8.4) features
"paca",
"pacg",
"ssbs",
// neoverse_v1 (armv8.4) features
"asimdfhm",
"bf16",
"dcpodp",
"dgh",
"dit",
"flagm",
"i8mm",
"ilrcpc",
"jscvt",
"rng",
"sha3",
"sha512",
"svebf16",
"svei8mm",
"uscat",
// neoverse_n1 (armv8.2) features
"asimddp",
"lrcpc",
// a64fx (armv8.2) features
"asimdhp",
"dcpop",
"fcma",
"fphp",
"sve",
// thunderx2 (armv8.1) features
"asimdrdm",
"atomics",
// cortex_a72 (armv8.0) features
"aes",
"asimd",
"cpuid",
"crc32",
"evtstrm",
"fp",
"pmull",
"sha1",
"sha2",
}

// armFeatures is the subset of HWCAP flags consulted by supportedVersions,
// extracted as a struct so tests can construct arbitrary CPUs.
type armFeatures struct {
ATOMICS, ASIMDRDM, CRC32 bool
DCPOP bool
JSCVT, FCMA, LRCPC bool
DIT, ASIMDFHM bool
SVE2 bool
}

func hostARMFeatures() armFeatures {
return armFeatures{
ATOMICS: cpu.ARM64.HasATOMICS, ASIMDRDM: cpu.ARM64.HasASIMDRDM,
CRC32: cpu.ARM64.HasCRC32,
DCPOP: cpu.ARM64.HasDCPOP,
JSCVT: cpu.ARM64.HasJSCVT, FCMA: cpu.ARM64.HasFCMA, LRCPC: cpu.ARM64.HasLRCPC,
DIT: cpu.ARM64.HasDIT, ASIMDFHM: cpu.ARM64.HasASIMDFHM,
SVE2: cpu.ARM64.HasSVE2,
}
}

// supportedVersions computes the Armv8.x/v9.x version ladder from the
// mandatory features of each revision, limited to the flags golang.org/x/sys
// exposes: v8.1 FEAT_LSE/RDM/CRC32, v8.2 FEAT_DPB, v8.3
// FEAT_JSCVT/FCMA/LRCPC, v8.4 FEAT_DIT/FHM, v9.0 FEAT_SVE2. Armv8.5–v8.6
// have no detectable mandatory flags here, but Armv9.0 compliance implies
// Armv8.5, so "8.5a" is emitted alongside "9.0a".
func supportedVersions(f armFeatures) []string {
v81 := f.ATOMICS && f.ASIMDRDM && f.CRC32
v82 := v81 && f.DCPOP
v83 := v82 && f.JSCVT && f.FCMA && f.LRCPC
v84 := v83 && f.DIT && f.ASIMDFHM
v90 := v84 && f.SVE2
versions := []string{"8a"}
if v81 {
versions = append([]string{"8.1a"}, versions...)
}
if v82 {
versions = append([]string{"8.2a"}, versions...)
}
if v83 {
versions = append([]string{"8.3a"}, versions...)
}
if v84 {
versions = append([]string{"8.4a"}, versions...)
}
if v90 {
versions = append([]string{"9.0a", "8.5a"}, versions...)
}
return versions
}
62 changes: 62 additions & 0 deletions pkg/providers/cpuinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package providers

import (
"bufio"
"io"
"os"
"strings"
)

// Per-feature detection shared by the CPU providers. The WheelNext reference
// providers (provider-variant-x86-64, provider-variant-aarch64) emit each
// supported CPU feature as its own property (feature name -> "on"), with
// names taken from the Linux /proc/cpuinfo flag vocabulary via archspec. We
// read the same source directly.

// hostCPUFlags returns the feature tokens the kernel reports in
// /proc/cpuinfo ("flags" on x86, "Features" on arm64), or nil on hosts
// without it (non-Linux) — there, per-feature properties are simply omitted
// and only the level/version ladder is detected.
func hostCPUFlags() map[string]bool {
f, err := os.Open("/proc/cpuinfo")
if err != nil {
return nil
}
defer f.Close()
return parseCPUFlags(f)
}

// parseCPUFlags extracts the tokens of the first "flags" (x86) or
// "Features" (arm64) line of a /proc/cpuinfo stream.
func parseCPUFlags(r io.Reader) map[string]bool {
sc := bufio.NewScanner(r)
for sc.Scan() {
key, rest, ok := strings.Cut(sc.Text(), ":")
if !ok {
continue
}
switch strings.TrimSpace(key) {
case "flags", "Features":
flags := map[string]bool{}
for _, tok := range strings.Fields(rest) {
flags[tok] = true
}
return flags
}
}
return nil
}

// onFeatures maps every reference feature present on the host to the single
// value "on", mirroring the reference providers' per-feature properties.
// Host flags outside the reference list are ignored so the emitted feature
// vocabulary stays identical to the reference providers'.
func onFeatures(ref []string, host map[string]bool) map[string][]string {
features := map[string][]string{}
for _, f := range ref {
if host[f] {
features[f] = []string{"on"}
}
}
return features
}
76 changes: 76 additions & 0 deletions pkg/providers/providers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package providers
import (
"context"
"errors"
"maps"
"os"
"path/filepath"
"slices"
"strings"
"testing"
)

Expand Down Expand Up @@ -35,6 +37,80 @@ func TestSupportedLevels(t *testing.T) {
}
}

func TestSupportedVersions(t *testing.T) {
v81CPU := armFeatures{ATOMICS: true, ASIMDRDM: true, CRC32: true}
v82CPU := v81CPU
v82CPU.DCPOP = true
v83CPU := v82CPU
v83CPU.JSCVT, v83CPU.FCMA, v83CPU.LRCPC = true, true, true
v84CPU := v83CPU
v84CPU.DIT, v84CPU.ASIMDFHM = true, true
v90CPU := v84CPU
v90CPU.SVE2 = true
sve2Only := armFeatures{SVE2: true} // missing v8.1–v8.4 prerequisites

cases := []struct {
name string
cpu armFeatures
want []string
}{
{"bare", armFeatures{}, []string{"8a"}},
{"v8.1", v81CPU, []string{"8.1a", "8a"}},
{"v8.2", v82CPU, []string{"8.2a", "8.1a", "8a"}},
{"v8.3", v83CPU, []string{"8.3a", "8.2a", "8.1a", "8a"}},
{"v8.4", v84CPU, []string{"8.4a", "8.3a", "8.2a", "8.1a", "8a"}},
{"v9.0", v90CPU, []string{"9.0a", "8.5a", "8.4a", "8.3a", "8.2a", "8.1a", "8a"}},
{"sve2 without v8.4 base", sve2Only, []string{"8a"}},
}
for _, c := range cases {
if got := supportedVersions(c.cpu); !slices.Equal(got, c.want) {
t.Errorf("%s: supportedVersions = %v, want %v", c.name, got, c.want)
}
}
}

func TestParseCPUFlags(t *testing.T) {
x86 := `processor : 0
vendor_id : GenuineIntel
flags : fpu vme sse sse2 ssse3 sse4_1 sse4_2 avx avx2 sha_ni
bugs : spectre_v1 spectre_v2
`
arm := `processor : 0
Features : fp asimd evtstrm aes sve sve2 i8mm bf16
CPU implementer : 0x41
`
got := parseCPUFlags(strings.NewReader(x86))
for _, f := range []string{"sse4_2", "sha_ni", "avx2"} {
if !got[f] {
t.Errorf("x86 flags: %q missing from %v", f, got)
}
}
if got["spectre_v1"] {
t.Error("x86 flags: bugs line must not be parsed as flags")
}
got = parseCPUFlags(strings.NewReader(arm))
for _, f := range []string{"sve2", "bf16", "asimd"} {
if !got[f] {
t.Errorf("arm Features: %q missing from %v", f, got)
}
}
if parseCPUFlags(strings.NewReader("no flag line here")) != nil {
t.Error("no flags line: want nil")
}
}

func TestOnFeatures(t *testing.T) {
host := map[string]bool{"sve2": true, "aes": true, "weird_vendor_flag": true}
got := onFeatures(aarch64RefFeatures, host)
want := map[string][]string{"sve2": {"on"}, "aes": {"on"}}
if !maps.EqualFunc(got, want, slices.Equal) {
t.Errorf("onFeatures = %v, want %v", got, want)
}
if got := onFeatures(x86RefFeatures, nil); len(got) != 0 {
t.Errorf("nil host flags: want empty map, got %v", got)
}
}

func TestParseNvidiaSMI(t *testing.T) {
banner := `Mon Jul 27 10:00:00 2026
+-----------------------------------------------------------------------------------------+
Expand Down
Loading
Loading