Fix build.go

This commit is contained in:
Alexander Neumann
2020-04-04 20:41:32 +02:00
parent 46fd57c36e
commit 9ae066589d
+144 -238
View File
@@ -1,9 +1,17 @@
// Description
//
// This program aims to make building Go programs for end users easier by just
// calling it with `go run`, without having to setup a GOPATH.
//
// This program needs Go >= 1.11. It'll use Go modules for compilation. It
// builds the package configured as Main in the Config struct.
// BSD 2-Clause License // BSD 2-Clause License
// //
// Copyright (c) 2016-2018, Alexander Neumann <alexander@bumpern.de> // Copyright (c) 2016-2018, Alexander Neumann <alexander@bumpern.de>
// All rights reserved. // All rights reserved.
// //
// This file has been copied from the repository at: // This file has been derived from the repository at:
// https://github.com/fd0/build-go // https://github.com/fd0/build-go
// //
// Redistribution and use in source and binary forms, with or without // Redistribution and use in source and binary forms, with or without
@@ -37,7 +45,6 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
@@ -46,163 +53,31 @@ import (
// config contains the configuration for the program to build. // config contains the configuration for the program to build.
var config = Config{ var config = Config{
Name: "rest-server", // name of the program executable and directory Name: "rest-server", // name of the program executable and directory
Namespace: "github.com/restic/rest-server", // subdir of GOPATH, e.g. "github.com/foo/bar" Namespace: "github.com/restic/rest-server", // subdir of GOPATH, e.g. "github.com/foo/bar"
Main: "github.com/restic/rest-server/cmd/rest-server", // package name for the main package Main: "github.com/restic/rest-server/cmd/rest-server", // package name for the main package
Tests: []string{ // tests to run Tests: []string{"./..."}, // tests to run
"github.com/restic/rest-server", MinVersion: GoVersion{Major: 1, Minor: 11, Patch: 0}, // minimum Go version supported
"github.com/restic/rest-server/cmd/rest-server",
},
MinVersion: GoVersion{Major: 1, Minor: 7, Patch: 0}, // minimum Go version supported
} }
// Config configures the build. // Config configures the build.
type Config struct { type Config struct {
Name string Name string
Namespace string Namespace string
Main string Main string
Tests []string DefaultBuildTags []string
MinVersion GoVersion Tests []string
MinVersion GoVersion
} }
var ( var (
verbose bool verbose bool
keepGopath bool runTests bool
runTests bool enableCGO bool
enableCGO bool enablePIE bool
goVersion = ParseGoVersion(runtime.Version())
) )
// specialDir returns true if the file begins with a special character ('.' or '_').
func specialDir(name string) bool {
if name == "." {
return false
}
base := filepath.Base(name)
if base == "vendor" || base[0] == '_' || base[0] == '.' {
return true
}
return false
}
// excludePath returns true if the file should not be copied to the new GOPATH.
func excludePath(name string) bool {
ext := path.Ext(name)
if ext == ".go" || ext == ".s" || ext == ".h" {
return false
}
parentDir := filepath.Base(filepath.Dir(name))
if parentDir == "testdata" {
return false
}
return true
}
// updateGopath builds a valid GOPATH at dst, with all Go files in src/ copied
// to dst/prefix/, so calling
//
// updateGopath("/tmp/gopath", "/home/u/restic", "github.com/restic/restic")
//
// with "/home/u/restic" containing the file "foo.go" yields the following tree
// at "/tmp/gopath":
//
// /tmp/gopath
// └── src
// └── github.com
// └── restic
// └── restic
// └── foo.go
func updateGopath(dst, src, prefix string) error {
verbosePrintf("copy contents of %v to %v\n", src, filepath.Join(dst, prefix))
return filepath.Walk(src, func(name string, fi os.FileInfo, err error) error {
if name == src {
return err
}
if specialDir(name) {
if fi.IsDir() {
return filepath.SkipDir
}
return nil
}
if err != nil {
return err
}
if fi.IsDir() {
return nil
}
if excludePath(name) {
return nil
}
intermediatePath, err := filepath.Rel(src, name)
if err != nil {
return err
}
fileSrc := filepath.Join(src, intermediatePath)
fileDst := filepath.Join(dst, "src", prefix, intermediatePath)
return copyFile(fileDst, fileSrc)
})
}
func directoryExists(dirname string) bool {
stat, err := os.Stat(dirname)
if err != nil && os.IsNotExist(err) {
return false
}
return stat.IsDir()
}
// copyFile creates dst from src, preserving file attributes and timestamps.
func copyFile(dst, src string) error {
fi, err := os.Stat(src)
if err != nil {
return err
}
fsrc, err := os.Open(src)
if err != nil {
return err
}
if err = os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return err
}
fdst, err := os.Create(dst)
if err != nil {
return err
}
if _, err = io.Copy(fdst, fsrc); err != nil {
return err
}
if err = fsrc.Close(); err != nil {
return err
}
if err = fdst.Close(); err != nil {
return err
}
if err = os.Chmod(dst, fi.Mode()); err != nil {
return err
}
return os.Chtimes(dst, fi.ModTime(), fi.ModTime())
}
// die prints the message with fmt.Fprintf() to stderr and exits with an error // die prints the message with fmt.Fprintf() to stderr and exits with an error
// code. // code.
func die(message string, args ...interface{}) { func die(message string, args ...interface{}) {
@@ -216,12 +91,13 @@ func showUsage(output io.Writer) {
fmt.Fprintf(output, "OPTIONS:\n") fmt.Fprintf(output, "OPTIONS:\n")
fmt.Fprintf(output, " -v --verbose output more messages\n") fmt.Fprintf(output, " -v --verbose output more messages\n")
fmt.Fprintf(output, " -t --tags specify additional build tags\n") fmt.Fprintf(output, " -t --tags specify additional build tags\n")
fmt.Fprintf(output, " -k --keep-gopath do not remove the GOPATH after build\n")
fmt.Fprintf(output, " -T --test run tests\n") fmt.Fprintf(output, " -T --test run tests\n")
fmt.Fprintf(output, " -o --output set output file name\n") fmt.Fprintf(output, " -o --output set output file name\n")
fmt.Fprintf(output, " --enable-cgo use CGO to link against libc\n") fmt.Fprintf(output, " --enable-cgo use CGO to link against libc\n")
fmt.Fprintf(output, " --enable-pie use PIE buildmode\n")
fmt.Fprintf(output, " --goos value set GOOS for cross-compilation\n") fmt.Fprintf(output, " --goos value set GOOS for cross-compilation\n")
fmt.Fprintf(output, " --goarch value set GOARCH for cross-compilation\n") fmt.Fprintf(output, " --goarch value set GOARCH for cross-compilation\n")
fmt.Fprintf(output, " --goarm value set GOARM for cross-compilation\n")
} }
func verbosePrintf(message string, args ...interface{}) { func verbosePrintf(message string, args ...interface{}) {
@@ -232,49 +108,77 @@ func verbosePrintf(message string, args ...interface{}) {
fmt.Printf("build: "+message, args...) fmt.Printf("build: "+message, args...)
} }
// cleanEnv returns a clean environment with GOPATH and GOBIN removed (if // printEnv prints Go-relevant environment variables in a nice way using verbosePrintf.
// present). func printEnv(env []string) {
func cleanEnv() (env []string) { verbosePrintf("environment (GO*):\n")
for _, v := range os.Environ() { for _, v := range env {
if strings.HasPrefix(v, "GOPATH=") || strings.HasPrefix(v, "GOBIN=") { // ignore environment variables which do not start with GO*.
if !strings.HasPrefix(v, "GO") {
continue continue
} }
verbosePrintf(" %s\n", v)
env = append(env, v)
} }
return env
} }
// build runs "go build args..." with GOPATH set to gopath. // build runs "go build args..." with GOPATH set to gopath.
func build(cwd, goos, goarch, gopath string, args ...string) error { func build(cwd string, env map[string]string, args ...string) error {
a := []string{"build"} a := []string{"build"}
a = append(a, "-asmflags", fmt.Sprintf("-trimpath=%s", gopath))
a = append(a, "-gcflags", fmt.Sprintf("-trimpath=%s", gopath)) // try to remove all absolute paths from resulting binary
if goVersion.AtLeast(GoVersion{1, 13, 0}) {
// use the new flag introduced by Go 1.13
a = append(a, "-trimpath")
} else {
// otherwise try to trim as many paths as possible
a = append(a, "-asmflags", fmt.Sprintf("all=-trimpath=%s", cwd))
a = append(a, "-gcflags", fmt.Sprintf("all=-trimpath=%s", cwd))
}
if enablePIE {
a = append(a, "-buildmode=pie")
}
a = append(a, args...) a = append(a, args...)
cmd := exec.Command("go", a...) cmd := exec.Command("go", a...)
cmd.Env = append(cleanEnv(), "GOPATH="+gopath, "GOARCH="+goarch, "GOOS="+goos) cmd.Env = os.Environ()
for k, v := range env {
cmd.Env = append(cmd.Env, k+"="+v)
}
if !enableCGO { if !enableCGO {
cmd.Env = append(cmd.Env, "CGO_ENABLED=0") cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
} }
printEnv(cmd.Env)
cmd.Dir = cwd cmd.Dir = cwd
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
verbosePrintf("go %s\n", args)
verbosePrintf("chdir %q\n", cwd)
verbosePrintf("go %q\n", a)
return cmd.Run() return cmd.Run()
} }
// test runs "go test args..." with GOPATH set to gopath. // test runs "go test args..." with GOPATH set to gopath.
func test(cwd, gopath string, args ...string) error { func test(cwd string, env map[string]string, args ...string) error {
args = append([]string{"test"}, args...) args = append([]string{"test", "-count", "1"}, args...)
cmd := exec.Command("go", args...) cmd := exec.Command("go", args...)
cmd.Env = append(cleanEnv(), "GOPATH="+gopath) cmd.Env = os.Environ()
for k, v := range env {
cmd.Env = append(cmd.Env, k+"="+v)
}
if !enableCGO {
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
}
cmd.Dir = cwd cmd.Dir = cwd
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
verbosePrintf("go %s\n", args)
printEnv(cmd.Env)
verbosePrintf("chdir %q\n", cwd)
verbosePrintf("go %q\n", args)
return cmd.Run() return cmd.Run()
} }
@@ -357,30 +261,39 @@ func ParseGoVersion(s string) (v GoVersion) {
s = s[2:] s = s[2:]
data := strings.Split(s, ".") data := strings.Split(s, ".")
if len(data) != 3 { if len(data) < 2 || len(data) > 3 {
return // invalid version
return GoVersion{}
} }
major, err := strconv.Atoi(data[0]) var err error
v.Major, err = strconv.Atoi(data[0])
if err != nil { if err != nil {
return return GoVersion{}
} }
minor, err := strconv.Atoi(data[1]) // try to parse the minor version while removing an eventual suffix (like
if err != nil { // "rc2" or so)
return for s := data[1]; s != ""; s = s[:len(s)-1] {
v.Minor, err = strconv.Atoi(s)
if err == nil {
break
}
} }
patch, err := strconv.Atoi(data[2]) if v.Minor == 0 {
if err != nil { // no minor version found
return return GoVersion{}
} }
v = GoVersion{ if len(data) >= 3 {
Major: major, v.Patch, err = strconv.Atoi(data[2])
Minor: minor, if err != nil {
Patch: patch, return GoVersion{}
}
} }
return return
} }
@@ -413,19 +326,26 @@ func (v GoVersion) String() string {
} }
func main() { func main() {
ver := ParseGoVersion(runtime.Version()) if !goVersion.AtLeast(GoVersion{1, 11, 0}) {
if !ver.AtLeast(config.MinVersion) { die("Go version (%v) is too old, Go <= 1.11 does not support Go Modules\n", goVersion)
fmt.Fprintf(os.Stderr, "%s detected, this program requires at least %s\n", ver, config.MinVersion) }
if !goVersion.AtLeast(config.MinVersion) {
fmt.Fprintf(os.Stderr, "%s detected, this program requires at least %s\n", goVersion, config.MinVersion)
os.Exit(1) os.Exit(1)
} }
buildTags := []string{} buildTags := config.DefaultBuildTags
skipNext := false skipNext := false
params := os.Args[1:] params := os.Args[1:]
targetGOOS := runtime.GOOS env := map[string]string{
targetGOARCH := runtime.GOARCH "GO111MODULE": "on", // make sure we build in Module mode
"GOOS": runtime.GOOS,
"GOARCH": runtime.GOARCH,
"GOARM": "",
}
var outputFilename string var outputFilename string
@@ -438,14 +358,12 @@ func main() {
switch arg { switch arg {
case "-v", "--verbose": case "-v", "--verbose":
verbose = true verbose = true
case "-k", "--keep-gopath":
keepGopath = true
case "-t", "-tags", "--tags": case "-t", "-tags", "--tags":
if i+1 >= len(params) { if i+1 >= len(params) {
die("-t given but no tag specified") die("-t given but no tag specified")
} }
skipNext = true skipNext = true
buildTags = strings.Split(params[i+1], " ") buildTags = append(buildTags, strings.Split(params[i+1], " ")...)
case "-o", "--output": case "-o", "--output":
skipNext = true skipNext = true
outputFilename = params[i+1] outputFilename = params[i+1]
@@ -453,12 +371,17 @@ func main() {
runTests = true runTests = true
case "--enable-cgo": case "--enable-cgo":
enableCGO = true enableCGO = true
case "--enable-pie":
enablePIE = true
case "--goos": case "--goos":
skipNext = true skipNext = true
targetGOOS = params[i+1] env["GOOS"] = params[i+1]
case "--goarch": case "--goarch":
skipNext = true skipNext = true
targetGOARCH = params[i+1] env["GOARCH"] = params[i+1]
case "--goarm":
skipNext = true
env["GOARM"] = params[i+1]
case "-h": case "-h":
showUsage(os.Stdout) showUsage(os.Stdout)
return return
@@ -469,10 +392,7 @@ func main() {
} }
} }
if len(buildTags) == 0 { verbosePrintf("detected Go version %v\n", goVersion)
verbosePrintf("adding build-tag release\n")
buildTags = []string{"release"}
}
for i := range buildTags { for i := range buildTags {
buildTags[i] = strings.TrimSpace(buildTags[i]) buildTags[i] = strings.TrimSpace(buildTags[i])
@@ -485,48 +405,16 @@ func main() {
die("Getwd(): %v\n", err) die("Getwd(): %v\n", err)
} }
gopath, err := ioutil.TempDir("", fmt.Sprintf("%v-build-", config.Name))
if err != nil {
die("TempDir(): %v\n", err)
}
verbosePrintf("create GOPATH at %v\n", gopath)
if err = updateGopath(gopath, root, config.Namespace); err != nil {
die("copying files from %v/src to %v/src failed: %v\n", root, gopath, err)
}
vendor := filepath.Join(root, "vendor")
if directoryExists(vendor) {
if err = updateGopath(gopath, vendor, filepath.Join(config.Namespace, "vendor")); err != nil {
die("copying files from %v to %v failed: %v\n", root, gopath, err)
}
}
defer func() {
if !keepGopath {
verbosePrintf("remove %v\n", gopath)
if err = os.RemoveAll(gopath); err != nil {
die("remove GOPATH at %s failed: %v\n", err)
}
} else {
verbosePrintf("leaving temporary GOPATH at %v\n", gopath)
}
}()
if outputFilename == "" { if outputFilename == "" {
outputFilename = config.Name outputFilename = config.Name
if targetGOOS == "windows" { if env["GOOS"] == "windows" {
outputFilename += ".exe" outputFilename += ".exe"
} }
} }
cwd, err := os.Getwd()
if err != nil {
die("Getwd() returned %v\n", err)
}
output := outputFilename output := outputFilename
if !filepath.IsAbs(output) { if !filepath.IsAbs(output) {
output = filepath.Join(cwd, output) output = filepath.Join(root, output)
} }
version := getVersion() version := getVersion()
@@ -537,13 +425,29 @@ func main() {
ldflags := "-s -w " + constants.LDFlags() ldflags := "-s -w " + constants.LDFlags()
verbosePrintf("ldflags: %s\n", ldflags) verbosePrintf("ldflags: %s\n", ldflags)
args := []string{ var (
"-tags", strings.Join(buildTags, " "), buildArgs []string
"-ldflags", ldflags, testArgs []string
"-o", output, config.Main, )
mainPackage := config.Main
if strings.HasPrefix(mainPackage, config.Namespace) {
mainPackage = strings.Replace(mainPackage, config.Namespace, "./", 1)
} }
err = build(filepath.Join(gopath, "src"), targetGOOS, targetGOARCH, gopath, args...) buildTarget := filepath.FromSlash(mainPackage)
buildCWD, err := os.Getwd()
if err != nil {
die("unable to determine current working directory: %v\n", err)
}
buildArgs = append(buildArgs,
"-tags", strings.Join(buildTags, " "),
"-ldflags", ldflags,
"-o", output, buildTarget,
)
err = build(buildCWD, env, buildArgs...)
if err != nil { if err != nil {
die("build failed: %v\n", err) die("build failed: %v\n", err)
} }
@@ -551,7 +455,9 @@ func main() {
if runTests { if runTests {
verbosePrintf("running tests\n") verbosePrintf("running tests\n")
err = test(cwd, gopath, config.Tests...) testArgs = append(testArgs, config.Tests...)
err = test(buildCWD, env, testArgs...)
if err != nil { if err != nil {
die("running tests failed: %v\n", err) die("running tests failed: %v\n", err)
} }