Annotate BEP output files with "prefix" information sufficient to reconstruct the full output path.
This should be backwards-compatible, as we are just adding a new field to the File proto.

RELNOTES: None
PiperOrigin-RevId: 250485470
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ArtifactRoot.java b/src/main/java/com/google/devtools/build/lib/actions/ArtifactRoot.java
index ad3399f..272513e 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ArtifactRoot.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ArtifactRoot.java
@@ -14,7 +14,9 @@
 
 package com.google.devtools.build.lib.actions;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Interner;
 import com.google.common.collect.Interners;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
@@ -26,6 +28,7 @@
 import com.google.devtools.build.lib.vfs.Root;
 import java.io.Serializable;
 import java.util.Objects;
+import javax.annotation.Nullable;
 
 /**
  * A root for an artifact. The roots are the directories containing artifacts, and they are mapped
@@ -47,7 +50,6 @@
 @Immutable
 public final class ArtifactRoot implements Comparable<ArtifactRoot>, Serializable, FileRootApi {
   private static final Interner<ArtifactRoot> INTERNER = Interners.newWeakInterner();
-
   /**
    * Do not use except in tests and in {@link
    * com.google.devtools.build.lib.skyframe.SkyframeExecutor}.
@@ -59,17 +61,35 @@
   }
 
   /**
+   * Constructs an ArtifactRoot given the output prefixes. (eg, "bin"), and (eg, "testlogs")
+   * relative to the execRoot.
+   *
+   * <p>Be careful with this method - all derived roots must be registered with the artifact factory
+   * before the analysis phase.
+   */
+  public static ArtifactRoot asDerivedRoot(Path execRoot, PathFragment... prefixes) {
+    Path root = execRoot;
+    for (PathFragment prefix : prefixes) {
+      root = root.getRelative(prefix);
+    }
+    Preconditions.checkArgument(root.startsWith(execRoot));
+    Preconditions.checkArgument(!root.equals(execRoot));
+    PathFragment execPath = root.relativeTo(execRoot);
+    return INTERNER.intern(
+        new ArtifactRoot(
+            Root.fromPath(root), execPath, RootType.Output, ImmutableList.copyOf(prefixes)));
+  }
+
+  /**
    * Returns the given path as a derived root, relative to the given exec root. The root must be a
    * proper sub-directory of the exec root (i.e. not equal). Neither may be {@code null}.
    *
    * <p>Be careful with this method - all derived roots must be registered with the artifact factory
    * before the analysis phase.
    */
