@@ -2,6 +2,7 @@ package main
22
33import (
44 "fmt"
5+ "runtime"
56 "strings"
67 "time"
78
@@ -10,7 +11,37 @@ import (
1011 "dagger/tapes/internal/dagger"
1112)
1213
13- // Build and return directory of go binaries
14+ const (
15+ zigVersion string = "0.15.2"
16+
17+ // osxcross image provides the macOS SDK and cross-compilation toolchain
18+ // for building CGO-enabled Go binaries targeting darwin from Linux containers.
19+ osxcrossImage string = "crazymax/osxcross:latest-ubuntu"
20+ )
21+
22+ type buildTarget struct {
23+ goos string
24+ goarch string
25+ cc string
26+ cxx string
27+ cgoFlags string
28+ cgoLdFlags string
29+ }
30+
31+ func zigArch () string {
32+ switch runtime .GOARCH {
33+ case "arm64" :
34+ return "aarch64"
35+ case "amd64" :
36+ return "x86_64"
37+ default :
38+ return runtime .GOARCH
39+ }
40+ }
41+
42+ // Build and return directory of go binaries for all platforms.
43+ // Linux targets are cross-compiled using Zig as the C toolchain.
44+ // Darwin targets are cross-compiled using osxcross (macOS SDK + clang).
1445func (t * Tapes ) Build (
1546 ctx context.Context ,
1647
@@ -19,41 +50,117 @@ func (t *Tapes) Build(
1950 // +default="-s -w"
2051 ldflags string ,
2152) * dagger.Directory {
22- // define build matrix
23- gooses := []string {"linux" , "darwin" }
24- goarches := []string {"amd64" , "arm64" }
25-
26- // create empty directory to put build artifacts
2753 outputs := dag .Directory ()
54+ outputs = t .buildLinux (outputs , ldflags )
55+ outputs = t .buildDarwin (outputs , ldflags )
56+ return outputs
57+ }
58+
59+ // buildLinux compiles Go binaries for linux/amd64 and linux/arm64
60+ // using Zig as the cross-compilation C toolchain.
61+ func (t * Tapes ) buildLinux (outputs * dagger.Directory , ldflags string ) * dagger.Directory {
62+ cgoFlags := "-I/opt/sqlite -fno-sanitize=all"
63+ cgoLdFlags := "-fno-sanitize=all"
64+
65+ targets := []buildTarget {
66+ {"linux" , "amd64" , "zig cc -target x86_64-linux-gnu" , "zig c++ -target x86_64-linux-gnu" , cgoFlags , cgoLdFlags },
67+ {"linux" , "arm64" , "zig cc -target aarch64-linux-gnu" , "zig c++ -target aarch64-linux-gnu" , cgoFlags , cgoLdFlags },
68+ }
69+
70+ // Build zig download URL based on host architecture
71+ zigArch := zigArch ()
72+ zigDownloadURL := fmt .Sprintf ("https://ziglang.org/download/%s/zig-%s-linux-%s.tar.xz" , zigVersion , zigArch , zigVersion )
73+ zigDir := fmt .Sprintf ("zig-%s-linux-%s" , zigArch , zigVersion )
74+
75+ golang := dag .Container ().
76+ From ("golang:1.25-bookworm" ).
77+ WithExec ([]string {"apt-get" , "update" }).
78+ WithExec ([]string {"apt-get" , "install" , "-y" , "libsqlite3-dev" , "xz-utils" }).
79+ WithExec ([]string {"mkdir" , "-p" , "/opt/sqlite" }).
80+ WithExec ([]string {"cp" , "/usr/include/sqlite3.h" , "/opt/sqlite/" }).
81+ WithExec ([]string {"cp" , "/usr/include/sqlite3ext.h" , "/opt/sqlite/" }).
82+ WithExec ([]string {"sh" , "-c" , fmt .Sprintf ("curl -L %s | tar -xJ -C /usr/local" , zigDownloadURL )}).
83+ WithEnvVariable ("PATH" , fmt .Sprintf ("/usr/local/%s:$PATH" , zigDir ), dagger.ContainerWithEnvVariableOpts {Expand : true }).
84+ WithMountedCache ("/go/pkg/mod" , dag .CacheVolume ("go-mod" )).
85+ WithMountedCache ("/root/.cache/go-build" , dag .CacheVolume ("go-build" )).
86+ WithDirectory ("/src" , t .Source ).
87+ WithWorkdir ("/src" )
88+
89+ for _ , target := range targets {
90+ path := fmt .Sprintf ("%s/%s/" , target .goos , target .goarch )
91+
92+ build := golang .
93+ WithEnvVariable ("CGO_ENABLED" , "1" ).
94+ WithEnvVariable ("GOEXPERIMENT" , "jsonv2" ).
95+ WithEnvVariable ("GOOS" , target .goos ).
96+ WithEnvVariable ("GOARCH" , target .goarch ).
97+ WithEnvVariable ("CC" , target .cc ).
98+ WithEnvVariable ("CXX" , target .cxx ).
99+ WithEnvVariable ("CGO_CFLAGS" , target .cgoFlags ).
100+ WithEnvVariable ("CGO_LDFLAGS" , target .cgoLdFlags ).
101+ WithExec ([]string {"go" , "build" , "-ldflags" , ldflags , "-o" , path , "./cli/tapes" }).
102+ WithExec ([]string {"go" , "build" , "-ldflags" , ldflags , "-o" , path , "./cli/tapesprox" }).
103+ WithExec ([]string {"go" , "build" , "-ldflags" , ldflags , "-o" , path , "./cli/tapesapi" })
104+
105+ outputs = outputs .WithDirectory (path , build .Directory (path ))
106+ }
107+
108+ return outputs
109+ }
110+
111+ // buildDarwin compiles Go binaries for darwin/amd64 and darwin/arm64
112+ // using the osxcross toolchain which provides the macOS SDK and clang
113+ // cross-compilers inside a Linux container.
114+ func (t * Tapes ) buildDarwin (outputs * dagger.Directory , ldflags string ) * dagger.Directory {
115+ cgoFlags := "-I/opt/sqlite"
116+ cgoLdFlags := ""
117+
118+ targets := []buildTarget {
119+ {"darwin" , "amd64" , "o64-clang" , "o64-clang++" , cgoFlags , cgoLdFlags },
120+ {"darwin" , "arm64" , "oa64-clang" , "oa64-clang++" , cgoFlags , cgoLdFlags },
121+ }
122+
123+ // Pull the osxcross toolchain (macOS SDK + clang cross-compilers)
124+ osxcross := dag .Container ().
125+ From (osxcrossImage ).
126+ Directory ("/osxcross" )
28127
128+ // Use Debian Trixie as the base for darwin builds because the osxcross
129+ // toolchain binaries require GLIBC 2.38+ (Bookworm only has 2.36).
29130 golang := dag .Container ().
30- From ("golang:1.25-alpine" ).
31- WithEnvVariable ("CGO_ENABLED" , "0" ).
32- WithEnvVariable ("GOEXPERIMENT" , "jsonv2" ).
131+ From ("golang:1.25-trixie" ).
132+ WithExec ([]string {"apt-get" , "update" }).
133+ WithExec ([]string {"apt-get" , "install" , "-y" , "clang" , "lld" , "libsqlite3-dev" }).
134+ WithExec ([]string {"mkdir" , "-p" , "/opt/sqlite" }).
135+ WithExec ([]string {"cp" , "/usr/include/sqlite3.h" , "/opt/sqlite/" }).
136+ WithExec ([]string {"cp" , "/usr/include/sqlite3ext.h" , "/opt/sqlite/" }).
137+ WithDirectory ("/osxcross" , osxcross ).
138+ WithEnvVariable ("PATH" , "/osxcross/bin:$PATH" , dagger.ContainerWithEnvVariableOpts {Expand : true }).
139+ WithEnvVariable ("LD_LIBRARY_PATH" , "/osxcross/lib:$LD_LIBRARY_PATH" , dagger.ContainerWithEnvVariableOpts {Expand : true }).
33140 WithMountedCache ("/go/pkg/mod" , dag .CacheVolume ("go-mod" )).
34141 WithMountedCache ("/root/.cache/go-build" , dag .CacheVolume ("go-build" )).
35142 WithDirectory ("/src" , t .Source ).
36143 WithWorkdir ("/src" )
37144
38- for _ , goos := range gooses {
39- for _ , goarch := range goarches {
40- // create directory for each OS and architecture
41- path := fmt .Sprintf ("%s/%s/" , goos , goarch )
42-
43- // build artifact
44- build := golang .
45- WithEnvVariable ("GOOS" , goos ).
46- WithEnvVariable ("GOARCH" , goarch ).
47- WithExec ([]string {"go" , "build" , "-ldflags" , ldflags , "-o" , path , "./cli/tapes" }).
48- WithExec ([]string {"go" , "build" , "-ldflags" , ldflags , "-o" , path , "./cli/tapesprox" }).
49- WithExec ([]string {"go" , "build" , "-ldflags" , ldflags , "-o" , path , "./cli/tapesapi" })
50-
51- // add build to outputs
52- outputs = outputs .WithDirectory (path , build .Directory (path ))
53- }
145+ for _ , target := range targets {
146+ path := fmt .Sprintf ("%s/%s/" , target .goos , target .goarch )
147+
148+ build := golang .
149+ WithEnvVariable ("CGO_ENABLED" , "1" ).
150+ WithEnvVariable ("GOEXPERIMENT" , "jsonv2" ).
151+ WithEnvVariable ("GOOS" , target .goos ).
152+ WithEnvVariable ("GOARCH" , target .goarch ).
153+ WithEnvVariable ("CC" , target .cc ).
154+ WithEnvVariable ("CXX" , target .cxx ).
155+ WithEnvVariable ("CGO_CFLAGS" , target .cgoFlags ).
156+ WithEnvVariable ("CGO_LDFLAGS" , target .cgoLdFlags ).
157+ WithExec ([]string {"go" , "build" , "-ldflags" , ldflags , "-o" , path , "./cli/tapes" }).
158+ WithExec ([]string {"go" , "build" , "-ldflags" , ldflags , "-o" , path , "./cli/tapesprox" }).
159+ WithExec ([]string {"go" , "build" , "-ldflags" , ldflags , "-o" , path , "./cli/tapesapi" })
160+
161+ outputs = outputs .WithDirectory (path , build .Directory (path ))
54162 }
55163
56- // return build directory
57164 return outputs
58165}
59166
0 commit comments