Skip to content

Commit b0f75d8

Browse files
committed
feat: Add output formats
1 parent c8d407a commit b0f75d8

5 files changed

Lines changed: 319 additions & 5 deletions

File tree

cmd/multibuild/integration_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os/exec"
1111
"path/filepath"
1212
"runtime"
13+
"sort"
1314
"strings"
1415
"testing"
1516
)
@@ -110,6 +111,7 @@ func main() {
110111
expectedConfig: `//go:multibuild:include=linux/amd64,linux/arm64
111112
//go:multibuild:exclude=android/*,ios/*
112113
//go:multibuild:output=${TARGET}-${GOOS}-${GOARCH}
114+
//go:multibuild:format=raw
113115
`,
114116
expectedTargets: "linux/amd64\nlinux/arm64\n",
115117
},
@@ -124,6 +126,7 @@ func main() {
124126
expectedConfig: `//go:multibuild:include=*/arm64
125127
//go:multibuild:exclude=android/arm64,darwin/arm64,freebsd/arm64,ios/arm64,netbsd/arm64,openbsd/arm64,windows/arm64,android/*,ios/*
126128
//go:multibuild:output=${TARGET}-${GOOS}-${GOARCH}
129+
//go:multibuild:format=raw
127130
`,
128131
expectedTargets: "linux/arm64\n",
129132
},
@@ -139,6 +142,75 @@ func main() {
139142
expectedConfig: `//go:multibuild:include=linux/amd64,linux/arm64
140143
//go:multibuild:exclude=android/*,ios/*
141144
//go:multibuild:output=bin/${TARGET}-hello-${GOOS}-world-${GOARCH}
145+
//go:multibuild:format=raw
146+
`,
147+
expectedTargets: "linux/amd64\nlinux/arm64\n",
148+
},
149+
{
150+
name: "format=raw",
151+
config: `//go:multibuild:include=linux/amd64,linux/arm64
152+
//go:multibuild:format=raw
153+
`,
154+
expectedBinaries: []string{
155+
"${TARGET}-linux-amd64",
156+
"${TARGET}-linux-arm64",
157+
},
158+
expectedConfig: `//go:multibuild:include=linux/amd64,linux/arm64
159+
//go:multibuild:exclude=android/*,ios/*
160+
//go:multibuild:output=${TARGET}-${GOOS}-${GOARCH}
161+
//go:multibuild:format=raw
162+
`,
163+
expectedTargets: "linux/amd64\nlinux/arm64\n",
164+
},
165+
{
166+
name: "format=zip",
167+
config: `//go:multibuild:include=linux/amd64,linux/arm64
168+
//go:multibuild:format=zip
169+
`,
170+
expectedBinaries: []string{
171+
"${TARGET}-linux-amd64.zip",
172+
"${TARGET}-linux-arm64.zip",
173+
},
174+
expectedConfig: `//go:multibuild:include=linux/amd64,linux/arm64
175+
//go:multibuild:exclude=android/*,ios/*
176+
//go:multibuild:output=${TARGET}-${GOOS}-${GOARCH}
177+
//go:multibuild:format=zip
178+
`,
179+
expectedTargets: "linux/amd64\nlinux/arm64\n",
180+
},
181+
{
182+
name: "format=tar.gz",
183+
config: `//go:multibuild:include=linux/amd64,linux/arm64
184+
//go:multibuild:format=tar.gz
185+
`,
186+
expectedBinaries: []string{
187+
"${TARGET}-linux-amd64.tar.gz",
188+
"${TARGET}-linux-arm64.tar.gz",
189+
},
190+
expectedConfig: `//go:multibuild:include=linux/amd64,linux/arm64
191+
//go:multibuild:exclude=android/*,ios/*
192+
//go:multibuild:output=${TARGET}-${GOOS}-${GOARCH}
193+
//go:multibuild:format=tar.gz
194+
`,
195+
expectedTargets: "linux/amd64\nlinux/arm64\n",
196+
},
197+
{
198+
name: "format=raw,zip,tar.gz",
199+
config: `//go:multibuild:include=linux/amd64,linux/arm64
200+
//go:multibuild:format=raw,zip,tar.gz
201+
`,
202+
expectedBinaries: []string{
203+
"${TARGET}-linux-amd64",
204+
"${TARGET}-linux-arm64",
205+
"${TARGET}-linux-amd64.zip",
206+
"${TARGET}-linux-arm64.zip",
207+
"${TARGET}-linux-amd64.tar.gz",
208+
"${TARGET}-linux-arm64.tar.gz",
209+
},
210+
expectedConfig: `//go:multibuild:include=linux/amd64,linux/arm64
211+
//go:multibuild:exclude=android/*,ios/*
212+
//go:multibuild:output=${TARGET}-${GOOS}-${GOARCH}
213+
//go:multibuild:format=raw,zip,tar.gz
142214
`,
143215
expectedTargets: "linux/amd64\nlinux/arm64\n",
144216
},
@@ -187,7 +259,14 @@ func main() {
187259
if err != nil {
188260
t.Fatalf("failed to multibuild: %v\nOutput:\n%s", err, out)
189261
}
262+
if len(out) != 0 {
263+
t.Fatalf("unexpected output: %s", out)
264+
}
190265

266+
// FIXME: This test has a small oversight. It was written to assert that the expected output is created.
267+
// But ideally it should also be asserting that no *unexpected* output is created.
268+
//
269+
// An example here is that if we request format=zip, we should assert that the 'raw' binaries are removed.
191270
for _, want := range test.expectedBinaries {
192271
want := strings.ReplaceAll(want, "${TARGET}", filepath.Base(testTmp))
193272
if _, err := os.Stat(filepath.Join(testTmp, want)); err != nil {
@@ -364,3 +443,42 @@ func main() { fmt.Println("Hello from main%d in pkg%d") }
364443
})
365444
}
366445
}
446+
447+
func tree(path string) ([]string, error) {
448+
var lines []string
449+
450+
var walk func(dir string, prefix string)
451+
walk = func(dir string, prefix string) {
452+
entries, err := os.ReadDir(dir)
453+
if err != nil {
454+
return
455+
}
456+
457+
sort.Slice(entries, func(i, j int) bool {
458+
return strings.ToLower(entries[i].Name()) < strings.ToLower(entries[j].Name())
459+
})
460+
461+
for i, e := range entries {
462+
last := i == len(entries)-1
463+
464+
connector := "├── "
465+
childPrefix := prefix + "│ "
466+
467+
if last {
468+
connector = "└── "
469+
childPrefix = prefix + " "
470+
}
471+
472+
lines = append(lines, prefix+connector+e.Name())
473+
474+
if e.IsDir() {
475+
walk(filepath.Join(dir, e.Name()), childPrefix)
476+
}
477+
}
478+
}
479+
480+
lines = append(lines, filepath.Base(path))
481+
walk(path, "")
482+
483+
return lines, nil
484+
}

