Skip to content

Commit 00da366

Browse files
authored
Implement ignoring directories based on wildcards. (#24203)
This is accomplished by a new directive in REPO.bazel, "ignore_directories()". It takes a single argument, a list of directories to ignore and it allows the same wildcards as glob(). This is done separately from .bazelignore to provide a migration path off of that weird single-purpose configuration file. Implementing this requires splitting RepoFileFunction into two: a part that parses the repository file and one that creates a PackageArgs instance. This was necessary to avoid a Skyframe dependency cycle: when a WORKSPACE file is present and it loads a .bzl file from a repository with a REPO.bazel file, the repo mapping for the main repository depends on the WORKSPACE file, which depends on the .bzl file, which depends on the IgnoredPackagePrefixesValue of its repository, which then depends on the repo mapping of the main repository and the one the .bzl file is in, which then depend on the WORKSPACE file. Fixes #7093. RELNOTES[NEW]: REPO.bazel now allows another directive, "ignore_directories()". It takes a list of directories to ignore just like .bazelignore does, but with glob semantics. Closes #24032. PiperOrigin-RevId: 693227896 Change-Id: Ia3e02a2bfe9caf999fc641f75261b528b19c1d03
1 parent 1466f99 commit 00da366

32 files changed

+784
-249
lines changed

src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkGlobalsImpl.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import com.google.devtools.build.lib.collect.nestedset.Depset;
2424
import com.google.devtools.build.lib.packages.BuildGlobals;
2525
import com.google.devtools.build.lib.packages.Proto;
26-
import com.google.devtools.build.lib.packages.RepoCallable;
26+
import com.google.devtools.build.lib.packages.RepoFileGlobals;
2727
import com.google.devtools.build.lib.packages.SelectorList;
2828
import com.google.devtools.build.lib.packages.StarlarkGlobals;
2929
import com.google.devtools.build.lib.packages.StarlarkNativeModule;
@@ -133,7 +133,7 @@ public ImmutableMap<String, Object> getModuleToplevels() {
133133
@Override
134134
public ImmutableMap<String, Object> getRepoToplevels() {
135135
ImmutableMap.Builder<String, Object> env = ImmutableMap.builder();
136-
Starlark.addMethods(env, RepoCallable.INSTANCE);
136+
Starlark.addMethods(env, RepoFileGlobals.INSTANCE);
137137
return env.buildOrThrow();
138138
}
139139

src/main/java/com/google/devtools/build/lib/cmdline/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ java_library(
5151
"//src/main/java/com/google/devtools/build/lib/util:detailed_exit_code",
5252
"//src/main/java/com/google/devtools/build/lib/util:hash_codes",
5353
"//src/main/java/com/google/devtools/build/lib/util:string",
54+
"//src/main/java/com/google/devtools/build/lib/vfs",
5455
"//src/main/java/com/google/devtools/build/lib/vfs:ospathpolicy",
5556
"//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
5657
"//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",

src/main/java/com/google/devtools/build/lib/cmdline/IgnoredSubdirectories.java

+105-18
Original file line numberDiff line numberDiff line change
@@ -14,57 +14,132 @@
1414

1515
package com.google.devtools.build.lib.cmdline;
1616

17+
import static com.google.common.collect.ImmutableList.toImmutableList;
1718
import static com.google.common.collect.ImmutableSet.toImmutableSet;
1819

1920
import com.google.common.base.MoreObjects;
2021
import com.google.common.base.Preconditions;
22+
import com.google.common.base.Splitter;
23+
import com.google.common.collect.ImmutableList;
2124
import com.google.common.collect.ImmutableSet;
25+
import com.google.common.collect.Iterables;
26+
import com.google.devtools.build.lib.skyframe.serialization.DeserializationContext;
27+
import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec;
28+
import com.google.devtools.build.lib.skyframe.serialization.SerializationContext;
29+
import com.google.devtools.build.lib.skyframe.serialization.SerializationException;
2230
import com.google.devtools.build.lib.vfs.PathFragment;
31+
import com.google.devtools.build.lib.vfs.UnixGlob;
32+
import com.google.protobuf.CodedInputStream;
33+
import com.google.protobuf.CodedOutputStream;
34+
import java.io.IOException;
2335
import java.util.Objects;
2436
import javax.annotation.Nullable;
2537

2638
/**
2739
* A set of subdirectories to ignore during target pattern matching or globbing.
28-
*
29-
* <p>This is currently just a prefix, but will eventually support glob-style wildcards.
3040
*/
3141
public final class IgnoredSubdirectories {
32-
public static final IgnoredSubdirectories EMPTY = new IgnoredSubdirectories(ImmutableSet.of());
42+
public static final IgnoredSubdirectories EMPTY =
43+
new IgnoredSubdirectories(ImmutableSet.of(), ImmutableList.of());
44+
45+
private static final Splitter SLASH_SPLITTER = Splitter.on("/");
3346

3447
private final ImmutableSet<PathFragment> prefixes;
3548

36-
private IgnoredSubdirectories(ImmutableSet<PathFragment> prefixes) {
37-
for (PathFragment prefix : prefixes) {
38-
Preconditions.checkArgument(!prefix.isAbsolute());
49+
// String[] is mutable; we keep the split version because that's faster to match and the non-split
50+
// one because that allows for simpler equality checking and then matchingEntry() doesn't need to
51+
// allocate new objects.
52+
private final ImmutableList<String> patterns;
53+
private final ImmutableList<String[]> splitPatterns;
54+
55+
private static class Codec implements ObjectCodec<IgnoredSubdirectories> {
56+
private static final Codec INSTANCE = new Codec();
57+
58+
@Override
59+
public Class<? extends IgnoredSubdirectories> getEncodedClass() {
60+
return IgnoredSubdirectories.class;
61+
}
62+
63+
@Override
64+
public void serialize(
65+
SerializationContext context, IgnoredSubdirectories obj, CodedOutputStream codedOut)
66+
throws SerializationException, IOException {
67+
context.serialize(obj.prefixes, codedOut);
68+
context.serialize(obj.patterns, codedOut);
3969
}
70+
71+
@Override
72+
public IgnoredSubdirectories deserialize(
73+
DeserializationContext context, CodedInputStream codedIn)
74+
throws SerializationException, IOException {
75+
ImmutableSet<PathFragment> prefixes = context.deserialize(codedIn);
76+
ImmutableList<String> patterns = context.deserialize(codedIn);
77+
78+
return new IgnoredSubdirectories(prefixes, patterns);
79+
}
80+
}
81+
82+
private IgnoredSubdirectories(
83+
ImmutableSet<PathFragment> prefixes, ImmutableList<String> patterns) {
4084
this.prefixes = prefixes;
85+
this.patterns = patterns;
86+
this.splitPatterns =
87+
patterns.stream()
88+
.map(p -> Iterables.toArray(SLASH_SPLITTER.split(p), String.class))
89+
.collect(toImmutableList());
4190
}
4291

4392
public static IgnoredSubdirectories of(ImmutableSet<PathFragment> prefixes) {
44-
if (prefixes.isEmpty()) {
93+
return of(prefixes, ImmutableList.of());
94+
}
95+
96+
public static IgnoredSubdirectories of(
97+
ImmutableSet<PathFragment> prefixes, ImmutableList<String> patterns) {
98+
if (prefixes.isEmpty() && patterns.isEmpty()) {
4599
return EMPTY;
46-
} else {
47-
return new IgnoredSubdirectories(prefixes);
48100
}
101+
102+
for (PathFragment prefix : prefixes) {
103+
Preconditions.checkArgument(!prefix.isAbsolute());
104+
}
105+
106+
return new IgnoredSubdirectories(prefixes, patterns);
49107
}
50108

51109
public IgnoredSubdirectories withPrefix(PathFragment prefix) {
52-
ImmutableSet<PathFragment> prefixed =
110+
Preconditions.checkArgument(!prefix.isAbsolute());
111+
112+
ImmutableSet<PathFragment> prefixedPrefixes =
53113
prefixes.stream().map(prefix::getRelative).collect(toImmutableSet());
54-
return new IgnoredSubdirectories(prefixed);
114+
115+
ImmutableList<String> prefixedPatterns =
116+
patterns.stream().map(p -> prefix + "/" + p).collect(toImmutableList());
117+
118+
return new IgnoredSubdirectories(prefixedPrefixes, prefixedPatterns);
55119
}
56120

57121
public IgnoredSubdirectories union(IgnoredSubdirectories other) {
58122
return new IgnoredSubdirectories(
59-
ImmutableSet.<PathFragment>builder().addAll(prefixes).addAll(other.prefixes).build());
123+
ImmutableSet.<PathFragment>builder().addAll(prefixes).addAll(other.prefixes).build(),
124+
ImmutableList.copyOf(
125+
ImmutableSet.<String>builder().addAll(patterns).addAll(other.patterns).build()));
60126
}
61127

62128
/** Filters out entries that cannot match anything under {@code directory}. */
63129
public IgnoredSubdirectories filterForDirectory(PathFragment directory) {
64130
ImmutableSet<PathFragment> filteredPrefixes =
65131
prefixes.stream().filter(p -> p.startsWith(directory)).collect(toImmutableSet());
66132

67-
return new IgnoredSubdirectories(filteredPrefixes);
133+
String[] splitDirectory =
134+
Iterables.toArray(SLASH_SPLITTER.split(directory.getPathString()), String.class);
135+
ImmutableList.Builder<String> filteredPatterns = ImmutableList.builder();
136+
for (int i = 0; i < patterns.size(); i++) {
137+
if (UnixGlob.canMatchChild(splitPatterns.get(i), splitDirectory)) {
138+
filteredPatterns.add(patterns.get(i));
139+
}
140+
}
141+
142+
return new IgnoredSubdirectories(filteredPrefixes, filteredPatterns.build());
68143
}
69144

70145
public ImmutableSet<PathFragment> prefixes() {
@@ -95,10 +170,17 @@ public boolean allPathsAreUnder(PathFragment directory) {
95170

96171
/** Returns the entry that matches a given directory or {@code null} if none. */
97172
@Nullable
98-
public PathFragment matchingEntry(PathFragment directory) {
173+
public String matchingEntry(PathFragment directory) {
99174
for (PathFragment prefix : prefixes) {
100175
if (directory.startsWith(prefix)) {
101-
return prefix;
176+
return prefix.getPathString();
177+
}
178+
}
179+
180+
String[] segmentArray = Iterables.toArray(directory.segments(), String.class);
181+
for (int i = 0; i < patterns.size(); i++) {
182+
if (UnixGlob.matchesPrefix(splitPatterns.get(i), segmentArray)) {
183+
return patterns.get(i);
102184
}
103185
}
104186

@@ -111,17 +193,22 @@ public boolean equals(Object other) {
111193
return false;
112194
}
113195

196+
// splitPatterns is a function of patterns so it's enough to check if patterns is equal
114197
IgnoredSubdirectories that = (IgnoredSubdirectories) other;
115-
return Objects.equals(this.prefixes, that.prefixes);
198+
return Objects.equals(this.prefixes, that.prefixes)
199+
&& Objects.equals(this.patterns, that.patterns);
116200
}
117201

118202
@Override
119203
public int hashCode() {
120-
return prefixes.hashCode();
204+
return Objects.hash(prefixes, patterns);
121205
}
122206

123207
@Override
124208
public String toString() {
125-
return MoreObjects.toStringHelper("IgnoredSubdirectories").add("prefixes", prefixes).toString();
209+
return MoreObjects.toStringHelper("IgnoredSubdirectories")
210+
.add("prefixes", prefixes)
211+
.add("patterns", patterns)
212+
.toString();
126213
}
127214
}

src/main/java/com/google/devtools/build/lib/cmdline/TargetPattern.java

+3-5
Original file line numberDiff line numberDiff line change
@@ -595,8 +595,7 @@ public <T, E extends Exception & QueryExceptionMarkerInterface> void eval(
595595
this,
596596
excludedSubdirectories);
597597
IgnoredSubdirectories ignoredSubdirectories = ignoredSubdirectoriesSupplier.get();
598-
PathFragment matchingEntry =
599-
ignoredSubdirectories.matchingEntry(directory.getPackageFragment());
598+
String matchingEntry = ignoredSubdirectories.matchingEntry(directory.getPackageFragment());
600599
if (warnIfFiltered(matchingEntry, resolver)) {
601600
return;
602601
}
@@ -632,8 +631,7 @@ ListenableFuture<Void> evalAsync(
632631
IgnoredSubdirectories filteredIgnoredSubdirectories;
633632
try {
634633
IgnoredSubdirectories ignoredSubdirectories = ignoredSubdirectoriesSupplier.get();
635-
PathFragment matchingEntry =
636-
ignoredSubdirectories.matchingEntry(directory.getPackageFragment());
634+
String matchingEntry = ignoredSubdirectories.matchingEntry(directory.getPackageFragment());
637635
if (warnIfFiltered(matchingEntry, resolver)) {
638636
return immediateVoidFuture();
639637
}
@@ -654,7 +652,7 @@ ListenableFuture<Void> evalAsync(
654652
executor);
655653
}
656654

657-
private boolean warnIfFiltered(PathFragment matchingEntry, TargetPatternResolver<?> resolver) {
655+
private boolean warnIfFiltered(String matchingEntry, TargetPatternResolver<?> resolver) {
658656
if (matchingEntry != null) {
659657
resolver.warn(
660658
"Pattern '"

src/main/java/com/google/devtools/build/lib/packages/RepoCallable.java

-59
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright 2023 The Bazel Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.devtools.build.lib.packages;
16+
17+
import java.util.Map;
18+
import net.starlark.java.annot.Param;
19+
import net.starlark.java.annot.ParamType;
20+
import net.starlark.java.annot.StarlarkMethod;
21+
import net.starlark.java.eval.EvalException;
22+
import net.starlark.java.eval.Sequence;
23+
import net.starlark.java.eval.Starlark;
24+
import net.starlark.java.eval.StarlarkThread;
25+
26+
/** Definition of the {@code repo()} function used in REPO.bazel files. */
27+
public final class RepoFileGlobals {
28+
private RepoFileGlobals() {}
29+
30+
public static final RepoFileGlobals INSTANCE = new RepoFileGlobals();
31+
32+
@StarlarkMethod(
33+
name = "ignore_directories",
34+
doc =
35+
"The list of directories to ignore in this repository. <p>This function takes a list"
36+
+ " of strings and a directory is ignored if any of the given strings matches its"
37+
+ " repository-relative path according to the semantics of the <code>glob()</code>"
38+
+ " function. This function can be used to ignore directories that are implementation"
39+
+ " details of source control systems, output files of other build systems, etc.",
40+
useStarlarkThread = true,
41+
parameters = {
42+
@Param(
43+
name = "dirs",
44+
allowedTypes = {
45+
@ParamType(type = Sequence.class, generic1 = String.class),
46+
})
47+
})
48+
public void ignoreDirectories(Iterable<?> dirsUnchecked, StarlarkThread thread)
49+
throws EvalException {
50+
Sequence<String> dirs = Sequence.cast(dirsUnchecked, String.class, "dirs");
51+
RepoThreadContext context = RepoThreadContext.fromOrFail(thread, "repo()");
52+
53+
if (context.isIgnoredDirectoriesSet()) {
54+
throw new EvalException("'ignored_directories()' can only be called once");
55+
}
56+
57+
context.setIgnoredDirectories(dirs);
58+
}
59+
60+
@StarlarkMethod(
61+
name = "repo",
62+
documented = false, // documented separately
63+
extraKeywords = @Param(name = "kwargs"),
64+
useStarlarkThread = true)
65+
public void repoCallable(Map<String, Object> kwargs, StarlarkThread thread) throws EvalException {
66+
RepoThreadContext context = RepoThreadContext.fromOrFail(thread, "repo()");
67+
if (context.isRepoFunctionCalled()) {
68+
throw Starlark.errorf("'repo' can only be called once in the REPO.bazel file");
69+
}
70+
71+
if (context.isIgnoredDirectoriesSet()) {
72+
throw Starlark.errorf("if repo() is called, it must be called before any other functions");
73+
}
74+
75+
if (kwargs.isEmpty()) {
76+
throw Starlark.errorf("at least one argument must be given to the 'repo' function");
77+
}
78+
79+
context.setPackageArgsMap(kwargs);
80+
}
81+
}

0 commit comments

Comments
 (0)