blob: 0b1eced45e8edae4e012ddaa5b29f04367328434 [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can
// found in the LICENSE file.
/**
buildproxywrap is a command wrapper that starts/stops build service
relays around a command that typically involves bazel.
Usage: buildproxywrap --cfg FILE -- command...
An example relay configuration file looks like:
[
{
"name": "sponge",
"socket_file_name": "sponge.sock",
"socket_path_env_var": "BAZEL_sponge_socket_path",
"server_address": "buildeventservice-pa.googleapis.com:443"
},
{
"name": "resultstore",
"socket_file_name": "resultstore.sock",
"socket_path_env_var": "BAZEL_resultstore_socket_path",
"server_address": "buildeventservice.googleapis.com:443"
},
{
"name": "RBE",
"socket_file_name": "rbe.sock",
"socket_path_env_var": "BAZEL_rbe_socket_path",
"server_address": "remotebuildexecution.googleapis.com:443"
}
]
The above configuration will setup:
Service -> Socket file environment
----------------------------------------------------------------------
sponge -> BAZEL_sponge_socket_path (for bazel --bes_proxy)
resultstore -> BAZEL_resultstore_socket_path (for bazel --bes_proxy)
RBE -> BAZEL_rbe_socket_path (for bazel --remote_proxy)
Example: run bazel using remote services through a proxy
(assuming that fx bazel responds to the above environment variables)
buildproxywrap ... -- fx bazel build --config=remote ...
**/
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"os/exec"
"os/signal"
"github.com/golang/glog"
)
// wrapCommand runs `command` with environment `env` in a subprocess,
// and returns its exit code.
// The `env` environment contains paths to various socket files used
// by the relays.
// Forward all standard pipes.
func wrapCommand(ctx context.Context, command []string, env []string) error {
cmd := exec.CommandContext(ctx, command[0], command[1:]...)
cmd.Env = append(os.Environ(), env...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
return cmd.Run()
}
// errorToExitCode converts an error to a program exit code.
func errorToExitCode(err error) int {
if err == nil {
return 0
}
if exiterr, ok := err.(*exec.ExitError); ok {
exitCode := exiterr.ExitCode()
glog.V(0).Infof("Command exited %d", exitCode)
return exitCode
}
glog.Errorf("Error: %v", err)
return 2 // Some other error.
}
// innerMain is the main routine, that returns an error from the subprocess.
// Separating this function from main() ensures that all defer calls are
// executed before os.Exit().
func innerMain(ctx context.Context) error {
var socatPath string
var socketDir string
var configFile string
flag.StringVar(&socatPath, "socat", "socat", "Path to the 'socat' tool.")
flag.StringVar(&socketDir, "socket_dir", "", "Temporary directory for sockets. If empty, this directory will be automatically chosen. In all cases, this directory will be cleaned up on exit.")
flag.StringVar(&configFile, "cfg", "", "Relay configuration file (required).")
flag.Parse()
command := flag.Args()
// Load relay configuration.
if configFile == "" {
return fmt.Errorf("missing required --cfg flag")
}
cfgData, err := os.ReadFile(configFile)
if err != nil {
return err
}
var relays []*socketRelay
if err := json.Unmarshal(cfgData, &relays); err != nil {
return fmt.Errorf("failed to parse JSON file %s: %v", configFile, err)
}
// Setup a temporary directory for sockets.
if socketDir == "" {
var err error
socketDir, err = os.MkdirTemp("", "bazel_service_proxy.*")
if err != nil {
return err
}
}
defer os.RemoveAll(socketDir)
finished := make(chan int, 0) // sent after wrapped command completes
ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
defer stop()
cmdErr := multiRelayWrap(ctx, relays, socketDir, socatPath, func(env []string) error {
defer func() {
close(finished)
}()
return wrapCommand(ctx, command, env)
})
// Wait for either completion or interrupt
select {
case <-finished:
case <-ctx.Done():
}
return cmdErr
}
func main() {
os.Exit(errorToExitCode(innerMain(context.Background())))
}