diff --git a/internal/classify/classify.go b/internal/classify/classify.go index d4f5abb..22e52c2 100644 --- a/internal/classify/classify.go +++ b/internal/classify/classify.go @@ -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`)}, diff --git a/internal/classify/classify_test.go b/internal/classify/classify_test.go index e48acd0..2d1f9f7 100644 --- a/internal/classify/classify_test.go +++ b/internal/classify/classify_test.go @@ -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", diff --git a/internal/classifypkg/classifypkg.go b/internal/classifypkg/classifypkg.go index 6a804ec..1c96164 100644 --- a/internal/classifypkg/classifypkg.go +++ b/internal/classifypkg/classifypkg.go @@ -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, }) } diff --git a/internal/storage/legacy.go b/internal/storage/legacy.go index 5eaaa98..8bfff8b 100644 --- a/internal/storage/legacy.go +++ b/internal/storage/legacy.go @@ -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{}