mirror of
https://github.com/webinstall/webi-installers.git
synced 2026-04-07 02:46:50 +00:00
Rust *-unknown-linux-musl builds are statically linked with zero runtime libc dependency. Detect this pattern in classifyGitHub and override libc from 'musl' to 'none'. Hard-musl packages (pwsh, bun, node) use different filename patterns and keep libc='musl'.
401 lines
13 KiB
Go
401 lines
13 KiB
Go
package resolve_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/webinstall/webi-installers/internal/buildmeta"
|
|
"github.com/webinstall/webi-installers/internal/resolve"
|
|
)
|
|
|
|
// legacyAsset matches the _cache/ JSON format.
|
|
type legacyAsset struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
LTS bool `json:"lts"`
|
|
Channel string `json:"channel"`
|
|
Date string `json:"date"`
|
|
OS string `json:"os"`
|
|
Arch string `json:"arch"`
|
|
Libc string `json:"libc"`
|
|
Ext string `json:"ext"`
|
|
Download string `json:"download"`
|
|
}
|
|
|
|
type legacyCache struct {
|
|
Releases []legacyAsset `json:"releases"`
|
|
}
|
|
|
|
func loadCacheDists(t *testing.T, pkg string) []resolve.Dist {
|
|
t.Helper()
|
|
cacheDir := filepath.Join("..", "..", "_cache", "2026-03")
|
|
path := filepath.Join(cacheDir, pkg+".json")
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Skipf("no cache file for %s: %v", pkg, err)
|
|
}
|
|
var lc legacyCache
|
|
if err := json.Unmarshal(data, &lc); err != nil {
|
|
t.Fatalf("parse %s: %v", pkg, err)
|
|
}
|
|
dists := make([]resolve.Dist, len(lc.Releases))
|
|
for i, la := range lc.Releases {
|
|
dists[i] = resolve.Dist{
|
|
Filename: la.Name,
|
|
Version: la.Version,
|
|
LTS: la.LTS,
|
|
Channel: la.Channel,
|
|
Date: la.Date,
|
|
OS: la.OS,
|
|
Arch: la.Arch,
|
|
Libc: la.Libc,
|
|
Format: la.Ext,
|
|
Download: la.Download,
|
|
}
|
|
}
|
|
return dists
|
|
}
|
|
|
|
// platforms is the standard webi target matrix.
|
|
var platforms = []struct {
|
|
name string
|
|
os buildmeta.OS
|
|
arch buildmeta.Arch
|
|
formats []string
|
|
}{
|
|
{"darwin-arm64", buildmeta.OSDarwin, buildmeta.ArchARM64, []string{".tar.xz", ".tar.gz", ".zip"}},
|
|
{"darwin-amd64", buildmeta.OSDarwin, buildmeta.ArchAMD64, []string{".tar.xz", ".tar.gz", ".zip"}},
|
|
{"linux-amd64", buildmeta.OSLinux, buildmeta.ArchAMD64, []string{".tar.xz", ".exe.xz", ".tar.gz", ".xz", ".gz", ".zip"}},
|
|
{"linux-arm64", buildmeta.OSLinux, buildmeta.ArchARM64, []string{".tar.xz", ".exe.xz", ".tar.gz", ".xz", ".gz", ".zip"}},
|
|
{"linux-armv7", buildmeta.OSLinux, buildmeta.ArchARMv7, []string{".tar.xz", ".tar.gz", ".xz", ".gz", ".zip"}},
|
|
{"linux-armv6", buildmeta.OSLinux, buildmeta.ArchARMv6, []string{".tar.xz", ".tar.gz", ".xz", ".gz", ".zip"}},
|
|
{"windows-amd64", buildmeta.OSWindows, buildmeta.ArchAMD64, []string{".zip", ".tar.gz", ".exe", ".7z"}},
|
|
{"windows-arm64", buildmeta.OSWindows, buildmeta.ArchARM64, []string{".zip", ".tar.gz", ".exe", ".7z"}},
|
|
}
|
|
|
|
// TestResolveAllPackages loads every package from the cache and verifies
|
|
// the resolver finds a match for each platform the package supports.
|
|
func TestResolveAllPackages(t *testing.T) {
|
|
cacheDir := filepath.Join("..", "..", "_cache", "2026-03")
|
|
entries, err := os.ReadDir(cacheDir)
|
|
if err != nil {
|
|
t.Skipf("no cache dir: %v", err)
|
|
}
|
|
|
|
var pkgs []string
|
|
for _, e := range entries {
|
|
if strings.HasSuffix(e.Name(), ".json") {
|
|
pkgs = append(pkgs, strings.TrimSuffix(e.Name(), ".json"))
|
|
}
|
|
}
|
|
|
|
if len(pkgs) < 50 {
|
|
t.Fatalf("expected at least 50 packages, got %d", len(pkgs))
|
|
}
|
|
|
|
for _, pkg := range pkgs {
|
|
t.Run(pkg, func(t *testing.T) {
|
|
dists := loadCacheDists(t, pkg)
|
|
if len(dists) == 0 {
|
|
t.Skip("no releases")
|
|
}
|
|
|
|
// Determine which platforms this package supports.
|
|
cat := resolve.Survey(dists)
|
|
osSet := make(map[string]bool, len(cat.OSes))
|
|
for _, o := range cat.OSes {
|
|
osSet[o] = true
|
|
}
|
|
|
|
for _, plat := range platforms {
|
|
platOS := string(plat.os)
|
|
// Check if this package has any assets for this OS
|
|
// (including POSIX/ANYOS which are compatible).
|
|
supported := osSet[platOS] ||
|
|
osSet[string(buildmeta.OSAny)] ||
|
|
(platOS != "windows" && (osSet[string(buildmeta.OSPosix2017)] || osSet[string(buildmeta.OSPosix2024)]))
|
|
|
|
if !supported {
|
|
continue
|
|
}
|
|
|
|
t.Run(plat.name, func(t *testing.T) {
|
|
m := resolve.Best(dists, resolve.Query{
|
|
OS: plat.os,
|
|
Arch: plat.arch,
|
|
Formats: plat.formats,
|
|
})
|
|
if m == nil {
|
|
// This is a warning, not a failure — some packages
|
|
// legitimately don't have builds for all arches.
|
|
// But log it so we can spot unexpected gaps.
|
|
t.Logf("WARN: no match for %s on %s (has OSes: %v, Arches: %v)",
|
|
pkg, plat.name, cat.OSes, cat.Arches)
|
|
return
|
|
}
|
|
if m.Version == "" {
|
|
t.Error("matched but Version is empty")
|
|
}
|
|
if m.Download == "" {
|
|
t.Error("matched but Download is empty")
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Packages with known platform expectations. Each entry specifies
|
|
// platforms that MUST resolve and the expected latest version.
|
|
var knownPackages = []struct {
|
|
pkg string
|
|
version string // expected latest stable version (prefix match)
|
|
platforms []string // platform names from the platforms table
|
|
}{
|
|
{"bat", "0.26", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
|
|
{"caddy", "2.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv7", "linux-armv6", "windows-amd64"}},
|
|
{"delta", "0.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
|
|
{"fd", "10.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv7", "windows-amd64"}},
|
|
{"fzf", "0.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv7", "windows-amd64"}},
|
|
{"gh", "2.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv6", "windows-amd64"}},
|
|
{"rg", "", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
|
|
{"shellcheck", "0.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv6", "windows-amd64"}},
|
|
{"shfmt", "3.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv6", "windows-amd64"}},
|
|
{"xz", "", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
|
|
{"yq", "4.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv6", "windows-amd64"}},
|
|
{"zoxide", "0.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv7", "windows-amd64"}},
|
|
{"aliasman", "", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64"}},
|
|
{"comrak", "0.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "windows-amd64"}},
|
|
{"hugo", "0.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
|
|
{"node", "", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
|
|
{"terraform", "", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
|
|
{"zig", "", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
|
|
}
|
|
|
|
// TestKnownPackages verifies specific packages resolve correctly
|
|
// with expected versions and platform coverage.
|
|
func TestKnownPackages(t *testing.T) {
|
|
platMap := make(map[string]struct {
|
|
os buildmeta.OS
|
|
arch buildmeta.Arch
|
|
formats []string
|
|
})
|
|
for _, p := range platforms {
|
|
platMap[p.name] = struct {
|
|
os buildmeta.OS
|
|
arch buildmeta.Arch
|
|
formats []string
|
|
}{p.os, p.arch, p.formats}
|
|
}
|
|
|
|
for _, kp := range knownPackages {
|
|
t.Run(kp.pkg, func(t *testing.T) {
|
|
dists := loadCacheDists(t, kp.pkg)
|
|
|
|
for _, platName := range kp.platforms {
|
|
plat := platMap[platName]
|
|
t.Run(platName, func(t *testing.T) {
|
|
m := resolve.Best(dists, resolve.Query{
|
|
OS: plat.os,
|
|
Arch: plat.arch,
|
|
Formats: plat.formats,
|
|
})
|
|
if m == nil {
|
|
t.Skipf("no build available for %s on %s — upstream gap", kp.pkg, platName)
|
|
return
|
|
}
|
|
if kp.version != "" {
|
|
// Strip leading "v" for prefix comparison.
|
|
v := strings.TrimPrefix(m.Version, "v")
|
|
if !strings.HasPrefix(v, kp.version) {
|
|
t.Errorf("Version = %q, want prefix %q", m.Version, kp.version)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestResolveVersionConstraints tests version pinning across real packages.
|
|
func TestResolveVersionConstraints(t *testing.T) {
|
|
tests := []struct {
|
|
pkg string
|
|
version string // constraint
|
|
wantPfx string // expected version prefix in result
|
|
}{
|
|
{"bat", "0.25", "0.25"},
|
|
{"bat", "0.26", "0.26"},
|
|
{"gh", "2.40", "2.40"},
|
|
{"node", "20", "20."},
|
|
{"node", "22", "22."},
|
|
{"hugo", "0.121", "0.121"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
name := fmt.Sprintf("%s@%s", tt.pkg, tt.version)
|
|
t.Run(name, func(t *testing.T) {
|
|
dists := loadCacheDists(t, tt.pkg)
|
|
m := resolve.Best(dists, resolve.Query{
|
|
OS: buildmeta.OSLinux,
|
|
Arch: buildmeta.ArchAMD64,
|
|
Formats: []string{".tar.xz", ".tar.gz", ".zip"},
|
|
Version: tt.version,
|
|
})
|
|
if m == nil {
|
|
t.Fatalf("no match for %s@%s", tt.pkg, tt.version)
|
|
}
|
|
v := strings.TrimPrefix(m.Version, "v")
|
|
if !strings.HasPrefix(v, tt.wantPfx) {
|
|
t.Errorf("Version = %q, want prefix %q", m.Version, tt.wantPfx)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestResolveArchFallbackReal tests arch fallback with real package data.
|
|
func TestResolveArchFallbackReal(t *testing.T) {
|
|
// awless only has amd64 builds — macOS ARM64 should fall back.
|
|
dists := loadCacheDists(t, "awless")
|
|
m := resolve.Best(dists, resolve.Query{
|
|
OS: buildmeta.OSDarwin,
|
|
Arch: buildmeta.ArchARM64,
|
|
Formats: []string{".tar.gz", ".zip"},
|
|
})
|
|
if m == nil {
|
|
t.Fatal("expected Rosetta 2 fallback for awless")
|
|
}
|
|
if m.Arch != "x86_64" {
|
|
t.Errorf("Arch = %q, want x86_64", m.Arch)
|
|
}
|
|
}
|
|
|
|
// TestResolvePosixPackages tests packages that use posix_2017/ANYOS.
|
|
func TestResolvePosixPackages(t *testing.T) {
|
|
posixPkgs := []string{"aliasman", "pathman", "serviceman"}
|
|
for _, pkg := range posixPkgs {
|
|
t.Run(pkg, func(t *testing.T) {
|
|
dists := loadCacheDists(t, pkg)
|
|
if len(dists) == 0 {
|
|
t.Skip("no releases")
|
|
}
|
|
|
|
// Should resolve on Linux.
|
|
m := resolve.Best(dists, resolve.Query{
|
|
OS: buildmeta.OSLinux,
|
|
Arch: buildmeta.ArchAMD64,
|
|
Formats: []string{".tar.xz", ".tar.gz", ".zip", ".xz", ".gz"},
|
|
})
|
|
if m == nil {
|
|
t.Error("expected match on Linux for POSIX package")
|
|
}
|
|
|
|
// Should resolve on macOS.
|
|
m = resolve.Best(dists, resolve.Query{
|
|
OS: buildmeta.OSDarwin,
|
|
Arch: buildmeta.ArchARM64,
|
|
Formats: []string{".tar.xz", ".tar.gz", ".zip"},
|
|
})
|
|
if m == nil {
|
|
t.Error("expected match on macOS for POSIX package")
|
|
}
|
|
|
|
// Should NOT resolve on Windows (POSIX packages aren't Windows-compatible).
|
|
m = resolve.Best(dists, resolve.Query{
|
|
OS: buildmeta.OSWindows,
|
|
Arch: buildmeta.ArchAMD64,
|
|
Formats: []string{".zip", ".tar.gz"},
|
|
})
|
|
// This may or may not resolve depending on whether the package
|
|
// also has Windows builds. Don't assert nil — just check it
|
|
// doesn't return a posix_2017 match for Windows.
|
|
if m != nil && (m.OS == "posix_2017" || m.OS == "posix_2024") {
|
|
t.Errorf("POSIX package should not match Windows, got OS=%q", m.OS)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestResolveLibcPreference tests libc selection.
|
|
// bat is a Rust project — its musl builds are static (libc='none').
|
|
// pwsh has hard musl dependencies (libc='musl').
|
|
func TestResolveLibcPreference(t *testing.T) {
|
|
batDists := loadCacheDists(t, "bat")
|
|
|
|
// Musl host requesting bat: gets the static musl build (tagged 'none').
|
|
m := resolve.Best(batDists, resolve.Query{
|
|
OS: buildmeta.OSLinux,
|
|
Arch: buildmeta.ArchAMD64,
|
|
Libc: buildmeta.LibcMusl,
|
|
Formats: []string{".tar.gz"},
|
|
})
|
|
if m == nil {
|
|
t.Fatal("expected match for musl host")
|
|
}
|
|
// Rust musl builds are static — tagged as 'none', not 'musl'.
|
|
if m.Libc != "none" {
|
|
t.Errorf("bat musl request: Libc = %q, want none (static musl)", m.Libc)
|
|
}
|
|
|
|
// Explicit gnu request.
|
|
m = resolve.Best(batDists, resolve.Query{
|
|
OS: buildmeta.OSLinux,
|
|
Arch: buildmeta.ArchAMD64,
|
|
Libc: buildmeta.LibcGNU,
|
|
Formats: []string{".tar.gz"},
|
|
})
|
|
if m == nil {
|
|
t.Fatal("expected gnu match")
|
|
}
|
|
if m.Libc != "gnu" {
|
|
t.Errorf("Libc = %q, want gnu", m.Libc)
|
|
}
|
|
|
|
// No preference — should still match (accepts any).
|
|
m = resolve.Best(batDists, resolve.Query{
|
|
OS: buildmeta.OSLinux,
|
|
Arch: buildmeta.ArchAMD64,
|
|
Formats: []string{".tar.gz"},
|
|
})
|
|
if m == nil {
|
|
t.Fatal("expected match with no libc preference")
|
|
}
|
|
|
|
// pwsh has hard musl builds (dynamically linked, requires musl runtime).
|
|
pwshDists := loadCacheDists(t, "pwsh")
|
|
m = resolve.Best(pwshDists, resolve.Query{
|
|
OS: buildmeta.OSLinux,
|
|
Arch: buildmeta.ArchAMD64,
|
|
Libc: buildmeta.LibcMusl,
|
|
Formats: []string{".tar.gz"},
|
|
})
|
|
if m == nil {
|
|
t.Fatal("expected pwsh musl match")
|
|
}
|
|
if m.Libc != "musl" {
|
|
t.Errorf("pwsh musl request: Libc = %q, want musl", m.Libc)
|
|
}
|
|
}
|
|
|
|
// TestResolveFormatFallback tests format preference cascading.
|
|
func TestResolveFormatFallback(t *testing.T) {
|
|
// Request .tar.xz first, fall back to .tar.gz.
|
|
dists := loadCacheDists(t, "bat")
|
|
m := resolve.Best(dists, resolve.Query{
|
|
OS: buildmeta.OSLinux,
|
|
Arch: buildmeta.ArchAMD64,
|
|
Formats: []string{".tar.xz", ".tar.gz", ".zip"},
|
|
})
|
|
if m == nil {
|
|
t.Fatal("expected match")
|
|
}
|
|
// bat only has .tar.gz — should fall back from .tar.xz.
|
|
if m.Format != ".tar.gz" {
|
|
t.Errorf("Format = %q, want .tar.gz (fallback from .tar.xz)", m.Format)
|
|
}
|
|
}
|