Skip to content

Commit 5b46a48

Browse files
tjgqcopybara-github
authored andcommitted
Upload dangling symlinks found in the outputs of local actions.
Since we don't know whether a dangling symlink is supposed to point to a file or a directory, we always report it as a symlink to a file. The distinction only matters in V2 of the protocol, with V2.1 reporting them uniformly, so it doesn't seem to be worth the effort to match up the input and output types. Dangling symlinks to absolute paths are unconditionally uploaded. A followup PR will gate their upload on the presence of the respective server capability (SymlinkAbsolutePathStrategy.ALLOW). The change is behind a new --incompatible_remote_dangling_symlinks flag, which will be introduced in the flipped state. See #16353. Progress towards #16289. Closes #16352. PiperOrigin-RevId: 477735370 Change-Id: I6210d152529e4603a49377a9e72e8e116ced6b85
1 parent 6c78ed8 commit 5b46a48

File tree

6 files changed

+336
-46
lines changed

6 files changed

+336
-46
lines changed

src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
import com.google.devtools.build.lib.vfs.FileSystemUtils;
116116
import com.google.devtools.build.lib.vfs.Path;
117117
import com.google.devtools.build.lib.vfs.PathFragment;
118+
import com.google.devtools.build.lib.vfs.Symlinks;
118119
import com.google.protobuf.ByteString;
119120
import com.google.protobuf.ExtensionRegistry;
120121
import com.google.protobuf.Message;
@@ -1257,8 +1258,10 @@ private Single<UploadManifest> buildUploadManifestAsync(
12571258
ImmutableList.Builder<Path> outputFiles = ImmutableList.builder();
12581259
// Check that all mandatory outputs are created.
12591260
for (ActionInput outputFile : action.getSpawn().getOutputFiles()) {
1261+
Symlinks followSymlinks = outputFile.isSymlink() ? Symlinks.NOFOLLOW : Symlinks.FOLLOW;
12601262
Path localPath = execRoot.getRelative(outputFile.getExecPath());
1261-
if (action.getSpawn().isMandatoryOutput(outputFile) && !localPath.exists()) {
1263+
if (action.getSpawn().isMandatoryOutput(outputFile)
1264+
&& !localPath.exists(followSymlinks)) {
12621265
throw new IOException(
12631266
"Expected output " + prettyPrint(outputFile) + " was not created locally.");
12641267
}

src/main/java/com/google/devtools/build/lib/remote/UploadManifest.java

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public class UploadManifest {
7676
private final RemotePathResolver remotePathResolver;
7777
private final ActionResult.Builder result;
7878
private final boolean followSymlinks;
79+
private final boolean allowDanglingSymlinks;
7980
private final Map<Digest, Path> digestToFile = new HashMap<>();
8081
private final Map<Digest, ByteString> digestToBlobs = new HashMap<>();
8182
@Nullable private ActionKey actionKey;
@@ -103,7 +104,8 @@ public static UploadManifest create(
103104
digestUtil,
104105
remotePathResolver,
105106
result,
106-
/* followSymlinks= */ !remoteOptions.incompatibleRemoteSymlinks);
107+
/* followSymlinks= */ !remoteOptions.incompatibleRemoteSymlinks,
108+
/* allowDanglingSymlinks= */ remoteOptions.incompatibleRemoteDanglingSymlinks);
107109
manifest.addFiles(outputFiles);
108110
manifest.setStdoutStderr(outErr);
109111
manifest.addAction(actionKey, action, command);
@@ -140,11 +142,13 @@ public UploadManifest(
140142
DigestUtil digestUtil,
141143
RemotePathResolver remotePathResolver,
142144
ActionResult.Builder result,
143-
boolean followSymlinks) {
145+
boolean followSymlinks,
146+
boolean allowDanglingSymlinks) {
144147
this.digestUtil = digestUtil;
145148
this.remotePathResolver = remotePathResolver;
146149
this.result = result;
147150
this.followSymlinks = followSymlinks;
151+
this.allowDanglingSymlinks = allowDanglingSymlinks;
148152
}
149153

150154
private void setStdoutStderr(FileOutErr outErr) throws IOException {
@@ -186,25 +190,32 @@ void addFiles(Collection<Path> files) throws ExecException, IOException {
186190
// Need to resolve the symbolic link to know what to add, file or directory.
187191
FileStatus statFollow = file.statIfFound(Symlinks.FOLLOW);
188192
if (statFollow == null) {
189-
throw new IOException(
190-
String.format("Action output %s is a dangling symbolic link to %s ", file, target));
191-
}
192-
if (statFollow.isSpecialFile()) {
193-
illegalOutput(file);
194-
}
195-
Preconditions.checkState(
196-
statFollow.isFile() || statFollow.isDirectory(), "Unknown stat type for %s", file);
197-
if (!followSymlinks && !target.isAbsolute()) {
198-
if (statFollow.isFile()) {
193+
if (allowDanglingSymlinks) {
194+
// Report symlink to a file since we don't know any better.
195+
// TODO(tjgq): Check for the SymlinkAbsolutePathStrategy.ALLOW server capability before
196+
// uploading an absolute symlink.
199197
addFileSymbolicLink(file, target);
200198
} else {
201-
addDirectorySymbolicLink(file, target);
199+
throw new IOException(
200+
String.format("Action output %s is a dangling symbolic link to %s ", file, target));
202201
}
202+
} else if (statFollow.isSpecialFile()) {
203+
illegalOutput(file);
203204
} else {
204-
if (statFollow.isFile()) {
205-
addFile(digestUtil.compute(file), file);
205+
Preconditions.checkState(
206+
statFollow.isFile() || statFollow.isDirectory(), "Unknown stat type for %s", file);
207+
if (!followSymlinks && !target.isAbsolute()) {
208+
if (statFollow.isFile()) {
209+
addFileSymbolicLink(file, target);
210+
} else {
211+
addDirectorySymbolicLink(file, target);
212+
}
206213
} else {
207-
addDirectory(file);
214+
if (statFollow.isFile()) {
215+
addFile(digestUtil.compute(file), file);
216+
} else {
217+
addDirectory(file);
218+
}
208219
}
209220
}
210221
} else {

src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,18 @@ public RemoteBuildEventUploadModeConverter() {
412412
+ " represented as files or directories. See #6631 for details.")
413413
public boolean incompatibleRemoteSymlinks;
414414

415+
@Option(
416+
name = "incompatible_remote_dangling_symlinks",
417+
defaultValue = "true",
418+
category = "remote",
419+
documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
420+
effectTags = {OptionEffectTag.EXECUTION},
421+
metadataTags = {OptionMetadataTag.INCOMPATIBLE_CHANGE},
422+
help =
423+
"If set to true and --incompatible_remote_symlinks is also true, symlinks in action"
424+
+ " outputs are allowed to dangle.")
425+
public boolean incompatibleRemoteDanglingSymlinks;
426+
415427
@Option(
416428
name = "experimental_remote_cache_compression",
417429
defaultValue = "false",

src/test/java/com/google/devtools/build/lib/actions/util/ActionsTestUtil.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,12 @@ public static SpecialArtifact createTreeArtifactWithGeneratingAction(
267267
return treeArtifact;
268268
}
269269

270+
public static SpecialArtifact createUnresolvedSymlinkArtifactWithExecPath(
271+
ArtifactRoot root, PathFragment execPath) {
272+
return SpecialArtifact.create(
273+
root, execPath, NULL_ARTIFACT_OWNER, SpecialArtifactType.UNRESOLVED_SYMLINK);
274+
}
275+
270276
public static void assertNoArtifactEndingWith(RuleConfiguredTarget target, String path) {
271277
Pattern endPattern = Pattern.compile(path + "$");
272278
for (ActionAnalysisMetadata action : target.getActions()) {

src/test/java/com/google/devtools/build/lib/remote/RemoteExecutionServiceTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,6 +1367,35 @@ public void uploadOutputs_uploadNestedDirectory_works() throws Exception {
13671367
.isEmpty();
13681368
}
13691369

1370+
@Test
1371+
public void uploadOutputs_uploadRelativeDanglingSymlink_works() throws Exception {
1372+
// arrange
1373+
Path linkPath = execRoot.getRelative("outputs/link");
1374+
linkPath.createSymbolicLink(PathFragment.create("some/path"));
1375+
Artifact outputSymlink =
1376+
ActionsTestUtil.createUnresolvedSymlinkArtifactWithExecPath(
1377+
artifactRoot, linkPath.relativeTo(execRoot));
1378+
RemoteExecutionService service = newRemoteExecutionService();
1379+
Spawn spawn = newSpawn(ImmutableMap.of(), ImmutableSet.of(outputSymlink));
1380+
FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn);
1381+
RemoteAction action = service.buildRemoteAction(spawn, context);
1382+
SpawnResult spawnResult =
1383+
new SpawnResult.Builder()
1384+
.setExitCode(0)
1385+
.setStatus(SpawnResult.Status.SUCCESS)
1386+
.setRunnerName("test")
1387+
.build();
1388+
1389+
// act
1390+
UploadManifest manifest = service.buildUploadManifest(action, spawnResult);
1391+
service.uploadOutputs(action, spawnResult);
1392+
1393+
// assert
1394+
ActionResult.Builder expectedResult = ActionResult.newBuilder();
1395+
expectedResult.addOutputFileSymlinksBuilder().setPath("outputs/link").setTarget("some/path");
1396+
assertThat(manifest.getActionResult()).isEqualTo(expectedResult.build());
1397+
}
1398+
13701399
@Test
13711400
public void uploadOutputs_emptyOutputs_doNotPerformUpload() throws Exception {
13721401
// Test that uploading an empty output does not try to perform an upload.

0 commit comments

Comments
 (0)