Make GuardedValue match repo name substring instead of exact name.
This allows guarded values to work with bzlmod, where a repo's name has
various bzlmod pieces surrounding the apparent repo name. For example,
under bzlmod:
* `@rules_python` -> `@rules_python~override` (module repo)
* `@rules_python_internal` -> `@rules_python~override~internal_deps~rules_python_internal` (repo created
as part of a module extension)
PiperOrigin-RevId: 568689554
Change-Id: I2e8cda4717bcd3dc1cd79d44dfd66e3d436aa8f8
diff --git a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/core/ContextAndFlagGuardedValue.java b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/core/ContextAndFlagGuardedValue.java
index 91be2e9..82d4741 100644
--- a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/core/ContextAndFlagGuardedValue.java
+++ b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/core/ContextAndFlagGuardedValue.java
@@ -15,8 +15,6 @@
package com.google.devtools.build.lib.starlarkbuildapi.core;
import com.google.common.collect.ImmutableSet;
-import com.google.devtools.build.lib.cmdline.BazelCompileContext;
-import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
@@ -32,41 +30,30 @@
public final class ContextAndFlagGuardedValue {
/**
* Creates a flag guard which only permits access of the given object when the given boolean flag
- * is false or the requesting .bzl file is in a specific patckage path. If the given flag is true
- * and the object would be accessed, an error is thrown describing the feature as deprecated, and
- * describing that the flag may be set to false to re-enable it.
+ * is false or the requesting .bzl file's (repo, path) matches one of the allowed entries (see
+ * ContextGuardedFlag for match criteria). If the given flag is true and the object would be
+ * accessed, an error is thrown describing the feature as deprecated, and describing that the flag
+ * may be set to false to re-enable it.
*
* <p>The flag identifier must have a + or - prefix; see StarlarkSemantics.
*/
public static GuardedValue onlyInAllowedReposOrWhenIncompatibleFlagIsFalse(
- String flag, Object obj, ImmutableSet<PackageIdentifier> allowedPrefixes) {
+ String flag, Object obj, ImmutableSet<PackageIdentifier> allowedEntries) {
GuardedValue flagGuard = FlagGuardedValue.onlyWhenIncompatibleFlagIsFalse(flag, obj);
+ GuardedValue contextGuard = ContextGuardedValue.onlyInAllowedRepos(obj, allowedEntries);
return new GuardedValue() {
@Override
public boolean isObjectAccessibleUsingSemantics(
StarlarkSemantics semantics, @Nullable Object clientData) {
- boolean accessible = flagGuard.isObjectAccessibleUsingSemantics(semantics, clientData);
- // Filtering of predeclareds is only done at compile time, when the client data is
- // BazelCompileContext and not BazelModuleContext.
- if (!accessible && clientData != null && clientData instanceof BazelCompileContext) {
- BazelCompileContext context = (BazelCompileContext) clientData;
- Label label = context.label();
-
- for (PackageIdentifier prefix : allowedPrefixes) {
- if (label.getRepository().equals(prefix.getRepository())
- && label.getPackageFragment().startsWith(prefix.getPackageFragment())) {
- return true;
- }
- }
- }
- return accessible;
+ return flagGuard.isObjectAccessibleUsingSemantics(semantics, clientData)
+ || contextGuard.isObjectAccessibleUsingSemantics(semantics, clientData);
}
@Override
public String getErrorFromAttemptingAccess(String name) {
return name
+ " may only be used from one of the following repositories or prefixes: "
- + allowedPrefixes.stream()
+ + allowedEntries.stream()
.map(PackageIdentifier::toString)
.collect(Collectors.joining(", "))
+ ". It may be temporarily re-enabled for general use by setting --"
diff --git a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/core/ContextGuardedValue.java b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/core/ContextGuardedValue.java
index 7c670f6..5223e1b 100644
--- a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/core/ContextGuardedValue.java
+++ b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/core/ContextGuardedValue.java
@@ -18,6 +18,7 @@
import com.google.devtools.build.lib.cmdline.BazelCompileContext;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import net.starlark.java.eval.GuardedValue;
@@ -32,12 +33,13 @@
private ContextGuardedValue() {}
/**
- * Creates a guard which only permits access of the given object when the requesting .bzl file is
- * in a specific patckage path. An error is thrown if accessing it is done outside the allowed
- * package paths.
+ * Creates a guard which only permits access of the given object when the requesting .bzl file's
+ * repo name matches one of the (repoNameSubstr, pathPrefix) entries (note: under bzlmod, repos
+ * are prefixed with the bzlmod module name). An error is thrown if accessing it is done outside
+ * the allowed package paths.
*/
public static GuardedValue onlyInAllowedRepos(
- Object obj, ImmutableSet<PackageIdentifier> allowedPrefixes) {
+ Object obj, ImmutableSet<PackageIdentifier> allowedEntries) {
return new GuardedValue() {
@Override
public boolean isObjectAccessibleUsingSemantics(
@@ -48,9 +50,20 @@
BazelCompileContext context = (BazelCompileContext) clientData;
Label label = context.label();
- for (PackageIdentifier prefix : allowedPrefixes) {
- if (label.getRepository().equals(prefix.getRepository())
- && label.getPackageFragment().startsWith(prefix.getPackageFragment())) {
+ for (PackageIdentifier entry : allowedEntries) {
+ String pattern;
+ if (entry.getRepository().getName().isEmpty()) {
+ // String.matches has ^$ implicilty, so an empty pattern matches exactly the empty
+ // string.
+ pattern = "";
+ } else {
+ // Surrounding .* because String.matches has implicit "^$" anchor.
+ // Surrounding \b so it doesn't match a substring of the intended repo name.
+ // Quote the name so dots in the repo name don't get treated as part of the pattern
+ pattern = ".*\\b" + Pattern.quote(entry.getRepository().getName()) + "\\b.*";
+ }
+ if (label.getRepository().getName().matches(pattern)
+ && label.getPackageFragment().startsWith(entry.getPackageFragment())) {
return true;
}
}
@@ -62,7 +75,7 @@
public String getErrorFromAttemptingAccess(String name) {
return name
+ " may only be used from one of the following repositories or prefixes: "
- + allowedPrefixes.stream()
+ + allowedEntries.stream()
.map(PackageIdentifier::toString)
.collect(Collectors.joining(", "));
}
diff --git a/src/test/java/com/google/devtools/build/lib/starlarkbuildapi/core/ContextGuardedValueTest.java b/src/test/java/com/google/devtools/build/lib/starlarkbuildapi/core/ContextGuardedValueTest.java
new file mode 100644
index 0000000..5727b3d
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/starlarkbuildapi/core/ContextGuardedValueTest.java
@@ -0,0 +1,133 @@
+// Copyright 2022 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.starlarkbuildapi.core;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.Arrays.stream;
+
+import com.google.devtools.build.lib.cmdline.BazelCompileContext;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
+import com.google.devtools.build.lib.cmdline.PackageIdentifier;
+import net.starlark.java.eval.GuardedValue;
+import net.starlark.java.eval.StarlarkSemantics;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ContextGuardedValueTest {
+
+ /**
+ * We want to make sure the empty string doesn't result in "allow everything". That would be bad.
+ * Most allowlists have an entry like ("", "tools/build_defs/lang") for usage within Google.
+ */
+ @Test
+ public void emptyRepoAllowed_doesntMatchNonAllowed() throws Exception {
+ assertNotAllowed("@mylang//bar:baz", "@//tools/lang");
+ }
+
+ @Test
+ public void emptyRepoAllowed_matchesAllowed() throws Exception {
+ assertAllowed("@//tools/lang", "@//tools/lang");
+ }
+
+ @Test
+ public void workspaceRepo_matchesAllowedRepo() throws Exception {
+ assertAllowed("@rules_foo//tools/lang", "@rules_foo//");
+ }
+
+ @Test
+ public void workspaceRepo_doesntMatchCommonSubstr() throws Exception {
+ assertNotAllowed("@my_rules_foo_helper//tools/lang", "@rules_foo//");
+ }
+
+ @Test
+ public void bzlmodRepo_matchesStart() throws Exception {
+ assertAllowed("@rules_foo~override//tools/lang", "@rules_foo//");
+ assertAllowed("@rules_foo~1.2.3//tools/lang", "@rules_foo//");
+ }
+
+ @Test
+ public void bzlmodRepo_matchesWithin() throws Exception {
+ assertAllowed("@rules_lang~override~ext~foo_helper//tools/lang", "@foo_helper//");
+ }
+
+ @Test
+ public void bzlmodRepo_doesntMatchCommonSubstr() throws Exception {
+ assertNotAllowed("@rules_lang~override~ext~my_foo_helper_lib//tools/lang", "@foo_helper//");
+ }
+
+ @Test
+ public void reposWithDotsDontMatch() throws Exception {
+ assertNotAllowed("@my.lang//foo", "@my_lang//");
+ }
+
+ @Test
+ public void verifySomeRealisticCases() throws Exception {
+ // Python with workspace
+ assertAllowed("@//tools/build_defs/python/private", "@//tools/build_defs/python");
+ assertAllowed("@rules_python//python/private", "@rules_python//");
+
+ // Python with bzlmod
+ assertAllowed(
+ "@rules_python~override~internal_deps~rules_python_internal//private", "@rules_python//");
+
+ // CC with workspace
+ assertAllowed("@//tools/build_defs/cc", "@//tools/build_defs/cc");
+ assertNotAllowed("@rules_cc_helper//tools/build_defs/cc", "@rules_cc//");
+
+ // CC with Bzlmod
+ assertAllowed("@rules_cc~1.2.3~ext_name~local_cc_config//foo", "@local_cc_config//");
+ }
+
+ private Object createClientData(String callerLabelStr) {
+ return BazelCompileContext.create(
+ Label.parseCanonicalUnchecked(callerLabelStr), "unused_caller.bzl");
+ }
+
+ private GuardedValue createGuard(Object clientData, String... allowedLabelStrs) throws Exception {
+ var allowed =
+ stream(allowedLabelStrs)
+ .map(
+ labelStr -> {
+ try {
+ return PackageIdentifier.parse(labelStr);
+ } catch (LabelSyntaxException e) {
+ // We have to manually catch and re-throw this, otherwise Java is unhappy.
+ throw new RuntimeException(e);
+ }
+ })
+ .collect(toImmutableSet());
+
+ return ContextGuardedValue.onlyInAllowedRepos(clientData, allowed);
+ }
+
+ private void assertAllowed(String callerLabelStr, String... allowedLabelStrs) throws Exception {
+ var clientData = createClientData(callerLabelStr);
+ var guard = createGuard(clientData, allowedLabelStrs);
+ assertThat(guard.isObjectAccessibleUsingSemantics(StarlarkSemantics.DEFAULT, clientData))
+ .isTrue();
+ }
+
+ private void assertNotAllowed(String callerLabelStr, String... allowedLabelStrs)
+ throws Exception {
+ var clientData = createClientData(callerLabelStr);
+ var guard = createGuard(clientData, allowedLabelStrs);
+ assertThat(guard.isObjectAccessibleUsingSemantics(StarlarkSemantics.DEFAULT, clientData))
+ .isFalse();
+ }
+}