blob: 613838e4e62cccda134a915ceeda3862b63bc4e2 [file]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package buildconfig
import (
"context"
"errors"
"fmt"
"io/fs"
"path/filepath"
"strings"
"time"
log "github.com/golang/glog"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
rpb "go.chromium.org/build/remote-apis/build/bazel/remote/execution/v2"
"go.chromium.org/build/siso/execute"
"go.chromium.org/build/siso/o11y/clog"
"go.chromium.org/build/siso/path"
)
// starCmdActions returns actions, which contains
//
// fix(inputs, tool_inputs, outputs): fix the cmd.
// write(fname, content, is_executable): write a file.
// copy(src, dst, recursive): copy a file or a dir.
// symlink(target, linkpath): create a symlink.
// exit(exit_status, stdout, stderr): finish the cmd.
func starCmdActions(ctx context.Context, cmd *execute.Cmd) starlark.Value {
receiver := starCmdValue{
ctx: ctx,
cmd: cmd,
}
actionsFix := starlark.NewBuiltin("fix", starActionsFix).BindReceiver(receiver)
actionsWrite := starlark.NewBuiltin("write", starActionsWrite).BindReceiver(receiver)
actionsCopy := starlark.NewBuiltin("copy", starActionsCopy).BindReceiver(receiver)
actionsSymlink := starlark.NewBuiltin("symlink", starActionsSymlink).BindReceiver(receiver)
actionsExit := starlark.NewBuiltin("exit", starActionsExit).BindReceiver(receiver)
return starlarkstruct.FromStringDict(starlark.String("actions"), map[string]starlark.Value{
"fix": actionsFix,
"write": actionsWrite,
"copy": actionsCopy,
"symlink": actionsSymlink,
"exit": actionsExit,
})
}
type starCmdValue struct {
ctx context.Context
cmd *execute.Cmd
}
func (c starCmdValue) String() string {
return fmt.Sprintf("cmd[%s]", c.cmd)
}
func (starCmdValue) Type() string { return "execute.Cmd" }
func (starCmdValue) Freeze() {}
func (starCmdValue) Truth() starlark.Bool { return starlark.True }
func (starCmdValue) Hash() (uint32, error) { return 0, errors.New("execute.Cmd is not hashable") }
// Starlark function `actions.fix(inputs, tool_inputs, outputs, args)`
// to fix the command's inputs/outputs/args in the context.
func starActionsFix(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
log.V(1).Infof("actions.fix args=%s kwargs=%s", args, kwargs)
c, ok := fn.Receiver().(starCmdValue)
if !ok {
return starlark.None, fmt.Errorf("unexpected receiver: %v", fn.Receiver())
}
var inputsValue, toolInputsValue, outputsValue starlark.Value
var cmdArgsValue starlark.Value
var rspfileContentValue starlark.Value
var reconcileOutputdirsValue starlark.Value
var auxiliaryLogOutputFilesValue starlark.Value
var auxiliaryLogOutputDirsValue starlark.Value
err := starlark.UnpackArgs("fix", args, kwargs,
"inputs?", &inputsValue,
"tool_inputs?", &toolInputsValue,
"outputs?", &outputsValue,
"args?", &cmdArgsValue,
"rspfile_content?", &rspfileContentValue,
"reconcile_outputdirs?", &reconcileOutputdirsValue,
"auxiliary_log_output_files?", &auxiliaryLogOutputFilesValue,
"auxiliary_log_output_dirs?", &auxiliaryLogOutputDirsValue)
if err != nil {
return starlark.None, err
}
var inputs, toolInputs, outputs []string
if inputsValue != nil {
inputs, err = unpackList(inputsValue)
if err != nil {
return starlark.None, err
}
}
if toolInputsValue != nil {
toolInputs, err = unpackList(toolInputsValue)
if err != nil {
return starlark.None, err
}
}
if outputsValue != nil {
outputs, err = unpackList(outputsValue)
if err != nil {
return starlark.None, err
}
}
var auxiliaryLogOutputFiles, auxiliaryLogOutputDirs []string
if auxiliaryLogOutputFilesValue != nil {
auxiliaryLogOutputFiles, err = unpackList(auxiliaryLogOutputFilesValue)
if err != nil {
return starlark.None, err
}
}
if auxiliaryLogOutputDirsValue != nil {
auxiliaryLogOutputDirs, err = unpackList(auxiliaryLogOutputDirsValue)
if err != nil {
return starlark.None, err
}
}
var cmdArgs []string
if cmdArgsValue != nil && cmdArgsValue != starlark.None {
cmdArgs, err = unpackList(cmdArgsValue)
if err != nil {
return starlark.None, err
}
}
var rspfileContent starlark.Bytes
if rspfileContentValue != nil && rspfileContentValue != starlark.None {
rspfileContent, ok = rspfileContentValue.(starlark.Bytes)
if !ok {
v, ok := starlark.AsString(rspfileContentValue)
if !ok {
return starlark.None, fmt.Errorf("rspfile_content is not bytes nor a string")
}
rspfileContent = starlark.Bytes(v)
}
}
var reconcileOutputdirs []string
if reconcileOutputdirsValue != nil {
reconcileOutputdirs, err = unpackList(reconcileOutputdirsValue)
if err != nil {
return starlark.None, err
}
}
if inputsValue != nil {
c.cmd.Inputs = path.Paths(uniqueList(inputs))
}
if toolInputsValue != nil {
c.cmd.ToolInputs = path.Paths(uniqueList(toolInputs))
}
if outputsValue != nil {
// TODO: drop cmd.OutputDirs and use tailing-/ as dir target.
seen := make(map[string]bool)
var outputFiles, outputDirs []string
for _, out := range outputs {
if out == "" {
continue
}
if seen[out] {
continue
}
seen[out] = true
out, isDir := strings.CutSuffix(out, "/")
if isDir {
outputDirs = append(outputDirs, out)
} else {
outputFiles = append(outputFiles, out)
}
}
c.cmd.Outputs = path.Paths(outputFiles)
c.cmd.OutputDirs = path.Paths(outputDirs)
}
if auxiliaryLogOutputFilesValue != nil {
c.cmd.AuxiliaryLogOutputFiles = path.Paths(uniqueList(auxiliaryLogOutputFiles))
}
if auxiliaryLogOutputDirsValue != nil {
c.cmd.AuxiliaryLogOutputDirs = path.Paths(uniqueList(auxiliaryLogOutputDirs))
}
if cmdArgsValue != nil {
c.cmd.Args = cmdArgs
}
if rspfileContentValue != nil {
c.cmd.RSPFileContent = []byte(rspfileContent)
}
if reconcileOutputdirs != nil {
c.cmd.ReconcileOutputdirs = path.Paths(reconcileOutputdirs)
}
return starlark.None, nil
}
// Starlark function `actions.write(fname, content, is_executable)` to write the content a file in hashfs.
func starActionsWrite(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
log.V(1).Infof("actions.write args=%s kwargs=%s", args, kwargs)
c, ok := fn.Receiver().(starCmdValue)
if !ok {
return starlark.None, fmt.Errorf("unexpected receiver: %v", fn.Receiver())
}
var fname string
var content starlark.Bytes
var isExecutable bool
err := starlark.UnpackArgs("write", args, kwargs, "fname", &fname, "content?", &content, "is_executable?", isExecutable)
if err != nil {
return starlark.None, err
}
err = c.cmd.HashFS.WriteFile(c.ctx, c.cmd.WorkspaceRoot, path.New(fname), []byte(string(content)), isExecutable, time.Now(), c.cmd.CmdHash, c.cmd.EdgeHash)
return starlark.None, err
}
// Starlark function `actions.copy(src, dst, recursive)` to copy a file, or a dir, in hashfs.
func starActionsCopy(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
log.V(1).Infof("actions.copy args=%s kwargs=%s", args, kwargs)
c, ok := fn.Receiver().(starCmdValue)
if !ok {
return starlark.None, fmt.Errorf("unexpected receiver: %v", fn.Receiver())
}
var src, dst string
var recursive bool
ignoreMissingIntermediates := true // TODO: make it false by default.
err := starlark.UnpackArgs("copy", args, kwargs, "src", &src, "dst", &dst, "recursive?", &recursive, "ignore_missing_intermediates?", &ignoreMissingIntermediates)
if err != nil {
return starlark.None, err
}
if recursive {
// TODO: handler explicitly call ctx.actions.remove or so?
// Not sure it is good idea to provide remove in handler,
// as it would not be recommended action and may be abused
// if it is available.
// Until when we find legid use case of remove, just remove
// target dir of copy, as it would be ok to use copy
// to replace a directory hierarchy.
err = c.cmd.HashFS.RemoveAll(c.ctx, c.cmd.WorkspaceRoot, path.New(dst))
if err != nil {
return starlark.None, err
}
_, err = c.cmd.HashFS.Stat(c.ctx, c.cmd.WorkspaceRoot, path.New(src))
if err != nil {
// toplevel src must exist even if ignore_missing_intermediates.
// it would be build graph error.
return starlark.None, err
}
var files []string
files, err = actionsCopyRecursively(c.ctx, c.cmd, src, dst, time.Now(), c.cmd.CmdHash, c.cmd.EdgeHash, ignoreMissingIntermediates)
if err == nil && len(files) > 0 {
err = c.cmd.HashFS.Flush(c.ctx, c.cmd.WorkspaceRoot, path.Paths(files))
}
} else {
err = c.cmd.HashFS.Copy(c.ctx, c.cmd.WorkspaceRoot, path.New(src), path.New(dst), time.Now(), c.cmd.CmdHash, c.cmd.EdgeHash)
}
return starlark.None, err
}
// recursively copy from src to dst by setting mtime t with cmdhash,
// and returns a list of files that needs to be written to the disk.
// if src is a directory, it recurrsively calls itself without cmdhash.
// if src is a file, it just copies the file.
func actionsCopyRecursively(ctx context.Context, cmd *execute.Cmd, src, dst string, t time.Time, cmdhash, edgehash []byte, ignoreMissingIntermediates bool) ([]string, error) {
fi, err := cmd.HashFS.Stat(ctx, cmd.WorkspaceRoot, path.New(src))
if ignoreMissingIntermediates && errors.Is(err, fs.ErrNotExist) {
clog.Warningf(ctx, "copy src not exists %s: %v", src, err)
// just ignores
return nil, nil
}
if err != nil {
return nil, err
}
if fi.IsDir() {
ents, err := cmd.HashFS.ReadDir(ctx, cmd.WorkspaceRoot, path.New(src))
if ignoreMissingIntermediates && errors.Is(err, fs.ErrNotExist) {
// hashfs Stat exists, but ReadDir may detect missing dir on local disk.
clog.Warningf(ctx, "copy src not exists %s: %v", src, err)
return nil, nil
}
if err != nil {
return nil, err
}
err = cmd.HashFS.Mkdir(ctx, cmd.WorkspaceRoot, path.New(dst), cmdhash, edgehash)
if err != nil {
return nil, err
}
var files []string
for _, ent := range ents {
s := filepath.Join(src, ent.Name())
d := filepath.Join(dst, ent.Name())
// don't record cmdhash for recursive copy
// since these are not appeared in build graph
// but cleandead will try to delete these dirs/files.
f, err := actionsCopyRecursively(ctx, cmd, s, d, t, nil, nil, ignoreMissingIntermediates)
if err != nil {
return files, err
}
files = append(files, f...)
}
return files, nil
}
err = cmd.HashFS.Copy(ctx, cmd.WorkspaceRoot, path.New(src), path.New(dst), t, cmdhash, edgehash)
if ignoreMissingIntermediates && errors.Is(err, fs.ErrNotExist) {
clog.Warningf(ctx, "copy src not exists %s: %v", src, err)
// just ignores
return nil, nil
}
if err != nil {
return nil, err
}
if cmd.HashFS.NeedFlush(ctx, cmd.WorkspaceRoot, path.New(dst)) {
return []string{dst}, nil
}
return nil, nil
}
// Starlark function `actions.symlink(target, linkpath)` to create a symlink in hashfs.
func starActionsSymlink(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
log.V(1).Infof("actions.symlink args=%s kwargs=%s", args, kwargs)
c, ok := fn.Receiver().(starCmdValue)
if !ok {
return starlark.None, fmt.Errorf("unexpected receiver: %v", fn.Receiver())
}
var target, linkpath string
err := starlark.UnpackArgs("symlink", args, kwargs, "target", &target, "linkpath", &linkpath)
if err != nil {
return starlark.None, err
}
err = c.cmd.HashFS.Symlink(c.ctx, c.cmd.WorkspaceRoot, target, path.New(linkpath), time.Now(), c.cmd.CmdHash, c.cmd.EdgeHash)
return starlark.None, err
}
// Starlark function `actions.exit(exit_status, stdout, stderr)` to finish the execution of the command.
// can be used when no actual command invocation is needed, such as stamp, copy.
func starActionsExit(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
log.V(1).Infof("actions.exit args=%s kwargs=%s", args, kwargs)
c, ok := fn.Receiver().(starCmdValue)
if !ok {
return nil, fmt.Errorf("unexpected receiver: %v", fn.Receiver())
}
var exitStatus int32
var stdout, stderr starlark.Bytes
err := starlark.UnpackArgs("exit", args, kwargs, "exit_status", &exitStatus, "stdout?", &stdout, "stderr?", &stderr)
if err != nil {
return starlark.None, err
}
result := &rpb.ActionResult{
ExitCode: exitStatus,
}
if stdout != "" {
result.StdoutRaw = []byte(string(stdout))
}
if stderr != "" {
result.StderrRaw = []byte(string(stderr))
}
entries, err := c.cmd.HashFS.RawEntries(c.ctx, c.cmd.WorkspaceRoot, c.cmd.Outputs)
if err != nil {
return starlark.None, err
}
execute.ResultFromEntries(c.ctx, c.cmd.HashFS.DigestFunction(), result, string(c.cmd.WorkDir), entries)
c.cmd.SetActionResult(result, false)
return starlark.None, nil
}