build: improve cross compilation setup (#22804)

This PR cleans up the CI build system and fixes a couple of issues.

- The go tool launcher code has been moved to internal/build. With the new
  toolchain functions, the environment of the host Go (i.e. the one that built
  ci.go) and the target Go (i.e. the toolchain downloaded by -dlgo) are isolated
  more strictly. This is important to make cross compilation and -dlgo work
  correctly in more cases.
- The -dlgo option now skips the download and uses the host Go if the running Go
  version matches dlgoVersion exactly.
- The 'test' command now supports -dlgo, -cc and -arch. Running unit tests with
  foreign GOARCH is occasionally useful. For example, it can be used to run
  32-bit tests on Windows. It can also be used to run darwin/amd64 tests on
  darwin/arm64 using Rosetta 2.
- The 'aar', 'xcode' and 'xgo' commands now use a slightly different method to
  install external tools. They previously used `go get`, but this comes with the
  annoying side effect of modifying go.mod. They now use `go install` instead,
  which is the recommended way of installing tools without modifying the local
  module.
- The old build warning about outdated Go version has been removed because we're
  much better at keeping backwards compatibility now.
pull/22811/head
Felix Lange 4 years ago committed by GitHub
parent b8040a430e
commit effaf18523
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      Makefile
  2. 206
      build/ci.go
  3. 3
      internal/build/env.go
  4. 149
      internal/build/gotool.go
  5. 14
      internal/build/util.go

@ -26,7 +26,7 @@ android:
@echo "Import \"$(GOBIN)/geth.aar\" to use the library."
@echo "Import \"$(GOBIN)/geth-sources.jar\" to add javadocs"
@echo "For more info see https://stackoverflow.com/questions/20994336/android-studio-how-to-attach-javadoc"
ios:
$(GORUN) build/ci.go xcode --local
@echo "Done building."
@ -46,12 +46,11 @@ clean:
# You need to put $GOBIN (or $GOPATH/bin) in your PATH to use 'go generate'.
devtools:
env GOBIN= go get -u golang.org/x/tools/cmd/stringer
env GOBIN= go get -u github.com/kevinburke/go-bindata/go-bindata
env GOBIN= go get -u github.com/fjl/gencodec
env GOBIN= go get -u github.com/golang/protobuf/protoc-gen-go
env GOBIN= go install golang.org/x/tools/cmd/stringer@latest
env GOBIN= go install github.com/kevinburke/go-bindata/go-bindata@latest
env GOBIN= go install github.com/fjl/gencodec@latest
env GOBIN= go install github.com/golang/protobuf/protoc-gen-go@latest
env GOBIN= go install ./cmd/abigen
@type "npm" 2> /dev/null || echo 'Please install node.js and npm'
@type "solc" 2> /dev/null || echo 'Please install solc'
@type "protoc" 2> /dev/null || echo 'Please install protoc'

@ -208,58 +208,25 @@ func doInstall(cmdline []string) {
cc = flag.String("cc", "", "C compiler to cross build with")
)
flag.CommandLine.Parse(cmdline)
env := build.Env()
// Check local Go version. People regularly open issues about compilation
// failure with outdated Go. This should save them the trouble.
if !strings.Contains(runtime.Version(), "devel") {
// Figure out the minor version number since we can't textually compare (1.10 < 1.9)
var minor int
fmt.Sscanf(strings.TrimPrefix(runtime.Version(), "go1."), "%d", &minor)
if minor < 13 {
log.Println("You have Go version", runtime.Version())
log.Println("go-ethereum requires at least Go version 1.13 and cannot")
log.Println("be compiled with an earlier version. Please upgrade your Go installation.")
os.Exit(1)
}
}
// Choose which go command we're going to use.
var gobuild *exec.Cmd
if !*dlgo {
// Default behavior: use the go version which runs ci.go right now.
gobuild = goTool("build")
} else {
// Download of Go requested. This is for build environments where the
// installed version is too old and cannot be upgraded easily.
cachedir := filepath.Join("build", "cache")
goroot := downloadGo(runtime.GOARCH, runtime.GOOS, cachedir)
gobuild = localGoTool(goroot, "build")
// Configure the toolchain.
tc := build.GoToolchain{GOARCH: *arch, CC: *cc}
if *dlgo {
csdb := build.MustLoadChecksums("build/checksums.txt")
tc.Root = build.DownloadGo(csdb, dlgoVersion)
}
// Configure environment for cross build.
if *arch != "" || *arch != runtime.GOARCH {
gobuild.Env = append(gobuild.Env, "CGO_ENABLED=1")
gobuild.Env = append(gobuild.Env, "GOARCH="+*arch)
}
// Configure C compiler.
if *cc != "" {
gobuild.Env = append(gobuild.Env, "CC="+*cc)
} else if os.Getenv("CC") != "" {
gobuild.Env = append(gobuild.Env, "CC="+os.Getenv("CC"))
}
// Configure the build.
env := build.Env()
gobuild := tc.Go("build", buildFlags(env)...)
// arm64 CI builders are memory-constrained and can't handle concurrent builds,
// better disable it. This check isn't the best, it should probably
// check for something in env instead.
if runtime.GOARCH == "arm64" {
if env.CI && runtime.GOARCH == "arm64" {
gobuild.Args = append(gobuild.Args, "-p", "1")
}
// Put the default settings in.
gobuild.Args = append(gobuild.Args, buildFlags(env)...)
// We use -trimpath to avoid leaking local paths into the built executables.
gobuild.Args = append(gobuild.Args, "-trimpath")
@ -301,53 +268,30 @@ func buildFlags(env build.Environment) (flags []string) {
return flags
}
// goTool returns the go tool. This uses the Go version which runs ci.go.
func goTool(subcmd string, args ...string) *exec.Cmd {
cmd := build.GoTool(subcmd, args...)
goToolSetEnv(cmd)
return cmd
}
// localGoTool returns the go tool from the given GOROOT.
func localGoTool(goroot string, subcmd string, args ...string) *exec.Cmd {
gotool := filepath.Join(goroot, "bin", "go")
cmd := exec.Command(gotool, subcmd)
goToolSetEnv(cmd)
cmd.Env = append(cmd.Env, "GOROOT="+goroot)
cmd.Args = append(cmd.Args, args...)
return cmd
}
// goToolSetEnv forwards the build environment to the go tool.
func goToolSetEnv(cmd *exec.Cmd) {
cmd.Env = append(cmd.Env, "GOBIN="+GOBIN)
for _, e := range os.Environ() {
if strings.HasPrefix(e, "GOBIN=") || strings.HasPrefix(e, "CC=") {
continue
}
cmd.Env = append(cmd.Env, e)
}
}
// Running The Tests
//
// "tests" also includes static analysis tools such as vet.
func doTest(cmdline []string) {
coverage := flag.Bool("coverage", false, "Whether to record code coverage")
verbose := flag.Bool("v", false, "Whether to log verbosely")
var (
dlgo = flag.Bool("dlgo", false, "Download Go and build with it")
arch = flag.String("arch", "", "Run tests for given architecture")
cc = flag.String("cc", "", "Sets C compiler binary")
coverage = flag.Bool("coverage", false, "Whether to record code coverage")
verbose = flag.Bool("v", false, "Whether to log verbosely")
)
flag.CommandLine.Parse(cmdline)
env := build.Env()
packages := []string{"./..."}
if len(flag.CommandLine.Args()) > 0 {
packages = flag.CommandLine.Args()
// Configure the toolchain.
tc := build.GoToolchain{GOARCH: *arch, CC: *cc}
if *dlgo {
csdb := build.MustLoadChecksums("build/checksums.txt")
tc.Root = build.DownloadGo(csdb, dlgoVersion)
}
gotest := tc.Go("test")
// Run the actual tests.
// Test a single package at a time. CI builders are slow
// and some tests run into timeouts under load.
gotest := goTool("test", buildFlags(env)...)
gotest.Args = append(gotest.Args, "-p", "1")
if *coverage {
gotest.Args = append(gotest.Args, "-covermode=atomic", "-cover")
@ -356,6 +300,10 @@ func doTest(cmdline []string) {
gotest.Args = append(gotest.Args, "-v")
}
packages := []string{"./..."}
if len(flag.CommandLine.Args()) > 0 {
packages = flag.CommandLine.Args()
}
gotest.Args = append(gotest.Args, packages...)
build.MustRun(gotest)
}
@ -415,8 +363,7 @@ func doArchive(cmdline []string) {
}
var (
env = build.Env()
env = build.Env()
basegeth = archiveBasename(*arch, params.ArchiveVersion(env.Commit))
geth = "geth-" + basegeth + ext
alltools = "geth-alltools-" + basegeth + ext
@ -492,15 +439,15 @@ func archiveUpload(archive string, blobstore string, signer string, signifyVar s
// skips archiving for some build configurations.
func maybeSkipArchive(env build.Environment) {
if env.IsPullRequest {
log.Printf("skipping because this is a PR build")
log.Printf("skipping archive creation because this is a PR build")
os.Exit(0)
}
if env.IsCronJob {
log.Printf("skipping because this is a cron job")
log.Printf("skipping archive creation because this is a cron job")
os.Exit(0)
}
if env.Branch != "master" && !strings.HasPrefix(env.Tag, "v1.") {
log.Printf("skipping because branch %q, tag %q is not on the whitelist", env.Branch, env.Tag)
log.Printf("skipping archive creation because branch %q, tag %q is not on the whitelist", env.Branch, env.Tag)
os.Exit(0)
}
}
@ -518,6 +465,7 @@ func doDebianSource(cmdline []string) {
flag.CommandLine.Parse(cmdline)
*workdir = makeWorkdir(*workdir)
env := build.Env()
tc := new(build.GoToolchain)
maybeSkipArchive(env)
// Import the signing key.
@ -531,12 +479,12 @@ func doDebianSource(cmdline []string) {
gobundle := downloadGoSources(*cachedir)
// Download all the dependencies needed to build the sources and run the ci script
srcdepfetch := goTool("mod", "download")
srcdepfetch.Env = append(os.Environ(), "GOPATH="+filepath.Join(*workdir, "modgopath"))
srcdepfetch := tc.Go("mod", "download")
srcdepfetch.Env = append(srcdepfetch.Env, "GOPATH="+filepath.Join(*workdir, "modgopath"))
build.MustRun(srcdepfetch)
cidepfetch := goTool("run", "./build/ci.go")
cidepfetch.Env = append(os.Environ(), "GOPATH="+filepath.Join(*workdir, "modgopath"))
cidepfetch := tc.Go("run", "./build/ci.go")
cidepfetch.Env = append(cidepfetch.Env, "GOPATH="+filepath.Join(*workdir, "modgopath"))
cidepfetch.Run() // Command fails, don't care, we only need the deps to start it
// Create Debian packages and upload them.
@ -592,41 +540,6 @@ func downloadGoSources(cachedir string) string {
return dst
}
// downloadGo downloads the Go binary distribution and unpacks it into a temporary
// directory. It returns the GOROOT of the unpacked toolchain.
func downloadGo(goarch, goos, cachedir string) string {
if goarch == "arm" {
goarch = "armv6l"
}
csdb := build.MustLoadChecksums("build/checksums.txt")
file := fmt.Sprintf("go%s.%s-%s", dlgoVersion, goos, goarch)
if goos == "windows" {
file += ".zip"
} else {
file += ".tar.gz"
}
url := "https://golang.org/dl/" + file
dst := filepath.Join(cachedir, file)
if err := csdb.DownloadFile(url, dst); err != nil {
log.Fatal(err)
}
ucache, err := os.UserCacheDir()
if err != nil {
log.Fatal(err)
}
godir := filepath.Join(ucache, fmt.Sprintf("geth-go-%s-%s-%s", dlgoVersion, goos, goarch))
if err := build.ExtractArchive(dst, godir); err != nil {
log.Fatal(err)
}
goroot, err := filepath.Abs(filepath.Join(godir, "go"))
if err != nil {
log.Fatal(err)
}
return goroot
}
func ppaUpload(workdir, ppa, sshUser string, files []string) {
p := strings.Split(ppa, "/")
if len(p) != 2 {
@ -901,13 +814,23 @@ func doAndroidArchive(cmdline []string) {
)
flag.CommandLine.Parse(cmdline)
env := build.Env()
tc := new(build.GoToolchain)
// Sanity check that the SDK and NDK are installed and set
if os.Getenv("ANDROID_HOME") == "" {
log.Fatal("Please ensure ANDROID_HOME points to your Android SDK")
}
// Build gomobile.
install := tc.Install(GOBIN, "golang.org/x/mobile/cmd/gomobile@latest", "golang.org/x/mobile/cmd/gobind@latest")
install.Env = append(install.Env)
build.MustRun(install)
// Ensure all dependencies are available. This is required to make
// gomobile bind work because it expects go.sum to contain all checksums.
build.MustRun(tc.Go("mod", "download"))
// Build the Android archive and Maven resources
build.MustRun(goTool("get", "golang.org/x/mobile/cmd/gomobile", "golang.org/x/mobile/cmd/gobind"))
build.MustRun(gomobileTool("bind", "-ldflags", "-s -w", "--target", "android", "--javapkg", "org.ethereum", "-v", "github.com/ethereum/go-ethereum/mobile"))
if *local {
@ -1027,10 +950,16 @@ func doXCodeFramework(cmdline []string) {
)
flag.CommandLine.Parse(cmdline)
env := build.Env()
tc := new(build.GoToolchain)
// Build gomobile.
build.MustRun(tc.Install(GOBIN, "golang.org/x/mobile/cmd/gomobile@latest", "golang.org/x/mobile/cmd/gobind@latest"))
// Ensure all dependencies are available. This is required to make
// gomobile bind work because it expects go.sum to contain all checksums.
build.MustRun(tc.Go("mod", "download"))
// Build the iOS XCode framework
build.MustRun(goTool("get", "golang.org/x/mobile/cmd/gomobile", "golang.org/x/mobile/cmd/gobind"))
build.MustRun(gomobileTool("init"))
bind := gomobileTool("bind", "-ldflags", "-s -w", "--target", "ios", "-v", "github.com/ethereum/go-ethereum/mobile")
if *local {
@ -1039,17 +968,14 @@ func doXCodeFramework(cmdline []string) {
build.MustRun(bind)
return
}
// Create the archive.
maybeSkipArchive(env)
archive := "geth-" + archiveBasename("ios", params.ArchiveVersion(env.Commit))
if err := os.Mkdir(archive, os.ModePerm); err != nil {
log.Fatal(err)
}
bind.Dir, _ = filepath.Abs(archive)
build.MustRun(bind)
build.MustRunCommand("tar", "-zcvf", archive+".tar.gz", archive)
// Skip CocoaPods deploy and Azure upload for PR builds
maybeSkipArchive(env)
// Sign and upload the framework to Azure
if err := archiveUpload(archive+".tar.gz", *upload, *signer, *signify); err != nil {
log.Fatal(err)
@ -1115,10 +1041,10 @@ func doXgo(cmdline []string) {
)
flag.CommandLine.Parse(cmdline)
env := build.Env()
var tc build.GoToolchain
// Make sure xgo is available for cross compilation
gogetxgo := goTool("get", "github.com/karalabe/xgo")
build.MustRun(gogetxgo)
build.MustRun(tc.Install(GOBIN, "github.com/karalabe/xgo@latest"))
// If all tools building is requested, build everything the builder wants
args := append(buildFlags(env), flag.Args()...)
@ -1129,27 +1055,23 @@ func doXgo(cmdline []string) {
if strings.HasPrefix(res, GOBIN) {
// Binary tool found, cross build it explicitly
args = append(args, "./"+filepath.Join("cmd", filepath.Base(res)))
xgo := xgoTool(args)
build.MustRun(xgo)
build.MustRun(xgoTool(args))
args = args[:len(args)-1]
}
}
return
}
// Otherwise xxecute the explicit cross compilation
// Otherwise execute the explicit cross compilation
path := args[len(args)-1]
args = append(args[:len(args)-1], []string{"--dest", GOBIN, path}...)
xgo := xgoTool(args)
build.MustRun(xgo)
build.MustRun(xgoTool(args))
}
func xgoTool(args []string) *exec.Cmd {
cmd := exec.Command(filepath.Join(GOBIN, "xgo"), args...)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, []string{
"GOBIN=" + GOBIN,
}...)
cmd.Env = append(cmd.Env, []string{"GOBIN=" + GOBIN}...)
return cmd
}

@ -38,6 +38,7 @@ var (
// Environment contains metadata provided by the build environment.
type Environment struct {
CI bool
Name string // name of the environment
Repo string // name of GitHub repo
Commit, Date, Branch, Tag string // Git info
@ -61,6 +62,7 @@ func Env() Environment {
commit = os.Getenv("TRAVIS_COMMIT")
}
return Environment{
CI: true,
Name: "travis",
Repo: os.Getenv("TRAVIS_REPO_SLUG"),
Commit: commit,
@ -77,6 +79,7 @@ func Env() Environment {
commit = os.Getenv("APPVEYOR_REPO_COMMIT")
}
return Environment{
CI: true,
Name: "appveyor",
Repo: os.Getenv("APPVEYOR_REPO_NAME"),
Commit: commit,

@ -0,0 +1,149 @@
// Copyright 2021 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package build
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
type GoToolchain struct {
Root string // GOROOT
// Cross-compilation variables. These are set when running the go tool.
GOARCH string
GOOS string
CC string
}
// Go creates an invocation of the go command.
func (g *GoToolchain) Go(command string, args ...string) *exec.Cmd {
tool := g.goTool(command, args...)
// Configure environment for cross build.
if g.GOARCH != "" && g.GOARCH != runtime.GOARCH {
tool.Env = append(tool.Env, "CGO_ENABLED=1")
tool.Env = append(tool.Env, "GOARCH="+g.GOARCH)
}
if g.GOOS != "" && g.GOOS != runtime.GOOS {
tool.Env = append(tool.Env, "GOOS="+g.GOOS)
}
// Configure C compiler.
if g.CC != "" {
tool.Env = append(tool.Env, "CC="+g.CC)
} else if os.Getenv("CC") != "" {
tool.Env = append(tool.Env, "CC="+os.Getenv("CC"))
}
return tool
}
// Install creates an invocation of 'go install'. The command is configured to output
// executables to the given 'gobin' directory.
//
// This can be used to install auxiliary build tools without modifying the local go.mod and
// go.sum files. To install tools which are not required by go.mod, ensure that all module
// paths in 'args' contain a module version suffix (e.g. "...@latest").
func (g *GoToolchain) Install(gobin string, args ...string) *exec.Cmd {
if !filepath.IsAbs(gobin) {
panic("GOBIN must be an absolute path")
}
tool := g.goTool("install")
tool.Env = append(tool.Env, "GOBIN="+gobin)
tool.Args = append(tool.Args, "-mod=readonly")
tool.Args = append(tool.Args, args...)
// Ensure GOPATH is set because go install seems to absolutely require it. This uses
// 'go env' because it resolves the default value when GOPATH is not set in the
// environment. Ignore errors running go env and leave any complaining about GOPATH to
// the install command.
pathTool := g.goTool("env", "GOPATH")
output, _ := pathTool.Output()
tool.Env = append(tool.Env, "GOPATH="+string(output))
return tool
}
func (g *GoToolchain) goTool(command string, args ...string) *exec.Cmd {
if g.Root == "" {
g.Root = runtime.GOROOT()
}
tool := exec.Command(filepath.Join(g.Root, "bin", "go"), command)
tool.Args = append(tool.Args, args...)
tool.Env = append(tool.Env, "GOROOT="+g.Root)
// Forward environment variables to the tool, but skip compiler target settings.
// TODO: what about GOARM?
skip := map[string]struct{}{"GOROOT": {}, "GOARCH": {}, "GOOS": {}, "GOBIN": {}, "CC": {}}
for _, e := range os.Environ() {
if i := strings.IndexByte(e, '='); i >= 0 {
if _, ok := skip[e[:i]]; ok {
continue
}
}
tool.Env = append(tool.Env, e)
}
return tool
}
// DownloadGo downloads the Go binary distribution and unpacks it into a temporary
// directory. It returns the GOROOT of the unpacked toolchain.
func DownloadGo(csdb *ChecksumDB, version string) string {
// Shortcut: if the Go version that runs this script matches the
// requested version exactly, there is no need to download anything.
activeGo := strings.TrimPrefix(runtime.Version(), "go")
if activeGo == version {
log.Printf("-dlgo version matches active Go version %s, skipping download.", activeGo)
return runtime.GOROOT()
}
ucache, err := os.UserCacheDir()
if err != nil {
log.Fatal(err)
}
// For Arm architecture, GOARCH includes ISA version.
os := runtime.GOOS
arch := runtime.GOARCH
if arch == "arm" {
arch = "armv6l"
}
file := fmt.Sprintf("go%s.%s-%s", version, os, arch)
if os == "windows" {
file += ".zip"
} else {
file += ".tar.gz"
}
url := "https://golang.org/dl/" + file
dst := filepath.Join(ucache, file)
if err := csdb.DownloadFile(url, dst); err != nil {
log.Fatal(err)
}
godir := filepath.Join(ucache, fmt.Sprintf("geth-go-%s-%s-%s", version, os, arch))
if err := ExtractArchive(dst, godir); err != nil {
log.Fatal(err)
}
goroot, err := filepath.Abs(filepath.Join(godir, "go"))
if err != nil {
log.Fatal(err)
}
return goroot
}

@ -29,7 +29,6 @@ import (
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
"text/template"
)
@ -111,19 +110,6 @@ func render(tpl *template.Template, outputFile string, outputPerm os.FileMode, x
}
}
// GoTool returns the command that runs a go tool. This uses go from GOROOT instead of PATH
// so that go commands executed by build use the same version of Go as the 'host' that runs
// build code. e.g.
//
// /usr/lib/go-1.12.1/bin/go run build/ci.go ...
//
// runs using go 1.12.1 and invokes go 1.12.1 tools from the same GOROOT. This is also important
// because runtime.Version checks on the host should match the tools that are run.
func GoTool(tool string, args ...string) *exec.Cmd {
args = append([]string{tool}, args...)
return exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), args...)
}
// UploadSFTP uploads files to a remote host using the sftp command line tool.
// The destination host may be specified either as [user@]host[: or as a URI in
// the form sftp://[user@]host[:port].

Loading…
Cancel
Save