metropolis/build: add gotoolwrap
This adds gotoolwrap, a tiny Go executable used to wrap binaries that
want to access the monogon workspace as a GOPATH during build steps.
Test Plan: Used further down the stack in code generation.
X-Origin-Diff: phab/D750
GitOrigin-RevId: 83d11b94d025d3652fce88917b1664d93454c60f
diff --git a/metropolis/build/gotoolwrap/BUILD.bazel b/metropolis/build/gotoolwrap/BUILD.bazel
new file mode 100644
index 0000000..ce2aa6a
--- /dev/null
+++ b/metropolis/build/gotoolwrap/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["main.go"],
+ importpath = "source.monogon.dev/metropolis/build/gotoolwrap",
+ visibility = ["//visibility:private"],
+)
+
+go_binary(
+ name = "gotoolwrap",
+ embed = [":go_default_library"],
+ visibility = ["//visibility:public"],
+)
diff --git a/metropolis/build/gotoolwrap/main.go b/metropolis/build/gotoolwrap/main.go
new file mode 100644
index 0000000..5b36048
--- /dev/null
+++ b/metropolis/build/gotoolwrap/main.go
@@ -0,0 +1,148 @@
+// 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"
+ "io/ioutil"
+ "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 := ioutil.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)
+ }
+ cmd.Env = append(cmd.Env,
+ fmt.Sprintf("GOROOT=%s", gorootAbs),
+ fmt.Sprintf("GOPATH=%s", gopathAbs),
+ fmt.Sprintf("PATH=%s", path),
+ )
+
+ // 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)
+ }
+ }
+}