feat: whitespace-delimited releases.conf, variant tagging

- Switch installerconf parser from comma to whitespace delimiters
- Add asset_exclude as alias for exclude (fixes hugo)
- Add variants key (documentation cue, detection in Go code)
- Add per-package variant taggers: bun (profile, amd64v3 arch),
  pwsh (fxdependent), ollama (rocm, jetpack5/6), git (installer),
  node (msi installer), lsd (deb, msvc), fish (pkg), xcaddy (deb)
- Update releases.conf files with variant declarations
This commit is contained in:
AJ ONeal
2026-03-10 13:30:33 -06:00
parent d229eb618d
commit 39c136caa3
8 changed files with 200 additions and 26 deletions

View File

@@ -1,3 +1,7 @@
source = github
owner = oven-sh
repo = bun
# non-baseline=amd64v3, -baseline=amd64
default_x86_64 = x86_64_v3
x86_64_v2 = baseline
variants = profile

View File

@@ -199,6 +199,9 @@ func (wc *WebiCache) refreshPackage(ctx context.Context, pkg pkgConf) error {
return fmt.Errorf("classify: %w", err)
}
// Step 2.5: Tag build variants.
tagVariants(name, conf, assets)
// Step 3: Apply config transforms.
assets = applyConfig(assets, conf)
@@ -1449,3 +1452,135 @@ func isMetaAsset(name string) bool {
return false
}
// tagVariants sets Asset.Variants for known build variants.
// Detection logic is per-package; the releases.conf "variants" key
// is documentation only — actual pattern matching lives here.
func tagVariants(pkg string, conf *installerconf.Conf, assets []storage.Asset) {
switch pkg {
case "bun":
tagVariantsBun(assets)
case "pwsh":
tagVariantsPwsh(assets)
case "ollama":
tagVariantsOllama(assets)
case "git":
tagVariantsGit(assets)
case "node":
tagVariantsNode(assets)
case "lsd":
tagVariantsLsd(assets)
case "fish":
tagVariantsFish(assets)
case "xcaddy":
tagVariantsXcaddy(assets)
}
}
// tagVariantsBun: -profile is a debug build, -baseline is actually amd64
// (non-baseline is amd64v3). Arch remapping + variant tagging.
func tagVariantsBun(assets []storage.Asset) {
for i := range assets {
lower := strings.ToLower(assets[i].Filename)
if strings.Contains(lower, "-profile") {
assets[i].Variants = append(assets[i].Variants, "profile")
}
// Non-baseline x86_64 is actually amd64v3; baseline is plain amd64.
if assets[i].Arch == "amd64" {
if strings.Contains(lower, "-baseline") {
// baseline stays amd64, no variant needed
} else {
// non-baseline is the v3 microarchitecture
assets[i].Arch = "amd64v3"
}
}
}
}
// tagVariantsPwsh: -fxdependent and -fxdependentWinDesktop are
// .NET framework-dependent builds (smaller, require .NET runtime).
func tagVariantsPwsh(assets []storage.Asset) {
for i := range assets {
lower := strings.ToLower(assets[i].Filename)
if strings.Contains(lower, "-fxdependentwindesktop") {
assets[i].Variants = append(assets[i].Variants, "fxdependentWinDesktop")
} else if strings.Contains(lower, "-fxdependent") {
assets[i].Variants = append(assets[i].Variants, "fxdependent")
}
}
}
// tagVariantsOllama: GPU accelerator builds (-rocm, -jetpack5, -jetpack6).
func tagVariantsOllama(assets []storage.Asset) {
for i := range assets {
lower := strings.ToLower(assets[i].Filename)
for _, v := range []string{"rocm", "jetpack5", "jetpack6"} {
if strings.Contains(lower, "-"+v) {
assets[i].Variants = append(assets[i].Variants, v)
}
}
}
}
// tagVariantsGit: GUI installer .exe files (Git-*-bit.exe, PortableGit, etc.)
// vs MinGit .zip which is the actual portable binary.
func tagVariantsGit(assets []storage.Asset) {
for i := range assets {
name := assets[i].Filename
lower := strings.ToLower(name)
// Git-2.48.1-64-bit.exe and similar are GUI installers
if assets[i].Format == ".exe" {
assets[i].Variants = append(assets[i].Variants, "installer")
}
// PortableGit is a self-extracting installer
if strings.Contains(lower, "portablegit") {
assets[i].Variants = append(assets[i].Variants, "installer")
}
// .pdb archives are debug symbols
if strings.Contains(lower, "-pdb") {
assets[i].Variants = append(assets[i].Variants, "pdb")
}
}
}
// tagVariantsNode: .exe files for Windows are the bare binary, but
// node-v*-x64.msi and similar are GUI installers.
func tagVariantsNode(assets []storage.Asset) {
for i := range assets {
if assets[i].Format == ".msi" {
assets[i].Variants = append(assets[i].Variants, "installer")
}
// node-v25.8.0-win-x64.exe is a bare binary, not an installer.
// Only .msi is the installer for node.
}
}
// tagVariantsLsd: .deb packages and windows-msvc builds.
func tagVariantsLsd(assets []storage.Asset) {
for i := range assets {
if assets[i].Format == ".deb" {
assets[i].Variants = append(assets[i].Variants, "deb")
}
if strings.Contains(strings.ToLower(assets[i].Filename), "-msvc") {
assets[i].Variants = append(assets[i].Variants, "msvc")
}
}
}
// tagVariantsFish: .pkg installers and source tarballs.
func tagVariantsFish(assets []storage.Asset) {
for i := range assets {
if assets[i].Format == ".pkg" {
assets[i].Variants = append(assets[i].Variants, "installer")
}
}
}
// tagVariantsXcaddy: .deb packages.
func tagVariantsXcaddy(assets []storage.Asset) {
for i := range assets {
if assets[i].Format == ".deb" {
assets[i].Variants = append(assets[i].Variants, "deb")
}
}
}

