github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/command/command.go (about)

     1  // Package command defines common types to be used with command execution.
     2  package command
     3  
     4  import (
     5  	"crypto/sha256"
     6  	"encoding/hex"
     7  	"errors"
     8  	"fmt"
     9  	"os"
    10  	"sort"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/digest"
    15  	log "github.com/golang/glog"
    16  	"github.com/google/uuid"
    17  
    18  	cpb "github.com/bazelbuild/remote-apis-sdks/go/api/command"
    19  	repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
    20  	anypb "google.golang.org/protobuf/types/known/anypb"
    21  	tspb "google.golang.org/protobuf/types/known/timestamppb"
    22  )
    23  
    24  // InputType can be specified to narrow down the matching for a given input path.
    25  type InputType int
    26  
    27  const (
    28  	// UnspecifiedInputType means any input type will match.
    29  	UnspecifiedInputType InputType = iota
    30  
    31  	// DirectoryInputType means only directories match.
    32  	DirectoryInputType
    33  
    34  	// FileInputType means only files match.
    35  	FileInputType
    36  
    37  	// SymlinkInputType means only symlink match.
    38  	SymlinkInputType
    39  )
    40  
    41  var inputTypes = [...]string{"UnspecifiedInputType", "DirectoryInputType", "FileInputType"}
    42  
    43  func (s InputType) String() string {
    44  	if UnspecifiedInputType <= s && s <= FileInputType {
    45  		return inputTypes[s]
    46  	}
    47  	return fmt.Sprintf("InvalidInputType(%d)", s)
    48  }
    49  
    50  // SymlinkBehaviorType represents how symlinks are handled.
    51  type SymlinkBehaviorType int
    52  
    53  const (
    54  	// UnspecifiedSymlinkBehavior means following clients.TreeSymlinkOpts
    55  	// or DefaultTreeSymlinkOpts if clients.TreeSymlinkOpts is null.
    56  	UnspecifiedSymlinkBehavior SymlinkBehaviorType = iota
    57  
    58  	// ResolveSymlink means symlinks are resolved.
    59  	ResolveSymlink
    60  
    61  	// PreserveSymlink means symlinks are kept as-is.
    62  	PreserveSymlink
    63  )
    64  
    65  var symlinkBehaviorType = [...]string{"UnspecifiedSymlinkBehavior", "ResolveSymlink", "PreserveSymlink"}
    66  
    67  func (s SymlinkBehaviorType) String() string {
    68  	if UnspecifiedSymlinkBehavior <= s && s <= PreserveSymlink {
    69  		return symlinkBehaviorType[s-UnspecifiedSymlinkBehavior]
    70  	}
    71  	return fmt.Sprintf("InvalidSymlinkBehaviorType(%d)", s)
    72  }
    73  
    74  // InputExclusion represents inputs to be excluded from being considered for command execution.
    75  type InputExclusion struct {
    76  	// Required: the path regular expression to match for exclusion.
    77  	Regex string
    78  
    79  	// The input type to match for exclusion.
    80  	Type InputType
    81  }
    82  
    83  // VirtualInput represents an input that may exist on disk but shouldn't be accessed.
    84  // We want to stage it on disk for the command execution.
    85  type VirtualInput struct {
    86  	// The path for the input to be staged at, relative to the ExecRoot.
    87  	Path string
    88  
    89  	// The byte contents of the file to be staged.
    90  	Contents []byte
    91  
    92  	// The digest of the virtual input that is expected to exist in the CAS.
    93  	// Should not be used together with Contents.
    94  	Digest string
    95  
    96  	// Whether the file should be staged as executable.
    97  	IsExecutable bool
    98  
    99  	// Whether the file is actually an empty directory. This is used to provide
   100  	// empty directory inputs. When this is set, Contents and IsExecutable are
   101  	// ignored.
   102  	IsEmptyDirectory bool
   103  
   104  	// Mtime of the virtual input.
   105  	Mtime time.Time
   106  
   107  	// The virtual inputs' mode and permissions bits.
   108  	FileMode os.FileMode
   109  }
   110  
   111  // InputSpec represents all the required inputs to a remote command.
   112  type InputSpec struct {
   113  	// Input paths (files or directories) that need to be present for the command execution.
   114  	Inputs []string
   115  
   116  	// Inputs not present on the local file system, but should be staged for command execution.
   117  	VirtualInputs []*VirtualInput
   118  
   119  	// Inputs matching these patterns will be excluded.
   120  	InputExclusions []*InputExclusion
   121  
   122  	// Environment variables the command relies on.
   123  	EnvironmentVariables map[string]string
   124  
   125  	// SymlinkBehavior represents the way symlinks will be handled.
   126  	SymlinkBehavior SymlinkBehaviorType
   127  
   128  	// Node properties of inputs.
   129  	InputNodeProperties map[string]*cpb.NodeProperties
   130  }
   131  
   132  // String returns the string representation of the VirtualInput.
   133  func (s *VirtualInput) String() string {
   134  	return fmt.Sprintf("%+v", *s)
   135  }
   136  
   137  // String returns the string representation of the InputExclusion.
   138  func (s *InputExclusion) String() string {
   139  	return fmt.Sprintf("%+v", *s)
   140  }
   141  
   142  // Identifiers is a group of identifiers of a command.
   143  type Identifiers struct {
   144  	// CommandID is an optional id to use to identify a command.
   145  	CommandID string
   146  
   147  	// InvocationID is an optional id to use to identify an invocation spanning multiple commands.
   148  	InvocationID string
   149  
   150  	// CorrelatedInvocationID is an optional id to use to identify a build spanning multiple invocations.
   151  	CorrelatedInvocationID string
   152  
   153  	// ToolName is an optional tool name to pass to the remote server for logging.
   154  	ToolName string
   155  
   156  	// ToolVersion is an optional tool version to pass to the remote server for logging.
   157  	ToolVersion string
   158  
   159  	// ExecutionID is a UUID generated for a particular execution of this command.
   160  	ExecutionID string
   161  }
   162  
   163  // Command encompasses the complete information required to execute a command remotely.
   164  // To make sure to initialize a valid Command object, call FillDefaultFieldValues on the created
   165  // struct.
   166  type Command struct {
   167  	// Identifiers used to identify this command to be passed to RE.
   168  	Identifiers *Identifiers
   169  
   170  	// Args (required): command line elements to execute.
   171  	Args []string
   172  
   173  	// ExecRoot is an absolute path to the execution root of the command. All the other paths are
   174  	// specified relatively to this path.
   175  	ExecRoot string
   176  
   177  	// WorkingDir is the working directory, relative to the exec root, for the command to run
   178  	// in. It must be a directory which exists in the input tree. If it is left empty, then the
   179  	// action is run from the exec root.
   180  	WorkingDir string
   181  
   182  	// RemoteWorkingDir is the working directory when executing the command on RE server.
   183  	// It's relative to exec root and, if provided, needs to have the same number of levels
   184  	// as WorkingDir. If not provided, the remote command is run from the WorkingDir
   185  	RemoteWorkingDir string
   186  
   187  	// InputSpec: the command inputs.
   188  	InputSpec *InputSpec
   189  
   190  	// OutputFiles are the command output files.
   191  	OutputFiles []string
   192  
   193  	// OutputDirs are the command output directories.
   194  	// The files and directories will likely be merged into a single Outputs field in the future.
   195  	OutputDirs []string
   196  
   197  	// Timeout is an optional duration to wait for command execution before timing out.
   198  	Timeout time.Duration
   199  
   200  	// Platform is the platform to use for the execution.
   201  	Platform map[string]string
   202  }
   203  
   204  func marshallMap(m map[string]string, buf *[]byte) {
   205  	var pkeys []string
   206  	for k := range m {
   207  		pkeys = append(pkeys, k)
   208  	}
   209  	sort.Strings(pkeys)
   210  	for _, k := range pkeys {
   211  		*buf = append(*buf, []byte(k)...)
   212  		*buf = append(*buf, []byte(m[k])...)
   213  	}
   214  }
   215  
   216  func marshallSlice(s []string, buf *[]byte) {
   217  	for _, i := range s {
   218  		*buf = append(*buf, []byte(i)...)
   219  	}
   220  }
   221  
   222  func marshallSortedSlice(s []string, buf *[]byte) {
   223  	ss := make([]string, len(s))
   224  	copy(ss, s)
   225  	sort.Strings(ss)
   226  	marshallSlice(ss, buf)
   227  }
   228  
   229  // Validate checks whether all required command fields have been specified.
   230  func (c *Command) Validate() error {
   231  	if c == nil {
   232  		return nil
   233  	}
   234  	if len(c.Args) == 0 {
   235  		return errors.New("missing command arguments")
   236  	}
   237  	if c.ExecRoot == "" {
   238  		return errors.New("missing command exec root")
   239  	}
   240  	if c.InputSpec == nil {
   241  		return errors.New("missing command input spec")
   242  	}
   243  	if c.Identifiers == nil {
   244  		return errors.New("missing command identifiers")
   245  	}
   246  	if c.RemoteWorkingDir != "" && levels(c.RemoteWorkingDir) != levels(c.WorkingDir) {
   247  		return fmt.Errorf("invalid RemoteWorkingDir=%q[%v level(s)], it's expected to have the same depth as WorkingDir=%q[%v level(s)]",
   248  			c.RemoteWorkingDir, levels(c.RemoteWorkingDir), c.WorkingDir, levels(c.WorkingDir))
   249  	}
   250  	// TODO(olaola): make Platform required?
   251  	return nil
   252  }
   253  
   254  // Generates a stable id for the command.
   255  func (c *Command) stableID() string {
   256  	var buf []byte
   257  	marshallSlice(c.Args, &buf)
   258  	buf = append(buf, []byte(c.ExecRoot)...)
   259  	buf = append(buf, []byte(c.WorkingDir)...)
   260  	marshallSortedSlice(c.OutputFiles, &buf)
   261  	marshallSortedSlice(c.OutputDirs, &buf)
   262  	buf = append(buf, []byte(c.Timeout.String())...)
   263  	marshallMap(c.Platform, &buf)
   264  	if c.InputSpec != nil {
   265  		marshallMap(c.InputSpec.EnvironmentVariables, &buf)
   266  		marshallSortedSlice(c.InputSpec.Inputs, &buf)
   267  		inputExclusions := make([]*InputExclusion, len(c.InputSpec.InputExclusions))
   268  		copy(inputExclusions, c.InputSpec.InputExclusions)
   269  		sort.Slice(inputExclusions, func(i, j int) bool {
   270  			e1 := inputExclusions[i]
   271  			e2 := inputExclusions[j]
   272  			return e1.Regex > e2.Regex || e1.Regex == e2.Regex && e1.Type > e2.Type
   273  		})
   274  		for _, e := range inputExclusions {
   275  			buf = append(buf, []byte(e.Regex)...)
   276  			buf = append(buf, []byte(e.Type.String())...)
   277  		}
   278  	}
   279  	sha256Arr := sha256.Sum256(buf)
   280  	return hex.EncodeToString(sha256Arr[:])[:8]
   281  }
   282  
   283  // FillDefaultFieldValues initializes valid default values to inner Command fields.
   284  // This function should be called on every new Command object before use.
   285  func (c *Command) FillDefaultFieldValues() {
   286  	if c == nil {
   287  		return
   288  	}
   289  	if c.Identifiers == nil {
   290  		c.Identifiers = &Identifiers{}
   291  	}
   292  	if c.Identifiers.CommandID == "" {
   293  		c.Identifiers.CommandID = c.stableID()
   294  	}
   295  	if c.Identifiers.ToolName == "" {
   296  		c.Identifiers.ToolName = "remote-client"
   297  	}
   298  	if c.Identifiers.InvocationID == "" {
   299  		if id, err := uuid.NewRandom(); err == nil {
   300  			c.Identifiers.InvocationID = id.String()
   301  		} else {
   302  			log.Warningf("Failed to generate InvocationID: %s", err)
   303  		}
   304  	}
   305  	if c.Identifiers.ExecutionID == "" {
   306  		if id, err := uuid.NewRandom(); err == nil {
   307  			c.Identifiers.ExecutionID = id.String()
   308  		} else {
   309  			log.Warningf("Failed to generate ExecutionID: %s", err)
   310  		}
   311  	}
   312  	if c.InputSpec == nil {
   313  		c.InputSpec = &InputSpec{}
   314  	}
   315  }
   316  
   317  func levels(path string) int {
   318  	return len(strings.Split(path, string(os.PathSeparator)))
   319  }
   320  
   321  // ExecutionOptions specify how to execute a given Command.
   322  type ExecutionOptions struct {
   323  	// Whether to accept cached action results. Defaults to true.
   324  	AcceptCached bool
   325  
   326  	// When set, this execution results will not be cached.
   327  	DoNotCache bool
   328  
   329  	// Download command outputs after execution. Defaults to true.
   330  	DownloadOutputs bool
   331  
   332  	// Preserve mtimes for unchanged outputs when downloading. Defaults to false.
   333  	PreserveUnchangedOutputMtime bool
   334  
   335  	// Download command stdout and stderr. Defaults to true. If StreamOutErr is also set, this value
   336  	// is ignored for uncached actions if the server provides log streams for both stdout and stderr.
   337  	// For cached action results, or if the server does not provide log streams for stdout or stderr,
   338  	// this value will determine whether stdout and stderr is downloaded.
   339  	DownloadOutErr bool
   340  
   341  	// Request that stdout and stderr be streamed back to the client while the action is running.
   342  	// Defaults to false. If either stream is not provided by the server, the client will fall back to
   343  	// downloading the corresponding streams after the action has completed, provided DownloadOutErr
   344  	// is also set. The client may expect a delay in this scenario as the streams are downloaded after
   345  	// the fact.
   346  	StreamOutErr bool
   347  }
   348  
   349  // DefaultExecutionOptions returns the recommended ExecutionOptions.
   350  func DefaultExecutionOptions() *ExecutionOptions {
   351  	return &ExecutionOptions{
   352  		AcceptCached:                 true,
   353  		DoNotCache:                   false,
   354  		DownloadOutputs:              true,
   355  		PreserveUnchangedOutputMtime: false,
   356  		DownloadOutErr:               true,
   357  		StreamOutErr:                 false,
   358  	}
   359  }
   360  
   361  // ResultStatus represents the options for a finished command execution.
   362  type ResultStatus int
   363  
   364  const (
   365  	// UnspecifiedResultStatus is an invalid value, should not be used.
   366  	UnspecifiedResultStatus ResultStatus = iota
   367  
   368  	// SuccessResultStatus indicates that the command executed successfully.
   369  	SuccessResultStatus
   370  
   371  	// CacheHitResultStatus indicates that the command was a cache hit.
   372  	CacheHitResultStatus
   373  
   374  	// NonZeroExitResultStatus indicates that the command executed with a non zero exit code.
   375  	NonZeroExitResultStatus
   376  
   377  	// TimeoutResultStatus indicates that the command exceeded its specified deadline.
   378  	TimeoutResultStatus
   379  
   380  	// InterruptedResultStatus indicates that the command execution was interrupted.
   381  	InterruptedResultStatus
   382  
   383  	// RemoteErrorResultStatus indicates that an error occurred on the remote server.
   384  	RemoteErrorResultStatus
   385  
   386  	// LocalErrorResultStatus indicates that an error occurred locally.
   387  	LocalErrorResultStatus
   388  )
   389  
   390  var resultStatuses = [...]string{
   391  	"UnspecifiedResultStatus",
   392  	"SuccessResultStatus",
   393  	"CacheHitResultStatus",
   394  	"NonZeroExitResultStatus",
   395  	"TimeoutResultStatus",
   396  	"InterruptedResultStatus",
   397  	"RemoteErrorResultStatus",
   398  	"LocalErrorResultStatus",
   399  }
   400  
   401  // IsOk returns whether the status indicates a successful action.
   402  func (s ResultStatus) IsOk() bool {
   403  	return s == SuccessResultStatus || s == CacheHitResultStatus
   404  }
   405  
   406  func (s ResultStatus) String() string {
   407  	if UnspecifiedResultStatus <= s && s <= LocalErrorResultStatus {
   408  		return resultStatuses[s]
   409  	}
   410  	return fmt.Sprintf("InvalidResultStatus(%d)", s)
   411  }
   412  
   413  // Result is the result of a finished command execution.
   414  type Result struct {
   415  	// Command exit code.
   416  	ExitCode int
   417  	// Status of the finished run.
   418  	Status ResultStatus
   419  	// Any error encountered.
   420  	Err error
   421  }
   422  
   423  // IsOk returns whether the result was successful.
   424  func (r *Result) IsOk() bool {
   425  	return r.Status.IsOk()
   426  }
   427  
   428  // LocalErrorExitCode is an exit code corresponding to a local error.
   429  const LocalErrorExitCode = 35
   430  
   431  // TimeoutExitCode is an exit code corresponding to the command timing out remotely.
   432  const TimeoutExitCode = /*SIGNAL_BASE=*/ 128 + /*SIGALRM=*/ 14
   433  
   434  // RemoteErrorExitCode is an exit code corresponding to a remote server error.
   435  const RemoteErrorExitCode = 45
   436  
   437  // InterruptedExitCode is an exit code corresponding to an execution interruption by the user.
   438  const InterruptedExitCode = 8
   439  
   440  // NewLocalErrorResult constructs a Result from a local error.
   441  func NewLocalErrorResult(err error) *Result {
   442  	return &Result{
   443  		ExitCode: LocalErrorExitCode,
   444  		Status:   LocalErrorResultStatus,
   445  		Err:      err,
   446  	}
   447  }
   448  
   449  // NewRemoteErrorResult constructs a Result from a remote error.
   450  func NewRemoteErrorResult(err error) *Result {
   451  	return &Result{
   452  		ExitCode: RemoteErrorExitCode,
   453  		Status:   RemoteErrorResultStatus,
   454  		Err:      err,
   455  	}
   456  }
   457  
   458  // NewResultFromExitCode constructs a Result from a given command exit code.
   459  func NewResultFromExitCode(exitCode int) *Result {
   460  	st := SuccessResultStatus
   461  	if exitCode != 0 {
   462  		st = NonZeroExitResultStatus
   463  	}
   464  	return &Result{
   465  		ExitCode: exitCode,
   466  		Status:   st,
   467  	}
   468  }
   469  
   470  // NewTimeoutResult constructs a new result for a timeout-exceeded command.
   471  func NewTimeoutResult() *Result {
   472  	return &Result{
   473  		ExitCode: TimeoutExitCode,
   474  		Status:   TimeoutResultStatus,
   475  	}
   476  }
   477  
   478  // TimeInterval is a time window for an event.
   479  type TimeInterval struct {
   480  	From, To time.Time
   481  }
   482  
   483  // These are the events that we export time metrics on:
   484  const (
   485  	// EventServerQueued: Queued time on the remote server.
   486  	EventServerQueued = "ServerQueued"
   487  
   488  	// EventServerWorker: The total remote worker (bot) time.
   489  	EventServerWorker = "ServerWorker"
   490  
   491  	// EventServerWorkerInputFetch: Time to fetch inputs to the remote bot.
   492  	EventServerWorkerInputFetch = "ServerWorkerInputFetch"
   493  
   494  	// EventServerWorkerExecution: The actual execution on the remote bot.
   495  	EventServerWorkerExecution = "ServerWorkerExecution"
   496  
   497  	// EventServerWorkerOutputUpload: Uploading outputs to the CAS on the bot.
   498  	EventServerWorkerOutputUpload = "ServerWorkerOutputUpload"
   499  
   500  	// EventDownloadResults: Downloading action results from CAS.
   501  	EventDownloadResults = "DownloadResults"
   502  
   503  	// EventComputeMerkleTree: Computing the input Merkle tree.
   504  	EventComputeMerkleTree = "ComputeMerkleTree"
   505  
   506  	// EventCheckActionCache: Checking the action cache.
   507  	EventCheckActionCache = "CheckActionCache"
   508  
   509  	// EventUpdateCachedResult: Uploading local outputs to CAS and updating cached
   510  	// action result.
   511  	EventUpdateCachedResult = "UpdateCachedResult"
   512  
   513  	// EventUploadInputs: Uploading action inputs to CAS for remote execution.
   514  	EventUploadInputs = "UploadInputs"
   515  
   516  	// EventExecuteRemotely: Total time to execute remotely.
   517  	EventExecuteRemotely = "ExecuteRemotely"
   518  )
   519  
   520  // Metadata is general information associated with a Command execution.
   521  type Metadata struct {
   522  	// CommandDigest is a digest of the command being executed. It can be used
   523  	// to detect changes in the command between builds.
   524  	CommandDigest digest.Digest
   525  	// ActionDigest is a digest of the action being executed. It can be used
   526  	// to detect changes in the action between builds.
   527  	ActionDigest digest.Digest
   528  	// The total number of input files.
   529  	InputFiles int
   530  	// The total number of input directories.
   531  	InputDirectories int
   532  	// The overall number of bytes from all the inputs.
   533  	TotalInputBytes int64
   534  	// Event times for remote events, by event name.
   535  	EventTimes map[string]*TimeInterval
   536  
   537  	AuxiliaryMetadata []*anypb.Any
   538  	// The total number of output files (incl symlinks).
   539  	OutputFiles int
   540  	// The total number of output directories (incl symlinks, but not recursive).
   541  	OutputDirectories int
   542  	// The overall number of bytes from all the output files (incl. stdout/stderr, but not symlinks).
   543  	TotalOutputBytes int64
   544  	// Output File digests.
   545  	OutputFileDigests map[string]digest.Digest
   546  	// Output Directory digests.
   547  	OutputDirectoryDigests map[string]digest.Digest
   548  	// Output Symlinks.
   549  	OutputSymlinks map[string]string
   550  	// Missing digests that are uploaded to CAS.
   551  	MissingDigests []digest.Digest
   552  	// LogicalBytesUploaded is the sum of sizes in bytes of the blobs that were uploaded. It should be
   553  	// the same value as the sum of digest sizes in MissingDigests.
   554  	LogicalBytesUploaded int64
   555  	// RealBytesUploaded is the number of bytes that were put on the wire for upload (exclusing metadata).
   556  	// It may differ from LogicalBytesUploaded due to compression.
   557  	RealBytesUploaded int64
   558  	// LogicalBytesDownloaded is the sum of sizes in bytes of the blobs that were downloaded. It should be
   559  	// the same value as the sum of digest sizes in OutputDigests.
   560  	LogicalBytesDownloaded int64
   561  	// RealBytesDownloaded is the number of bytes that were put on the wire for download (exclusing metadata).
   562  	// It may differ from LogicalBytesDownloaded due to compression.
   563  	RealBytesDownloaded int64
   564  	// StderrDigest is a digest of the standard error after being executed.
   565  	StderrDigest digest.Digest
   566  	// StdoutDigest is a digest of the standard output after being executed.
   567  	StdoutDigest digest.Digest
   568  	// TODO(olaola): Add a lot of other fields.
   569  }
   570  
   571  // ToREProto converts the Command to an RE API Command proto.
   572  // `useOutputPathsField` selects what field/s to fill with the paths of outputs,
   573  // which will depend on the RE API version.
   574  func (c *Command) ToREProto(useOutputPathsField bool) *repb.Command {
   575  	workingDir := c.RemoteWorkingDir
   576  	if workingDir == "" {
   577  		workingDir = c.WorkingDir
   578  	}
   579  	cmdPb := &repb.Command{
   580  		Arguments:        c.Args,
   581  		WorkingDirectory: workingDir,
   582  	}
   583  
   584  	// In v2.1 of the RE API the `output_{files, directories}` fields were
   585  	// replaced by a single field: `output_paths`.
   586  	if useOutputPathsField {
   587  		cmdPb.OutputPaths = append(c.OutputFiles, c.OutputDirs...)
   588  		sort.Strings(cmdPb.OutputPaths)
   589  	} else {
   590  		cmdPb.OutputFiles = make([]string, len(c.OutputFiles))
   591  		copy(cmdPb.OutputFiles, c.OutputFiles)
   592  		sort.Strings(cmdPb.OutputFiles)
   593  
   594  		cmdPb.OutputDirectories = make([]string, len(c.OutputDirs))
   595  		copy(cmdPb.OutputDirectories, c.OutputDirs)
   596  		sort.Strings(cmdPb.OutputDirectories)
   597  	}
   598  
   599  	for name, val := range c.InputSpec.EnvironmentVariables {
   600  		cmdPb.EnvironmentVariables = append(cmdPb.EnvironmentVariables, &repb.Command_EnvironmentVariable{Name: name, Value: val})
   601  	}
   602  	sort.Slice(cmdPb.EnvironmentVariables, func(i, j int) bool { return cmdPb.EnvironmentVariables[i].Name < cmdPb.EnvironmentVariables[j].Name })
   603  	if len(c.Platform) > 0 {
   604  		cmdPb.Platform = &repb.Platform{}
   605  		for name, val := range c.Platform {
   606  			cmdPb.Platform.Properties = append(cmdPb.Platform.Properties, &repb.Platform_Property{Name: name, Value: val})
   607  		}
   608  		sort.Slice(cmdPb.Platform.Properties, func(i, j int) bool { return cmdPb.Platform.Properties[i].Name < cmdPb.Platform.Properties[j].Name })
   609  	}
   610  	return cmdPb
   611  }
   612  
   613  func FromREProto(cmdPb *repb.Command) *Command {
   614  	cmd := &Command{
   615  		InputSpec: &InputSpec{
   616  			EnvironmentVariables: make(map[string]string),
   617  			InputNodeProperties:  make(map[string]*cpb.NodeProperties),
   618  		},
   619  		Identifiers: &Identifiers{},
   620  		WorkingDir:  cmdPb.WorkingDirectory,
   621  		OutputFiles: cmdPb.OutputFiles,
   622  		OutputDirs:  cmdPb.OutputDirectories,
   623  		Platform:    make(map[string]string),
   624  		Args:        cmdPb.Arguments,
   625  	}
   626  
   627  	// In v2.1 of the RE API the `output_{files, directories}` fields were
   628  	// replaced by a single field: `output_paths`.
   629  	if len(cmdPb.OutputPaths) > 0 {
   630  		cmd.OutputFiles = cmdPb.OutputPaths
   631  		cmd.OutputDirs = nil
   632  	}
   633  	for _, ev := range cmdPb.EnvironmentVariables {
   634  		cmd.InputSpec.EnvironmentVariables[ev.Name] = ev.Value
   635  	}
   636  	for _, pt := range cmdPb.GetPlatform().GetProperties() {
   637  		cmd.Platform[pt.Name] = pt.Value
   638  	}
   639  	return cmd
   640  }
   641  
   642  // FromProto parses a Command struct from a proto message.
   643  func FromProto(p *cpb.Command) *Command {
   644  	ids := &Identifiers{
   645  		CommandID:              p.GetIdentifiers().GetCommandId(),
   646  		InvocationID:           p.GetIdentifiers().GetInvocationId(),
   647  		CorrelatedInvocationID: p.GetIdentifiers().GetCorrelatedInvocationsId(),
   648  		ToolName:               p.GetIdentifiers().GetToolName(),
   649  		ToolVersion:            p.GetIdentifiers().GetToolVersion(),
   650  		ExecutionID:            p.GetIdentifiers().GetExecutionId(),
   651  	}
   652  	is := inputSpecFromProto(p.GetInput())
   653  	return &Command{
   654  		Identifiers:      ids,
   655  		ExecRoot:         p.ExecRoot,
   656  		Args:             p.Args,
   657  		WorkingDir:       p.WorkingDirectory,
   658  		RemoteWorkingDir: p.RemoteWorkingDirectory,
   659  		InputSpec:        is,
   660  		OutputFiles:      p.GetOutput().GetOutputFiles(),
   661  		OutputDirs:       p.GetOutput().GetOutputDirectories(),
   662  		Timeout:          time.Duration(p.ExecutionTimeout) * time.Second,
   663  		Platform:         p.Platform,
   664  	}
   665  }
   666  
   667  func inputSpecFromProto(is *cpb.InputSpec) *InputSpec {
   668  	var excl []*InputExclusion
   669  	for _, ex := range is.GetExcludeInputs() {
   670  		excl = append(excl, &InputExclusion{
   671  			Regex: ex.Regex,
   672  			Type:  inputTypeFromProto(ex.Type),
   673  		})
   674  	}
   675  	var vis []*VirtualInput
   676  	for _, vi := range is.GetVirtualInputs() {
   677  		contents := make([]byte, len(vi.Contents))
   678  		copy(contents, vi.Contents)
   679  		vis = append(vis, &VirtualInput{
   680  			Path:             vi.Path,
   681  			Contents:         contents,
   682  			IsExecutable:     vi.IsExecutable,
   683  			IsEmptyDirectory: vi.IsEmptyDirectory,
   684  			Digest:           vi.Digest,
   685  			Mtime:            vi.Mtime.AsTime(),
   686  			FileMode:         os.FileMode(vi.Filemode),
   687  		})
   688  	}
   689  	return &InputSpec{
   690  		Inputs:               is.GetInputs(),
   691  		VirtualInputs:        vis,
   692  		InputExclusions:      excl,
   693  		EnvironmentVariables: is.GetEnvironmentVariables(),
   694  		SymlinkBehavior:      symlinkBehaviorFromProto(is.GetSymlinkBehavior()),
   695  		InputNodeProperties:  is.GetInputNodeProperties(),
   696  	}
   697  }
   698  
   699  func NodePropertiesToAPI(np *cpb.NodeProperties) *repb.NodeProperties {
   700  	if np == nil {
   701  		return nil
   702  	}
   703  	res := &repb.NodeProperties{
   704  		Mtime:    np.GetMtime(),
   705  		UnixMode: np.GetUnixMode(),
   706  	}
   707  	if np.Properties != nil {
   708  		res.Properties = make([]*repb.NodeProperty, 0, len(np.Properties))
   709  	}
   710  	for _, p := range np.GetProperties() {
   711  		res.Properties = append(res.Properties, &repb.NodeProperty{
   712  			Name:  p.Name,
   713  			Value: p.Value,
   714  		})
   715  	}
   716  	return res
   717  }
   718  
   719  func NodePropertiesFromAPI(np *repb.NodeProperties) *cpb.NodeProperties {
   720  	if np == nil {
   721  		return nil
   722  	}
   723  	res := &cpb.NodeProperties{
   724  		Mtime:    np.GetMtime(),
   725  		UnixMode: np.GetUnixMode(),
   726  	}
   727  	if np.Properties != nil {
   728  		res.Properties = make([]*cpb.NodeProperty, 0, len(np.Properties))
   729  	}
   730  	for _, p := range np.GetProperties() {
   731  		res.Properties = append(res.Properties, &cpb.NodeProperty{
   732  			Name:  p.Name,
   733  			Value: p.Value,
   734  		})
   735  	}
   736  	return res
   737  }
   738  
   739  func inputSpecToProto(is *InputSpec) *cpb.InputSpec {
   740  	var excl []*cpb.ExcludeInput
   741  	for _, ex := range is.InputExclusions {
   742  		excl = append(excl, &cpb.ExcludeInput{
   743  			Regex: ex.Regex,
   744  			Type:  inputTypeToProto(ex.Type),
   745  		})
   746  	}
   747  	var vis []*cpb.VirtualInput
   748  	for _, vi := range is.VirtualInputs {
   749  		contents := make([]byte, len(vi.Contents))
   750  		copy(contents, vi.Contents)
   751  		vis = append(vis, &cpb.VirtualInput{
   752  			Path:             vi.Path,
   753  			Contents:         contents,
   754  			IsExecutable:     vi.IsExecutable,
   755  			IsEmptyDirectory: vi.IsEmptyDirectory,
   756  			Digest:           vi.Digest,
   757  			Mtime:            tspb.New(vi.Mtime),
   758  			Filemode:         uint32(vi.FileMode),
   759  		})
   760  	}
   761  	return &cpb.InputSpec{
   762  		Inputs:               is.Inputs,
   763  		VirtualInputs:        vis,
   764  		ExcludeInputs:        excl,
   765  		EnvironmentVariables: is.EnvironmentVariables,
   766  		SymlinkBehavior:      symlinkBehaviorToProto(is.SymlinkBehavior),
   767  		InputNodeProperties:  is.InputNodeProperties,
   768  	}
   769  }
   770  
   771  func inputTypeFromProto(t cpb.InputType_Value) InputType {
   772  	switch t {
   773  	case cpb.InputType_DIRECTORY:
   774  		return DirectoryInputType
   775  	case cpb.InputType_FILE:
   776  		return FileInputType
   777  	default:
   778  		return UnspecifiedInputType
   779  	}
   780  }
   781  
   782  func inputTypeToProto(t InputType) cpb.InputType_Value {
   783  	switch t {
   784  	case DirectoryInputType:
   785  		return cpb.InputType_DIRECTORY
   786  	case FileInputType:
   787  		return cpb.InputType_FILE
   788  	default:
   789  		return cpb.InputType_UNSPECIFIED
   790  	}
   791  }
   792  
   793  func symlinkBehaviorFromProto(t cpb.SymlinkBehaviorType_Value) SymlinkBehaviorType {
   794  	switch t {
   795  	case cpb.SymlinkBehaviorType_RESOLVE:
   796  		return ResolveSymlink
   797  	case cpb.SymlinkBehaviorType_PRESERVE:
   798  		return PreserveSymlink
   799  	default:
   800  		return UnspecifiedSymlinkBehavior
   801  	}
   802  }
   803  
   804  func symlinkBehaviorToProto(t SymlinkBehaviorType) cpb.SymlinkBehaviorType_Value {
   805  	switch t {
   806  	case ResolveSymlink:
   807  		return cpb.SymlinkBehaviorType_RESOLVE
   808  	case PreserveSymlink:
   809  		return cpb.SymlinkBehaviorType_PRESERVE
   810  	default:
   811  		return cpb.SymlinkBehaviorType_UNSPECIFIED
   812  	}
   813  }
   814  
   815  func protoStatusFromResultStatus(s ResultStatus) cpb.CommandResultStatus_Value {
   816  	switch s {
   817  	case SuccessResultStatus:
   818  		return cpb.CommandResultStatus_SUCCESS
   819  	case CacheHitResultStatus:
   820  		return cpb.CommandResultStatus_CACHE_HIT
   821  	case NonZeroExitResultStatus:
   822  		return cpb.CommandResultStatus_NON_ZERO_EXIT
   823  	case TimeoutResultStatus:
   824  		return cpb.CommandResultStatus_TIMEOUT
   825  	case InterruptedResultStatus:
   826  		return cpb.CommandResultStatus_INTERRUPTED
   827  	case RemoteErrorResultStatus:
   828  		return cpb.CommandResultStatus_REMOTE_ERROR
   829  	case LocalErrorResultStatus:
   830  		return cpb.CommandResultStatus_LOCAL_ERROR
   831  	default:
   832  		return cpb.CommandResultStatus_UNKNOWN
   833  	}
   834  }
   835  
   836  func protoStatusToResultStatus(s cpb.CommandResultStatus_Value) ResultStatus {
   837  	switch s {
   838  	case cpb.CommandResultStatus_SUCCESS:
   839  		return SuccessResultStatus
   840  	case cpb.CommandResultStatus_CACHE_HIT:
   841  		return CacheHitResultStatus
   842  	case cpb.CommandResultStatus_NON_ZERO_EXIT:
   843  		return NonZeroExitResultStatus
   844  	case cpb.CommandResultStatus_TIMEOUT:
   845  		return TimeoutResultStatus
   846  	case cpb.CommandResultStatus_INTERRUPTED:
   847  		return InterruptedResultStatus
   848  	case cpb.CommandResultStatus_REMOTE_ERROR:
   849  		return RemoteErrorResultStatus
   850  	case cpb.CommandResultStatus_LOCAL_ERROR:
   851  		return LocalErrorResultStatus
   852  	default:
   853  		return UnspecifiedResultStatus
   854  	}
   855  }
   856  
   857  // ToProto serializes a Command struct into a proto message.
   858  func ToProto(cmd *Command) *cpb.Command {
   859  	if cmd == nil {
   860  		return nil
   861  	}
   862  	cPb := &cpb.Command{
   863  		ExecRoot:               cmd.ExecRoot,
   864  		Input:                  inputSpecToProto(cmd.InputSpec),
   865  		Output:                 &cpb.OutputSpec{OutputFiles: cmd.OutputFiles, OutputDirectories: cmd.OutputDirs},
   866  		Args:                   cmd.Args,
   867  		ExecutionTimeout:       int32(cmd.Timeout.Seconds()),
   868  		WorkingDirectory:       cmd.WorkingDir,
   869  		RemoteWorkingDirectory: cmd.RemoteWorkingDir,
   870  		Platform:               cmd.Platform,
   871  	}
   872  	if cmd.Identifiers != nil {
   873  		cPb.Identifiers = &cpb.Identifiers{
   874  			CommandId:    cmd.Identifiers.CommandID,
   875  			InvocationId: cmd.Identifiers.InvocationID,
   876  			ToolName:     cmd.Identifiers.ToolName,
   877  			ExecutionId:  cmd.Identifiers.ExecutionID,
   878  		}
   879  	}
   880  	return cPb
   881  }
   882  
   883  // ResultToProto serializes a command.Result struct into a proto message.
   884  func ResultToProto(res *Result) *cpb.CommandResult {
   885  	if res == nil {
   886  		return nil
   887  	}
   888  	resPb := &cpb.CommandResult{
   889  		Status:   protoStatusFromResultStatus(res.Status),
   890  		ExitCode: int32(res.ExitCode),
   891  	}
   892  	if res.Err != nil {
   893  		resPb.Msg = res.Err.Error()
   894  	}
   895  	return resPb
   896  }
   897  
   898  // ResultFromProto parses a command.Result struct from a proto message.
   899  func ResultFromProto(res *cpb.CommandResult) *Result {
   900  	if res == nil {
   901  		return nil
   902  	}
   903  	var err error
   904  	if res.Msg != "" {
   905  		err = errors.New(res.Msg)
   906  	}
   907  	return &Result{
   908  		Status:   protoStatusToResultStatus(res.Status),
   909  		ExitCode: int(res.ExitCode),
   910  		Err:      err,
   911  	}
   912  }
   913  
   914  // TimeToProto converts a valid time.Time into a proto Timestamp.
   915  func TimeToProto(t time.Time) *tspb.Timestamp {
   916  	if t.IsZero() {
   917  		return nil
   918  	}
   919  	return tspb.New(t)
   920  }
   921  
   922  // TimeFromProto converts a valid Timestamp proto into a time.Time.
   923  func TimeFromProto(tPb *tspb.Timestamp) time.Time {
   924  	if tPb == nil {
   925  		return time.Time{}
   926  	}
   927  	return tPb.AsTime()
   928  }
   929  
   930  // TimeIntervalToProto serializes the SDK TimeInterval into a proto.
   931  func TimeIntervalToProto(t *TimeInterval) *cpb.TimeInterval {
   932  	if t == nil {
   933  		return nil
   934  	}
   935  	return &cpb.TimeInterval{
   936  		From: TimeToProto(t.From),
   937  		To:   TimeToProto(t.To),
   938  	}
   939  }
   940  
   941  // TimeIntervalFromProto parses the SDK TimeInterval from a proto.
   942  func TimeIntervalFromProto(t *cpb.TimeInterval) *TimeInterval {
   943  	if t == nil {
   944  		return nil
   945  	}
   946  	return &TimeInterval{
   947  		From: TimeFromProto(t.From),
   948  		To:   TimeFromProto(t.To),
   949  	}
   950  }