diff --git a/java/runfiles/src/main/java/com/google/devtools/build/runfiles/Runfiles.java b/java/runfiles/src/main/java/com/google/devtools/build/runfiles/Runfiles.java
index bec2091a..681f2479 100644
--- a/java/runfiles/src/main/java/com/google/devtools/build/runfiles/Runfiles.java
+++ b/java/runfiles/src/main/java/com/google/devtools/build/runfiles/Runfiles.java
@@ -24,8 +24,9 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import java.util.NavigableMap;
import java.util.Objects;
-import java.util.stream.Collectors;
+import java.util.TreeMap;
/**
* Runfiles lookup library for Bazel-built Java binaries and tests.
@@ -45,7 +46,7 @@
*
2. Import the runfiles library.
*
*
- * import com.google.devtools.build.runfiles.Runfiles;
+ * import Runfiles;
*
*
* 3. Create a {@link Runfiles.Preloaded} object:
@@ -59,11 +60,11 @@
*
4. To look up a runfile, use either of the following approaches:
*
*
4a. Annotate the class from which runfiles should be looked up with {@link
- * AutoBazelRepository} and obtain the name of the Bazel repository containing the class from a
- * constant generated by this annotation:
+ * AutoBazelRepository} and obtain the name of the Bazel
+ * repository containing the class from a constant generated by this annotation:
*
*
- * import com.google.devtools.build.runfiles.AutoBazelRepository;
+ * import AutoBazelRepository;
* @AutoBazelRepository
* public class MyClass {
* public void myFunction() {
@@ -95,7 +96,7 @@
*
*
*
- * For more details on why it is required to pass in the current repository name, see {@see
+ * For more details on why it is required to pass in the current repository name, see {@see
* https://bazel.build/build/bzlmod#repository-names}.
*
*
Subprocesses
@@ -204,7 +205,7 @@ public final Runfiles unmapped() {
protected abstract String rlocationChecked(String path);
- protected abstract Map getRepoMapping();
+ protected abstract RepositoryMapping getRepoMapping();
// Private constructor, so only nested classes may extend it.
private Preloaded() {}
@@ -406,47 +407,19 @@ private static String getRunfilesDir(Map env) throws IOException
return value;
}
- private static Map loadRepositoryMapping(String path)
- throws IOException {
- if (path == null || !new File(path).exists()) {
- return Collections.emptyMap();
- }
-
- try (BufferedReader r =
- new BufferedReader(
- new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8))) {
- return Collections.unmodifiableMap(
- r.lines()
- .filter(line -> !line.isEmpty())
- .map(
- line -> {
- String[] split = line.split(",");
- if (split.length != 3) {
- throw new IllegalArgumentException(
- "Invalid line in repository mapping: '" + line + "'");
- }
- return split;
- })
- .collect(
- Collectors.toMap(
- split -> new Preloaded.RepoMappingKey(split[0], split[1]),
- split -> split[2])));
- }
- }
-
/** {@link Runfiles} implementation that parses a runfiles-manifest file to look up runfiles. */
private static final class ManifestBased extends Preloaded {
private final Map runfiles;
private final String manifestPath;
- private final Map repoMapping;
+ private final RepositoryMapping repoMapping;
ManifestBased(String manifestPath) throws IOException {
Util.checkArgument(manifestPath != null);
Util.checkArgument(!manifestPath.isEmpty());
this.manifestPath = manifestPath;
this.runfiles = loadRunfiles(manifestPath);
- this.repoMapping = loadRepositoryMapping(rlocationChecked("_repo_mapping"));
+ this.repoMapping = RepositoryMapping.readFromFile(rlocationChecked("_repo_mapping"));
}
@Override
@@ -481,7 +454,7 @@ protected Map getEnvVars() {
}
@Override
- protected Map getRepoMapping() {
+ protected RepositoryMapping getRepoMapping() {
return repoMapping;
}
@@ -543,13 +516,13 @@ private static String findRunfilesDir(String manifest) {
private static final class DirectoryBased extends Preloaded {
private final String runfilesRoot;
- private final Map repoMapping;
+ private final RepositoryMapping repoMapping;
DirectoryBased(String runfilesDir) throws IOException {
Util.checkArgument(!Util.isNullOrEmpty(runfilesDir));
Util.checkArgument(new File(runfilesDir).isDirectory());
this.runfilesRoot = runfilesDir;
- this.repoMapping = loadRepositoryMapping(rlocationChecked("_repo_mapping"));
+ this.repoMapping = RepositoryMapping.readFromFile(rlocationChecked("_repo_mapping"));
}
@Override
@@ -558,7 +531,7 @@ protected String rlocationChecked(String path) {
}
@Override
- protected Map getRepoMapping() {
+ protected RepositoryMapping getRepoMapping() {
return repoMapping;
}
@@ -579,4 +552,62 @@ static Preloaded createManifestBasedForTesting(String manifestPath) throws IOExc
static Preloaded createDirectoryBasedForTesting(String runfilesDir) throws IOException {
return new DirectoryBased(runfilesDir);
}
+
+ static final class RepositoryMapping {
+ private final Map exactMappings;
+ private final NavigableMap> wildcardMappings;
+
+ private RepositoryMapping(
+ Map exactMappings,
+ NavigableMap> wildcardMappings) {
+ this.exactMappings = exactMappings;
+ this.wildcardMappings = wildcardMappings;
+ }
+
+ static RepositoryMapping readFromFile(String path) throws IOException {
+ if (path == null || !new File(path).exists()) {
+ return new RepositoryMapping(Collections.emptyMap(), Collections.emptyNavigableMap());
+ }
+
+ try (BufferedReader r =
+ new BufferedReader(
+ new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8))) {
+ Map exactMappings = new HashMap<>();
+ NavigableMap> wildcardMappings = new TreeMap<>();
+ String line;
+ while ((line = r.readLine()) != null) {
+ if (line.isEmpty()) {
+ continue;
+ }
+ String[] split = line.split(",");
+ if (split.length != 3) {
+ throw new IllegalArgumentException(
+ "Invalid line in repository mapping: '" + line + "'");
+ }
+ if (split[0].endsWith("*")) {
+ Map targetMap =
+ wildcardMappings.computeIfAbsent(
+ split[0].substring(0, split[0].length() - 1), k -> new HashMap<>());
+ targetMap.put(split[1], split[2]);
+ } else {
+ exactMappings.put(new Preloaded.RepoMappingKey(split[0], split[1]), split[2]);
+ }
+ }
+ return new RepositoryMapping(exactMappings, wildcardMappings);
+ }
+ }
+
+ String getOrDefault(Preloaded.RepoMappingKey key, String defaultValue) {
+ String exactMatch = exactMappings.get(key);
+ if (exactMatch != null) {
+ return exactMatch;
+ }
+ Map.Entry> floorEntry =
+ wildcardMappings.floorEntry(key.sourceRepo);
+ if (floorEntry == null || !key.sourceRepo.startsWith(floorEntry.getKey())) {
+ return defaultValue;
+ }
+ return floorEntry.getValue().getOrDefault(key.targetRepoApparentName, defaultValue);
+ }
+ }
}
diff --git a/test/java/runfiles/src/test/java/com/google/devtools/build/runfiles/RunfilesTest.java b/test/java/runfiles/src/test/java/com/google/devtools/build/runfiles/RunfilesTest.java
index 035a0b52..406ebe8f 100644
--- a/test/java/runfiles/src/test/java/com/google/devtools/build/runfiles/RunfilesTest.java
+++ b/test/java/runfiles/src/test/java/com/google/devtools/build/runfiles/RunfilesTest.java
@@ -427,6 +427,40 @@ public void testManifestBasedRlocationWithRepoMapping_fromOtherRepo() throws Exc
assertThat(r.rlocation("protobuf")).isNull();
}
+ @Test
+ public void testManifestBasedRlocationWithRepoMapping_fromExtensionRepo() throws Exception {
+ Path rm =
+ tempFile(
+ "foo.repo_mapping",
+ ImmutableList.of(
+ ",config.json,config.json+1.2.3",
+ ",my_module,_main",
+ ",my_protobuf,protobuf+3.19.2",
+ ",my_workspace,_main",
+ "my_module++ext+*,my_module,my_module+",
+ "my_module++ext+*,repo1,my_module++ext+repo1"));
+ Path mf =
+ tempFile(
+ "foo.runfiles/MANIFEST",
+ ImmutableList.of(
+ "_repo_mapping " + rm,
+ "config.json /etc/config.json",
+ "protobuf+3.19.2/foo/runfile C:/Actual Path\\protobuf\\runfile",
+ "_main/bar/runfile /the/path/./to/other//other runfile.txt",
+ "protobuf+3.19.2/bar/dir E:\\Actual Path\\Directory",
+ "my_module+/foo/runfile /the/path/to/my_module+/runfile",
+ "my_module++ext+repo1/foo/runfile /the/path/to/my_module++ext+repo1/runfile",
+ "repo2+/foo/runfile /the/path/to/repo2+/runfile"));
+ Runfiles r =
+ Runfiles.createManifestBasedForTesting(mf.toString())
+ .withSourceRepository("my_module++ext+repo1");
+
+ assertThat(r.rlocation("my_module/foo/runfile")).isEqualTo("/the/path/to/my_module+/runfile");
+ assertThat(r.rlocation("repo1/foo/runfile"))
+ .isEqualTo("/the/path/to/my_module++ext+repo1/runfile");
+ assertThat(r.rlocation("repo2+/foo/runfile")).isEqualTo("/the/path/to/repo2+/runfile");
+ }
+
@Test
public void testDirectoryBasedRlocationWithRepoMapping_fromMain() throws Exception {
Path dir = tempDir.newFolder("foo.runfiles").toPath();
@@ -549,6 +583,28 @@ public void testDirectoryBasedRlocationWithRepoMapping_fromOtherRepo() throws Ex
assertThat(r.rlocation("config.json")).isEqualTo(dir + "/config.json");
}
+ @Test
+ public void testDirectoryBasedRlocationWithRepoMapping_fromExtensionRepo() throws Exception {
+ Path dir = tempDir.newFolder("foo.runfiles").toPath();
+ Path unused =
+ tempFile(
+ dir.resolve("_repo_mapping").toString(),
+ ImmutableList.of(
+ ",config.json,config.json+1.2.3",
+ ",my_module,_main",
+ ",my_protobuf,protobuf+3.19.2",
+ ",my_workspace,_main",
+ "my_module++ext+*,my_module,my_module+",
+ "my_module++ext+*,repo1,my_module++ext+repo1"));
+ Runfiles r =
+ Runfiles.createDirectoryBasedForTesting(dir.toString())
+ .withSourceRepository("my_module++ext+repo1");
+
+ assertThat(r.rlocation("my_module/foo")).isEqualTo(dir + "/my_module+/foo");
+ assertThat(r.rlocation("repo1/foo")).isEqualTo(dir + "/my_module++ext+repo1/foo");
+ assertThat(r.rlocation("repo2+/foo")).isEqualTo(dir + "/repo2+/foo");
+ }
+
@Test
public void testDirectoryBasedCtorArgumentValidation() throws IOException {
assertThrows(
@@ -581,7 +637,8 @@ public void testManifestBasedCtorArgumentValidation() throws Exception {
() -> Runfiles.createManifestBasedForTesting("").withSourceRepository(""));
Path mf = tempFile("foobar", ImmutableList.of("a b"));
- Runfiles unused = Runfiles.createManifestBasedForTesting(mf.toString()).withSourceRepository("");
+ Runfiles unused =
+ Runfiles.createManifestBasedForTesting(mf.toString()).withSourceRepository("");
}
@Test