Support mapping of Paths to URIs

Bazel-created files (like log files of test runs) are internally reported
as Paths. However, this is not always the most useful representation of the
location of that artifact for a consumer of build events. Therefore, support
a mapping of paths to more useful URIs.

--
PiperOrigin-RevId: 144843525
MOS_MIGRATED_REVID=144843525
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD
index cd84e57..679722b 100644
--- a/src/main/java/com/google/devtools/build/lib/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -300,6 +300,7 @@
     name = "buildeventstream",
     srcs = glob(["buildeventstream/*.java"]),
     deps = [
+        ":vfs",
         "//src/main/java/com/google/devtools/build/lib/buildeventstream/proto:build_event_stream_java_proto",
         "//src/main/java/com/google/devtools/build/lib/causes",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutedEvent.java b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutedEvent.java
index 889165c..aed5bd7 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutedEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutedEvent.java
@@ -19,8 +19,10 @@
 import com.google.devtools.build.lib.buildeventstream.BuildEventId;
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
 import com.google.devtools.build.lib.buildeventstream.GenericBuildEvent;
+import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import com.google.devtools.build.lib.causes.ActionFailed;
 import com.google.devtools.build.lib.causes.Cause;
+import com.google.devtools.build.lib.vfs.Path;
 import java.util.Collection;
 
 /**
@@ -30,11 +32,11 @@
 public class ActionExecutedEvent implements BuildEvent {
   private final Action action;
   private final ActionExecutionException exception;
-  private final String stdout;
-  private final String stderr;
+  private final Path stdout;
+  private final Path stderr;
 
   public ActionExecutedEvent(Action action,
-      ActionExecutionException exception, String stdout, String stderr) {
+      ActionExecutionException exception, Path stdout, Path stderr) {
     this.action = action;
     this.exception = exception;
     this.stdout = stdout;
@@ -51,11 +53,17 @@
   }
 
   public String getStdout() {
-    return stdout;
+    if (stdout == null) {
+      return null;
+    }
+    return stdout.toString();
   }
 
   public String getStderr() {
-    return stderr;
+    if (stderr == null) {
+      return null;
+    }
+    return stderr.toString();
   }
 
   @Override
@@ -71,7 +79,7 @@
   }
 
   @Override
-  public BuildEventStreamProtos.BuildEvent asStreamProto() {
+  public BuildEventStreamProtos.BuildEvent asStreamProto(PathConverter pathConverter) {
     BuildEventStreamProtos.ActionExecuted.Builder actionBuilder =
         BuildEventStreamProtos.ActionExecuted.newBuilder().setSuccess(getException() == null);
     if (exception.getExitCode() != null) {
@@ -79,11 +87,17 @@
     }
     if (stdout != null) {
       actionBuilder.setStdout(
-          BuildEventStreamProtos.File.newBuilder().setName("stdout").setUri(stdout).build());
+          BuildEventStreamProtos.File.newBuilder()
+          .setName("stdout")
+          .setUri(pathConverter.apply(stdout))
+          .build());
     }
     if (stderr != null) {
       actionBuilder.setStdout(
-          BuildEventStreamProtos.File.newBuilder().setName("stderr").setUri(stderr).build());
+          BuildEventStreamProtos.File.newBuilder()
+          .setName("stderr")
+          .setUri(pathConverter.apply(stderr))
+          .build());
     }
     if (action.getOwner() != null) {
       actionBuilder.setLabel(action.getOwner().getLabel().toString());
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 aa2e9b4..b4cd606 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
@@ -20,6 +20,7 @@
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
 import com.google.devtools.build.lib.buildeventstream.BuildEventWithOrderConstraint;
 import com.google.devtools.build.lib.buildeventstream.GenericBuildEvent;
+import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import com.google.devtools.build.lib.causes.Cause;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
@@ -106,7 +107,7 @@
   }
 
   @Override
-  public BuildEventStreamProtos.BuildEvent asStreamProto() {
+  public BuildEventStreamProtos.BuildEvent asStreamProto(PathConverter pathConverter) {
     BuildEventStreamProtos.TargetComplete complete =
         BuildEventStreamProtos.TargetComplete.newBuilder().setSuccess(!failed()).build();
     return GenericBuildEvent.protoChaining(this).setCompleted(complete).build();
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/AbortedEvent.java b/src/main/java/com/google/devtools/build/lib/buildeventstream/AbortedEvent.java
index 3421b97..fe20063 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventstream/AbortedEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/AbortedEvent.java
@@ -33,7 +33,7 @@
   }
 
   @Override
-  public BuildEventStreamProtos.BuildEvent asStreamProto() {
+  public BuildEventStreamProtos.BuildEvent asStreamProto(PathConverter pathConverter) {
     return GenericBuildEvent.protoChaining(this)
         .setAborted(
             BuildEventStreamProtos.Aborted.newBuilder()
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/BuildEvent.java b/src/main/java/com/google/devtools/build/lib/buildeventstream/BuildEvent.java
index 901586f..0a42d83 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventstream/BuildEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/BuildEvent.java
@@ -14,6 +14,7 @@
 
 package com.google.devtools.build.lib.buildeventstream;
 
+
 /**
  * Interface for objects that can be posted on the public event stream.
  *
@@ -27,5 +28,5 @@
    * <p>Provide a presentation of the event according to the specified binary format, as appropriate
    * protocol buffer.
    */
-  BuildEventStreamProtos.BuildEvent asStreamProto();
+  BuildEventStreamProtos.BuildEvent asStreamProto(PathConverter pathConverter);
 }
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/GenericBuildEvent.java b/src/main/java/com/google/devtools/build/lib/buildeventstream/GenericBuildEvent.java
index 4e4e42a..613ee62 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventstream/GenericBuildEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/GenericBuildEvent.java
@@ -52,7 +52,7 @@
   }
 
   @Override
