From 1f977d52cb34ecf69df38c7ece491c1b2adb2bc2 Mon Sep 17 00:00:00 2001 From: Adrian Lai Date: Wed, 26 Apr 2023 15:37:36 +0100 Subject: [PATCH] Allow specification of the binary name/path Some container usage expects to run an executable in a specific location, and some projects aren't well laid out to generate binaries with the expected names. This allows the binary name to be specified in configuration (in a similar way to goreleaser), which should allow for easier migration from other build mechanisms (and usage patterns of their images) to `ko`. This does not affect the name or path for Windows builds - the path structure is fundamentally different, and I think that if it's desirable to change the name/path for them, we should use a separate config argument. --- pkg/build/config.go | 4 +- pkg/build/gobuild.go | 11 ++++- pkg/build/gobuild_test.go | 93 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 100 insertions(+), 8 deletions(-) diff --git a/pkg/build/config.go b/pkg/build/config.go index f5cb3479f5..a585e33db4 100644 --- a/pkg/build/config.go +++ b/pkg/build/config.go @@ -81,6 +81,9 @@ type Config struct { // Env allows setting environment variables for `go build` Env []string `yaml:",omitempty"` + // Binary allows setting the name of the binary + Binary string `yaml:",omitempty"` + // Other GoReleaser fields that are not supported or do not make sense // in the context of ko, for reference or for future use: // Goos []string `yaml:",omitempty"` @@ -88,7 +91,6 @@ type Config struct { // Goarm []string `yaml:",omitempty"` // Gomips []string `yaml:",omitempty"` // Targets []string `yaml:",omitempty"` - // Binary string `yaml:",omitempty"` // Lang string `yaml:",omitempty"` // Asmflags StringArray `yaml:",omitempty"` // Gcflags StringArray `yaml:",omitempty"` diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index 13abd30d16..df5d64eb2e 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -830,8 +830,9 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl if !g.platformMatcher.matches(platform) { return nil, fmt.Errorf("base image platform %q does not match desired platforms %v", platform, g.platformMatcher.platforms) } + conf := g.configForImportPath(ref.Path()) // Do the build into a temporary file. - file, err := g.build(ctx, ref.Path(), g.dir, *platform, g.configForImportPath(ref.Path())) + file, err := g.build(ctx, ref.Path(), g.dir, *platform, conf) if err != nil { return nil, fmt.Errorf("build: %w", err) } @@ -865,6 +866,14 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl appDir := "/ko-app" appFileName := appFilename(ref.Path()) + if conf.Binary != "" { + if path.IsAbs(conf.Binary) { + appDir = path.Dir(conf.Binary) + appFileName = path.Base(conf.Binary) + } else { + appFileName = conf.Binary + } + } appPath := path.Join(appDir, appFileName) miss := func() (v1.Layer, error) { diff --git a/pkg/build/gobuild_test.go b/pkg/build/gobuild_test.go index 3d9ec9db8f..047096846e 100644 --- a/pkg/build/gobuild_test.go +++ b/pkg/build/gobuild_test.go @@ -346,6 +346,21 @@ func TestBuildConfig(t *testing.T) { Flags: FlagArray{"-v", "-trimpath"}, }, }, + { + description: "build config with binary", + options: []Option{ + WithBaseImages(nilGetBase), + WithConfig(map[string]Config{ + "example.com/foo": { + Binary: "bar", + }, + }), + }, + importpath: "example.com/foo", + expectConfig: Config{ + Binary: "bar", + }, + }, { description: "no trimpath overridden by build config flag", options: []Option{ @@ -516,7 +531,7 @@ func TestGoBuildNoKoData(t *testing.T) { }) } -func validateImage(t *testing.T, img oci.SignedImage, baseLayers int64, creationTime v1.Time, checkAnnotations bool, expectSBOM bool) { +func validateImage(t *testing.T, img oci.SignedImage, baseLayers int64, creationTime v1.Time, checkAnnotations bool, expectSBOM bool, buildConfig *Config) { t.Helper() ls, err := img.Layers() @@ -598,7 +613,15 @@ func validateImage(t *testing.T, img oci.SignedImage, baseLayers int64, creation t.Errorf("len(entrypoint) = %v, want %v", got, want) } - if got, want := entrypoint[0], "/ko-app/test"; got != want { + exp := "/ko-app/test" + if buildConfig != nil && buildConfig.Binary != "" { + if path.IsAbs(buildConfig.Binary) { + exp = buildConfig.Binary + } else { + exp = path.Join("/ko-app", buildConfig.Binary) + } + } + if got, want := entrypoint[0], exp; got != want { t.Errorf("entrypoint = %v, want %v", got, want) } }) @@ -626,13 +649,19 @@ func validateImage(t *testing.T, img oci.SignedImage, baseLayers int64, creation if err != nil { t.Errorf("ConfigFile() = %v", err) } + exp := "/ko-app" + if buildConfig != nil && buildConfig.Binary != "" { + if path.IsAbs(buildConfig.Binary) { + exp = path.Dir(buildConfig.Binary) + } + } found := false for _, envVar := range cfg.Config.Env { if strings.HasPrefix(envVar, "PATH=") { pathValue := strings.TrimPrefix(envVar, "PATH=") pathEntries := strings.Split(pathValue, ":") for _, pathEntry := range pathEntries { - if pathEntry == "/ko-app" { + if pathEntry == exp { found = true } } @@ -736,7 +765,7 @@ func TestGoBuild(t *testing.T) { t.Fatalf("Build() not a SignedImage: %T", result) } - validateImage(t, img, baseLayers, creationTime, true, true) + validateImage(t, img, baseLayers, creationTime, true, true, nil) // Check that rebuilding the image again results in the same image digest. t.Run("check determinism", func(t *testing.T) { @@ -776,6 +805,58 @@ func TestGoBuild(t *testing.T) { }) } +func TestGoBuildWithBinaryName(t *testing.T) { + tests := map[string]string{ + "override binary name": "bar", + "override path": "/usr/bin/bar", + "relative path": "foo/bar", + } + for description, filename := range tests { + t.Run(description, func(t *testing.T) { + baseLayers := int64(3) + base, err := random.Image(1024, baseLayers) + if err != nil { + t.Fatalf("random.Image() = %v", err) + } + importpath := "github.com/google/ko" + + creationTime := v1.Time{Time: time.Unix(5000, 0)} + buildConfig := Config{ + Binary: filename, + } + ng, err := NewGo( + context.Background(), + "", + WithCreationTime(creationTime), + WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), + withBuilder(writeTempFile), + withSBOMber(fauxSBOM), + WithLabel("foo", "bar"), + WithLabel("hello", "world"), + WithPlatforms("all"), + WithConfig(map[string]Config{ + "github.com/google/ko/test": buildConfig, + }), + ) + if err != nil { + t.Fatalf("NewGo() = %v", err) + } + + result, err := ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test")) + if err != nil { + t.Fatalf("Build() = %v", err) + } + + img, ok := result.(oci.SignedImage) + if !ok { + t.Fatalf("Build() not a SignedImage: %T", result) + } + + validateImage(t, img, baseLayers, creationTime, true, true, &buildConfig) + }) + } +} + func TestGoBuildWithKOCACHE(t *testing.T) { now := time.Now() // current local time sec := now.Unix() @@ -850,7 +931,7 @@ func TestGoBuildWithoutSBOM(t *testing.T) { t.Fatalf("Build() not a SignedImage: %T", result) } - validateImage(t, img, baseLayers, creationTime, true, false) + validateImage(t, img, baseLayers, creationTime, true, false, nil) } func TestGoBuildIndex(t *testing.T) { @@ -896,7 +977,7 @@ func TestGoBuildIndex(t *testing.T) { if err != nil { t.Fatalf("idx.Image(%s) = %v", desc.Digest, err) } - validateImage(t, img, baseLayers, creationTime, false, true) + validateImage(t, img, baseLayers, creationTime, false, true, nil) } if want, got := images, int64(len(im.Manifests)); want != got {