fix(classifypkg): tag Rust static musl builds as libc='none'

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'.
This commit is contained in:
AJ ONeal
2026-03-11 12:19:21 -06:00
parent 9095b34c22
commit 47419b7eee
3 changed files with 50 additions and 15 deletions

View File

@@ -14,6 +14,9 @@ import (
"strings"
"time"
"regexp"
"github.com/webinstall/webi-installers/internal/buildmeta"
"github.com/webinstall/webi-installers/internal/classify"
"github.com/webinstall/webi-installers/internal/installerconf"
"github.com/webinstall/webi-installers/internal/rawcache"
@@ -305,6 +308,13 @@ type ghAsset struct {
Size int64 `json:"size"`
}
// reRustMuslStatic matches Rust target triples that indicate a statically-linked
// musl build. Rust's *-unknown-linux-musl targets are always static — they have
// zero runtime libc dependency. This is distinct from packages like pwsh
// (-linux-musl-x64), bun (-linux-x64-musl), and node (-linux-x64-musl) which
// dynamically link against musl and require it at runtime.
var reRustMuslStatic = regexp.MustCompile(`(?i)-unknown-linux-musl`)
func classifyGitHub(pkg string, conf *installerconf.Conf, d *rawcache.Dir) ([]storage.Asset, error) {
tagPrefix := conf.TagPrefix
releases, err := ReadAllRaw(d)
@@ -356,13 +366,19 @@ func classifyGitHub(pkg string, conf *installerconf.Conf, d *rawcache.Dir) ([]st
name = name[:len(name)-4] + ".tar.gz"
}
libc := r.Libc
// Rust static musl builds have zero runtime libc dependency.
if libc == buildmeta.LibcMusl && reRustMuslStatic.MatchString(a.Name) {
libc = buildmeta.LibcNone
}
assets = append(assets, storage.Asset{
Filename: name,
Version: version,
Channel: channel,
OS: string(r.OS),
Arch: string(r.Arch),
Libc: string(r.Libc),
Libc: string(libc),
Format: string(r.Format),
Download: a.BrowserDownloadURL,
Date: date,

View File

@@ -320,26 +320,29 @@ func TestResolvePosixPackages(t *testing.T) {
}
}
// TestResolveLibcPreference tests musl vs gnu selection.
// 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) {
dists := loadCacheDists(t, "bat")
batDists := loadCacheDists(t, "bat")
// Explicit musl request.
m := resolve.Best(dists, resolve.Query{
// 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 musl match")
t.Fatal("expected match for musl host")
}
if m.Libc != "musl" {
t.Errorf("Libc = %q, want musl", m.Libc)
// 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(dists, resolve.Query{
m = resolve.Best(batDists, resolve.Query{
OS: buildmeta.OSLinux,
Arch: buildmeta.ArchAMD64,
Libc: buildmeta.LibcGNU,
@@ -352,8 +355,8 @@ func TestResolveLibcPreference(t *testing.T) {
t.Errorf("Libc = %q, want gnu", m.Libc)
}
// No preference — should still match (accepts either).
m = resolve.Best(dists, resolve.Query{
// No preference — should still match (accepts any).
m = resolve.Best(batDists, resolve.Query{
OS: buildmeta.OSLinux,
Arch: buildmeta.ArchAMD64,
Formats: []string{".tar.gz"},
@@ -361,6 +364,21 @@ func TestResolveLibcPreference(t *testing.T) {
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.

View File

@@ -250,20 +250,21 @@ func TestCacheGitPackages(t *testing.T) {
}
// TestCacheLibcPreference tests explicit libc selection.
// bat is Rust — its musl builds are static (tagged 'none').
func TestCacheLibcPreference(t *testing.T) {
assets := loadAssets(t, "bat")
// Explicit musl.
// Musl host requesting bat: gets static musl build (tagged 'none').
res, err := resolver.Resolve(assets, resolver.Request{
OS: "linux",
Arch: "x86_64",
Libc: "musl",
})
if err != nil {
t.Fatal("expected musl match")
t.Fatal("expected match for musl host")
}
if res.Asset.Libc != "musl" {
t.Errorf("Libc = %q, want musl", res.Asset.Libc)
if res.Asset.Libc != "none" {
t.Errorf("Libc = %q, want none (static musl)", res.Asset.Libc)
}
// Explicit gnu.