fix(classify): fix ARM, ppc64el, winx64 detection; fix legacy universal2/solaris export

Classifier fixes:
- Remove Windows arm→arm64 auto-promotion; packages like caddy/fzf/goreleaser
  have genuine arm32 Windows builds (windows_armv6) that were wrongly promoted
- Add armel and gnueabihf as ARMv6 aliases (jq, caddy and others use these)
- Add winx64 to Windows OS pattern (MariaDB uses winx64 in filenames)
- Add ppc64el as ppc64le alias (Debian/Ubuntu naming, used by jq)
- Normalize armv6l → armv6 in normalizeGoArch (Go dist had armv6l filenames)
- Fix classifyGPGDist hardcoded "amd64" → buildmeta.ArchAMD64 ("x86_64")

Legacy export fixes:
- Map solaris/illumos → sunos globally (Node.js only knows "sunos")
- Expand universal2 → two entries (aarch64 + x86_64) so Hugo/cmake/gh/syncthing
  work on both Apple Silicon and Intel Mac in the legacy resolver
- Remove double-application of legacyFieldBackport (toLegacy no longer calls it)
This commit is contained in:
AJ ONeal
2026-03-11 14:54:25 -06:00
parent e60c4279d3
commit aec68692a1
4 changed files with 74 additions and 25 deletions

View File

