ArtifactRoot correctly switches output base when serializing.

PiperOrigin-RevId: 190779535
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 a3abeb5..4f08d6b 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
@@ -17,8 +17,10 @@
 import com.google.common.base.Preconditions;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
-import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
-import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization;
+import com.google.devtools.build.lib.skyframe.serialization.DeserializationContext;
+import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec;
+import com.google.devtools.build.lib.skyframe.serialization.SerializationContext;
+import com.google.devtools.build.lib.skyframe.serialization.SerializationException;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory;
@@ -27,6 +29,9 @@
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.Root;
+import com.google.protobuf.CodedInputStream;
+import com.google.protobuf.CodedOutputStream;
+import java.io.IOException;
 import java.io.Serializable;
 import java.util.Objects;
 
@@ -54,7 +59,6 @@
           + "together into a single directory tree to form the execution environment."
 )
 @Immutable
-@AutoCodec
 public final class ArtifactRoot implements Comparable<ArtifactRoot>, Serializable, SkylarkValue {
   // This must always be consistent with Package.getSourceRoot; otherwise computing source roots
   // from exec paths does not work, which can break the action cache for input-discovering actions.
@@ -98,8 +102,7 @@
     return new ArtifactRoot(Root.fromPath(root), execPath, RootType.Middleman);
   }
 
