// 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{} 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] } if subcommand != "test" { buildFlags = append(buildFlags, "-trimpath") } 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"): if env.Get("TARGET_DEVICE_PLATFORM_NAME", "") == "appletvsimulator" { xcodeFlags = append(xcodeFlags, "-mtvos-simulator-version-min="+env.Get("TVOS_DEPLOYMENT_TARGET", "")) } else { 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() }