From fd3cb8ba990f39a35d77146b52edb0d05ecb9de9 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 1 Mar 2026 18:22:28 -0700 Subject: [PATCH] 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. --- tools/monorel/main.go | 208 +++++++++++++++++++++++++++--------------- 1 file changed, 133 insertions(+), 75 deletions(-) diff --git a/tools/monorel/main.go b/tools/monorel/main.go index 31d0004..ea4e072 100644 --- a/tools/monorel/main.go +++ b/tools/monorel/main.go @@ -31,6 +31,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "sort" "strconv" "strings" @@ -44,14 +45,45 @@ import ( // Adjust this slice if you ever need to search across repository boundaries. var stopMarkers = []string{".git"} -// defaultGoos is the CGO_ENABLED=0 build matrix used in generated .goreleaser.yaml files. -// Platforms requiring CGO or special toolchains (ios, android) are emitted as comments. +// defaultGoos is the conservative CGO_ENABLED=0 goos list used in generated +// .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{ + "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", "js", "linux", "netbsd", "openbsd", "plan9", "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 ────────────────────────────────────────────────────────────────── // binary describes one Go main package to build and release. @@ -75,6 +107,14 @@ type releaseStep struct { 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 ──────────────────────────────────────────────────────────── func main() { @@ -121,6 +161,7 @@ func usage() { func runRelease(args []string) { fs := flag.NewFlagSet("monorel release", flag.ExitOnError) 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(&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") @@ -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(&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(&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() { fmt.Fprintln(os.Stderr, "usage: monorel release [options] ...") fmt.Fprintln(os.Stderr, "") @@ -149,6 +193,8 @@ func runRelease(args []string) { os.Exit(2) } + opts := buildOptions{almostAll: almostAll, ios: ios, androidNDK: androidNDK} + allPaths, err := expandPaths(binPaths, recursive, all) if err != nil { fatalf("%v", err) @@ -170,7 +216,7 @@ func runRelease(args []string) { printGroupHeader(cwd, group) relPath, _ := filepath.Rel(cwd, group.root) 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) { fs := flag.NewFlagSet("monorel init", flag.ExitOnError) 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(&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(&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() { fmt.Fprintln(os.Stderr, "usage: monorel init [options] ...") fmt.Fprintln(os.Stderr, "") @@ -280,6 +330,8 @@ func runInit(args []string) { os.Exit(2) } + opts := buildOptions{almostAll: almostAll, ios: ios, androidNDK: androidNDK} + if cmd { initCmdModules(binPaths, dryRun) } @@ -301,14 +353,14 @@ func runInit(args []string) { fmt.Fprintln(os.Stderr) } printGroupHeader(cwd, group) - initModuleGroup(group, dryRun) + initModuleGroup(group, dryRun, opts) } } // initModuleGroup writes .goreleaser.yaml, commits it (if changed), and // 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. -func initModuleGroup(group *moduleGroup, dryRun bool) { +func initModuleGroup(group *moduleGroup, dryRun bool, opts buildOptions) { modRoot := group.root bins := group.bins @@ -331,7 +383,7 @@ func initModuleGroup(group *moduleGroup, dryRun bool) { projectName := prefixParts[len(prefixParts)-1] // 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") existing, readErr := os.ReadFile(yamlPath) 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 // not matched here and we keep descending — consistent with // findModuleRoot's behaviour. - skip := false - for _, stop := range stopMarkers { - if name == stop { - skip = true - break - } - } + skip := slices.Contains(stopMarkers, name) if skip { continue } @@ -652,8 +698,8 @@ func readModulePath(modRoot string) string { } for _, line := range strings.Split(string(data), "\n") { line = strings.TrimSpace(line) - if strings.HasPrefix(line, "module ") { - return strings.TrimSpace(strings.TrimPrefix(line, "module ")) + if str, ok := strings.CutPrefix(line, "module "); ok { + return strings.TrimSpace(str) } } return "" @@ -735,13 +781,7 @@ func initCmdModules(roots []string, dryRun bool) { continue } name := e.Name() - isStop := false - for _, stop := range stopMarkers { - if name == stop { - isStop = true - break - } - } + isStop := slices.Contains(stopMarkers, name) if isStop || (len(name) > 0 && (name[0] == '.' || name[0] == '_')) { 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 // 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. -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 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 // var — local edits that add extra binaries etc. are preserved. // 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") existing, readErr := os.ReadFile(yamlPath) 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. // - release.disable: true — we use `gh release` instead (goreleaser Pro // 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 w := func(s string) { b.WriteString(s) } 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(" - 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 // once with a YAML anchor on the first build and merge them into the rest. // Single-binary modules use plain fields (no anchor overhead). 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") for i, bin := range bins { wf(" - id: %s\n", bin.name) @@ -1266,63 +1345,42 @@ func goreleaserYAML(projectName string, bins []binary) string { switch { case !multibin: // Single binary: plain fields. - w(" env:\n - CGO_ENABLED=0\n") - 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) - } + writeBuildDefaults(" ") 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(" env:\n - CGO_ENABLED=0\n") - 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) - } + writeBuildDefaults(" ") default: // Subsequent binaries: reference the anchor. w(" <<: *build_defaults\n") } - // iOS requires CGO and Xcode toolchain — commented out by default. - wf(" # iOS requires CGO_ENABLED=1 and the Xcode toolchain.\n") - wf(" #- id: %s-ios\n", bin.name) - wf(" # binary: %s\n", bin.name) - if bin.mainPath != "." { - wf(" # main: %s\n", bin.mainPath) + // iOS build — only when --ios is set. + if opts.ios { + w(" # iOS build — requires CGO_ENABLED=1 and the Xcode toolchain.\n") + wf(" - id: %s-ios\n", bin.name) + wf(" binary: %s\n", bin.name) + 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. - wf(" # Android CGO_ENABLED=0 builds arm64 only; CGO builds require the NDK.\n") - wf(" #- id: %s-android\n", bin.name) - wf(" # binary: %s\n", bin.name) - if bin.mainPath != "." { - wf(" # main: %s\n", bin.mainPath) + // Android NDK build — only when --android-ndk is set. + if opts.androidNDK { + w(" # Android NDK build — requires CGO_ENABLED=1 and the Android NDK.\n") + wf(" - id: %s-android\n", bin.name) + wf(" binary: %s\n", bin.name) + 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") @@ -1531,8 +1589,8 @@ func buildModuleSteps( fmt.Sprintf(" --target %q", headSHA), } steps = append(steps, releaseStep{ - title: "Create GitHub release", - prompt: fmt.Sprintf("create GitHub release %s (draft, pre-release)", currentTag), + title: "Create GitHub release", + prompt: fmt.Sprintf("create GitHub release %s (draft, pre-release)", currentTag), display: ghCreateDisplay, run: func() error { return execIn(modRoot, "gh", "release", "create", currentTag, @@ -1662,7 +1720,7 @@ func runSteps(steps []releaseStep, dryRun, yes bool) error { // ── Helpers ──────────────────────────────────────────────────────────────── // 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 { return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" }