mirror of
https://github.com/caddyserver/xcaddy.git
synced 2024-09-07 13:09:15 +01:00
* Extend XCADDY_GO_BUILD_FLAGS usage (#102)
Commit 47f9ded5d8
is not sufficient alone to
ensure that all go modules are installed with write bit set because 'go mod'
or 'go get' might install modules as read-only, too.
If set, the environment variable is appended for the other go commands that
support build flags.
* Make running 'go' implicit for build environment
The method newCommand runs only go commands so let's make command 'go'
implicit and rename it to make it more verbose.
280 lines
8.9 KiB
Go
280 lines
8.9 KiB
Go
// 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 (
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
)
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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 {
|
|
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()
|
|
|
|
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.newGoCommand("mod", "tidy")
|
|
if err := buildEnv.runCommand(ctx, tidyCmd, b.TimeoutGet); err != nil {
|
|
return err
|
|
}
|
|
|
|
// compile
|
|
cmd := buildEnv.newGoCommand("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",
|
|
)
|
|
}
|
|
}
|
|
|
|
if b.RaceDetector {
|
|
cmd.Args = append(cmd.Args, "-race")
|
|
}
|
|
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:"module_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 ioutil.TempDir(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"
|
|
)
|