tailscale/tool/gocross/autoflags.go
James Tucker 538c2e8f7c tool/gocross: add debug data to CGO builds
We don't build a lot of tools with CGO, but we do build some, and it's
extremely valuable for production services in particular to have symbols
included - for perf and so on.

I tested various other builds that could be affected negatively, in
particular macOS/iOS, but those use split-dwarf already as part of their
build path, and Android which does not currently use gocross.

One binary which is normally 120mb only grew to 123mb, so the trade-off
is definitely worthwhile in context.

Updates tailscale/corp#20296

Signed-off-by: James Tucker <james@tailscale.com>
2024-05-22 20:47:28 -07:00

275 lines
9.4 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"cmp"
"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 = ""
cc = "cc"
targetOS = cmp.Or(env.Get("GOOS", ""), nativeGOOS)
targetArch = cmp.Or(env.Get("GOARCH", ""), nativeGOARCH)
buildFlags = []string{"-trimpath"}
cgoCflags = []string{"-O3", "-std=gnu11", "-g"}
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 "android":
cgo = env.Get("CGO_ENABLED", "0") == "1"
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 "windowsdll":
// Fake GOOS that translates to "windows, but building .dlls not .exes"
targetOS = "windows"
cgo = true
buildFlags = append(buildFlags, "-buildmode=c-shared")
ldflags = append(ldflags, "-H", "windows", "-s")
cgoLdflags = append(cgoLdflags, "-static")
var mingwArch string
switch targetArch {
case "amd64":
mingwArch = "x86_64"
case "386":
mingwArch = "i686"
default:
return nil, nil, fmt.Errorf("unsupported GOARCH=%q when building with cgo", targetArch)
}
cc = fmt.Sprintf("%s-w64-mingw32-gcc", mingwArch)
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") {
// If we're building via Xcode, we must be making the extension
// version (as opposed to tailscaled on Mac).
tags = append(tags, "ts_macext")
var xcodeFlags []string
// Minimum OS version being targeted, results in
// e.g. -mmacosx-version-min=11.3, -miphoneos-version-min=15.0
switch {
case env.IsSet("XROS_DEPLOYMENT_TARGET"):
if env.Get("TARGET_DEVICE_PLATFORM_NAME", "") == "xrsimulator" {
xcodeFlags = append(xcodeFlags, "-mtargetos=xros"+env.Get("XROS_DEPLOYMENT_TARGET", "")+"-simulator")
} else {
xcodeFlags = append(xcodeFlags, "-mtargetos=xros"+env.Get("XROS_DEPLOYMENT_TARGET", ""))
}
case env.IsSet("IPHONEOS_DEPLOYMENT_TARGET"):
if env.Get("TARGET_DEVICE_PLATFORM_NAME", "") == "iphonesimulator" {
xcodeFlags = append(xcodeFlags, "-miphonesimulator-version-min="+env.Get("IPHONEOS_DEPLOYMENT_TARGET", ""))
} else {
xcodeFlags = append(xcodeFlags, "-miphoneos-version-min="+env.Get("IPHONEOS_DEPLOYMENT_TARGET", ""))
}
case env.IsSet("MACOSX_DEPLOYMENT_TARGET"):
xcodeFlags = append(xcodeFlags, "-mmacosx-version-min="+env.Get("MACOSX_DEPLOYMENT_TARGET", ""))
case env.IsSet("TVOS_DEPLOYMENT_TARGET"):
xcodeFlags = append(xcodeFlags, "-mtvos-version-min="+env.Get("TVOS_DEPLOYMENT_TARGET", ""))
default:
return nil, nil, fmt.Errorf("invoked by Xcode but couldn't figure out deployment target. Did Xcode change its envvars again?")
}
// 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
filteredArgvPostSubcmd, originalTags := extractTags(argv[1], argv[2:])
newArgv = append(newArgv, buildFlags...)
tags = append(tags, originalTags...)
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, filteredArgvPostSubcmd...)
env.Set("GOOS", targetOS)
env.Set("GOARCH", targetArch)
if !env.IsSet("GOARM") {
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)
env.Set("GOTOOLCHAIN", "local")
if subcommand == "env" {
return argv, env, nil
}
return newArgv, env, nil
}
// extractTags parses out "-tags=foo,bar" (or double hyphen or "-tags",
// "foo,bar") in its various forms and returns v filtered to remove the 0, 1 or
// 2 build tag elements, then the tags parsed, split on commas ("foo", "bar").
func extractTags(gocmd string, v []string) (filtered, tags []string) {
for len(v) > 0 {
e := v[0]
if strings.HasPrefix(e, "--tags=") {
e = e[1:] // remove one of the hyphens for the next line
}
if suf, ok := strings.CutPrefix(e, "-tags="); ok {
v = v[1:]
if suf != "" {
tags = strings.Split(suf, ",")
}
continue
}
if e == "-tags" || e == "--tags" {
v = v[1:]
if len(v) > 0 {
tagStr := v[0]
v = v[1:]
if tagStr != "" {
tags = strings.Split(tagStr, ",")
}
}
continue
}
if gocmd == "run" && !strings.HasPrefix(e, "-") {
// go run can include arguments to pass to the program
// being run. They all appear after the name of the
// package or Go file to run, so when we hit the first
// non-flag positional argument, stop extracting tags and
// wrap up.
filtered = append(filtered, v...)
break
}
filtered = append(filtered, e)
v = v[1:]
}
return filtered, tags
}
// 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()
}