View File

@@ -1,3 +1,4 @@
source = github
owner = git-for-windows
repo = git
variants = installer

View File

@@ -1,4 +1,4 @@
source = github
owner = gohugoio
repo = hugo
asset_exclude = extended
exclude = extended

View File

@@ -2,6 +2,7 @@
//
// The format is simple key=value, one per line. Blank lines and lines
// starting with # are ignored. Keys and values are trimmed of whitespace.
// Multi-value keys are whitespace-delimited.
//
// Minimal example (covers ~60% of packages):
//
@@ -16,12 +17,13 @@
// repo = jq
// version_prefixes = jq-
//
// With filename exclusions (hugo publishes _extended_ variants):
// With filename exclusions and variant documentation:
//
// source = github
// owner = gohugoio
// repo = hugo
// exclude = _extended_, Linux-64bit
// exclude = _extended_ Linux-64bit
// variants = extended extended_withdeploy
//
// Monorepo with tag prefix:
//
@@ -43,6 +45,8 @@
// Complex packages that need custom logic beyond what the classifier
// auto-detects (e.g. ollama's universal binaries, ffmpeg's non-standard
// naming) should put that logic in Go code, not in the config.
// The variants key documents known build variants for human readers;
// actual variant detection logic lives in Go.
package installerconf
import (
@@ -74,17 +78,21 @@ type Conf struct {
TagPrefix string
// VersionPrefixes are stripped from version/tag strings.
// Comma-separated. Each release tag is checked against these in order;
// the first match is stripped. Projects may change tag conventions across
// versions (e.g. "jq-1.7.1" in older releases, bare "1.8.0" later).
// Example: "jq-, cli-"
// Whitespace-delimited. Each release tag is checked against these
// in order; the first match is stripped. Projects may change tag
// conventions across versions (e.g. "jq-1.7.1" older, "1.8.0" later).
VersionPrefixes []string
// Exclude lists filename substrings to filter out.
// Assets whose name contains any of these are skipped.
// Example: ["_extended_", "-gogit-", "-docs-"]
// Whitespace-delimited. Assets whose name contains any of these
// are skipped entirely (not stored).
Exclude []string
// Variants documents known build variant names for this package.
// Whitespace-delimited. This is a human-readable cue — actual
// variant detection logic lives in Go code per-package.
Variants []string
// Extra holds any unrecognized keys for forward compatibility.
Extra map[string]string
}
@@ -121,15 +129,9 @@ func Read(path string) (*Conf, error) {
c.TagPrefix = raw["tag_prefix"]
if v := raw["version_prefixes"]; v != "" {
for _, p := range strings.Split(v, ",") {
p = strings.TrimSpace(p)
if p != "" {
c.VersionPrefixes = append(c.VersionPrefixes, p)
}
}
c.VersionPrefixes = strings.Fields(v)
} else if v := raw["version_prefix"]; v != "" {
// Back-compat with singular form.
c.VersionPrefixes = []string{v}
c.VersionPrefixes = strings.Fields(v)
}
if v := raw["base_url"]; v != "" {
@@ -138,13 +140,15 @@ func Read(path string) (*Conf, error) {
c.BaseURL = raw["url"]
}
// Accept both "exclude" and "asset_exclude" (back-compat).
if v := raw["exclude"]; v != "" {
for _, p := range strings.Split(v, ",") {
p = strings.TrimSpace(p)
if p != "" {
c.Exclude = append(c.Exclude, p)
}
}
c.Exclude = strings.Fields(v)
} else if v := raw["asset_exclude"]; v != "" {
c.Exclude = strings.Fields(v)
}
if v := raw["variants"]; v != "" {
c.Variants = strings.Fields(v)
}
// Collect unrecognized keys.
@@ -152,7 +156,7 @@ func Read(path string) (*Conf, error) {
"source": true, "owner": true, "repo": true,
"base_url": true, "url": true,
"tag_prefix": true, "version_prefix": true, "version_prefixes": true,
"exclude": true,
"exclude": true, "asset_exclude": true, "variants": true,
}
for k, v := range raw {
if !known[k] {

View File

@@ -32,7 +32,7 @@ func TestVersionPrefixes(t *testing.T) {
source = github
owner = jqlang
repo = jq
version_prefixes = jq-, cli-
version_prefixes = jq- cli-
`)
if len(c.VersionPrefixes) != 2 {
t.Fatalf("VersionPrefixes has %d items, want 2: %v", len(c.VersionPrefixes), c.VersionPrefixes)
@@ -46,7 +46,7 @@ func TestExclude(t *testing.T) {
source = github
owner = gohugoio
repo = hugo
exclude = _extended_, Linux-64bit
exclude = _extended_ Linux-64bit
`)
if len(c.Exclude) != 2 {
t.Fatalf("Exclude has %d items, want 2: %v", len(c.Exclude), c.Exclude)
@@ -110,6 +110,34 @@ custom_thing = hello
}
}
func TestAssetExcludeAlias(t *testing.T) {
c := confFromString(t, `
source = github
owner = gohugoio
repo = hugo
asset_exclude = extended
`)
if len(c.Exclude) != 1 {
t.Fatalf("Exclude has %d items, want 1: %v", len(c.Exclude), c.Exclude)
}
assertEqual(t, "Exclude[0]", c.Exclude[0], "extended")
}
func TestVariants(t *testing.T) {
c := confFromString(t, `
source = github
owner = jmorganca
repo = ollama
variants = rocm jetpack5 jetpack6
`)
if len(c.Variants) != 3 {
t.Fatalf("Variants has %d items, want 3: %v", len(c.Variants), c.Variants)
}
assertEqual(t, "Variants[0]", c.Variants[0], "rocm")
assertEqual(t, "Variants[1]", c.Variants[1], "jetpack5")
assertEqual(t, "Variants[2]", c.Variants[2], "jetpack6")
}
func TestEmptyExclude(t *testing.T) {
c := confFromString(t, "source = github\n")
if c.Exclude != nil {

View File

@@ -1,3 +1,4 @@
source = github
owner = jmorganca
repo = ollama
variants = rocm jetpack5 jetpack6

View File

@@ -1,3 +1,4 @@
source = github
owner = powershell
repo = powershell
variants = fxdependent fxdependentWinDesktop