blob: 17564ef30951b16ed7acb5f1bb113c3dc2a9c2bc [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package build
import (
"context"
"errors"
"io/fs"
"os"
"path/filepath"
"strings"
log "github.com/golang/glog"
"go.chromium.org/build/siso/execute"
"go.chromium.org/build/siso/execute/localexec"
"go.chromium.org/build/siso/o11y/clog"
"go.chromium.org/build/siso/path"
)
func (b *Builder) tapCanonicalizeCmd(ctx context.Context, cmd *execute.Cmd) error {
res, _ := cmd.ActionResult()
tapData, tapped := localexec.ExtractTapResult(res)
if !tapped {
return errors.New("no tap data")
}
var ninputs, noutputs int
// ignore out of workspace root
// TODO: use with input root absolute path?
// TODO: just use detected inputs?
seen := make(map[path.Path]bool)
for _, input := range cmd.AllInputs() {
seen[input] = true
ninputs++
}
// os.TempDir() is $TMPDIR or /tmp.
ignores := map[path.Path]struct{}{path.New(os.TempDir()): {}}
ignored := 0
if len(cmd.Env) > 0 {
for _, env := range cmd.Env {
tmpdir, ok := strings.CutPrefix(env, "TMPDIR=")
if ok {
if !filepath.IsAbs(tmpdir) {
clog.Warningf(ctx, "ignore TMPDIR: not absolute path: %q", tmpdir)
continue
}
ignores[path.New(tmpdir)] = struct{}{}
}
}
}
shouldIgnore := func(op string, p path.Path) bool {
for ignore := range ignores {
if p.HasPrefix(ignore) {
if log.V(1) {
clog.Infof(ctx, "ignore %s %q in %q", op, p, ignore)
}
ignored++
return true
}
}
return false
}
for _, input := range tapData.Reads {
if shouldIgnore("reads", path.New(input)) {
continue
}
rel, err := filepath.Rel(b.path.WorkspaceRoot, input)
if err != nil {
clog.Warningf(ctx, "reads relpath %q: %v", input, err)
continue
}
if !filepath.IsLocal(rel) {
continue
}
relPath := path.New(rel)
_, err = b.hashFS.Stat(ctx, b.path.WorkspaceRoot, relPath)
if errors.Is(err, fs.ErrNotExist) {
continue
}
if seen[relPath] {
continue
}
seen[relPath] = true
cmd.Inputs = append(cmd.Inputs, relPath)
}
clear(seen)
// need to use both original outputs and detected outputs.
// it might not detect output for restat action.
// it might detect unspecified outputs.
for _, output := range cmd.AllOutputs() {
seen[output] = true
noutputs++
}
for _, output := range tapData.Writes {
if shouldIgnore("writes", path.New(output)) {
continue
}
rel, err := filepath.Rel(b.path.WorkspaceRoot, output)
if err != nil {
clog.Warningf(ctx, "writes relpath %q: %v", output, err)
continue
}
if !filepath.IsLocal(rel) {
continue
}
relPath := path.New(rel)
fi, err := b.hashFS.Stat(ctx, b.path.WorkspaceRoot, relPath)
if err != nil {
continue
}
if fi.IsDir() {
// don't include output directories
// as it would forget all entries in the directory
// by hashfs Update.
continue
}
if seen[relPath] {
continue
}
seen[relPath] = true
cmd.Outputs = append(cmd.Outputs, relPath)
}
for _, del := range tapData.Deletes {
rel, err := filepath.Rel(b.path.WorkspaceRoot, del)
if err != nil {
clog.Warningf(ctx, "deletes relpath %q: %v", del, err)
continue
}
if !filepath.IsLocal(rel) {
continue
}
relPath := path.New(rel)
_, err = b.hashFS.Stat(ctx, b.path.WorkspaceRoot, relPath)
if errors.Is(err, fs.ErrNotExist) {
continue
}
b.hashFS.Forget(ctx, b.path.WorkspaceRoot, []path.Path{relPath})
clog.Infof(ctx, "delete %q", relPath)
}
// now tap results are applied to cmd, so we can consider
// this cmd is pure, thus cacheable.
cmd.Pure = true
clog.Infof(ctx, "tap canonicalized inputs=%d->%d outputs=%d->%d ignored=%d", ninputs, len(cmd.Inputs), noutputs, len(cmd.Outputs), ignored)
return nil
}