mirror of https://github.com/tailscale/tailscale/
tool/gocross: a tool for building Tailscale binaries
Signed-off-by: David Anderson <danderson@tailscale.com>pull/7344/head
parent
0b8f89c79c
commit
860734aed9
@ -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()
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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")
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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" "$@"
|
@ -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")
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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