Repository: bazelbuild/bazel
Issue: Get the name of current repository from macro
Labels: type: feature request
P3
team-ExternalDeps

### Description of the problem / feature request:
I am trying to get the name of the current repository from a macro.
```
.
├── a
│   ├── BUILD.bazel
│   ├── WORKSPACE
│   ├── empty.txt
│   └── txt_files.bzl
└── b
    ├── BUILD.bazel
    ├── WORKSPACE
    └── empty2.txt
```
_(see full example [here](https://github.com/or-shachar/bazel_repository_name_issue) )_

I want that if repo `b` will call the macro in `@a//:txt_files.bzl` then somehow I'd be able to retrieve that it was called from repo `b`.

Right now (not sure if it's a bug or work as intended) - calling `native.repository_name()` will return `@` and not the name of repo as declared in its `WORKSPACE` file.

### Feature requests: what underlying problem are you trying to solve with this feature?

I want to change the behavior of a macro based on the callee repository.

Can we allow `native.repository_name()` to return the explicit name of the repository and not `@`?  To maintain backward compatibility - I assume it would be better if we add a flag to this method that will be turned off by default.




Issue comments:
As a workaround I wrote the following repository rule:
```python
def _define_repository_name_impl(repository_ctx):
    repository_ctx.file("BUILD.bazel",
        """package(default_visibility = ["//visibility:public"])
        """)
    repository_ctx.file("defs.bzl",
        """main_repository_name="{}"
        """.format(repository_ctx.attr.repo_name))

_define_repository_name = repository_rule(
    implementation = _define_repository_name_impl,
    attrs={"repo_name": attr.string(mandatory=True)}
  )

def define_repository_name(repo_name):
    _define_repository_name(name = "main_repository_name",repo_name = repo_name)

```

which allows me to load the main repository name by calling:
```python
load("@main_repository_name//:defs.bzl","main_repository_name")
```

The `WORKSPACE` file looks like this:
```python
repository_name = "xxx"
workspace(name = repository_name)

load("@bazel_tooling//rules:define_repository_name.bzl","define_repository_name")
define_repository_name(repository_name)
```

I don't like it... given that `workspace()` call should be the first line of the WORKSPACE file. But it works (`bazel 2.1.0`)
hey @laurentlb ... May I ask for your opinion on this matter? :-)
@or-shachar I have the same issue, did you ever find a resolution to this?
@or-shachar @jheaff1 Could you describe in more detail what you want to use the name declared in the `WORKSPACE` file for in a macro? Knowing more about the actual problem you are trying to solve would help me answer these questions.
You most likely want just "@" instead. With the recent fixes on repo mapping, "@workspace_name//:foo" is equivalent to "@//:foo".

(Unless you're doing exec path or runfiles shenanigans, in which case refer to @fmeum's comment.)
@fmeum I am trying to write a sh_binary target for the rules_foreign_cc repo that wraps around an executable binary (built by rules_foreign_cc) with shared library dependencies. This would allow such a binary to be used as a build tool, eg the protobuf compiler or various generators part of the Qt framework.

I would like to use the `rlocation` bash function provided here:

https://github.com/bazelbuild/bazel/blob/a76cf2f4c3aa514dffa1ddc038c196103ae4583c/tools/bash/runfiles/runfiles.bash#L68

The `rlocation` bash function requires the workspace name, which I would like to obtain programatically in a macro.

I’ve tried to use native.repository_name() but that returns `@` rather than the name of the repository.


@jheaff1 Would an analogue of `$(location //foo)` that returns the correct path for `rlocation` help you realize this? I could see that being more user-friendly than yet another "some type of name of a repo" thing.
@fmeum Absolutely that would be very useful. Ive found that using `$(rootpath` rather than `$(location` provides exactly what `$(rlocation` requires apart from the workspace name.
@lberki I have been thinking about proposing a `$(location ...)` variant that returns the correct path for Rlocation for a while. The way runfiles work with Bzlmod would almost make this a requirement for location expansion to remain useful in `args`. What do you think?
@fmeum without taking sides on the `$(location) question, it's a feature orthogonal to the one proposed in this bug.

re: this bug in particular, what is the argument *against* introducing `repository_name()`? Naively, one would think that it's a very logical extension to `package_name()`. @Wyverald maybe you can think of a counter-argument?
> this bug in particular, what is the argument against introducing repository_name()? Naively, one would think that it's a very logical extension to package_name(). @Wyverald maybe you can think of a counter-argument?

We already have `native.repository_name` and it does what it says on the tin (https://bazel.build/rules/lib/native#repository_name). @fmeum's & @jheaff1's question is more about the runfiles/execpath use case, where we sometimes need the *workspace name*, which is only accessible right now in a rule ctx object (https://bazel.build/rules/lib/ctx#workspace_name; also to note, *not* the same as [Label.workspace_name](https://bazel.build/rules/lib/Label#workspace_name)...)
@Wyverald I'm embarrassed! I specifically looked for the identifier called `repository_name` and didn't find it in the source code. My bad.
The issue with making the (global) workspace name available in Starlark code is that then the evaluation of BUILD files in external repositories would be a function of the content of the main repository, which isn't too intuitive (it's problematic enough for `ctx.workspace_name()`, but we can't help that because that's how runfiles work at the moment.
@lberki That plus the fact that having yet another `workspace_name` would be confusing made me think that being able to get Rlocation paths out of location expansion may indeed be the best solution to this issue (see also the comment by @jheaff1). Since `native.repository_name` returns the right identifier for everything *except* runfiles discovery, why not add a way for users to get the full runfiles path right away without having them stitch it together themselves?
The reason why I was confused is that for some reason, I thought `$(rlocation)` is already a thing in Bazel so I went on a wild goose chase trying to figure out what *else* you want :)

I think both adding `$(rlocation)` and the same method to File is reasonable. @Wyverald , it looks like you also agree? 
@lberki `$(rlocation)` seems reasonably easy to do, but `File.rlocation_path` is difficult: Without `--experimental_sibling_repository_layout`, that function needs to know the workspace name. I'm not sure how the API for that would look like, should we just let the user pass in `ctx`?
Not necessarily; there is `BazelStarlarkContext`, which is available to the Java implementations of Starlark functions without an extra argument and it, weirdly, already contains the repository name of the tools repository. So I could imagine plumbing the workspace name in there that way, too. Rule implementations need to know the name of the main workspace anyway, so it would probably not require too much extra plumbing.
@lberki I will give that a try. We will have to make a concession though: Using `StarlarkThread` is incompatible with `structField = True`, so this will have to be a function instead.
Today I Learned. It seems to be an artificial choice and not an inherent limitation of how Starlark works, though, and I don't see an immediate reason for this limitation. @c-parsons , do you remember why you prohibited setting `useStarlarkThread` and `structField` to true at the same time in 73e1016 ?

Pull request review comments:

Patch that resolved the issue:
diff --git a/src/main/java/com/google/devtools/build/docgen/templates/be/make-variables.vm b/src/main/java/com/google/devtools/build/docgen/templates/be/make-variables.vm
index 096a6eac0aea0d..f5ac7d1c84abcf 100644
--- a/src/main/java/com/google/devtools/build/docgen/templates/be/make-variables.vm
+++ b/src/main/java/com/google/devtools/build/docgen/templates/be/make-variables.vm
@@ -262,15 +262,15 @@
     </p>
     <p>
       Output files are staged similarly, but are also prefixed with the subpath
-      <code>bazel-out/cpu-compilation_mode/bin</code> (or for certain outputs:
-      <code>bazel-out/cpu-compilation_mode/genfiles</code>, or for the outputs
-      of host tools: <code>bazel-out/host/bin</code>). In the above example,
-      <code>//testapp:app</code> is a host tool because it appears in
+      <code>bazel-out/cpu-compilation_mode/bin</code> (or for the outputs of
+      tools: <code>bazel-out/cpu-opt-exec-hash/bin</code>). In the above example,
+      <code>//testapp:app</code> is a tool because it appears in
       <code>show_app_output</code>'s <code><a
       href="$expander.expandRef("genrule.tools")">tools</a></code> attribute.
       So its output file <code>app</code> is written to
-      <code>bazel-myproject/bazel-out/host/bin/testapp/app</code>. The exec path
-      is thus <code>bazel-out/host/bin/testapp/app</code>. This extra prefix
+      <code>bazel-myproject/bazel-out/cpu-opt-exec-hash/bin/testapp/app</code>.
+      The exec path is thus <code>
+      bazel-out/cpu-opt-exec-hash/bin/testapp/app</code>. This extra prefix
       makes it possible to build the same target for, say, two different CPUs in
       the same build without the results clobbering each other.
     </p>
@@ -284,15 +284,57 @@
 
   <li>
     <p>
-      <code>rootpath</code>: Denotes the runfiles path that a built binary can
-      use to find its dependencies at runtime.
-    </p>
+      <code>rootpath</code>: Denotes the path that a built binary can use to
+      find a dependency at runtime relative to the subdirectory of its runfiles
+      directory corresponding to the main repository.
+      <strong>Note:</strong> This only works if <a
+            href="/reference/command-line-reference#flag--enable_runfiles">
+      <code>--enable_runfiles</code></a> is enabled, which is not the case on
+      Windows by default. Use <code>rlocationpath</code> instead for
+      cross-platform support.
     <p>
-      This is the same as <code>execpath</code> but strips the output prefixes
-      described above. In the above example this means both
+      This is similar to <code>execpath</code> but strips the configuration
+      prefixes described above. In the example from above this means both
       <code>empty.source</code> and <code>app</code> use pure workspace-relative
       paths: <code>testapp/empty.source</code> and <code>testapp/app</code>.
     </p>
+    <p>
+      The <code>rootpath</code> of a file in an external repository
+      <code>repo</code> will start with <code>../repo/</code>, followed by the
+      repository-relative path.
+    </p>
+    <p>
+      This has the same "one output only" requirements as <code>execpath</code>.
+    </p>
+  </li>
+
+  <li>
+    <p>
+      <code>rlocationpath</code>: The path a built binary can pass to the <code>
+      Rlocation</code> function of a runfiles library to find a dependency at
+      runtime, either in the runfiles directory (if available) or using the
+      runfiles manifest.
+    </p>
+    <p>
+      This is similar to <code>rootpath</code> in that it does not contain
+      configuration prefixes, but differs in that it always starts with the
+      name of the repository. In the example from above this means that <code>
+      empty.source</code> and <code>app</code> result in the following
+      paths: <code>myproject/testapp/empty.source</code> and <code>
+      myproject/testapp/app</code>.
+    </p>
+    <p>
+      The <code>rlocationpath</code> of a file in an external repository
+      <code>repo</code> will start with <code>repo/</code>, followed by the
+      repository-relative path.
+    </p>
+    <p>
+      Passing this path to a binary and resolving it to a file system path using
+      the runfiles libraries is the preferred approach to find dependencies at
+      runtime. Compared to <code>rootpath</code>, it has the advantage that it
+      works on all platforms and even if the runfiles directory is not
+      available.
+    </p>
     <p>
       This has the same "one output only" requirements as <code>execpath</code>.
     </p>
@@ -303,24 +345,25 @@
     <code>rootpath</code>, depending on the attribute being expanded. This is
     legacy pre-Starlark behavior and not recommended unless you really know what
     it does for a particular rule. See <a
-    href="https://github.com/bazelbuild/bazel/issues/2475#issuecomment-339318016">#2475</a>
+          href="https://github.com/bazelbuild/bazel/issues/2475#issuecomment-339318016">#2475</a>
     for details.
   </li>
 </ul>
 
 <p>
-  <code>execpaths</code>, <code>rootpaths</code>, and <code>locations</code> are
-  the plural variations of <code>execpath</code>, <code>rootpath</code>, and
-  <code>location</code>, respectively. They support labels producing multiple
-  outputs, in which case each output is listed separated by a space. Zero-output
-  rules and malformed labels produce build errors.
+  <code>execpaths</code>, <code>rootpaths</code>, <code>rlocationpaths</code>,
+  and <code>locations</code> are the plural variations of <code>execpath</code>,
+  <code>rootpath</code>, <code>rlocationpaths</code>, and<code>location</code>,
+  respectively. They support labels producing multiple outputs, in which case
+  each output is listed separated by a space. Zero-output rules and malformed
+  labels produce build errors.
 </p>
 
 <p>
   All referenced labels must appear in the consuming target's <code>srcs</code>,
   output files, or <code>deps</code>. Otherwise the build fails. C++ targets can
   also reference labels in <code><a
-  href="$expander.expandRef("cc_binary.data")">data</a></code>.
+        href="$expander.expandRef("cc_binary.data")">data</a></code>.
 </p>
 
 <p>
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java b/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java
index d82d3de89a0f7d..ddcf5510ac174d 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java
@@ -17,6 +17,7 @@
 import static java.util.stream.Collectors.joining;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableCollection;
@@ -26,7 +27,9 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.LocationExpander.LocationFunction.PathType;
 import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.LabelConstants;
 import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
 import com.google.devtools.build.lib.cmdline.RepositoryMapping;
 import com.google.devtools.build.lib.packages.BuildType;
@@ -63,34 +66,35 @@ public final class LocationExpander {
   private static final boolean EXACTLY_ONE = false;
   private static final boolean ALLOW_MULTIPLE = true;
 
-  private static final boolean USE_LOCATION_PATHS = false;
-  private static final boolean USE_EXEC_PATHS = true;
-
   private final RuleErrorConsumer ruleErrorConsumer;
   private final ImmutableMap<String, LocationFunction> functions;
   private final RepositoryMapping repositoryMapping;
+  private final String workspaceRunfilesDirectory;
 
   @VisibleForTesting
   LocationExpander(
       RuleErrorConsumer ruleErrorConsumer,
       Map<String, LocationFunction> functions,
-      RepositoryMapping repositoryMapping) {
+      RepositoryMapping repositoryMapping,
+      String workspaceRunfilesDirectory) {
     this.ruleErrorConsumer = ruleErrorConsumer;
     this.functions = ImmutableMap.copyOf(functions);
     this.repositoryMapping = repositoryMapping;
+    this.workspaceRunfilesDirectory = workspaceRunfilesDirectory;
   }
 
   private LocationExpander(
-      RuleErrorConsumer ruleErrorConsumer,
+      RuleContext ruleContext,
       Label root,
       Supplier<Map<Label, Collection<Artifact>>> locationMap,
       boolean execPaths,
       boolean legacyExternalRunfiles,
       RepositoryMapping repositoryMapping) {
     this(
-        ruleErrorConsumer,
+        ruleContext,
         allLocationFunctions(root, locationMap, execPaths, legacyExternalRunfiles),
-        repositoryMapping);
+        repositoryMapping,
+        ruleContext.getWorkspaceName());
   }
 
   /**
@@ -204,7 +208,10 @@ private String expand(String value, ErrorReporter reporter) {
       // (2) Call appropriate function to obtain string replacement.
       String functionValue = value.substring(nextWhitespace + 1, end).trim();
       try {
-        String replacement = functions.get(fname).apply(functionValue, repositoryMapping);
+        String replacement =
+            functions
+                .get(fname)
+                .apply(functionValue, repositoryMapping, workspaceRunfilesDirectory);
         result.append(replacement);
       } catch (IllegalStateException ise) {
         reporter.report(ise.getMessage());
@@ -232,23 +239,29 @@ public String expandAttribute(String attrName, String attrValue) {
 
   @VisibleForTesting
   static final class LocationFunction {
+    enum PathType {
+      LOCATION,
+      EXEC,
+      RLOCATION,
+    }
+
     private static final int MAX_PATHS_SHOWN = 5;
 
     private final Label root;
     private final Supplier<Map<Label, Collection<Artifact>>> locationMapSupplier;
-    private final boolean execPaths;
+    private final PathType pathType;
     private final boolean legacyExternalRunfiles;
     private final boolean multiple;
 
     LocationFunction(
         Label root,
         Supplier<Map<Label, Collection<Artifact>>> locationMapSupplier,
-        boolean execPaths,
+        PathType pathType,
         boolean legacyExternalRunfiles,
         boolean multiple) {
       this.root = root;
       this.locationMapSupplier = locationMapSupplier;
-      this.execPaths = execPaths;
+      this.pathType = Preconditions.checkNotNull(pathType);
       this.legacyExternalRunfiles = legacyExternalRunfiles;
       this.multiple = multiple;
     }
@@ -259,10 +272,13 @@ static final class LocationFunction {
      * using the {@code repositoryMapping}.
      *
      * @param arg The label-like string to be expanded, e.g. ":foo" or "//foo:bar"
-     * @param repositoryMapping map of {@code RepositoryName}s defined in the main workspace
+     * @param repositoryMapping map of apparent repository names to {@code RepositoryName}s
+     * @param workspaceRunfilesDirectory name of the runfiles directory corresponding to the main
+     *     repository
      * @return The expanded value
      */
-    public String apply(String arg, RepositoryMapping repositoryMapping) {
+    public String apply(
+        String arg, RepositoryMapping repositoryMapping, String workspaceRunfilesDirectory) {
       Label label;
       try {
         label = root.getRelativeWithRemapping(arg, repositoryMapping);
@@ -271,14 +287,13 @@ public String apply(String arg, RepositoryMapping repositoryMapping) {
             String.format(
                 "invalid label in %s expression: %s", functionName(), e.getMessage()), e);
       }
-      Collection<String> paths = resolveLabel(label);
+      Set<String> paths = resolveLabel(label, workspaceRunfilesDirectory);
       return joinPaths(paths);
     }
 
-    /**
-     * Returns all target location(s) of the given label.
-     */
-    private Collection<String> resolveLabel(Label unresolved) throws IllegalStateException {
+    /** Returns all target location(s) of the given label. */
+    private Set<String> resolveLabel(Label unresolved, String workspaceRunfilesDirectory)
+        throws IllegalStateException {
       Collection<Artifact> artifacts = locationMapSupplier.get().get(unresolved);
 
       if (artifacts == null) {
@@ -288,7 +303,7 @@ private Collection<String> resolveLabel(Label unresolved) throws IllegalStateExc
                 unresolved, functionName()));
       }
 
-      Set<String> paths = getPaths(artifacts);
+      Set<String> paths = getPaths(artifacts, workspaceRunfilesDirectory);
       if (paths.isEmpty()) {
         throw new IllegalStateException(
             String.format(
@@ -313,24 +328,41 @@ private Collection<String> resolveLabel(Label unresolved) throws IllegalStateExc
      * Extracts list of all executables associated with given collection of label artifacts.
      *
      * @param artifacts to get the paths of
+     * @param workspaceRunfilesDirectory name of the runfiles directory corresponding to the main
+     *     repository
      * @return all associated executable paths
      */
-    private Set<String> getPaths(Collection<Artifact> artifacts) {
+    private Set<String> getPaths(
+        Collection<Artifact> artifacts, String workspaceRunfilesDirectory) {
       TreeSet<String> paths = Sets.newTreeSet();
       for (Artifact artifact : artifacts) {
-        PathFragment execPath =
-            execPaths
-                ? artifact.getExecPath()
-                : legacyExternalRunfiles
-                    ? artifact.getPathForLocationExpansion()
-                    : artifact.getRunfilesPath();
-        if (execPath != null) {  // omit middlemen etc
-          paths.add(execPath.getCallablePathString());
+        PathFragment path = getPath(artifact, workspaceRunfilesDirectory);
+        if (path != null) { // omit middlemen etc
+          paths.add(path.getCallablePathString());
         }
       }
       return paths;
     }
 
+    private PathFragment getPath(Artifact artifact, String workspaceRunfilesDirectory) {
+      switch (pathType) {
+        case LOCATION:
+          return legacyExternalRunfiles
+              ? artifact.getPathForLocationExpansion()
+              : artifact.getRunfilesPath();
+        case EXEC:
+          return artifact.getExecPath();
+        case RLOCATION:
+          PathFragment runfilesPath = artifact.getRunfilesPath();
+          if (runfilesPath.startsWith(LabelConstants.EXTERNAL_RUNFILES_PATH_PREFIX)) {
+            return runfilesPath.relativeTo(LabelConstants.EXTERNAL_RUNFILES_PATH_PREFIX);
+          } else {
+            return PathFragment.create(workspaceRunfilesDirectory).getRelative(runfilesPath);
+          }
+      }
+      throw new IllegalStateException("Unexpected PathType: " + pathType);
+    }
+
     private String joinPaths(Collection<String> paths) {
       return paths.stream().map(ShellEscaper::escapeString).collect(joining(" "));
     }
@@ -348,27 +380,44 @@ static ImmutableMap<String, LocationFunction> allLocationFunctions(
     return new ImmutableMap.Builder<String, LocationFunction>()
         .put(
             "location",
-            new LocationFunction(root, locationMap, execPaths, legacyExternalRunfiles, EXACTLY_ONE))
+            new LocationFunction(
+                root,
+                locationMap,
+                execPaths ? PathType.EXEC : PathType.LOCATION,
+                legacyExternalRunfiles,
+                EXACTLY_ONE))
         .put(
             "locations",
             new LocationFunction(
-                root, locationMap, execPaths, legacyExternalRunfiles, ALLOW_MULTIPLE))
+                root,
+                locationMap,
+                execPaths ? PathType.EXEC : PathType.LOCATION,
+                legacyExternalRunfiles,
+                ALLOW_MULTIPLE))
         .put(
             "rootpath",
             new LocationFunction(
-                root, locationMap, USE_LOCATION_PATHS, legacyExternalRunfiles, EXACTLY_ONE))
+                root, locationMap, PathType.LOCATION, legacyExternalRunfiles, EXACTLY_ONE))
         .put(
             "rootpaths",
             new LocationFunction(
-                root, locationMap, USE_LOCATION_PATHS, legacyExternalRunfiles, ALLOW_MULTIPLE))
+                root, locationMap, PathType.LOCATION, legacyExternalRunfiles, ALLOW_MULTIPLE))
         .put(
             "execpath",
             new LocationFunction(
-                root, locationMap, USE_EXEC_PATHS, legacyExternalRunfiles, EXACTLY_ONE))
+                root, locationMap, PathType.EXEC, legacyExternalRunfiles, EXACTLY_ONE))
         .put(
             "execpaths",
             new LocationFunction(
-                root, locationMap, USE_EXEC_PATHS, legacyExternalRunfiles, ALLOW_MULTIPLE))
+                root, locationMap, PathType.EXEC, legacyExternalRunfiles, ALLOW_MULTIPLE))
+        .put(
+            "rlocationpath",
+            new LocationFunction(
+                root, locationMap, PathType.RLOCATION, legacyExternalRunfiles, EXACTLY_ONE))
+        .put(
+            "rlocationpaths",
+            new LocationFunction(
+                root, locationMap, PathType.RLOCATION, legacyExternalRunfiles, ALLOW_MULTIPLE))
         .buildOrThrow();
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/LocationTemplateContext.java b/src/main/java/com/google/devtools/build/lib/analysis/LocationTemplateContext.java
index f3e66781c7dec3..0de4a4e498c49b 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/LocationTemplateContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/LocationTemplateContext.java
@@ -50,6 +50,7 @@ final class LocationTemplateContext implements TemplateContext {
   private final ImmutableMap<String, LocationFunction> functions;
   private final RepositoryMapping repositoryMapping;
   private final boolean windowsPath;
+  private final String workspaceRunfilesDirectory;
 
   private LocationTemplateContext(
       TemplateContext delegate,
@@ -58,12 +59,14 @@ private LocationTemplateContext(
       boolean execPaths,
       boolean legacyExternalRunfiles,
       RepositoryMapping repositoryMapping,
-      boolean windowsPath) {
+      boolean windowsPath,
+      String workspaceRunfilesDirectory) {
     this.delegate = delegate;
     this.functions =
         LocationExpander.allLocationFunctions(root, locationMap, execPaths, legacyExternalRunfiles);
     this.repositoryMapping = repositoryMapping;
     this.windowsPath = windowsPath;
+    this.workspaceRunfilesDirectory = workspaceRunfilesDirectory;
   }
 
   public LocationTemplateContext(
@@ -83,7 +86,8 @@ public LocationTemplateContext(
         execPaths,
         ruleContext.getConfiguration().legacyExternalRunfiles(),
         ruleContext.getRule().getPackage().getRepositoryMapping(),
-        windowsPath);
+        windowsPath,
+        ruleContext.getWorkspaceName());
   }
 
   @Override
@@ -108,7 +112,7 @@ private String lookupFunctionImpl(String name, String param) throws ExpansionExc
     try {
       LocationFunction f = functions.get(name);
       if (f != null) {
-        return f.apply(param, repositoryMapping);
+        return f.apply(param, repositoryMapping, workspaceRunfilesDirectory);
       }
     } catch (IllegalStateException e) {
       throw new ExpansionException(e.getMessage(), e);


Test patch:
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/LocationExpanderIntegrationTest.java b/src/test/java/com/google/devtools/build/lib/analysis/LocationExpanderIntegrationTest.java
index 4f40434ea75b74..7a31156694281c 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/LocationExpanderIntegrationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/LocationExpanderIntegrationTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.testutil.TestConstants;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.ModifiedFileSet;
 import com.google.devtools.build.lib.vfs.Root;
@@ -121,6 +122,12 @@ public void otherPathExpansion() throws Exception {
         "genrule(name='foo', outs=['foo.txt'], cmd='never executed')",
         "sh_library(name='lib', srcs=[':foo'])");
 
+    FileSystemUtils.appendIsoLatin1(scratch.resolve("WORKSPACE"), "workspace(name='workspace')");
+    // Invalidate WORKSPACE to pick up the name.
+    getSkyframeExecutor()
+        .invalidateFilesUnderPathForTesting(
+            reporter, ModifiedFileSet.EVERYTHING_MODIFIED, Root.fromPath(rootDirectory));
+
     LocationExpander expander = makeExpander("//expansion:lib");
     assertThat(expander.expand("foo $(execpath :foo) bar"))
         .matches("foo .*-out/.*/expansion/foo\\.txt bar");
@@ -130,6 +137,22 @@ public void otherPathExpansion() throws Exception {
         .matches("foo expansion/foo.txt bar");
     assertThat(expander.expand("foo $(rootpaths :foo) bar"))
         .matches("foo expansion/foo.txt bar");
+    assertThat(expander.expand("foo $(rlocationpath :foo) bar"))
+        .isEqualTo("foo workspace/expansion/foo.txt bar");
+    assertThat(expander.expand("foo $(rlocationpaths :foo) bar"))
+        .isEqualTo("foo workspace/expansion/foo.txt bar");
+    assertThat(expander.expand("foo $(rlocationpath //expansion:foo) bar"))
+        .isEqualTo("foo workspace/expansion/foo.txt bar");
+    assertThat(expander.expand("foo $(rlocationpaths //expansion:foo) bar"))
+        .isEqualTo("foo workspace/expansion/foo.txt bar");
+    assertThat(expander.expand("foo $(rlocationpath @//expansion:foo) bar"))
+        .isEqualTo("foo workspace/expansion/foo.txt bar");
+    assertThat(expander.expand("foo $(rlocationpaths @//expansion:foo) bar"))
+        .isEqualTo("foo workspace/expansion/foo.txt bar");
+    assertThat(expander.expand("foo $(rlocationpath @workspace//expansion:foo) bar"))
+        .isEqualTo("foo workspace/expansion/foo.txt bar");
+    assertThat(expander.expand("foo $(rlocationpaths @workspace//expansion:foo) bar"))
+        .isEqualTo("foo workspace/expansion/foo.txt bar");
   }
 
   @Test
@@ -158,6 +181,10 @@ public void otherPathExternalExpansion() throws Exception {
         .matches("foo external/r/p/foo.txt bar");
     assertThat(expander.expand("foo $(rootpaths @r//p:foo) bar"))
         .matches("foo external/r/p/foo.txt bar");
+    assertThat(expander.expand("foo $(rlocationpath @r//p:foo) bar"))
+        .isEqualTo("foo r/p/foo.txt bar");
+    assertThat(expander.expand("foo $(rlocationpath @r//p:foo) bar"))
+        .isEqualTo("foo r/p/foo.txt bar");
   }
 
   @Test
@@ -183,6 +210,10 @@ public void otherPathExternalExpansionNoLegacyExternalRunfiles() throws Exceptio
         .matches("foo .*-out/.*/external/r/p/foo\\.txt bar");
     assertThat(expander.expand("foo $(rootpath @r//p:foo) bar")).matches("foo ../r/p/foo.txt bar");
     assertThat(expander.expand("foo $(rootpaths @r//p:foo) bar")).matches("foo ../r/p/foo.txt bar");
+    assertThat(expander.expand("foo $(rlocationpath @r//p:foo) bar"))
+        .isEqualTo("foo r/p/foo.txt bar");
+    assertThat(expander.expand("foo $(rlocationpath @r//p:foo) bar"))
+        .isEqualTo("foo r/p/foo.txt bar");
   }
 
   @Test
@@ -210,6 +241,10 @@ public void otherPathExternalExpansionNoLegacyExternalRunfilesSiblingRepositoryL
         .matches("foo .*-out/r/.*/p/foo\\.txt bar");
     assertThat(expander.expand("foo $(rootpath @r//p:foo) bar")).matches("foo ../r/p/foo.txt bar");
     assertThat(expander.expand("foo $(rootpaths @r//p:foo) bar")).matches("foo ../r/p/foo.txt bar");
+    assertThat(expander.expand("foo $(rlocationpath @r//p:foo) bar"))
+        .isEqualTo("foo r/p/foo.txt bar");
+    assertThat(expander.expand("foo $(rlocationpaths @r//p:foo) bar"))
+        .isEqualTo("foo r/p/foo.txt bar");
   }
 
   @Test
@@ -224,5 +259,9 @@ public void otherPathMultiExpansion() throws Exception {
         .matches("foo .*-out/.*/expansion/bar\\.txt .*-out/.*/expansion/foo\\.txt bar");
     assertThat(expander.expand("foo $(rootpaths :foo) bar"))
         .matches("foo expansion/bar.txt expansion/foo.txt bar");
+    assertThat(expander.expand("foo $(rlocationpaths :foo) bar"))
+        .isEqualTo(
+            "foo __main__/expansion/bar.txt __main__/expansion/foo.txt bar"
+                .replace("__main__", TestConstants.WORKSPACE_NAME));
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/LocationExpanderTest.java b/src/test/java/com/google/devtools/build/lib/analysis/LocationExpanderTest.java
index abdeb1ed742df4..9e6b63490ef638 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/LocationExpanderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/LocationExpanderTest.java
@@ -59,22 +59,25 @@ public boolean hasErrors() {
   }
 
   private LocationExpander makeExpander(RuleErrorConsumer ruleErrorConsumer) throws Exception {
-    LocationFunction f1 = new LocationFunctionBuilder("//a", false)
-        .setExecPaths(false)
-        .add("//a", "/exec/src/a")
-        .build();
-
-    LocationFunction f2 = new LocationFunctionBuilder("//b", true)
-        .setExecPaths(false)
-        .add("//b", "/exec/src/b")
-        .build();
+    LocationFunction f1 =
+        new LocationFunctionBuilder("//a", false)
+            .setPathType(LocationFunction.PathType.LOCATION)
+            .add("//a", "/exec/src/a")
+            .build();
+
+    LocationFunction f2 =
+        new LocationFunctionBuilder("//b", true)
+            .setPathType(LocationFunction.PathType.LOCATION)
+            .add("//b", "/exec/src/b")
+            .build();
 
     return new LocationExpander(
         ruleErrorConsumer,
         ImmutableMap.<String, LocationFunction>of(
             "location", f1,
             "locations", f2),
-        RepositoryMapping.ALWAYS_FALLBACK);
+        RepositoryMapping.ALWAYS_FALLBACK,
+        "workspace");
   }
 
   private String expand(String input) throws Exception {
@@ -125,10 +128,11 @@ public void noExpansionOnError() throws Exception {
 
   @Test
   public void expansionWithRepositoryMapping() throws Exception {
-    LocationFunction f1 = new LocationFunctionBuilder("//a", false)
-        .setExecPaths(false)
-        .add("@bar//a", "/exec/src/a")
-        .build();
+    LocationFunction f1 =
+        new LocationFunctionBuilder("//a", false)
+            .setPathType(LocationFunction.PathType.LOCATION)
+            .add("@bar//a", "/exec/src/a")
+            .build();
 
     ImmutableMap<String, RepositoryName> repositoryMapping =
         ImmutableMap.of("foo", RepositoryName.create("bar"));
@@ -137,7 +141,8 @@ public void expansionWithRepositoryMapping() throws Exception {
         new LocationExpander(
             new Capture(),
             ImmutableMap.<String, LocationFunction>of("location", f1),
-            RepositoryMapping.createAllowingFallback(repositoryMapping));
+            RepositoryMapping.createAllowingFallback(repositoryMapping),
+            "workspace");
 
     String value = locationExpander.expand("$(location @foo//a)");
     assertThat(value).isEqualTo("src/a");
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/LocationFunctionTest.java b/src/test/java/com/google/devtools/build/lib/analysis/LocationFunctionTest.java
index 38addac4979d01..608b39b34fa105 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/LocationFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/LocationFunctionTest.java
@@ -49,16 +49,16 @@ public class LocationFunctionTest {
   public void absoluteAndRelativeLabels() throws Exception {
     LocationFunction func =
         new LocationFunctionBuilder("//foo", false).add("//foo", "/exec/src/bar").build();
-    assertThat(func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK)).isEqualTo("src/bar");
-    assertThat(func.apply(":foo", RepositoryMapping.ALWAYS_FALLBACK)).isEqualTo("src/bar");
-    assertThat(func.apply("foo", RepositoryMapping.ALWAYS_FALLBACK)).isEqualTo("src/bar");
+    assertThat(func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK, null)).isEqualTo("src/bar");
+    assertThat(func.apply(":foo", RepositoryMapping.ALWAYS_FALLBACK, null)).isEqualTo("src/bar");
+    assertThat(func.apply("foo", RepositoryMapping.ALWAYS_FALLBACK, null)).isEqualTo("src/bar");
   }
 
   @Test
   public void pathUnderExecRootUsesDotSlash() throws Exception {
     LocationFunction func =
         new LocationFunctionBuilder("//foo", false).add("//foo", "/exec/bar").build();
-    assertThat(func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK)).isEqualTo("./bar");
+    assertThat(func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK, null)).isEqualTo("./bar");
   }
 
   @Test
@@ -67,7 +67,7 @@ public void noSuchLabel() throws Exception {
     IllegalStateException expected =
         assertThrows(
             IllegalStateException.class,
-            () -> func.apply("//bar", RepositoryMapping.ALWAYS_FALLBACK));
+            () -> func.apply("//bar", RepositoryMapping.ALWAYS_FALLBACK, null));
     assertThat(expected)
         .hasMessageThat()
         .isEqualTo(
@@ -81,7 +81,7 @@ public void emptyList() throws Exception {
     IllegalStateException expected =
         assertThrows(
             IllegalStateException.class,
-            () -> func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK));
+            () -> func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK, null));
     assertThat(expected)
         .hasMessageThat()
         .isEqualTo("label '//foo:foo' in $(location) expression expands to no files");
@@ -94,7 +94,7 @@ public void tooMany() throws Exception {
     IllegalStateException expected =
         assertThrows(
             IllegalStateException.class,
-            () -> func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK));
+            () -> func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK, null));
     assertThat(expected)
         .hasMessageThat()
         .isEqualTo(
@@ -109,7 +109,7 @@ public void noSuchLabelMultiple() throws Exception {
     IllegalStateException expected =
         assertThrows(
             IllegalStateException.class,
-            () -> func.apply("//bar", RepositoryMapping.ALWAYS_FALLBACK));
+            () -> func.apply("//bar", RepositoryMapping.ALWAYS_FALLBACK, null));
     assertThat(expected)
         .hasMessageThat()
         .isEqualTo(
@@ -121,7 +121,7 @@ public void noSuchLabelMultiple() throws Exception {
   public void fileWithSpace() throws Exception {
     LocationFunction func =
         new LocationFunctionBuilder("//foo", false).add("//foo", "/exec/file/with space").build();
-    assertThat(func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK))
+    assertThat(func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK, null))
         .isEqualTo("'file/with space'");
   }
 
@@ -130,7 +130,7 @@ public void multipleFiles() throws Exception {
     LocationFunction func = new LocationFunctionBuilder("//foo", true)
         .add("//foo", "/exec/foo/bar", "/exec/out/foo/foobar")
         .build();
-    assertThat(func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK))
+    assertThat(func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK, null))
         .isEqualTo("foo/bar foo/foobar");
   }
 
@@ -139,27 +139,41 @@ public void filesWithSpace() throws Exception {
     LocationFunction func = new LocationFunctionBuilder("//foo", true)
         .add("//foo", "/exec/file/with space", "/exec/file/with spaces ")
         .build();
-    assertThat(func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK))
+    assertThat(func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK, null))
         .isEqualTo("'file/with space' 'file/with spaces '");
   }
 
   @Test
   public void execPath() throws Exception {
-    LocationFunction func = new LocationFunctionBuilder("//foo", true)
-        .setExecPaths(true)
-        .add("//foo", "/exec/bar", "/exec/out/foobar")
-        .build();
-    assertThat(func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK))
+    LocationFunction func =
+        new LocationFunctionBuilder("//foo", true)
+            .setPathType(LocationFunction.PathType.EXEC)
+            .add("//foo", "/exec/bar", "/exec/out/foobar")
+            .build();
+    assertThat(func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK, null))
         .isEqualTo("./bar out/foobar");
   }
 
+  @Test
+  public void rlocationPath() throws Exception {
+    LocationFunction func =
+        new LocationFunctionBuilder("//foo", true)
+            .setPathType(LocationFunction.PathType.RLOCATION)
+            .add("//foo", "/exec/bar", "/exec/out/foobar")
+            .build();
+    assertThat(func.apply("//foo", RepositoryMapping.ALWAYS_FALLBACK, "workspace"))
+        .isEqualTo("workspace/bar workspace/foobar");
+  }
+
   @Test
   public void locationFunctionWithMappingReplace() throws Exception {
     RepositoryName b = RepositoryName.create("b");
     ImmutableMap<String, RepositoryName> repositoryMapping = ImmutableMap.of("a", b);
     LocationFunction func =
         new LocationFunctionBuilder("//foo", false).add("@b//foo", "/exec/src/bar").build();
-    assertThat(func.apply("@a//foo", RepositoryMapping.createAllowingFallback(repositoryMapping)))
+    assertThat(
+            func.apply(
+                "@a//foo", RepositoryMapping.createAllowingFallback(repositoryMapping), null))
         .isEqualTo("src/bar");
   }
 
@@ -170,7 +184,8 @@ public void locationFunctionWithMappingIgnoreRepo() throws Exception {
     LocationFunction func =
         new LocationFunctionBuilder("//foo", false).add("@potato//foo", "/exec/src/bar").build();
     assertThat(
-            func.apply("@potato//foo", RepositoryMapping.createAllowingFallback(repositoryMapping)))
+            func.apply(
+                "@potato//foo", RepositoryMapping.createAllowingFallback(repositoryMapping), null))
         .isEqualTo("src/bar");
   }
 }
@@ -178,7 +193,7 @@ public void locationFunctionWithMappingIgnoreRepo() throws Exception {
 final class LocationFunctionBuilder {
   private final Label root;
   private final boolean multiple;
-  private boolean execPaths;
+  private LocationFunction.PathType pathType = LocationFunction.PathType.LOCATION;
   private boolean legacyExternalRunfiles;
   private final Map<Label, Collection<Artifact>> labelMap = new HashMap<>();
 
@@ -189,12 +204,12 @@ final class LocationFunctionBuilder {
 
   public LocationFunction build() {
     return new LocationFunction(
-        root, Suppliers.ofInstance(labelMap), execPaths, legacyExternalRunfiles, multiple);
+        root, Suppliers.ofInstance(labelMap), pathType, legacyExternalRunfiles, multiple);
   }
 
   @CanIgnoreReturnValue
-  public LocationFunctionBuilder setExecPaths(boolean execPaths) {
-    this.execPaths = execPaths;
+  public LocationFunctionBuilder setPathType(LocationFunction.PathType pathType) {
+    this.pathType = pathType;
     return this;
   }
 
diff --git a/src/test/py/bazel/runfiles_test.py b/src/test/py/bazel/runfiles_test.py
index cb37929531360e..6c2692e58b1098 100644
--- a/src/test/py/bazel/runfiles_test.py
+++ b/src/test/py/bazel/runfiles_test.py
@@ -312,6 +312,41 @@ def testLegacyExternalRunfilesOption(self):
                                    "bin/bin.runfiles_manifest")
     self.AssertFileContentNotContains(manifest_path, "__main__/external/A")
 
+  def testRunfilesLibrariesFindRlocationpathExpansion(self):
+    self.ScratchDir("A")
+    self.ScratchFile("A/WORKSPACE")
+    self.ScratchFile("A/p/BUILD", ["exports_files(['foo.txt'])"])
+    self.ScratchFile("A/p/foo.txt", ["Hello, World!"])
+    self.ScratchFile("WORKSPACE", ["local_repository(name = 'A', path='A')"])
+    self.ScratchFile("pkg/BUILD", [
+        "py_binary(",
+        "  name = 'bin',",
+        "  srcs = ['bin.py'],",
+        "  args = [",
+        "    '$(rlocationpath bar.txt)',",
+        "    '$(rlocationpath @A//p:foo.txt)',",
+        "  ],",
+        "  data = [",
+        "    'bar.txt',",
+        "    '@A//p:foo.txt'",
+        "  ],",
+        "  deps = ['@bazel_tools//tools/python/runfiles'],",
+        ")",
+    ])
+    self.ScratchFile("pkg/bar.txt", ["Hello, Bazel!"])
+    self.ScratchFile("pkg/bin.py", [
+        "import sys",
+        "from tools.python.runfiles import runfiles",
+        "r = runfiles.Create()",
+        "for arg in sys.argv[1:]:",
+        "  print(open(r.Rlocation(arg)).read().strip())",
+    ])
+    _, stdout, _ = self.RunBazel(["run", "//pkg:bin"])
+    if len(stdout) != 2:
+      self.fail("stdout: %s" % stdout)
+    self.assertEqual(stdout[0], "Hello, Bazel!")
+    self.assertEqual(stdout[1], "Hello, World!")
+
 
 if __name__ == "__main__":
   unittest.main()