diff --git a/src/main/java/com/google/devtools/build/lib/exec/RunfilesTreeUpdater.java b/src/main/java/com/google/devtools/build/lib/exec/RunfilesTreeUpdater.java index b9bccc71142f1c..1a22c514779f5e 100644 --- a/src/main/java/com/google/devtools/build/lib/exec/RunfilesTreeUpdater.java +++ b/src/main/java/com/google/devtools/build/lib/exec/RunfilesTreeUpdater.java @@ -13,6 +13,8 @@ // limitations under the License. package com.google.devtools.build.lib.exec; +import static com.google.devtools.build.lib.analysis.config.BuildConfigurationValue.RunfileSymlinksMode.SKIP; +import static java.nio.charset.StandardCharsets.ISO_8859_1; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableMap; @@ -21,12 +23,16 @@ import com.google.devtools.build.lib.actions.RunfilesSupplier.RunfilesTree; import com.google.devtools.build.lib.analysis.RunfilesSupport; import com.google.devtools.build.lib.runtime.CommandEnvironment; +import com.google.devtools.build.lib.util.OS; import com.google.devtools.build.lib.util.io.OutErr; import com.google.devtools.build.lib.vfs.DigestUtils; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.lib.vfs.Symlinks; import com.google.devtools.build.lib.vfs.XattrProvider; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; import java.util.Arrays; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -122,11 +128,17 @@ private void updateRunfilesTree( // implying the symlinks exist and are already up to date. If the output manifest is a // symbolic link, it is likely a symbolic link to the input manifest, so we cannot trust it as // an up-to-date check. - if (!outputManifest.isSymbolicLink() + // On Windows, where symlinks may be silently replaced by copies, a previous run in SKIP mode + // could have resulted in an output manifest that is an identical copy of the input manifest, + // which we must not treat as up to date, but we also don't want to unnecessarily rebuild the + // runfiles directory all the time. Instead, check for the presence of the first runfile in + // the manifest. If it is present, we can be certain that the previous mode wasn't SKIP. + if (tree.getSymlinksMode() != SKIP + && !outputManifest.isSymbolicLink() && Arrays.equals( DigestUtils.getDigestWithManualFallbackWhenSizeUnknown(outputManifest, xattrProvider), - DigestUtils.getDigestWithManualFallbackWhenSizeUnknown( - inputManifest, xattrProvider))) { + DigestUtils.getDigestWithManualFallbackWhenSizeUnknown(inputManifest, xattrProvider)) + && (OS.getCurrent() != OS.WINDOWS || isRunfilesDirectoryPopulated(runfilesDirPath))) { return; } } catch (IOException e) { @@ -142,7 +154,7 @@ private void updateRunfilesTree( switch (tree.getSymlinksMode()) { case SKIP: - helper.linkManifest(); + helper.clearAndLinkManifest(); break; case EXTERNAL: helper.createSymlinksUsingCommand(execRoot, binTools, env, outErr); @@ -153,4 +165,15 @@ private void updateRunfilesTree( break; } } + + private boolean isRunfilesDirectoryPopulated(Path runfilesDirPath) throws IOException { + Path outputManifest = RunfilesSupport.outputManifestPath(runfilesDirPath); + String relativeRunfilePath; + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(outputManifest.getInputStream(), ISO_8859_1))) { + // If it is created at all, the manifest always contains at least one line. + relativeRunfilePath = reader.readLine().split(" ")[0]; + } + return runfilesDirPath.getRelative(relativeRunfilePath).exists(Symlinks.NOFOLLOW); + } } 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 504b9b89ab7370..1182020f8c8772 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 @@ -121,8 +121,17 @@ public void createSymlinksDirectly(Path symlinkTreeRoot, Map