diff --git a/README.md b/README.md index 2fc5a22cc9..008890bf90 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,42 @@ baseImageOverrides: github.com/my-user/my-repo/cmd/foo: registry.example.com/base/for/foo ``` +### Overriding Go build settings + +By default, `ko` builds the binary with no additional build flags other than +`--trimpath` (depending on the Go version). You can replace the default build +arguments by providing build flags and ldflags using a +[GoReleaser](https://github.com/goreleaser/goreleaser) influenced `builds` +configuration section in your `.ko.yaml`. + +```yaml +builds: +- id: foo + dir: ./foobar/foo + flags: + - -tags + - netgo + ldflags: + - -s -w + - -extldflags "-static" + - -X main.version={{.Env.VERSION}} +- id: bar + main: ./foobar/bar/main.go + ldflags: + - -s + - -w +``` + +For the build, `ko` will pick the entry based on the respective import path +being used. It will be matched against the local path that is configured using +`dir` and `main`. In the context of `ko`, it is fine just to specify `dir` +with the intended import path. + +_Please note:_ Even though the configuration section is similar to the +[GoReleaser `builds` section](https://goreleaser.com/customization/build/), +only the `flags` and `ldflags` fields are currently supported. Also, the +templating support is currently limited to environment variables only. + ## Naming Images `ko` provides a few different strategies for naming the image it pushes, to @@ -320,10 +356,17 @@ is a common way to embed version info in go binaries (In fact, we do this for this flag directly; however, you can use the `GOFLAGS` environment variable instead: -``` +```sh GOFLAGS="-ldflags=-X=main.version=1.2.3" ko publish . ``` +## How can I set multiple `ldflags`? + +Currently, there is a limitation that does not allow to set multiple arguments +in `ldflags` using `GOFLAGS`. Using `-ldflags` multiple times also does not +work. In this use case, it works best to use the [`builds` section](#overriding-go-build-settings) +in the `.ko.yaml` file. + ## Why are my images all created in 1970? In order to support [reproducible builds](https://reproducible-builds.org), `ko` diff --git a/pkg/build/config.go b/pkg/build/config.go new file mode 100644 index 0000000000..af44c7ed4d --- /dev/null +++ b/pkg/build/config.go @@ -0,0 +1,80 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build + +import "strings" + +// Note: The structs, types, and functions are based upon GoReleaser build +// configuration to have a loosly compatible YAML configuration: +// https://github.com/goreleaser/goreleaser/blob/master/pkg/config/config.go + +// StringArray is a wrapper for an array of strings. +type StringArray []string + +// UnmarshalYAML is a custom unmarshaler that wraps strings in arrays. +func (a *StringArray) UnmarshalYAML(unmarshal func(interface{}) error) error { + var strings []string + if err := unmarshal(&strings); err != nil { + var str string + if err := unmarshal(&str); err != nil { + return err + } + *a = []string{str} + } else { + *a = strings + } + return nil +} + +// FlagArray is a wrapper for an array of strings. +type FlagArray []string + +// UnmarshalYAML is a custom unmarshaler that wraps strings in arrays. +func (a *FlagArray) UnmarshalYAML(unmarshal func(interface{}) error) error { + var flags []string + if err := unmarshal(&flags); err != nil { + var flagstr string + if err := unmarshal(&flagstr); err != nil { + return err + } + *a = strings.Fields(flagstr) + } else { + *a = flags + } + return nil +} + +// Config contains the build configuration section. The name was changed from +// the original GoReleaser name to match better with the ko namings. +type Config struct { + ID string `yaml:",omitempty"` + Goos []string `yaml:",omitempty"` + Goarch []string `yaml:",omitempty"` + Goarm []string `yaml:",omitempty"` + Gomips []string `yaml:",omitempty"` + Targets []string `yaml:",omitempty"` + Dir string `yaml:",omitempty"` + Main string `yaml:",omitempty"` + Ldflags StringArray `yaml:",omitempty"` + Flags FlagArray `yaml:",omitempty"` + Binary string `yaml:",omitempty"` + Env []string `yaml:",omitempty"` + Lang string `yaml:",omitempty"` + Asmflags StringArray `yaml:",omitempty"` + Gcflags StringArray `yaml:",omitempty"` + ModTimestamp string `yaml:"mod_timestamp,omitempty"` + Skip bool `yaml:",omitempty"` + GoBinary string `yaml:",omitempty"` +} diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index 4f0565e896..d93082f9a2 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -31,6 +31,7 @@ import ( "path/filepath" "strconv" "strings" + "text/template" "github.com/containerd/stargz-snapshotter/estargz" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -64,7 +65,7 @@ For more information see: // GetBase takes an importpath and returns a base image. type GetBase func(context.Context, string) (Result, error) -type builder func(context.Context, string, v1.Platform, bool) (string, error) +type builder func(context.Context, string, v1.Platform, []string) (string, error) type buildContext interface { Import(path string, srcDir string, mode gb.ImportMode) (*gb.Package, error) @@ -80,6 +81,7 @@ type gobuild struct { creationTime v1.Time build builder disableOptimizations bool + buildCfgOverrides map[string]Config mod *modules buildContext buildContext platformMatcher *platformMatcher @@ -94,6 +96,7 @@ type gobuildOpener struct { creationTime v1.Time build builder disableOptimizations bool + buildCfgOverrides map[string]Config mod *modules buildContext buildContext platform string @@ -113,6 +116,7 @@ func (gbo *gobuildOpener) Open() (Interface, error) { creationTime: gbo.creationTime, build: gbo.build, disableOptimizations: gbo.disableOptimizations, + buildCfgOverrides: gbo.buildCfgOverrides, mod: gbo.mod, buildContext: gbo.buildContext, labels: gbo.labels, @@ -303,21 +307,17 @@ func platformToString(p v1.Platform) string { return fmt.Sprintf("%s/%s", p.OS, p.Architecture) } -func build(ctx context.Context, ip string, platform v1.Platform, disableOptimizations bool) (string, error) { +func build(ctx context.Context, ip string, platform v1.Platform, buildArgs []string) (string, error) { tmpDir, err := ioutil.TempDir("", "ko") if err != nil { return "", err } file := filepath.Join(tmpDir, "out") - args := make([]string, 0, 7) + args := make([]string, 0, 4+len(buildArgs)) args = append(args, "build") - if disableOptimizations { - // Disable optimizations (-N) and inlining (-l). - args = append(args, "-gcflags", "all=-N -l") - } + args = append(args, buildArgs...) args = append(args, "-o", file) - args = addGo113TrimPathFlag(args) args = append(args, ip) cmd := exec.CommandContext(ctx, "go", args...) @@ -520,6 +520,72 @@ func (g *gobuild) tarKoData(ref reference) (*bytes.Buffer, error) { return buf, walkRecursive(tw, root, kodataRoot) } +func createTemplateData() map[string]interface{} { + envVars := map[string]string{} + for _, entry := range os.Environ() { + kv := strings.SplitN(entry, "=", 2) + envVars[kv[0]] = kv[1] + } + + return map[string]interface{}{ + "Env": envVars, + } +} + +func applyTemplating(list []string, data map[string]interface{}) error { + for i, entry := range list { + tmpl, err := template.New("argsTmpl").Option("missingkey=error").Parse(entry) + if err != nil { + return err + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return err + } + + list[i] = buf.String() + } + + return nil +} + +func (g *gobuild) buildArgs(ip string) ([]string, error) { + var args []string + + if buildCfg, ok := g.buildCfgOverrides[ip]; ok { + data := createTemplateData() + + if len(buildCfg.Flags) > 0 { + if err := applyTemplating(buildCfg.Flags, data); err != nil { + return nil, err + } + + args = append(args, buildCfg.Flags...) + } + + if len(buildCfg.Ldflags) > 0 { + if err := applyTemplating(buildCfg.Ldflags, data); err != nil { + return nil, err + } + + args = append(args, fmt.Sprintf("-ldflags=%s", strings.Join(buildCfg.Ldflags, " "))) + } + } + + // Apply default build flags in case none were supplied + if args == nil { + args = addGo113TrimPathFlag(args) + } + + if g.disableOptimizations { + // Disable optimizations (-N) and inlining (-l). + args = append(args, "-gcflags", "all=-N -l") + } + + return args, nil +} + func (g *gobuild) buildOne(ctx context.Context, s string, base v1.Image, platform *v1.Platform) (v1.Image, error) { ref := newRef(s) @@ -535,8 +601,13 @@ func (g *gobuild) buildOne(ctx context.Context, s string, base v1.Image, platfor } } + buildArgs, err := g.buildArgs(ref.Path()) + if err != nil { + return nil, err + } + // Do the build into a temporary file. - file, err := g.build(ctx, ref.Path(), *platform, g.disableOptimizations) + file, err := g.build(ctx, ref.Path(), *platform, buildArgs) if err != nil { return nil, err } diff --git a/pkg/build/gobuild_test.go b/pkg/build/gobuild_test.go index 85dd4ba76c..57376b3db1 100644 --- a/pkg/build/gobuild_test.go +++ b/pkg/build/gobuild_test.go @@ -132,7 +132,7 @@ func TestGoBuildIsSupportedRefWithModules(t *testing.T) { } // A helper method we use to substitute for the default "build" method. -func writeTempFile(_ context.Context, s string, _ v1.Platform, _ bool) (string, error) { +func writeTempFile(_ context.Context, s string, _ v1.Platform, _ []string) (string, error) { tmpDir, err := ioutil.TempDir("", "ko") if err != nil { return "", err diff --git a/pkg/build/options.go b/pkg/build/options.go index a8a765811b..7e0d6be66c 100644 --- a/pkg/build/options.go +++ b/pkg/build/options.go @@ -45,6 +45,15 @@ func WithDisabledOptimizations() Option { } } +// WithConfig is a functional option for providing build settings based on a +// GoReleaser Build influenced configuration. +func WithConfig(buildCfgOverrides map[string]Config) Option { + return func(gbo *gobuildOpener) error { + gbo.buildCfgOverrides = buildCfgOverrides + return nil + } +} + // WithPlatforms is a functional option for building certain platforms for // multi-platform base images. To build everything from the base, use "all", // otherwise use a comma-separated list of platform specs, i.e.: diff --git a/pkg/commands/config.go b/pkg/commands/config.go index 20f94f94be..1a1758a410 100644 --- a/pkg/commands/config.go +++ b/pkg/commands/config.go @@ -17,9 +17,11 @@ package commands import ( "context" "fmt" + gb "go/build" "log" "os" "os/signal" + "path/filepath" "strconv" "strings" "syscall" @@ -37,6 +39,7 @@ import ( var ( defaultBaseImage name.Reference baseImageOverrides map[string]name.Reference + buildCfgOverrides map[string]build.Config ) func getBaseImage(platform string) build.GetBase { @@ -160,4 +163,32 @@ func init() { } baseImageOverrides[k] = bi } + + var builds []build.Config + if err := viper.UnmarshalKey("builds", &builds); err != nil { + log.Fatalf("configuration section 'builds' cannot be parsed") + } + + buildCfgOverrides = make(map[string]build.Config) + for _, build := range builds { + path := build.Dir + if len(path) == 0 { + path = "./" + } + + if len(build.Main) > 0 { + if mainDir := filepath.Dir(build.Main); mainDir != "." { + path = path + string(filepath.Separator) + mainDir + } + } + + if gb.IsLocalImport(path) { + path, err = qualifyLocalImport(path) + if err != nil { + log.Fatalf("failed to create qualified import path using path %s", path) + } + } + + buildCfgOverrides[path] = build + } } diff --git a/pkg/commands/resolver.go b/pkg/commands/resolver.go index 2d813196ee..c77c4f1b9e 100644 --- a/pkg/commands/resolver.go +++ b/pkg/commands/resolver.go @@ -95,6 +95,11 @@ func gobuildOptions(bo *options.BuildOptions) ([]build.Option, error) { } opts = append(opts, build.WithLabel(parts[0], parts[1])) } + + if len(buildCfgOverrides) > 0 { + opts = append(opts, build.WithConfig(buildCfgOverrides)) + } + return opts, nil }