-
-
Notifications
You must be signed in to change notification settings - Fork 113
/
builder.go
320 lines (284 loc) · 10.1 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
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
// 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 xcaddy
import (
"bytes"
"context"
"fmt"
"log"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/caddyserver/xcaddy/internal/utils"
)
// Builder can produce a custom Caddy build with the
// configuration it represents.
type Builder struct {
Compile
CaddyVersion string `json:"caddy_version,omitempty"`
Plugins []Dependency `json:"plugins,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"`
SkipBuild bool `json:"skip_build,omitempty"`
Debug bool `json:"debug,omitempty"`
BuildFlags string `json:"build_flags,omitempty"`
ModFlags string `json:"mod_flags,omitempty"`
// Experimental: subject to change
EmbedDirs []struct {
Dir string `json:"dir,omitempty"`
Name string `json:"name,omitempty"`
} `json:"embed_dir,omitempty"`
}
// Build builds Caddy at the configured version with the
// configured plugins and plops down a binary at outputFile.
func (b Builder) Build(ctx context.Context, outputFile string) error {
var cancel context.CancelFunc
if b.TimeoutBuild > 0 {
ctx, cancel = context.WithTimeout(ctx, b.TimeoutBuild)
defer cancel()
}
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
}
log.Printf("[INFO] absolute output file path: %s", absOutputFile)
// set some defaults from the environment, if applicable
if b.OS == "" {
b.OS = utils.GetGOOS()
}
if b.Arch == "" {
b.Arch = utils.GetGOARCH()
}
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()
// generating windows resources for embedding
if b.OS == "windows" {
// get version string, we need to parse the output to get the exact version instead tag, branch or commit
cmd := buildEnv.newGoBuildCommand(ctx, "list", "-m", buildEnv.caddyModulePath)
var buffer bytes.Buffer
cmd.Stdout = &buffer
err = buildEnv.runCommand(ctx, cmd)
if err != nil {
return err
}
// output looks like: github.com/caddyserver/caddy/v2 v2.7.6
version := strings.TrimSpace(strings.TrimPrefix(buffer.String(), buildEnv.caddyModulePath))
err = utils.WindowsResource(version, outputFile, buildEnv.tempFolder)
if err != nil {
return err
}
}
if b.SkipBuild {
log.Printf("[INFO] Skipping build as requested")
return nil
}
// 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)
if b.RaceDetector && !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 Caddy")
// tidy the module to ensure go.mod and go.sum are consistent with the module prereq
tidyCmd := buildEnv.newGoModCommand(ctx, "tidy", "-e")
if err := buildEnv.runCommand(ctx, tidyCmd); err != nil {
return err
}
// compile
cmd := buildEnv.newGoBuildCommand(ctx, "build",
"-o", absOutputFile,
)
if b.Debug {
// support dlv
cmd.Args = append(cmd.Args, "-gcflags", "all=-N -l")
} else {
if buildEnv.buildFlags == "" {
cmd.Args = append(cmd.Args,
"-ldflags", "-w -s", // trim debug symbols
"-trimpath",
"-tags", "nobadger,nomysql,nopgx",
)
}
}
if b.RaceDetector {
cmd.Args = append(cmd.Args, "-race")
}
cmd.Env = env
err = buildEnv.runCommand(ctx, cmd)
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:"module_path,omitempty"`
// The version of the Go module, as used with `go get`.
Version string `json:"version,omitempty"`
}
func (d Dependency) String() string {
if d.Version != "" {
return d.PackagePath + "@" + d.Version
}
return d.PackagePath
}
// 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"
defaultCaddyModulePath = "github.com/caddyserver/caddy"
)