-
Notifications
You must be signed in to change notification settings - Fork 20
/
builder.go
302 lines (264 loc) · 9.47 KB
/
builder.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
// Copyright 2020 Matthew Holt
//
// 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 xk6
import (
"context"
"fmt"
"log"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
"github.com/Masterminds/semver/v3"
)
// Builder can produce a custom k6 build with the
// configuration it represents.
type Builder struct {
Compile
K6Repo string `json:"k6_repo,omitempty"`
K6Version string `json:"k6_version,omitempty"`
Extensions []Dependency `json:"extensions,omitempty"`
Replacements []Replace `json:"replacements,omitempty"`
TimeoutGet time.Duration `json:"timeout_get,omitempty"`
TimeoutBuild time.Duration `json:"timeout_build,omitempty"`
RaceDetector bool `json:"race_detector,omitempty"`
SkipCleanup bool `json:"skip_cleanup,omitempty"`
}
// Build builds k6 at the configured version with the
// configured extensions and writes a binary at outputFile.
func (b Builder) Build(ctx context.Context, outputFile string) error {
if outputFile == "" {
return fmt.Errorf("output file path is required")
}
// the user's specified output file might be relative, and
// because the `go build` command is executed in a different,
// temporary folder, we convert the user's input to an
// absolute path so it goes the expected place
absOutputFile, err := filepath.Abs(outputFile)
if err != nil {
return err
}
// set some defaults from the environment, if applicable
if b.OS == "" {
b.OS = os.Getenv("GOOS")
}
if b.Arch == "" {
b.Arch = os.Getenv("GOARCH")
}
if b.ARM == "" {
b.ARM = os.Getenv("GOARM")
}
// prepare the build environment
buildEnv, err := b.newEnvironment(ctx)
if err != nil {
return err
}
defer buildEnv.Close()
// prepare the environment for the go command; for
// the most part we want it to inherit our current
// environment, with a few customizations
env := os.Environ()
env = setEnv(env, "GOOS="+b.OS)
env = setEnv(env, "GOARCH="+b.Arch)
env = setEnv(env, "GOARM="+b.ARM)
raceArg := "-race"
// trim debug symbols by default
buildFlags := b.osEnvOrDefaultValue("XK6_BUILD_FLAGS", "-ldflags='-w -s' -trimpath")
buildFlagsSlice := buildCommandArgs(buildFlags, absOutputFile)
if (b.RaceDetector || strings.Contains(buildFlags, raceArg)) && !b.Compile.Cgo {
log.Println("[WARNING] Enabling cgo because it is required by the race detector")
b.Compile.Cgo = true
}
env = setEnv(env, fmt.Sprintf("CGO_ENABLED=%s", b.Compile.CgoEnabled()))
log.Println("[INFO] Building k6")
if err := buildEnv.execGoModTidy(ctx); err != nil {
return err
}
// compile
cmd := buildEnv.newCommand("go",
buildFlagsSlice...,
)
// dont add raceArg again if it already in place
if b.RaceDetector && !strings.Contains(buildFlags, raceArg) {
cmd.Args = append(cmd.Args, raceArg)
}
cmd.Env = env
err = buildEnv.runCommand(ctx, cmd, b.TimeoutBuild)
if err != nil {
return err
}
log.Printf("[INFO] Build complete: %s", outputFile)
return nil
}
// setEnv sets an environment variable-value pair in
// env, overriding an existing variable if it already
// exists. The env slice is one such as is returned
// by os.Environ(), and set must also have the form
// of key=value.
func setEnv(env []string, set string) []string {
parts := strings.SplitN(set, "=", 2)
key := parts[0]
for i := 0; i < len(env); i++ {
if strings.HasPrefix(env[i], key+"=") {
env[i] = set
return env
}
}
return append(env, set)
}
// Dependency pairs a Go module path with a version.
type Dependency struct {
// The name (import path) of the Go package. If at a version > 1,
// it should contain semantic import version (i.e. "/v2").
// Used with `go get`.
PackagePath string `json:"package_path,omitempty"`
// The version of the Go module, as used with `go get`.
Version string `json:"version,omitempty"`
}
// ReplacementPath represents an old or new path component
// within a Go module replacement directive.
type ReplacementPath string
// Param reformats a go.mod replace directive to be
// compatible with the `go mod edit` command.
func (r ReplacementPath) Param() string {
return strings.Replace(string(r), " ", "@", 1)
}
func (r ReplacementPath) String() string { return string(r) }
// Replace represents a Go module replacement.
type Replace struct {
// The import path of the module being replaced.
Old ReplacementPath `json:"old,omitempty"`
// The path to the replacement module.
New ReplacementPath `json:"new,omitempty"`
}
// NewReplace creates a new instance of Replace provided old and
// new Go module paths
func NewReplace(old, new string) Replace {
return Replace{
Old: ReplacementPath(old),
New: ReplacementPath(new),
}
}
// newTempFolder creates a new folder in a temporary location.
// It is the caller's responsibility to remove the folder when finished.
func newTempFolder() (string, error) {
var parentDir string
if runtime.GOOS == "darwin" {
// After upgrading to macOS High Sierra, Caddy builds mysteriously
// started missing the embedded version information that -ldflags
// was supposed to produce. But it only happened on macOS after
// upgrading to High Sierra, and it didn't happen with the usual
// `go run build.go` -- only when using a buildenv. Bug in git?
// Nope. Not a bug in Go 1.10 either. Turns out it's a breaking
// change in macOS High Sierra. When the GOPATH of the buildenv
// was set to some other folder, like in the $HOME dir, it worked
// fine. Only within $TMPDIR it broke. The $TMPDIR folder is inside
// /var, which is symlinked to /private/var, which is mounted
// with noexec. I don't understand why, but evidently that
// makes -ldflags of `go build` not work. Bizarre.
// The solution, I guess, is to just use our own "temp" dir
// outside of /var. Sigh... as long as it still gets cleaned up,
// I guess it doesn't matter too much.
// See: https://github.com/caddyserver/caddy/issues/2036
// and https://twitter.com/mholt6/status/978345803365273600 (thread)
// (using an absolute path prevents problems later when removing this
// folder if the CWD changes)
var err error
parentDir, err = filepath.Abs(".")
if err != nil {
return "", err
}
}
ts := time.Now().Format(yearMonthDayHourMin)
return os.MkdirTemp(parentDir, fmt.Sprintf("buildenv_%s.", ts))
}
// versionedModulePath helps enforce Go Module's Semantic Import Versioning (SIV) by
// returning the form of modulePath with the major component of moduleVersion added,
// if > 1. For example, inputs of "foo" and "v1.0.0" will return "foo", but inputs
// of "foo" and "v2.0.0" will return "foo/v2", for use in Go imports and go commands.
// Inputs that conflict, like "foo/v2" and "v3.1.0" are an error. This function
// returns the input if the moduleVersion is not a valid semantic version string.
// If moduleVersion is empty string, the input modulePath is returned without error.
func versionedModulePath(modulePath, moduleVersion string) (string, error) {
if moduleVersion == "" {
return modulePath, nil
}
ver, err := semver.StrictNewVersion(strings.TrimPrefix(moduleVersion, "v"))
if err != nil {
// only return the error if we know they were trying to use a semantic version
// (could have been a commit SHA or something)
if strings.HasPrefix(moduleVersion, "v") {
return "", fmt.Errorf("%s: %v", moduleVersion, err)
}
return modulePath, nil
}
major := ver.Major()
// see if the module path has a major version at the end (SIV)
matches := moduleVersionRegexp.FindStringSubmatch(modulePath)
if len(matches) == 2 {
modPathVer, err := strconv.Atoi(matches[1])
if err != nil {
return "", fmt.Errorf("this error should be impossible, but module path %s has bad version: %v", modulePath, err)
}
if modPathVer != int(major) {
return "", fmt.Errorf("versioned module path (%s) and requested module major version (%d) diverge", modulePath, major)
}
} else if major > 1 {
modulePath += fmt.Sprintf("/v%d", major)
}
return path.Clean(modulePath), nil
}
var moduleVersionRegexp = regexp.MustCompile(`.+/v(\d+)$`)
const (
// yearMonthDayHourMin is the date format
// used for temporary folder paths.
yearMonthDayHourMin = "2006-01-02-1504"
defaultK6ModulePath = "go.k6.io/k6"
)
func (b Builder) osEnvOrDefaultValue(name, defaultValue string) string {
s, ok := os.LookupEnv(name)
if !ok {
return defaultValue
}
return s
}
// buildCommandArgs parses the build flags passed by environment variable XK6_BUILD_FLAGS
// or the default values when no value for it is given
// so we may pass args separately to newCommand()
func buildCommandArgs(buildFlags, absOutputFile string) []string {
buildFlagsSlice := make([]string, 0, 10)
buildFlagsSlice = append(buildFlagsSlice, "build", "-o", absOutputFile)
tmp := []string{}
sb := &strings.Builder{}
quoted := false
for _, r := range buildFlags {
if r == '"' || r == '\'' {
quoted = !quoted
} else if !quoted && r == ' ' {
tmp = append(tmp, sb.String())
sb.Reset()
} else {
sb.WriteRune(r)
}
}
if sb.Len() > 0 {
tmp = append(tmp, sb.String())
}
buildFlagsSlice = append(buildFlagsSlice, tmp...)
return buildFlagsSlice
}