diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index 172e9e3163..0fa143f4cd 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -16,6 +16,7 @@ package build import ( "archive/tar" + "bufio" "bytes" "context" "errors" @@ -252,7 +253,14 @@ func getGoBinary() string { } func build(ctx context.Context, ip string, dir string, platform v1.Platform, config Config) (string, error) { - buildArgs, err := createBuildArgs(config) + // Merge the user and config environment variables. + mergedEnv, err := buildEnv(platform, os.Environ(), config.Env) + if err != nil { + return "", fmt.Errorf("could not create env for %s: %w", ip, err) + } + + // Get the version of the GO binary used to build. + buildArgs, err := createBuildArgs(ctx, mergedEnv, config) if err != nil { return "", err } @@ -261,11 +269,6 @@ func build(ctx context.Context, ip string, dir string, platform v1.Platform, con args = append(args, "build") args = append(args, buildArgs...) - env, err := buildEnv(platform, os.Environ(), config.Env) - if err != nil { - return "", fmt.Errorf("could not create env for %s: %w", ip, err) - } - tmpDir := "" if dir := os.Getenv("KOCACHE"); dir != "" { @@ -301,7 +304,7 @@ func build(ctx context.Context, ip string, dir string, platform v1.Platform, con gobin := getGoBinary() cmd := exec.CommandContext(ctx, gobin, args...) cmd.Dir = dir - cmd.Env = env + cmd.Env = mergedEnv var output bytes.Buffer cmd.Stderr = &output @@ -310,13 +313,49 @@ func build(ctx context.Context, ip string, dir string, platform v1.Platform, con log.Printf("Building %s for %s", ip, platform) if err := cmd.Run(); err != nil { if os.Getenv("KOCACHE") == "" { - os.RemoveAll(tmpDir) + _ = os.RemoveAll(tmpDir) } return "", fmt.Errorf("go build: %w: %s", err, output.String()) } return file, nil } +func goenv(ctx context.Context) (map[string]string, error) { + gobin := getGoBinary() + cmd := exec.CommandContext(ctx, gobin, "env") + var output bytes.Buffer + cmd.Stdout = &output + cmd.Stderr = &output + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("go env: %w: %s", err, output.String()) + } + + env := make(map[string]string) + scanner := bufio.NewScanner(bytes.NewReader(output.Bytes())) + + line := 0 + for scanner.Scan() { + line++ + kv := strings.SplitN(scanner.Text(), "=", 2) + if len(kv) != 2 { + return nil, fmt.Errorf("go env: failed parsing line: %d", line) + } + key := strings.TrimSpace(kv[0]) + value := strings.TrimSpace(kv[1]) + + // Unquote the value. Handle single or double quoted strings. + if len(value) > 1 && ((value[0] == '\'' && value[len(value)-1] == '\'') || + (value[0] == '"' && value[len(value)-1] == '"')) { + value = value[1 : len(value)-1] + } + env[key] = value + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("go env: failed parsing: %w", err) + } + return env, nil +} + func goversionm(ctx context.Context, file string, appPath string, appFileName string, se oci.SignedEntity, dir string) ([]byte, types.MediaType, error) { gobin := getGoBinary() @@ -708,18 +747,35 @@ func (g *gobuild) tarKoData(ref reference, platform *v1.Platform) (*bytes.Buffer return buf, walkRecursive(tw, root, chroot, creationTime, platform) } -func createTemplateData() map[string]interface{} { +func createTemplateData(ctx context.Context, mergedEnv []string) (map[string]interface{}, error) { envVars := map[string]string{ "LDFLAGS": "", } - for _, entry := range os.Environ() { + for _, entry := range mergedEnv { kv := strings.SplitN(entry, "=", 2) + if len(kv) != 2 { + return nil, fmt.Errorf("invalid entry in os.Environ %q", entry) + } envVars[kv[0]] = kv[1] } - return map[string]interface{}{ - "Env": envVars, + // Get the go environment. + goEnv, err := goenv(ctx) + if err != nil { + return nil, err } + + // Override go env with any matching values from the merged variables. + for k, v := range envVars { + if _, ok := goEnv[k]; ok { + goEnv[k] = v + } + } + + return map[string]interface{}{ + "Env": envVars, + "GoEnv": goEnv, + }, nil } func applyTemplating(list []string, data map[string]interface{}) ([]string, error) { @@ -741,10 +797,13 @@ func applyTemplating(list []string, data map[string]interface{}) ([]string, erro return result, nil } -func createBuildArgs(buildCfg Config) ([]string, error) { +func createBuildArgs(ctx context.Context, mergedEnv []string, buildCfg Config) ([]string, error) { var args []string - data := createTemplateData() + data, err := createTemplateData(ctx, mergedEnv) + if err != nil { + return nil, err + } if len(buildCfg.Flags) > 0 { flags, err := applyTemplating(buildCfg.Flags, data) @@ -1063,7 +1122,7 @@ func (g *gobuild) buildAll(ctx context.Context, ref string, baseRef name.Referen return nil, err } - matches := []v1.Descriptor{} + var matches []v1.Descriptor for _, desc := range im.Manifests { // Nested index is pretty rare. We could support this in theory, but return an error for now. if desc.MediaType != types.OCIManifestSchema1 && desc.MediaType != types.DockerManifestSchema2 { @@ -1188,7 +1247,7 @@ func parseSpec(spec []string) (*platformMatcher, error) { return &platformMatcher{spec: spec}, nil } - platforms := []v1.Platform{} + var platforms []v1.Platform for _, s := range spec { p, err := v1.ParsePlatform(s) if err != nil { diff --git a/pkg/build/gobuild_test.go b/pkg/build/gobuild_test.go index 923a9bd3d2..b5eb4fbb5b 100644 --- a/pkg/build/gobuild_test.go +++ b/pkg/build/gobuild_test.go @@ -297,6 +297,100 @@ func TestBuildEnv(t *testing.T) { } } +func TestGoEnv(t *testing.T) { + goVars, err := goenv(context.TODO()) + if err != nil { + t.Fatalf("unexpected error running goenv(): %v", err) + } + + // Just check some basic values. + if goVars["GOOS"] != runtime.GOOS { + t.Fatalf("goenv(): invalid GOOS value: '%s', expected: '%s'", goVars["GOOS"], runtime.GOOS) + } + if goVars["GOARCH"] != runtime.GOARCH { + t.Fatalf("goenv(): invalid GOARCH value: '%s', expected: '%s'", goVars["GOARCH"], runtime.GOARCH) + } +} + +func TestCreateTemplateData(t *testing.T) { + tests := []struct { + name string + mergedEnv []string + expectError bool + expectedVars map[string]string + expectedGoVars map[string]string + }{ + { + name: "bad env", + mergedEnv: []string{"bad"}, + expectError: true, + }, + { + name: "no env vars", + expectedVars: map[string]string{ + "LDFLAGS": "", + }, + expectedGoVars: map[string]string{ + "GOOS": runtime.GOOS, + "GOARCH": runtime.GOARCH, + }, + }, + { + name: "env vars", + mergedEnv: []string{"foo=bar", "bar=baz"}, + expectedVars: map[string]string{ + "LDFLAGS": "", + "foo": "bar", + "bar": "baz", + }, + expectedGoVars: map[string]string{ + "GOOS": runtime.GOOS, + "GOARCH": runtime.GOARCH, + }, + }, + { + name: "go env vars", + mergedEnv: []string{"GOOS=testgoos", "GOARCH=testgoarch"}, + expectedVars: map[string]string{ + "LDFLAGS": "", + "GOOS": "testgoos", + "GOARCH": "testgoarch", + }, + expectedGoVars: map[string]string{ + "GOOS": "testgoos", + "GOARCH": "testgoarch", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, err := createTemplateData(context.TODO(), test.mergedEnv) + if test.expectError { + if err == nil { + t.Fatalf("expected an error") + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + actualVars := actual["Env"].(map[string]string) + actualGoVars := actual["GoEnv"].(map[string]string) + for k, v := range test.expectedVars { + if actualVars[k] != v { + t.Fatalf("expected env var %s=%s, got %s", k, v, actual[k]) + } + } + for k, v := range test.expectedGoVars { + if actualGoVars[k] != v { + t.Fatalf("expected go env var %s=%s, got %s", k, v, actualGoVars[k]) + } + } + } + }) + } +} + func TestBuildConfig(t *testing.T) { tests := []struct { description string