Make the FileArtifactvalue of generated targets available via the FilesetOutputSymlink whenever available.

In this change I'm simply plumbing the FileArtifactValue we requested within RecursiveFilesystemTraversalFunction to the FilesetOutputSymlink.

This does not work when the targets are output directories (or symlink to output dirs). The main scenarios this happens is when:
1. Fileset depends on the output dir created by a genrule.
2. Fileset depends on a GoAppengineBinary which creates an output dir.
3. Fileset depends on another Fileset in the non-recommended way (Fileset.entry.files = [<another_fileset>]) instead of the recommended way (FilesetEntry.srcdir = <another_fileset>).

RELNOTES: None
PiperOrigin-RevId: 204209612
diff --git a/src/main/java/com/google/devtools/build/lib/actions/FilesetOutputSymlink.java b/src/main/java/com/google/devtools/build/lib/actions/FilesetOutputSymlink.java
index 92cf7a9..f344039 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/FilesetOutputSymlink.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/FilesetOutputSymlink.java
@@ -14,43 +14,52 @@
 package com.google.devtools.build.lib.actions;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Objects;
 import com.google.common.base.Preconditions;
 import com.google.devtools.build.lib.vfs.PathFragment;
+import java.util.Objects;
+import javax.annotation.Nullable;
 
 /** Definition of a symlink in the output tree of a Fileset rule. */
