blob: 86dc3a1066b05b0771daff6ee766b2c3e0068976 [file] [log] [blame] [edit]
// Copyright 2020 The Monogon Project Authors.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// gotoolwrap is a tiny wrapper used to run executables that expect a standard
// Go development environment setup to act on the monogon workspace. Notably,
// it's used in Bazel rules to run tools like gofmt when invoked by some kinds
// of code generation tools.
//
// Usage: ./gotoolwrap executable arg1 arg2
//
// gotoolwrap expects the following environment variables to be set (and unsets
// them before calling the given executable):
// - GOTOOLWRAP_GOPATH: A synthetic GOPATH, eg. one generated by rules_go's
// go_path target.
// - GOTOOLWRAP_GOROOT: A Go SDK's GOROOT, eg. one from rules_go's GoSDK
// provider.
//
// gotoolwrap will set PATH to contain GOROOT/bin, and set GOPATH and GOROOT as
// resolved, absolute paths. Absolute paths are expected by tools like 'gofmt'.
package main
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)
func main() {
gopath := os.Getenv("GOTOOLWRAP_GOPATH")
if gopath == "" {
log.Fatal("GOTOOLWRAP_GOPATH must be set")
}
goroot := os.Getenv("GOTOOLWRAP_GOROOT")
if goroot == "" {
log.Fatal("GOTOOLWRAP_GOROOT must be set")
}
if len(os.Args) < 2 {
log.Fatalf("No command specified")
}
// Resolve gopath and goroot to absolute paths.
gopathAbs, err := filepath.Abs(gopath)
if err != nil {
log.Fatalf("Abs(%q): %v", gopath, err)
}
gorootAbs, err := filepath.Abs(goroot)
if err != nil {
log.Fatalf("Abs(%q): %v", goroot, err)
}
// Ensure the resolved GOROOT has a bin/go and bin/gofmt.
gorootBin := filepath.Join(gorootAbs, "bin")
stat, err := os.Stat(gorootBin)
if err != nil {
log.Fatalf("Could not stat $GOTOOLWRAP_GOROOT/bin (%q): %v", gorootBin, err)
}
if !stat.IsDir() {
log.Fatalf("$GOTOOLWRAP_GOROOT/bin (%q) is not a directory", gorootBin)
}
// We list all files inside so that we can print them to the user for
// debugging purposes if that's not the case.
binFiles := make(map[string]bool)
files, err := os.ReadDir(gorootBin)
if err != nil {
log.Fatalf("Could not read dir $GOTOOLWRAP_GOROOT/bin (%q): %v", gorootBin, err)
}
for _, f := range files {
if f.IsDir() {
continue
}
binFiles[f.Name()] = true
}
if !binFiles["go"] || !binFiles["gofmt"] {
msg := "no files"
if len(binFiles) > 0 {
var names []string
for name, _ := range binFiles {
names = append(names, fmt.Sprintf("%q", name))
}
msg = fmt.Sprintf(": %s", strings.Join(names, ", "))
}
log.Fatalf("$GOTOOLWRAP_GOROOT/bin (%q) does not contain go and/or gofmt, found %s", gorootBin, msg)
}
// Make new PATH.
path := os.Getenv("PATH")
if path == "" {
path = gorootBin
} else {
path = fmt.Sprintf("%s:%s", gorootBin, path)
}
cmd := exec.Command(os.Args[1], os.Args[2:]...)
// Copy current env into command's env, filtering out GOTOOLWRAP env vars
// and PATH (which we set ourselves).
for _, v := range os.Environ() {
if strings.HasPrefix(v, "GOTOOLWRAP_GOROOT=") {
continue
}
if strings.HasPrefix(v, "GOTOOLWRAP_GOPATH=") {
continue
}
if strings.HasPrefix(v, "PATH=") {
continue
}
cmd.Env = append(cmd.Env, v)
}
// Create a temporary cache directory and remove everything afterwards.
// Based on code from
// @io_bazel_rules_go//go/tools/builders:stdliblist.go L174
tempDir, err := os.MkdirTemp("", "gocache")
if err != nil {
log.Fatalf("Cannot create temporary directory: %v", err)
}
defer os.RemoveAll(tempDir)
cmd.Env = append(cmd.Env,
fmt.Sprintf("GOROOT=%s", gorootAbs),
fmt.Sprintf("GOPATH=%s", gopathAbs),
fmt.Sprintf("PATH=%s", path),
fmt.Sprintf("GO111MODULE=off"),
fmt.Sprintf("GOCACHE=%s", tempDir),
)
// Run the command interactively.
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
if err := cmd.Run(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
os.Exit(exitErr.ExitCode())
} else {
log.Fatalf("Could not run %q: %v", os.Args[1], err)
}
}
}