mirror of
https://github.com/caddyserver/xcaddy.git
synced 2024-09-07 13:09:15 +01:00
edc1f41778
* 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.
313 lines
8.4 KiB
Go
313 lines
8.4 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 (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/caddyserver/xcaddy/internal/utils"
|
|
"github.com/google/shlex"
|
|
)
|
|
|
|
func (b Builder) newEnvironment(ctx context.Context) (*environment, error) {
|
|
// assume Caddy v2 if no semantic version is provided
|
|
caddyModulePath := defaultCaddyModulePath
|
|
if !strings.HasPrefix(b.CaddyVersion, "v") || !strings.Contains(b.CaddyVersion, ".") {
|
|
caddyModulePath += "/v2"
|
|
}
|
|
caddyModulePath, err := versionedModulePath(caddyModulePath, b.CaddyVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// clean up any SIV-incompatible module paths real quick
|
|
for i, p := range b.Plugins {
|
|
b.Plugins[i].PackagePath, err = versionedModulePath(p.PackagePath, p.Version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// create the context for the main module template
|
|
tplCtx := goModTemplateContext{
|
|
CaddyModule: caddyModulePath,
|
|
}
|
|
for _, p := range b.Plugins {
|
|
tplCtx.Plugins = append(tplCtx.Plugins, p.PackagePath)
|
|
}
|
|
|
|
// evaluate the template for the main module
|
|
var buf bytes.Buffer
|
|
tpl, err := template.New("main").Parse(mainModuleTemplate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = tpl.Execute(&buf, tplCtx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// create the folder in which the build environment will operate
|
|
tempFolder, err := newTempFolder()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
err2 := os.RemoveAll(tempFolder)
|
|
if err2 != nil {
|
|
err = fmt.Errorf("%w; additionally, cleaning up folder: %v", err, err2)
|
|
}
|
|
}
|
|
}()
|
|
log.Printf("[INFO] Temporary folder: %s", tempFolder)
|
|
|
|
// write the main module file to temporary folder
|
|
mainPath := filepath.Join(tempFolder, "main.go")
|
|
log.Printf("[INFO] Writing main module: %s\n%s", mainPath, buf.Bytes())
|
|
err = ioutil.WriteFile(mainPath, buf.Bytes(), 0644)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
env := &environment{
|
|
caddyVersion: b.CaddyVersion,
|
|
plugins: b.Plugins,
|
|
caddyModulePath: caddyModulePath,
|
|
tempFolder: tempFolder,
|
|
timeoutGoGet: b.TimeoutGet,
|
|
skipCleanup: b.SkipCleanup,
|
|
buildFlags: b.BuildFlags,
|
|
}
|
|
|
|
// initialize the go module
|
|
log.Println("[INFO] Initializing Go module")
|
|
cmd := env.newGoCommand("mod", "init")
|
|
cmd.Args = append(cmd.Args, "caddy")
|
|
err = env.runCommand(ctx, cmd, 10*time.Second)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// specify module replacements before pinning versions
|
|
replaced := make(map[string]string)
|
|
for _, r := range b.Replacements {
|
|
log.Printf("[INFO] Replace %s => %s", r.Old.String(), r.New.String())
|
|
cmd := env.newGoCommand("mod", "edit",
|
|
"-replace", fmt.Sprintf("%s=%s", r.Old.Param(), r.New.Param()))
|
|
err := env.runCommand(ctx, cmd, 10*time.Second)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
replaced[r.Old.String()] = r.New.String()
|
|
}
|
|
|
|
// check for early abort
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
// pin versions by populating go.mod, first for Caddy itself and then plugins
|
|
log.Println("[INFO] Pinning versions")
|
|
err = env.execGoGet(ctx, caddyModulePath, env.caddyVersion, "", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextPlugin:
|
|
for _, p := range b.Plugins {
|
|
// if module is locally available, do not "go get" it;
|
|
// also note that we iterate and check prefixes, because
|
|
// a plugin package may be a subfolder of a module, i.e.
|
|
// foo/a/plugin is within module foo/a.
|
|
for repl := range replaced {
|
|
if strings.HasPrefix(p.PackagePath, repl) {
|
|
continue nextPlugin
|
|
}
|
|
}
|
|
// also pass the Caddy version to prevent it from being upgraded
|
|
err = env.execGoGet(ctx, p.PackagePath, p.Version, caddyModulePath, env.caddyVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// check for early abort
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
}
|
|
|
|
// doing an empty "go get -d" can potentially resolve some
|
|
// ambiguities introduced by one of the plugins;
|
|
// see https://github.com/caddyserver/xcaddy/pull/92
|
|
err = env.execGoGet(ctx, "", "", "", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Println("[INFO] Build environment ready")
|
|
return env, nil
|
|
}
|
|
|
|
type environment struct {
|
|
caddyVersion string
|
|
plugins []Dependency
|
|
caddyModulePath string
|
|
tempFolder string
|
|
timeoutGoGet time.Duration
|
|
skipCleanup bool
|
|
buildFlags string
|
|
}
|
|
|
|
// Close cleans up the build environment, including deleting
|
|
// the temporary folder from the disk.
|
|
func (env environment) Close() error {
|
|
if env.skipCleanup {
|
|
log.Printf("[INFO] Skipping cleanup as requested; leaving folder intact: %s", env.tempFolder)
|
|
return nil
|
|
}
|
|
log.Printf("[INFO] Cleaning up temporary folder: %s", env.tempFolder)
|
|
return os.RemoveAll(env.tempFolder)
|
|
}
|
|
|
|
func (env environment) newGoCommand(args ...string) *exec.Cmd {
|
|
cmd := exec.Command(utils.GetGo(), args...)
|
|
cmd.Dir = env.tempFolder
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if env.buildFlags == "" {
|
|
return cmd
|
|
}
|
|
|
|
flags, err := shlex.Split(env.buildFlags)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Splitting arguments failed: %s", env.buildFlags)
|
|
return cmd
|
|
}
|
|
cmd.Args = append(cmd.Args, flags...)
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (env environment) runCommand(ctx context.Context, cmd *exec.Cmd, timeout time.Duration) error {
|
|
log.Printf("[INFO] exec (timeout=%s): %+v ", timeout, cmd)
|
|
|
|
if timeout > 0 {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
}
|
|
|
|
// start the command; if it fails to start, report error immediately
|
|
err := cmd.Start()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// wait for the command in a goroutine; the reason for this is
|
|
// very subtle: if, in our select, we do `case cmdErr := <-cmd.Wait()`,
|
|
// then that case would be chosen immediately, because cmd.Wait() is
|
|
// immediately available (even though it blocks for potentially a long
|
|
// time, it can be evaluated immediately). So we have to remove that
|
|
// evaluation from the `case` statement.
|
|
cmdErrChan := make(chan error)
|
|
go func() {
|
|
cmdErrChan <- cmd.Wait()
|
|
}()
|
|
|
|
// unblock either when the command finishes, or when the done
|
|
// channel is closed -- whichever comes first
|
|
select {
|
|
case cmdErr := <-cmdErrChan:
|
|
// process ended; report any error immediately
|
|
return cmdErr
|
|
case <-ctx.Done():
|
|
// context was canceled, either due to timeout or
|
|
// maybe a signal from higher up canceled the parent
|
|
// context; presumably, the OS also sent the signal
|
|
// to the child process, so wait for it to die
|
|
select {
|
|
case <-time.After(15 * time.Second):
|
|
_ = cmd.Process.Kill()
|
|
case <-cmdErrChan:
|
|
}
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
// execGoGet runs "go get -d -v" with the given module/version as an argument.
|
|
// Also allows passing in a second module/version pair, meant to be the main
|
|
// Caddy module/version we're building against; this will prevent the
|
|
// plugin module from causing the Caddy version to upgrade, if the plugin
|
|
// version requires a newer version of Caddy.
|
|
// See https://github.com/caddyserver/xcaddy/issues/54
|
|
func (env environment) execGoGet(ctx context.Context, modulePath, moduleVersion, caddyModulePath, caddyVersion string) error {
|
|
mod := modulePath
|
|
if moduleVersion != "" {
|
|
mod += "@" + moduleVersion
|
|
}
|
|
caddy := caddyModulePath
|
|
if caddyVersion != "" {
|
|
caddy += "@" + caddyVersion
|
|
}
|
|
|
|
cmd := env.newGoCommand("get", "-d", "-v")
|
|
// using an empty string as an additional argument to "go get"
|
|
// breaks the command since it treats the empty string as a
|
|
// distinct argument, so we're using an if statement to avoid it.
|
|
if caddy != "" {
|
|
cmd.Args = append(cmd.Args, mod, caddy)
|
|
} else {
|
|
cmd.Args = append(cmd.Args, mod)
|
|
}
|
|
|
|
return env.runCommand(ctx, cmd, env.timeoutGoGet)
|
|
}
|
|
|
|
type goModTemplateContext struct {
|
|
CaddyModule string
|
|
Plugins []string
|
|
}
|
|
|
|
const mainModuleTemplate = `package main
|
|
|
|
import (
|
|
caddycmd "{{.CaddyModule}}/cmd"
|
|
|
|
// plug in Caddy modules here
|
|
_ "{{.CaddyModule}}/modules/standard"
|
|
{{- range .Plugins}}
|
|
_ "{{.}}"
|
|
{{- end}}
|
|
)
|
|
|
|
func main() {
|
|
caddycmd.Main()
|
|
}
|
|
`
|