-public final class FilesetOutputSymlink {
+public abstract class FilesetOutputSymlink {
   private static final String STRIPPED_METADATA = "<stripped-for-testing>";
+  private final PathFragment name;
+  private final PathFragment target;
+  private final String metadata;
 
-  /** Final name of the symlink relative to the Fileset's output directory. */
-  public final PathFragment name;
-
-  /** Target of the symlink. Depending on FilesetEntry.symlinks it may be relative or absolute. */
-  public final PathFragment target;
-
-  /** Opaque metadata about the link and its target; should change if either of them changes. */
-  public final String metadata;
-
-  @VisibleForTesting
-  public FilesetOutputSymlink(PathFragment name, PathFragment target) {
-    this.name = name;
-    this.target = target;
-    this.metadata = STRIPPED_METADATA;
-  }
-
-  /**
-   * @param name relative path under the Fileset's output directory, including FilesetEntry.destdir
-   *        with and FilesetEntry.strip_prefix applied (if applicable)
-   * @param target relative or absolute value of the link
-   * @param metadata opaque metadata about the link and its target; should change if either the link
-   *        or its target changes
-   */
-  public FilesetOutputSymlink(PathFragment name, PathFragment target, String metadata) {
+  FilesetOutputSymlink(PathFragment name, PathFragment target, String metadata) {
     this.name = Preconditions.checkNotNull(name);
     this.target = Preconditions.checkNotNull(target);
     this.metadata = Preconditions.checkNotNull(metadata);
   }
 
+  /** Final name of the symlink relative to the Fileset's output directory. */
+  public PathFragment getName() {
+    return name;
+  }
+
+  /**
+   * Target of the symlink. This may be relative to the target's location if the target itself is a
+   * relative symlink. We can override it by using FilesetEntry.symlinks = 'dereference'.
+   */
+  public PathFragment getTargetPath() {
+    return target;
+  }
+
+  /** Opaque metadata about the link and its target; should change if either of them changes. */
+  public String getMetadata() {
+    return metadata;
+  }
+
+  /** true if the target is a generated artifact */
+  public abstract boolean isGeneratedTarget();
+
+  /**
+   * returns the target artifact if it's generated {@link #isGeneratedTarget() == true}, or null
+   * otherwise.
+   */
+  @Nullable
+  public abstract FileArtifactValue getTargetArtifactValue();
+
   @Override
   public boolean equals(Object obj) {
     if (this == obj) {
@@ -60,22 +69,101 @@
       return false;
     }
     FilesetOutputSymlink o = (FilesetOutputSymlink) obj;
-    return name.equals(o.name) && target.equals(o.target) && metadata.equals(o.metadata);
+    return getName().equals(o.getName())
+        && getTargetPath().equals(o.getTargetPath())
+        && getMetadata().equals(o.getMetadata())
+        && isGeneratedTarget() == o.isGeneratedTarget()
+        && Objects.equals(getTargetArtifactValue(), o.getTargetArtifactValue());
   }
 
   @Override
   public int hashCode() {
-    return Objects.hashCode(name, target, metadata);
+    return Objects.hash(
+        getName(), getTargetPath(), getMetadata(), isGeneratedTarget(), getTargetArtifactValue());
   }
 
   @Override
   public String toString() {
-    if (metadata.equals(STRIPPED_METADATA)) {
-      return String.format("FilesetOutputSymlink(%s -> %s)",
-          name.getPathString(), target.getPathString());
+    if (getMetadata().equals(STRIPPED_METADATA)) {
+      return String.format(
+          "FilesetOutputSymlink(%s -> %s)",
+          getName().getPathString(), getTargetPath().getPathString());
     } else {
-      return String.format("FilesetOutputSymlink(%s -> %s | metadata=%s)",
-          name.getPathString(), target.getPathString(), metadata);
+      return String.format(
+          "FilesetOutputSymlink(%s -> %s | metadata=%s)",
+          getName().getPathString(), getTargetPath().getPathString(), getMetadata());
+    }
+  }
+
+  @VisibleForTesting
+  public static FilesetOutputSymlink createForTesting(PathFragment name, PathFragment target) {
+    return new SourceOutputSymlink(name, target, STRIPPED_METADATA);
+  }
+
+  /**
+   * @param name relative path under the Fileset's output directory, including FilesetEntry.destdir
+   *     with and FilesetEntry.strip_prefix applied (if applicable)
+   * @param target relative or absolute value of the link
+   * @param metadata opaque metadata about the link and its target; should change if either the link
+   *     or its target changes
+   */
+  public static FilesetOutputSymlink createForSourceTarget(
+      PathFragment name, PathFragment target, String metadata) {
+    return new SourceOutputSymlink(name, target, metadata);
+  }
+
+  /**
+   * @param name relative path under the Fileset's output directory, including FilesetEntry.destdir
+   *     with and FilesetEntry.strip_prefix applied (if applicable)
+   * @param target relative or absolute value of the link
+   * @param metadata opaque metadata about the link and its target; should change if either the link
+   *     or its target changes
+   * @param fileArtifactValue the {@link FileArtifactValue} corresponding to the target.
+   */
+  public static FilesetOutputSymlink createForDerivedTarget(
+      PathFragment name,
+      PathFragment target,
+      String metadata,
+      @Nullable FileArtifactValue fileArtifactValue) {
+    return new DerivedOutputSymlink(name, target, metadata, fileArtifactValue);
+  }
+
+  private static class DerivedOutputSymlink extends FilesetOutputSymlink {
+    private final FileArtifactValue fileArtifactValue;
+
+    DerivedOutputSymlink(
+        PathFragment name,
+        PathFragment target,
+        String metadata,
+        FileArtifactValue fileArtifactValue) {
+      super(name, target, metadata);
+      this.fileArtifactValue = fileArtifactValue;
+    }
+
+    @Override
+    public boolean isGeneratedTarget() {
+      return true;
+    }
+
+    @Override
+    public FileArtifactValue getTargetArtifactValue() {
+      return fileArtifactValue;
+    }
+  }
+
+  private static class SourceOutputSymlink extends FilesetOutputSymlink {
+    SourceOutputSymlink(PathFragment name, PathFragment target, String metadata) {
+      super(name, target, metadata);
+    }
+
+    @Override
+    public boolean isGeneratedTarget() {
+      return false;
+    }
+
+    @Override
+    public FileArtifactValue getTargetArtifactValue() {
+      return null;
     }
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/exec/FilesetManifest.java b/src/main/java/com/google/devtools/build/lib/exec/FilesetManifest.java
index c48674a..1b35631 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/FilesetManifest.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/FilesetManifest.java
@@ -78,8 +78,8 @@
     LinkedHashMap<PathFragment, String> entries = new LinkedHashMap<>();
     Map<PathFragment, String> relativeLinks = new HashMap<>();
     for (FilesetOutputSymlink outputSymlink : outputSymlinks) {
-      PathFragment fullLocation = targetPrefix.getRelative(outputSymlink.name);
-      String artifact = outputSymlink.target.getPathString();
+      PathFragment fullLocation = targetPrefix.getRelative(outputSymlink.getName());
+      String artifact = outputSymlink.getTargetPath().getPathString();
       artifact = artifact.isEmpty() ? null : artifact;
       addSymlinkEntry(artifact, fullLocation, relSymlinkbehavior, entries, relativeLinks);
     }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunction.java
index aa4fa7b..8f4901f 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunction.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.actions.FileArtifactValue;
 import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
 import com.google.devtools.build.lib.actions.FilesetTraversalParams;
 import com.google.devtools.build.lib.actions.FilesetTraversalParams.DirectTraversal;
@@ -59,7 +60,8 @@
 
     // Create the set of excluded files. Only top-level files can be excluded, i.e. ones that are
     // directly under the root if the root is a directory.
-    Set<String> exclusions = createExclusionSet(t.getExcludedFiles());
+    Set<String> exclusions =
+        Sets.filter(t.getExcludedFiles(), e -> PathFragment.create(e).segmentCount() == 1);
 
     // The map of output symlinks. Each key is the path of a output symlink that the Fileset must
     // create, relative to the Fileset.out directory, and each value specifies extra information
@@ -79,7 +81,7 @@
       for (SkyKey nestedKey : nestedKeys) {
         FilesetEntryValue nested = (FilesetEntryValue) results.get(nestedKey);
         for (FilesetOutputSymlink s : nested.getSymlinks()) {
-          if (!exclusions.contains(s.name.getPathString())) {
+          if (!exclusions.contains(s.getName().getPathString())) {
             maybeStoreSymlink(s, t.getDestPath(), outputSymlinks);
           }
         }
@@ -189,7 +191,15 @@
 
         // Metadata field must be present. It can only be absent when stripped by tests.
         String metadata = Integer.toHexString(f.getMetadataHash());
-        maybeStoreSymlink(linkName, targetName, metadata, t.getDestPath(), outputSymlinks);
+
+        maybeStoreSymlink(
+            linkName,
+            targetName,
+            metadata,
+            t.getDestPath(),
+            direct.isGenerated(),
+            f.getValueForDerivedArtifacts(),
+            outputSymlinks);
       }
     }
 
@@ -201,7 +211,14 @@
       FilesetOutputSymlink nestedLink,
       PathFragment destPath,
       Map<PathFragment, FilesetOutputSymlink> result) {
-    maybeStoreSymlink(nestedLink.name, nestedLink.target, nestedLink.metadata, destPath, result);
+    maybeStoreSymlink(
+        nestedLink.getName(),
+        nestedLink.getTargetPath(),
+        nestedLink.getMetadata(),
+        destPath,
+        nestedLink.isGeneratedTarget(),
+        nestedLink.getTargetArtifactValue(),
+        result);
   }
 
   /** Stores an output symlink unless it would overwrite an existing one. */
@@ -210,22 +227,21 @@
       PathFragment linkTarget,
       String metadata,
       PathFragment destPath,
+      boolean isGenerated,
+      FileArtifactValue targetArtifactValue,
       Map<PathFragment, FilesetOutputSymlink> result) {
     linkName = destPath.getRelative(linkName);
     if (!result.containsKey(linkName)) {
-      result.put(linkName, new FilesetOutputSymlink(linkName, linkTarget, metadata));
-    }
-  }
-
-  private static Set<String> createExclusionSet(Set<String> input) {
-    return Sets.filter(input, new Predicate<String>() {
-      @Override
-      public boolean apply(String e) {
-        // Keep the top-level exclusions only. Do not look for "/" but count the path segments
-        // instead, in anticipation of future Windows support.
-        return PathFragment.create(e).segmentCount() == 1;
+      if (isGenerated) {
+        result.put(
+            linkName,
+            FilesetOutputSymlink.createForDerivedTarget(
+                linkName, linkTarget, metadata, targetArtifactValue));
+      } else {
+        result.put(
+            linkName, FilesetOutputSymlink.createForSourceTarget(linkName, linkTarget, metadata));
       }
-    });
+    }
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
index d91e46e..818d227 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
@@ -203,13 +203,19 @@
     final FileStateValue metadata;
     @Nullable final RootedPath realPath;
     @Nullable final PathFragment unresolvedSymlinkTarget;
+    @Nullable final FileArtifactValue fileArtifactValue;
 
-    FileInfo(FileType type, FileStateValue metadata, @Nullable RootedPath realPath,
-        @Nullable PathFragment unresolvedSymlinkTarget) {
+    FileInfo(
+        FileType type,
+        FileStateValue metadata,
+        @Nullable RootedPath realPath,
+        @Nullable PathFragment unresolvedSymlinkTarget,
+        @Nullable FileArtifactValue fileArtifactValue) {
       this.type = Preconditions.checkNotNull(type);
       this.metadata = Preconditions.checkNotNull(metadata);
       this.realPath = realPath;
       this.unresolvedSymlinkTarget = unresolvedSymlinkTarget;
+      this.fileArtifactValue = fileArtifactValue;
     }
 
     @Override
@@ -226,7 +232,7 @@
   private static FileInfo lookUpFileInfo(Environment env, TraversalRequest traversal)
       throws MissingDepException, IOException, InterruptedException {
     if (traversal.isRootGenerated) {
-      byte[] digest = null;
+      FileArtifactValue fsVal = null;
       if (traversal.root.getOutputArtifact() != null) {
         Artifact artifact = traversal.root.getOutputArtifact();
         SkyKey artifactKey = ArtifactSkyKey.key(artifact, true);
@@ -236,11 +242,9 @@
         }
 
         if (value instanceof FileArtifactValue) {
-          FileArtifactValue fsVal = (FileArtifactValue) value;
-          digest = fsVal.getDigest();
+          fsVal = (FileArtifactValue) value;
         } else {
-          return new FileInfo(
-              FileType.NONEXISTENT, null, null, null);
+          return new FileInfo(FileType.NONEXISTENT, null, null, null, null);
         }
       }
 
@@ -269,10 +273,15 @@
             path.resolveSymbolicLinks());
         type = followStat.isFile() ? FileType.SYMLINK_TO_FILE : FileType.SYMLINK_TO_DIRECTORY;
       }
-      return new FileInfo(type,
-          FileStateValue.createWithStatNoFollow(traversal.root.asRootedPath(),
-              new StatWithDigest(noFollowStat, digest), null),
-          realPath, unresolvedLinkTarget);
+      return new FileInfo(
+          type,
+          FileStateValue.createWithStatNoFollow(
+              traversal.root.asRootedPath(),
+              new StatWithDigest(noFollowStat, fsVal != null ? fsVal.getDigest() : null),
+              null),
+          realPath,
+          unresolvedLinkTarget,
+          fsVal);
     } else {
       // Stat the file.
       FileValue fileValue =
@@ -292,14 +301,20 @@
         } else {
           type = fileValue.isDirectory() ? FileType.DIRECTORY : FileType.FILE;
         }
-        return new FileInfo(type, fileValue.realFileStateValue(),
-            fileValue.realRootedPath(), unresolvedLinkTarget);
+        return new FileInfo(
+            type,
+            fileValue.realFileStateValue(),
+            fileValue.realRootedPath(),
+            unresolvedLinkTarget,
+            null);
       } else {
         // If it doesn't exist, or it's a dangling symlink, we still want to handle that gracefully.
         return new FileInfo(
             fileValue.isSymlink() ? FileType.DANGLING_SYMLINK : FileType.NONEXISTENT,
-            fileValue.realFileStateValue(), null,
-            fileValue.isSymlink() ? fileValue.getUnresolvedLinkTarget() : null);
+            fileValue.realFileStateValue(),
+            null,
+            fileValue.isSymlink() ? fileValue.getUnresolvedLinkTarget() : null,
+            null);
       }
     }
   }
