diff --git a/src/main/java/com/google/devtools/build/lib/exec/CompactSpawnLogContext.java b/src/main/java/com/google/devtools/build/lib/exec/CompactSpawnLogContext.java
index 93eab45bb15538..dbfc6bdc4767f5 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/CompactSpawnLogContext.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/CompactSpawnLogContext.java
@@ -70,7 +70,7 @@ private interface ExecLogEntrySupplier {
private final XattrProvider xattrProvider;
// Maps a key identifying an entry into its ID.
- // Each key is either a NestedSet.Node or the String path of a file or directory.
+ // Each key is either a NestedSet.Node or the String path of a file, directory or symlink.
// Only entries that are likely to be referenced by future entries are stored.
// Use a specialized map for minimal memory footprint.
@GuardedBy("this")
@@ -143,17 +143,21 @@ public void logSpawn(
for (ActionInput output : spawn.getOutputFiles()) {
Path path = fileSystem.getPath(execRoot.getRelative(output.getExecPath()));
- if (!path.exists()) {
+ if (!output.isDirectory() && !output.isSymlink() && path.isFile()) {
builder.addOutputs(
- ExecLogEntry.Output.newBuilder().setMissingPath(output.getExecPathString()));
- } else if (path.isDirectory()) {
+ ExecLogEntry.Output.newBuilder()
+ .setFileId(logFile(output, path, inputMetadataProvider)));
+ } else if (output.isDirectory() && path.isDirectory()) {
builder.addOutputs(
ExecLogEntry.Output.newBuilder()
.setDirectoryId(logDirectory(output, path, inputMetadataProvider)));
- } else {
+ } else if (output.isSymlink() && path.isSymbolicLink()) {
builder.addOutputs(
ExecLogEntry.Output.newBuilder()
- .setFileId(logFile(output, path, inputMetadataProvider)));
+ .setUnresolvedSymlinkId(logUnresolvedSymlink(output, path)));
+ } else {
+ builder.addOutputs(
+ ExecLogEntry.Output.newBuilder().setInvalidOutputPath(output.getExecPathString()));
}
}
@@ -335,12 +339,12 @@ private int logFile(ActionInput input, Path path, InputMetadataProvider inputMet
/**
* Logs a directory.
*
- *
This may be either a source directory, a fileset or an output directory. An output directory
- * is not guaranteed to be a tree artifact; we must support the case where a directory is produced
- * when a file was expected. For runfiles, {@link #logRunfilesDirectory} must be used instead.
+ *
This may be either a source directory, a fileset or an output directory. For runfiles,
+ * {@link #logRunfilesDirectory} must be used instead.
*
- * @param input the input representing the directory
- * @param root the path to the directory
+ * @param input the input representing the directory.
+ * @param root the path to the directory, which must have already been verified to be of the
+ * correct type.
* @return the entry ID of the {@link ExecLogEntry.Directory} describing the directory.
*/
private int logDirectory(
@@ -455,6 +459,26 @@ private ImmutableList expandDirectory(
return builder.build();
}
+ /**
+ * Logs an unresolved symlink.
+ *
+ * @param input the input representing the unresolved symlink.
+ * @param path the path to the unresolved symlink, which must have already been verified to be of
+ * the correct type.
+ * @return the entry ID of the {@link ExecLogEntry.UnresolvedSymlink} describing the unresolved
+ * symlink.
+ */
+ private int logUnresolvedSymlink(ActionInput input, Path path) throws IOException {
+ return logEntry(
+ input.getExecPathString(),
+ () ->
+ ExecLogEntry.newBuilder()
+ .setUnresolvedSymlink(
+ ExecLogEntry.UnresolvedSymlink.newBuilder()
+ .setPath(input.getExecPathString())
+ .setTargetPath(path.readSymbolicLink().getPathString())));
+ }
+
/**
* Ensures an entry is written to the log and returns its assigned ID.
*
diff --git a/src/main/java/com/google/devtools/build/lib/exec/ExpandedSpawnLogContext.java b/src/main/java/com/google/devtools/build/lib/exec/ExpandedSpawnLogContext.java
index be90501d3496d9..6054e5d7f00669 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/ExpandedSpawnLogContext.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/ExpandedSpawnLogContext.java
@@ -24,6 +24,7 @@
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.FileArtifactValue.UnresolvedSymlinkArtifactValue;
import com.google.devtools.build.lib.actions.InputMetadataProvider;
import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.actions.SpawnResult;
@@ -56,7 +57,6 @@
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
-import java.util.TreeMap;
import java.util.function.Consumer;
import javax.annotation.Nullable;
@@ -144,7 +144,6 @@ public void logSpawn(
SpawnResult result)
throws IOException, ExecException {
try (SilentCloseable c = Profiler.instance().profile("logSpawn")) {
- SortedMap existingOutputs = listExistingOutputs(spawn, fileSystem);
SpawnExec.Builder builder = SpawnExec.newBuilder();
builder.addAllCommandArgs(spawn.getArguments());
builder.addAllEnvironmentVariables(getEnvironmentVariables(spawn));
@@ -175,6 +174,16 @@ public void logSpawn(
continue;
}
+ if (input.isSymlink()) {
+ UnresolvedSymlinkArtifactValue metadata =
+ (UnresolvedSymlinkArtifactValue) inputMetadataProvider.getInputMetadata(input);
+ builder
+ .addInputsBuilder()
+ .setPath(displayPath.getPathString())
+ .setSymlinkTargetPath(metadata.getSymlinkTarget());
+ continue;
+ }
+
Digest digest =
computeDigest(
input,
@@ -201,29 +210,33 @@ public void logSpawn(
Collections.sort(outputPaths);
builder.addAllListedOutputs(outputPaths);
try {
- for (Map.Entry e : existingOutputs.entrySet()) {
- Path path = e.getKey();
- ActionInput output = e.getValue();
- if (path.isDirectory()) {
+ for (ActionInput output : spawn.getOutputFiles()) {
+ Path path = fileSystem.getPath(execRoot.getRelative(output.getExecPathString()));
+ if (!output.isDirectory() && !output.isSymlink() && path.isFile()) {
+ builder
+ .addActualOutputsBuilder()
+ .setPath(output.getExecPathString())
+ .setDigest(
+ computeDigest(
+ output,
+ path,
+ inputMetadataProvider,
+ xattrProvider,
+ digestHashFunction,
+ /* includeHashFunctionName= */ true));
+ } else if (output.isDirectory() && path.isDirectory()) {
listDirectoryContents(
output.getExecPath(),
path,
builder::addActualOutputs,
inputMetadataProvider,
/* isTool= */ false);
- continue;
+ } else if (output.isSymlink() && path.isSymbolicLink()) {
+ builder
+ .addActualOutputsBuilder()
+ .setPath(output.getExecPathString())
+ .setSymlinkTargetPath(path.readSymbolicLink().getPathString());
}
- builder
- .addActualOutputsBuilder()
- .setPath(output.getExecPathString())
- .setDigest(
- computeDigest(
- output,
- path,
- inputMetadataProvider,
- xattrProvider,
- digestHashFunction,
- /* includeHashFunctionName= */ true));
}
} catch (IOException ex) {
logger.atWarning().withCause(ex).log("Error computing spawn output properties");
@@ -292,18 +305,6 @@ public void close() throws IOException {
}
}
- private SortedMap listExistingOutputs(Spawn spawn, FileSystem fileSystem) {
- TreeMap result = new TreeMap<>();
- for (ActionInput output : spawn.getOutputFiles()) {
- Path outputPath = fileSystem.getPath(execRoot.getRelative(output.getExecPathString()));
- // TODO(olaola): once symlink API proposal is implemented, report symlinks here.
- if (outputPath.exists()) {
- result.put(outputPath, output);
- }
- }
- return result;
- }
-
/**
* Expands a directory into its contents.
*
@@ -318,7 +319,6 @@ private void listDirectoryContents(
InputMetadataProvider inputMetadataProvider,
boolean isTool)
throws IOException {
- // TODO(olaola): once symlink API proposal is implemented, report symlinks here.
List sortedDirent = new ArrayList<>(contentPath.readdir(Symlinks.NOFOLLOW));
sortedDirent.sort(Comparator.comparing(Dirent::getName));
diff --git a/src/main/protobuf/spawn.proto b/src/main/protobuf/spawn.proto
index d5c3d21c9b04aa..fe01830f6f06fc 100644
--- a/src/main/protobuf/spawn.proto
+++ b/src/main/protobuf/spawn.proto
@@ -42,8 +42,12 @@ message File {
// Path to the file relative to the execution root.
string path = 1;
+ // Symlink target path.
+ // Only set for unresolved symlinks.
+ string symlink_target_path = 4;
+
// File digest.
- // May be omitted for empty files.
+ // Always omitted for unresolved symlinks. May be omitted for empty files.
Digest digest = 2;
// Whether the file is a tool.
@@ -147,6 +151,8 @@ message SpawnExec {
string mnemonic = 10;
// The outputs generated by the execution.
+ // In order for one of the listed_outputs to appear here, it must have been
+ // produced and have the expected type (file, directory or symlink).
repeated File actual_outputs = 11;
// If the spawn did not hit a disk or remote cache, this will be the name of
@@ -225,6 +231,7 @@ message ExecLogEntry {
}
// An input or output directory.
+ // May be a source directory, a runfiles or fileset tree, or a tree artifact.
message Directory {
// The directory path.
string path = 1;
@@ -232,6 +239,14 @@ message ExecLogEntry {
repeated File files = 2;
}
+ // An unresolved symlink.
+ message UnresolvedSymlink {
+ // The symlink path.
+ string path = 1;
+ // The path the symlink points to.
+ string target_path = 2;
+ }
+
// A set of spawn inputs.
// The contents of the set are the directly referenced files and directories
// in addition to the contents of all transitively referenced sets. Sets are
@@ -249,12 +264,15 @@ message ExecLogEntry {
// A spawn output.
message Output {
oneof type {
- // An output file.
+ // An output file, i.e., ctx.actions.declare_file.
int32 file_id = 1;
- // An output directory.
+ // An output directory, i.e., ctx.actions.declare_directory.
int32 directory_id = 2;
- // A declared output that was not produced.
- string missing_path = 3;
+ // An output unresolved symlink, i.e., ctx.actions.declare_symlink.
+ int32 unresolved_symlink_id = 3;
+ // A declared output that is either missing or has the wrong type
+ // (e.g., a file where a directory was expected).
+ string invalid_output_path = 4;
}
}
@@ -325,7 +343,8 @@ message ExecLogEntry {
Invocation invocation = 2;
File file = 3;
Directory directory = 4;
- InputSet input_set = 5;
- Spawn spawn = 6;
+ UnresolvedSymlink unresolved_symlink = 5;
+ InputSet input_set = 6;
+ Spawn spawn = 7;
}
}
diff --git a/src/test/java/com/google/devtools/build/lib/exec/CompactSpawnLogContextTest.java b/src/test/java/com/google/devtools/build/lib/exec/CompactSpawnLogContextTest.java
index a60379acd31ed7..69dd8dfeb168ed 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/CompactSpawnLogContextTest.java
+++ b/src/test/java/com/google/devtools/build/lib/exec/CompactSpawnLogContextTest.java
@@ -198,8 +198,14 @@ private SpawnExec reconstructSpawnExec(
builder.addActualOutputs(reconstructFile(dir, dirFile, hashFunctionName));
}
break;
- case MISSING_PATH:
- builder.addListedOutputs(output.getMissingPath());
+ case UNRESOLVED_SYMLINK_ID:
+ ExecLogEntry.UnresolvedSymlink symlink =
+ checkNotNull(entryMap.get(output.getUnresolvedSymlinkId())).getUnresolvedSymlink();
+ builder.addListedOutputs(symlink.getPath());
+ builder.addActualOutputs(reconstructSymlink(symlink));
+ break;
+ case INVALID_OUTPUT_PATH:
+ builder.addListedOutputs(output.getInvalidOutputPath());
break;
case TYPE_NOT_SET:
throw new AssertionError("malformed log entry");
@@ -252,4 +258,11 @@ private File reconstructFile(
return builder.build();
}
+
+ private File reconstructSymlink(ExecLogEntry.UnresolvedSymlink symlink) {
+ return File.newBuilder()
+ .setPath(symlink.getPath())
+ .setSymlinkTargetPath(symlink.getTargetPath())
+ .build();
+ }
}
diff --git a/src/test/java/com/google/devtools/build/lib/exec/SpawnLogContextTestBase.java b/src/test/java/com/google/devtools/build/lib/exec/SpawnLogContextTestBase.java
index bf414f1cbabb5c..3524301a02dd66 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/SpawnLogContextTestBase.java
+++ b/src/test/java/com/google/devtools/build/lib/exec/SpawnLogContextTestBase.java
@@ -124,6 +124,16 @@ boolean isEmpty() {
}
}
+ /** Test parameter determining whether an output is indirected through a symlink. */
+ enum OutputIndirection {
+ DIRECT,
+ INDIRECT;
+
+ boolean viaSymlink() {
+ return this == INDIRECT;
+ }
+ }
+
@Test
public void testFileInput(@TestParameter InputsMode inputsMode) throws Exception {
Artifact fileInput = ActionsTestUtil.createArtifact(rootDir, "file");
@@ -384,10 +394,22 @@ public void testFilesetInput(@TestParameter DirContents dirContents) throws Exce
}
@Test
- public void testFileOutput(@TestParameter OutputsMode outputsMode) throws Exception {
+ public void testFileOutput(
+ @TestParameter OutputsMode outputsMode, @TestParameter OutputIndirection indirection)
+ throws Exception {
Artifact fileOutput = ActionsTestUtil.createArtifact(outputDir, "file");
- writeFile(fileOutput, "abc");
+ Path actualPath =
+ indirection.viaSymlink()
+ ? outputDir.getRoot().asPath().getChild("actual")
+ : fileOutput.getPath();
+
+ if (indirection.viaSymlink()) {
+ fileOutput.getPath().getParentDirectory().createDirectoryAndParents();
+ fileOutput.getPath().createSymbolicLink(actualPath);
+ }
+
+ writeFile(actualPath, "abc");
Spawn spawn = defaultSpawnBuilder().withOutputs(fileOutput).build();
@@ -410,17 +432,13 @@ public void testFileOutput(@TestParameter OutputsMode outputsMode) throws Except
}
@Test
- public void testDirectoryOutput(
- @TestParameter OutputsMode outputsMode, @TestParameter DirContents dirContents)
+ public void testFileOutputWithInvalidType(@TestParameter OutputsMode outputsMode)
throws Exception {
- Artifact dirOutput = ActionsTestUtil.createArtifact(outputDir, "dir");
+ Artifact fileOutput = ActionsTestUtil.createArtifact(outputDir, "file");
- dirOutput.getPath().createDirectoryAndParents();
- if (!dirContents.isEmpty()) {
- writeFile(dirOutput.getPath().getChild("file"), "abc");
- }
+ fileOutput.getPath().createDirectoryAndParents();
- SpawnBuilder spawn = defaultSpawnBuilder().withOutputs(dirOutput);
+ SpawnBuilder spawn = defaultSpawnBuilder().withOutputs(fileOutput);
SpawnLogContext context = createSpawnLogContext();
@@ -432,31 +450,31 @@ public void testDirectoryOutput(
defaultTimeout(),
defaultSpawnResult());
- closeAndAssertLog(
- context,
- defaultSpawnExecBuilder()
- .addListedOutputs("out/dir")
- .addAllActualOutputs(
- dirContents.isEmpty()
- ? ImmutableList.of()
- : ImmutableList.of(
- File.newBuilder()
- .setPath("out/dir/file")
- .setDigest(getDigest("abc"))
- .build()))
- .build());
+ closeAndAssertLog(context, defaultSpawnExecBuilder().addListedOutputs("out/file").build());
}
@Test
public void testTreeOutput(
- @TestParameter OutputsMode outputsMode, @TestParameter DirContents dirContents)
+ @TestParameter OutputsMode outputsMode,
+ @TestParameter DirContents dirContents,
+ @TestParameter OutputIndirection indirection)
throws Exception {
SpecialArtifact treeOutput =
ActionsTestUtil.createTreeArtifactWithGeneratingAction(outputDir, "tree");
- treeOutput.getPath().createDirectoryAndParents();
+ Path actualPath =
+ indirection.viaSymlink()
+ ? outputDir.getRoot().asPath().getChild("actual")
+ : treeOutput.getPath();
+
+ if (indirection.viaSymlink()) {
+ treeOutput.getPath().getParentDirectory().createDirectoryAndParents();
+ treeOutput.getPath().createSymbolicLink(actualPath);
+ }
+
+ actualPath.createDirectoryAndParents();
if (!dirContents.isEmpty()) {
- writeFile(treeOutput.getPath().getChild("child"), "abc");
+ writeFile(actualPath.getChild("child"), "abc");
}
Spawn spawn = defaultSpawnBuilder().withOutputs(treeOutput).build();
@@ -486,6 +504,78 @@ public void testTreeOutput(
.build());
}
+ @Test
+ public void testTreeOutputWithInvalidType(@TestParameter OutputsMode outputsMode)
+ throws Exception {
+ Artifact treeOutput = ActionsTestUtil.createTreeArtifactWithGeneratingAction(outputDir, "tree");
+
+ writeFile(treeOutput, "abc");
+
+ SpawnBuilder spawn = defaultSpawnBuilder().withOutputs(treeOutput);
+
+ SpawnLogContext context = createSpawnLogContext();
+
+ context.logSpawn(
+ spawn.build(),
+ createInputMetadataProvider(),
+ createInputMap(),
+ outputsMode.getActionFileSystem(fs),
+ defaultTimeout(),
+ defaultSpawnResult());
+
+ closeAndAssertLog(context, defaultSpawnExecBuilder().addListedOutputs("out/tree").build());
+ }
+
+ @Test
+ public void testUnresolvedSymlinkOutput(@TestParameter OutputsMode outputsMode) throws Exception {
+ Artifact symlinkOutput = ActionsTestUtil.createUnresolvedSymlinkArtifact(outputDir, "symlink");
+
+ symlinkOutput.getPath().getParentDirectory().createDirectoryAndParents();
+ symlinkOutput.getPath().createSymbolicLink(PathFragment.create("/some/path"));
+
+ SpawnBuilder spawn = defaultSpawnBuilder().withOutputs(symlinkOutput);
+
+ SpawnLogContext context = createSpawnLogContext();
+
+ context.logSpawn(
+ spawn.build(),
+ createInputMetadataProvider(),
+ createInputMap(),
+ outputsMode.getActionFileSystem(fs),
+ defaultTimeout(),
+ defaultSpawnResult());
+
+ closeAndAssertLog(
+ context,
+ defaultSpawnExecBuilder()
+ .addListedOutputs("out/symlink")
+ .addActualOutputs(
+ File.newBuilder().setPath("out/symlink").setSymlinkTargetPath("/some/path"))
+ .build());
+ }
+
+ @Test
+ public void testUnresolvedSymlinkOutputWithInvalidType(@TestParameter OutputsMode outputsMode)
+ throws Exception {
+ Artifact symlinkOutput = ActionsTestUtil.createUnresolvedSymlinkArtifact(outputDir, "symlink");
+
+ writeFile(symlinkOutput, "abc");
+
+ SpawnBuilder spawn = defaultSpawnBuilder().withOutputs(symlinkOutput);
+
+ SpawnLogContext context = createSpawnLogContext();
+
+ context.logSpawn(
+ spawn.build(),
+ createInputMetadataProvider(),
+ createInputMap(),
+ outputsMode.getActionFileSystem(fs),
+ defaultTimeout(),
+ defaultSpawnResult());
+
+ closeAndAssertLog(context, defaultSpawnExecBuilder().addListedOutputs("out/symlink").build());
+ }
+
@Test
public void testMissingOutput(@TestParameter OutputsMode outputsMode) throws Exception {
Artifact missingOutput = ActionsTestUtil.createArtifact(outputDir, "missing");