blob: 5efb3ad4d7b1bcf35f199aa509bd4c8ec4f3584b [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package pointless
import (
"fmt"
"github.com/bmatcuk/doublestar"
"github.com/golang/protobuf/ptypes/wrappers"
chromite "go.chromium.org/chromiumos/infra/proto/go/chromite/api"
testplans_pb "go.chromium.org/chromiumos/infra/proto/go/testplans"
bbproto "go.chromium.org/luci/buildbucket/proto"
"log"
"path/filepath"
"sort"
"strings"
"testplans/internal/git"
)
const (
// The location of the repo checkout inside the chroot.
// This prefix needs to be trimmed from dependencySourcePaths at the moment, but that will become
// unnecessary once https://crbug.com/957090 is resolved.
depSourcePathPrefix = "/mnt/host/source/"
)
// CheckBuilder assesses whether a child builder is pointless for a given CQ run. This may be the
// case if the commits in the CQ run don't affect any files that could possibly affect this
// builder's Portage graph.
func CheckBuilder(
build *bbproto.Build,
changeRevs *git.ChangeRevData,
depGraph *chromite.DepGraph,
repoToSrcRoot map[string]string,
cfg testplans_pb.BuildIrrelevanceCfg) (*testplans_pb.PointlessBuildCheckResponse, error) {
// Get all of the files referenced by each GerritCommit in the Build.
affectedFiles, err := extractAffectedFiles(build, changeRevs, repoToSrcRoot)
if err != nil {
return nil, fmt.Errorf("error in extractAffectedFiles: %+v", err)
}
if len(affectedFiles) == 0 {
log.Printf("Builder %s: No affected files, so this can't be a CQ run. "+
"Aborting with BuildIsPointless := false", getBuilderName(build))
return &testplans_pb.PointlessBuildCheckResponse{
BuildIsPointless: &wrappers.BoolValue{Value: false},
}, nil
}
// Filter out files that are irrelevant to Portage because of the config.
affectedFiles = filterByBuildIrrelevantPaths(affectedFiles, cfg)
if len(affectedFiles) == 0 {
log.Printf("Builder %s: All files ruled out by build-irrelevant paths. This means that "+
"none of the Gerrit changes in the build input could affect the outcome of the build",
getBuilderName(build))
return &testplans_pb.PointlessBuildCheckResponse{
BuildIsPointless: &wrappers.BoolValue{Value: true},
PointlessBuildReason: testplans_pb.PointlessBuildCheckResponse_IRRELEVANT_TO_KNOWN_NON_PORTAGE_DIRECTORIES,
}, nil
}
log.Printf("After considering build-irrelevant paths, we still must consider files:\n%v",
strings.Join(affectedFiles, "\n"))
// Filter out files that aren't in the Portage dep graph.
affectedFiles = filterByPortageDeps(affectedFiles, depGraph)
if len(affectedFiles) == 0 {
log.Printf("Builder %s: All files ruled out after checking dep graph", getBuilderName(build))
return &testplans_pb.PointlessBuildCheckResponse{
BuildIsPointless: &wrappers.BoolValue{Value: true},
PointlessBuildReason: testplans_pb.PointlessBuildCheckResponse_IRRELEVANT_TO_DEPS_GRAPH,
}, nil
}
log.Printf("Builder %s: This build is not pointless, due to files:\n%v",
getBuilderName(build), strings.Join(affectedFiles, "\n"))
return &testplans_pb.PointlessBuildCheckResponse{
BuildIsPointless: &wrappers.BoolValue{Value: false},
}, nil
}
func extractAffectedFiles(build *bbproto.Build,
changeRevs *git.ChangeRevData, repoToSrcRoot map[string]string) ([]string, error) {
allAffectedFiles := make([]string, 0)
for _, gc := range build.Input.GerritChanges {
rev, err := changeRevs.GetChangeRev(gc.Host, gc.Change, int32(gc.Patchset))
if err != nil {
return nil, err
}
srcRootMapping, found := repoToSrcRoot[rev.Project]
if !found {
return nil, fmt.Errorf("Found no source mapping for project %s", rev.Project)
}
affectedFiles := make([]string, 0, len(rev.Files))
for _, file := range rev.Files {
fileSrcPath := fmt.Sprintf("%s/%s", srcRootMapping, file)
affectedFiles = append(affectedFiles, fileSrcPath)
}
sort.Strings(affectedFiles)
log.Printf("For https://%s/%d, affected files:\n%v\n\n",
gc.Host, gc.Change, strings.Join(affectedFiles, "\n"))
allAffectedFiles = append(allAffectedFiles, affectedFiles...)
}
sort.Strings(allAffectedFiles)
log.Printf("All affected files:\n%v\n\n", strings.Join(allAffectedFiles, "\n"))
return allAffectedFiles, nil
}
func filterByBuildIrrelevantPaths(files []string, cfg testplans_pb.BuildIrrelevanceCfg) []string {
pipFilteredFiles := make([]string, 0)
affectedFile:
for _, f := range files {
for _, pattern := range cfg.IrrelevantFilePatterns {
match, err := doublestar.Match(pattern.Pattern, f)
if err != nil {
log.Fatalf("Failed to match pattern %s against file %s: %v", pattern, f, err)
}
if match {
log.Printf("Ignoring file %s, since it matches Portage irrelevant pattern %s", f, pattern.Pattern)
continue affectedFile
}
}
for _, isp := range cfg.IrrelevantSourcePaths {
if f == isp.Path {
log.Printf("Ignoring file %s, since it matches Portage irrelevant path %s", f, isp.Path)
continue affectedFile
}
spAsDir := strings.TrimSuffix(isp.Path, "/") + "/"
if strings.HasPrefix(f, spAsDir) {
log.Printf("Ignoring file %s, since it's contained in Portage irrelevant dir %s", f, spAsDir)
continue affectedFile
}
}
for _, ifbn := range cfg.IrrelevantFileBaseNames {
if filepath.Base(f) == ifbn.Name {
log.Printf("Ignoring file %s, since it has Portage irrelevant file base name %s", f, ifbn)
continue affectedFile
}
}
log.Printf("Cannot ignore file %s by Portage irrelevant path rules", f)
pipFilteredFiles = append(pipFilteredFiles, f)
}
return pipFilteredFiles
}
func filterByPortageDeps(files []string, depGraph *chromite.DepGraph) []string {
if depGraph == nil {
log.Printf("No Portage DepGraph was provided, so no files can be filtered out by this rule.")
return files
}
portageDeps := make([]string, 0)
for _, pd := range depGraph.PackageDeps {
for _, sp := range pd.DependencySourcePaths {
portageDeps = append(portageDeps, strings.TrimPrefix(sp.Path, depSourcePathPrefix))
}
}
log.Printf("Found %d Portage deps to consider from the build graph:\n"+
"<portage dep paths>\n%v\n</portage dep paths>",
len(portageDeps), strings.Join(portageDeps, "\n"))
portageFilteredFiles := make([]string, 0)
affectedFile:
for _, f := range files {
for _, pd := range portageDeps {
if f == pd {
log.Printf("Cannot ignore file %s due to Portage dependency %s", f, pd)
portageFilteredFiles = append(portageFilteredFiles, f)
continue affectedFile
}
pdAsDir := strings.TrimSuffix(pd, "/") + "/"
if strings.HasPrefix(f, pdAsDir) {
log.Printf("Cannot ignore file %s since it's in Portage dependency %s", f, pd)
portageFilteredFiles = append(portageFilteredFiles, f)
continue affectedFile
}
}
log.Printf("Ignoring file %s because no prefix of it is referenced in the dep graph", f)
}
return portageFilteredFiles
}
func getBuilderName(bb *bbproto.Build) string {
return bb.GetBuilder().GetBuilder()
}