mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 11:05:45 +00:00
tool/gocross: a tool for building Tailscale binaries
Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:
parent
0b8f89c79c
commit
860734aed9
2
.gitignore
vendored
2
.gitignore
vendored
@ -34,3 +34,5 @@ cmd/tailscaled/tailscaled
|
||||
|
||||
# Ignore direnv nix-shell environment cache
|
||||
.direnv/
|
||||
|
||||
/gocross
|
||||
|
79
tool/go
79
tool/go
@ -4,81 +4,4 @@
|
||||
# currently-desired version from https://github.com/tailscale/go,
|
||||
# downloading it first if necessary.
|
||||
|
||||
set -eu
|
||||
|
||||
log() {
|
||||
echo "$@" >&2
|
||||
}
|
||||
|
||||
DEFAULT_TOOLCHAIN_DIR="${HOME}/.cache/tailscale-go"
|
||||
TOOLCHAIN="${TOOLCHAIN-${DEFAULT_TOOLCHAIN_DIR}}"
|
||||
TOOLCHAIN_GO="${TOOLCHAIN}/bin/go"
|
||||
read -r REV < "$(dirname "$0")/../go.toolchain.rev"
|
||||
|
||||
# Fast, quiet path, when Tailscale is already current.
|
||||
if [ -e "${TOOLCHAIN_GO}" ]; then
|
||||
short_hash=$("${TOOLCHAIN_GO}" version | sed 's/.*-ts//; s/ .*//')
|
||||
case $REV in
|
||||
"$short_hash"*)
|
||||
unset GOROOT
|
||||
exec "${TOOLCHAIN_GO}" "$@"
|
||||
esac
|
||||
fi
|
||||
|
||||
# This works for linux and darwin, which is sufficient
|
||||
# (we do not build tailscale-go for other targets).
|
||||
host_os=$(uname -s | tr A-Z a-z)
|
||||
host_arch="$(uname -m)"
|
||||
if [ "$host_arch" = "aarch64" ]; then
|
||||
# Go uses the name "arm64".
|
||||
host_arch="arm64"
|
||||
elif [ "$host_arch" = "x86_64" ]; then
|
||||
# Go uses the name "amd64".
|
||||
host_arch="amd64"
|
||||
fi
|
||||
|
||||
get_cached() {
|
||||
if [ ! -d "$TOOLCHAIN" ]; then
|
||||
mkdir -p "$TOOLCHAIN"
|
||||
fi
|
||||
|
||||
archive="$TOOLCHAIN-$REV.tar.gz"
|
||||
mark="$TOOLCHAIN.extracted"
|
||||
extracted=
|
||||
|
||||
# Ignore the error from read, which may error if the mark file does not contain a line end.
|
||||
read -r extracted < "$mark" || true
|
||||
|
||||
if [ "$extracted" = "$REV" ] && [ -e "${TOOLCHAIN_GO}" ]; then
|
||||
# already ok
|
||||
log "Go toolchain '$REV' already extracted."
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f "$archive.new" "$TOOLCHAIN.extracted"
|
||||
if [ ! -e "$archive" ]; then
|
||||
log "Need to download go '$REV'."
|
||||
curl -f -L -o "$archive.new" "https://github.com/tailscale/go/releases/download/build-${REV}/${host_os}-${host_arch}.tar.gz"
|
||||
rm -f "$archive"
|
||||
mv "$archive.new" "$archive"
|
||||
fi
|
||||
|
||||
log "Extracting tailscale/go rev '$REV'" >&2
|
||||
log " into '$TOOLCHAIN'." >&2
|
||||
rm -rf "$TOOLCHAIN"
|
||||
mkdir -p "$TOOLCHAIN"
|
||||
(cd "$TOOLCHAIN" && tar --strip-components=1 -xf "$archive")
|
||||
echo "$REV" >$mark
|
||||
}
|
||||
|
||||
if [ "${REV}" = "SKIP" ] ||
|
||||
[ "${host_os}" != "darwin" -a "${host_os}" != "linux" ] ||
|
||||
[ "${host_arch}" != "amd64" -a "${host_arch}" != "arm64" ]; then
|
||||
# Use whichever go is available
|
||||
exec go "$@"
|
||||
else
|
||||
get_cached
|
||||
fi
|
||||
|
||||
unset GOROOT
|
||||
exec "${TOOLCHAIN_GO}" "$@"
|
||||
exec "$(dirname "$0")/../tool/gocross/gocross-wrapper.sh" "$@"
|
||||
|
183
tool/gocross/autoflags.go
Normal file
183
tool/gocross/autoflags.go
Normal file
@ -0,0 +1,183 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/version/mkversion"
|
||||
)
|
||||
|
||||
// Autoflags adjusts the commandline argv into a new commandline
|
||||
// newArgv and envvar alterations in env.
|
||||
func Autoflags(argv []string, goroot string) (newArgv []string, env *Environment, err error) {
|
||||
return autoflagsForTest(argv, NewEnvironment(), goroot, runtime.GOOS, runtime.GOARCH, mkversion.Info)
|
||||
}
|
||||
|
||||
func autoflagsForTest(argv []string, env *Environment, goroot, nativeGOOS, nativeGOARCH string, getVersion func() mkversion.VersionInfo) (newArgv []string, newEnv *Environment, err error) {
|
||||
// This is where all our "automatic flag injection" decisions get
|
||||
// made. Modifying this code will modify the environment variables
|
||||
// and commandline flags that the final `go` tool invocation will
|
||||
// receive.
|
||||
//
|
||||
// When choosing between making this code concise or readable,
|
||||
// please err on the side of being readable. Our build
|
||||
// environments are relatively complicated by Go standards, and we
|
||||
// want to keep it intelligible and malleable for our future
|
||||
// selves.
|
||||
var (
|
||||
subcommand = ""
|
||||
|
||||
targetOS = env.Get("GOOS", nativeGOOS)
|
||||
targetArch = env.Get("GOARCH", nativeGOARCH)
|
||||
buildFlags = []string{"-trimpath"}
|
||||
cgoCflags = []string{"-O3", "-std=gnu11"}
|
||||
cgoLdflags []string
|
||||
ldflags []string
|
||||
tags = []string{"tailscale_go"}
|
||||
cgo = false
|
||||
failReflect = false
|
||||
)
|
||||
if len(argv) > 1 {
|
||||
subcommand = argv[1]
|
||||
}
|
||||
|
||||
switch subcommand {
|
||||
case "build", "env", "install", "run", "test", "list":
|
||||
default:
|
||||
return argv, env, nil
|
||||
}
|
||||
|
||||
vi := getVersion()
|
||||
ldflags = []string{
|
||||
"-X", "tailscale.com/version.longStamp=" + vi.Long,
|
||||
"-X", "tailscale.com/version.shortStamp=" + vi.Short,
|
||||
"-X", "tailscale.com/version.gitCommitStamp=" + vi.GitHash,
|
||||
"-X", "tailscale.com/version.extraGitCommitStamp=" + vi.OtherHash,
|
||||
}
|
||||
|
||||
switch targetOS {
|
||||
case "linux":
|
||||
// Getting Go to build a static binary with cgo enabled is a
|
||||
// minor ordeal. The incantations you apparently need are
|
||||
// documented at: https://github.com/golang/go/issues/26492
|
||||
tags = append(tags, "osusergo", "netgo")
|
||||
cgo = targetOS == nativeGOOS && targetArch == nativeGOARCH
|
||||
// When in a Nix environment, the gcc package is built with only dynamic
|
||||
// versions of glibc. You can get a static version of glibc via
|
||||
// pkgs.glibc.static, but then you are reliant on Nix's gcc wrapper
|
||||
// magic to inject that as a -L path to linker invocations.
|
||||
//
|
||||
// We can't rely on that magic linker flag injection, because that
|
||||
// injection breaks redo's go machinery for dynamic go+cgo linking due
|
||||
// to flag ordering issues that we can't easily fix (since the nix
|
||||
// machinery controls the flag ordering, not us).
|
||||
//
|
||||
// So, instead, we unset NIX_LDFLAGS in our nix shell, which disables
|
||||
// the magic linker flag passing; and we have shell.nix drop the path to
|
||||
// the static glibc files in GOCROSS_GLIBC_DIR. Finally, we reinject it
|
||||
// into the build process here, so that the linker can find static glibc
|
||||
// and complete a static-with-cgo linkage.
|
||||
extldflags := []string{"-static"}
|
||||
if glibcDir := env.Get("GOCROSS_GLIBC_DIR", ""); glibcDir != "" {
|
||||
extldflags = append(extldflags, "-L", glibcDir)
|
||||
}
|
||||
// -extldflags, when it contains multiple external linker flags, must be
|
||||
// quoted in its entirety as a member of -ldflags. Source:
|
||||
// https://github.com/golang/go/issues/6234
|
||||
ldflags = append(ldflags, fmt.Sprintf("'-extldflags=%s'", strings.Join(extldflags, " ")))
|
||||
case "windowsgui":
|
||||
// Fake GOOS that translates to "windows, but building GUI .exes not console .exes"
|
||||
targetOS = "windows"
|
||||
ldflags = append(ldflags, "-H", "windowsgui", "-s")
|
||||
case "windows":
|
||||
ldflags = append(ldflags, "-H", "windows", "-s")
|
||||
case "ios":
|
||||
failReflect = true
|
||||
fallthrough
|
||||
case "darwin":
|
||||
cgo = nativeGOOS == "darwin"
|
||||
tags = append(tags, "omitidna", "omitpemdecrypt")
|
||||
if env.IsSet("XCODE_VERSION_ACTUAL") {
|
||||
var xcodeFlags []string
|
||||
// Minimum OS version being targeted, results in
|
||||
// e.g. -mmacosx-version-min=11.3
|
||||
minOSKey := env.Get("DEPLOYMENT_TARGET_CLANG_FLAG_NAME", "")
|
||||
minOSVal := env.Get(env.Get("DEPLOYMENT_TARGET_CLANG_ENV_NAME", ""), "")
|
||||
xcodeFlags = append(xcodeFlags, fmt.Sprintf("-%s=%s", minOSKey, minOSVal))
|
||||
|
||||
// Target-specific SDK directory. Must be passed as two
|
||||
// words ("-isysroot PATH", not "-isysroot=PATH").
|
||||
xcodeFlags = append(xcodeFlags, "-isysroot", env.Get("SDKROOT", ""))
|
||||
|
||||
// What does clang call the target GOARCH?
|
||||
var clangArch string
|
||||
switch targetArch {
|
||||
case "amd64":
|
||||
clangArch = "x86_64"
|
||||
case "arm64":
|
||||
clangArch = "arm64"
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unsupported GOARCH=%q when building from Xcode", targetArch)
|
||||
}
|
||||
xcodeFlags = append(xcodeFlags, "-arch", clangArch)
|
||||
cgoCflags = append(cgoCflags, xcodeFlags...)
|
||||
cgoLdflags = append(cgoLdflags, xcodeFlags...)
|
||||
ldflags = append(ldflags, "-w")
|
||||
}
|
||||
}
|
||||
|
||||
// Finished computing the settings we want. Generate the modified
|
||||
// commandline and environment modifications.
|
||||
newArgv = append(newArgv, argv[:2]...) // Program name and `go` tool subcommand
|
||||
newArgv = append(newArgv, buildFlags...)
|
||||
if len(tags) > 0 {
|
||||
newArgv = append(newArgv, fmt.Sprintf("-tags=%s", strings.Join(tags, ",")))
|
||||
}
|
||||
if len(ldflags) > 0 {
|
||||
newArgv = append(newArgv, "-ldflags", strings.Join(ldflags, " "))
|
||||
}
|
||||
newArgv = append(newArgv, argv[2:]...)
|
||||
|
||||
env.Set("GOOS", targetOS)
|
||||
env.Set("GOARCH", targetArch)
|
||||
env.Set("GOARM", "5") // TODO: fix, see go/internal-bug/3092
|
||||
env.Set("GOMIPS", "softfloat")
|
||||
env.Set("CGO_ENABLED", boolStr(cgo))
|
||||
env.Set("CGO_CFLAGS", strings.Join(cgoCflags, " "))
|
||||
env.Set("CGO_LDFLAGS", strings.Join(cgoLdflags, " "))
|
||||
env.Set("CC", "cc")
|
||||
env.Set("TS_LINK_FAIL_REFLECT", boolStr(failReflect))
|
||||
env.Set("GOROOT", goroot)
|
||||
|
||||
if subcommand == "env" {
|
||||
return argv, env, nil
|
||||
}
|
||||
|
||||
return newArgv, env, nil
|
||||
}
|
||||
|
||||
// boolStr formats v as a string 0 or 1.
|
||||
// Used because CGO_ENABLED doesn't strconv.ParseBool, so
|
||||
// strconv.FormatBool breaks.
|
||||
func boolStr(v bool) string {
|
||||
if v {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
// formatArgv formats a []string similarly to %v, but quotes each
|
||||
// string so that the reader can clearly see each array element.
|
||||
func formatArgv(v []string) string {
|
||||
var ret strings.Builder
|
||||
ret.WriteByte('[')
|
||||
for _, s := range v {
|
||||
fmt.Fprintf(&ret, "%q ", s)
|
||||
}
|
||||
ret.WriteByte(']')
|
||||
return ret.String()
|
||||
}
|
409
tool/gocross/autoflags_test.go
Normal file
409
tool/gocross/autoflags_test.go
Normal file
@ -0,0 +1,409 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/version/mkversion"
|
||||
)
|
||||
|
||||
var fakeVersion = mkversion.VersionInfo{
|
||||
Short: "1.2.3",
|
||||
Long: "1.2.3-long",
|
||||
GitHash: "abcd",
|
||||
OtherHash: "defg",
|
||||
Xcode: "100.2.3",
|
||||
Winres: "1,2,3,0",
|
||||
}
|
||||
|
||||
func TestAutoflags(t *testing.T) {
|
||||
tests := []struct {
|
||||
// name convention: "<hostos>_<hostarch>_to_<targetos>_<targetarch>_<anything else?>"
|
||||
name string
|
||||
env map[string]string
|
||||
argv []string
|
||||
goroot string
|
||||
nativeGOOS string
|
||||
nativeGOARCH string
|
||||
|
||||
wantEnv map[string]string
|
||||
envDiff string
|
||||
wantArgv []string
|
||||
}{
|
||||
{
|
||||
name: "linux_amd64_to_linux_amd64",
|
||||
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
|
||||
goroot: "/goroot",
|
||||
nativeGOOS: "linux",
|
||||
nativeGOARCH: "amd64",
|
||||
|
||||
envDiff: `CC=cc (was <nil>)
|
||||
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
|
||||
CGO_ENABLED=1 (was <nil>)
|
||||
CGO_LDFLAGS= (was <nil>)
|
||||
GOARCH=amd64 (was <nil>)
|
||||
GOARM=5 (was <nil>)
|
||||
GOMIPS=softfloat (was <nil>)
|
||||
GOOS=linux (was <nil>)
|
||||
GOROOT=/goroot (was <nil>)
|
||||
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
|
||||
wantArgv: []string{
|
||||
"gocross", "build",
|
||||
"-trimpath",
|
||||
"-tags=tailscale_go,osusergo,netgo",
|
||||
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static'",
|
||||
"./cmd/tailcontrol",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "install_linux_amd64_to_linux_amd64",
|
||||
argv: []string{"gocross", "install", "./cmd/tailcontrol"},
|
||||
goroot: "/goroot",
|
||||
nativeGOOS: "linux",
|
||||
nativeGOARCH: "amd64",
|
||||
|
||||
envDiff: `CC=cc (was <nil>)
|
||||
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
|
||||
CGO_ENABLED=1 (was <nil>)
|
||||
CGO_LDFLAGS= (was <nil>)
|
||||
GOARCH=amd64 (was <nil>)
|
||||
GOARM=5 (was <nil>)
|
||||
GOMIPS=softfloat (was <nil>)
|
||||
GOOS=linux (was <nil>)
|
||||
GOROOT=/goroot (was <nil>)
|
||||
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
|
||||
wantArgv: []string{
|
||||
"gocross", "install",
|
||||
"-trimpath",
|
||||
"-tags=tailscale_go,osusergo,netgo",
|
||||
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static'",
|
||||
"./cmd/tailcontrol",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "linux_amd64_to_linux_riscv64",
|
||||
env: map[string]string{
|
||||
"GOARCH": "riscv64",
|
||||
},
|
||||
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
|
||||
goroot: "/goroot",
|
||||
nativeGOOS: "linux",
|
||||
nativeGOARCH: "amd64",
|
||||
|
||||
envDiff: `CC=cc (was <nil>)
|
||||
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
|
||||
CGO_ENABLED=0 (was <nil>)
|
||||
CGO_LDFLAGS= (was <nil>)
|
||||
GOARCH=riscv64 (was riscv64)
|
||||
GOARM=5 (was <nil>)
|
||||
GOMIPS=softfloat (was <nil>)
|
||||
GOOS=linux (was <nil>)
|
||||
GOROOT=/goroot (was <nil>)
|
||||
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
|
||||
wantArgv: []string{
|
||||
"gocross", "build",
|
||||
"-trimpath",
|
||||
"-tags=tailscale_go,osusergo,netgo",
|
||||
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static'",
|
||||
"./cmd/tailcontrol",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "linux_amd64_to_freebsd_amd64",
|
||||
env: map[string]string{
|
||||
"GOOS": "freebsd",
|
||||
},
|
||||
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
|
||||
goroot: "/goroot",
|
||||
nativeGOOS: "linux",
|
||||
nativeGOARCH: "amd64",
|
||||
|
||||
envDiff: `CC=cc (was <nil>)
|
||||
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
|
||||
CGO_ENABLED=0 (was <nil>)
|
||||
CGO_LDFLAGS= (was <nil>)
|
||||
GOARCH=amd64 (was <nil>)
|
||||
GOARM=5 (was <nil>)
|
||||
GOMIPS=softfloat (was <nil>)
|
||||
GOOS=freebsd (was freebsd)
|
||||
GOROOT=/goroot (was <nil>)
|
||||
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
|
||||
wantArgv: []string{
|
||||
"gocross", "build",
|
||||
"-trimpath",
|
||||
"-tags=tailscale_go",
|
||||
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg",
|
||||
"./cmd/tailcontrol",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "linux_amd64_to_linux_amd64_race",
|
||||
argv: []string{"gocross", "test", "-race", "./cmd/tailcontrol"},
|
||||
goroot: "/goroot",
|
||||
nativeGOOS: "linux",
|
||||
nativeGOARCH: "amd64",
|
||||
|
||||
envDiff: `CC=cc (was <nil>)
|
||||
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
|
||||
CGO_ENABLED=1 (was <nil>)
|
||||
CGO_LDFLAGS= (was <nil>)
|
||||
GOARCH=amd64 (was <nil>)
|
||||
GOARM=5 (was <nil>)
|
||||
GOMIPS=softfloat (was <nil>)
|
||||
GOOS=linux (was <nil>)
|
||||
GOROOT=/goroot (was <nil>)
|
||||
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
|
||||
wantArgv: []string{
|
||||
"gocross", "test",
|
||||
"-trimpath",
|
||||
"-tags=tailscale_go,osusergo,netgo",
|
||||
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static'",
|
||||
"-race",
|
||||
"./cmd/tailcontrol",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "linux_amd64_to_windows_amd64",
|
||||
env: map[string]string{
|
||||
"GOOS": "windows",
|
||||
},
|
||||
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
|
||||
goroot: "/goroot",
|
||||
nativeGOOS: "linux",
|
||||
nativeGOARCH: "amd64",
|
||||
|
||||
envDiff: `CC=cc (was <nil>)
|
||||
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
|
||||
CGO_ENABLED=0 (was <nil>)
|
||||
CGO_LDFLAGS= (was <nil>)
|
||||
GOARCH=amd64 (was <nil>)
|
||||
GOARM=5 (was <nil>)
|
||||
GOMIPS=softfloat (was <nil>)
|
||||
GOOS=windows (was windows)
|
||||
GOROOT=/goroot (was <nil>)
|
||||
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
|
||||
wantArgv: []string{
|
||||
"gocross", "build",
|
||||
"-trimpath",
|
||||
"-tags=tailscale_go",
|
||||
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg -H windows -s",
|
||||
"./cmd/tailcontrol",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "darwin_arm64_to_darwin_arm64",
|
||||
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
|
||||
goroot: "/goroot",
|
||||
nativeGOOS: "darwin",
|
||||
nativeGOARCH: "arm64",
|
||||
|
||||
envDiff: `CC=cc (was <nil>)
|
||||
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
|
||||
CGO_ENABLED=1 (was <nil>)
|
||||
CGO_LDFLAGS= (was <nil>)
|
||||
GOARCH=arm64 (was <nil>)
|
||||
GOARM=5 (was <nil>)
|
||||
GOMIPS=softfloat (was <nil>)
|
||||
GOOS=darwin (was <nil>)
|
||||
GOROOT=/goroot (was <nil>)
|
||||
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
|
||||
wantArgv: []string{
|
||||
"gocross", "build",
|
||||
"-trimpath",
|
||||
"-tags=tailscale_go,omitidna,omitpemdecrypt",
|
||||
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg",
|
||||
"./cmd/tailcontrol",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "darwin_arm64_to_darwin_amd64",
|
||||
env: map[string]string{
|
||||
"GOARCH": "amd64",
|
||||
},
|
||||
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
|
||||
goroot: "/goroot",
|
||||
nativeGOOS: "darwin",
|
||||
nativeGOARCH: "arm64",
|
||||
|
||||
envDiff: `CC=cc (was <nil>)
|
||||
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
|
||||
CGO_ENABLED=1 (was <nil>)
|
||||
CGO_LDFLAGS= (was <nil>)
|
||||
GOARCH=amd64 (was amd64)
|
||||
GOARM=5 (was <nil>)
|
||||
GOMIPS=softfloat (was <nil>)
|
||||
GOOS=darwin (was <nil>)
|
||||
GOROOT=/goroot (was <nil>)
|
||||
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
|
||||
wantArgv: []string{
|
||||
"gocross", "build",
|
||||
"-trimpath",
|
||||
"-tags=tailscale_go,omitidna,omitpemdecrypt",
|
||||
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg",
|
||||
"./cmd/tailcontrol",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "darwin_arm64_to_ios_arm64",
|
||||
env: map[string]string{
|
||||
"GOOS": "ios",
|
||||
},
|
||||
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
|
||||
goroot: "/goroot",
|
||||
nativeGOOS: "darwin",
|
||||
nativeGOARCH: "arm64",
|
||||
|
||||
envDiff: `CC=cc (was <nil>)
|
||||
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
|
||||
CGO_ENABLED=1 (was <nil>)
|
||||
CGO_LDFLAGS= (was <nil>)
|
||||
GOARCH=arm64 (was <nil>)
|
||||
GOARM=5 (was <nil>)
|
||||
GOMIPS=softfloat (was <nil>)
|
||||
GOOS=ios (was ios)
|
||||
GOROOT=/goroot (was <nil>)
|
||||
TS_LINK_FAIL_REFLECT=1 (was <nil>)`,
|
||||
wantArgv: []string{
|
||||
"gocross", "build",
|
||||
"-trimpath",
|
||||
"-tags=tailscale_go,omitidna,omitpemdecrypt",
|
||||
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg",
|
||||
"./cmd/tailcontrol",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "darwin_arm64_to_darwin_amd64_xcode",
|
||||
env: map[string]string{
|
||||
"GOOS": "darwin",
|
||||
"GOARCH": "amd64",
|
||||
"XCODE_VERSION_ACTUAL": "1300",
|
||||
"DEPLOYMENT_TARGET_CLANG_FLAG_NAME": "mmacosx-version-min",
|
||||
"MACOSX_DEPLOYMENT_TARGET": "11.3",
|
||||
"DEPLOYMENT_TARGET_CLANG_ENV_NAME": "MACOSX_DEPLOYMENT_TARGET",
|
||||
"SDKROOT": "/my/sdk/root",
|
||||
},
|
||||
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
|
||||
goroot: "/goroot",
|
||||
nativeGOOS: "darwin",
|
||||
nativeGOARCH: "arm64",
|
||||
|
||||
envDiff: `CC=cc (was <nil>)
|
||||
CGO_CFLAGS=-O3 -std=gnu11 -mmacosx-version-min=11.3 -isysroot /my/sdk/root -arch x86_64 (was <nil>)
|
||||
CGO_ENABLED=1 (was <nil>)
|
||||
CGO_LDFLAGS=-mmacosx-version-min=11.3 -isysroot /my/sdk/root -arch x86_64 (was <nil>)
|
||||
GOARCH=amd64 (was amd64)
|
||||
GOARM=5 (was <nil>)
|
||||
GOMIPS=softfloat (was <nil>)
|
||||
GOOS=darwin (was darwin)
|
||||
GOROOT=/goroot (was <nil>)
|
||||
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
|
||||
wantArgv: []string{
|
||||
"gocross", "build",
|
||||
"-trimpath",
|
||||
"-tags=tailscale_go,omitidna,omitpemdecrypt",
|
||||
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg -w",
|
||||
"./cmd/tailcontrol",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "linux_amd64_to_linux_amd64_in_goroot",
|
||||
argv: []string{"go", "build", "./cmd/tailcontrol"},
|
||||
goroot: "/special/toolchain/path",
|
||||
nativeGOOS: "linux",
|
||||
nativeGOARCH: "amd64",
|
||||
|
||||
envDiff: `CC=cc (was <nil>)
|
||||
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
|
||||
CGO_ENABLED=1 (was <nil>)
|
||||
CGO_LDFLAGS= (was <nil>)
|
||||
GOARCH=amd64 (was <nil>)
|
||||
GOARM=5 (was <nil>)
|
||||
GOMIPS=softfloat (was <nil>)
|
||||
GOOS=linux (was <nil>)
|
||||
GOROOT=/special/toolchain/path (was <nil>)
|
||||
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
|
||||
wantArgv: []string{
|
||||
"go", "build",
|
||||
"-trimpath",
|
||||
"-tags=tailscale_go,osusergo,netgo",
|
||||
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static'",
|
||||
"./cmd/tailcontrol",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "linux_list_amd64_to_linux_amd64",
|
||||
argv: []string{"gocross", "list", "./cmd/tailcontrol"},
|
||||
goroot: "/goroot",
|
||||
nativeGOOS: "linux",
|
||||
nativeGOARCH: "amd64",
|
||||
|
||||
envDiff: `CC=cc (was <nil>)
|
||||
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
|
||||
CGO_ENABLED=1 (was <nil>)
|
||||
CGO_LDFLAGS= (was <nil>)
|
||||
GOARCH=amd64 (was <nil>)
|
||||
GOARM=5 (was <nil>)
|
||||
GOMIPS=softfloat (was <nil>)
|
||||
GOOS=linux (was <nil>)
|
||||
GOROOT=/goroot (was <nil>)
|
||||
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
|
||||
wantArgv: []string{
|
||||
"gocross", "list",
|
||||
"-trimpath",
|
||||
"-tags=tailscale_go,osusergo,netgo",
|
||||
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static'",
|
||||
"./cmd/tailcontrol",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "linux_amd64_to_linux_amd64_with_extra_glibc_path",
|
||||
env: map[string]string{
|
||||
"GOCROSS_GLIBC_DIR": "/my/glibc/path",
|
||||
},
|
||||
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
|
||||
goroot: "/goroot",
|
||||
nativeGOOS: "linux",
|
||||
nativeGOARCH: "amd64",
|
||||
|
||||
envDiff: `CC=cc (was <nil>)
|
||||
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
|
||||
CGO_ENABLED=1 (was <nil>)
|
||||
CGO_LDFLAGS= (was <nil>)
|
||||
GOARCH=amd64 (was <nil>)
|
||||
GOARM=5 (was <nil>)
|
||||
GOMIPS=softfloat (was <nil>)
|
||||
GOOS=linux (was <nil>)
|
||||
GOROOT=/goroot (was <nil>)
|
||||
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
|
||||
wantArgv: []string{
|
||||
"gocross", "build",
|
||||
"-trimpath",
|
||||
"-tags=tailscale_go,osusergo,netgo",
|
||||
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static -L /my/glibc/path'",
|
||||
"./cmd/tailcontrol",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
getver := func() mkversion.VersionInfo { return fakeVersion }
|
||||
env := newEnvironmentForTest(test.env, nil, nil)
|
||||
|
||||
gotArgv, env, err := autoflagsForTest(test.argv, env, test.goroot, test.nativeGOOS, test.nativeGOARCH, getver)
|
||||
if err != nil {
|
||||
t.Fatalf("newAutoflagsForTest failed: %v", err)
|
||||
}
|
||||
|
||||
if diff := env.Diff(); diff != test.envDiff {
|
||||
t.Errorf("wrong environment diff, got:\n%s\n\nwant:\n%s", diff, test.envDiff)
|
||||
}
|
||||
if !reflect.DeepEqual(gotArgv, test.wantArgv) {
|
||||
t.Errorf("wrong argv:\n got : %s\n want: %s", formatArgv(gotArgv), formatArgv(test.wantArgv))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
131
tool/gocross/env.go
Normal file
131
tool/gocross/env.go
Normal file
@ -0,0 +1,131 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Environment starts from an initial set of environment variables, and tracks
|
||||
// mutations to the environment. It can then apply those mutations to the
|
||||
// environment, or produce debugging output that illustrates the changes it
|
||||
// would make.
|
||||
type Environment struct {
|
||||
init map[string]string
|
||||
set map[string]string
|
||||
unset map[string]bool
|
||||
|
||||
setenv func(string, string) error
|
||||
unsetenv func(string) error
|
||||
}
|
||||
|
||||
// NewEnvironment returns an Environment initialized from os.Environ.
|
||||
func NewEnvironment() *Environment {
|
||||
init := map[string]string{}
|
||||
for _, env := range os.Environ() {
|
||||
fs := strings.SplitN(env, "=", 2)
|
||||
if len(fs) != 2 {
|
||||
panic("bad environ provided")
|
||||
}
|
||||
init[fs[0]] = fs[1]
|
||||
}
|
||||
|
||||
return newEnvironmentForTest(init, os.Setenv, os.Unsetenv)
|
||||
}
|
||||
|
||||
func newEnvironmentForTest(init map[string]string, setenv func(string, string) error, unsetenv func(string) error) *Environment {
|
||||
return &Environment{
|
||||
init: init,
|
||||
set: map[string]string{},
|
||||
unset: map[string]bool{},
|
||||
setenv: setenv,
|
||||
unsetenv: unsetenv,
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets the environment variable k to v.
|
||||
func (e *Environment) Set(k, v string) {
|
||||
e.set[k] = v
|
||||
delete(e.unset, k)
|
||||
}
|
||||
|
||||
// Unset removes the environment variable k.
|
||||
func (e *Environment) Unset(k string) {
|
||||
delete(e.set, k)
|
||||
e.unset[k] = true
|
||||
}
|
||||
|
||||
// IsSet reports whether the environment variable k is set.
|
||||
func (e *Environment) IsSet(k string) bool {
|
||||
if e.unset[k] {
|
||||
return false
|
||||
}
|
||||
if _, ok := e.init[k]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := e.set[k]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Get returns the value of the environment variable k, or defaultVal if it is
|
||||
// not set.
|
||||
func (e *Environment) Get(k, defaultVal string) string {
|
||||
if e.unset[k] {
|
||||
return defaultVal
|
||||
}
|
||||
if v, ok := e.set[k]; ok {
|
||||
return v
|
||||
}
|
||||
if v, ok := e.init[k]; ok {
|
||||
return v
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
// Apply applies all pending mutations to the environment.
|
||||
func (e *Environment) Apply() error {
|
||||
for k, v := range e.set {
|
||||
if err := e.setenv(k, v); err != nil {
|
||||
return fmt.Errorf("setting %q: %v", k, err)
|
||||
}
|
||||
e.init[k] = v
|
||||
delete(e.set, k)
|
||||
}
|
||||
for k := range e.unset {
|
||||
if err := e.unsetenv(k); err != nil {
|
||||
return fmt.Errorf("unsetting %q: %v", k, err)
|
||||
}
|
||||
delete(e.init, k)
|
||||
delete(e.unset, k)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Diff returns a string describing the pending mutations to the environment.
|
||||
func (e *Environment) Diff() string {
|
||||
lines := make([]string, 0, len(e.set)+len(e.unset))
|
||||
for k, v := range e.set {
|
||||
old, ok := e.init[k]
|
||||
if ok {
|
||||
lines = append(lines, fmt.Sprintf("%s=%s (was %s)", k, v, old))
|
||||
} else {
|
||||
lines = append(lines, fmt.Sprintf("%s=%s (was <nil>)", k, v))
|
||||
}
|
||||
}
|
||||
for k := range e.unset {
|
||||
old, ok := e.init[k]
|
||||
if ok {
|
||||
lines = append(lines, fmt.Sprintf("%s=<nil> (was %s)", k, old))
|
||||
} else {
|
||||
lines = append(lines, fmt.Sprintf("%s=<nil> (was <nil>)", k))
|
||||
}
|
||||
}
|
||||
sort.Strings(lines)
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
99
tool/gocross/env_test.go
Normal file
99
tool/gocross/env_test.go
Normal file
@ -0,0 +1,99 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestEnv(t *testing.T) {
|
||||
|
||||
var (
|
||||
init = map[string]string{
|
||||
"FOO": "bar",
|
||||
}
|
||||
|
||||
wasSet = map[string]string{}
|
||||
wasUnset = map[string]bool{}
|
||||
|
||||
setenv = func(k, v string) error {
|
||||
wasSet[k] = v
|
||||
return nil
|
||||
}
|
||||
unsetenv = func(k string) error {
|
||||
wasUnset[k] = true
|
||||
return nil
|
||||
}
|
||||
)
|
||||
|
||||
env := newEnvironmentForTest(init, setenv, unsetenv)
|
||||
|
||||
if got, want := env.Get("FOO", ""), "bar"; got != want {
|
||||
t.Errorf(`env.Get("FOO") = %q, want %q`, got, want)
|
||||
}
|
||||
if got, want := env.IsSet("FOO"), true; got != want {
|
||||
t.Errorf(`env.IsSet("FOO") = %v, want %v`, got, want)
|
||||
}
|
||||
|
||||
if got, want := env.Get("BAR", "defaultVal"), "defaultVal"; got != want {
|
||||
t.Errorf(`env.Get("BAR") = %q, want %q`, got, want)
|
||||
}
|
||||
if got, want := env.IsSet("BAR"), false; got != want {
|
||||
t.Errorf(`env.IsSet("BAR") = %v, want %v`, got, want)
|
||||
}
|
||||
|
||||
env.Set("BAR", "quux")
|
||||
if got, want := env.Get("BAR", ""), "quux"; got != want {
|
||||
t.Errorf(`env.Get("BAR") = %q, want %q`, got, want)
|
||||
}
|
||||
if got, want := env.IsSet("BAR"), true; got != want {
|
||||
t.Errorf(`env.IsSet("BAR") = %v, want %v`, got, want)
|
||||
}
|
||||
diff := "BAR=quux (was <nil>)"
|
||||
if got := env.Diff(); got != diff {
|
||||
t.Errorf("env.Diff() = %q, want %q", got, diff)
|
||||
}
|
||||
|
||||
env.Set("FOO", "foo2")
|
||||
if got, want := env.Get("FOO", ""), "foo2"; got != want {
|
||||
t.Errorf(`env.Get("FOO") = %q, want %q`, got, want)
|
||||
}
|
||||
if got, want := env.IsSet("FOO"), true; got != want {
|
||||
t.Errorf(`env.IsSet("FOO") = %v, want %v`, got, want)
|
||||
}
|
||||
diff = `BAR=quux (was <nil>)
|
||||
FOO=foo2 (was bar)`
|
||||
if got := env.Diff(); got != diff {
|
||||
t.Errorf("env.Diff() = %q, want %q", got, diff)
|
||||
}
|
||||
|
||||
env.Unset("FOO")
|
||||
if got, want := env.Get("FOO", "default"), "default"; got != want {
|
||||
t.Errorf(`env.Get("FOO") = %q, want %q`, got, want)
|
||||
}
|
||||
if got, want := env.IsSet("FOO"), false; got != want {
|
||||
t.Errorf(`env.IsSet("FOO") = %v, want %v`, got, want)
|
||||
}
|
||||
diff = `BAR=quux (was <nil>)
|
||||
FOO=<nil> (was bar)`
|
||||
if got := env.Diff(); got != diff {
|
||||
t.Errorf("env.Diff() = %q, want %q", got, diff)
|
||||
}
|
||||
|
||||
if err := env.Apply(); err != nil {
|
||||
t.Fatalf("env.Apply() failed: %v", err)
|
||||
}
|
||||
|
||||
wantSet := map[string]string{"BAR": "quux"}
|
||||
wantUnset := map[string]bool{"FOO": true}
|
||||
|
||||
if diff := cmp.Diff(wasSet, wantSet); diff != "" {
|
||||
t.Errorf("env.Apply didn't set as expected (-got+want):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(wasUnset, wantUnset); diff != "" {
|
||||
t.Errorf("env.Apply didn't unset as expected (-got+want):\n%s", diff)
|
||||
}
|
||||
}
|
20
tool/gocross/exec_other.go
Normal file
20
tool/gocross/exec_other.go
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !unix
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func doExec(cmd string, args []string, env []string) error {
|
||||
c := exec.Command(cmd, args...)
|
||||
c.Env = env
|
||||
c.Stdin = os.Stdin
|
||||
c.Stdout = os.Stdout
|
||||
c.Stderr = os.Stderr
|
||||
return c.Run()
|
||||
}
|
12
tool/gocross/exec_unix.go
Normal file
12
tool/gocross/exec_unix.go
Normal file
@ -0,0 +1,12 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build unix
|
||||
|
||||
package main
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
func doExec(cmd string, args []string, env []string) error {
|
||||
return unix.Exec(cmd, args, env)
|
||||
}
|
72
tool/gocross/gocross-wrapper.sh
Executable file
72
tool/gocross/gocross-wrapper.sh
Executable file
@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env sh
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#
|
||||
# gocross-wrapper.sh is a wrapper that can be aliased to 'go', which
|
||||
# transparently builds gocross using a "bootstrap" Go toolchain, and
|
||||
# then invokes gocross.
|
||||
|
||||
set -eu
|
||||
|
||||
if [ "${CI:-}" = "true" ]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
repo_root="$(dirname $0)/../.."
|
||||
|
||||
toolchain="$HOME/.cache/tailscale-go"
|
||||
|
||||
if [ ! -d "$toolchain" ]; then
|
||||
mkdir -p "$HOME/.cache"
|
||||
|
||||
# We need any Go toolchain to build gocross, but the toolchain also has to
|
||||
# be reasonably recent because we upgrade eagerly and gocross might not
|
||||
# build with Go N-1. So, if we have no cached tailscale toolchain at all,
|
||||
# fetch the initial one in shell. Once gocross is built, it'll manage
|
||||
# updates.
|
||||
read -r REV <$repo_root/go.toolchain.rev
|
||||
|
||||
# This works for linux and darwin, which is sufficient
|
||||
# (we do not build tailscale-go for other targets).
|
||||
HOST_OS=$(uname -s | tr A-Z a-z)
|
||||
HOST_ARCH="$(uname -m)"
|
||||
if [ "$HOST_ARCH" = "aarch64" ]; then
|
||||
# Go uses the name "arm64".
|
||||
HOST_ARCH="arm64"
|
||||
elif [ "$HOST_ARCH" = "x86_64" ]; then
|
||||
# Go uses the name "amd64".
|
||||
HOST_ARCH="amd64"
|
||||
fi
|
||||
|
||||
rm -rf "$toolchain" "$toolchain.extracted"
|
||||
curl -f -L -o "$toolchain.tar.gz" "https://github.com/tailscale/go/releases/download/build-${REV}/${HOST_OS}-${HOST_ARCH}.tar.gz"
|
||||
mkdir -p "$toolchain"
|
||||
(cd "$toolchain" && tar --strip-components=1 -xf "$toolchain.tar.gz")
|
||||
echo "$REV" >"$toolchain.extracted"
|
||||
fi
|
||||
|
||||
# Binaries run with `gocross run` can reinvoke gocross, resulting in a
|
||||
# potentially fancy build that invokes external linkers, might be
|
||||
# cross-building for other targets, and so forth. In one hilarious
|
||||
# case, cmd/cloner invokes go with GO111MODULE=off at some stage.
|
||||
#
|
||||
# Anyway, build gocross in a stripped down universe.
|
||||
gocross_path="$repo_root/gocross"
|
||||
gocross_ok=0
|
||||
if [ -x "$gocross_path" ]; then
|
||||
gotver="$($gocross_path gocross-version 2>/dev/null || echo '')"
|
||||
wantver="$(git rev-parse HEAD)"
|
||||
if [ "$gotver" = "$wantver" ]; then
|
||||
gocross_ok=1
|
||||
fi
|
||||
fi
|
||||
if [ "$gocross_ok" = "0" ]; then
|
||||
(
|
||||
unset GOOS
|
||||
unset GOARCH
|
||||
unset GO111MODULE
|
||||
export CGO_ENABLED=0
|
||||
"$toolchain/bin/go" build -o "$gocross_path" tailscale.com/tool/gocross
|
||||
)
|
||||
fi
|
||||
exec "$gocross_path" "$@"
|
132
tool/gocross/gocross.go
Normal file
132
tool/gocross/gocross.go
Normal file
@ -0,0 +1,132 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// gocross is a wrapper around the `go` tool that invokes `go` from Tailscale's
|
||||
// custom toolchain, with the right build parameters injected based on the
|
||||
// native+target GOOS/GOARCH.
|
||||
//
|
||||
// In short, when aliased to `go`, using `go build`, `go test` behave like the
|
||||
// upstream Go tools, but produce correctly configured, correctly linked
|
||||
// binaries stamped with version information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
runtimeDebug "runtime/debug"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 {
|
||||
// These additional subcommands are various support commands to handle
|
||||
// integration with Tailscale's existing build system. Unless otherwise
|
||||
// specified, these are not stable APIs, and may change or go away at
|
||||
// any time.
|
||||
switch os.Args[1] {
|
||||
case "gocross-version":
|
||||
hash, err := embeddedCommit()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "getting commit hash: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(hash)
|
||||
os.Exit(0)
|
||||
case "is-gocross":
|
||||
// This subcommand exits with an error code when called on a
|
||||
// regular go binary, so it can be used to detect when `go` is
|
||||
// actually gocross.
|
||||
os.Exit(0)
|
||||
case "make-goroot":
|
||||
_, gorootDir, err := getToolchain()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "getting toolchain: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println(gorootDir)
|
||||
os.Exit(0)
|
||||
case "gocross-get-toolchain-go":
|
||||
toolchain, _, err := getToolchain()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "getting toolchain: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(filepath.Join(toolchain, "bin/go"))
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
toolchain, goroot, err := getToolchain()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "getting toolchain: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
args := os.Args
|
||||
if os.Getenv("GOCROSS_BYPASS") == "" {
|
||||
newArgv, env, err := Autoflags(os.Args, goroot)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "computing flags: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Make sure the right version of cmd/go is the first thing in the PATH
|
||||
// for tests that execute `go build` or `go test`.
|
||||
// TODO: if we really need to do this, do it inside Autoflags, not here.
|
||||
path := filepath.Join(toolchain, "bin") + string(os.PathListSeparator) + os.Getenv("PATH")
|
||||
env.Set("PATH", path)
|
||||
|
||||
debug("Input: %s\n", formatArgv(os.Args))
|
||||
debug("Command: %s\n", formatArgv(newArgv))
|
||||
debug("Set the following flags/envvars:\n%s\n", env.Diff())
|
||||
|
||||
args = newArgv
|
||||
if err := env.Apply(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "modifying environment: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
doExec(filepath.Join(toolchain, "bin/go"), args, os.Environ())
|
||||
}
|
||||
|
||||
func debug(format string, args ...interface{}) {
|
||||
debug := os.Getenv("GOCROSS_DEBUG")
|
||||
var (
|
||||
out *os.File
|
||||
err error
|
||||
)
|
||||
switch debug {
|
||||
case "0", "":
|
||||
return
|
||||
case "1":
|
||||
out = os.Stderr
|
||||
default:
|
||||
out, err = os.OpenFile(debug, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0640)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "opening debug file %q: %v", debug, err)
|
||||
out = os.Stderr
|
||||
} else {
|
||||
defer out.Close() // May lose some write errors, but we don't care.
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, format, args...)
|
||||
}
|
||||
|
||||
func embeddedCommit() (string, error) {
|
||||
bi, ok := runtimeDebug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return "", fmt.Errorf("no build info")
|
||||
}
|
||||
for _, s := range bi.Settings {
|
||||
if s.Key == "vcs.revision" {
|
||||
return s.Value, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no git commit found")
|
||||
}
|
90
tool/gocross/goroot.go
Normal file
90
tool/gocross/goroot.go
Normal file
@ -0,0 +1,90 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// makeGoroot constructs a GOROOT-like file structure in outPath,
|
||||
// which consists of toolchainRoot except for the `go` binary, which
|
||||
// points to gocross.
|
||||
//
|
||||
// It's useful for integrating with tooling that expects to be handed
|
||||
// a GOROOT, like the Goland IDE or depaware.
|
||||
func makeGoroot(toolchainRoot, outPath string) error {
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting gocross's path: %v", err)
|
||||
}
|
||||
|
||||
os.RemoveAll(outPath)
|
||||
if err := os.MkdirAll(filepath.Join(outPath, "bin"), 0750); err != nil {
|
||||
return fmt.Errorf("making %q: %v", outPath, err)
|
||||
}
|
||||
if err := os.Symlink(self, filepath.Join(outPath, "bin/go")); err != nil {
|
||||
return fmt.Errorf("linking gocross into outpath: %v", err)
|
||||
}
|
||||
|
||||
if err := linkFarm(toolchainRoot, outPath); err != nil {
|
||||
return fmt.Errorf("creating GOROOT link farm: %v", err)
|
||||
}
|
||||
if err := linkFarm(filepath.Join(toolchainRoot, "bin"), filepath.Join(outPath, "bin")); err != nil {
|
||||
return fmt.Errorf("creating GOROOT/bin link farm: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
s, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening %q: %v", src, err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
d, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening %q: %v", dst, err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(d, s); err != nil {
|
||||
d.Close()
|
||||
return fmt.Errorf("copying %q to %q: %v", src, dst, err)
|
||||
}
|
||||
|
||||
if err := d.Close(); err != nil {
|
||||
return fmt.Errorf("closing %q: %v", dst, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// linkFarm symlinks every entry in srcDir into outDir, unless that
|
||||
// directory entry already exists.
|
||||
func linkFarm(srcDir, outDir string) error {
|
||||
ents, err := os.ReadDir(srcDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading %q: %v", srcDir, err)
|
||||
}
|
||||
|
||||
for _, ent := range ents {
|
||||
dst := filepath.Join(outDir, ent.Name())
|
||||
_, err := os.Lstat(dst)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
if err := os.Symlink(filepath.Join(srcDir, ent.Name()), dst); err != nil {
|
||||
return fmt.Errorf("symlinking %q to %q: %v", ent.Name(), outDir, err)
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("stat-ing %q: %v", dst, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
173
tool/gocross/toolchain.go
Normal file
173
tool/gocross/toolchain.go
Normal file
@ -0,0 +1,173 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func toolchainRev() (string, error) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting CWD: %v", err)
|
||||
}
|
||||
d := cwd
|
||||
findTopLevel:
|
||||
for {
|
||||
if _, err := os.Lstat(filepath.Join(d, ".git")); err == nil {
|
||||
break findTopLevel
|
||||
} else if !os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("finding .git: %v", err)
|
||||
}
|
||||
d = filepath.Dir(d)
|
||||
if d == "/" {
|
||||
return "", fmt.Errorf("couldn't find .git starting from %q, cannot manage toolchain", cwd)
|
||||
}
|
||||
}
|
||||
|
||||
return readRevFile(filepath.Join(d, "go.toolchain.rev"))
|
||||
}
|
||||
|
||||
func readRevFile(path string) (string, error) {
|
||||
bs, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return string(bytes.TrimSpace(bs)), nil
|
||||
}
|
||||
|
||||
func getToolchain() (toolchainDir, gorootDir string, err error) {
|
||||
cache := filepath.Join(os.Getenv("HOME"), ".cache")
|
||||
toolchainDir = filepath.Join(cache, "tailscale-go")
|
||||
gorootDir = filepath.Join(toolchainDir, "gocross-goroot")
|
||||
|
||||
// You might wonder why getting the toolchain also provisions and returns a
|
||||
// path suitable for use as GOROOT. Wonder no longer!
|
||||
//
|
||||
// A bunch of our tests and build processes involve re-invoking 'go build'
|
||||
// or other build-ish commands (install, run, ...). These typically use
|
||||
// runtime.GOROOT + "bin/go" to get at the Go binary. Even more edge case-y,
|
||||
// tailscale.com/cmd/tsconnect needs to fish a javascript glue file out of
|
||||
// GOROOT in order to build the javascript bundle for serving.
|
||||
//
|
||||
// Gocross always does a -trimpath on builds for reproducibility, which
|
||||
// wipes out the burned-in runtime.GOROOT value from the binary. This means
|
||||
// that using gocross on these various test and build processes ends up
|
||||
// breaking with mysterious path errors.
|
||||
//
|
||||
// We don't want to stop using -trimpath, or otherwise make GOROOT work in
|
||||
// "normal" builds, because that is a footgun that lets people accidentally
|
||||
// create assumptions that the build toolchain is still around at runtime.
|
||||
// Instead, we want to make 'go test' and 'go run' have access to GOROOT,
|
||||
// while still removing it from standalone binaries.
|
||||
//
|
||||
// So, construct and pass a GOROOT to the actual 'go' invocation, which lets
|
||||
// tests and build processes locate and use GOROOT. For consistency, the
|
||||
// GOROOT that's passed in is a symlink farm that mostly points to the
|
||||
// toolchain's underlying GOROOT, but 'bin/go' points back to gocross. This
|
||||
// means that if you invoke 'go test' via gocross, and that test tries to
|
||||
// build code, that build will also end up using gocross.
|
||||
|
||||
if err := ensureToolchain(cache, toolchainDir); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if err := ensureGoroot(toolchainDir, gorootDir); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return toolchainDir, gorootDir, nil
|
||||
}
|
||||
|
||||
func ensureToolchain(cacheDir, toolchainDir string) error {
|
||||
stampFile := toolchainDir + ".extracted"
|
||||
|
||||
wantRev, err := toolchainRev()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gotRev, err := readRevFile(stampFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading stamp file %q: %v", stampFile, err)
|
||||
}
|
||||
if gotRev == wantRev {
|
||||
// Toolchain already good.
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(toolchainDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.RemoveAll(stampFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := downloadCachedgo(toolchainDir, wantRev); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(stampFile, []byte(wantRev), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureGoroot(toolchainDir, gorootDir string) error {
|
||||
if _, err := os.Stat(gorootDir); err == nil {
|
||||
return nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return makeGoroot(toolchainDir, gorootDir)
|
||||
|
||||
}
|
||||
|
||||
func downloadCachedgo(toolchainDir, toolchainRev string) error {
|
||||
url := fmt.Sprintf("https://github.com/tailscale/go/releases/download/build-%s/%s-%s.tar.gz", toolchainRev, runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
archivePath := toolchainDir + ".tar.gz"
|
||||
f, err := os.Create(archivePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("failed to get %q: %v", url, resp.Status)
|
||||
}
|
||||
if _, err := io.Copy(f, resp.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(toolchainDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command("tar", "--strip-components=1", "-xf", archivePath)
|
||||
cmd.Dir = toolchainDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(archivePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user