tailscale/tool/gocross/gocross.go
Aaron Klotz 4bb03609bc tool/gocross: ensure child process error codes are propagated on non-Unix
The Unix implementation of doExec propagates error codes by virtue of
the fact that it does an execve; the replacement binary will return the
exit code.

On non-Unix, we need to simulate these semantics by checking for an
ExitError and, when present, passing that value on to os.Exit.

We also add error handling to the doExec call for the benefit of
handling any errors where doExec fails before being able to execute
the desired binary.

Updates https://github.com/tailscale/corp/issues/29940

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2025-09-15 12:33:26 -07:00

153 lines
4.2 KiB
Go

// 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"
"runtime/debug"
"strings"
"tailscale.com/atomicfile"
)
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":
bi, ok := debug.ReadBuildInfo()
if !ok {
fmt.Fprintln(os.Stderr, "failed getting build info")
os.Exit(1)
}
for _, s := range bi.Settings {
if s.Key == "vcs.revision" {
fmt.Println(s.Value)
os.Exit(0)
}
}
fmt.Fprintln(os.Stderr, "did not find vcs.revision in build info")
os.Exit(1)
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)
case "gocross-write-wrapper-script":
if len(os.Args) != 3 {
fmt.Fprintf(os.Stderr, "usage: gocross write-wrapper-script <path>\n")
os.Exit(1)
}
if err := atomicfile.WriteFile(os.Args[2], wrapperScriptBash, 0755); err != nil {
fmt.Fprintf(os.Stderr, "writing bash wrapper script: %v\n", err)
os.Exit(1)
}
psFileName := strings.TrimSuffix(os.Args[2], filepath.Ext(os.Args[2])) + ".ps1"
if err := atomicfile.WriteFile(psFileName, wrapperScriptPowerShell, 0644); err != nil {
fmt.Fprintf(os.Stderr, "writing PowerShell wrapper script: %v\n", err)
os.Exit(1)
}
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)
debugf("Input: %s\n", formatArgv(os.Args))
debugf("Command: %s\n", formatArgv(newArgv))
debugf("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)
}
}
// Note that doExec only returns if the exec call failed.
if err := doExec(filepath.Join(toolchain, "bin", "go"), args, os.Environ()); err != nil {
fmt.Fprintf(os.Stderr, "executing process: %v\n", err)
os.Exit(1)
}
}
//go:embed gocross-wrapper.sh
var wrapperScriptBash []byte
//go:embed gocross-wrapper.ps1
var wrapperScriptPowerShell []byte
func debugf(format string, args ...any) {
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...)
}