Skip to content

Commit

Permalink
Fix multi-arch build. Merge code with docker
Browse files Browse the repository at this point in the history
  • Loading branch information
valerian-roche committed Dec 29, 2023
1 parent 53297f5 commit c009954
Show file tree
Hide file tree
Showing 10 changed files with 357 additions and 366 deletions.
162 changes: 162 additions & 0 deletions internal/containers/context.go
@@ -0,0 +1,162 @@
package containers

import (
"fmt"
"os"
"path/filepath"

"github.com/caarlos0/log"
"github.com/goreleaser/goreleaser/internal/gio"
"github.com/goreleaser/goreleaser/internal/pipe"
"github.com/goreleaser/goreleaser/internal/tmpl"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
)

type ImageBuildContext struct {
ID string
BuildPath string
BuildFlags []string
PushFlags []string
Platforms []config.ContainerPlatform
Images []string
}

func (p ImageBuildContext) Log() *log.Entry {
if len(p.Images) > 0 {
return log.WithField("image", p.Images[0])
} else {

Check warning on line 28 in internal/containers/context.go

View workflow job for this annotation

GitHub Actions / lint

indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)
return log.WithField("id", p.ID)
}

Check warning on line 30 in internal/containers/context.go

View check run for this annotation

Codecov / codecov/patch

internal/containers/context.go#L29-L30

Added lines #L29 - L30 were not covered by tests
}

func LogEntry(ctx *context.Context, config config.Container) *log.Entry {
images, _ := processImageTemplates(ctx, config.ImageTemplates)
if len(images) > 0 {
return log.WithField("image", images[0])
} else {

Check warning on line 37 in internal/containers/context.go

View workflow job for this annotation

GitHub Actions / lint

indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)
return log.WithField("id", config.ID)
}

Check warning on line 39 in internal/containers/context.go

View check run for this annotation

Codecov / codecov/patch

internal/containers/context.go#L33-L39

Added lines #L33 - L39 were not covered by tests
}

func BuildContext(ctx *context.Context, id string, imageDef config.ImageDefinition, platforms []config.ContainerPlatform) (ImageBuildContext, func(), error) {
context := ImageBuildContext{
ID: id,
Platforms: platforms,
}

images, err := processImageTemplates(ctx, imageDef.ImageTemplates)
if err != nil {
return context, nil, err
}

if len(images) == 0 {
return context, nil, pipe.Skip("no image templates found")
}
context.Images = images

tmp, err := os.MkdirTemp("", "goreleaserdocker")
if err != nil {
return context, nil, fmt.Errorf("failed to create temporary dir: %w", err)
}

Check warning on line 61 in internal/containers/context.go

View check run for this annotation

Codecov / codecov/patch

internal/containers/context.go#L60-L61

Added lines #L60 - L61 were not covered by tests
context.BuildPath = tmp

keepContext := false
defer func() {
if !keepContext {
os.RemoveAll(tmp)
}
}()

log := log.WithField("image", images[0])
log.Debug("tempdir: " + tmp)

// This will set all binaries for all architectures within the context
// To ensure multi-arch builds can access the correct binaries, they are copied to $tmp/$TARGETPLATFORM/$name
artifacts := getApplicableArtifacts(ctx, imageDef, platforms)
if len(artifacts.List()) == 0 {
log.Warn("no binaries or packages found for the given platform - COPY/ADD may not work")
}

Check warning on line 79 in internal/containers/context.go

View check run for this annotation

Codecov / codecov/patch

internal/containers/context.go#L78-L79

Added lines #L78 - L79 were not covered by tests
log.WithField("artifacts", artifacts.Paths()).Debug("found artifacts")

if err := tmpl.New(ctx).ApplyAll(
&imageDef.Dockerfile,
); err != nil {
return context, nil, err
}
if err := gio.Copy(
imageDef.Dockerfile,
filepath.Join(tmp, "Dockerfile"),
); err != nil {
return context, nil, fmt.Errorf("failed to copy dockerfile: %w", err)
}

for _, file := range imageDef.Files {
if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(file)), 0o755); err != nil {
return context, nil, fmt.Errorf("failed to copy extra file '%s': %w", file, err)
}