+  @VisibleForTesting
   public static ArtifactRoot asDerivedRoot(Path execRoot, Path root) {
-    Preconditions.checkArgument(root.startsWith(execRoot));
-    Preconditions.checkArgument(!root.equals(execRoot));
-    PathFragment execPath = root.relativeTo(execRoot);
-    return INTERNER.intern(new ArtifactRoot(Root.fromPath(root), execPath, RootType.Output));
+    return asDerivedRoot(execRoot, root.relativeTo(execRoot));
   }
 
   public static ArtifactRoot middlemanRoot(Path execRoot, Path outputDir) {
@@ -82,8 +102,9 @@
 
   @AutoCodec.VisibleForSerialization
   @AutoCodec.Instantiator
-  static ArtifactRoot createForSerialization(Root root, PathFragment execPath, RootType rootType) {
-    return INTERNER.intern(new ArtifactRoot(root, execPath, rootType));
+  static ArtifactRoot createForSerialization(
+      Root root, PathFragment execPath, RootType rootType, ImmutableList<PathFragment> components) {
+    return INTERNER.intern(new ArtifactRoot(root, execPath, rootType, components));
   }
 
   @AutoCodec.VisibleForSerialization
@@ -96,11 +117,18 @@
   private final Root root;
   private final PathFragment execPath;
   private final RootType rootType;
+  @Nullable private final ImmutableList<PathFragment> components;
 
-  private ArtifactRoot(Root root, PathFragment execPath, RootType rootType) {
+  private ArtifactRoot(
+      Root root, PathFragment execPath, RootType rootType, ImmutableList<PathFragment> components) {
     this.root = Preconditions.checkNotNull(root);
     this.execPath = execPath;
     this.rootType = rootType;
+    this.components = components;
+  }
+
+  private ArtifactRoot(Root root, PathFragment execPath, RootType rootType) {
+    this(root, execPath, rootType, /* components= */ null);
   }
 
   public Root getRoot() {
@@ -120,6 +148,9 @@
     return getExecPath().getPathString();
   }
 
+  public ImmutableList<PathFragment> getComponents() {
+    return components;
+  }
 
   public boolean isSourceRoot() {
     return rootType == RootType.Source;
@@ -136,7 +167,7 @@
 
   @Override
   public int hashCode() {
-    return Objects.hash(root, execPath, rootType);
+    return Objects.hash(root, execPath, rootType, components);
   }
 
   @Override
@@ -148,7 +179,10 @@
       return false;
     }
     ArtifactRoot r = (ArtifactRoot) o;
-    return root.equals(r.root) && execPath.equals(r.execPath) && rootType == r.rootType;
+    return root.equals(r.root)
+        && execPath.equals(r.execPath)
+        && rootType == r.rootType
+        && Objects.equals(components, r.components);
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BlazeDirectories.java b/src/main/java/com/google/devtools/build/lib/analysis/BlazeDirectories.java
index a1b4c3e..27fce36 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/BlazeDirectories.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BlazeDirectories.java
@@ -21,6 +21,7 @@
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.util.StringCanonicalizer;
 import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
 import java.util.Objects;
 
 /**
@@ -185,7 +186,8 @@
    * {@link BlazeDirectories} of this server instance. Nothing else should be placed here.
    */
   public ArtifactRoot getBuildDataDirectory(String workspaceName) {
-    return ArtifactRoot.asDerivedRoot(getExecRoot(workspaceName), getOutputPath(workspaceName));
+    return ArtifactRoot.asDerivedRoot(
+        getExecRoot(workspaceName), PathFragment.create(getRelativeOutputPath(productName)));
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/TargetCompleteEvent.java b/src/main/java/com/google/devtools/build/lib/analysis/TargetCompleteEvent.java
index fb9ebe3..028c033 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/TargetCompleteEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/TargetCompleteEvent.java
@@ -56,6 +56,7 @@
 import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
 import com.google.devtools.build.lib.syntax.Type;
 import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.skyframe.SkyValue;
 import java.util.Collection;
 import java.util.function.Function;
@@ -117,6 +118,11 @@
   private final ExecutableTargetData executableTargetData;
   private final boolean bepReportOnlyImportantArtifacts;
 
+  private static final String DISABLE_PREFIX_NAME =
+      "com.google.devtools.build.lib.analysis.TargetCompleteEvent.disable_path_prefix";
+  private static final boolean INCLUDE_PATH_PREFIX =
+      !"1".equals(System.getProperty(DISABLE_PREFIX_NAME));
+
   private TargetCompleteEvent(
       ConfiguredTargetAndData targetAndData,
       NestedSet<Cause> rootCauses,
@@ -305,11 +311,26 @@
       String name = artifactNameFunction.apply(artifact);
       String uri = converters.pathConverter().apply(pathResolver.toPath(artifact));
       if (uri != null) {
-        builder.addImportantOutput(File.newBuilder().setName(name).setUri(uri).build());
+        builder.addImportantOutput(newFileFromArtifact(name, artifact).setUri(uri).build());
       }
     }
   }
 
+  public static BuildEventStreamProtos.File.Builder newFileFromArtifact(
+      String name, Artifact artifact) {
+    File.Builder builder =
+        File.newBuilder().setName(name == null ? artifact.getRootRelativePathString() : name);
+    if (INCLUDE_PATH_PREFIX && artifact.getRoot().getComponents() != null) {
+      builder.addAllPathPrefix(
+          Iterables.transform(artifact.getRoot().getComponents(), PathFragment::getPathString));
+    }
+    return builder;
+  }
+
+  public static BuildEventStreamProtos.File.Builder newFileFromArtifact(Artifact artifact) {
+    return newFileFromArtifact(/* name= */ null, artifact);
+  }
+
   @Override
   public Collection<LocalFile> referencedLocalFiles() {
     ImmutableList.Builder<LocalFile> builder = ImmutableList.builder();
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/OutputDirectories.java b/src/main/java/com/google/devtools/build/lib/analysis/config/OutputDirectories.java
index 17d47c1..713ad72 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/OutputDirectories.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/OutputDirectories.java
@@ -104,13 +104,17 @@
       // e.g., execroot/repo1
       Path execRoot = directories.getExecRoot(mainRepositoryName.strippedName());
       // e.g., execroot/repo1/bazel-out/config/bin
-      Path outputDir =
-          execRoot.getRelative(directories.getRelativeOutputPath()).getRelative(outputDirName);
       if (middleman) {
+        Path outputDir =
+            execRoot.getRelative(directories.getRelativeOutputPath()).getRelative(outputDirName);
         return ArtifactRoot.middlemanRoot(execRoot, outputDir);
       }
       // e.g., [[execroot/repo1]/bazel-out/config/bin]
-      return ArtifactRoot.asDerivedRoot(execRoot, outputDir.getRelative(nameFragment));
+      return ArtifactRoot.asDerivedRoot(
+          execRoot,
+          PathFragment.create(directories.getRelativeOutputPath()),
+          PathFragment.create(outputDirName),
+          nameFragment);
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto b/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto
index 00d2592..79f4882 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto
@@ -384,6 +384,13 @@
 }
 
 message File {
+  // A sequence of prefixes to apply to the file name to construct a full path.
+  // In most but not all cases, there will be 3 entries:
+  //  1. A root output directory, eg "bazel-out"
+  //  2. A configuration mnemonic, eg "k8-fastbuild"
+  //  3. An output category, eg "genfiles"
+  repeated string path_prefix = 4;
+
   // identifier indicating the nature of the file (e.g., "stdout", "stderr")
   string name = 1;
 
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/NamedArtifactGroup.java b/src/main/java/com/google/devtools/build/lib/runtime/NamedArtifactGroup.java
index 2ec940d..320519a 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/NamedArtifactGroup.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/NamedArtifactGroup.java
@@ -14,6 +14,8 @@
 
 package com.google.devtools.build.lib.runtime;
 
+import static com.google.devtools.build.lib.analysis.TargetCompleteEvent.newFileFromArtifact;
+
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.actions.Artifact;
@@ -81,10 +83,9 @@
       if (artifact.isMiddlemanArtifact()) {
         continue;
       }
-      String name = artifact.getRootRelativePathString();
       String uri = pathConverter.apply(pathResolver.toPath(artifact));
       if (uri != null) {
-        builder.addFiles(BuildEventStreamProtos.File.newBuilder().setName(name).setUri(uri));
+        builder.addFiles(newFileFromArtifact(artifact).setUri(uri));
       }
     }
     for (NestedSetView<Artifact> child : view.transitives()) {
diff --git a/src/test/java/com/google/devtools/build/lib/actions/ArtifactRootTest.java b/src/test/java/com/google/devtools/build/lib/actions/ArtifactRootTest.java
index f486ba9..18be4a5 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/ArtifactRootTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/ArtifactRootTest.java
@@ -73,9 +73,9 @@
   }
 
   @Test
-  public void testBadAsDerivedRootNullDir() throws IOException {
+  public void testBadAsDerivedRootIsExecRoot() throws IOException {
     Path execRoot = scratch.dir("/exec");
-    assertThrows(NullPointerException.class, () -> ArtifactRoot.asDerivedRoot(execRoot, null));
+    assertThrows(IllegalArgumentException.class, () -> ArtifactRoot.asDerivedRoot(execRoot));
   }
 
   @Test