diff --git a/internal/classifypkg/classifypkg.go b/internal/classifypkg/classifypkg.go index 63db759..ccb711b 100644 --- a/internal/classifypkg/classifypkg.go +++ b/internal/classifypkg/classifypkg.go @@ -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, diff --git a/internal/resolve/resolve_cache_test.go b/internal/resolve/resolve_cache_test.go index 67f0633..dc57831 100644 --- a/internal/resolve/resolve_cache_test.go +++ b/internal/resolve/resolve_cache_test.go @@ -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. diff --git a/internal/resolver/resolver_cache_test.go b/internal/resolver/resolver_cache_test.go index 7a8c2bd..ca6607f 100644 --- a/internal/resolver/resolver_cache_test.go +++ b/internal/resolver/resolver_cache_test.go @@ -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.