@@ -436,7 +451,8 @@
     Preconditions.checkState(info.type.isSymlink() && !info.type.exists(), "{%s} {%s}", linkName,
         info.type);
     return RecursiveFilesystemTraversalValue.of(
-        ResolvedFileFactory.danglingSymlink(linkName, info.unresolvedSymlinkTarget, info.metadata));
+        ResolvedFileFactory.danglingSymlink(
+            linkName, info.unresolvedSymlinkTarget, info.metadata, info.fileArtifactValue));
   }
 
   /**
@@ -452,10 +468,14 @@
     if (info.type.isSymlink()) {
       return RecursiveFilesystemTraversalValue.of(
           ResolvedFileFactory.symlinkToFile(
-              info.realPath, path, info.unresolvedSymlinkTarget, info.metadata));
+              info.realPath,
+              path,
+              info.unresolvedSymlinkTarget,
+              info.metadata,
+              info.fileArtifactValue));
     } else {
       return RecursiveFilesystemTraversalValue.of(
-          ResolvedFileFactory.regularFile(path, info.metadata));
+          ResolvedFileFactory.regularFile(path, info.metadata, info.fileArtifactValue));
     }
   }
 
@@ -474,7 +494,8 @@
               rootInfo.realPath,
               traversal.root.asRootedPath(),
               rootInfo.unresolvedSymlinkTarget,
-              hashDirectorySymlink(children, rootInfo.metadata.hashCode()));
+              hashDirectorySymlink(children, rootInfo.metadata.hashCode()),
+              rootInfo.fileArtifactValue);
       paths = NestedSetBuilder.<ResolvedFile>stableOrder().addTransitive(children).add(root);
     } else {
       root = ResolvedFileFactory.directory(rootInfo.realPath);
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalValue.java
index 05bce4b..0b91f51 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalValue.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Interner;
+import com.google.devtools.build.lib.actions.FileArtifactValue;
 import com.google.devtools.build.lib.actions.FileStateValue;
 import com.google.devtools.build.lib.actions.FilesetTraversalParams.DirectTraversalRoot;
 import com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode;
@@ -262,16 +263,19 @@
   private static final class RegularFile implements ResolvedFile {
     private final RootedPath path;
     @Nullable private final FileStateValue metadata;
+    @Nullable private final FileArtifactValue artifactValue;
 
     /** C'tor for {@link #stripMetadataForTesting()}. */
     private RegularFile(RootedPath path) {
       this.path = Preconditions.checkNotNull(path);
       this.metadata = null;
+      this.artifactValue = null;
     }
 
