diff --git a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java index 487bd6b8a1942d..9fd78eaa55d43d 100644 --- a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java +++ b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java @@ -305,8 +305,7 @@ void syncTreeRecursively(Path at) throws IOException { if (dirent.getType() != Dirent.Type.SYMLINK) { next.deleteTree(); } - // TODO(ulfjack): On Windows, this call makes a copy rather than creating a symlink. - FileSystemUtils.ensureSymbolicLink(next, value.getPath().asFragment()); + FileSystemUtils.ensureSymbolicLink(next, getSymlinkTargetPath(value)); } } else if (directories.containsKey(basename)) { Directory nextDir = directories.remove(basename); @@ -321,12 +320,11 @@ void syncTreeRecursively(Path at) throws IOException { for (Map.Entry entry : symlinks.entrySet()) { Path next = at.getChild(entry.getKey()); - if (entry.getValue() == null) { + Artifact value = entry.getValue(); + if (value == null) { FileSystemUtils.createEmptyFile(next); - } else if (entry.getValue().isSymlink()) { - FileSystemUtils.ensureSymbolicLink(next, entry.getValue().getPath().readSymbolicLink()); } else { - FileSystemUtils.ensureSymbolicLink(next, entry.getValue().getPath().asFragment()); + FileSystemUtils.ensureSymbolicLink(next, getSymlinkTargetPath(value)); } } for (Map.Entry entry : directories.entrySet()) { @@ -334,4 +332,12 @@ void syncTreeRecursively(Path at) throws IOException { } } } + + private static PathFragment getSymlinkTargetPath(Artifact artifact) throws IOException { + if (artifact.isSymlink()) { + // Unresolved symlinks are created textually. + return artifact.getPath().readSymbolicLink(); + } + return artifact.getPath().asFragment(); + } } diff --git a/src/test/java/com/google/devtools/build/lib/exec/SymlinkTreeHelperTest.java b/src/test/java/com/google/devtools/build/lib/exec/SymlinkTreeHelperTest.java index 3e28af0b2f5bc2..783c1fde2ccf6c 100644 --- a/src/test/java/com/google/devtools/build/lib/exec/SymlinkTreeHelperTest.java +++ b/src/test/java/com/google/devtools/build/lib/exec/SymlinkTreeHelperTest.java @@ -15,25 +15,44 @@ package com.google.devtools.build.lib.exec; import static com.google.common.truth.Truth.assertThat; +import static com.google.devtools.build.lib.testutil.TestConstants.WORKSPACE_NAME; +import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.actions.ArtifactRoot; +import com.google.devtools.build.lib.actions.ArtifactRoot.RootType; import com.google.devtools.build.lib.actions.FilesetOutputSymlink; +import com.google.devtools.build.lib.actions.util.ActionsTestUtil; import com.google.devtools.build.lib.shell.Command; import com.google.devtools.build.lib.vfs.DigestHashFunction; import com.google.devtools.build.lib.vfs.FileSystem; +import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import java.util.HashMap; import java.util.Map; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; /** Unit tests for {@link SymlinkTreeHelper}. */ -@RunWith(JUnit4.class) +@RunWith(TestParameterInjector.class) public final class SymlinkTreeHelperTest { + private final FileSystem fs = new InMemoryFileSystem(DigestHashFunction.SHA256); + private final Path execRoot = fs.getPath("/execroot"); + private final ArtifactRoot outputRoot = + ArtifactRoot.asDerivedRoot(execRoot, RootType.Output, "out"); + + @Before + public void setUp() throws Exception { + outputRoot.getRoot().asPath().createDirectoryAndParents(); + } @Test public void checkCreatedSpawn() { @@ -104,4 +123,48 @@ public void readMultilineManifest() { PathFragment.create("workspace2/rel2"), PathFragment.create("/path")); } + + @Test + public void createSymlinksDirectly(@TestParameter boolean replace) throws Exception { + Path treeRoot = execRoot.getRelative("foo.runfiles"); + Path inputManifestPath = execRoot.getRelative("foo.runfiles_manifest"); + SymlinkTreeHelper helper = + new SymlinkTreeHelper(execRoot, inputManifestPath, treeRoot, false, WORKSPACE_NAME); + + Artifact file = ActionsTestUtil.createArtifact(outputRoot, "file"); + Artifact symlink = ActionsTestUtil.createUnresolvedSymlinkArtifact(outputRoot, "symlink"); + + FileSystemUtils.writeContent(file.getPath(), UTF_8, "content"); + FileSystemUtils.ensureSymbolicLink(symlink.getPath(), "/path/to/target"); + + HashMap symlinkMap = new HashMap<>(); + symlinkMap.put(PathFragment.create(WORKSPACE_NAME + "/empty"), null); + symlinkMap.put(PathFragment.create(WORKSPACE_NAME + "/file"), file); + symlinkMap.put(PathFragment.create(WORKSPACE_NAME + "/symlink"), symlink); + + Path treeWorkspace = treeRoot.getRelative(WORKSPACE_NAME); + Path treeEmpty = treeWorkspace.getRelative("empty"); + Path treeFile = treeWorkspace.getRelative("file"); + Path treeSymlink = treeWorkspace.getRelative("symlink"); + Path treeMissing = treeWorkspace.getRelative("missing"); + + if (replace) { + treeEmpty.createDirectoryAndParents(); + treeFile.createDirectoryAndParents(); + treeSymlink.createDirectoryAndParents(); + treeMissing.createDirectoryAndParents(); + } + + helper.createSymlinksDirectly(symlinkMap); + + assertThat(treeRoot.isDirectory()).isTrue(); + assertThat(treeWorkspace.isDirectory()).isTrue(); + assertThat(treeEmpty.isFile()).isTrue(); + assertThat(FileSystemUtils.readContent(treeEmpty)).isEmpty(); + assertThat(treeFile.isSymbolicLink()).isTrue(); + assertThat(treeFile.readSymbolicLink()).isEqualTo(file.getPath().asFragment()); + assertThat(treeSymlink.isSymbolicLink()).isTrue(); + assertThat(treeSymlink.readSymbolicLink()).isEqualTo(PathFragment.create("/path/to/target")); + assertThat(treeMissing.exists()).isFalse(); + } }