Check warning on line 97 in internal/containers/context.go

View check run for this annotation

Codecov / codecov/patch

internal/containers/context.go#L96-L97

Added lines #L96 - L97 were not covered by tests
if err := gio.Copy(file, filepath.Join(tmp, file)); err != nil {
return context, nil, fmt.Errorf("failed to copy extra file '%s': %w", file, err)
}
}

for _, art := range artifacts.List() {
target := filepath.Join(tmp, art.Goos, art.Goarch, art.Name)
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return context, nil, fmt.Errorf("failed to make dir for artifact: %w", err)
}

Check warning on line 107 in internal/containers/context.go

View check run for this annotation

Codecov / codecov/patch

internal/containers/context.go#L106-L107

Added lines #L106 - L107 were not covered by tests

if err := gio.Copy(art.Path, target); err != nil {
return context, nil, fmt.Errorf("failed to copy artifact: %w", err)
}

if len(context.Platforms) == 1 {
if err := gio.Link(target, filepath.Join(tmp, art.Name)); err != nil {
return context, nil, fmt.Errorf("failed to link artifact: %w", err)
}

Check warning on line 116 in internal/containers/context.go

View check run for this annotation

Codecov / codecov/patch

internal/containers/context.go#L115-L116

Added lines #L115 - L116 were not covered by tests
}
}

buildFlags, err := processBuildFlagTemplates(ctx, imageDef.BuildFlagTemplates)
if err != nil {
return context, nil, err
}
context.BuildFlags = buildFlags
context.PushFlags = imageDef.PushFlags

keepContext = true
return context, func() {
os.RemoveAll(tmp)
}, nil
}

func processImageTemplates(ctx *context.Context, templates []string) ([]string, error) {
// nolint:prealloc
var images []string
for _, imageTemplate := range templates {
image, err := tmpl.New(ctx).Apply(imageTemplate)
if err != nil {
return nil, fmt.Errorf("failed to execute image template '%s': %w", imageTemplate, err)
}
if image == "" {
continue
}

images = append(images, image)
}

return images, nil
}

func processBuildFlagTemplates(ctx *context.Context, flagTemplates []string) ([]string, error) {
// nolint:prealloc
var buildFlags []string
for _, buildFlagTemplate := range flagTemplates {
buildFlag, err := tmpl.New(ctx).Apply(buildFlagTemplate)
if err != nil {
return nil, fmt.Errorf("failed to process build flag template '%s': %w", buildFlagTemplate, err)
}
buildFlags = append(buildFlags, buildFlag)
}
return buildFlags, nil
}
51 changes: 51 additions & 0 deletions internal/containers/context_test.go
@@ -0,0 +1,51 @@
package containers

import (
"testing"

"github.com/goreleaser/goreleaser/internal/testctx"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/stretchr/testify/require"
)

func Test_processImageTemplates(t *testing.T) {
ctx := testctx.NewWithCfg(
config.Project{
Builds: []config.Build{
{
ID: "default",
},
},
Dockers: []config.Docker{
{
ImageDefinition: config.ImageDefinition{
Dockerfile: "Dockerfile.foo",
ImageTemplates: []string{
"user/image:{{.Tag}}",
"gcr.io/image:{{.Tag}}-{{.Env.FOO}}",
"gcr.io/image:v{{.Major}}.{{.Minor}}",
},
},
SkipPush: "true",
},
},
Env: []string{"FOO=123"},
},
testctx.WithVersion("1.0.0"),
testctx.WithCurrentTag("v1.0.0"),
testctx.WithCommit("a1b2c3d4"),
testctx.WithSemver(1, 0, 0, ""),
)
require.Len(t, ctx.Config.Dockers, 1)

docker := ctx.Config.Dockers[0]
require.Equal(t, "Dockerfile.foo", docker.Dockerfile)

images, err := processImageTemplates(ctx, docker.ImageTemplates)
require.NoError(t, err)
require.Equal(t, []string{
"user/image:v1.0.0",
"gcr.io/image:v1.0.0-123",
"gcr.io/image:v1.0",
}, images)
}
62 changes: 62 additions & 0 deletions internal/containers/platform.go
@@ -0,0 +1,62 @@
package containers

