feat(monorel): add goarch/goarm/goamd64 to build matrix with --almost-all/--ios/--android-ndk

Default matrix (conservative, CGO_ENABLED=0):
  goos:   darwin freebsd js linux netbsd openbsd wasip1 windows
  goarch: amd64 arm arm64 mips64le mipsle ppc64le riscv64 wasm
  goarm:  6 7  (always included when arm is in goarch)

--almost-all widens to esoteric goos (aix dragonfly illumos plan9
solaris), adds 386/loong64/mips/mips64/ppc64/s390x to goarch, and
emits goamd64: v1 v2 v3 v4.

--ios generates an active iOS build entry (CGO_ENABLED=1, arm64)
instead of the default commented stub.

--android-ndk generates an active Android NDK build entry
(CGO_ENABLED=1, arm64) instead of the default commented stub.

Both --ios and --android-ndk are available on init and release.
The existing -A flag (include hidden dirs) is unchanged.
This commit is contained in:
AJ ONeal 2026-03-01 18:22:28 -07:00
parent 1cb478b475
commit fd3cb8ba99
No known key found for this signature in database

View File

@ -31,6 +31,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -44,14 +45,45 @@ import (
// Adjust this slice if you ever need to search across repository boundaries. // Adjust this slice if you ever need to search across repository boundaries.
var stopMarkers = []string{".git"} var stopMarkers = []string{".git"}
// defaultGoos is the CGO_ENABLED=0 build matrix used in generated .goreleaser.yaml files. // defaultGoos is the conservative CGO_ENABLED=0 goos list used in generated
// Platforms requiring CGO or special toolchains (ios, android) are emitted as comments. // .goreleaser.yaml files. Use --almost-all to widen the net.
// Platforms requiring CGO or special toolchains (ios, android) are handled
// separately via the --ios and --android-ndk flags.
var defaultGoos = []string{ var defaultGoos = []string{
"darwin", "freebsd", "js", "linux",
"netbsd", "openbsd", "wasip1", "windows",
}
// almostAllGoos extends defaultGoos with less-commonly-targeted CGO_ENABLED=0 platforms.
var almostAllGoos = []string{
"aix", "darwin", "dragonfly", "freebsd", "illumos", "aix", "darwin", "dragonfly", "freebsd", "illumos",
"js", "linux", "netbsd", "openbsd", "plan9", "js", "linux", "netbsd", "openbsd", "plan9",
"solaris", "wasip1", "windows", "solaris", "wasip1", "windows",
} }
// defaultGoarch is the conservative architecture list for generated builds.
var defaultGoarch = []string{
"amd64", "arm", "arm64", "mips64le", "mipsle", "ppc64le", "riscv64", "wasm",
}
// almostAllGoarch extends defaultGoarch with less-common architectures.
var almostAllGoarch = []string{
"386",
"amd64", "arm", "arm64",
"loong64", "mips", "mips64", "mips64le", "mipsle",
"ppc64", "ppc64le", "riscv64", "s390x", "wasm",
}
// defaultGoarm is the ARM sub-architecture list (ARMv6 and ARMv7).
// Included whenever "arm" appears in the goarch list.
var defaultGoarm = []string{"6", "7"}
// defaultGoamd64 is the amd64 micro-architecture level list used with --almost-all.
var defaultGoamd64 = []string{"v1", "v2"}
// almostAllGoamd64 is the amd64 micro-architecture level list used with --almost-all.
var almostAllGoamd64 = []string{"v1", "v2", "v3", "v4"}
// ── Types ────────────────────────────────────────────────────────────────── // ── Types ──────────────────────────────────────────────────────────────────
// binary describes one Go main package to build and release. // binary describes one Go main package to build and release.
@ -75,6 +107,14 @@ type releaseStep struct {
run func() error run func() error
} }
// buildOptions controls which platforms and features are included in the
// generated .goreleaser.yaml.
type buildOptions struct {
almostAll bool // include esoteric goos/goarch targets and goamd64 sub-versions
ios bool // generate active iOS build (requires CGO_ENABLED=1 + Xcode)
androidNDK bool // generate active Android NDK build (requires CGO_ENABLED=1 + NDK)
}
// ── Entry point ──────────────────────────────────────────────────────────── // ── Entry point ────────────────────────────────────────────────────────────
func main() { func main() {
@ -121,6 +161,7 @@ func usage() {
func runRelease(args []string) { func runRelease(args []string) {
fs := flag.NewFlagSet("monorel release", flag.ExitOnError) fs := flag.NewFlagSet("monorel release", flag.ExitOnError)
var recursive, all, dryRun, yes, force, draft, prerelease bool var recursive, all, dryRun, yes, force, draft, prerelease bool
var almostAll, ios, androidNDK bool
fs.BoolVar(&recursive, "recursive", false, "find all main packages recursively under each path") fs.BoolVar(&recursive, "recursive", false, "find all main packages recursively under each path")
fs.BoolVar(&all, "A", false, "include dot/underscore-prefixed directories; warn rather than error on failures") fs.BoolVar(&all, "A", false, "include dot/underscore-prefixed directories; warn rather than error on failures")
fs.BoolVar(&dryRun, "dry-run", false, "show each step without running it") fs.BoolVar(&dryRun, "dry-run", false, "show each step without running it")
@ -128,6 +169,9 @@ func runRelease(args []string) {
fs.BoolVar(&force, "force", false, "overwrite .goreleaser.yaml without prompting even if it has been modified") fs.BoolVar(&force, "force", false, "overwrite .goreleaser.yaml without prompting even if it has been modified")
fs.BoolVar(&draft, "draft", false, "keep the GitHub release in draft state after uploading (default: publish)") fs.BoolVar(&draft, "draft", false, "keep the GitHub release in draft state after uploading (default: publish)")
fs.BoolVar(&prerelease, "prerelease", false, "keep the GitHub release marked as pre-release even for clean tags (default: promote clean tags to stable)") fs.BoolVar(&prerelease, "prerelease", false, "keep the GitHub release marked as pre-release even for clean tags (default: promote clean tags to stable)")
fs.BoolVar(&almostAll, "almost-all", false, "widen build matrix to include esoteric goos/goarch targets and goamd64 v1-v4")
fs.BoolVar(&ios, "ios", false, "add an iOS build entry to the generated .goreleaser.yaml (requires CGO_ENABLED=1 and Xcode)")
fs.BoolVar(&androidNDK, "android-ndk", false, "add an Android NDK build entry to the generated .goreleaser.yaml (requires CGO_ENABLED=1 and NDK)")
fs.Usage = func() { fs.Usage = func() {
fmt.Fprintln(os.Stderr, "usage: monorel release [options] <binary-path>...") fmt.Fprintln(os.Stderr, "usage: monorel release [options] <binary-path>...")
fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "")
@ -149,6 +193,8 @@ func runRelease(args []string) {
os.Exit(2) os.Exit(2)
} }
opts := buildOptions{almostAll: almostAll, ios: ios, androidNDK: androidNDK}
allPaths, err := expandPaths(binPaths, recursive, all) allPaths, err := expandPaths(binPaths, recursive, all)
if err != nil { if err != nil {
fatalf("%v", err) fatalf("%v", err)
@ -170,7 +216,7 @@ func runRelease(args []string) {
printGroupHeader(cwd, group) printGroupHeader(cwd, group)
relPath, _ := filepath.Rel(cwd, group.root) relPath, _ := filepath.Rel(cwd, group.root)
relPath = filepath.ToSlash(relPath) relPath = filepath.ToSlash(relPath)
processModule(group, relPath, dryRun, yes, force, draft, prerelease) processModule(group, relPath, dryRun, yes, force, draft, prerelease, opts)
} }
} }
@ -251,10 +297,14 @@ func runBump(args []string) {
func runInit(args []string) { func runInit(args []string) {
fs := flag.NewFlagSet("monorel init", flag.ExitOnError) fs := flag.NewFlagSet("monorel init", flag.ExitOnError)
var recursive, all, dryRun, cmd bool var recursive, all, dryRun, cmd bool
var almostAll, ios, androidNDK bool
fs.BoolVar(&recursive, "recursive", false, "find all main packages recursively under each path") fs.BoolVar(&recursive, "recursive", false, "find all main packages recursively under each path")
fs.BoolVar(&all, "A", false, "include dot/underscore-prefixed directories; warn rather than error on failures") fs.BoolVar(&all, "A", false, "include dot/underscore-prefixed directories; warn rather than error on failures")
fs.BoolVar(&dryRun, "dry-run", false, "print what would happen without writing files, creating commits, or tags") fs.BoolVar(&dryRun, "dry-run", false, "print what would happen without writing files, creating commits, or tags")
fs.BoolVar(&cmd, "cmd", false, "for each cmd/ child with package main, run go mod init+tidy (suggests a commit at the end)") fs.BoolVar(&cmd, "cmd", false, "for each cmd/ child with package main, run go mod init+tidy (suggests a commit at the end)")
fs.BoolVar(&almostAll, "almost-all", false, "widen build matrix to include esoteric goos/goarch targets and goamd64 v1-v4")
fs.BoolVar(&ios, "ios", false, "add an iOS build entry to the generated .goreleaser.yaml (requires CGO_ENABLED=1 and Xcode)")
fs.BoolVar(&androidNDK, "android-ndk", false, "add an Android NDK build entry to the generated .goreleaser.yaml (requires CGO_ENABLED=1 and NDK)")
fs.Usage = func() { fs.Usage = func() {
fmt.Fprintln(os.Stderr, "usage: monorel init [options] <binary-path>...") fmt.Fprintln(os.Stderr, "usage: monorel init [options] <binary-path>...")
fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "")
@ -280,6 +330,8 @@ func runInit(args []string) {
os.Exit(2) os.Exit(2)
} }
opts := buildOptions{almostAll: almostAll, ios: ios, androidNDK: androidNDK}
if cmd { if cmd {
initCmdModules(binPaths, dryRun) initCmdModules(binPaths, dryRun)
} }
@ -301,14 +353,14 @@ func runInit(args []string) {
fmt.Fprintln(os.Stderr) fmt.Fprintln(os.Stderr)
} }
printGroupHeader(cwd, group) printGroupHeader(cwd, group)
initModuleGroup(group, dryRun) initModuleGroup(group, dryRun, opts)
} }
} }
// initModuleGroup writes .goreleaser.yaml, commits it (if changed), and // initModuleGroup writes .goreleaser.yaml, commits it (if changed), and
// creates an initial version tag (bump patch) for one module group. // creates an initial version tag (bump patch) for one module group.
// When dryRun is true no files are written and no git mutations are made. // When dryRun is true no files are written and no git mutations are made.
func initModuleGroup(group *moduleGroup, dryRun bool) { func initModuleGroup(group *moduleGroup, dryRun bool, opts buildOptions) {
modRoot := group.root modRoot := group.root
bins := group.bins bins := group.bins
@ -331,7 +383,7 @@ func initModuleGroup(group *moduleGroup, dryRun bool) {
projectName := prefixParts[len(prefixParts)-1] projectName := prefixParts[len(prefixParts)-1]
// 1. Write .goreleaser.yaml only when the content has changed. // 1. Write .goreleaser.yaml only when the content has changed.
yamlContent := goreleaserYAML(projectName, bins) yamlContent := goreleaserYAML(projectName, bins, opts)
yamlPath := filepath.Join(modRoot, ".goreleaser.yaml") yamlPath := filepath.Join(modRoot, ".goreleaser.yaml")
existing, readErr := os.ReadFile(yamlPath) existing, readErr := os.ReadFile(yamlPath)
isNewFile := readErr != nil isNewFile := readErr != nil
@ -586,13 +638,7 @@ func findMainPackages(root string, all bool) ([]string, error) {
// A .git FILE (submodule pointer) is not a directory, so it is // A .git FILE (submodule pointer) is not a directory, so it is
// not matched here and we keep descending — consistent with // not matched here and we keep descending — consistent with
// findModuleRoot's behaviour. // findModuleRoot's behaviour.
skip := false skip := slices.Contains(stopMarkers, name)
for _, stop := range stopMarkers {
if name == stop {
skip = true
break
}
}
if skip { if skip {
continue continue
} }
@ -652,8 +698,8 @@ func readModulePath(modRoot string) string {
} }
for _, line := range strings.Split(string(data), "\n") { for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if strings.HasPrefix(line, "module ") { if str, ok := strings.CutPrefix(line, "module "); ok {
return strings.TrimSpace(strings.TrimPrefix(line, "module ")) return strings.TrimSpace(str)
} }
} }
return "" return ""
@ -735,13 +781,7 @@ func initCmdModules(roots []string, dryRun bool) {
continue continue
} }
name := e.Name() name := e.Name()
isStop := false isStop := slices.Contains(stopMarkers, name)
for _, stop := range stopMarkers {
if name == stop {
isStop = true
break
}
}
if isStop || (len(name) > 0 && (name[0] == '.' || name[0] == '_')) { if isStop || (len(name) > 0 && (name[0] == '.' || name[0] == '_')) {
continue continue
} }
@ -961,7 +1001,7 @@ func printGroupHeader(cwd string, group *moduleGroup) {
// for one module group. relPath is the path from the caller's CWD to the // for one module group. relPath is the path from the caller's CWD to the
// module root; it is used in the script for all paths so that the script can // module root; it is used in the script for all paths so that the script can
// be run from the directory where monorel was invoked. // be run from the directory where monorel was invoked.
func processModule(group *moduleGroup, relPath string, dryRun, yes, force, draft, prerelease bool) { func processModule(group *moduleGroup, relPath string, dryRun, yes, force, draft, prerelease bool, opts buildOptions) {
modRoot := group.root modRoot := group.root
bins := group.bins bins := group.bins
@ -985,7 +1025,7 @@ func processModule(group *moduleGroup, relPath string, dryRun, yes, force, draft
// {{ .ProjectName }} template and at least one binary uses the VERSION env // {{ .ProjectName }} template and at least one binary uses the VERSION env
// var — local edits that add extra binaries etc. are preserved. // var — local edits that add extra binaries etc. are preserved.
// Auto-commit and auto-tag only apply when the file is brand new. // Auto-commit and auto-tag only apply when the file is brand new.
yamlContent := goreleaserYAML(projectName, bins) yamlContent := goreleaserYAML(projectName, bins, opts)
yamlPath := filepath.Join(modRoot, ".goreleaser.yaml") yamlPath := filepath.Join(modRoot, ".goreleaser.yaml")
existing, readErr := os.ReadFile(yamlPath) existing, readErr := os.ReadFile(yamlPath)
isNewFile := readErr != nil isNewFile := readErr != nil
@ -1233,7 +1273,7 @@ func preNum(s string) int {
// - Each binary gets its own build (id) and archive (ids) for separate tarballs. // - Each binary gets its own build (id) and archive (ids) for separate tarballs.
// - release.disable: true — we use `gh release` instead (goreleaser Pro // - release.disable: true — we use `gh release` instead (goreleaser Pro
// would be needed to publish via a prefixed tag). // would be needed to publish via a prefixed tag).
func goreleaserYAML(projectName string, bins []binary) string { func goreleaserYAML(projectName string, bins []binary, opts buildOptions) string {
var b strings.Builder var b strings.Builder
w := func(s string) { b.WriteString(s) } w := func(s string) { b.WriteString(s) }
wf := func(format string, args ...any) { fmt.Fprintf(&b, format, args...) } wf := func(format string, args ...any) { fmt.Fprintf(&b, format, args...) }
@ -1249,11 +1289,50 @@ func goreleaserYAML(projectName string, bins []binary) string {
w(" # you may remove this if you don't need go generate\n") w(" # you may remove this if you don't need go generate\n")
w(" - go generate ./...\n") w(" - go generate ./...\n")
// Select goos/goarch lists based on options.
goos := defaultGoos
goarch := defaultGoarch
goamd64 := defaultGoamd64
if opts.almostAll {
goos = almostAllGoos
goarch = almostAllGoarch
goamd64 = almostAllGoamd64
}
// When multiple binaries share a module, define the common build options // When multiple binaries share a module, define the common build options
// once with a YAML anchor on the first build and merge them into the rest. // once with a YAML anchor on the first build and merge them into the rest.
// Single-binary modules use plain fields (no anchor overhead). // Single-binary modules use plain fields (no anchor overhead).
multibin := len(bins) > 1 multibin := len(bins) > 1
// writeBuildDefaults emits env, ldflags, goos, goarch, goarm, and (if
// --almost-all) goamd64 at the given indent level (2 or 4 spaces of extra
// indent relative to the builds list item).
writeBuildDefaults := func(indent string) {
wf("%senv:\n%s - CGO_ENABLED=0\n", indent, indent)
wf("%sldflags:\n", indent)
wf("%s - -s -w"+
" -X main.version={{.Env.VERSION}}"+
" -X main.commit={{.Commit}}"+
" -X main.date={{.Date}}"+
" -X main.builtBy=goreleaser\n", indent)
wf("%sgoos:\n", indent)
for _, g := range goos {
wf("%s - %s\n", indent, g)
}
wf("%sgoarch:\n", indent)
for _, a := range goarch {
wf("%s - %s\n", indent, a)
}
wf("%sgoarm:\n", indent)
for _, v := range defaultGoarm {
wf("%s - %s\n", indent, v)
}
wf("%sgoamd64:\n", indent)
for _, v := range goamd64 {
wf("%s - %s\n", indent, v)
}
}
w("\nbuilds:\n") w("\nbuilds:\n")
for i, bin := range bins { for i, bin := range bins {
wf(" - id: %s\n", bin.name) wf(" - id: %s\n", bin.name)
@ -1266,63 +1345,42 @@ func goreleaserYAML(projectName string, bins []binary) string {
switch { switch {
case !multibin: case !multibin:
// Single binary: plain fields. // Single binary: plain fields.
w(" env:\n - CGO_ENABLED=0\n") writeBuildDefaults(" ")
w(" ldflags:\n")
w(" - -s -w" +
" -X main.version={{.Env.VERSION}}" +
" -X main.commit={{.Commit}}" +
" -X main.date={{.Date}}" +
" -X main.builtBy=goreleaser\n")
w(" goos:\n")
for _, g := range defaultGoos {
wf(" - %s\n", g)
}
case i == 0: case i == 0:
// First of multiple binaries: define the anchor. // First of multiple binaries: define the anchor, content indented
// one extra level so it is nested under the merge key.
w(" <<: &build_defaults\n") w(" <<: &build_defaults\n")
w(" env:\n - CGO_ENABLED=0\n") writeBuildDefaults(" ")
w(" ldflags:\n")
w(" - -s -w" +
" -X main.version={{.Env.VERSION}}" +
" -X main.commit={{.Commit}}" +
" -X main.date={{.Date}}" +
" -X main.builtBy=goreleaser\n")
w(" goos:\n")
for _, g := range defaultGoos {
wf(" - %s\n", g)
}
default: default:
// Subsequent binaries: reference the anchor. // Subsequent binaries: reference the anchor.
w(" <<: *build_defaults\n") w(" <<: *build_defaults\n")
} }
// iOS requires CGO and Xcode toolchain — commented out by default. // iOS build — only when --ios is set.
wf(" # iOS requires CGO_ENABLED=1 and the Xcode toolchain.\n") if opts.ios {
wf(" #- id: %s-ios\n", bin.name) w(" # iOS build — requires CGO_ENABLED=1 and the Xcode toolchain.\n")
wf(" # binary: %s\n", bin.name) wf(" - id: %s-ios\n", bin.name)
if bin.mainPath != "." { wf(" binary: %s\n", bin.name)
wf(" # main: %s\n", bin.mainPath) if bin.mainPath != "." {
wf(" main: %s\n", bin.mainPath)
}
w(" env:\n - CGO_ENABLED=1\n")
w(" goos:\n - ios\n")
w(" goarch:\n - arm64\n")
} }
if multibin && i == 0 {
w(" # <<: &build_defaults_ios\n")
w(" # env:\n # - CGO_ENABLED=1\n")
} else if multibin {
w(" # <<: *build_defaults_ios\n")
} else {
w(" # env:\n # - CGO_ENABLED=1\n")
}
w(" # goos:\n # - ios\n")
// Android: CGO_ENABLED=0 supports arm64 only; full CGO requires the NDK. // Android NDK build — only when --android-ndk is set.
wf(" # Android CGO_ENABLED=0 builds arm64 only; CGO builds require the NDK.\n") if opts.androidNDK {
wf(" #- id: %s-android\n", bin.name) w(" # Android NDK build — requires CGO_ENABLED=1 and the Android NDK.\n")
wf(" # binary: %s\n", bin.name) wf(" - id: %s-android\n", bin.name)
if bin.mainPath != "." { wf(" binary: %s\n", bin.name)
wf(" # main: %s\n", bin.mainPath) if bin.mainPath != "." {
wf(" main: %s\n", bin.mainPath)
}
w(" env:\n - CGO_ENABLED=1\n")
w(" goos:\n - android\n")
w(" goarch:\n - arm64\n")
} }
w(" # env:\n # - CGO_ENABLED=0\n")
w(" # goos:\n # - android\n")
w(" # goarch:\n # - arm64\n")
} }
w("\narchives:\n") w("\narchives:\n")
@ -1531,8 +1589,8 @@ func buildModuleSteps(
fmt.Sprintf(" --target %q", headSHA), fmt.Sprintf(" --target %q", headSHA),
} }
steps = append(steps, releaseStep{ steps = append(steps, releaseStep{
title: "Create GitHub release", title: "Create GitHub release",
prompt: fmt.Sprintf("create GitHub release %s (draft, pre-release)", currentTag), prompt: fmt.Sprintf("create GitHub release %s (draft, pre-release)", currentTag),
display: ghCreateDisplay, display: ghCreateDisplay,
run: func() error { run: func() error {
return execIn(modRoot, "gh", "release", "create", currentTag, return execIn(modRoot, "gh", "release", "create", currentTag,
@ -1662,7 +1720,7 @@ func runSteps(steps []releaseStep, dryRun, yes bool) error {
// ── Helpers ──────────────────────────────────────────────────────────────── // ── Helpers ────────────────────────────────────────────────────────────────
// shellSingleQuote wraps s in bash single quotes, escaping any literal single // shellSingleQuote wraps s in bash single quotes, escaping any literal single
// quotes inside s as '\''. For example: it's → 'it'\''s'. // quotes inside s as '\”. For example: it's → 'it'\”s'.
func shellSingleQuote(s string) string { func shellSingleQuote(s string) string {
return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
} }