-  @VisibleForSerialization
-  enum RootType {
+  private enum RootType {
     Source,
     Output,
     Middleman
@@ -109,7 +112,6 @@
   private final PathFragment execPath;
   private final RootType rootType;
 
-  @AutoCodec.Instantiator
   ArtifactRoot(Root root, PathFragment execPath, RootType rootType) {
     this.root = Preconditions.checkNotNull(root);
     this.execPath = execPath;
@@ -174,4 +176,49 @@
   public void repr(SkylarkPrinter printer) {
     printer.append(isSourceRoot() ? "<source root>" : "<derived root>");
   }
+
+  /** Custom codec that replaces output base with local output base on deserialization. */
+  private static class ArtifactRootCodec implements ObjectCodec<ArtifactRoot> {
+    @Override
+    public Class<ArtifactRoot> getEncodedClass() {
+      return ArtifactRoot.class;
+    }
+
+    @Override
+    public void serialize(
+        SerializationContext context, ArtifactRoot input, CodedOutputStream codedOut)
+        throws SerializationException, IOException {
+      context.serialize(input.rootType, codedOut);
+      switch (input.rootType) {
+        case Source:
+          context.serialize(input.root, codedOut);
+          break;
+        case Output: // fall-through, same behavior as Middleman
+        case Middleman:
+          Path outputBase = context.getDependency(OutputBaseSupplier.class).get();
+          context.serialize(input.root.asPath().relativeTo(outputBase), codedOut);
+          break;
+      }
+      context.serialize(input.execPath, codedOut);
+    }
+
+    @Override
+    public ArtifactRoot deserialize(DeserializationContext context, CodedInputStream codedIn)
+        throws SerializationException, IOException {
+      ArtifactRoot.RootType rootType = context.deserialize(codedIn);
+      Root root = null;
+      switch (rootType) {
+        case Source:
+          root = context.deserialize(codedIn);
+          break;
+        case Output: // fall-through, same behavior as Middleman
+        case Middleman:
+          Path outputBase = context.getDependency(OutputBaseSupplier.class).get();
+          PathFragment relativeRoot = context.deserialize(codedIn);
+          root = Root.fromPath(outputBase.getRelative(relativeRoot));
+          break;
+      }
+      return new ArtifactRoot(root, context.deserialize(codedIn), rootType);
+    }
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/actions/OutputBaseSupplier.java b/src/main/java/com/google/devtools/build/lib/actions/OutputBaseSupplier.java
new file mode 100644
index 0000000..f915869
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/OutputBaseSupplier.java
@@ -0,0 +1,23 @@
+// Copyright 2018 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.actions;
+
+import com.google.devtools.build.lib.vfs.Path;
+
+/** Provides this Blaze server's output base used for serialization. */
+@FunctionalInterface
+public interface OutputBaseSupplier {
+  Path get();
+}
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 71714c3..1ce0ac1 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
@@ -397,6 +397,10 @@
     return projectFileProvider;
   }
 
+  public Path getOutputBase() {
+    return getWorkspace().getDirectories().getOutputBase();
+  }
+
   /**
    * Hook method called by the BlazeCommandDispatcher prior to the dispatch of
    * each command.
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
index 2f35acc..a089b0b 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
@@ -2034,6 +2034,11 @@
     return pkgFactory.getPackageBuilderHelperForTesting();
   }
 
+  @VisibleForTesting
+  public Path getOutputBaseForTesting() {
+    return directories.getOutputBase();
+  }
+
   /**
    * Initializes and syncs the graph with the given options, readying it for the next evaluation.
    */
diff --git a/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java b/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java
index 26efc19..e7ead54 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java
@@ -346,6 +346,7 @@
                 PathFragment.create("src/c"),
                 new LabelArtifactOwner(Label.parseAbsoluteUnchecked("//foo:bar"))))
         .addDependency(FileSystem.class, scratch.getFileSystem())
+        .addDependency(OutputBaseSupplier.class, () -> scratch.getFileSystem().getPath("/"))
         .runTests();
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/actions/ExecutableSymlinkActionTest.java b/src/test/java/com/google/devtools/build/lib/actions/ExecutableSymlinkActionTest.java
index fedc601..83e3f31 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/ExecutableSymlinkActionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/ExecutableSymlinkActionTest.java
@@ -37,6 +37,7 @@
 @RunWith(JUnit4.class)
 public class ExecutableSymlinkActionTest {
   private Scratch scratch = new Scratch();
+  private Path execRoot;
   private ArtifactRoot inputRoot;
   private ArtifactRoot outputRoot;
   TestFileOutErr outErr;
@@ -46,7 +47,7 @@
   @Before
   public final void createExecutor() throws Exception  {
     final Path inputDir = scratch.dir("/in");
-    Path execRoot = scratch.getFileSystem().getPath("/");
+    execRoot = scratch.getFileSystem().getPath("/");
     inputRoot = ArtifactRoot.asDerivedRoot(execRoot, inputDir);
     outputRoot = ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/out"));
     outErr = new TestFileOutErr();
@@ -125,6 +126,7 @@
     ExecutableSymlinkAction action = new ExecutableSymlinkAction(NULL_ACTION_OWNER, input, output);
     new SerializationTester(action)
         .addDependency(FileSystem.class, scratch.getFileSystem())
+        .addDependency(OutputBaseSupplier.class, () -> execRoot)
         .setVerificationFunction(
             (in, out) -> {
               ExecutableSymlinkAction inAction = (ExecutableSymlinkAction) in;
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/SymlinkActionTest.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/SymlinkActionTest.java
index b304558..cdd4760 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/actions/SymlinkActionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/SymlinkActionTest.java
@@ -22,6 +22,7 @@
 import com.google.devtools.build.lib.actions.ActionResult;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.OutputBaseSupplier;
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
 import com.google.devtools.build.lib.exec.util.TestExecutorBuilder;
 import com.google.devtools.build.lib.skyframe.serialization.testutils.SerializationTester;
@@ -47,7 +48,7 @@
   private SymlinkAction action;
 
   @Before
-  public final void setUp() throws Exception  {
+  public final void setUp() throws Exception {
     input = scratch.file("input.txt", "Hello, world.");
     inputArtifact = getSourceArtifact("input.txt");
     Path linkedInput =
@@ -98,6 +99,7 @@
   public void testCodec() throws Exception {
     new SerializationTester(action)
         .addDependency(FileSystem.class, scratch.getFileSystem())
+        .addDependency(OutputBaseSupplier.class, () -> outputBase)
         .setVerificationFunction(
             (in, out) -> {
               SymlinkAction inAction = (SymlinkAction) in;
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java b/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
index 7580624..637bd39 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
@@ -1999,8 +1999,7 @@
     Artifact inputManifest = Iterables.getOnlyElement(symlinkTreeAction.getInputs());
     SourceManifestAction inputManifestAction =
         (SourceManifestAction) getGeneratingAction(inputManifest);
-        // Ask the manifest to write itself to a byte array so that we can
-    // read its contents.
+    // Ask the manifest to write itself to a byte array so that we can read its contents.
     ByteArrayOutputStream stream = new ByteArrayOutputStream();
     inputManifestAction.writeOutputFile(stream, reporter);
     String contents = stream.toString();
@@ -2053,4 +2052,8 @@
     return view.getArtifactFactory()
         .getDerivedArtifact(target.getLabel().getPackageFragment().getRelative(path), root, owner);
   }
+
+  public Path getExecRoot() {
+    return directories.getExecRoot(ruleClassProvider.getRunfilesPrefix());
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/exec/MiddlemanActionTest.java b/src/test/java/com/google/devtools/build/lib/exec/MiddlemanActionTest.java
index 5c176fe..c49d5e5 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/MiddlemanActionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/exec/MiddlemanActionTest.java
@@ -23,6 +23,7 @@
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.MiddlemanAction;
 import com.google.devtools.build.lib.actions.MiddlemanFactory;
+import com.google.devtools.build.lib.actions.OutputBaseSupplier;
 import com.google.devtools.build.lib.analysis.util.AnalysisTestUtil;
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
@@ -142,6 +143,7 @@
   public void testCodec() throws Exception {
     new SerializationTester(getGeneratingAction(middle))
         .addDependency(FileSystem.class, scratch.getFileSystem())
+        .addDependency(OutputBaseSupplier.class, () -> outputBase)
         .setVerificationFunction(MiddlemanActionTest::verifyEquivalent)
         .runTests();
   }
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java
index a7b4602..2905030 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java
@@ -42,6 +42,7 @@
 import com.google.devtools.build.lib.actions.ArtifactRoot;
 import com.google.devtools.build.lib.actions.BuildFailedException;
 import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
+import com.google.devtools.build.lib.actions.OutputBaseSupplier;
 import com.google.devtools.build.lib.actions.cache.MetadataHandler;
 import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.actions.util.TestAction;
@@ -117,6 +118,8 @@
   public void testCodec() throws Exception {
     new SerializationTester(outOne, outOneFileOne)
         .addDependency(FileSystem.class, scratch.getFileSystem())
+        .addDependency(
+            OutputBaseSupplier.class, () -> scratch.getFileSystem().getPath(TestUtils.tmpDir()))
         .runTests();
   }