initial commit [141.14.140.180,mike]
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Aliases commonly used in release asset names, including Rust target triples.
|
||||
func osAlias(o string) []string {
|
||||
switch o {
|
||||
case "darwin":
|
||||
return []string{"darwin", "macos", "osx", "mac", "apple"}
|
||||
case "windows":
|
||||
return []string{"windows", "win"}
|
||||
}
|
||||
return []string{o}
|
||||
}
|
||||
|
||||
func archAlias(a string) []string {
|
||||
switch a {
|
||||
case "amd64":
|
||||
return []string{"amd64", "x86_64", "x64", "64bit"}
|
||||
case "arm64":
|
||||
return []string{"arm64", "aarch64"}
|
||||
case "386":
|
||||
return []string{"386", "i386", "i686", "x86", "32bit"}
|
||||
case "arm":
|
||||
return []string{"arm", "armv7", "armv6", "armhf"}
|
||||
case "riscv64":
|
||||
return []string{"riscv64", "riscv64gc"}
|
||||
}
|
||||
return []string{a}
|
||||
}
|
||||
|
||||
var (
|
||||
// Checksums, signatures and OS packages are not plain binaries.
|
||||
notBinaryRe = regexp.MustCompile(`\.(sha\d*|sha256sum|md5|asc|sig|pem|sbom|json|txt|deb|rpm|apk|dmg|pkg|msi|snap|flatpak|appimage)$`)
|
||||
// Cross-compile targets whose triples contain a host OS token:
|
||||
// aarch64-linux-android is Android, not Linux.
|
||||
foreignRe = regexp.MustCompile(`(^|[^a-z0-9])(android\w*|ios|wasi|wasm\w*|emscripten)([^a-z0-9]|$)`)
|
||||
archiveRe = regexp.MustCompile(`\.(tar\.gz|tgz|zip|tar\.xz|tar\.bz2|tar\.zst|tzst)$`)
|
||||
noiseRe = regexp.MustCompile(`debug|symbols|static-pie|profile`)
|
||||
tarballRe = regexp.MustCompile(`\.(tar\.gz|tgz|tar\.bz2|tbz|tar\.xz|txz|tar\.zst|tzst|tar)$`)
|
||||
singleRe = regexp.MustCompile(`\.(gz|bz2|xz|zst|lz4|lzma)$`)
|
||||
refuseRe = regexp.MustCompile(`\.(rar|dmg|pkg|msi|deb|rpm)$`)
|
||||
widerArm = regexp.MustCompile(`arm64|aarch64`)
|
||||
wider386 = regexp.MustCompile(`x86[_-]?64|amd64`)
|
||||
|
||||
win64Re = regexp.MustCompile(`(^|[^a-z0-9])win64($|[^a-z0-9])`)
|
||||
win32Re = regexp.MustCompile(`(^|[^a-z0-9])win32($|[^a-z0-9])`)
|
||||
)
|
||||
|
||||
// Compound tokens that name OS and architecture in one word. Rewritten before
|
||||
// matching so the normal token rules apply. "win32" is Node-speak for Windows
|
||||
// in general (pnpm-win32-x64.zip), so it contributes no architecture.
|
||||
func normalizeTokens(n string) string {
|
||||
n = win64Re.ReplaceAllString(n, "${1}windows-amd64${2}")
|
||||
n = win32Re.ReplaceAllString(n, "${1}windows${2}")
|
||||
return n
|
||||
}
|
||||
|
||||
// tokenRe matches any of the words as a token, i.e. not glued to letters or
|
||||
// digits. RE2 has no look-around, so the boundaries are ordinary characters.
|
||||
func tokenRe(words []string) *regexp.Regexp {
|
||||
quoted := make([]string, len(words))
|
||||
for i, w := range words {
|
||||
quoted[i] = regexp.QuoteMeta(w)
|
||||
}
|
||||
return regexp.MustCompile(`(^|[^a-z0-9])(` + strings.Join(quoted, "|") + `)([^a-z0-9]|$)`)
|
||||
}
|
||||
|
||||
// Can this machine turn the asset into a binary? tar, zip, gzip and bzip2 are
|
||||
// handled in-process; the rest needs a helper on $PATH.
|
||||
func unpackable(n string) bool {
|
||||
n = strings.ToLower(n)
|
||||
switch {
|
||||
case regexp.MustCompile(`\.(tar\.gz|tgz|tar\.bz2|tbz|tar|zip)$`).MatchString(n):
|
||||
return true
|
||||
case regexp.MustCompile(`\.(tar\.xz|txz)$`).MatchString(n):
|
||||
return decompressor("xz") != nil
|
||||
case regexp.MustCompile(`\.(tar\.zst|tzst)$`).MatchString(n):
|
||||
return decompressor("zst") != nil
|
||||
case strings.HasSuffix(n, ".7z"):
|
||||
return sevenZip() != ""
|
||||
case singleRe.MatchString(n):
|
||||
return decompressor(singleRe.FindStringSubmatch(n)[1]) != nil
|
||||
case refuseRe.MatchString(n):
|
||||
return false
|
||||
}
|
||||
return true // bare binary or an unversioned name
|
||||
}
|
||||
|
||||
// Glibc vs musl builds: pick what this system actually runs.
|
||||
var muslOnce = sync.OnceValue(func() bool {
|
||||
if _, err := os.Stat("/etc/alpine-release"); err == nil {
|
||||
return true
|
||||
}
|
||||
ldd, err := exec.LookPath("ldd")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
out, _ := exec.Command(ldd, "--version").CombinedOutput()
|
||||
return strings.Contains(strings.ToLower(string(out)), "musl")
|
||||
})
|
||||
|
||||
type candidate struct {
|
||||
score int
|
||||
pref int
|
||||
length int
|
||||
asset *Asset
|
||||
}
|
||||
|
||||
// pickAsset mirrors the Perl scoring one to one - the corpus test in
|
||||
// asset_test.go checks that the two keep agreeing.
|
||||
func pickAsset(assets []Asset, bin, wantOS, wantArch string, spec *Spec) (*Asset, error) {
|
||||
if spec.Asset != "" {
|
||||
for i := range assets {
|
||||
if assets[i].Name == spec.Asset {
|
||||
return &assets[i], nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("asset %q is not part of the release", spec.Asset)
|
||||
}
|
||||
if spec.Pattern != "" {
|
||||
re, err := regexp.Compile(spec.Pattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid --pattern: %w", err)
|
||||
}
|
||||
for i := range assets {
|
||||
if re.MatchString(assets[i].Name) {
|
||||
return &assets[i], nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
arches := archAlias(wantArch)
|
||||
// Universal macOS builds serve both architectures.
|
||||
if wantOS == "darwin" {
|
||||
arches = append(arches, "universal", "universal2")
|
||||
}
|
||||
osRe, archRe := tokenRe(osAlias(wantOS)), tokenRe(arches)
|
||||
|
||||
var cands []candidate
|
||||
var rejected []string
|
||||
for i := range assets {
|
||||
as := &assets[i]
|
||||
n := strings.ToLower(as.Name)
|
||||
m := normalizeTokens(n)
|
||||
|
||||
switch {
|
||||
case notBinaryRe.MatchString(n):
|
||||
rejected = append(rejected, n+" (not a binary)")
|
||||
continue
|
||||
case foreignRe.MatchString(m):
|
||||
rejected = append(rejected, n+" (foreign target)")
|
||||
continue
|
||||
case !osRe.MatchString(m):
|
||||
rejected = append(rejected, n+" (os)")
|
||||
continue
|
||||
case !archRe.MatchString(m):
|
||||
rejected = append(rejected, n+" (arch)")
|
||||
continue
|
||||
// Keep 32-bit "arm" off "arm64" and "386" off "x86_64".
|
||||
case wantArch == "arm" && widerArm.MatchString(m),
|
||||
wantArch == "386" && wider386.MatchString(m),
|
||||
wantArch == "amd64" && widerArm.MatchString(m):
|
||||
rejected = append(rejected, n+" (wider arch)")
|
||||
continue
|
||||
}
|
||||
|
||||
score := 0
|
||||
if bin != "" && strings.HasPrefix(n, strings.ToLower(bin)) {
|
||||
score += 10 // named after the binary
|
||||
}
|
||||
if archiveRe.MatchString(n) || !strings.Contains(n, ".") {
|
||||
score += 3
|
||||
}
|
||||
if noiseRe.MatchString(n) {
|
||||
score -= 5
|
||||
}
|
||||
// Only avoid formats this machine has no tool for - restic, for one,
|
||||
// ships nothing but .bz2, and that is handled in-process.
|
||||
if !unpackable(n) {
|
||||
score -= 8
|
||||
}
|
||||
|
||||
// Toolchain preference is a tie-break, not a penalty: a project that
|
||||
// ships musl only (or mingw only) must not be downranked for it.
|
||||
pref := 0
|
||||
switch {
|
||||
case wantOS == "linux" && (strings.Contains(n, "musl") || strings.Contains(n, "gnu")):
|
||||
libc := "gnu"
|
||||
if muslOnce() {
|
||||
libc = "musl"
|
||||
}
|
||||
if !strings.Contains(n, libc) {
|
||||
pref = 1
|
||||
}
|
||||
case wantOS == "windows" && (strings.Contains(n, "msvc") || strings.Contains(n, "gnu")):
|
||||
if !strings.Contains(n, "msvc") { // msvc is the normal Windows build
|
||||
pref = 1
|
||||
}
|
||||
}
|
||||
cands = append(cands, candidate{score: score, pref: pref, length: len(n), asset: as})
|
||||
}
|
||||
|
||||
// Best score, then preferred toolchain, then the shortest name (which
|
||||
// avoids special variants like -baseline or -static).
|
||||
sort.SliceStable(cands, func(i, j int) bool {
|
||||
a, b := cands[i], cands[j]
|
||||
if a.score != b.score {
|
||||
return a.score > b.score
|
||||
}
|
||||
if a.pref != b.pref {
|
||||
return a.pref < b.pref
|
||||
}
|
||||
return a.length < b.length
|
||||
})
|
||||
|
||||
if opt.verbose {
|
||||
for _, c := range cands {
|
||||
verbose("candidate score=%-3d pref=%d %s", c.score, c.pref, c.asset.Name)
|
||||
}
|
||||
for _, r := range rejected {
|
||||
verbose("rejected: %s", r)
|
||||
}
|
||||
}
|
||||
if len(cands) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return cands[0].asset, nil
|
||||
}
|
||||
Reference in New Issue
Block a user