-  public BuildEventStreamProtos.BuildEvent asStreamProto() {
+  public BuildEventStreamProtos.BuildEvent asStreamProto(PathConverter pathConverter) {
     return protoChaining(this).build();
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/PathConverter.java b/src/main/java/com/google/devtools/build/lib/buildeventstream/PathConverter.java
new file mode 100644
index 0000000..e230d36
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/PathConverter.java
@@ -0,0 +1,27 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.buildeventstream;
+
+import com.google.devtools.build.lib.vfs.Path;
+
+/**
+ * Interface for conversion of paths to URIs.
+ */
+public interface PathConverter {
+  /**
+   * Return the URI corresponding to the given path, if the path can be converted to a URI by this
+   * path converter; return {@link null} otherwise.
+   */
+  String apply(Path path);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/ProgressEvent.java b/src/main/java/com/google/devtools/build/lib/buildeventstream/ProgressEvent.java
index c679233..3a3ab15 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventstream/ProgressEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/ProgressEvent.java
@@ -34,7 +34,7 @@
   }
 
   @Override
-  public BuildEventStreamProtos.BuildEvent asStreamProto() {
+  public BuildEventStreamProtos.BuildEvent asStreamProto(PathConverter pathConverter) {
     return GenericBuildEvent.protoChaining(this)
         .setProgress(BuildEventStreamProtos.Progress.newBuilder().build())
         .build();
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BUILD b/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BUILD
index 11b6c22..14674f5 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BUILD
@@ -10,6 +10,7 @@
     srcs = glob(["*.java"]),
     deps = [
         "//src/main/java/com/google/devtools/build/lib:buildeventstream",
+        "//src/main/java/com/google/devtools/build/lib:vfs",
         "//src/main/java/com/google/devtools/build/lib/buildeventstream/proto:build_event_stream_java_proto",
         "//src/main/java/com/google/devtools/common/options",
         "//third_party:guava",
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BinaryFormatFileTransport.java b/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BinaryFormatFileTransport.java
index c0f341e..1bf30b7 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BinaryFormatFileTransport.java
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BinaryFormatFileTransport.java
@@ -16,6 +16,7 @@
 
 import com.google.devtools.build.lib.buildeventstream.BuildEvent;
 import com.google.devtools.build.lib.buildeventstream.BuildEventTransport;
+import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import java.io.BufferedOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
@@ -29,14 +30,17 @@
  */
 public final class BinaryFormatFileTransport implements BuildEventTransport {
   private final BufferedOutputStream out;
+  private final PathConverter pathConverter;
 
-  public BinaryFormatFileTransport(String path) throws IOException {
+  public BinaryFormatFileTransport(String path, PathConverter pathConverter)
+      throws IOException {
     this.out = new BufferedOutputStream(new FileOutputStream(new File(path)));
+    this.pathConverter = pathConverter;
   }
 
   @Override
   public synchronized void sendBuildEvent(BuildEvent event) throws IOException {
-    event.asStreamProto().writeDelimitedTo(out);
+    event.asStreamProto(pathConverter).writeDelimitedTo(out);
     out.flush();
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactory.java b/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactory.java
index f456b18..6d8f97f 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactory.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSet.Builder;
 import com.google.devtools.build.lib.buildeventstream.BuildEventTransport;
+import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import java.io.IOException;
 
 /** Factory used to create a Set of BuildEventTransports from BuildEventStreamOptions. */
@@ -30,8 +31,9 @@
     }
 
     @Override
-    protected BuildEventTransport create(BuildEventStreamOptions options) throws IOException {
-      return new TextFormatFileTransport(options.getBuildEventTextFile());
+    protected BuildEventTransport create(BuildEventStreamOptions options,
+        PathConverter pathConverter) throws IOException {
+      return new TextFormatFileTransport(options.getBuildEventTextFile(), pathConverter);
     }
   },
 
@@ -42,8 +44,9 @@
     }
 
     @Override
-    protected BuildEventTransport create(BuildEventStreamOptions options) throws IOException {
-      return new BinaryFormatFileTransport(options.getBuildEventBinaryFile());
+    protected BuildEventTransport create(BuildEventStreamOptions options,
+        PathConverter pathConverter) throws IOException {
+      return new BinaryFormatFileTransport(options.getBuildEventBinaryFile(), pathConverter);
     }
   };
 
@@ -55,12 +58,12 @@
    * @return A {@link ImmutableSet} of BuildEventTransports. This set may be empty.
    * @throws IOException Exception propagated from a {@link BuildEventTransport} creation failure.
    */
-  public static ImmutableSet<BuildEventTransport> createFromOptions(BuildEventStreamOptions options)
-      throws IOException {
+  public static ImmutableSet<BuildEventTransport> createFromOptions(BuildEventStreamOptions options,
+      PathConverter pathConverter) throws IOException {
     Builder<BuildEventTransport> buildEventTransportsBuilder = ImmutableSet.builder();
     for (BuildEventTransportFactory transportFactory : BuildEventTransportFactory.values()) {
       if (transportFactory.enabled(options)) {
-        buildEventTransportsBuilder.add(transportFactory.create(options));
+        buildEventTransportsBuilder.add(transportFactory.create(options, pathConverter));
       }
     }
     return buildEventTransportsBuilder.build();
@@ -70,5 +73,6 @@
   protected abstract boolean enabled(BuildEventStreamOptions options);
 
   /** Creates a BuildEventTransport from the specified options. */
-  protected abstract BuildEventTransport create(BuildEventStreamOptions options) throws IOException;
+  protected abstract BuildEventTransport create(BuildEventStreamOptions options,
+      PathConverter pathConverter) throws IOException;
 }
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/TextFormatFileTransport.java b/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/TextFormatFileTransport.java
index 98b7520..5f8f59d 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/TextFormatFileTransport.java
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/TextFormatFileTransport.java
@@ -16,6 +16,7 @@
 
 import com.google.devtools.build.lib.buildeventstream.BuildEvent;
 import com.google.devtools.build.lib.buildeventstream.BuildEventTransport;
+import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import com.google.protobuf.TextFormat;
 import java.io.File;
 import java.io.FileOutputStream;
@@ -28,15 +29,18 @@
  */
 public final class TextFormatFileTransport implements BuildEventTransport {
   private FileOutputStream out;
+  private final PathConverter pathConverter;
 
-  public TextFormatFileTransport(String path) throws IOException {
+  public TextFormatFileTransport(String path, PathConverter pathConverter)
+      throws IOException {
     this.out = new FileOutputStream(new File(path));
+    this.pathConverter = pathConverter;
   }
 
   @Override
   public synchronized void sendBuildEvent(BuildEvent event) throws IOException {
     if (out != null) {
-      String protoTextRepresentation = TextFormat.printToString(event.asStreamProto());
+      String protoTextRepresentation = TextFormat.printToString(event.asStreamProto(pathConverter));
       out.write(("event {\n" + protoTextRepresentation + "}\n\n").getBytes(StandardCharsets.UTF_8));
       out.flush();
     }
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildStartingEvent.java b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildStartingEvent.java
index b819732..3a87b5db 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildStartingEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildStartingEvent.java
@@ -20,6 +20,7 @@
 import com.google.devtools.build.lib.buildeventstream.BuildEventId;
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
 import com.google.devtools.build.lib.buildeventstream.GenericBuildEvent;
+import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import com.google.devtools.build.lib.buildeventstream.ProgressEvent;
 import com.google.devtools.build.lib.buildtool.BuildRequest;
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
@@ -85,7 +86,7 @@
   }
 
   @Override
-  public BuildEventStreamProtos.BuildEvent asStreamProto() {
+  public BuildEventStreamProtos.BuildEvent asStreamProto(PathConverter pathConverter) {
     BuildEventStreamProtos.BuildStarted.Builder started =
         BuildEventStreamProtos.BuildStarted.newBuilder()
             .setUuid(request.getId().toString())
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/TargetParsingCompleteEvent.java b/src/main/java/com/google/devtools/build/lib/pkgcache/TargetParsingCompleteEvent.java
index 82fbcf1..94d150f 100644
--- a/src/main/java/com/google/devtools/build/lib/pkgcache/TargetParsingCompleteEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/TargetParsingCompleteEvent.java
@@ -22,6 +22,7 @@
 import com.google.devtools.build.lib.buildeventstream.BuildEventId;
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
 import com.google.devtools.build.lib.buildeventstream.GenericBuildEvent;
+import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.packages.TargetUtils;
@@ -122,7 +123,7 @@
   }
 
   @Override
-  public BuildEventStreamProtos.BuildEvent asStreamProto() {
+  public BuildEventStreamProtos.BuildEvent asStreamProto(PathConverter pathConverter) {
     return GenericBuildEvent.protoChaining(this)
         .setExpanded(BuildEventStreamProtos.PatternExpanded.newBuilder().build())
         .build();
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
index 434d099..07b4908 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
@@ -30,6 +30,7 @@
 import com.google.devtools.build.lib.analysis.config.BinTools;
 import com.google.devtools.build.lib.analysis.config.BuildOptions;
 import com.google.devtools.build.lib.analysis.config.ConfigurationFactory;
+import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.OutputFilter;
 import com.google.devtools.build.lib.flags.CommandNameCache;
@@ -143,6 +144,7 @@
   private final String defaultsPackageContent;
   private final SubscriberExceptionHandler eventBusExceptionHandler;
   private final String productName;
+  private final PathConverter pathToUriConverter;
 
   // Workspace state (currently exactly one workspace per server)
   private BlazeWorkspace workspace;
@@ -163,7 +165,8 @@
       ProjectFile.Provider projectFileProvider,
       InvocationPolicy invocationPolicy,
       Iterable<BlazeCommand> commands,
-      String productName) {
+      String productName,
+      PathConverter pathToUriConverter) {
     // Server state
     this.blazeModules = blazeModules;
     overrideCommands(commands);
@@ -188,6 +191,7 @@
     CommandNameCache.CommandNameCacheInstance.INSTANCE.setCommandNameCache(
         new CommandNameCacheImpl(getCommandMap()));
     this.productName = productName;
+    this.pathToUriConverter = pathToUriConverter;
   }
 
   public void initWorkspace(BlazeDirectories directories, BinTools binTools)
@@ -1067,6 +1071,10 @@
     return productName;
   }
 
+  public PathConverter getPathToUriConverter() {
+    return pathToUriConverter;
+  }
+
   /**
    * A builder for {@link BlazeRuntime} objects. The only required fields are the {@link
    * BlazeDirectories}, and the {@link RuleClassProvider} (except for testing). All other fields
@@ -1167,7 +1175,8 @@
           projectFileProvider,
           serverBuilder.getInvocationPolicy(),
           serverBuilder.getCommands(),
-          productName);
+          productName,
+          serverBuilder.getPathToUriConverter());
     }
 
     public Builder setProductName(String productName) {
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BuildEventStreamerModule.java b/src/main/java/com/google/devtools/build/lib/runtime/BuildEventStreamerModule.java
index b405c79..47f5b08 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BuildEventStreamerModule.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BuildEventStreamerModule.java
@@ -23,9 +23,11 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.buildeventstream.BuildEventTransport;
+import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import com.google.devtools.build.lib.buildeventstream.transports.BuildEventStreamOptions;
 import com.google.devtools.build.lib.util.AbruptExitException;
 import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.common.options.OptionsBase;
 import com.google.devtools.common.options.OptionsProvider;
 import java.io.IOException;
@@ -62,11 +64,23 @@
   Optional<BuildEventStreamer> tryCreateStreamer(
       OptionsProvider optionsProvider, ModuleEnvironment moduleEnvironment) {
     try {
+      PathConverter pathConverter;
+      if (commandEnvironment == null) {
+        pathConverter = new PathConverter() {
+            @Override
+            public String apply(Path path) {
+              return path.getPathString();
+            }
+          };
+      } else {
+        pathConverter = commandEnvironment.getRuntime().getPathToUriConverter();
+      }
       BuildEventStreamOptions besOptions =
           checkNotNull(
               optionsProvider.getOptions(BuildEventStreamOptions.class),
               "Could not get BuildEventStreamOptions");
-      ImmutableSet<BuildEventTransport> buildEventTransports = createFromOptions(besOptions);
+      ImmutableSet<BuildEventTransport> buildEventTransports
+          = createFromOptions(besOptions, pathConverter);
       if (!buildEventTransports.isEmpty()) {
         BuildEventStreamer streamer = new BuildEventStreamer(buildEventTransports);
         return Optional.of(streamer);
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/ServerBuilder.java b/src/main/java/com/google/devtools/build/lib/runtime/ServerBuilder.java
index dde7062..f8266b41 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/ServerBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/ServerBuilder.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import com.google.devtools.build.lib.packages.AttributeContainer;
 import com.google.devtools.build.lib.packages.PackageFactory;
 import com.google.devtools.build.lib.packages.RuleClass;
@@ -27,6 +28,7 @@
 import com.google.devtools.build.lib.runtime.commands.InfoItem;
 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
 import com.google.devtools.build.lib.util.Preconditions;
+import com.google.devtools.build.lib.vfs.Path;
 
 /**
  * Builder class to create a {@link BlazeRuntime} instance. This class is part of the module API,
@@ -43,6 +45,8 @@
       ImmutableList.builder();
   private final ImmutableList.Builder<PackageFactory.EnvironmentExtension> environmentExtensions =
       ImmutableList.builder();
+  private final ImmutableList.Builder<PathConverter> pathToUriConverters
+      = ImmutableList.builder();
 
   @VisibleForTesting
   public ServerBuilder() {}
@@ -86,6 +90,27 @@
   }
 
   /**
+   * Return the derived total converter from Paths to URIs. It returns the answer of the first
+   * registered converter that can convert the given path, if any. If no registered converter can
+   * convert the given path, the "file" URI scheme is used.
+   */
+  public PathConverter getPathToUriConverter() {
+    final ImmutableList<PathConverter> converters = this.pathToUriConverters.build();
+    return new PathConverter(){
+      @Override
+      public String apply(Path path) {
+        for (PathConverter converter : converters) {
+          String value = converter.apply(path);
+          if (value != null) {
+            return value;
+          }
+        }
+        return "file://" + path.getPathString();
+      }
+    };
+  }
+
+  /**
    * Merges the given invocation policy into the per-server invocation policy. While this can accept
    * any number of policies, the end result is order-dependent if multiple policies attempt to
    * police the same options, so it's probably a good idea to not have too many modules that call
@@ -171,4 +196,12 @@
     this.environmentExtensions.add(extension);
     return this;
   }
+
+  /**
+   * Register a new {@link PathConverter}. Contervers are tried in the order they are registered.
+   */
+  public ServerBuilder addPathToUriConverter(PathConverter converter) {
+    this.pathToUriConverters.add(converter);
+    return this;
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java b/src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java
index 7e84395..394fa9a 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java
@@ -25,6 +25,7 @@
 import com.google.devtools.build.lib.buildeventstream.BuildEventId;
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
 import com.google.devtools.build.lib.buildeventstream.GenericBuildEvent;
+import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import com.google.devtools.build.lib.util.Preconditions;
 import com.google.devtools.build.lib.util.io.AnsiTerminalPrinter.Mode;
 import com.google.devtools.build.lib.vfs.Path;
@@ -463,16 +464,16 @@
   }
 
   @Override
-  public BuildEventStreamProtos.BuildEvent asStreamProto() {
+  public BuildEventStreamProtos.BuildEvent asStreamProto(PathConverter pathConverter) {
     BuildEventStreamProtos.TestSummary.Builder summaryBuilder =
         BuildEventStreamProtos.TestSummary.newBuilder().setTotalRunCount(totalRuns());
     for (Path path : getFailedLogs()) {
       summaryBuilder.addFailed(
-          BuildEventStreamProtos.File.newBuilder().setUri(path.toString()).build());
+          BuildEventStreamProtos.File.newBuilder().setUri(pathConverter.apply(path)).build());
     }
     for (Path path : getPassedLogs()) {
       summaryBuilder.addPassed(
-          BuildEventStreamProtos.File.newBuilder().setUri(path.toString()).build());
+          BuildEventStreamProtos.File.newBuilder().setUri(pathConverter.apply(path)).build());
     }
     return GenericBuildEvent.protoChaining(this).setTestSummary(summaryBuilder.build()).build();
   }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
index 3c3b455..bff5be7 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
@@ -1080,14 +1080,14 @@
 
   private void reportActionExecution(Action action,
       ActionExecutionException exception, FileOutErr outErr) {
-    String stdout = null;
-    String stderr = null;
+    Path stdout = null;
+    Path stderr = null;
 
     if (outErr.hasRecordedStdout()) {
-      stdout = outErr.getOutputPath().toString();
+      stdout = outErr.getOutputPath();
     }
     if (outErr.hasRecordedStderr()) {
-      stderr = outErr.getErrorPath().toString();
+      stderr = outErr.getErrorPath();
     }
     postEvent(new ActionExecutedEvent(action, exception, stdout, stderr));
   }
diff --git a/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BUILD b/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BUILD
index 6472d8a..896cb5a 100644
--- a/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BUILD
@@ -12,6 +12,7 @@
     deps = [
         "//src/main/java/com/google/devtools/build/lib:buildeventstream",
         "//src/main/java/com/google/devtools/build/lib:runtime",
+        "//src/main/java/com/google/devtools/build/lib:vfs",
         "//src/main/java/com/google/devtools/build/lib/buildeventstream/proto:build_event_stream_java_proto",
         "//src/main/java/com/google/devtools/build/lib/buildeventstream/transports",
         "//src/test/java/com/google/devtools/build/lib:packages_testutil",
diff --git a/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BinaryFormatFileTransportTest.java b/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BinaryFormatFileTransportTest.java
index e241e84..62ac3da 100644
--- a/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BinaryFormatFileTransportTest.java
+++ b/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BinaryFormatFileTransportTest.java
@@ -22,6 +22,7 @@
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildStarted;
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.Progress;
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.TargetComplete;
+import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -45,6 +46,8 @@
 
   @Mock public BuildEvent buildEvent;
 
+  @Mock public PathConverter pathConverter;
+
   @Before
   public void initMocks() {
     MockitoAnnotations.initMocks(this);
@@ -63,20 +66,21 @@
         BuildEventStreamProtos.BuildEvent.newBuilder()
             .setStarted(BuildStarted.newBuilder().setCommand("build"))
             .build();
-    when(buildEvent.asStreamProto()).thenReturn(started);
-    BinaryFormatFileTransport transport = new BinaryFormatFileTransport(output.getAbsolutePath());
+    when(buildEvent.asStreamProto(pathConverter)).thenReturn(started);
+    BinaryFormatFileTransport transport =
+        new BinaryFormatFileTransport(output.getAbsolutePath(), pathConverter);
     transport.sendBuildEvent(buildEvent);
 
     BuildEventStreamProtos.BuildEvent progress =
         BuildEventStreamProtos.BuildEvent.newBuilder().setProgress(Progress.newBuilder()).build();
-    when(buildEvent.asStreamProto()).thenReturn(progress);
+    when(buildEvent.asStreamProto(pathConverter)).thenReturn(progress);
     transport.sendBuildEvent(buildEvent);
 
     BuildEventStreamProtos.BuildEvent completed =
         BuildEventStreamProtos.BuildEvent.newBuilder()
             .setCompleted(TargetComplete.newBuilder().setSuccess(true))
             .build();
-    when(buildEvent.asStreamProto()).thenReturn(completed);
+    when(buildEvent.asStreamProto(pathConverter)).thenReturn(completed);
     transport.sendBuildEvent(buildEvent);
 
     transport.close();
diff --git a/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactoryTest.java b/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactoryTest.java
index 8d4aa52..58bd175 100644
--- a/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactoryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactoryTest.java
@@ -24,6 +24,7 @@
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildStarted;
 import com.google.devtools.build.lib.buildeventstream.BuildEventTransport;
+import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import java.io.File;
 import java.io.IOException;
 import org.junit.After;
@@ -60,10 +61,12 @@
 
   @Mock public BuildEvent buildEvent;
 
+  @Mock public PathConverter pathConverter;
+
   @Before
   public void before() {
     MockitoAnnotations.initMocks(this);
-    when(buildEvent.asStreamProto()).thenReturn(BUILD_EVENT_AS_PROTO);
+    when(buildEvent.asStreamProto(pathConverter)).thenReturn(BUILD_EVENT_AS_PROTO);
   }
 
   @After
@@ -77,7 +80,7 @@
     when(options.getBuildEventTextFile()).thenReturn(textFile.getAbsolutePath());
     when(options.getBuildEventBinaryFile()).thenReturn("");
     ImmutableSet<BuildEventTransport> transports =
-        BuildEventTransportFactory.createFromOptions(options);
+        BuildEventTransportFactory.createFromOptions(options, pathConverter);
     assertThat(FluentIterable.from(transports).transform(GET_CLASS))
         .containsExactly(TextFormatFileTransport.class);
     sendEventsAndClose(buildEvent, transports);
@@ -90,7 +93,7 @@
     when(options.getBuildEventTextFile()).thenReturn("");
     when(options.getBuildEventBinaryFile()).thenReturn(binaryFile.getAbsolutePath());
     ImmutableSet<BuildEventTransport> transports =
-        BuildEventTransportFactory.createFromOptions(options);
+        BuildEventTransportFactory.createFromOptions(options, pathConverter);
     assertThat(FluentIterable.from(transports).transform(GET_CLASS))
         .containsExactly(BinaryFormatFileTransport.class);
     sendEventsAndClose(buildEvent, transports);
@@ -104,7 +107,7 @@
     when(options.getBuildEventTextFile()).thenReturn(textFile.getAbsolutePath());
     when(options.getBuildEventBinaryFile()).thenReturn(binaryFile.getAbsolutePath());
     ImmutableSet<BuildEventTransport> transports =
-        BuildEventTransportFactory.createFromOptions(options);
+        BuildEventTransportFactory.createFromOptions(options, pathConverter);
     assertThat(FluentIterable.from(transports).transform(GET_CLASS))
         .containsExactly(TextFormatFileTransport.class, BinaryFormatFileTransport.class);
     sendEventsAndClose(buildEvent, transports);
@@ -116,7 +119,7 @@
   public void testCreatesNoTransports() throws IOException {
     when(options.getBuildEventTextFile()).thenReturn("");
     ImmutableSet<BuildEventTransport> transports =
-        BuildEventTransportFactory.createFromOptions(options);
+        BuildEventTransportFactory.createFromOptions(options, pathConverter);
     assertThat(transports).isEmpty();
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/TextFormatFileTransportTest.java b/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/TextFormatFileTransportTest.java
index bb1d1fd..2f36d40 100644
--- a/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/TextFormatFileTransportTest.java
+++ b/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/TextFormatFileTransportTest.java
@@ -24,6 +24,7 @@
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildStarted;
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.Progress;
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.TargetComplete;
+import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import com.google.protobuf.TextFormat;
 import java.io.File;
 import java.io.IOException;
@@ -47,6 +48,8 @@
 
   @Mock public BuildEvent buildEvent;
 
+  @Mock public PathConverter pathConverter;
+
   @Before
   public void initMocks() {
     MockitoAnnotations.initMocks(this);
@@ -65,20 +68,21 @@
         BuildEventStreamProtos.BuildEvent.newBuilder()
             .setStarted(BuildStarted.newBuilder().setCommand("build"))
             .build();
-    when(buildEvent.asStreamProto()).thenReturn(started);
-    TextFormatFileTransport transport = new TextFormatFileTransport(output.getAbsolutePath());
+    when(buildEvent.asStreamProto(pathConverter)).thenReturn(started);
+    TextFormatFileTransport transport =
+        new TextFormatFileTransport(output.getAbsolutePath(), pathConverter);
     transport.sendBuildEvent(buildEvent);
 
     BuildEventStreamProtos.BuildEvent progress =
         BuildEventStreamProtos.BuildEvent.newBuilder().setProgress(Progress.newBuilder()).build();
-    when(buildEvent.asStreamProto()).thenReturn(progress);
+    when(buildEvent.asStreamProto(pathConverter)).thenReturn(progress);
     transport.sendBuildEvent(buildEvent);
 
     BuildEventStreamProtos.BuildEvent completed =
         BuildEventStreamProtos.BuildEvent.newBuilder()
             .setCompleted(TargetComplete.newBuilder().setSuccess(true))
             .build();
-    when(buildEvent.asStreamProto()).thenReturn(completed);
+    when(buildEvent.asStreamProto(pathConverter)).thenReturn(completed);
     transport.sendBuildEvent(buildEvent);
 
     transport.close();
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java b/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java
index a5a79d0..d03ec6b 100644
--- a/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java
@@ -24,6 +24,7 @@
 import com.google.devtools.build.lib.buildeventstream.BuildEventTransport;
 import com.google.devtools.build.lib.buildeventstream.BuildEventWithOrderConstraint;
 import com.google.devtools.build.lib.buildeventstream.GenericBuildEvent;
+import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import com.google.devtools.build.lib.buildeventstream.ProgressEvent;
 import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
 import java.util.ArrayList;
@@ -84,7 +85,7 @@
     }
 
     @Override
-    public BuildEventStreamProtos.BuildEvent asStreamProto() {
+    public BuildEventStreamProtos.BuildEvent asStreamProto(PathConverter converter) {
       return GenericBuildEvent.protoChaining(this).build();
     }