github.com/distbuild/reclient@v0.0.0-20240401075343-3de72e395564/cmd/rewrapper/main.go (about)

     1  // Copyright 2023 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Main package for the rewrapper binary.
    16  //
    17  // The rewrapper is used as a prefix to a build/test command that should be remoted to RBE. The
    18  // rewrapper works as follows:
    19  //
    20  // rewrapper --labels=type=compile,lang=cpp --command_id=12345 --exec_strategy=remote -- \
    21  //
    22  //	clang -c test.c -o test.o
    23  //
    24  // In the above example, the clang command is packaged as a request to a long running proxy server
    25  // which receives the command and returns its result. The result could be obtained via remote
    26  // execution, local execution, or cache.
    27  package main
    28  
    29  import (
    30  	"context"
    31  	"flag"
    32  	"fmt"
    33  	"os"
    34  	"path"
    35  	"path/filepath"
    36  	"strings"
    37  	"time"
    38  
    39  	"github.com/bazelbuild/reclient/internal/pkg/ipc"
    40  	"github.com/bazelbuild/reclient/internal/pkg/protoencoding"
    41  	"github.com/bazelbuild/reclient/internal/pkg/rbeflag"
    42  	"github.com/bazelbuild/reclient/internal/pkg/rewrapper"
    43  	"github.com/bazelbuild/reclient/pkg/version"
    44  
    45  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/moreflag"
    46  
    47  	pb "github.com/bazelbuild/reclient/api/proxy"
    48  
    49  	log "github.com/golang/glog"
    50  )
    51  
    52  var (
    53  	cOpts       = &rewrapper.CommandOptions{StartTime: time.Now()}
    54  	serverAddr  = "127.0.0.1:8000"
    55  	dialTimeout *time.Duration
    56  
    57  	execStrategies = []string{"local", "remote", "remote_local_fallback", "racing"}
    58  )
    59  
    60  func initFlags() {
    61  	flag.StringVar(&serverAddr, "server_address", "127.0.0.1:8000", "The server address in the format of host:port for network, or unix:///file for unix domain sockets.")
    62  	flag.StringVar(&cOpts.CommandID, "command_id", "", "An identifier for the command for use in future debugging")
    63  	flag.StringVar(&cOpts.InvocationID, "invocation_id", "", "An identifier for a group of commands for use in future debugging")
    64  	flag.StringVar(&cOpts.ToolName, "tool_name", "", "The name of the tool to associate with executed commands")
    65  	flag.Var((*moreflag.StringMapValue)(&cOpts.Labels), "labels", "Comma-separated key value pairs in the form key=value. This is used to identify the type of command to help the proxy make decisions regarding remote execution. Defaults to type=tool.")
    66  	flag.StringVar(&cOpts.ExecRoot, "exec_root", "", "The exec root of the command. The path from which all inputs and outputs are defined relatively. Defaults to current working directory.")
    67  	flag.DurationVar(&cOpts.ExecTimeout, "exec_timeout", time.Hour, "Timeout for the command on RBE. Default is 1 hour.")
    68  	flag.DurationVar(&cOpts.ReclientTimeout, "reclient_timeout", time.Hour, "Timeout for remotely executed actions to wait for a response from RBE. Default is 1 hour.")
    69  	flag.Var((*moreflag.StringMapValue)(&cOpts.Platform), "platform", "Comma-separated key value pairs in the form key=value. This is used to identify remote platform settings like the docker image to use to run the command.")
    70  	flag.Var((*moreflag.StringListValue)(&cOpts.EnvVarAllowlist), "env_var_allowlist", "List of environment variables allowed to pass to the proxy.")
    71  	flag.Var((*moreflag.StringListValue)(&cOpts.Inputs), "inputs", "Comma-separated command input paths, relative to exec root. Each path may be either a file or a directory.")
    72  	flag.Var((*moreflag.StringListValue)(&cOpts.ToolchainInputs), "toolchain_inputs", "Comma-separated command toolchain inputs relative to the exec root, which are paths to binaries needed to execute the action. Each binary can have a <binary>_remote_toolchain_inputs file next to it to refer to all dependencies of the toolchain binary. Paths in the <binary>_remote_toolchain_inputs file should be normalized.")
    73  	flag.Var((*moreflag.StringListValue)(&cOpts.InputListPaths), "input_list_paths", "Comma-separated paths to files containing lists of inputs (rsp files). Used when inputs are too long to add to the command line. Paths contained in this file should be relative to the exec_root.")
    74  	flag.Var((*moreflag.StringListValue)(&cOpts.OutputListPaths), "output_list_paths", "Comma-separated paths to files containing lists of outputs (rsp files). Used when outputs are too long to add to the command line. Paths contained in this file should be relative to the exec_root.")
    75  	flag.Var((*moreflag.StringListValue)(&cOpts.OutputFiles), "output_files", "Comma-separated command output file paths, relative to exec root.")
    76  	flag.Var((*moreflag.StringListValue)(&cOpts.OutputDirectories), "output_directories", "Comma-separated command output directory paths, relative to exec root.")
    77  	flag.StringVar(&cOpts.ExecStrategy, "exec_strategy", "remote", fmt.Sprintf("one of %s. Defaults to remote.", execStrategies))
    78  	flag.BoolVar(&cOpts.Compare, "compare", false, "Boolean indicating whether to compare chosen exec strategy with local execution. Default is false.")
    79  	flag.IntVar(&cOpts.NumRetriesIfMismatched, "num_retries_if_mismatched", 0, "Deprecated: Number of times the action should be remotely executed to identify determinism. Used only when compare is set to true.")
    80  	flag.IntVar(&cOpts.NumLocalReruns, "num_local_reruns", 0, "Number of times the action should be rerun locally.")
    81  	flag.IntVar(&cOpts.NumRemoteReruns, "num_remote_reruns", 0, "Number of times the action should be rerun remotely.")
    82  	flag.BoolVar(&cOpts.RemoteAcceptCache, "remote_accept_cache", true, "Boolean indicating whether to accept remote cache hits. Default is true.")
    83  	flag.BoolVar(&cOpts.RemoteUpdateCache, "remote_update_cache", true, "Boolean indicating whether to cache the command result remotely. Default is true.")
    84  	flag.BoolVar(&cOpts.DownloadOutputs, "download_outputs", true, "Boolean indicating whether to download outputs after the command is executed. Default is true.")
    85  	flag.BoolVar(&cOpts.EnableAtomicDownloads, "enable_atomic_downloads", true, "Boolean indicating whether to download outputs atomically in remote/remote_local_fallback modes. Default is true.")
    86  	flag.BoolVar(&cOpts.LogEnvironment, "log_env", false, "Boolean indicating whether to pass the entire environment of the rewrapper to the reproxy for logging. Default is false.")
    87  	flag.BoolVar(&cOpts.PreserveUnchangedOutputMtime, "preserve_unchanged_output_mtime", false, "Boolean indicating whether or not to preserve mtimes of unchanged outputs when they are downloaded. Default is false.")
    88  	flag.StringVar(&cOpts.LocalWrapper, "local_wrapper", "", "Wrapper path to execute locally only. Relative to the current working directory of rewrapper.")
    89  	flag.StringVar(&cOpts.RemoteWrapper, "remote_wrapper", "", "Wrapper path to execute on remote worker. Relative to the current working directory of rewrapper.")
    90  	dialTimeout = flag.Duration("dial_timeout", 3*time.Minute, "Timeout for dialing reproxy. Default is 3 minutes.")
    91  	flag.BoolVar(&cOpts.PreserveSymlink, "preserve_symlink", false, "Boolean indicating whether to preserve symlinks in input tree. Default is false.")
    92  	flag.BoolVar(&cOpts.CanonicalizeWorkingDir, "canonicalize_working_dir", false, "Replaces local working directory with a canonical value when running on RE server. The feature makes actions working-dir agnostic and enables to cache them across various same depth (e.g. out/default and out/rbe-build) local working directories (default: false)")
    93  	flag.StringVar(&cOpts.ActionLog, "action_log", "", "If set, write a reproxy log entry for this remote action to the named file.")
    94  }
    95  
    96  func execStrategyValid() bool {
    97  	for _, s := range execStrategies {
    98  		if cOpts.ExecStrategy == s {
    99  			return true
   100  		}
   101  	}
   102  	return false
   103  }
   104  
   105  func main() {
   106  	initFlags()
   107  	version.PrintAndExitOnVersionFlag(false)
   108  	flag.Usage = func() {
   109  		fmt.Fprintf(flag.CommandLine.Output(), "Usage: %v [-flags] -- command ...\n", path.Base(os.Args[0]))
   110  		flag.PrintDefaults()
   111  	}
   112  	rbeflag.Parse()
   113  	rbeflag.LogAllFlags(1)
   114  	cmd := flag.Args()
   115  	if len(cmd) == 0 {
   116  		flag.Usage()
   117  		log.Exitf("No command provided")
   118  	}
   119  	if !execStrategyValid() {
   120  		flag.Usage()
   121  		log.Exitf("No exec_strategy provided, must be one of %v", execStrategies)
   122  	}
   123  	if serverAddr == "" {
   124  		log.Exitf("Invalid server address (%q), must be non empty.", serverAddr)
   125  	}
   126  
   127  	ctx := context.Background()
   128  	conn, err := ipc.DialContext(ctx, serverAddr)
   129  	if err != nil {
   130  		log.Exitf("Fail to dial %s: %v", serverAddr, err)
   131  	}
   132  	defer conn.Close()
   133  
   134  	proxy := pb.NewCommandsClient(conn)
   135  	wd, err := os.Getwd()
   136  	if err != nil {
   137  		log.Exitf("Failed to get current working directory: %v", err)
   138  	}
   139  	if wd == "/proc/self/cwd" {
   140  		wd, err = os.Readlink(wd)
   141  		if err != nil {
   142  			log.Exitf("Failed to get current true directory: %v", err)
   143  		}
   144  	}
   145  	if cOpts.ExecRoot == "" {
   146  		cOpts.ExecRoot = wd
   147  	}
   148  	if !filepath.IsAbs(cOpts.ExecRoot) {
   149  		cOpts.ExecRoot, err = filepath.Abs(cOpts.ExecRoot)
   150  		if err != nil {
   151  			log.Exitf("Failed to get abs path for exec_root: %v", err)
   152  		}
   153  	}
   154  	if len(cOpts.Labels) == 0 {
   155  		cOpts.Labels = map[string]string{"type": "tool"}
   156  	}
   157  	cOpts.WorkDir, err = filepath.Rel(cOpts.ExecRoot, wd)
   158  	if err != nil {
   159  		log.Exitf("Failed to compute working directory path relative to the exec root: %v", err)
   160  	}
   161  	if strings.HasPrefix(cOpts.WorkDir, "..") {
   162  		log.Exitf("Current working directory (%q) is not under the exec root (%q), relative working dir = %q", wd, cOpts.ExecRoot, cOpts.WorkDir)
   163  	}
   164  
   165  	if cOpts.NumRemoteReruns < 0 {
   166  		log.Exitf("Expected num_remote_reruns to be at least 0, got %v.", cOpts.NumRemoteReruns)
   167  	}
   168  	if cOpts.NumLocalReruns < 0 {
   169  		log.Exitf("Expected num_local_reruns to be at least 0, got %v.", cOpts.NumLocalReruns)
   170  	}
   171  
   172  	// TODO (b/296409009): Add support for preserve true and download outputs false for downloading stubs.
   173  
   174  	resp, err := rewrapper.RunCommand(ctx, *dialTimeout, proxy, cmd, cOpts)
   175  	if err != nil {
   176  		// Don't use log.Fatalf to avoid printing a stack trace.
   177  		log.Exitf("Command failed: %v", err)
   178  	}
   179  	if cOpts.ActionLog != "" && resp.ActionLog != nil {
   180  		if err := os.WriteFile(cOpts.ActionLog, []byte(protoencoding.TextWithIndent.Format(resp.ActionLog)), 0644); err != nil {
   181  			log.Errorf("Failed to write reproxy action log %v", cOpts.ActionLog)
   182  		}
   183  	}
   184  	os.Stdout.Write(resp.GetStdout())
   185  	os.Stderr.Write(resp.GetStderr())
   186  	log.Flush()
   187  	os.Exit(int(resp.GetResult().GetExitCode()))
   188  }