Skip to content

Commit

Permalink
tool/gocross: a tool for building Tailscale binaries
Browse files Browse the repository at this point in the history
Signed-off-by: David Anderson <danderson@tailscale.com>
  • Loading branch information
danderson committed Feb 22, 2023
1 parent 0b8f89c commit 860734a
Show file tree
Hide file tree
Showing 12 changed files with 1,324 additions and 78 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ cmd/tailscaled/tailscaled

# Ignore direnv nix-shell environment cache
.direnv/

/gocross
79 changes: 1 addition & 78 deletions tool/go
Original file line number Diff line number Diff line change
Expand Up @@ -4,81 +4,4 @@
# currently-desired version from https://github.com/tailscale/go,
# downloading it first if necessary.

set -eu

log() {
echo "$@" >&2
}

DEFAULT_TOOLCHAIN_DIR="${HOME}/.cache/tailscale-go"
TOOLCHAIN="${TOOLCHAIN-${DEFAULT_TOOLCHAIN_DIR}}"
TOOLCHAIN_GO="${TOOLCHAIN}/bin/go"
read -r REV < "$(dirname "$0")/../go.toolchain.rev"

# Fast, quiet path, when Tailscale is already current.
if [ -e "${TOOLCHAIN_GO}" ]; then
short_hash=$("${TOOLCHAIN_GO}" version | sed 's/.*-ts//; s/ .*//')
case $REV in
"$short_hash"*)
unset GOROOT
exec "${TOOLCHAIN_GO}" "$@"
esac
fi

# This works for linux and darwin, which is sufficient
# (we do not build tailscale-go for other targets).
host_os=$(uname -s | tr A-Z a-z)
host_arch="$(uname -m)"
if [ "$host_arch" = "aarch64" ]; then
# Go uses the name "arm64".
host_arch="arm64"
elif [ "$host_arch" = "x86_64" ]; then
# Go uses the name "amd64".
host_arch="amd64"
fi

get_cached() {
if [ ! -d "$TOOLCHAIN" ]; then
mkdir -p "$TOOLCHAIN"
fi

archive="$TOOLCHAIN-$REV.tar.gz"
mark="$TOOLCHAIN.extracted"
extracted=

# Ignore the error from read, which may error if the mark file does not contain a line end.
read -r extracted < "$mark" || true

if [ "$extracted" = "$REV" ] && [ -e "${TOOLCHAIN_GO}" ]; then
# already ok
log "Go toolchain '$REV' already extracted."
return 0
fi

rm -f "$archive.new" "$TOOLCHAIN.extracted"
if [ ! -e "$archive" ]; then
log "Need to download go '$REV'."
curl -f -L -o "$archive.new" "https://github.com/tailscale/go/releases/download/build-${REV}/${host_os}-${host_arch}.tar.gz"
rm -f "$archive"
mv "$archive.new" "$archive"
fi

log "Extracting tailscale/go rev '$REV'" >&2
log " into '$TOOLCHAIN'." >&2
rm -rf "$TOOLCHAIN"
mkdir -p "$TOOLCHAIN"
(cd "$TOOLCHAIN" && tar --strip-components=1 -xf "$archive")
echo "$REV" >$mark
}

if [ "${REV}" = "SKIP" ] ||
[ "${host_os}" != "darwin" -a "${host_os}" != "linux" ] ||
[ "${host_arch}" != "amd64" -a "${host_arch}" != "arm64" ]; then
# Use whichever go is available
exec go "$@"
else
get_cached
fi

unset GOROOT
exec "${TOOLCHAIN_GO}" "$@"
exec "$(dirname "$0")/../tool/gocross/gocross-wrapper.sh" "$@"
183 changes: 183 additions & 0 deletions tool/gocross/autoflags.go
Original file line number Diff line number Diff line change
@@ -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()
}
Loading

0 comments on commit 860734a

Please sign in to comment.