cmd/multibuild/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package main
77

88
//go:multibuild:output=bin/${TARGET}-${GOOS}-${GOARCH}
9+
//go:multibuild:format=raw,tar.gz,zip
910

1011
import (
1112
"fmt"
@@ -31,6 +32,7 @@ func displayConfigAndExit(opts options) {
3132
fmt.Fprintf(os.Stderr, "//go:multibuild:include=%s\n", strings.Join(mapSlice(opts.Include, func(f filter) string { return string(f) }), ","))
3233
fmt.Fprintf(os.Stderr, "//go:multibuild:exclude=%s\n", strings.Join(mapSlice(opts.Exclude, func(f filter) string { return string(f) }), ","))
3334
fmt.Fprintf(os.Stderr, "//go:multibuild:output=%s\n", opts.Output)
35+
fmt.Fprintf(os.Stderr, "//go:multibuild:format=%s\n", strings.Join(mapSlice(opts.Format, func(f format) string { return string(f) }), ","))
3436
os.Exit(0)
3537
}
3638

cmd/multibuild/multibuild.go

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
package main
66

77
import (
8+
"archive/tar"
9+
"archive/zip"
810
"bufio"
911
"bytes"
12+
"compress/gzip"
1013
"encoding/json"
1114
"fmt"
1215
"io"
1316
"os"
1417
"os/exec"
1518
"path/filepath"
19+
"slices"
1620
"strings"
1721
"sync"
1822
)
@@ -126,21 +130,69 @@ func doMultibuild(args cliArgs) {
126130
buildArgs = append(buildArgs, args.goBuildArgs...)
127131

128132
wg.Add(1) // acquire for global
129-
go func(goos, goarch string, buildArgs []string) {
133+
go func(out, goos, goarch string, buildArgs []string) {
130134
if args.verbose {
131135
fmt.Fprintf(os.Stderr, "%s/%s: waiting\n", goos, goarch)
132136
}
133137
sem <- struct{}{} // acquire for job
134138
if args.verbose {
135-
fmt.Fprintf(os.Stderr, "%s/%s: building\n", goos, goarch)
139+
fmt.Fprintf(os.Stderr, "%s/%s: build\n", goos, goarch)
136140
}
137141
runBuild(buildArgs, goos, goarch)
138142
if args.verbose {
139-
fmt.Fprintf(os.Stderr, "%s/%s: done\n", goos, goarch)
143+
fmt.Fprintf(os.Stderr, "%s/%s: archive\n", goos, goarch)
144+
}
145+
146+
// FIXME: windows case out has .exe, and we don't want that here.
147+
for _, format := range opts.Format {
148+
switch format {
149+
case formatRaw:
150+
// already built (obvs)..
151+
case formatZip:
152+
arPath := out + ".zip"
153+
// FIXME: handle errors
154+
f, _ := os.Create(arPath)
155+
defer f.Close()
156+
157+
zw := zip.NewWriter(f)
158+
defer zw.Close()
159+
160+
w, _ := zw.Create(out)
161+
bin, _ := os.Open(out)
162+
defer bin.Close()
163+
_, _ = io.Copy(w, bin)
164+
case formatTgz:
165+
arPath := out + ".tar.gz"
166+
// FIXME: handle errors
167+
f, _ := os.Create(arPath)
168+
defer f.Close()
169+
170+
gz := gzip.NewWriter(f)
171+
defer gz.Close()
172+
173+
tw := tar.NewWriter(gz)
174+
defer tw.Close()
175+
176+
st, _ := os.Stat(out)
177+
bin, _ := os.Open(out)
178+
defer bin.Close()
179+
180+
hdr := &tar.Header{Name: out, Mode: 0644, Size: st.Size()}
181+
tw.WriteHeader(hdr)
182+
_, _ = io.Copy(tw, bin)
183+
}
184+
}
185+
186+
if !slices.Contains(opts.Format, formatRaw) {
187+
err := os.Remove(out)
188+
if err != nil {
189+
// FIXME: handle errors
190+
panic(err)
191+
}
140192
}
141193
<-sem // release for job
142194
wg.Done() // release for global
143-
}(goos, goarch, buildArgs)
195+
}(out, goos, goarch, buildArgs)
144196
}
145197

146198
wg.Wait()

cmd/multibuild/options.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,23 @@ type target string
2828
// e.g. ${TARGET}_${GOOS}_${GOARCH}
2929
type outputTemplate string
3030

31+
// raw, tar.gz, ...
32+
type format string
33+
34+
const (
35+
formatRaw format = "raw"
36+
formatZip = "zip"
37+
formatTgz = "tar.gz"
38+
)
39+
3140
// All options for multibuild go here..
3241
type options struct {
33-
// Output format
42+
// Output filename format
3443
Output outputTemplate
3544

45+
// Output formats to produce
46+
Format []format
47+
3648
// Targets to include
3749
Include []filter
3850

@@ -171,6 +183,30 @@ func validateTemplate(s string) (outputTemplate, error) {
171183
return outputTemplate(s), nil
172184
}
173185

186+
// Validates that the 's' is a list of formats.
187+
func validateFormatString(s string) ([]format, error) {
188+
if s == "" {
189+
return nil, fmt.Errorf("empty string is not a valid format")
190+
}
191+
192+
var allowedFormats = map[format]struct{}{
193+
formatRaw: {},
194+
formatZip: {},
195+
formatTgz: {},
196+
}
197+
198+
var formats []format
199+
formatStrs := strings.SplitSeq(s, ",")
200+
for formatStr := range formatStrs {
201+
format := format(formatStr)
202+
if _, ok := allowedFormats[format]; !ok {
203+
return nil, fmt.Errorf("format %q is not valid", formatStr)
204+
}
205+
formats = append(formats, format)
206+
}
207+
return formats, nil
208+
}
209+
174210
func validateFilterString(s string) ([]filter, error) {
175211
isAlphaNum := func(b byte) bool {
176212
return (b >= 'a' && b <= 'z') ||
@@ -274,6 +310,19 @@ func scanBuildPath(reader io.Reader, path string) (options, error) {
274310
return options{}, fmt.Errorf("%s:%d: go:multibuild:output=%s is invalid: %s", path, i, rest, err)
275311
}
276312
opts.Output = parsed
313+
} else if strings.HasPrefix(line, "//go:multibuild:format=") {
314+
if dlog {
315+
log.Printf("Found format: %s:%d: %s", path, i, line)
316+
}
317+
rest := strings.TrimPrefix(line, "//go:multibuild:format=")
318+
if len(opts.Format) > 0 {
319+
return options{}, fmt.Errorf("%s:%d: go:multibuild:format was already set to %s, found: %q here", path, i, opts.Format, rest)
320+
}
321+
parsed, err := validateFormatString(rest)
322+
if err != nil {
323+
return options{}, fmt.Errorf("%s:%d: go:multibuild:format=%s is invalid: %s", path, i, rest, err)
324+
}
325+
opts.Format = parsed
277326
} else if strings.HasPrefix(line, "//go:multibuild:include=") {
278327
if dlog {
279328
log.Printf("Found include: %s:%d: %s", path, i, line)
@@ -321,6 +370,11 @@ func scanBuildDir(sources []string) (options, error) {
321370
} else if len(topts.Output) > 0 {
322371
opts.Output = topts.Output
323372
}
373+
if len(opts.Format) > 0 && len(topts.Format) > 0 {
374+
return options{}, fmt.Errorf("%s: format= already set elsewhere", path)
375+
} else if len(topts.Format) > 0 {
376+
opts.Format = topts.Format
377+
}
324378
opts.Exclude = append(opts.Exclude, topts.Exclude...)
325379
opts.Include = append(opts.Include, topts.Include...)
326380
}
@@ -329,6 +383,9 @@ func scanBuildDir(sources []string) (options, error) {
329383
if len(opts.Include) == 0 {
330384
opts.Include = []filter{"*/*"}
331385
}
386+
if len(opts.Format) == 0 {
387+
opts.Format = []format{formatRaw}
388+
}
332389

333390
// These require CGO_ENABLED=1, which I don't want to touch right now.
334391
// As I don't have a use for it, let's just disable them.

0 commit comments

Comments
 (0)