Skip to content

Commit 8cf99a5

Browse files
committed
Split command line handling out of the multibuild flow a bit
Makes things a little more digestible.
1 parent 8bea84c commit 8cf99a5

2 files changed

Lines changed: 154 additions & 113 deletions

File tree

cmd/multibuild/main.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright 2025 Robin Burchell. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// A simplistic tool to build Go binaries for multiple platforms.
6+
package main
7+
8+
//go:multibuild:output=bin/${TARGET}-${GOOS}-${GOARCH}
9+
10+
import (
11+
"fmt"
12+
"os"
13+
"path/filepath"
14+
"strings"
15+
)
16+
17+
func displayUsageAndExit(self string) {
18+
fmt.Fprintf(os.Stderr, "usage: %s [-o output] [build flags] [packages]\n", self)
19+
fmt.Fprintln(os.Stderr, "multibuild is a thin wrapper around 'go build'.")
20+
fmt.Fprintln(os.Stderr, "For documentation on multibuild's configuration, see https://github.com/rburchell/multibuild")
21+
fmt.Fprintln(os.Stderr, "Otherwise, run 'go help build' for command line flags.")
22+
fmt.Fprintln(os.Stderr, "")
23+
fmt.Fprintln(os.Stderr, "multibuild-specific options:")
24+
fmt.Fprintln(os.Stderr, " -v: enable verbose logs during building. this will also imply `go build -v`")
25+
fmt.Fprintln(os.Stderr, " --multibuild-configuration: display the multibuild configuration parsed from the package")
26+
fmt.Fprintln(os.Stderr, " --multibuild-targets: list targets that will be built")
27+
os.Exit(0)
28+
}
29+
30+
func displayConfigAndExit(opts options) {
31+
fmt.Fprintf(os.Stderr, "//go:multibuild:include=%s\n", strings.Join(mapSlice(opts.Include, func(f filter) string { return string(f) }), ","))
32+
fmt.Fprintf(os.Stderr, "//go:multibuild:exclude=%s\n", strings.Join(mapSlice(opts.Exclude, func(f filter) string { return string(f) }), ","))
33+
fmt.Fprintf(os.Stderr, "//go:multibuild:output=%s\n", opts.Output)
34+
os.Exit(0)
35+
}
36+
37+
func displayTargetsAndExit(targets []target) {
38+
for _, target := range targets {
39+
fmt.Fprintln(os.Stderr, target)
40+
}
41+
os.Exit(0)
42+
}
43+
44+
type cliArgs struct {
45+
// The current binary name.
46+
self string
47+
48+
// The args for go build, with [0] (this binary) stripped off.
49+
goBuildArgs []string
50+
51+
// -o arg, or -o=
52+
// In case it's not specified explicitly, it is autodetected.
53+
output string
54+
55+
// The package path being built
56+
// In case it's not specified explicitly, it is set to ".".
57+
packagePath string
58+
59+
// The sources to be built
60+
// This will usually, but not always, be empty.
61+
// (e.g. multibuild foo/main.go)
62+
sources []string
63+
64+
displayConfig bool
65+
displayTargets bool
66+
verbose bool
67+
}
68+
69+
func main() {
70+
args := cliArgs{}
71+
args.self = filepath.Base(os.Args[0])
72+
args.goBuildArgs = os.Args[1:]
73+
expectOutput := false // seen -o, waiting for the rest
74+
75+
for _, arg := range args.goBuildArgs {
76+
switch {
77+
case expectOutput:
78+
args.output = arg
79+
expectOutput = false
80+
case arg == "-o":
81+
expectOutput = true
82+
case strings.HasPrefix(arg, "-o="):
83+
args.output = strings.TrimPrefix(arg, "-o=")
84+
85+
case arg == "-h" || arg == "--help":
86+
displayUsageAndExit(args.self)
87+
case arg == "-v":
88+
args.verbose = true
89+
case arg == "--multibuild-configuration":
90+
args.displayConfig = true
91+
case arg == "--multibuild-targets":
92+
args.displayTargets = true
93+
case strings.HasPrefix(arg, "--multibuild"):
94+
fatal("multibuild: unrecognized argument %q", arg)
95+
case !strings.HasPrefix(arg, "-"):
96+
if args.packagePath != "" {
97+
// For now, I'm cowardly refusing to handle this.
98+
// I think we need to refactor some to handle two cases:
99+
// - specifying a list of .go sources in a single ultimate package
100+
// - specifying a list of packages
101+
//
102+
// The former is handled quite easily I think, the latter will
103+
// require some additional thought and handling, as it's essentially
104+
// another level of looping on top of what we have now.
105+
//
106+
// We will need to discover sources for each package, scan independently,
107+
// and build independently.
108+
fatal("multibuild: cannot build multiple packages")
109+
}
110+
args.packagePath = arg
111+
}
112+
}
113+
114+
if args.packagePath == "" {
115+
args.packagePath = "."
116+
}
117+
118+
if args.output == "" {
119+
if args.packagePath == "." {
120+
// implicit case: multibuild on the current dir -> multibuild .
121+
args.packagePath = "."
122+
wd, err := os.Getwd()
123+
if err != nil {
124+
fatal("multibuild: failed to get cwd: %s", err)
125+
}
126+
args.output = filepath.Base(wd)
127+
} else {
128+
t := args.packagePath
129+
if strings.HasSuffix(t, ".go") {
130+
// multibuild cmd/foo.go
131+
args.packagePath = filepath.Dir(t)
132+
args.output = strings.TrimSuffix(filepath.Base(t), ".go")
133+
args.sources = append(args.sources, t)
134+
} else {
135+
// multibuild cmd/foo
136+
args.packagePath = t
137+
args.output = filepath.Base(t)
138+
}
139+
}
140+
}
141+
142+
doMultibuild(args)
143+
}

