mirror of
https://github.com/webinstall/webi-installers.git
synced 2026-04-07 02:46:50 +00:00
Git for Windows uses tags like v2.53.0.windows.1. Node.js strips ".windows.1" and replaces ".windows.N" (N>1) with ".N". Add NormalizeVersions to the git package and wire it into the classify pipeline. Also add version normalization to comparecache so the comparison uses canonical versions for both caches. Remaining git diffs: data freshness (.windows.2 releases Go hasn't fetched) and RC versions in Go that live doesn't have.
986 lines
23 KiB
Go
986 lines
23 KiB
Go
// Package classifypkg converts raw upstream release data into classified
|
|
// [storage.Asset] slices. Each source type (github, nodedist, gittag, etc.)
|
|
// has its own classifier that reads JSON from [rawcache.Dir] and produces
|
|
// assets with OS, arch, format, and channel fields populated.
|
|
//
|
|
// This is the second stage of the pipeline: fetch → classify → tag → filter → store.
|
|
package classifypkg
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/webinstall/webi-installers/internal/classify"
|
|
"github.com/webinstall/webi-installers/internal/installerconf"
|
|
"github.com/webinstall/webi-installers/internal/rawcache"
|
|
"github.com/webinstall/webi-installers/internal/releases/bun"
|
|
"github.com/webinstall/webi-installers/internal/releases/chromedist"
|
|
"github.com/webinstall/webi-installers/internal/releases/fish"
|
|
"github.com/webinstall/webi-installers/internal/releases/gitea"
|
|
"github.com/webinstall/webi-installers/internal/releases/flutterdist"
|
|
"github.com/webinstall/webi-installers/internal/releases/git"
|
|
"github.com/webinstall/webi-installers/internal/releases/golang"
|
|
"github.com/webinstall/webi-installers/internal/releases/gpgdist"
|
|
"github.com/webinstall/webi-installers/internal/releases/hashicorp"
|
|
"github.com/webinstall/webi-installers/internal/releases/iterm2dist"
|
|
"github.com/webinstall/webi-installers/internal/releases/juliadist"
|
|
"github.com/webinstall/webi-installers/internal/releases/lsd"
|
|
"github.com/webinstall/webi-installers/internal/releases/mariadbdist"
|
|
"github.com/webinstall/webi-installers/internal/releases/node"
|
|
"github.com/webinstall/webi-installers/internal/releases/ollama"
|
|
"github.com/webinstall/webi-installers/internal/releases/pwsh"
|
|
"github.com/webinstall/webi-installers/internal/releases/xcaddy"
|
|
"github.com/webinstall/webi-installers/internal/releases/zigdist"
|
|
"github.com/webinstall/webi-installers/internal/storage"
|
|
)
|
|
|
|
// Package classifies raw upstream data into assets, tags variants,
|
|
// and applies config-driven filters. This is the full classify pipeline
|
|
// for a single package.
|
|
func Package(pkg string, conf *installerconf.Conf, d *rawcache.Dir) ([]storage.Asset, error) {
|
|
assets, err := classifySource(pkg, conf, d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
TagVariants(pkg, assets)
|
|
NormalizeVersions(pkg, assets)
|
|
assets = ApplyConfig(assets, conf)
|
|
return assets, nil
|
|
}
|
|
|
|
// classifySource dispatches to the source-specific classifier.
|
|
func classifySource(pkg string, conf *installerconf.Conf, d *rawcache.Dir) ([]storage.Asset, error) {
|
|
switch conf.Source {
|
|
case "github":
|
|
return classifyGitHub(pkg, conf, d)
|
|
case "nodedist":
|
|
return classifyNodeDist(pkg, conf, d)
|
|
case "gittag":
|
|
return classifyGitTag(pkg, conf, d)
|
|
case "gitea":
|
|
return classifyGitea(pkg, conf, d)
|
|
case "chromedist":
|
|
return classifyChromeDist(d)
|
|
case "flutterdist":
|
|
return classifyFlutterDist(d)
|
|
case "golang":
|
|
return classifyGolang(d)
|
|
case "gpgdist":
|
|
return classifyGPGDist(d)
|
|
case "hashicorp":
|
|
return classifyHashiCorp(d)
|
|
case "iterm2dist":
|
|
return classifyITerm2Dist(d)
|
|
case "juliadist":
|
|
return classifyJuliaDist(d)
|
|
case "mariadbdist":
|
|
return classifyMariaDBDist(d)
|
|
case "zigdist":
|
|
return classifyZigDist(d)
|
|
default:
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
// NormalizeVersions applies package-specific version normalization.
|
|
// For example, Git for Windows strips ".windows.N" from version strings.
|
|
func NormalizeVersions(pkg string, assets []storage.Asset) {
|
|
switch pkg {
|
|
case "git":
|
|
git.NormalizeVersions(assets)
|
|
}
|
|
}
|
|
|
|
// TagVariants applies package-specific variant tags to classified assets.
|
|
// Each case delegates to a per-installer package under internal/releases/.
|
|
func TagVariants(pkg string, assets []storage.Asset) {
|
|
switch pkg {
|
|
case "bun":
|
|
bun.TagVariants(assets)
|
|
case "fish":
|
|
fish.TagVariants(assets)
|
|
case "git":
|
|
git.TagVariants(assets)
|
|
case "gitea":
|
|
gitea.TagVariants(assets)
|
|
case "lsd":
|
|
lsd.TagVariants(assets)
|
|
case "node":
|
|
node.TagVariants(assets)
|
|
case "ollama":
|
|
ollama.TagVariants(assets)
|
|
case "pwsh":
|
|
pwsh.TagVariants(assets)
|
|
case "xcaddy":
|
|
xcaddy.TagVariants(assets)
|
|
}
|
|
}
|
|
|
|
// ApplyConfig applies asset_filter, exclude, and version prefix stripping
|
|
// from a package's releases.conf.
|
|
func ApplyConfig(assets []storage.Asset, conf *installerconf.Conf) []storage.Asset {
|
|
filter := strings.ToLower(conf.AssetFilter)
|
|
excludes := conf.Exclude
|
|
prefixes := conf.VersionPrefixes
|
|
|
|
var out []storage.Asset
|
|
for _, a := range assets {
|
|
lower := strings.ToLower(a.Filename)
|
|
|
|
// Include filter: asset must contain this substring.
|
|
if filter != "" && !strings.Contains(lower, filter) {
|
|
continue
|
|
}
|
|
|
|
// Exclude filter.
|
|
skip := false
|
|
for _, ex := range excludes {
|
|
if strings.Contains(a.Filename, ex) {
|
|
skip = true
|
|
break
|
|
}
|
|
}
|
|
if skip {
|
|
continue
|
|
}
|
|
|
|
// Version prefix stripping.
|
|
for _, p := range prefixes {
|
|
if strings.HasPrefix(a.Version, p) {
|
|
a.Version = strings.TrimPrefix(a.Version, p)
|
|
break
|
|
}
|
|
}
|
|
|
|
out = append(out, a)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ReadAllRaw reads all non-directory, non-underscore-prefixed files from
|
|
// the active generation of a rawcache directory.
|
|
func ReadAllRaw(d *rawcache.Dir) (map[string][]byte, error) {
|
|
active, err := d.ActivePath()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entries, err := os.ReadDir(active)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make(map[string][]byte, len(entries))
|
|
for _, e := range entries {
|
|
if e.IsDir() || strings.HasPrefix(e.Name(), "_") {
|
|
continue
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(active, e.Name()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result[e.Name()] = data
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// --- GitHub ---
|
|
|
|
type ghRelease struct {
|
|
TagName string `json:"tag_name"`
|
|
Prerelease bool `json:"prerelease"`
|
|
Draft bool `json:"draft"`
|
|
PublishedAt string `json:"published_at"`
|
|
Assets []ghAsset `json:"assets"`
|
|
TarballURL string `json:"tarball_url"`
|
|
ZipballURL string `json:"zipball_url"`
|
|
}
|
|
|
|
type ghAsset struct {
|
|
Name string `json:"name"`
|
|
BrowserDownloadURL string `json:"browser_download_url"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
func classifyGitHub(pkg string, conf *installerconf.Conf, d *rawcache.Dir) ([]storage.Asset, error) {
|
|
tagPrefix := conf.TagPrefix
|
|
releases, err := ReadAllRaw(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var assets []storage.Asset
|
|
for _, data := range releases {
|
|
var rel ghRelease
|
|
if err := json.Unmarshal(data, &rel); err != nil {
|
|
continue
|
|
}
|
|
if rel.Draft {
|
|
continue
|
|
}
|
|
|
|
version := rel.TagName
|
|
if tagPrefix != "" {
|
|
version = strings.TrimPrefix(version, tagPrefix)
|
|
}
|
|
|
|
channel := "stable"
|
|
if rel.Prerelease {
|
|
channel = "beta"
|
|
}
|
|
|
|
date := ""
|
|
if len(rel.PublishedAt) >= 10 {
|
|
date = rel.PublishedAt[:10]
|
|
}
|
|
|
|
for _, a := range rel.Assets {
|
|
if classify.IsMetaAsset(a.Name) {
|
|
continue
|
|
}
|
|
|
|
r := classify.Filename(a.Name)
|
|
|
|
// Normalize .tgz → .tar.gz in the display filename.
|
|
// The download URL still points to the real file.
|
|
name := a.Name
|
|
if strings.HasSuffix(strings.ToLower(name), ".tgz") {
|
|
name = name[:len(name)-4] + ".tar.gz"
|
|
}
|
|
|
|
assets = append(assets, storage.Asset{
|
|
Filename: name,
|
|
Version: version,
|
|
Channel: channel,
|
|
OS: string(r.OS),
|
|
Arch: string(r.Arch),
|
|
Libc: string(r.Libc),
|
|
Format: string(r.Format),
|
|
Download: a.BrowserDownloadURL,
|
|
Date: date,
|
|
})
|
|
}
|
|
|
|
// Source archives for packages with no binary assets.
|
|
// These are installable on any POSIX system (shell scripts, etc.).
|
|
//
|
|
// GitHub has two archive URL formats:
|
|
// Releases with no uploaded assets are source-only — use the
|
|
// API-provided tarball/zipball URLs. These can also be installed
|
|
// via `git clone --branch <tag>` as a fallback.
|
|
//
|
|
// TODO: HEAD-follow the tarball_url at fetch time to get the
|
|
// resolved codeload URL and Content-Disposition filename
|
|
// (Owner-Repo-Tag-0-gCommitHash.ext). For now, use the tag
|
|
// as the filename since the actual download name comes from
|
|
// Content-Disposition anyway.
|
|
if len(rel.Assets) == 0 {
|
|
owner := conf.Owner
|
|
repo := conf.Repo
|
|
tag := rel.TagName
|
|
if rel.TarballURL != "" {
|
|
assets = append(assets, storage.Asset{
|
|
Filename: repo + "-" + tag + ".tar.gz",
|
|
Version: version,
|
|
Channel: channel,
|
|
OS: "posix_2017",
|
|
Arch: "*",
|
|
Format: ".tar.gz",
|
|
Download: rel.TarballURL,
|
|
Date: date,
|
|
})
|
|
}
|
|
if rel.ZipballURL != "" {
|
|
assets = append(assets, storage.Asset{
|
|
Filename: repo + "-" + tag + ".zip",
|
|
Version: version,
|
|
Channel: channel,
|
|
OS: "posix_2017",
|
|
Arch: "*",
|
|
Format: ".zip",
|
|
Download: rel.ZipballURL,
|
|
Date: date,
|
|
})
|
|
}
|
|
// Git clone asset — same as gittag source.
|
|
gitURL := fmt.Sprintf("https://github.com/%s/%s.git", owner, repo)
|
|
assets = append(assets, storage.Asset{
|
|
Filename: repo + "-" + tag,
|
|
Version: version,
|
|
Channel: channel,
|
|
Format: "git",
|
|
Download: gitURL,
|
|
Date: date,
|
|
})
|
|
}
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
// --- Node.js dist ---
|
|
|
|
type nodeEntry struct {
|
|
Version string `json:"version"`
|
|
Date string `json:"date"`
|
|
Files []string `json:"files"`
|
|
LTS json.RawMessage `json:"lts"`
|
|
}
|
|
|
|
func classifyNodeDist(pkg string, conf *installerconf.Conf, d *rawcache.Dir) ([]storage.Asset, error) {
|
|
officialURL := conf.BaseURL
|
|
unofficialURL := conf.Extra["unofficial_url"]
|
|
|
|
releases, err := ReadAllRaw(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var assets []storage.Asset
|
|
for tag, data := range releases {
|
|
var entry nodeEntry
|
|
if err := json.Unmarshal(data, &entry); err != nil {
|
|
continue
|
|
}
|
|
|
|
// Pick the right base URL from the tag prefix.
|
|
baseURL := officialURL
|
|
if strings.HasPrefix(tag, "unofficial_") {
|
|
baseURL = unofficialURL
|
|
}
|
|
|
|
lts := string(entry.LTS) != "false" && string(entry.LTS) != ""
|
|
channel := "stable"
|
|
ver := strings.TrimPrefix(entry.Version, "v")
|
|
parts := strings.SplitN(ver, ".", 2)
|
|
if len(parts) > 0 {
|
|
major := 0
|
|
fmt.Sscanf(parts[0], "%d", &major)
|
|
if major%2 != 0 {
|
|
channel = "beta"
|
|
}
|
|
}
|
|
|
|
for _, file := range entry.Files {
|
|
if file == "src" || file == "headers" {
|
|
continue
|
|
}
|
|
expanded := expandNodeFile(pkg, entry.Version, channel, entry.Date, lts, baseURL, file)
|
|
assets = append(assets, expanded...)
|
|
}
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
func expandNodeFile(pkg, version, channel, date string, lts bool, baseURL, file string) []storage.Asset {
|
|
parts := strings.Split(file, "-")
|
|
if len(parts) < 2 {
|
|
return nil
|
|
}
|
|
|
|
osMap := map[string]string{
|
|
"osx": "darwin", "linux": "linux", "win": "windows",
|
|
"sunos": "sunos", "aix": "aix",
|
|
}
|
|
archMap := map[string]string{
|
|
"x64": "x86_64", "x86": "x86", "arm64": "aarch64",
|
|
"armv7l": "armv7", "armv6l": "armv6",
|
|
"ppc64": "ppc64", "ppc64le": "ppc64le", "s390x": "s390x",
|
|
"riscv64": "riscv64", "loong64": "loong64",
|
|
}
|
|
|
|
os_ := osMap[parts[0]]
|
|
arch := archMap[parts[1]]
|
|
if os_ == "" || arch == "" {
|
|
return nil
|
|
}
|
|
|
|
libc := ""
|
|
pkgType := ""
|
|
if len(parts) > 2 {
|
|
pkgType = parts[2]
|
|
}
|
|
|
|
var formats []string
|
|
switch pkgType {
|
|
case "musl":
|
|
libc = "musl"
|
|
formats = []string{".tar.gz", ".tar.xz"}
|
|
case "tar":
|
|
formats = []string{".tar.gz", ".tar.xz"}
|
|
case "zip":
|
|
formats = []string{".zip"}
|
|
case "pkg":
|
|
formats = []string{".pkg"}
|
|
case "msi":
|
|
formats = []string{".msi"}
|
|
case "exe":
|
|
formats = []string{".exe"}
|
|
case "7z":
|
|
formats = []string{".7z"}
|
|
case "":
|
|
formats = []string{".tar.gz", ".tar.xz"}
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
if libc == "" && os_ == "linux" {
|
|
libc = "gnu"
|
|
}
|
|
|
|
osPart := parts[0]
|
|
if osPart == "osx" {
|
|
osPart = "darwin"
|
|
}
|
|
archPart := parts[1]
|
|
muslExtra := ""
|
|
if libc == "musl" {
|
|
muslExtra = "-musl"
|
|
}
|
|
|
|
var assets []storage.Asset
|
|
for _, format := range formats {
|
|
var filename string
|
|
if format == ".msi" {
|
|
filename = fmt.Sprintf("node-%s-%s%s%s", version, archPart, muslExtra, format)
|
|
} else {
|
|
filename = fmt.Sprintf("node-%s-%s-%s%s%s", version, osPart, archPart, muslExtra, format)
|
|
}
|
|
|
|
assets = append(assets, storage.Asset{
|
|
Filename: filename,
|
|
Version: version,
|
|
Channel: channel,
|
|
OS: os_,
|
|
Arch: arch,
|
|
Libc: libc,
|
|
Format: format,
|
|
Download: fmt.Sprintf("%s/%s/%s", baseURL, version, filename),
|
|
LTS: lts,
|
|
Date: date,
|
|
})
|
|
}
|
|
return assets
|
|
}
|
|
|
|
// --- Git tag ---
|
|
|
|
type gitTagEntry struct {
|
|
Version string `json:"Version"`
|
|
GitTag string `json:"GitTag"`
|
|
CommitHash string `json:"CommitHash"`
|
|
Date string `json:"Date"`
|
|
}
|
|
|
|
func classifyGitTag(pkg string, conf *installerconf.Conf, d *rawcache.Dir) ([]storage.Asset, error) {
|
|
releases, err := ReadAllRaw(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Derive repo name from the git URL for filenames.
|
|
// "https://github.com/tpope/vim-commentary.git" → "vim-commentary"
|
|
gitURL := conf.BaseURL
|
|
repoName := pkg
|
|
if gitURL != "" {
|
|
base := filepath.Base(gitURL)
|
|
repoName = strings.TrimSuffix(base, ".git")
|
|
}
|
|
|
|
var assets []storage.Asset
|
|
for _, data := range releases {
|
|
var entry gitTagEntry
|
|
if err := json.Unmarshal(data, &entry); err != nil {
|
|
continue
|
|
}
|
|
|
|
version := strings.TrimPrefix(entry.Version, "v")
|
|
date := ""
|
|
if len(entry.Date) >= 10 {
|
|
date = entry.Date[:10]
|
|
}
|
|
|
|
var filename string
|
|
if version != "" {
|
|
// Tagged release: "{repo}-{tag}" (e.g. "vim-commentary-v1.2")
|
|
filename = repoName + "-" + entry.GitTag
|
|
} else if len(entry.Date) >= 19 {
|
|
// Tagless repo (HEAD of master/main): synthesize a date-based
|
|
// version like Node.js does: "2023.10.10-18.42.21"
|
|
version = strings.ReplaceAll(entry.Date[:10], "-", ".") +
|
|
"-" + strings.ReplaceAll(entry.Date[11:19], ":", ".")
|
|
filename = repoName + "-v" + version
|
|
} else {
|
|
continue
|
|
}
|
|
|
|
assets = append(assets, storage.Asset{
|
|
Filename: filename,
|
|
Version: version,
|
|
Channel: "stable",
|
|
Format: "git",
|
|
Download: gitURL,
|
|
Date: date,
|
|
Extra: "commit:" + entry.CommitHash,
|
|
})
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
// --- Gitea ---
|
|
|
|
type giteaRelease struct {
|
|
TagName string `json:"tag_name"`
|
|
Prerelease bool `json:"prerelease"`
|
|
Draft bool `json:"draft"`
|
|
PublishedAt string `json:"published_at"`
|
|
Assets []giteaAsset `json:"assets"`
|
|
}
|
|
|
|
type giteaAsset struct {
|
|
Name string `json:"name"`
|
|
BrowserDownloadURL string `json:"browser_download_url"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
func classifyGitea(pkg string, conf *installerconf.Conf, d *rawcache.Dir) ([]storage.Asset, error) {
|
|
releases, err := ReadAllRaw(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var assets []storage.Asset
|
|
for _, data := range releases {
|
|
var rel giteaRelease
|
|
if err := json.Unmarshal(data, &rel); err != nil {
|
|
continue
|
|
}
|
|
if rel.Draft {
|
|
continue
|
|
}
|
|
|
|
channel := "stable"
|
|
if rel.Prerelease {
|
|
channel = "beta"
|
|
}
|
|
date := ""
|
|
if len(rel.PublishedAt) >= 10 {
|
|
date = rel.PublishedAt[:10]
|
|
}
|
|
|
|
for _, a := range rel.Assets {
|
|
if classify.IsMetaAsset(a.Name) {
|
|
continue
|
|
}
|
|
r := classify.Filename(a.Name)
|
|
|
|
assets = append(assets, storage.Asset{
|
|
Filename: a.Name,
|
|
Version: rel.TagName,
|
|
Channel: channel,
|
|
OS: string(r.OS),
|
|
Arch: string(r.Arch),
|
|
Libc: string(r.Libc),
|
|
Format: string(r.Format),
|
|
Download: a.BrowserDownloadURL,
|
|
Date: date,
|
|
})
|
|
}
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
// --- Chrome for Testing ---
|
|
|
|
func classifyChromeDist(d *rawcache.Dir) ([]storage.Asset, error) {
|
|
releases, err := ReadAllRaw(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var assets []storage.Asset
|
|
for _, data := range releases {
|
|
var ver chromedist.Version
|
|
if err := json.Unmarshal(data, &ver); err != nil {
|
|
continue
|
|
}
|
|
|
|
downloads := ver.Downloads["chromedriver"]
|
|
if len(downloads) == 0 {
|
|
continue
|
|
}
|
|
|
|
for _, dl := range downloads {
|
|
r := classify.Filename(dl.URL)
|
|
assets = append(assets, storage.Asset{
|
|
Filename: "chromedriver-" + dl.Platform + ".zip",
|
|
Version: ver.Version,
|
|
Channel: "stable",
|
|
OS: string(r.OS),
|
|
Arch: string(r.Arch),
|
|
Format: ".zip",
|
|
Download: dl.URL,
|
|
})
|
|
}
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
// --- Flutter ---
|
|
|
|
func classifyFlutterDist(d *rawcache.Dir) ([]storage.Asset, error) {
|
|
releases, err := ReadAllRaw(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var assets []storage.Asset
|
|
for _, data := range releases {
|
|
var rel flutterdist.Release
|
|
if err := json.Unmarshal(data, &rel); err != nil {
|
|
continue
|
|
}
|
|
|
|
date := ""
|
|
if len(rel.ReleaseDate) >= 10 {
|
|
date = rel.ReleaseDate[:10]
|
|
}
|
|
|
|
filename := filepath.Base(rel.Archive)
|
|
r := classify.Filename(filename)
|
|
|
|
assets = append(assets, storage.Asset{
|
|
Filename: filename,
|
|
Version: rel.Version,
|
|
Channel: rel.Channel,
|
|
OS: string(r.OS),
|
|
Arch: string(r.Arch),
|
|
Format: string(r.Format),
|
|
Download: rel.DownloadURL,
|
|
Date: date,
|
|
})
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
// --- Go (golang.org) ---
|
|
|
|
func classifyGolang(d *rawcache.Dir) ([]storage.Asset, error) {
|
|
releases, err := ReadAllRaw(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var assets []storage.Asset
|
|
for _, data := range releases {
|
|
var rel golang.Release
|
|
if err := json.Unmarshal(data, &rel); err != nil {
|
|
continue
|
|
}
|
|
|
|
// Strip "go" prefix from version: "go1.24.1" → "1.24.1"
|
|
version := strings.TrimPrefix(rel.Version, "go")
|
|
channel := "stable"
|
|
if !rel.Stable {
|
|
channel = "beta"
|
|
}
|
|
|
|
for _, f := range rel.Files {
|
|
if f.Kind == "source" {
|
|
continue
|
|
}
|
|
// Skip bootstrap and odd builds.
|
|
if strings.Contains(f.Filename, "bootstrap") {
|
|
continue
|
|
}
|
|
|
|
r := classify.Filename(f.Filename)
|
|
|
|
assets = append(assets, storage.Asset{
|
|
Filename: f.Filename,
|
|
Version: version,
|
|
Channel: channel,
|
|
OS: string(r.OS),
|
|
Arch: string(r.Arch),
|
|
Libc: string(r.Libc),
|
|
Format: string(r.Format),
|
|
Download: "https://dl.google.com/go/" + f.Filename,
|
|
})
|
|
}
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
// --- GPG (SourceForge) ---
|
|
|
|
func classifyGPGDist(d *rawcache.Dir) ([]storage.Asset, error) {
|
|
releases, err := ReadAllRaw(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var assets []storage.Asset
|
|
for _, data := range releases {
|
|
var entry gpgdist.Entry
|
|
if err := json.Unmarshal(data, &entry); err != nil {
|
|
continue
|
|
}
|
|
|
|
assets = append(assets, storage.Asset{
|
|
Filename: fmt.Sprintf("GnuPG-%s.dmg", entry.Version),
|
|
Version: entry.Version,
|
|
Channel: "stable",
|
|
OS: "darwin",
|
|
Arch: "amd64",
|
|
Format: ".dmg",
|
|
Download: entry.URL,
|
|
})
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
// --- HashiCorp ---
|
|
|
|
func classifyHashiCorp(d *rawcache.Dir) ([]storage.Asset, error) {
|
|
releases, err := ReadAllRaw(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var assets []storage.Asset
|
|
for _, data := range releases {
|
|
var ver hashicorp.Version
|
|
if err := json.Unmarshal(data, &ver); err != nil {
|
|
continue
|
|
}
|
|
|
|
channel := "stable"
|
|
v := ver.Version
|
|
if strings.Contains(v, "-rc") {
|
|
channel = "rc"
|
|
} else if strings.Contains(v, "-beta") {
|
|
channel = "beta"
|
|
} else if strings.Contains(v, "-alpha") {
|
|
channel = "alpha"
|
|
}
|
|
|
|
for _, b := range ver.Builds {
|
|
r := classify.Filename(b.Filename)
|
|
|
|
assets = append(assets, storage.Asset{
|
|
Filename: b.Filename,
|
|
Version: ver.Version,
|
|
Channel: channel,
|
|
OS: string(r.OS),
|
|
Arch: string(r.Arch),
|
|
Format: string(r.Format),
|
|
Download: b.URL,
|
|
})
|
|
}
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
// --- iTerm2 ---
|
|
|
|
func classifyITerm2Dist(d *rawcache.Dir) ([]storage.Asset, error) {
|
|
releases, err := ReadAllRaw(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var assets []storage.Asset
|
|
for _, data := range releases {
|
|
var entry iterm2dist.Entry
|
|
if err := json.Unmarshal(data, &entry); err != nil {
|
|
continue
|
|
}
|
|
|
|
filename := filepath.Base(entry.URL)
|
|
|
|
assets = append(assets, storage.Asset{
|
|
Filename: filename,
|
|
Version: entry.Version,
|
|
Channel: entry.Channel,
|
|
OS: "darwin",
|
|
Format: ".zip",
|
|
Download: entry.URL,
|
|
})
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
// --- Julia ---
|
|
|
|
func classifyJuliaDist(d *rawcache.Dir) ([]storage.Asset, error) {
|
|
releases, err := ReadAllRaw(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
osMap := map[string]string{
|
|
"mac": "darwin", "linux": "linux", "winnt": "windows",
|
|
"freebsd": "freebsd",
|
|
}
|
|
archMap := map[string]string{
|
|
"x86_64": "x86_64", "i686": "x86", "aarch64": "aarch64",
|
|
"armv7l": "armv7", "powerpc64le": "ppc64le",
|
|
}
|
|
|
|
var assets []storage.Asset
|
|
for _, data := range releases {
|
|
var rel juliadist.Release
|
|
if err := json.Unmarshal(data, &rel); err != nil {
|
|
continue
|
|
}
|
|
|
|
channel := "stable"
|
|
if !rel.Stable {
|
|
channel = "beta"
|
|
}
|
|
|
|
for _, f := range rel.Files {
|
|
if f.Kind == "installer" {
|
|
continue
|
|
}
|
|
|
|
os_ := osMap[f.OS]
|
|
arch := archMap[f.Arch]
|
|
libc := ""
|
|
if os_ == "linux" {
|
|
if strings.Contains(f.URL, "musl") {
|
|
libc = "musl"
|
|
} else {
|
|
libc = "gnu"
|
|
}
|
|
}
|
|
|
|
filename := filepath.Base(f.URL)
|
|
|
|
assets = append(assets, storage.Asset{
|
|
Filename: filename,
|
|
Version: rel.Version,
|
|
Channel: channel,
|
|
OS: os_,
|
|
Arch: arch,
|
|
Libc: libc,
|
|
Format: "." + f.Extension,
|
|
Download: f.URL,
|
|
})
|
|
}
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
// --- MariaDB ---
|
|
|
|
func classifyMariaDBDist(d *rawcache.Dir) ([]storage.Asset, error) {
|
|
releases, err := ReadAllRaw(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
channelMap := map[string]string{
|
|
"Stable": "stable", "RC": "rc", "Alpha": "preview",
|
|
}
|
|
|
|
var assets []storage.Asset
|
|
for _, data := range releases {
|
|
var rel mariadbdist.Release
|
|
if err := json.Unmarshal(data, &rel); err != nil {
|
|
continue
|
|
}
|
|
|
|
channel := channelMap[rel.MajorStatus]
|
|
if channel == "" {
|
|
channel = "preview"
|
|
}
|
|
|
|
lts := rel.MajorStatus == "Stable"
|
|
|
|
for _, f := range rel.Files {
|
|
// Skip source packages. The API uses OS="Source" and
|
|
// sometimes " " (not empty) for CPU on source tarballs.
|
|
if strings.EqualFold(f.OS, "source") || strings.TrimSpace(f.OS) == "" || strings.TrimSpace(f.CPU) == "" {
|
|
continue
|
|
}
|
|
// Skip debug builds.
|
|
if strings.Contains(strings.ToLower(f.FileName), "debug") {
|
|
continue
|
|
}
|
|
|
|
r := classify.Filename(f.FileName)
|
|
|
|
assets = append(assets, storage.Asset{
|
|
Filename: f.FileName,
|
|
Version: rel.ReleaseID,
|
|
Channel: channel,
|
|
LTS: lts,
|
|
OS: string(r.OS),
|
|
Arch: string(r.Arch),
|
|
Format: string(r.Format),
|
|
Download: f.FileDownloadURL,
|
|
Date: rel.DateOfRelease,
|
|
})
|
|
}
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
// --- Zig ---
|
|
|
|
func classifyZigDist(d *rawcache.Dir) ([]storage.Asset, error) {
|
|
releases, err := ReadAllRaw(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var assets []storage.Asset
|
|
for _, data := range releases {
|
|
var rel zigdist.Release
|
|
if err := json.Unmarshal(data, &rel); err != nil {
|
|
continue
|
|
}
|
|
|
|
channel := "stable"
|
|
if !strings.Contains(rel.Version, ".") {
|
|
// Branch names like "master" have no dots.
|
|
channel = "beta"
|
|
} else if strings.ContainsAny(rel.Version, "+-") {
|
|
channel = "beta"
|
|
}
|
|
|
|
for platform, p := range rel.Platforms {
|
|
// Skip source and odd entries.
|
|
if strings.Contains(platform, "bootstrap") || platform == "src" {
|
|
continue
|
|
}
|
|
if strings.Contains(platform, "armv6kz") {
|
|
continue
|
|
}
|
|
|
|
// Platform is "arch-os", e.g. "x86_64-linux", "aarch64-macos".
|
|
parts := strings.SplitN(platform, "-", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
filename := filepath.Base(p.Tarball)
|
|
r := classify.Filename(filename)
|
|
|
|
assets = append(assets, storage.Asset{
|
|
Filename: filename,
|
|
Version: rel.Version,
|
|
Channel: channel,
|
|
OS: string(r.OS),
|
|
Arch: string(r.Arch),
|
|
Format: string(r.Format),
|
|
Download: p.Tarball,
|
|
Date: rel.Date,
|
|
})
|
|
}
|
|
}
|
|
return assets, nil
|
|
}
|