Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions core/itg/graph/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ type UpdateGraphInput struct {
WorkspaceRoot string
FullHashRepos StringSet
UseBzlmod bool
ExcludedRegex []string
// RepoMarkerHashes maps canonical bzlmod repo names to repo rule input hashes. Required
// when UseBzlmod is set; see targethasher.HashConfig.RepoMarkerHashes.
RepoMarkerHashes map[string][]byte
// RepoMapping maps apparent repo names to canonical names; see targethasher.HashConfig.RepoMapping.
RepoMapping map[string]string
}

// UpdateGraph updates the dependency relationships and hashes of targets in the graph.
Expand All @@ -57,9 +63,21 @@ func (g *OptimizedGraph) UpdateGraph(

fullHashReposSet := set.NewSet(input.FullHashRepos.UnsortedList()...)

// HashExternalTargets adds external rule targets and hashes them
if err := targethasher.HashExternalTargets(ctx, rawQueryResults, targets, sourceHasher, input.WorkspaceRoot, fullHashReposSet, warns, input.UseBzlmod); err != nil {
return err
if !input.UseBzlmod {
// Legacy WORKSPACE: add external rule targets (//external:*) to the map and hash them.
if err := targethasher.HashExternalTargets(ctx, rawQueryResults, targets, sourceHasher, input.WorkspaceRoot, fullHashReposSet, warns, input.UseBzlmod); err != nil {
return err
}
} else {
// Bzlmod: collapse external source/generated file targets using Bazel marker file
// hashes instead of letting computeAvailableHashes read each file's content from disk.
excludedRegex, err := targethasher.CompileExcludedRegex(input.ExcludedRegex)
if err != nil {
return err
}
if err := targethasher.HashExternalTargetsBzlmod(targets, fullHashReposSet, excludedRegex, input.RepoMarkerHashes, input.RepoMapping); err != nil {
return err
}
}

allInvalidated := NewIntSet()
Expand Down Expand Up @@ -187,6 +205,10 @@ func computeAvailableHashes(
h.Write([]byte(name))
hash = h.Sum(nil)
case targethasher.SourceFileType:
if target.Hash != nil {
// already hashed, e.g. by the bzlmod external-repo collapse
continue
}
h, err := hasher.HashSourceFile(ctx, target.SourceFile)
if err != nil {
return err
Expand Down
67 changes: 67 additions & 0 deletions core/itg/graph/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@ func TestComputeAvailableHashes(t *testing.T) {
assert.Nil(t, targets[name].Hash, "full hash is not computed here — deps are needed")
})

t.Run("source file already hashed is not re-hashed", func(t *testing.T) {
t.Parallel()
existing := []byte{0xaa, 0xbb}
hasher := &fakeSourceHasher{err: assert.AnError}
name := "@@repo_a//pkg:file.py"
targets := map[string]*targethasher.Target{
name: {Name: name, RuleType: targethasher.SourceFileType, Hash: existing},
}

require.NoError(t, computeAvailableHashes(context.Background(), hasher, targets))
assert.Equal(t, existing, targets[name].Hash, "a pre-set hash (e.g. from the bzlmod collapse) must not be overwritten by the disk hasher")
})

t.Run("source hasher error is propagated", func(t *testing.T) {
t.Parallel()
hasher := &fakeSourceHasher{err: assert.AnError}
Expand Down Expand Up @@ -234,6 +247,60 @@ func TestComputeHashes(t *testing.T) {
})
}

// --- UpdateGraph ---

func strPtr(s string) *string { return &s }

func TestUpdateGraph_BzlmodCollapsesExternalFiles(t *testing.T) {
t.Parallel()

marker := []byte{0xaa, 0xbb, 0xcc}
qr := &buildpb.QueryResult{
Target: []*buildpb.Target{
{
Type: buildpb.Target_RULE.Enum(),
Rule: &buildpb.Rule{
Name: strPtr("//pkg:app"),
RuleClass: strPtr("go_binary"),
RuleInput: []string{"@@repo_a//pkg:file.py"},
},
},
{
Type: buildpb.Target_SOURCE_FILE.Enum(),
SourceFile: &buildpb.SourceFile{Name: strPtr("@@repo_a//pkg:file.py")},
},
},
}

// Fails the test loudly if the itg update path falls back to real content
// hashing for the bzlmod external file instead of the marker-based collapse.
hasher := &fakeSourceHasher{err: assert.AnError}

g := OptimizeGraph(nil)
err := g.UpdateGraph(context.Background(), hasher, UpdateGraphInput{
QueryResult: qr,
UseBzlmod: true,
FullHashRepos: NewStringSet(),
ChangedPkgs: NewStringSet(),
DeletedPkgs: NewStringSet(),
DeletedSrcFiles: NewStringSet(),
RepoMarkerHashes: map[string][]byte{"repo_a": marker},
})
require.NoError(t, err)

fileID, ok := g.TargetNameToID["@@repo_a//pkg:file.py"]
require.True(t, ok)

h := sha1.New()
h.Write(marker)
expected := h.Sum(nil)

assert.Equal(t, expected, g.OptimizedTargets[fileID].Hash, "external file hash should come from the repo marker collapse")

appID := g.TargetNameToID["//pkg:app"]
assert.NotNil(t, g.OptimizedTargets[appID].Hash, "root target hash should still be computable from the collapsed dep hash")
}

func TestComputeInvalidatedHashesCycleOrderInvariance(t *testing.T) {
t.Parallel()

Expand Down
25 changes: 18 additions & 7 deletions core/targethasher/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,9 @@ func FromProto(ctx context.Context, r *buildpb.QueryResult, workspaceroot string
// always calculate hash for individual files in the main repo.
fullHashRepos := set.NewSet(append([]string{""}, hashConfig.FullHashRepos...)...)

excludedRegex := make([]*regexp.Regexp, 0, len(hashConfig.ExcludedRegex))
for _, pattern := range hashConfig.ExcludedRegex {
re, err := regexp.Compile(pattern)
if err != nil {
return EmptyResult(), fmt.Errorf("failed to compile excluded regex pattern %q: %w", pattern, err)
}
excludedRegex = append(excludedRegex, re)
excludedRegex, err := CompileExcludedRegex(hashConfig.ExcludedRegex)
if err != nil {
return EmptyResult(), err
}

result, err := fromProto(ctx, r, &diskHashHelper{
Expand Down Expand Up @@ -491,6 +487,7 @@ func HashExternalTargetsBzlmod(targets map[string]*Target, fullHashRepos set.Set
markerHash, hasMarker := repoMarkerHashes[repo]
if !hasMarker || len(markerHash) == 0 {
if mappedFrom != "" {
// Built-in repos such as @bazel_tools have no marker.
continue
}
return fmt.Errorf("bzlmod repo %q has targets in query but no marker file", repo)
Expand Down Expand Up @@ -906,6 +903,20 @@ func isExternalTarget(targetName string) bool {
return strings.HasPrefix(targetName, externalWorkspaceFilePrefix)
}

// CompileExcludedRegex compiles the configured excluded-file regex patterns
// for use with isExcluded / shouldCollapseToBzlmodRepo.
func CompileExcludedRegex(patterns []string) ([]*regexp.Regexp, error) {
excludedRegex := make([]*regexp.Regexp, 0, len(patterns))
for _, pattern := range patterns {
re, err := regexp.Compile(pattern)
if err != nil {
return nil, fmt.Errorf("failed to compile excluded regex pattern %q: %w", pattern, err)
}
excludedRegex = append(excludedRegex, re)
}
return excludedRegex, nil
}

func isExcluded(targetName string, excludedRegex []*regexp.Regexp) bool {
for _, re := range excludedRegex {
if re.MatchString(targetName) {
Expand Down