Signed-off-by: David Anderson <danderson@tailscale.com>main
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