tailscale/tool/gocross/toolchain.go

174 lines
4.7 KiB
Go
Raw Normal View History

// 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
}