From 7dda3afec733d525f0dfcf9cba5776647e5e4148 Mon Sep 17 00:00:00 2001 From: laurentsimon <64505099+laurentsimon@users.noreply.github.com> Date: Fri, 10 Jun 2022 08:27:04 -0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20Version=20variable=20(#225)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * updates * updates * updates * updates * updates * updates * updates --- internal/builders/go/README.md | 2 +- internal/builders/go/pkg/build.go | 84 +++++++++++----- internal/builders/go/pkg/build_test.go | 134 ++++++++++++++++++++++++- 3 files changed, 189 insertions(+), 31 deletions(-) diff --git a/internal/builders/go/README.md b/internal/builders/go/README.md index b5b6b17c0..2f626d875 100644 --- a/internal/builders/go/README.md +++ b/internal/builders/go/README.md @@ -73,7 +73,7 @@ If you are already using Goreleaser, you may be able to migrate to our builder u In the meantime, you can use both Goreleaser and this builder in the same repository. For example, you can pick one build you would like to start generating provenance for. Goreleaser and this builder can co-exist without interfering with one another, so long as they build fr different OS/Arch. We think gradual adoption is good for project to get used to SLSA. -The configuration file accepts many of the common fields Goreleaser uses, as you can see in the [example](#configuration-file). The configuration file also supports two variables: `{{ .Os }}` and `{{ .Arch }}`. If you need suppport for other variables, please [open an issue](https://github.com/slsa-framework/slsa-github-generator/issues/new). +The configuration file accepts many of the common fields Goreleaser uses, as you can see in the [example](#configuration-file). The configuration file also supports two variables: `{{ .Os }}`, `{{ .Arch }}` and `{{ .Version }}`. If you need suppport for other variables, please [open an issue](https://github.com/slsa-framework/slsa-github-generator/issues/new). ### Workflow inputs diff --git a/internal/builders/go/pkg/build.go b/internal/builders/go/pkg/build.go index 6dde39f4e..7c03054c1 100644 --- a/internal/builders/go/pkg/build.go +++ b/internal/builders/go/pkg/build.go @@ -19,6 +19,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" "syscall" ) @@ -31,6 +32,8 @@ var ( errorInvalidFilename = errors.New("invalid filename") ) +var unknownVersion = "unknown" + // See `go build help`. // `-asmflags`, `-n`, `-mod`, `-installsuffix`, `-modfile`, // `-workfile`, `-overlay`, `-pkgdir`, `-toolexec`, `-o`, @@ -272,22 +275,30 @@ func (b *GoBuild) SetArgEnvVariables(envs string) error { } func (b *GoBuild) generateOutputFilename() (string, error) { - const alpha = "abcdefghijklmnopqrstuvwxyz1234567890-_" + // Note: the `.` is needed to accomodate the semantic version + // as part of the name. + const alpha = ".abcdefghijklmnopqrstuvwxyz1234567890-_" var name string // Replace .Os variable. if strings.Contains(b.cfg.Binary, "{{ .Os }}") && b.cfg.Goos == "" { - return "", fmt.Errorf("%w", errorEnvVariableNameEmpty) + return "", fmt.Errorf("%w: {{ .Os }}", errorEnvVariableNameEmpty) } name = strings.ReplaceAll(b.cfg.Binary, "{{ .Os }}", b.cfg.Goos) // Replace .Arch variable. if strings.Contains(name, "{{ .Arch }}") && b.cfg.Goarch == "" { - return "", fmt.Errorf("%w", errorEnvVariableNameEmpty) + return "", fmt.Errorf("%w: {{ .Arch }}", errorEnvVariableNameEmpty) } name = strings.ReplaceAll(name, "{{ .Arch }}", b.cfg.Goarch) + // Resolve other variables. + if strings.Contains(name, "{{ .Version }}") { + version := getVersion() + name = strings.ReplaceAll(name, "{{ .Version }}", version) + } + for _, char := range name { if !strings.Contains(alpha, strings.ToLower(string(char))) { return "", fmt.Errorf("%w: found character '%c'", errorInvalidFilename, char) @@ -297,6 +308,12 @@ func (b *GoBuild) generateOutputFilename() (string, error) { if name == "" { return "", fmt.Errorf("%w: filename is empty", errorInvalidFilename) } + + // Validate the path, since we allow '.' in the name. + if err := validatePath(name); err != nil { + return "", err + } + return name, nil } @@ -338,43 +355,56 @@ func isAllowedEnvVariable(name string) bool { // TODO: maybe not needed if handled directly by go compiler. func (b *GoBuild) generateLdflags() (string, error) { var a []string + reVar := regexp.MustCompile(`{{ \.([A-Z][a-z]*) }}`) + reDyn := regexp.MustCompile(`{{ \.Env\.(\w+) }}`) + // Resolve variables. for _, v := range b.cfg.Ldflags { - var res string - ss := "{{ .Env." - es := "}}" - found := false - for { - start := strings.Index(v, ss) - if start == -1 { - break - } - end := strings.Index(string(v[start+len(ss):]), es) - if end == -1 { - return "", fmt.Errorf("%w: %s", errorInvalidEnvArgument, v) - } - name := strings.Trim(string(v[start+len(ss):start+len(ss)+end]), " ") - if name == "" { - return "", fmt.Errorf("%w: %s", errorEnvVariableNameEmpty, v) + // Special variables. + names := reVar.FindAllString(v, -1) + for _, n := range names { + + name := strings.ReplaceAll(n, "{{ .", "") + name = strings.ReplaceAll(name, " }}", "") + + // {{ .Version }} variable. + if name != "Version" { + return "", fmt.Errorf("%w: %s", errorInvalidEnvArgument, n) } + version := getVersion() + v = strings.ReplaceAll(v, n, version) + } + + // Dynamic env variables provided by caller. + names = reDyn.FindAllString(v, -1) + for _, n := range names { + + name := strings.ReplaceAll(n, "{{ .Env.", "") + name = strings.ReplaceAll(name, " }}", "") + val, exists := b.argEnv[name] if !exists { - return "", fmt.Errorf("%w: %s", errorEnvVariableNameEmpty, name) + return "", fmt.Errorf("%w: %s", errorEnvVariableNameEmpty, n) } - res = fmt.Sprintf("%s%s%s", res, v[:start], val) - found = true - v = v[start+len(ss)+end+len(es):] - } - if !found { - res = v + v = strings.ReplaceAll(v, n, val) } - a = append(a, res) + + a = append(a, v) } + if len(a) > 0 { return strings.Join(a, " "), nil } return "", nil } + +func getVersion() string { + version := os.Getenv("GITHUB_REF_NAME") + if version == "" { + return unknownVersion + } + return version +} diff --git a/internal/builders/go/pkg/build_test.go b/internal/builders/go/pkg/build_test.go index 40c621220..17513783e 100644 --- a/internal/builders/go/pkg/build_test.go +++ b/internal/builders/go/pkg/build_test.go @@ -180,7 +180,7 @@ func Test_generateOutputFilename(t *testing.T) { filename string goos string goarch string - envs string + envs map[string]string expected struct { err error fn string @@ -305,12 +305,89 @@ func Test_generateOutputFilename(t *testing.T) { err: errorInvalidFilename, }, }, + { + name: "filename amd64/linux v1.2.3", + filename: "name-{{ .Os }}-{{ .Arch }}-{{ .Version }}", + goarch: "amd64", + goos: "linux", + envs: map[string]string{ + "GITHUB_REF_NAME": "v1.2.3", + }, + expected: struct { + err error + fn string + }{ + err: nil, + fn: "name-linux-amd64-v1.2.3", + }, + }, + { + name: "filename twice v1.2.3", + filename: "name-{{ .Version }}-{{ .Version }}", + goarch: "amd64", + goos: "linux", + envs: map[string]string{ + "GITHUB_REF_NAME": "v1.2.3", + }, + expected: struct { + err error + fn string + }{ + err: nil, + fn: "name-v1.2.3-v1.2.3", + }, + }, + { + name: "filename twice empty versions", + filename: "name-{{ .Version }}-{{ .Version }}", + goarch: "amd64", + goos: "linux", + envs: map[string]string{ + "GITHUB_REF_NAME": "", + }, + expected: struct { + err error + fn string + }{ + err: nil, + fn: fmt.Sprintf("name-%s-%s", unknownVersion, unknownVersion), + }, + }, + { + name: "invalid name with version", + filename: "name-{{ .Version }}/../bla", + goarch: "amd64", + goos: "linux", + envs: map[string]string{ + "GITHUB_REF_NAME": "v1.2.3", + }, + expected: struct { + err error + fn string + }{ + err: errorInvalidFilename, + }, + }, + { + name: "filename twice unset versions", + filename: "name-{{ .Version }}-{{ .Version }}", + goarch: "amd64", + goos: "linux", + expected: struct { + err error + fn string + }{ + err: nil, + fn: fmt.Sprintf("name-%s-%s", unknownVersion, unknownVersion), + }, + }, } for _, tt := range tests { tt := tt // Re-initializing variable so it is not changed while executing the closure below t.Run(tt.name, func(t *testing.T) { - t.Parallel() + // Note: disable parallelism to avoid env variable clobbering between tests. + // t.Parallel() cfg := goReleaserConfigFile{ Binary: tt.filename, @@ -322,6 +399,12 @@ func Test_generateOutputFilename(t *testing.T) { if err != nil { t.Errorf("fromConfig: %v", err) } + + // Set env variables. + for k, v := range tt.envs { + os.Setenv(k, v) + } + b := GoBuildNew("go compiler", c) fn, err := b.generateOutputFilename() @@ -329,6 +412,12 @@ func Test_generateOutputFilename(t *testing.T) { t.Errorf(cmp.Diff(err, tt.expected.err)) } + // Unset env variables, so that they don't + // affect other tests. + for k := range tt.envs { + os.Unsetenv(k) + } + if err != nil { return } @@ -588,6 +677,7 @@ func Test_generateLdflags(t *testing.T) { name string argEnv string inldflags []string + githubEnv map[string]string err error outldflags string }{ @@ -662,12 +752,38 @@ func Test_generateLdflags(t *testing.T) { }, outldflags: "value1-name-value2 value1-name-value3 value3-name-value1 value3-name-value2", }, + { + name: "several ldflags with start/end", + argEnv: "VAR1:value1, VAR2:value2, VAR3:value3", + inldflags: []string{ + "start-{{ .Env.VAR1 }}-name-{{ .Env.VAR2 }}-end", + "start-{{ .Env.VAR1 }}-name-{{ .Env.VAR3 }}-end", + "start-{{ .Env.VAR3 }}-name-{{ .Env.VAR1 }}-end", + "start-{{ .Env.VAR3 }}-name-{{ .Env.VAR2 }}-end", + }, + outldflags: "start-value1-name-value2-end start-value1-name-value3-end start-value3-name-value1-end start-value3-name-value2-end", + }, + { + name: "several ldflags and version", + argEnv: "VAR1:value1, VAR2:value2, VAR3:value3", + githubEnv: map[string]string{ + "GITHUB_REF_NAME": "v1.2.3", + }, + inldflags: []string{ + "start-{{ .Env.VAR1 }}-name-{{ .Env.VAR2 }}-{{ .Version }}-end", + "{{ .Env.VAR1 }}-name-{{ .Env.VAR3 }}", + "{{ .Env.VAR3 }}-name-{{ .Env.VAR1 }}-{{ .Version }}-{{ .Version }}", + "{{ .Env.VAR3 }}-name-{{ .Env.VAR2 }}-{{ .Version }}-end", + }, + outldflags: "start-value1-name-value2-v1.2.3-end value1-name-value3 value3-name-value1-v1.2.3-v1.2.3 value3-name-value2-v1.2.3-end", + }, } for _, tt := range tests { tt := tt // Re-initializing variable so it is not changed while executing the closure below t.Run(tt.name, func(t *testing.T) { - t.Parallel() + // Disable to avoid env clobbering between tests. + // t.Parallel() cfg := goReleaserConfigFile{ Version: 1, @@ -677,6 +793,12 @@ func Test_generateLdflags(t *testing.T) { if err != nil { t.Errorf("fromConfig: %v", err) } + + // Set GitHub env variables. + for k, v := range tt.githubEnv { + os.Setenv(k, v) + } + b := GoBuildNew("go compiler", c) err = b.SetArgEnvVariables(tt.argEnv) @@ -685,6 +807,12 @@ func Test_generateLdflags(t *testing.T) { } ldflags, err := b.generateLdflags() + // Unset env variables, so that they don't + // affect other tests. + for k := range tt.githubEnv { + os.Unsetenv(k) + } + if !errCmp(err, tt.err) { t.Errorf(cmp.Diff(err, tt.err)) }