cmd/multibuild/multibuild.go

Lines changed: 11 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
// A simplistic tool to build Go binaries for multiple platforms.
65
package main
76

8-
//go:multibuild:output=bin/${TARGET}-${GOOS}-${GOARCH}
9-
107
import (
118
"bufio"
129
"bytes"
@@ -67,111 +64,12 @@ func targetList() ([]target, error) {
6764
}), nil
6865
}
6966

70-
func displayUsageAndExit(self string) {
71-
fmt.Fprintf(os.Stderr, "usage: %s [-o output] [build flags] [packages]\n", self)
72-
fmt.Fprintln(os.Stderr, "multibuild is a thin wrapper around 'go build'.")
73-
fmt.Fprintln(os.Stderr, "For documentation on multibuild's configuration, see https://github.com/rburchell/multibuild")
74-
fmt.Fprintln(os.Stderr, "Otherwise, run 'go help build' for command line flags.")
75-
fmt.Fprintln(os.Stderr, "")
76-
fmt.Fprintln(os.Stderr, "multibuild-specific options:")
77-
fmt.Fprintln(os.Stderr, " -v: enable verbose logs during building. this will also imply `go build -v`")
78-
fmt.Fprintln(os.Stderr, " --multibuild-configuration: display the multibuild configuration parsed from the package")
79-
fmt.Fprintln(os.Stderr, " --multibuild-targets: list targets that will be built")
80-
os.Exit(0)
81-
}
82-
83-
func displayConfigAndExit(opts options) {
84-
fmt.Fprintf(os.Stderr, "//go:multibuild:include=%s\n", strings.Join(mapSlice(opts.Include, func(f filter) string { return string(f) }), ","))
85-
fmt.Fprintf(os.Stderr, "//go:multibuild:exclude=%s\n", strings.Join(mapSlice(opts.Exclude, func(f filter) string { return string(f) }), ","))
86-
fmt.Fprintf(os.Stderr, "//go:multibuild:output=%s\n", opts.Output)
87-
os.Exit(0)
88-
}
89-
90-
func displayTargetsAndExit(targets []target) {
91-
for _, target := range targets {
92-
fmt.Fprintln(os.Stderr, target)
93-
}
94-
os.Exit(0)
95-
}
96-
97-
func main() {
98-
self := filepath.Base(os.Args[0]) // current binary name
99-
args := os.Args[1:] // remaining args
100-
expectOutput := false // seen -o, waiting for the rest
101-
output := "" // -o or -o=
102-
var nonflags []string // non-flag arguments
103-
displayConfig := false
104-
displayTargets := false
105-
verbose := false
106-
107-
for _, arg := range args {
108-
switch {
109-
case expectOutput:
110-
output = arg
111-
expectOutput = false
112-
case arg == "-o":
113-
expectOutput = true
114-
case strings.HasPrefix(arg, "-o="):
115-
output = strings.TrimPrefix(arg, "-o=")
116-
117-
case arg == "-h" || arg == "--help":
118-
displayUsageAndExit(self)
119-
case arg == "-v":
120-
verbose = true
121-
case arg == "--multibuild-configuration":
122-
displayConfig = true
123-
case arg == "--multibuild-targets":
124-
displayTargets = true
125-
case strings.HasPrefix(arg, "--multibuild"):
126-
fatal("multibuild: unrecognized argument %q", arg)
127-
case !strings.HasPrefix(arg, "-"):
128-
nonflags = append(nonflags, arg)
129-
}
130-
}
131-
132-
var sources []string
133-
packagePath := "" // the path being built
134-
if output == "" {
135-
switch len(nonflags) {
136-
case 0:
137-
// implicit case: multibuild on the current dir -> multibuild .
138-
packagePath = "."
139-
wd, err := os.Getwd()
140-
if err != nil {
141-
fatal("multibuild: failed to get cwd: %s", err)
142-
}
143-
output = filepath.Base(wd)
144-
case 1:
145-
t := nonflags[0]
146-
if strings.HasSuffix(t, ".go") {
147-
// multibuild cmd/foo.go
148-
packagePath = filepath.Dir(t)
149-
output = strings.TrimSuffix(filepath.Base(t), ".go")
150-
sources = append(sources, t)
151-
} else {
152-
// multibuild cmd/foo
153-
packagePath = t
154-
output = filepath.Base(t)
155-
}
156-
default:
157-
// For now, I'm cowardly refusing to handle this.
158-
// I think we need to refactor some to handle two cases:
159-
// - specifying a list of .go sources in a single ultimate package
160-
// - specifying a list of packages
161-
//
162-
// The former is handled quite easily I think, the latter will
163-
// require some additional thought and handling, as it's essentially
164-
// another level of looping on top of what we have now.
165-
//
166-
// We will need to discover sources for each package, scan independently,
167-
// and build independently.
168-
fatal("multibuild: cannot build multiple packages")
169-
}
170-
}
67+
func doMultibuild(args cliArgs) {
68+
sources := args.sources
17169

17270
if len(sources) == 0 {
17371
var err error
174-
sources, err = sourcesList(packagePath)
72+
sources, err = sourcesList(args.packagePath)
17573
if err != nil {
17674
fatal("multibuild: failed to discover sources: %s", err)
17775
}
@@ -191,26 +89,26 @@ func main() {
19189
fatal("multibuild: failed to build target list: %s", err)
19290
}
19391

194-
if displayConfig {
92+
if args.displayConfig {
19593
displayConfigAndExit(opts)
19694
}
197-
if displayTargets {
95+
if args.displayTargets {
19896
displayTargetsAndExit(targets)
19997
}
20098

20199
// If there's an explicit GOOS/GOARCH, pass through.
202100
// We want to stay out of the way here.
203101
// TODO: But this might be a confusing mistake to fall over if you set it in .bashrc etc..
204102
if os.Getenv("GOOS") != "" || os.Getenv("GOARCH") != "" {
205-
runBuild(args, "", "")
103+
runBuild(args.goBuildArgs, "", "")
206104
return
207105
}
208106

209107
wg := sync.WaitGroup{}
210108
sem := make(chan struct{}, 4) // limit max parallel builds to save sanity...
211109

212110
formattedOutput := string(opts.Output)
213-
formattedOutput = strings.ReplaceAll(formattedOutput, "${TARGET}", output)
111+
formattedOutput = strings.ReplaceAll(formattedOutput, "${TARGET}", args.output)
214112

215113
for _, t := range targets {
216114
parts := strings.Split(string(t), "/")
@@ -225,19 +123,19 @@ func main() {
225123
}
226124

227125
buildArgs := []string{"-o", out}
228-
buildArgs = append(buildArgs, args...)
126+
buildArgs = append(buildArgs, args.goBuildArgs...)
229127

230128
wg.Add(1) // acquire for global
231129
go func(goos, goarch string, buildArgs []string) {
232-
if verbose {
130+
if args.verbose {
233131
fmt.Fprintf(os.Stderr, "%s/%s: waiting\n", goos, goarch)
234132
}
235133
sem <- struct{}{} // acquire for job
236-
if verbose {
134+
if args.verbose {
237135
fmt.Fprintf(os.Stderr, "%s/%s: building\n", goos, goarch)
238136
}
239137
runBuild(buildArgs, goos, goarch)
240-
if verbose {
138+
if args.verbose {
241139
fmt.Fprintf(os.Stderr, "%s/%s: done\n", goos, goarch)
242140
}
243141
<-sem // release for job

0 commit comments

Comments
 (0)