@@ -13,6 +13,7 @@ import (
1313 "os"
1414 "os/signal"
1515 "path/filepath"
16+ "regexp"
1617 "slices"
1718 "strings"
1819 "syscall"
@@ -31,7 +32,11 @@ import (
3132 "github.com/docker/docker/pkg/jsonmessage"
3233 "github.com/google/go-containerregistry/pkg/authn"
3334 "github.com/google/go-containerregistry/pkg/name"
35+ "github.com/google/go-containerregistry/pkg/v1/empty"
36+ "github.com/google/go-containerregistry/pkg/v1/mutate"
37+ "github.com/google/go-containerregistry/pkg/v1/partial"
3438 "github.com/google/go-containerregistry/pkg/v1/remote"
39+ "github.com/google/go-containerregistry/pkg/v1/types"
3540 "github.com/google/go-github/v49/github"
3641 "github.com/paketo-buildpacks/libpak/carton"
3742 "github.com/pelletier/go-toml"
@@ -52,7 +57,7 @@ func main() {
5257 var hadError bool
5358 for _ , variant := range []string {"tiny" , "base" , "full" } {
5459 fmt .Println ("::group::" + variant )
55- err := buildBuilderImage (ctx , variant )
60+ err := buildBuilderImageMultiArch (ctx , variant )
5661 if err != nil {
5762 _ , _ = fmt .Fprintf (os .Stderr , "ERROR: %v\n " , err )
5863 hadError = true
@@ -64,10 +69,10 @@ func main() {
6469 }
6570}
6671
67- func buildBuilderImage (ctx context.Context , variant string ) error {
72+ func buildBuilderImage (ctx context.Context , variant , arch string ) ( string , error ) {
6873 buildDir , err := os .MkdirTemp ("" , "" )
6974 if err != nil {
70- return fmt .Errorf ("cannot create temporary build directory: %w" , err )
75+ return "" , fmt .Errorf ("cannot create temporary build directory: %w" , err )
7176 }
7277 defer func (path string ) {
7378 _ = os .RemoveAll (path )
@@ -77,68 +82,73 @@ func buildBuilderImage(ctx context.Context, variant string) error {
7782 listOpts := & github.ListOptions {Page : 0 , PerPage : 1 }
7883 releases , ghResp , err := ghClient .Repositories .ListReleases (ctx , "paketo-buildpacks" , "builder-jammy-" + variant , listOpts )
7984 if err != nil {
80- return fmt .Errorf ("cannot get upstream builder release: %w" , err )
85+ return "" , fmt .Errorf ("cannot get upstream builder release: %w" , err )
8186 }
8287 defer func (Body io.ReadCloser ) {
8388 _ = Body .Close ()
8489 }(ghResp .Body )
8590
8691 if len (releases ) <= 0 {
87- return fmt .Errorf ("cannot get latest release" )
92+ return "" , fmt .Errorf ("cannot get latest release" )
8893 }
8994
9095 release := releases [0 ]
9196
9297 if release .Name == nil {
93- return fmt .Errorf ("the name of the release is not defined" )
98+ return "" , fmt .Errorf ("the name of the release is not defined" )
9499 }
95100 if release .TarballURL == nil {
96- return fmt .Errorf ("the tarball url of the release is not defined" )
101+ return "" , fmt .Errorf ("the tarball url of the release is not defined" )
97102 }
98-
99103 newBuilderImage := "ghcr.io/knative/builder-jammy-" + variant
100- newBuilderImageTagged := newBuilderImage + ":" + * release .Name
101- newBuilderImageLatest := newBuilderImage + ":latest"
104+ newBuilderImageTagged := newBuilderImage + ":" + * release .Name + "-" + arch
102105 dockerUser := "gh-action"
103106 dockerPassword := os .Getenv ("GITHUB_TOKEN" )
104107
105108 ref , err := name .ParseReference (newBuilderImageTagged )
106109 if err != nil {
107- return fmt .Errorf ("cannot parse reference to builder target: %w" , err )
110+ return "" , fmt .Errorf ("cannot parse reference to builder target: %w" , err )
108111 }
109- _ , err = remote .Head (ref , remote .WithAuth (auth {dockerUser , dockerPassword }))
112+ desc , err : = remote .Head (ref , remote .WithAuth (auth {dockerUser , dockerPassword }))
110113 if err == nil {
111114 fmt .Fprintln (os .Stderr , "The image has been already built." )
112- return nil
115+ return newBuilderImage + "@" + desc . Digest . String (), nil
113116 }
114117
115118 builderTomlPath := filepath .Join (buildDir , "builder.toml" )
116119 err = downloadBuilderToml (ctx , * release .TarballURL , builderTomlPath )
117120 if err != nil {
118- return fmt .Errorf ("cannot download builder toml: %w" , err )
121+ return "" , fmt .Errorf ("cannot download builder toml: %w" , err )
119122 }
120123
121124 builderConfig , _ , err := builder .ReadConfig (builderTomlPath )
122125 if err != nil {
123- return fmt .Errorf ("cannot parse builder.toml: %w" , err )
126+ return "" , fmt .Errorf ("cannot parse builder.toml: %w" , err )
124127 }
125128
126- err = updateJavaBuildpacks (ctx , & builderConfig )
129+ err = updateJavaBuildpacks (ctx , & builderConfig , arch )
127130 if err != nil {
128- return fmt .Errorf ("cannot patch java buildpacks: %w" , err )
131+ return "" , fmt .Errorf ("cannot patch java buildpacks: %w" , err )
129132 }
130133 addGoAndRustBuildpacks (& builderConfig )
131134
132135 packClient , err := pack .NewClient ()
133136 if err != nil {
134- return fmt .Errorf ("cannot create pack client: %w" , err )
137+ return "" , fmt .Errorf ("cannot create pack client: %w" , err )
135138 }
139+
136140 createBuilderOpts := pack.CreateBuilderOptions {
137141 RelativeBaseDir : buildDir ,
138- BuilderName : newBuilderImageTagged ,
139- Config : builderConfig ,
140- Publish : false ,
141- PullPolicy : bpimage .PullIfNotPresent ,
142+ Targets : []dist.Target {
143+ {
144+ OS : "linux" ,
145+ Arch : arch ,
146+ },
147+ },
148+ BuilderName : newBuilderImageTagged ,
149+ Config : builderConfig ,
150+ Publish : false ,
151+ PullPolicy : bpimage .PullAlways ,
142152 Labels : map [string ]string {
143153 "org.opencontainers.image.description" : "Paketo Jammy builder enriched with Rust and Func-Go buildpacks." ,
144154 "org.opencontainers.image.source" : "https://github.com/knative/func" ,
@@ -150,16 +160,12 @@ func buildBuilderImage(ctx context.Context, variant string) error {
150160
151161 err = packClient .CreateBuilder (ctx , createBuilderOpts )
152162 if err != nil {
153- return fmt .Errorf ("canont create builder: %w" , err )
163+ return "" , fmt .Errorf ("canont create builder: %w" , err )
154164 }
155165
156166 dockerClient , err := docker .NewClientWithOpts (docker .FromEnv , docker .WithAPIVersionNegotiation ())
157167 if err != nil {
158- return fmt .Errorf ("cannot create docker client" )
159- }
160- err = dockerClient .ImageTag (ctx , newBuilderImageTagged , newBuilderImageLatest )
161- if err != nil {
162- return fmt .Errorf ("cannot tag latest: %w" , err )
168+ return "" , fmt .Errorf ("cannot create docker client" )
163169 }
164170
165171 authConfig := registry.AuthConfig {
@@ -168,38 +174,161 @@ func buildBuilderImage(ctx context.Context, variant string) error {
168174 }
169175 bs , err := json .Marshal (& authConfig )
170176 if err != nil {
171- return fmt .Errorf ("cannot marshal credentials: %w" , err )
177+ return "" , fmt .Errorf ("cannot marshal credentials: %w" , err )
172178 }
173179 imagePushOptions := image.PushOptions {
174180 All : false ,
175181 RegistryAuth : base64 .StdEncoding .EncodeToString (bs ),
176182 }
177183
178- pushImage := func (image string ) error {
184+ pushImage := func (image string ) ( string , error ) {
179185 rc , err := dockerClient .ImagePush (ctx , image , imagePushOptions )
180186 if err != nil {
181- return fmt .Errorf ("cannot initialize image push: %w" , err )
187+ return "" , fmt .Errorf ("cannot initialize image push: %w" , err )
182188 }
183189 defer func (rc io.ReadCloser ) {
184190 _ = rc .Close ()
185191 }(rc )
192+
193+ pr , pw := io .Pipe ()
194+ digestCh := make (chan string )
195+ go func () {
196+ var (
197+ jm jsonmessage.JSONMessage
198+ dec = json .NewDecoder (pr )
199+ err error
200+ )
201+ for {
202+ err = dec .Decode (& jm )
203+ if err != nil {
204+ if errors .Is (err , io .EOF ) {
205+ break
206+ }
207+ panic (err )
208+ }
209+ if jm .Error != nil {
210+ continue
211+ }
212+
213+ re := regexp .MustCompile (`\sdigest: (?P<hash>sha256:[a-zA-Z0-9]+)\s` )
214+ matches := re .FindStringSubmatch (jm .Status )
215+ if len (matches ) == 2 {
216+ digestCh <- matches [1 ]
217+ }
218+ }
219+ }()
220+ r := io .TeeReader (rc , pw )
221+
186222 fd := os .Stdout .Fd ()
187223 isTerminal := term .IsTerminal (int (os .Stdout .Fd ()))
188- err = jsonmessage .DisplayJSONMessagesStream (rc , os .Stderr , fd , isTerminal , nil )
224+ err = jsonmessage .DisplayJSONMessagesStream (r , os .Stderr , fd , isTerminal , nil )
225+ _ = pw .Close ()
226+ if err != nil {
227+ return "" , err
228+ }
229+
230+ return <- digestCh , nil
231+ }
232+
233+ var d string
234+ d , err = pushImage (newBuilderImageTagged )
235+ if err != nil {
236+ return "" , fmt .Errorf ("cannot push the image: %w" , err )
237+ }
238+
239+ return newBuilderImage + "@" + d , nil
240+ }
241+
242+ // Builds builder for each arch and creates manifest list
243+ func buildBuilderImageMultiArch (ctx context.Context , variant string ) error {
244+ ghClient := newGHClient (ctx )
245+ listOpts := & github.ListOptions {Page : 0 , PerPage : 1 }
246+ releases , ghResp , err := ghClient .Repositories .ListReleases (ctx , "paketo-buildpacks" , "builder-jammy-" + variant , listOpts )
247+ if err != nil {
248+ return fmt .Errorf ("cannot get upstream builder release: %w" , err )
249+ }
250+ defer func (Body io.ReadCloser ) {
251+ _ = Body .Close ()
252+ }(ghResp .Body )
253+
254+ if len (releases ) <= 0 {
255+ return fmt .Errorf ("cannot get latest release" )
256+ }
257+
258+ release := releases [0 ]
259+
260+ if release .Name == nil {
261+ return fmt .Errorf ("the name of the release is not defined" )
262+ }
263+ if release .TarballURL == nil {
264+ return fmt .Errorf ("the tarball url of the release is not defined" )
265+ }
266+
267+ remoteOpts := []remote.Option {
268+ remote .WithAuth (authn .FromConfig (authn.AuthConfig {
269+ Username : "gh-action" ,
270+ Password : os .Getenv ("GITHUB_TOKEN" ),
271+ })),
272+ }
273+
274+ idx := mutate .IndexMediaType (empty .Index , types .DockerManifestList )
275+ for _ , arch := range []string {"arm64" , "amd64" } {
276+ if arch == "arm64" && variant != "tiny" {
277+ _ , _ = fmt .Fprintf (os .Stderr , "skipping arm64 build for variant: %q\n " , variant )
278+ continue
279+ }
280+
281+ var imgName string
282+
283+ imgName , err = buildBuilderImage (ctx , variant , arch )
189284 if err != nil {
190285 return err
191286 }
192- return nil
287+
288+ imgRef , err := name .ParseReference (imgName )
289+ if err != nil {
290+ return fmt .Errorf ("cannot parse image ref: %w" , err )
291+ }
292+ img , err := remote .Image (imgRef , remoteOpts ... )
293+ if err != nil {
294+ return fmt .Errorf ("cannot get the image: %w" , err )
295+ }
296+
297+ cf , err := img .ConfigFile ()
298+ if err != nil {
299+ return fmt .Errorf ("cannot get config file for the image: %w" , err )
300+ }
301+
302+ newDesc , err := partial .Descriptor (img )
303+ if err != nil {
304+ return fmt .Errorf ("cannot get partial descriptor for the image: %w" , err )
305+ }
306+ newDesc .Platform = cf .Platform ()
307+
308+ idx = mutate .AppendManifests (idx , mutate.IndexAddendum {
309+ Add : img ,
310+ Descriptor : * newDesc ,
311+ })
193312 }
194313
195- err = pushImage ( newBuilderImageTagged )
314+ idxRef , err := name . ParseReference ( "ghcr.io/knative/builder-jammy-" + variant + ":" + * release . Name )
196315 if err != nil {
197- return fmt .Errorf ("cannot push the image: %w" , err )
316+ return fmt .Errorf ("cannot parse image index ref : %w" , err )
198317 }
199318
200- err = pushImage ( newBuilderImageLatest )
319+ err = remote . WriteIndex ( idxRef , idx , remoteOpts ... )
201320 if err != nil {
202- return fmt .Errorf ("cannot push the image: %w" , err )
321+ return fmt .Errorf ("cannot write image index: %w" , err )
322+ }
323+
324+ idxRef , err = name .ParseReference ("ghcr.io/knative/builder-jammy-" + variant + ":latest" )
325+ if err != nil {
326+ return fmt .Errorf ("cannot parse image index ref: %w" , err )
327+ }
328+
329+ err = remote .WriteIndex (idxRef , idx , remoteOpts ... )
330+ if err != nil {
331+ return fmt .Errorf ("cannot write image index: %w" , err )
203332 }
204333
205334 return nil
@@ -212,7 +341,7 @@ type buildpack struct {
212341 patchFunc func (packageDesc * buildpackage.Config , bpDesc * dist.BuildpackDescriptor )
213342}
214343
215- func buildBuildpackImage (ctx context.Context , bp buildpack ) error {
344+ func buildBuildpackImage (ctx context.Context , bp buildpack , arch string ) error {
216345 ghClient := newGHClient (ctx )
217346
218347 var (
@@ -326,10 +455,16 @@ func buildBuildpackImage(ctx context.Context, bp buildpack) error {
326455 Format : pack .FormatImage ,
327456 Config : cfg ,
328457 Publish : false ,
329- PullPolicy : bpimage .PullIfNotPresent ,
458+ PullPolicy : bpimage .PullAlways ,
330459 Registry : "" ,
331460 Flatten : false ,
332461 FlattenExclude : nil ,
462+ Targets : []dist.Target {
463+ {
464+ OS : "linux" ,
465+ Arch : arch ,
466+ },
467+ },
333468 }
334469 packClient , err := pack .NewClient ()
335470 if err != nil {
@@ -473,7 +608,7 @@ func addGoAndRustBuildpacks(config *builder.Config) {
473608}
474609
475610// updated java and java-native-image buildpack to include quarkus buildpack
476- func updateJavaBuildpacks (ctx context.Context , builderConfig * builder.Config ) error {
611+ func updateJavaBuildpacks (ctx context.Context , builderConfig * builder.Config , arch string ) error {
477612 var err error
478613
479614 for _ , entry := range builderConfig .Order {
@@ -485,7 +620,7 @@ func updateJavaBuildpacks(ctx context.Context, builderConfig *builder.Config) er
485620 version : entry .Group [0 ].Version ,
486621 image : img ,
487622 patchFunc : addQuarkusBuildpack ,
488- })
623+ }, arch )
489624 // TODO we might want to push these images to registry
490625 // but it's not absolutely necessary since they are included in builder
491626 if err != nil {
0 commit comments