Repository: bazelbuild/bazel
Issue: Implement "Locating runfiles with Bzlmod" proposal
Labels: type: feature request
P2
team-ExternalDeps
area-Bzlmod

The ["Locating runfiles with Bzlmod" proposal](https://github.com/bazelbuild/proposals/blob/main/designs/2022-07-21-locating-runfiles-with-bzlmod.md) consists of the following individual steps:

* [x] ~Add the `RunfilesLibraryInfo` provider (#16125)~
* [x] Collect repository names and mappings of the transitive closure of a target (#16278)
* [x] Emit a repository mapping manifest for every executable (#16321 and https://github.com/bazelbuild/bazel/pull/16652)
* [x] Revert `RunfilesLibraryInfo`
* [x] Expose the current repository name in rules
  * [x] Bash (#16693)
  * [x] C++ (#16216)
  * [x] Java (#16534)
  * [x] Python (#16341)
* [x] Parse and use the repository mapping manifest in runfiles libraries (https://github.com/bazelbuild/bazel/issues/15029)
  * [x] Bash (#16693)
  * [x] C++ (#16623 and https://github.com/bazelbuild/bazel/pull/16701)
  * [x] Java (#16549)
  * [X] ~Python~ has been updated in rules_python
* [x] Use the Java runfiles library to make Stardoc work with repository mappings (https://github.com/bazelbuild/bazel/issues/14140)

Status of runfiles libraries in third-party rulesets:

* [x] rules_go (https://github.com/bazelbuild/rules_go/pull/3347)
* [ ] [rules_rust](https://github.com/bazelbuild/rules_rust/blob/main/tools/runfiles/runfiles.rs) (???)
* [ ] [rules_haskell](https://github.com/tweag/rules_haskell/blob/master/tools/runfiles/README.md) (???)

Issue comments:
@Wyverald Could you assign me and add the appropriate labels?
~~@comius Do you expect `java_binary` to be starlarkified in time for Bazel 6? That would simplify the changes to the Java rules.~~ Most likely no longer necessary, as the changes will apply to `buildjar` itself.
@Wyverald Oops, I missed the use of `Closes ...` in the PR description - we should use `Work towards ...` instead. Could you reopen? For some I lack the permission to do so.
It's the "To fix #NNN" line... GitHub trying to be clever here
The most likely future seems to be that we'll standardize on Python imports _not_ containing the repository name, in which case bzlmod is a no-op.

It's not an optimal solution because it requires an entry in `$PYTHONPATH` for each repository in the transitive closure of the Python binary, but it's the status quo and I'd much prefer thinking up a solution to that (e.g. binary-specific symlink trees that do not look like runfiles trees, dunno) than to standardize on having the repository names in imports because the concept of "repository" is specific to Bazel.

Put differently: the only way for bzlmod to cause trouble for Python is if we decide to require repository names in imports, which seems to be unlikely and it looks like the decision on that will take a while and I prefer not blocking either Bazel 6.0 or the removal of bzlmod from the experimental domain to avoid an unlikely failure mode. And should that happen, we can always paper over it with some sort of import hook.

/cc @rickeylev 
@oquenchil I'm thinking about whether we should use a macro to pass `BAZEL_CURRENT_REPOSITORY` to the `Rlocation` function automatically (e.g. do `#define Rlocation(path) RlocationReal(path, BAZEL_CURRENT_REPOSITORY)`). Do you think that would be too intrusive/magical?
Would users have to write that line or is it part of a file that is automatically generated?

I don't mind adding whatever will make it easier but if it gets too magical it should maybe be wrapped so that the details aren't visible to the user unless they want to look.
> Would users have to write that line or is it part of a file that is automatically generated?
> 
> I don't mind adding whatever will make it easier but if it gets too magical it should maybe be wrapped so that the details aren't visible to the user unless they want to look.

@oquenchil I would want to make that magic part of the runfiles library header so that users don't have to write it themselves.

What I am worried about is having an `Rlocation` macro mess with people's existing codebase since there doesn't seem to be a way to put it in a namespace or restrict it to member functions only.
Aren't we in control of the name of the macro? Can we rename it to something that would be very unlikely to clash with anything?
> Aren't we in control of the name of the macro? Can we rename it to something that would be very unlikely to clash with anything?

We can call it something else, we just won't be able to have existing C++ codebases use the new repo mapping-aware lookup automatically if we do so. But of course having to replace all `runfiles::create()` calls with something else won't be too bad.
If the macro name is Rlocation and that may start clashing with people's code in C++ files then I think it would be prudent to rename it if we decide to go with the macro.

I guess the call to make here is how much of a breaking change this is compared to the benefit of passing BAZEL_CURRENT_REPOSITORY automatically for users. I would say that the benefit is not too great but as far as I understood the use of Bazel runfiles in C++ code wasn't too wide-spread either?

>  But of course having to replace all runfiles::create() calls with something else won't be too bad.

How do you know? Is it based on search results from GH? If a release note would be enough for it not to be too disruptive then let's rename it.
I have experimented a bit more with APIs while working on the corresponding change for Java (https://github.com/bazelbuild/bazel/pull/16549).

What I now like more than the macro approach is to have users (optionally) pass in a canonical repository name when creating the `Runfiles` instance and default to that repo in `Rlocation` calls, while providing ways to override the default with an overload of `Rlocation` that accepts a repository name as a second parameter. In this way, users only have to pass in `BAZEL_CURRENT_REPOSITORY` only once in the common case, which should render a macro unnecessary.
Update: All changes relevant for Bazel 6.0.0 are now in review.
This is mostly done now, with only the bugfix https://github.com/bazelbuild/bazel/pull/16755 and a number of cherry-picks to 6.0.0 missing.
@Wyverald You can remove this from the "6.0.0 nice to have" milestone as the only remaining issue is not tied to a Bazel release (Stardoc).
Downgrading because the most important points have been addressed; the only outstanding issue is Stardoc, which is undergoing a rewrite currently by @tetromino.

Pull request review comments:

Patch that resolved the issue:
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java
index b382b1f39f7a36..313d1b07383d88 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java
@@ -371,9 +371,18 @@ public final void initializeJavacOpts() {
 
   /** Computes javacopts for the current rule. */
   private ImmutableList<String> computeJavacOpts(Collection<String> extraRuleJavacOpts) {
-    return ImmutableList.<String>builder()
-        .addAll(javaToolchain.getJavacOptions(ruleContext))
-        .addAll(extraRuleJavacOpts)
+    ImmutableList.Builder<String> javacOpts =
+        ImmutableList.<String>builder()
+            .addAll(javaToolchain.getJavacOptions(ruleContext))
+            .addAll(extraRuleJavacOpts);
+    if (activePlugins
+        .plugins()
+        .processorClasses()
+        .toSet()
+        .contains("com.google.devtools.build.runfiles.AutoBazelRepositoryProcessor")) {
+      javacOpts.add("-Abazel.repository=" + ruleContext.getRepository().getName());
+    }
+    return javacOpts
         .addAll(computePerPackageJavacOpts(ruleContext, javaToolchain))
         .addAll(addModuleJavacopts(ruleContext))
         .addAll(ruleContext.getExpander().withDataLocations().tokenized("javacopts"))
@@ -538,8 +547,8 @@ public JavaTargetAttributes.Builder initCommon() {
   public JavaTargetAttributes.Builder initCommon(
       Collection<Artifact> extraSrcs, Iterable<String> extraJavacOpts) {
     Preconditions.checkState(javacOpts == null);
-    javacOpts = computeJavacOpts(ImmutableList.copyOf(extraJavacOpts));
     activePlugins = collectPlugins();
+    javacOpts = computeJavacOpts(ImmutableList.copyOf(extraJavacOpts));
 
     JavaTargetAttributes.Builder javaTargetAttributes = new JavaTargetAttributes.Builder(semantics);
     javaCompilationHelper =
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaInfoBuildHelper.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaInfoBuildHelper.java
index 2a9d98e3b706d0..c732329beb2ce3 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaInfoBuildHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaInfoBuildHelper.java
@@ -284,6 +284,28 @@ public JavaInfo createJavaCompileAction(
 
     JavaToolchainProvider toolchainProvider = javaToolchain;
 
+    JavaPluginInfo pluginInfo = mergeExportedJavaPluginInfo(plugins, deps);
+    ImmutableList.Builder<String> allJavacOptsBuilder =
+        ImmutableList.<String>builder()
+            .addAll(toolchainProvider.getJavacOptions(starlarkRuleContext.getRuleContext()))
+            .addAll(
+                javaSemantics.getCompatibleJavacOptions(
+                    starlarkRuleContext.getRuleContext(), toolchainProvider));
+    if (pluginInfo
+        .plugins()
+        .processorClasses()
+        .toSet()
+        .contains("com.google.devtools.build.runfiles.AutoBazelRepositoryProcessor")) {
+      allJavacOptsBuilder.add(
+          "-Abazel.repository=" + starlarkRuleContext.getRuleContext().getRepository().getName());
+    }
+    allJavacOptsBuilder
+        .addAll(
+            JavaCommon.computePerPackageJavacOpts(
+                starlarkRuleContext.getRuleContext(), toolchainProvider))
+        .addAll(JavaModuleFlagsProvider.toFlags(addExports, addOpens))
+        .addAll(tokenize(javacOpts));
+
     JavaLibraryHelper helper =
         new JavaLibraryHelper(starlarkRuleContext.getRuleContext())
             .setOutput(outputJar)
@@ -295,18 +317,7 @@ public JavaInfo createJavaCompileAction(
             .setSourcePathEntries(sourcepathEntries)
             .addAdditionalOutputs(annotationProcessorAdditionalOutputs)
             .enableJspecify(enableJSpecify)
-            .setJavacOpts(
-                ImmutableList.<String>builder()
-                    .addAll(toolchainProvider.getJavacOptions(starlarkRuleContext.getRuleContext()))
-                    .addAll(
-                        javaSemantics.getCompatibleJavacOptions(
-                            starlarkRuleContext.getRuleContext(), toolchainProvider))
-                    .addAll(
-                        JavaCommon.computePerPackageJavacOpts(
-                            starlarkRuleContext.getRuleContext(), toolchainProvider))
-                    .addAll(JavaModuleFlagsProvider.toFlags(addExports, addOpens))
-                    .addAll(tokenize(javacOpts))
-                    .build());
+            .setJavacOpts(allJavacOptsBuilder.build());
 
     if (injectingRuleKind != Starlark.NONE) {
       helper.setInjectingRuleKind((String) injectingRuleKind);
@@ -316,7 +327,6 @@ public JavaInfo createJavaCompileAction(
     streamProviders(deps, JavaCompilationArgsProvider.class).forEach(helper::addDep);
     streamProviders(exports, JavaCompilationArgsProvider.class).forEach(helper::addExport);
     helper.setCompilationStrictDepsMode(getStrictDepsMode(Ascii.toUpperCase(strictDepsMode)));
-    JavaPluginInfo pluginInfo = mergeExportedJavaPluginInfo(plugins, deps);
     // Optimization: skip this if there are no annotation processors, to avoid unnecessarily
     // disabling the direct classpath optimization if `enable_annotation_processor = False`
     // but there aren't any annotation processors.
diff --git a/tools/java/runfiles/AutoBazelRepository.java b/tools/java/runfiles/AutoBazelRepository.java
new file mode 100644
index 00000000000000..6dc533001c299e
--- /dev/null
+++ b/tools/java/runfiles/AutoBazelRepository.java
@@ -0,0 +1,29 @@
+// 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.runfiles;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotating a class {@code Fooer} with this annotation generates a class {@code
+ * AutoBazelRepository_Fooer} defining a {@link String} constant {@code NAME} containing the
+ * canonical name of the repository containing the Bazel target that compiled the annotated class.
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.TYPE)
+public @interface AutoBazelRepository {}
diff --git a/tools/java/runfiles/AutoBazelRepositoryProcessor.java b/tools/java/runfiles/AutoBazelRepositoryProcessor.java
new file mode 100644
index 00000000000000..ae356d800f6d03
--- /dev/null
+++ b/tools/java/runfiles/AutoBazelRepositoryProcessor.java
@@ -0,0 +1,127 @@
+// 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.runfiles;
+
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedOptions;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.Name;
+import javax.lang.model.element.TypeElement;
+import javax.tools.Diagnostic.Kind;
+
+/** Processor for {@link AutoBazelRepository}. */
+@SupportedAnnotationTypes("com.google.devtools.build.runfiles.AutoBazelRepository")
+@SupportedOptions(AutoBazelRepositoryProcessor.BAZEL_REPOSITORY_OPTION)
+public final class AutoBazelRepositoryProcessor extends AbstractProcessor {
+
+  static final String BAZEL_REPOSITORY_OPTION = "bazel.repository";
+
+  @Override
+  public SourceVersion getSupportedSourceVersion() {
+    return SourceVersion.latestSupported();
+  }
+
+  @Override
+  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+    annotations.stream()
+        .flatMap(element -> roundEnv.getElementsAnnotatedWith(element).stream())
+        .map(element -> (TypeElement) element)
+        .forEach(this::emitClass);
+    return true;
+  }
+
+  private void emitClass(TypeElement annotatedClass) {
+    // This option is always provided by the Java rule implementations.
+    if (!processingEnv.getOptions().containsKey(BAZEL_REPOSITORY_OPTION)) {
+      processingEnv
+          .getMessager()
+          .printMessage(
+              Kind.ERROR,
+              String.format(
+                  "The %1$s annotation processor option is not set. To use this annotation"
+                      + " processor, provide the canonical repository name of the current target as"
+                      + " the value of the -A%1$s flag.",
+                  BAZEL_REPOSITORY_OPTION),
+              annotatedClass);
+      return;
+    }
+    String repositoryName = processingEnv.getOptions().get(BAZEL_REPOSITORY_OPTION);
+    if (repositoryName == null) {
+      // javac translates '-Abazel.repository=' into a null value.
+      // https://github.com/openjdk/jdk/blob/7a49c9baa1d4ad7df90e7ca626ec48ba76881822/src/jdk.compiler/share/classes/com/sun/tools/javac/processing/JavacProcessingEnvironment.java#L651
+      repositoryName = "";
+    }
+
+    // For a nested class Outer.Middle.Inner, generate a class with simple name
+    // AutoBazelRepository_Outer_Middle_Inner.
+    // Note: There can be collisions when local classes are involved, but since the definition of a
+    // class depends only on the containing Bazel target, this does not result in ambiguity.
+    List<String> nestedClassNames =
+        Stream.iterate(
+                annotatedClass,
+                element -> element instanceof TypeElement,
+                Element::getEnclosingElement)
+            .map(Element::getSimpleName)
+            .map(Name::toString)
+            .collect(toList());
+    Collections.reverse(nestedClassNames);
+    String generatedClassSimpleName =
+        Stream.concat(Stream.of("AutoBazelRepository"), nestedClassNames.stream())
+            .collect(joining("_"));
+
+    String generatedClassPackage =
+        processingEnv.getElementUtils().getPackageOf(annotatedClass).getQualifiedName().toString();
+
+    String generatedClassName =
+        generatedClassPackage.isEmpty()
+            ? generatedClassSimpleName
+            : generatedClassPackage + "." + generatedClassSimpleName;
+
+    try (PrintWriter out =
+        new PrintWriter(
+            processingEnv.getFiler().createSourceFile(generatedClassName).openWriter())) {
+      out.printf("package %s;\n", generatedClassPackage);
+      out.printf("\n");
+      out.printf("class %s {\n", generatedClassSimpleName);
+      out.printf("  /**\n");
+      out.printf("   * The canonical name of the repository containing the Bazel target that\n");
+      out.printf("   * compiled {@link %s}.\n", annotatedClass.getQualifiedName().toString());
+      out.printf("   */\n");
+      out.printf("  static final String NAME = \"%s\";\n", repositoryName);
+      out.printf("\n");
+      out.printf("  private %s() {}\n", generatedClassSimpleName);
+      out.printf("}\n");
+    } catch (IOException e) {
+      processingEnv
+          .getMessager()
+          .printMessage(
+              Kind.ERROR,
+              String.format("Failed to generate %s: %s", generatedClassName, e.getMessage()),
+              annotatedClass);
+    }
+  }
+}
diff --git a/tools/java/runfiles/BUILD b/tools/java/runfiles/BUILD
index fdd782d87db9dd..e0487a301761e0 100644
--- a/tools/java/runfiles/BUILD
+++ b/tools/java/runfiles/BUILD
@@ -26,6 +26,8 @@ filegroup(
 filegroup(
     name = "java-srcs",
     srcs = [
+        "AutoBazelRepository.java",
+        "AutoBazelRepositoryProcessor.java",
         "Runfiles.java",
         "Util.java",
     ],
@@ -33,6 +35,22 @@ filegroup(
 
 java_library(
     name = "runfiles",
-    srcs = [":java-srcs"],
+    srcs = [
+        "Runfiles.java",
+        "Util.java",
+    ],
+    exported_plugins = [":auto_bazel_repository_processor"],
     visibility = ["//tools/java/runfiles/testing:__pkg__"],
+    exports = [":auto_bazel_repository"],
+)
+
+java_library(
+    name = "auto_bazel_repository",
+    srcs = ["AutoBazelRepository.java"],
+)
+
+java_plugin(
+    name = "auto_bazel_repository_processor",
+    srcs = ["AutoBazelRepositoryProcessor.java"],
+    processor_class = "com.google.devtools.build.runfiles.AutoBazelRepositoryProcessor",
 )
diff --git a/tools/java/runfiles/BUILD.tools b/tools/java/runfiles/BUILD.tools
index a2ac4f58cd0677..70cd870d46ff5f 100644
--- a/tools/java/runfiles/BUILD.tools
+++ b/tools/java/runfiles/BUILD.tools
@@ -4,5 +4,18 @@ java_library(
         "Runfiles.java",
         "Util.java",
     ],
+    exported_plugins = [":auto_bazel_repository_processor"],
     visibility = ["//visibility:public"],
+    exports = [":auto_bazel_repository"],
+)
+
+java_library(
+    name = "auto_bazel_repository",
+    srcs = ["AutoBazelRepository.java"],
+)
+
+java_plugin(
+    name = "auto_bazel_repository_processor",
+    srcs = ["AutoBazelRepositoryProcessor.java"],
+    processor_class = "com.google.devtools.build.runfiles.AutoBazelRepositoryProcessor",
 )


Test patch:
diff --git a/src/test/shell/bazel/bazel_java_test.sh b/src/test/shell/bazel/bazel_java_test.sh
index c11137ffc2f7dd..5e55ff302f6b1b 100755
--- a/src/test/shell/bazel/bazel_java_test.sh
+++ b/src/test/shell/bazel/bazel_java_test.sh
@@ -1763,5 +1763,191 @@ EOF
   bazel build //java/main:C2 &>"${TEST_log}" || fail "Expected to build"
 }
 
+function test_auto_bazel_repository() {
+  cat >> WORKSPACE <<'EOF'
+local_repository(
+  name = "other_repo",
+  path = "other_repo",
+)
+EOF
+
+  mkdir -p pkg
+  cat > pkg/BUILD.bazel <<'EOF'
+java_library(
+  name = "library",
+  srcs = ["Library.java"],
+  deps = ["@bazel_tools//tools/java/runfiles"],
+  visibility = ["//visibility:public"],
+)
+
+java_binary(
+  name = "binary",
+  srcs = ["Binary.java"],
+  main_class = "com.example.Binary",
+  deps = [
+    ":library",
+    "@bazel_tools//tools/java/runfiles",
+  ],
+)
+
+java_test(
+  name = "test",
+  srcs = ["Test.java"],
+  main_class = "com.example.Test",
+  use_testrunner = False,
+  deps = [
+    ":library",
+    "@bazel_tools//tools/java/runfiles",
+  ],
+)
+EOF
+
+  cat > pkg/Library.java <<'EOF'
+package com.example;
+
+import com.google.devtools.build.runfiles.AutoBazelRepository;
+
+@AutoBazelRepository
+public class Library {
+  public static void printRepositoryName() {
+    System.out.printf("in pkg/Library.java: '%s'%n", AutoBazelRepository_Library.NAME);
+  }
+}
+EOF
+
+  cat > pkg/Binary.java <<'EOF'
+package com.example;
+
+import com.google.devtools.build.runfiles.AutoBazelRepository;
+
+public class Binary {
+  @AutoBazelRepository
+  private static class Class1 {
+  }
+
+  public static void main(String[] args) {
+    System.out.printf("in pkg/Binary.java: '%s'%n", AutoBazelRepository_Binary_Class1.NAME);
+    Library.printRepositoryName();
+  }
+}
+EOF
+
+  cat > pkg/Test.java <<'EOF'
+package com.example;
+
+import com.google.devtools.build.runfiles.AutoBazelRepository;
+
+public class Test {
+  private static class Class1 {
+    @AutoBazelRepository
+    private static class Class2 {
+    }
+  }
+
+  public static void main(String[] args) {
+    System.out.printf("in pkg/Test.java: '%s'%n", AutoBazelRepository_Test_Class1_Class2.NAME);
+    Library.printRepositoryName();
+  }
+}
+EOF
+
+  mkdir -p other_repo
+  touch other_repo/WORKSPACE
+
+  mkdir -p other_repo/pkg
+  cat > other_repo/pkg/BUILD.bazel <<'EOF'
+java_library(
+  name = "library2",
+  srcs = ["Library2.java"],
+  deps = ["@bazel_tools//tools/java/runfiles"],
+)
+
+java_binary(
+  name = "binary",
+  srcs = ["Binary.java"],
+  main_class = "com.example.Binary",
+  deps = [
+    ":library2",
+    "@//pkg:library",
+    "@bazel_tools//tools/java/runfiles",
+  ],
+)
+java_test(
+  name = "test",
+  srcs = ["Test.java"],
+  main_class = "com.example.Test",
+  use_testrunner = False,
+  deps = [
+    ":library2",
+    "@//pkg:library",
+    "@bazel_tools//tools/java/runfiles",
+  ],
+)
+EOF
+
+  cat > other_repo/pkg/Library2.java <<'EOF'
+package com.example;
+
+import com.google.devtools.build.runfiles.AutoBazelRepository;
+
+@AutoBazelRepository
+public class Library2 {
+  public static void printRepositoryName() {
+    System.out.printf("in external/other_repo/pkg/Library2.java: '%s'%n", AutoBazelRepository_Library2.NAME);
+  }
+}
+EOF
+
+  cat > other_repo/pkg/Binary.java <<'EOF'
+package com.example;
+
+import com.google.devtools.build.runfiles.AutoBazelRepository;
+import static com.example.AutoBazelRepository_Binary.NAME;
+
+@AutoBazelRepository
+public class Binary {
+  public static void main(String[] args) {
+    System.out.printf("in external/other_repo/pkg/Binary.java: '%s'%n", NAME);
+    Library2.printRepositoryName();
+    Library.printRepositoryName();
+  }
+}
+EOF
+
+  cat > other_repo/pkg/Test.java <<'EOF'
+package com.example;
+
+import com.google.devtools.build.runfiles.AutoBazelRepository;
+
+@AutoBazelRepository
+public class Test {
+  public static void main(String[] args) {
+    System.out.printf("in external/other_repo/pkg/Test.java: '%s'%n", AutoBazelRepository_Test.NAME);
+    Library2.printRepositoryName();
+    Library.printRepositoryName();
+  }
+}
+EOF
+
+  bazel run //pkg:binary &>"$TEST_log" || fail "Run should succeed"
+  expect_log "in pkg/Binary.java: ''"
+  expect_log "in pkg/Library.java: ''"
+
+  bazel test --test_output=streamed //pkg:test &>"$TEST_log" || fail "Test should succeed"
+  expect_log "in pkg/Test.java: ''"
+  expect_log "in pkg/Library.java: ''"
+
+  bazel run @other_repo//pkg:binary &>"$TEST_log" || fail "Run should succeed"
+  expect_log "in external/other_repo/pkg/Binary.java: 'other_repo'"
+  expect_log "in external/other_repo/pkg/Library2.java: 'other_repo'"
+  expect_log "in pkg/Library.java: ''"
+
+  bazel test --test_output=streamed \
+    @other_repo//pkg:test &>"$TEST_log" || fail "Test should succeed"
+  expect_log "in external/other_repo/pkg/Test.java: 'other_repo'"
+  expect_log "in external/other_repo/pkg/Library2.java: 'other_repo'"
+  expect_log "in pkg/Library.java: ''"
+}
+
 
 run_suite "Java integration tests"