@@ -40,14 +40,6 @@ func Filename(name string) Result {
lower := strings.ToLower(name)
os := detectOS(lower)
arch := detectArch(lower)
// On Windows, bare "arm" (detected as ARMv6) almost certainly means
// ARM64. Windows never shipped ARMv6 binaries — "ARM" became the
// marketing label for ARM64 (Windows on ARM).
if os == buildmeta.OSWindows && arch == buildmeta.ArchARMv6 {
arch = buildmeta.ArchARM64
}
format := detectFormat(lower)
// .deb, .rpm, .snap are Linux-only package formats.
@@ -80,7 +72,7 @@ var osPatterns = []struct {
}{
{buildmeta.OSDarwin, regexp.MustCompile(`(?i)(?:` + b + `(?:darwin|macos|macosx|osx|os-x|apple)` + bEnd + `|` + b + `mac` + bEnd + `)`)},
{buildmeta.OSLinux, regexp.MustCompile(`(?i)` + b + `linux` + bEnd)},
{buildmeta.OSWindows, regexp.MustCompile(`(?i)` + b + `(?:windows|win(?:32|64|dows)?)` + bEnd + `|\.exe(?:\.xz)?$|\.msi$`)},
{buildmeta.OSWindows, regexp.MustCompile(`(?i)` + b + `(?:windows|win(?:32|64|x64|dows)?)` + bEnd + `|\.exe(?:\.xz)?$|\.msi$`)},
{buildmeta.OSFreeBSD, regexp.MustCompile(`(?i)` + b + `freebsd` + bEnd)},
{buildmeta.OSOpenBSD, regexp.MustCompile(`(?i)` + b + `openbsd` + bEnd)},
{buildmeta.OSNetBSD, regexp.MustCompile(`(?i)` + b + `netbsd` + bEnd)},
@@ -118,10 +110,11 @@ var archPatterns = []struct {
// arm64 before armv7/armv6 — "aarch64" must not match as arm.
{buildmeta.ArchARM64, regexp.MustCompile(`(?i)(?:aarch64|arm64|armv8)`)},
{buildmeta.ArchARMv7, regexp.MustCompile(`(?i)(?:armv7l?|arm-?v7|arm7|arm32|armhf)`)},
{buildmeta.ArchARMv6, regexp.MustCompile(`(?i)(?:armv6l?|arm-?v6|aarch32|` + b + `arm` + bEnd + `)`)},
// armel and gnueabihf are ARMv6 soft/hard-float ABI names used in Debian and Rust triplets.
{buildmeta.ArchARMv6, regexp.MustCompile(`(?i)(?:armv6l?|arm-?v6|aarch32|armel|gnueabihf|` + b + `arm` + bEnd + `)`)},
{buildmeta.ArchARMv5, regexp.MustCompile(`(?i)(?:armv5)`)},
// ppc64le before ppc64.
{buildmeta.ArchPPC64LE, regexp.MustCompile(`(?i)ppc64le`)},
// ppc64le before ppc64. ppc64el is an alternative spelling used in Debian/Ubuntu.
{buildmeta.ArchPPC64LE, regexp.MustCompile(`(?i)(?:ppc64le|ppc64el)`)},
{buildmeta.ArchPPC64, regexp.MustCompile(`(?i)ppc64`)},
{buildmeta.ArchRISCV64, regexp.MustCompile(`(?i)riscv64`)},
{buildmeta.ArchS390X, regexp.MustCompile(`(?i)s390x`)},

View File

@@ -151,12 +151,20 @@ func TestFilename(t *testing.T) {
format: buildmeta.FormatTarGz,
},
// Windows ARM: bare "arm" means ARM64, not ARMv6/v7
// Windows ARM: bare "arm" is armv6 (some tools ship genuine arm32 Windows builds).
// Explicit "arm64" is always aarch64 regardless of OS.
{
name: "windows arm means arm64",
name: "windows bare arm stays armv6",
input: "tool-1.0.0-windows-arm.zip",
wantOS: buildmeta.OSWindows,
arch: buildmeta.ArchARM64,
arch: buildmeta.ArchARMv6,
format: buildmeta.FormatZip,
},
{
name: "windows armv6 stays armv6",
input: "tool-2.0.0-windows-armv6.zip",
wantOS: buildmeta.OSWindows,
arch: buildmeta.ArchARMv6,
format: buildmeta.FormatZip,
},
{
@@ -167,6 +175,37 @@ func TestFilename(t *testing.T) {
format: buildmeta.FormatZip,
},
// armel and gnueabihf are ARMv6 ABI names
{
name: "armel is armv6",
input: "jq-linux-armel",
wantOS: buildmeta.OSLinux,
arch: buildmeta.ArchARMv6,
},
{
name: "gnueabihf is armv6",
input: "tool-arm-unknown-linux-gnueabihf.tar.gz",
wantOS: buildmeta.OSLinux,
arch: buildmeta.ArchARMv6,
format: buildmeta.FormatTarGz,
},
// winx64 is a Windows x86_64 naming used by MariaDB
{
name: "winx64 is windows x86_64",
input: "mariadb-11.4.5-winx64.zip",
wantOS: buildmeta.OSWindows,
arch: buildmeta.ArchAMD64,
format: buildmeta.FormatZip,
},
// ppc64el is a Debian/Ubuntu alias for ppc64le
{
name: "ppc64el is ppc64le",
input: "jq-linux-ppc64el",
arch: buildmeta.ArchPPC64LE,
},
// amd64 micro-architecture levels
{
name: "amd64v2",

View File

@@ -879,7 +879,7 @@ func normalizeGoArch(goarch string) string {
return "aarch64"
case "386":
return "x86"
case "arm":
case "arm", "armv6l":
return "armv6"
case "ppc64le":
return "ppc64le"
@@ -977,9 +977,9 @@ func classifyGPGDist(d *rawcache.Dir) ([]storage.Asset, error) {
Filename: fmt.Sprintf("GnuPG-%s.dmg", entry.Version),
Version: entry.Version,
Channel: "stable",
OS: "darwin",
Arch: "amd64",
Format: ".dmg",
OS: string(buildmeta.OSDarwin),
Arch: string(buildmeta.ArchAMD64),
Format: string(buildmeta.FormatDMG),
Download: entry.URL,
})
}

View File

@@ -51,9 +51,8 @@ func (la LegacyAsset) ToAsset() Asset {
}
// toLegacy converts an Asset to the LegacyAsset wire format.
// It applies per-package field translations for Node.js compatibility.
func (a Asset) toLegacy(pkg string) LegacyAsset {
a = legacyFieldBackport(pkg, a)
// Callers must have already applied legacyFieldBackport before calling this.
func (a Asset) toLegacy() LegacyAsset {
return LegacyAsset{
Name: a.Filename,
Version: a.Version,
@@ -72,12 +71,18 @@ func (a Asset) toLegacy(pkg string) LegacyAsset {
// values the legacy Node.js resolver expects. This is called at export time
// only — the canonical values are preserved in Go-native storage (pgstore).
//
// Rules are package-specific because they replicate per-package overrides
// that production's releases.js files apply:
// Global rules (all packages):
// - solaris/illumos → sunos (Node.js only knows "sunos")
//
// Package-specific rules replicate per-package overrides in production's releases.js:
// - go: armv6 → arm (Go dist API uses bare "arm"; prod keeps it as-is)
// - ffmpeg: Windows .gz → .exe (prod releases.js: rel.ext = 'exe')
func legacyFieldBackport(pkg string, a Asset) Asset {
// Global OS normalization: Node.js uses "sunos" for both Solaris and Illumos.
if a.OS == "solaris" || a.OS == "illumos" {
a.OS = "sunos"
}
switch pkg {
case "go":
if a.Arch == "armv6" {
@@ -149,7 +154,19 @@ func ExportLegacy(pkg string, pd PackageData) (LegacyCache, LegacyDropStats) {
stats.Formats++
continue
}
releases = append(releases, a.toLegacy(pkg))
// universal2 (macOS fat binary) → expand to aarch64 + x86_64.
// Node.js doesn't know "universal2"; emitting both arches ensures
// the binary is found for both Apple Silicon and Intel Mac clients.
if a.Arch == "universal2" {
arm := a
arm.Arch = "aarch64"
releases = append(releases, arm.toLegacy())
intel := a
intel.Arch = "x86_64"
releases = append(releases, intel.toLegacy())
continue
}
releases = append(releases, a.toLegacy())
}
if releases == nil {
releases = []LegacyAsset{}