-    RegularFile(RootedPath path, FileStateValue metadata) {
+    RegularFile(RootedPath path, FileStateValue metadata, FileArtifactValue artifactValue) {
       this.path = Preconditions.checkNotNull(path);
       this.metadata = Preconditions.checkNotNull(metadata);
+      this.artifactValue = artifactValue;
     }
 
     @Override
@@ -323,6 +327,11 @@
     }
 
     @Override
+    public FileArtifactValue getValueForDerivedArtifacts() {
+      return artifactValue;
+    }
+
+    @Override
     public PathFragment getTargetInSymlinkTree(boolean followSymlinks) {
       return path.asPath().asFragment();
     }
@@ -382,6 +391,11 @@
     }
 
     @Override
+    public FileArtifactValue getValueForDerivedArtifacts() {
+      return null;
+    }
+
+    @Override
     public PathFragment getTargetInSymlinkTree(boolean followSymlinks) {
       return path.asPath().asFragment();
     }
@@ -390,21 +404,28 @@
   private static final class DanglingSymlink implements ResolvedFile {
     private final Symlink symlink;
     @Nullable private final FileStateValue metadata;
+    @Nullable private final FileArtifactValue artifactValue;
 
     private DanglingSymlink(Symlink symlink) {
       this.symlink = symlink;
       this.metadata = null;
+      this.artifactValue = null;
     }
 
     private DanglingSymlink(RootedPath linkNamePath, PathFragment linkTargetPath) {
       this.symlink = new Symlink(linkNamePath, linkTargetPath);
       this.metadata = null;
+      this.artifactValue = null;
     }
 
-    DanglingSymlink(RootedPath linkNamePath, PathFragment linkTargetPath,
-        FileStateValue metadata) {
+    DanglingSymlink(
+        RootedPath linkNamePath,
+        PathFragment linkTargetPath,
+        FileStateValue metadata,
+        @Nullable FileArtifactValue fileArtifactValue) {
       this.symlink = new Symlink(linkNamePath, linkTargetPath);
       this.metadata = Preconditions.checkNotNull(metadata);
+      this.artifactValue = fileArtifactValue;
     }
 
     @Override
@@ -457,6 +478,11 @@
     }
 
     @Override
+    public FileArtifactValue getValueForDerivedArtifacts() {
+      return artifactValue;
+    }
+
+    @Override
     public PathFragment getTargetInSymlinkTree(boolean followSymlinks)
         throws DanglingSymlinkException {
       if (followSymlinks) {
@@ -472,12 +498,14 @@
     private final RootedPath path;
     @Nullable private final FileStateValue metadata;
     private final Symlink symlink;
+    @Nullable private final FileArtifactValue fileArtifactValue;
 
     /** C'tor for {@link #stripMetadataForTesting()}. */
     private SymlinkToFile(RootedPath targetPath, Symlink symlink) {
       this.path = Preconditions.checkNotNull(targetPath);
       this.metadata = null;
       this.symlink = Preconditions.checkNotNull(symlink);
+      this.fileArtifactValue = null;
     }
 
     private SymlinkToFile(
@@ -485,13 +513,19 @@
       this.path = Preconditions.checkNotNull(targetPath);
       this.metadata = null;
       this.symlink = new Symlink(linkNamePath, linkTargetPath);
+      this.fileArtifactValue = null;
     }
 
-    SymlinkToFile(RootedPath targetPath, RootedPath linkNamePath,
-        PathFragment linkTargetPath, FileStateValue metadata) {
+    SymlinkToFile(
+        RootedPath targetPath,
+        RootedPath linkNamePath,
+        PathFragment linkTargetPath,
+        FileStateValue metadata,
+        @Nullable FileArtifactValue fileArtifactValue) {
       this.path = Preconditions.checkNotNull(targetPath);
       this.metadata = Preconditions.checkNotNull(metadata);
       this.symlink = new Symlink(linkNamePath, linkTargetPath);
+      this.fileArtifactValue = fileArtifactValue;
     }
 
     @Override
@@ -544,6 +578,11 @@
     }
 
     @Override
+    public FileArtifactValue getValueForDerivedArtifacts() {
+      return fileArtifactValue;
+    }
+
+    @Override
     public PathFragment getTargetInSymlinkTree(boolean followSymlinks) {
       return followSymlinks ? path.asPath().asFragment() : symlink.unresolvedLinkTarget;
     }
@@ -553,12 +592,14 @@
     private final RootedPath path;
     @Nullable private final int metadataHash;
     private final Symlink symlink;
+    @Nullable private final FileArtifactValue fileArtifactValue;
 
     /** C'tor for {@link #stripMetadataForTesting()}. */
     private SymlinkToDirectory(RootedPath targetPath, Symlink symlink) {
       this.path = Preconditions.checkNotNull(targetPath);
       this.metadataHash = 0;
       this.symlink = symlink;
+      this.fileArtifactValue = null;
     }
 
     private SymlinkToDirectory(
@@ -566,13 +607,19 @@
       this.path = Preconditions.checkNotNull(targetPath);
       this.metadataHash = 0;
       this.symlink = new Symlink(linkNamePath, linkValue);
+      this.fileArtifactValue = null;
     }
 
-    SymlinkToDirectory(RootedPath targetPath, RootedPath linkNamePath,
-        PathFragment linkValue, int metadataHash) {
+    SymlinkToDirectory(
+        RootedPath targetPath,
+        RootedPath linkNamePath,
+        PathFragment linkValue,
+        int metadataHash,
+        @Nullable FileArtifactValue fileArtifactValue) {
       this.path = Preconditions.checkNotNull(targetPath);
       this.metadataHash = metadataHash;
       this.symlink = new Symlink(linkNamePath, linkValue);
+      this.fileArtifactValue = fileArtifactValue;
     }
 
     @Override
@@ -625,6 +672,11 @@
     }
 
     @Override
+    public FileArtifactValue getValueForDerivedArtifacts() {
+      return fileArtifactValue;
+    }
+
+    @Override
     public PathFragment getTargetInSymlinkTree(boolean followSymlinks) {
       return followSymlinks ? path.asPath().asFragment() : symlink.unresolvedLinkTarget;
     }
@@ -633,27 +685,41 @@
   static final class ResolvedFileFactory {
     private ResolvedFileFactory() {}
 
-    public static ResolvedFile regularFile(RootedPath path, FileStateValue metadata) {
-      return new RegularFile(path, metadata);
+    public static ResolvedFile regularFile(
+        RootedPath path, FileStateValue metadata, FileArtifactValue fileArtifactValue) {
+      return new RegularFile(path, metadata, fileArtifactValue);
     }
 
     public static ResolvedFile directory(RootedPath path) {
       return new Directory(path);
     }
 
-    public static ResolvedFile symlinkToFile(RootedPath targetPath, RootedPath linkNamePath,
-        PathFragment linkTargetPath, FileStateValue metadata) {
-      return new SymlinkToFile(targetPath, linkNamePath, linkTargetPath, metadata);
+    public static ResolvedFile symlinkToFile(
+        RootedPath targetPath,
+        RootedPath linkNamePath,
+        PathFragment linkTargetPath,
+        FileStateValue metadata,
+        FileArtifactValue fileArtifactValue) {
+      return new SymlinkToFile(
+          targetPath, linkNamePath, linkTargetPath, metadata, fileArtifactValue);
     }
 
-    public static ResolvedFile symlinkToDirectory(RootedPath targetPath,
-        RootedPath linkNamePath, PathFragment linkValue, int metadataHash) {
-      return new SymlinkToDirectory(targetPath, linkNamePath, linkValue, metadataHash);
+    public static ResolvedFile symlinkToDirectory(
+        RootedPath targetPath,
+        RootedPath linkNamePath,
+        PathFragment linkValue,
+        int metadataHash,
+        FileArtifactValue fileArtifactValue) {
+      return new SymlinkToDirectory(
+          targetPath, linkNamePath, linkValue, metadataHash, fileArtifactValue);
     }
 
-    public static ResolvedFile danglingSymlink(RootedPath linkNamePath, PathFragment linkValue,
-        FileStateValue metadata) {
-      return new DanglingSymlink(linkNamePath, linkValue, metadata);
+    public static ResolvedFile danglingSymlink(
+        RootedPath linkNamePath,
+        PathFragment linkValue,
+        FileStateValue metadata,
+        FileArtifactValue fileArtifactValue) {
+      return new DanglingSymlink(linkNamePath, linkValue, metadata, fileArtifactValue);
     }
   }
 
@@ -726,6 +792,9 @@
      */
     PathFragment getTargetInSymlinkTree(boolean followSymlinks) throws DanglingSymlinkException;
 
+    @Nullable
+    FileArtifactValue getValueForDerivedArtifacts();
+
     /**
      * Returns a copy of this object with the metadata stripped away.
      *
diff --git a/src/test/java/com/google/devtools/build/lib/exec/SpawnInputExpanderTest.java b/src/test/java/com/google/devtools/build/lib/exec/SpawnInputExpanderTest.java
index 67be7fa..e21fdbb 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/SpawnInputExpanderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/exec/SpawnInputExpanderTest.java
@@ -271,7 +271,8 @@
   }
 
   private FilesetOutputSymlink filesetSymlink(String from, String to) {
-    return new FilesetOutputSymlink(PathFragment.create(from), PathFragment.create(to));
+    return FilesetOutputSymlink.createForTesting(
+        PathFragment.create(from), PathFragment.create(to));
   }
 
   private ImmutableMap<PathFragment, ImmutableList<FilesetOutputSymlink>> simpleFilesetManifest() {
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunctionTest.java
index fafe4b7..247b4dc 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunctionTest.java
@@ -192,15 +192,18 @@
   }
 
   private static FilesetOutputSymlink symlink(String from, Artifact to) {
-    return new FilesetOutputSymlink(PathFragment.create(from), to.getPath().asFragment());
+    return FilesetOutputSymlink.createForTesting(
+        PathFragment.create(from), to.getPath().asFragment());
   }
 
   private static FilesetOutputSymlink symlink(String from, String to) {
-    return new FilesetOutputSymlink(PathFragment.create(from), PathFragment.create(to));
+    return FilesetOutputSymlink.createForTesting(
+        PathFragment.create(from), PathFragment.create(to));
   }
 
   private static FilesetOutputSymlink symlink(String from, RootedPath to) {
-    return new FilesetOutputSymlink(PathFragment.create(from), to.asPath().asFragment());
+    return FilesetOutputSymlink.createForTesting(
+        PathFragment.create(from), to.asPath().asFragment());
   }
 
   private void assertSymlinksCreatedInOrder(
@@ -210,7 +213,8 @@
         Collections2.transform(
             evalFilesetTraversal(request).getSymlinks(),
             // Strip the metadata from the actual results.
-            (input) -> new FilesetOutputSymlink(input.name, input.target));
+            (input) ->
+                FilesetOutputSymlink.createForTesting(input.getName(), input.getTargetPath()));
     assertThat(actual).containsExactlyElementsIn(expected).inOrder();
   }