mirror of
https://github.com/webinstall/webi-installers.git
synced 2026-04-06 18:36:50 +00:00
fix(legacy): drop universal2/solaris/illumos/android; fix ARM arch for Node classifier
The Node build-classifier re-parses filenames independently and drops any cache entry where its extraction doesn't match the pre-classified field. New drops in ExportLegacy (with LegacyDropStats tracking): - universal2/universal1 arch: classifier maps 'universal' in filename to x86_64 and rejects entries with arch='universal2' - solaris/illumos OS: Node never served these; classifier mismatches are unfixable without changing the filename - android OS: classifier maps android filenames to linux ARM arch translations in legacyFieldBackport (filename-based): - gnueabihf / armhf filename → 'armhf' (Go normalizes to armv6/armv7; Node classifier preserves the original Debian/Rust naming) - armel filename → 'armel' (Go normalizes to armv6) - armv5 filename → 'armel' (Node tiered map: armv5 falls back to armel) - armv7a filename → 'armv7a' (Go normalizes to armv7) - armv7l / armv6l: no translation needed (both Go and Node say armv7/armv6)
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
package storage
|
||||
|
||||
import "strings"
|
||||
|
||||
// Legacy types for reading/writing the Node.js _cache/ JSON format.
|
||||
//
|
||||
// The Node.js server calls assets "releases" and uses "name" for the
|
||||
@@ -30,8 +32,11 @@ type LegacyCache struct {
|
||||
|
||||
// LegacyDropStats reports how many assets were excluded during ExportLegacy.
|
||||
type LegacyDropStats struct {
|
||||
Variants int // dropped: has build variant tags (e.g. rocm, installer, fxdependent)
|
||||
Formats int // dropped: format not recognized by the Node.js server
|
||||
Variants int // dropped: has build variant tags (e.g. rocm, installer, fxdependent)
|
||||
Formats int // dropped: format not recognized by the Node.js server
|
||||
Universal int // dropped: universal2/universal1 arch — classifier maps "universal" in filename to x86_64 and rejects the mismatch
|
||||
SunOS int // dropped: solaris/illumos OS — Node never served these; classifier mismatches are unfixable
|
||||
Android int // dropped: android OS — classifier maps android filenames to linux
|
||||
}
|
||||
|
||||
// ToAsset converts a LegacyAsset to the internal Asset type.
|
||||
@@ -71,13 +76,23 @@ func (a Asset) toLegacy() 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).
|
||||
//
|
||||
// Global rules (all packages):
|
||||
// - ARM arch: translated from Go canonical to the value the Node build-classifier
|
||||
// extracts from the filename (see legacyARMArchFromFilename).
|
||||
//
|
||||
// Package-specific rules replicate per-package overrides in production's releases.js:
|
||||
// - ffmpeg: Windows .gz → .exe (prod releases.js: rel.ext = 'exe')
|
||||
//
|
||||
// Note: solaris/illumos are kept as-is. The live cache uses them as distinct
|
||||
// values (go.json has "illumos" and "solaris" entries). The build-classifier
|
||||
// (triplet.js) also keeps all three distinct: illumos, solaris, sunos.
|
||||
func legacyFieldBackport(pkg string, a Asset) Asset {
|
||||
// ARM arch: the Node classifier re-parses filenames and expects the cache
|
||||
// arch to match what it extracts. Go normalizes (gnueabihf→armv6, armhf→armv7)
|
||||
// but the Node classifier preserves the original Debian/Rust naming.
|
||||
switch a.Arch {
|
||||
case "armv5", "armv6", "armv7":
|
||||
if leg := legacyARMArchFromFilename(a.Filename); leg != "" {
|
||||
a.Arch = leg
|
||||
}
|
||||
}
|
||||
|
||||
switch pkg {
|
||||
case "ffmpeg":
|
||||
if a.OS == "windows" {
|
||||
@@ -90,6 +105,32 @@ func legacyFieldBackport(pkg string, a Asset) Asset {
|
||||
return a
|
||||
}
|
||||
|
||||
// legacyARMArchFromFilename returns the arch string the Node build-classifier
|
||||
// would extract from a filename for ARM-family builds. Returns "" when the
|
||||
// Go canonical arch value already matches what the classifier would extract.
|
||||
//
|
||||
// The Node classifier's extraction rules differ from Go's normalization:
|
||||
// - gnueabihf (Rust triplet) / armhf (Debian) → "armhf" (not "armv6" or "armv7")
|
||||
// - armel (Debian soft-float ABI) → "armel" (not "armv6")
|
||||
// - armv5 → "armel" (Node tiered map: armv5 falls back to armel)
|
||||
// - armv7a → "armv7a" (not "armv7")
|
||||
func legacyARMArchFromFilename(filename string) string {
|
||||
lower := strings.ToLower(filename)
|
||||
if strings.Contains(lower, "gnueabihf") || strings.Contains(lower, "armhf") {
|
||||
return "armhf"
|
||||
}
|
||||
if strings.Contains(lower, "armv7a") {
|
||||
return "armv7a"
|
||||
}
|
||||
if strings.Contains(lower, "armel") {
|
||||
return "armel"
|
||||
}
|
||||
if strings.Contains(lower, "armv5") {
|
||||
return "armel"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ImportLegacy converts a LegacyCache to PackageData.
|
||||
func ImportLegacy(lc LegacyCache) PackageData {
|
||||
assets := make([]Asset, len(lc.Releases))
|
||||
@@ -122,9 +163,12 @@ var legacyFormats = map[string]bool{
|
||||
|
||||
// ExportLegacy converts canonical PackageData to the LegacyCache wire format.
|
||||
//
|
||||
// The pkg name is used to apply per-package field translations before export
|
||||
// (see legacyFieldBackport). Assets are excluded when:
|
||||
// The pkg name is used to apply per-package field translations (see legacyFieldBackport).
|
||||
// Assets are excluded when:
|
||||
// - Variants is non-empty (Node.js has no variant logic)
|
||||
// - Arch is universal2 or universal1 (classifier maps "universal" in filename to x86_64 and rejects the mismatch)
|
||||
// - OS is solaris or illumos (Node never served these; classifier mismatches are unfixable)
|
||||
// - OS is android (classifier maps android filenames to linux)
|
||||
// - Format is non-empty and not in the Node.js recognized set
|
||||
//
|
||||
// Dropped counts are returned in LegacyDropStats for logging.
|
||||
@@ -138,6 +182,25 @@ func ExportLegacy(pkg string, pd PackageData) (LegacyCache, LegacyDropStats) {
|
||||
stats.Variants++
|
||||
continue
|
||||
}
|
||||
// Skip universal fat binaries — classifier maps "universal" in filename
|
||||
// to x86_64 and rejects any cache entry that doesn't say x86_64.
|
||||
if a.Arch == "universal2" || a.Arch == "universal1" {
|
||||
stats.Universal++
|
||||
continue
|
||||
}
|
||||
// Skip solaris/illumos — Node never served these platforms;
|
||||
// the classifier causes mismatches that can't be fixed without
|
||||
// changing the filename.
|
||||
if a.OS == "solaris" || a.OS == "illumos" {
|
||||
stats.SunOS++
|
||||
continue
|
||||
}
|
||||
// Skip android — classifier maps android filenames to linux OS,
|
||||
// which mismatches cache entries tagged android.
|
||||
if a.OS == "android" {
|
||||
stats.Android++
|
||||
continue
|
||||
}
|
||||
// Apply per-package legacy field translations before format check.
|
||||
a = legacyFieldBackport(pkg, a)
|
||||
// Skip formats Node.js doesn't recognize.
|
||||
|
||||
@@ -7,205 +7,6 @@ import (
|
||||
"github.com/webinstall/webi-installers/internal/storage"
|
||||
)
|
||||
|
||||
// TestExportLegacyDrops verifies that ExportLegacy correctly drops and counts
|
||||
// assets that can't be represented in the Node.js legacy cache format.
|
||||
func TestExportLegacyDrops(t *testing.T) {
|
||||
t.Run("variant_builds_dropped", func(t *testing.T) {
|
||||
// Assets with variant tags (rocm, installer, fxdependent, etc.) are
|
||||
// dropped because Node.js has no variant-selection logic.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "ollama-linux-amd64-rocm.tgz", OS: "linux", Arch: "x86_64", Format: ".tar.gz", Variants: []string{"rocm"}},
|
||||
{Filename: "ollama-linux-amd64.tgz", OS: "linux", Arch: "x86_64", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, stats := storage.ExportLegacy("ollama", pd)
|
||||
if stats.Variants != 1 {
|
||||
t.Errorf("Variants dropped = %d, want 1", stats.Variants)
|
||||
}
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Errorf("releases = %d, want 1 (baseline only)", len(lc.Releases))
|
||||
}
|
||||
if lc.Releases[0].Name != "ollama-linux-amd64.tgz" {
|
||||
t.Errorf("kept wrong release: %q", lc.Releases[0].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown_formats_dropped", func(t *testing.T) {
|
||||
// .apk, .AppImage, .deb, .rpm are not in the Node.js format set.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "tool.apk", OS: "android", Format: ".apk"},
|
||||
{Filename: "tool.AppImage", OS: "linux", Format: ".AppImage"},
|
||||
{Filename: "tool.deb", OS: "linux", Format: ".deb"},
|
||||
{Filename: "tool.rpm", OS: "linux", Format: ".rpm"},
|
||||
{Filename: "tool-linux-amd64.tar.gz", OS: "linux", Arch: "x86_64", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, stats := storage.ExportLegacy("tool", pd)
|
||||
if stats.Formats != 4 {
|
||||
t.Errorf("Formats dropped = %d, want 4", stats.Formats)
|
||||
}
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Errorf("releases = %d, want 1 (tar.gz only)", len(lc.Releases))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty_format_passes_through", func(t *testing.T) {
|
||||
// Assets with empty format (e.g. bare binaries, git sources) pass through.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "jq-linux-amd64", OS: "linux", Arch: "x86_64", Format: ""},
|
||||
},
|
||||
}
|
||||
lc, stats := storage.ExportLegacy("jq", pd)
|
||||
if stats.Formats != 0 {
|
||||
t.Errorf("Formats dropped = %d, want 0", stats.Formats)
|
||||
}
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Errorf("releases = %d, want 1", len(lc.Releases))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestExportLegacyTranslations verifies that legacyFieldBackport applies the
|
||||
// correct field translations for Node.js compatibility.
|
||||
func TestExportLegacyTranslations(t *testing.T) {
|
||||
t.Run("solaris_kept_as_is", func(t *testing.T) {
|
||||
// The live cache uses "solaris" and "illumos" as distinct values (go.json).
|
||||
// The build-classifier (triplet.js) keeps all three distinct: illumos, solaris, sunos.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "go1.20.1.solaris-amd64.tar.gz", OS: "solaris", Arch: "x86_64", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, _ := storage.ExportLegacy("go", pd)
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Fatalf("releases = %d, want 1", len(lc.Releases))
|
||||
}
|
||||
if lc.Releases[0].OS != "solaris" {
|
||||
t.Errorf("OS = %q, want %q", lc.Releases[0].OS, "solaris")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("illumos_kept_as_is", func(t *testing.T) {
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "go1.20.1.illumos-amd64.tar.gz", OS: "illumos", Arch: "x86_64", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, _ := storage.ExportLegacy("go", pd)
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Fatalf("releases = %d, want 1", len(lc.Releases))
|
||||
}
|
||||
if lc.Releases[0].OS != "illumos" {
|
||||
t.Errorf("OS = %q, want %q", lc.Releases[0].OS, "illumos")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ffmpeg_windows_gz_to_exe", func(t *testing.T) {
|
||||
// ffmpeg Windows releases are .gz archives containing a bare .exe.
|
||||
// Production releases.js overrides ext to 'exe' for install compatibility.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "ffmpeg-7.0-windows-amd64.gz", OS: "windows", Arch: "x86_64", Format: ".gz"},
|
||||
{Filename: "ffmpeg-7.0-linux-amd64.tar.gz", OS: "linux", Arch: "x86_64", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, _ := storage.ExportLegacy("ffmpeg", pd)
|
||||
if len(lc.Releases) != 2 {
|
||||
t.Fatalf("releases = %d, want 2", len(lc.Releases))
|
||||
}
|
||||
var windowsExt string
|
||||
for _, r := range lc.Releases {
|
||||
if r.OS == "windows" {
|
||||
windowsExt = r.Ext
|
||||
}
|
||||
}
|
||||
if windowsExt != ".exe" {
|
||||
t.Errorf("ffmpeg windows ext = %q, want %q", windowsExt, ".exe")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ffmpeg_translation_not_applied_to_other_packages", func(t *testing.T) {
|
||||
// The ffmpeg .gz→.exe translation is package-specific.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "othertool-windows-amd64.gz", OS: "windows", Arch: "x86_64", Format: ".gz"},
|
||||
},
|
||||
}
|
||||
lc, _ := storage.ExportLegacy("othertool", pd)
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Fatalf("releases = %d, want 1", len(lc.Releases))
|
||||
}
|
||||
if lc.Releases[0].Ext != ".gz" {
|
||||
t.Errorf("ext = %q, want %q (no translation outside ffmpeg)", lc.Releases[0].Ext, ".gz")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("universal2_passes_through", func(t *testing.T) {
|
||||
// universal2 (x86_64 + ARM64 fat binary) is kept as-is in the legacy
|
||||
// cache. The Node.js side handles resolution via its WATERFALL/CompatArches.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "hugo_0.145.0_darwin-universal.tar.gz", OS: "darwin", Arch: "universal2", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, stats := storage.ExportLegacy("hugo", pd)
|
||||
if stats.Variants != 0 || stats.Formats != 0 {
|
||||
t.Errorf("unexpected drops: variants=%d formats=%d", stats.Variants, stats.Formats)
|
||||
}
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Fatalf("releases = %d, want 1", len(lc.Releases))
|
||||
}
|
||||
if lc.Releases[0].Arch != "universal2" {
|
||||
t.Errorf("arch = %q, want %q", lc.Releases[0].Arch, "universal2")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestExportLegacyMixed verifies correct counting when multiple drop/translate
|
||||
// categories appear together in a single export call.
|
||||
func TestExportLegacyMixed(t *testing.T) {
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
// kept: baseline linux build
|
||||
{Filename: "tool-linux-amd64.tar.gz", OS: "linux", Arch: "x86_64", Format: ".tar.gz"},
|
||||
// dropped: variant build
|
||||
{Filename: "tool-linux-amd64-rocm.tar.gz", OS: "linux", Arch: "x86_64", Format: ".tar.gz", Variants: []string{"rocm"}},
|
||||
// dropped: .apk format
|
||||
{Filename: "tool.apk", OS: "android", Format: ".apk"},
|
||||
// kept: illumos (translated to sunos)
|
||||
{Filename: "tool-illumos-amd64.tar.gz", OS: "illumos", Arch: "x86_64", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, stats := storage.ExportLegacy("tool", pd)
|
||||
|
||||
if stats.Variants != 1 {
|
||||
t.Errorf("Variants = %d, want 1", stats.Variants)
|
||||
}
|
||||
if stats.Formats != 1 {
|
||||
t.Errorf("Formats = %d, want 1", stats.Formats)
|
||||
}
|
||||
if len(lc.Releases) != 2 {
|
||||
t.Errorf("releases = %d, want 2 (linux + sunos)", len(lc.Releases))
|
||||
}
|
||||
|
||||
// Verify illumos is kept as-is (not translated to sunos).
|
||||
var foundIllumos bool
|
||||
for _, r := range lc.Releases {
|
||||
if r.OS == "illumos" {
|
||||
foundIllumos = true
|
||||
}
|
||||
if r.OS == "sunos" {
|
||||
t.Error("illumos should NOT be translated to sunos")
|
||||
}
|
||||
}
|
||||
if !foundIllumos {
|
||||
t.Error("expected an illumos release (kept as-is)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecodeLegacyJSON verifies we can parse the exact JSON format
|
||||
// the Node.js server writes to _cache/.
|
||||
func TestDecodeLegacyJSON(t *testing.T) {
|
||||
@@ -284,3 +85,343 @@ func TestDecodeLegacyJSON(t *testing.T) {
|
||||
t.Errorf("round-trip Ext = %q, want %q", lc3.Releases[0].Ext, a.Format)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExportLegacyDrops verifies that ExportLegacy correctly drops and counts
|
||||
// assets that can't be represented in the Node.js legacy cache format.
|
||||
func TestExportLegacyDrops(t *testing.T) {
|
||||
t.Run("variant_builds_dropped", func(t *testing.T) {
|
||||
// Assets with variant tags (rocm, installer, fxdependent, etc.) are
|
||||
// dropped because Node.js has no variant-selection logic.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "ollama-linux-amd64-rocm.tgz", OS: "linux", Arch: "x86_64", Format: ".tar.gz", Variants: []string{"rocm"}},
|
||||
{Filename: "ollama-linux-amd64.tgz", OS: "linux", Arch: "x86_64", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, stats := storage.ExportLegacy("ollama", pd)
|
||||
if stats.Variants != 1 {
|
||||
t.Errorf("Variants dropped = %d, want 1", stats.Variants)
|
||||
}
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Errorf("releases = %d, want 1 (baseline only)", len(lc.Releases))
|
||||
}
|
||||
if lc.Releases[0].Name != "ollama-linux-amd64.tgz" {
|
||||
t.Errorf("kept wrong release: %q", lc.Releases[0].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("universal2_dropped", func(t *testing.T) {
|
||||
// universal2 fat binaries are dropped. The Node classifier maps "universal"
|
||||
// in the filename to x86_64, then rejects the cache entry because it says
|
||||
// "universal2". The mismatch can't be fixed without changing the filename.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "hugo_0.145.0_darwin-universal.tar.gz", OS: "darwin", Arch: "universal2", Format: ".tar.gz"},
|
||||
{Filename: "hugo_0.145.0_darwin-arm64.tar.gz", OS: "darwin", Arch: "aarch64", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, stats := storage.ExportLegacy("hugo", pd)
|
||||
if stats.Universal != 1 {
|
||||
t.Errorf("Universal dropped = %d, want 1", stats.Universal)
|
||||
}
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Errorf("releases = %d, want 1 (arm64 only)", len(lc.Releases))
|
||||
}
|
||||
if len(lc.Releases) > 0 && lc.Releases[0].Arch != "aarch64" {
|
||||
t.Errorf("kept arch = %q, want aarch64", lc.Releases[0].Arch)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("solaris_dropped", func(t *testing.T) {
|
||||
// Solaris entries are dropped: Node never served these platforms and the
|
||||
// classifier produces unfixable mismatches for them.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "go1.20.1.solaris-amd64.tar.gz", OS: "solaris", Arch: "x86_64", Format: ".tar.gz"},
|
||||
{Filename: "go1.20.1.linux-amd64.tar.gz", OS: "linux", Arch: "x86_64", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, stats := storage.ExportLegacy("go", pd)
|
||||
if stats.SunOS != 1 {
|
||||
t.Errorf("SunOS dropped = %d, want 1", stats.SunOS)
|
||||
}
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Errorf("releases = %d, want 1 (linux only)", len(lc.Releases))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("illumos_dropped", func(t *testing.T) {
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "go1.20.1.illumos-amd64.tar.gz", OS: "illumos", Arch: "x86_64", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, stats := storage.ExportLegacy("go", pd)
|
||||
if stats.SunOS != 1 {
|
||||
t.Errorf("SunOS dropped = %d, want 1", stats.SunOS)
|
||||
}
|
||||
if len(lc.Releases) != 0 {
|
||||
t.Errorf("releases = %d, want 0", len(lc.Releases))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("android_dropped", func(t *testing.T) {
|
||||
// Android entries are dropped: the classifier maps android filenames to
|
||||
// linux OS and then rejects the cache entry that says android.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "fzf-0.57.0-android-arm64.tar.gz", OS: "android", Arch: "aarch64", Format: ".tar.gz"},
|
||||
{Filename: "fzf-0.57.0-linux-arm64.tar.gz", OS: "linux", Arch: "aarch64", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, stats := storage.ExportLegacy("fzf", pd)
|
||||
if stats.Android != 1 {
|
||||
t.Errorf("Android dropped = %d, want 1", stats.Android)
|
||||
}
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Errorf("releases = %d, want 1 (linux only)", len(lc.Releases))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown_formats_dropped", func(t *testing.T) {
|
||||
// .AppImage, .deb, .rpm are not in the Node.js format set.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "tool.AppImage", OS: "linux", Format: ".AppImage"},
|
||||
{Filename: "tool.deb", OS: "linux", Format: ".deb"},
|
||||
{Filename: "tool.rpm", OS: "linux", Format: ".rpm"},
|
||||
{Filename: "tool-linux-amd64.tar.gz", OS: "linux", Arch: "x86_64", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, stats := storage.ExportLegacy("tool", pd)
|
||||
if stats.Formats != 3 {
|
||||
t.Errorf("Formats dropped = %d, want 3", stats.Formats)
|
||||
}
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Errorf("releases = %d, want 1 (tar.gz only)", len(lc.Releases))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty_format_passes_through", func(t *testing.T) {
|
||||
// Assets with empty format (e.g. bare binaries, git sources) pass through.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "jq-linux-amd64", OS: "linux", Arch: "x86_64", Format: ""},
|
||||
},
|
||||
}
|
||||
lc, stats := storage.ExportLegacy("jq", pd)
|
||||
if stats.Formats != 0 {
|
||||
t.Errorf("Formats dropped = %d, want 0", stats.Formats)
|
||||
}
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Errorf("releases = %d, want 1", len(lc.Releases))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestExportLegacyTranslations verifies that legacyFieldBackport applies the
|
||||
// correct field translations for Node.js compatibility.
|
||||
func TestExportLegacyTranslations(t *testing.T) {
|
||||
t.Run("ffmpeg_windows_gz_to_exe", func(t *testing.T) {
|
||||
// ffmpeg Windows releases are .gz archives containing a bare .exe.
|
||||
// Production releases.js overrides ext to 'exe' for install compatibility.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "ffmpeg-7.0-windows-amd64.gz", OS: "windows", Arch: "x86_64", Format: ".gz"},
|
||||
{Filename: "ffmpeg-7.0-linux-amd64.tar.gz", OS: "linux", Arch: "x86_64", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, _ := storage.ExportLegacy("ffmpeg", pd)
|
||||
if len(lc.Releases) != 2 {
|
||||
t.Fatalf("releases = %d, want 2", len(lc.Releases))
|
||||
}
|
||||
var windowsExt string
|
||||
for _, r := range lc.Releases {
|
||||
if r.OS == "windows" {
|
||||
windowsExt = r.Ext
|
||||
}
|
||||
}
|
||||
if windowsExt != ".exe" {
|
||||
t.Errorf("ffmpeg windows ext = %q, want %q", windowsExt, ".exe")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ffmpeg_translation_not_applied_to_other_packages", func(t *testing.T) {
|
||||
// The ffmpeg .gz→.exe translation is package-specific.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "othertool-windows-amd64.gz", OS: "windows", Arch: "x86_64", Format: ".gz"},
|
||||
},
|
||||
}
|
||||
lc, _ := storage.ExportLegacy("othertool", pd)
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Fatalf("releases = %d, want 1", len(lc.Releases))
|
||||
}
|
||||
if lc.Releases[0].Ext != ".gz" {
|
||||
t.Errorf("ext = %q, want %q (no translation outside ffmpeg)", lc.Releases[0].Ext, ".gz")
|
||||
}
|
||||
})
|
||||
|
||||
// ARM arch translations: the Node build-classifier re-parses filenames and
|
||||
// rejects cache entries where the arch field doesn't match what it extracts.
|
||||
// Go normalizes ARM filenames to canonical arch values, but the Node classifier
|
||||
// preserves the original Debian/Rust naming. These translations undo Go's
|
||||
// normalization for the legacy cache only.
|
||||
t.Run("arm_gnueabihf_to_armhf", func(t *testing.T) {
|
||||
// Go: gnueabihf → armv6 (Rust soft-float ABI name)
|
||||
// Node classifier: gnueabihf → armhf
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "bat-v0.9.0-arm-unknown-linux-gnueabihf.tar.gz", OS: "linux", Arch: "armv6", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, _ := storage.ExportLegacy("bat", pd)
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Fatalf("releases = %d, want 1", len(lc.Releases))
|
||||
}
|
||||
if lc.Releases[0].Arch != "armhf" {
|
||||
t.Errorf("arch = %q, want armhf", lc.Releases[0].Arch)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("arm_armhf_filename_to_armhf", func(t *testing.T) {
|
||||
// Go: armhf → armv7 (Debian armhf = ARMv7 hard-float)
|
||||
// Node classifier: armhf → armhf
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "caddy_linux_armhf.tar.gz", OS: "linux", Arch: "armv7", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, _ := storage.ExportLegacy("caddy", pd)
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Fatalf("releases = %d, want 1", len(lc.Releases))
|
||||
}
|
||||
if lc.Releases[0].Arch != "armhf" {
|
||||
t.Errorf("arch = %q, want armhf", lc.Releases[0].Arch)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("arm_armel_to_armel", func(t *testing.T) {
|
||||
// Go: armel → armv6 (Debian soft-float ABI name)
|
||||
// Node classifier: armel → armel
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "caddy_linux_armel.tar.gz", OS: "linux", Arch: "armv6", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, _ := storage.ExportLegacy("caddy", pd)
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Fatalf("releases = %d, want 1", len(lc.Releases))
|
||||
}
|
||||
if lc.Releases[0].Arch != "armel" {
|
||||
t.Errorf("arch = %q, want armel", lc.Releases[0].Arch)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("arm_armv5_to_armel", func(t *testing.T) {
|
||||
// Go: armv5 → armv5
|
||||
// Node classifier tiered map: armv5 falls back to armel
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "caddy_linux_armv5.tar.gz", OS: "linux", Arch: "armv5", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, _ := storage.ExportLegacy("caddy", pd)
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Fatalf("releases = %d, want 1", len(lc.Releases))
|
||||
}
|
||||
if lc.Releases[0].Arch != "armel" {
|
||||
t.Errorf("arch = %q, want armel", lc.Releases[0].Arch)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("arm_armv7a_to_armv7a", func(t *testing.T) {
|
||||
// Go: armv7a → armv7 (Go normalizes armv7a to armv7)
|
||||
// Node classifier: armv7a → armv7a (preserves the -a suffix)
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "tool-armv7a-linux.tar.gz", OS: "linux", Arch: "armv7", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, _ := storage.ExportLegacy("tool", pd)
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Fatalf("releases = %d, want 1", len(lc.Releases))
|
||||
}
|
||||
if lc.Releases[0].Arch != "armv7a" {
|
||||
t.Errorf("arch = %q, want armv7a", lc.Releases[0].Arch)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("arm_armv7l_unchanged", func(t *testing.T) {
|
||||
// armv7l in filename: Go=armv7, Node classifier also extracts armv7. No translation.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "tool-armv7l-linux.tar.gz", OS: "linux", Arch: "armv7", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, _ := storage.ExportLegacy("tool", pd)
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Fatalf("releases = %d, want 1", len(lc.Releases))
|
||||
}
|
||||
if lc.Releases[0].Arch != "armv7" {
|
||||
t.Errorf("arch = %q, want armv7 (no translation for armv7l)", lc.Releases[0].Arch)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("arm_armv6l_unchanged", func(t *testing.T) {
|
||||
// armv6l in filename: Go=armv6, Node classifier also extracts armv6. No translation.
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
{Filename: "tool-armv6l-linux.tar.gz", OS: "linux", Arch: "armv6", Format: ".tar.gz"},
|
||||
},
|
||||
}
|
||||
lc, _ := storage.ExportLegacy("tool", pd)
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Fatalf("releases = %d, want 1", len(lc.Releases))
|
||||
}
|
||||
if lc.Releases[0].Arch != "armv6" {
|
||||
t.Errorf("arch = %q, want armv6 (no translation for armv6l)", lc.Releases[0].Arch)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestExportLegacyMixed verifies correct counting when multiple drop categories
|
||||
// appear together in a single export call.
|
||||
func TestExportLegacyMixed(t *testing.T) {
|
||||
pd := storage.PackageData{
|
||||
Assets: []storage.Asset{
|
||||
// kept: baseline linux build
|
||||
{Filename: "tool-linux-amd64.tar.gz", OS: "linux", Arch: "x86_64", Format: ".tar.gz"},
|
||||
// dropped: variant build
|
||||
{Filename: "tool-linux-amd64-rocm.tar.gz", OS: "linux", Arch: "x86_64", Format: ".tar.gz", Variants: []string{"rocm"}},
|
||||
// dropped: universal2
|
||||
{Filename: "tool-darwin-universal.tar.gz", OS: "darwin", Arch: "universal2", Format: ".tar.gz"},
|
||||
// dropped: illumos
|
||||
{Filename: "tool-illumos-amd64.tar.gz", OS: "illumos", Arch: "x86_64", Format: ".tar.gz"},
|
||||
// dropped: android
|
||||
{Filename: "tool-android-arm64.tar.gz", OS: "android", Arch: "aarch64", Format: ".tar.gz"},
|
||||
// dropped: .AppImage format
|
||||
{Filename: "tool.AppImage", OS: "linux", Format: ".AppImage"},
|
||||
},
|
||||
}
|
||||
lc, stats := storage.ExportLegacy("tool", pd)
|
||||
|
||||
if stats.Variants != 1 {
|
||||
t.Errorf("Variants = %d, want 1", stats.Variants)
|
||||
}
|
||||
if stats.Universal != 1 {
|
||||
t.Errorf("Universal = %d, want 1", stats.Universal)
|
||||
}
|
||||
if stats.SunOS != 1 {
|
||||
t.Errorf("SunOS = %d, want 1", stats.SunOS)
|
||||
}
|
||||
if stats.Android != 1 {
|
||||
t.Errorf("Android = %d, want 1", stats.Android)
|
||||
}
|
||||
if stats.Formats != 1 {
|
||||
t.Errorf("Formats = %d, want 1", stats.Formats)
|
||||
}
|
||||
if len(lc.Releases) != 1 {
|
||||
t.Errorf("releases = %d, want 1 (linux only)", len(lc.Releases))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user