import (
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
)

func DefaultPlatforms(platforms []config.ContainerPlatform) []config.ContainerPlatform {
if len(platforms) == 0 {
platforms = make([]config.ContainerPlatform, 1)
}
for i := range platforms {
DefaultPlatform(&platforms[i])
}
return platforms

Check warning on line 16 in internal/containers/platform.go

View check run for this annotation

Codecov / codecov/patch

internal/containers/platform.go#L9-L16

Added lines #L9 - L16 were not covered by tests
}

func DefaultPlatform(platform *config.ContainerPlatform) {
if platform.Goos == "" {
platform.Goos = "linux"
}
if platform.Goarch == "" {
platform.Goarch = "amd64"
}
if platform.Goarm == "" {
platform.Goarm = "6"
}
if platform.Goamd64 == "" {
platform.Goamd64 = "v1"
}
}

func getApplicableArtifacts(ctx *context.Context, imageDefinition config.ImageDefinition, platforms []config.ContainerPlatform) *artifact.Artifacts {
var platformFilters []artifact.Filter
for _, platform := range platforms {
filters := []artifact.Filter{
artifact.ByGoos(platform.Goos),
artifact.ByGoarch(platform.Goarch),
}
switch platform.Goarch {
case "amd64":
filters = append(filters, artifact.ByGoamd64(platform.Goamd64))
case "arm":
filters = append(filters, artifact.ByGoarm(platform.Goarm))

Check warning on line 45 in internal/containers/platform.go

View check run for this annotation

Codecov / codecov/patch

internal/containers/platform.go#L44-L45

Added lines #L44 - L45 were not covered by tests
}
platformFilters = append(platformFilters, artifact.And(
filters...,
))
}
filters := []artifact.Filter{
artifact.Or(platformFilters...),
artifact.Or(
artifact.ByType(artifact.Binary),
artifact.ByType(artifact.LinuxPackage),
),
}
if len(imageDefinition.IDs) > 0 {
filters = append(filters, artifact.ByIDs(imageDefinition.IDs...))
}
return ctx.Artifacts.Filter(artifact.And(filters...))
}
18 changes: 18 additions & 0 deletions internal/gio/link.go
@@ -0,0 +1,18 @@
package gio

import (
"fmt"
"os"
"path/filepath"

"github.com/caarlos0/log"
)

// Link creates a hard link and the parent directory if it does not exist yet.
func Link(src, dst string) error {
log.WithField("src", src).WithField("dst", dst).Debug("creating link")
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return fmt.Errorf("failed to make dir for destination: %w", err)
}
return os.Link(src, dst)
}
12 changes: 2 additions & 10 deletions internal/pipe/container/builder/api.go
Expand Up @@ -2,21 +2,13 @@ package builder

import (
"github.com/caarlos0/log"
"github.com/goreleaser/goreleaser/internal/containers"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
)

type ImageBuildParameters struct {
ID string
BuildPath string
BuildFlags []string
PushFlags []string
Platforms []config.ContainerPlatform
Images []string
}

type ContainerBuilder interface {
Build(ctx *context.Context, params ImageBuildParameters, importImages bool, pushImages bool, logger *log.Entry) error
Build(ctx *context.Context, buildContext containers.ImageBuildContext, importImages bool, pushImages bool, logger *log.Entry) error
// If returning true, Build should only be called once (in Run for `goreleaser build` or `goreleaser release --snapshot`, in Publish for `goreleaser release`)
// If returning false, Build should be called once in Run phase and again in Publish phase
SkipBuildIfPublish() bool
Expand Down

0 comments on commit c009954

Please sign in to comment.