Have remote_outputs=toplevel download runfiles. Fixes #8899
This change makes it so that when building a binary remotely and
having --experimental_remote_download_toplevel set the runfiles of the
binary will also be downloaded. This applies also to building a test
target. However, when executing a test remotely with bazel test neither
the test binary nor its runfiles are downloaded. Instead Bazel fetches
the test result: test.xml and test.log
This change also enables bazel run to be used with
--experimental_remote_download_toplevel.
Closes #9298.
PiperOrigin-RevId: 266781050
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteActionContextProvider.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteActionContextProvider.java
index 4f0fb92..535bba1 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteActionContextProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteActionContextProvider.java
@@ -49,7 +49,7 @@
private final DigestUtil digestUtil;
@Nullable private final Path logDir;
private final AtomicReference<SpawnRunner> fallbackRunner = new AtomicReference<>();
- private ImmutableSet<ActionInput> topLevelOutputs = ImmutableSet.of();
+ private ImmutableSet<ActionInput> filesToDownload = ImmutableSet.of();
private RemoteActionContextProvider(
CommandEnvironment env,
@@ -103,7 +103,7 @@
commandId,
env.getReporter(),
digestUtil,
- topLevelOutputs);
+ filesToDownload);
return ImmutableList.of(spawnCache);
} else {
RemoteSpawnRunner spawnRunner =
@@ -121,7 +121,7 @@
retrier,
digestUtil,
logDir,
- topLevelOutputs);
+ filesToDownload);
return ImmutableList.of(new RemoteSpawnStrategy(env.getExecRoot(), spawnRunner));
}
}
@@ -171,8 +171,8 @@
return cache;
}
- void setTopLevelOutputs(ImmutableSet<ActionInput> topLevelOutputs) {
- this.topLevelOutputs = Preconditions.checkNotNull(topLevelOutputs, "topLevelOutputs");
+ void setFilesToDownload(ImmutableSet<ActionInput> topLevelOutputs) {
+ this.filesToDownload = Preconditions.checkNotNull(topLevelOutputs, "filesToDownload");
}
@Override
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
index 7140b40..fc7c658 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
@@ -20,10 +20,14 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.RunfilesSupport;
import com.google.devtools.build.lib.analysis.TopLevelArtifactContext;
import com.google.devtools.build.lib.analysis.TopLevelArtifactHelper;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
@@ -316,6 +320,51 @@
}
}
+ private static ImmutableList<Artifact> getRunfiles(ConfiguredTarget buildTarget) {
+ FilesToRunProvider runfilesProvider = buildTarget.getProvider(FilesToRunProvider.class);
+ if (runfilesProvider == null) {
+ return ImmutableList.of();
+ }
+ RunfilesSupport runfilesSupport = runfilesProvider.getRunfilesSupport();
+ if (runfilesSupport == null) {
+ return ImmutableList.of();
+ }
+ boolean noPruningManifestsInBazel =
+ Iterables.isEmpty(runfilesSupport.getRunfiles().getPruningManifests());
+ Preconditions.checkState(
+ noPruningManifestsInBazel, "Bazel should not have pruning manifests. This is a bug.");
+ ImmutableList.Builder<Artifact> runfilesBuilder = ImmutableList.builder();
+ for (Artifact runfile : runfilesSupport.getRunfiles().getUnconditionalArtifacts()) {
+ if (runfile.isSourceArtifact()) {
+ continue;
+ }
+ runfilesBuilder.add(runfile);
+ }
+ return runfilesBuilder.build();
+ }
+
+ private static ImmutableList<ActionInput> getTestOutputs(ConfiguredTarget testTarget) {
+ TestProvider testProvider = testTarget.getProvider(TestProvider.class);
+ if (testProvider == null) {
+ return ImmutableList.of();
+ }
+ return testProvider.getTestParams().getOutputs();
+ }
+
+ private static Iterable<? extends ActionInput> getArtifactsToBuild(
+ ConfiguredTarget buildTarget, TopLevelArtifactContext topLevelArtifactContext) {
+ return TopLevelArtifactHelper.getAllArtifactsToBuild(buildTarget, topLevelArtifactContext)
+ .getImportantArtifacts();
+ }
+
+ private static boolean isTestRule(ConfiguredTarget configuredTarget) {
+ if (configuredTarget instanceof RuleConfiguredTarget) {
+ RuleConfiguredTarget ruleConfiguredTarget = (RuleConfiguredTarget) configuredTarget;
+ return TargetUtils.isTestRuleName(ruleConfiguredTarget.getRuleClassString());
+ }
+ return false;
+ }
+
@Override
public void afterAnalysis(
CommandEnvironment env,
@@ -325,45 +374,22 @@
ImmutableSet<AspectValue> aspects) {
if (remoteOutputsMode != null && remoteOutputsMode.downloadToplevelOutputsOnly()) {
Preconditions.checkState(actionContextProvider != null, "actionContextProvider was null");
- // Collect all top level output artifacts of regular targets as well as aspects. This
- // information is used by remote spawn runners to decide whether to download an artifact
- // if --experimental_remote_download_outputs=toplevel is set
- ImmutableSet.Builder<ActionInput> topLevelOutputsBuilder = ImmutableSet.builder();
+ boolean isTestCommand = env.getCommandName().equals("test");
+ TopLevelArtifactContext artifactContext = request.getTopLevelArtifactContext();
+ ImmutableSet.Builder<ActionInput> filesToDownload = ImmutableSet.builder();
for (ConfiguredTarget configuredTarget : configuredTargets) {
- topLevelOutputsBuilder.addAll(
- getTopLevelTargetOutputs(
- configuredTarget, request.getTopLevelArtifactContext(), env.getCommandName()));
+ if (isTestCommand && isTestRule(configuredTarget)) {
+ // When running a test download the test.log and test.xml.
+ filesToDownload.addAll(getTestOutputs(configuredTarget));
+ } else {
+ filesToDownload.addAll(getArtifactsToBuild(configuredTarget, artifactContext));
+ filesToDownload.addAll(getRunfiles(configuredTarget));
+ }
}
- actionContextProvider.setTopLevelOutputs(topLevelOutputsBuilder.build());
+ actionContextProvider.setFilesToDownload(filesToDownload.build());
}
}
- /** Returns a list of build or test outputs produced by the configured target. */
- private ImmutableList<ActionInput> getTopLevelTargetOutputs(
- ConfiguredTarget configuredTarget,
- TopLevelArtifactContext topLevelArtifactContext,
- String commandName) {
- if (commandName.equals("test") && isTestRule(configuredTarget)) {
- TestProvider testProvider = configuredTarget.getProvider(TestProvider.class);
- if (testProvider == null) {
- return ImmutableList.of();
- }
- return testProvider.getTestParams().getOutputs();
- } else {
- return ImmutableList.copyOf(
- TopLevelArtifactHelper.getAllArtifactsToBuild(configuredTarget, topLevelArtifactContext)
- .getImportantArtifacts());
- }
- }
-
- private static boolean isTestRule(ConfiguredTarget configuredTarget) {
- if (configuredTarget instanceof RuleConfiguredTarget) {
- RuleConfiguredTarget ruleConfiguredTarget = (RuleConfiguredTarget) configuredTarget;
- return TargetUtils.isTestRuleName(ruleConfiguredTarget.getRuleClassString());
- }
- return false;
- }
-
private static void cleanAndCreateRemoteLogsDir(Path logDir) throws AbruptExitException {
try {
// Clean out old logs files.
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java
index d24d5d5..4d738d5 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java
@@ -16,7 +16,7 @@
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.devtools.build.lib.remote.util.Utils.createSpawnResult;
import static com.google.devtools.build.lib.remote.util.Utils.getInMemoryOutputPath;
-import static com.google.devtools.build.lib.remote.util.Utils.hasTopLevelOutputs;
+import static com.google.devtools.build.lib.remote.util.Utils.hasFilesToDownload;
import static com.google.devtools.build.lib.remote.util.Utils.shouldDownloadAllSpawnOutputs;
import build.bazel.remote.execution.v2.Action;
@@ -85,12 +85,10 @@
private final DigestUtil digestUtil;
/**
- * Set of artifacts that are top level outputs
- *
- * <p>This set is empty unless {@link RemoteOutputsMode#TOPLEVEL} is specified. If so, this set is
- * used to decide whether to download an output.
+ * If {@link RemoteOutputsMode#TOPLEVEL} is specified it contains the artifacts that should be
+ * downloaded.
*/
- private final ImmutableSet<ActionInput> topLevelOutputs;
+ private final ImmutableSet<ActionInput> filesToDownload;
RemoteSpawnCache(
Path execRoot,
@@ -100,7 +98,7 @@
String commandId,
@Nullable Reporter cmdlineReporter,
DigestUtil digestUtil,
- ImmutableSet<ActionInput> topLevelOutputs) {
+ ImmutableSet<ActionInput> filesToDownload) {
this.execRoot = execRoot;
this.options = options;
this.remoteCache = remoteCache;
@@ -108,7 +106,7 @@
this.buildRequestId = buildRequestId;
this.commandId = commandId;
this.digestUtil = digestUtil;
- this.topLevelOutputs = Preconditions.checkNotNull(topLevelOutputs, "topLevelOutputs");
+ this.filesToDownload = Preconditions.checkNotNull(filesToDownload, "filesToDownload");
}
@Override
@@ -164,7 +162,7 @@
shouldDownloadAllSpawnOutputs(
remoteOutputsMode,
/* exitCode = */ 0,
- hasTopLevelOutputs(spawn.getOutputFiles(), topLevelOutputs));
+ hasFilesToDownload(spawn.getOutputFiles(), filesToDownload));
if (downloadOutputs) {
try (SilentCloseable c =
prof.profile(ProfilerTask.REMOTE_DOWNLOAD, "download outputs")) {
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java
index 8ff410f..6412122 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java
@@ -20,7 +20,7 @@
import static com.google.devtools.build.lib.remote.util.Utils.createSpawnResult;
import static com.google.devtools.build.lib.remote.util.Utils.getFromFuture;
import static com.google.devtools.build.lib.remote.util.Utils.getInMemoryOutputPath;
-import static com.google.devtools.build.lib.remote.util.Utils.hasTopLevelOutputs;
+import static com.google.devtools.build.lib.remote.util.Utils.hasFilesToDownload;
import static com.google.devtools.build.lib.remote.util.Utils.shouldDownloadAllSpawnOutputs;
import build.bazel.remote.execution.v2.Action;
@@ -107,12 +107,10 @@
private final Path logDir;
/**
- * Set of artifacts that are top level outputs
- *
- * <p>This set is empty unless {@link RemoteOutputsMode#TOPLEVEL} is specified. If so, this set is
- * used to decide whether to download an output.
+ * If {@link RemoteOutputsMode#TOPLEVEL} is specified it contains the artifacts that should be
+ * downloaded.
*/
- private final ImmutableSet<ActionInput> topLevelOutputs;
+ private final ImmutableSet<ActionInput> filesToDownload;
// Used to ensure that a warning is reported only once.
private final AtomicBoolean warningReported = new AtomicBoolean();
@@ -131,7 +129,7 @@
@Nullable RemoteRetrier retrier,
DigestUtil digestUtil,
Path logDir,
- ImmutableSet<ActionInput> topLevelOutputs) {
+ ImmutableSet<ActionInput> filesToDownload) {
this.execRoot = execRoot;
this.remoteOptions = remoteOptions;
this.executionOptions = executionOptions;
@@ -145,7 +143,7 @@
this.retrier = retrier;
this.digestUtil = digestUtil;
this.logDir = logDir;
- this.topLevelOutputs = Preconditions.checkNotNull(topLevelOutputs, "topLevelOutputs");
+ this.filesToDownload = Preconditions.checkNotNull(filesToDownload, "filesToDownload");
}
@Override
@@ -300,7 +298,7 @@
shouldDownloadAllSpawnOutputs(
remoteOutputsMode,
/* exitCode = */ actionResult.getExitCode(),
- hasTopLevelOutputs(spawn.getOutputFiles(), topLevelOutputs));
+ hasFilesToDownload(spawn.getOutputFiles(), filesToDownload));
InMemoryOutput inMemoryOutput = null;
if (downloadOutputs) {
try (SilentCloseable c = Profiler.instance().profile(REMOTE_DOWNLOAD, "download outputs")) {
diff --git a/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOutputsMode.java b/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOutputsMode.java
index d1bb349..c163fe3 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOutputsMode.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOutputsMode.java
@@ -27,9 +27,9 @@
MINIMAL,
/**
- * Downloads outputs of top level targets, but generally not intermediate outputs. The only
- * intermediate outputs to be downloaded are .d and .jdeps files for C++ and Java compilation
- * actions.
+ * Downloads outputs of top level targets. Top level targets are targets specified on the command
+ * line. If a top level target has runfile dependencies it will also download those. Intermediate
+ * outputs are generally not downloaded (See {@link #MINIMAL}.
*/
TOPLEVEL;
diff --git a/src/main/java/com/google/devtools/build/lib/remote/util/Utils.java b/src/main/java/com/google/devtools/build/lib/remote/util/Utils.java
index 422726a..4d824e0 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/util/Utils.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/util/Utils.java
@@ -101,12 +101,12 @@
}
/** Returns {@code true} if outputs contains one or more top level outputs. */
- public static boolean hasTopLevelOutputs(
- Collection<? extends ActionInput> outputs, ImmutableSet<ActionInput> topLevelOutputs) {
- if (topLevelOutputs.isEmpty()) {
+ public static boolean hasFilesToDownload(
+ Collection<? extends ActionInput> outputs, ImmutableSet<ActionInput> filesToDownload) {
+ if (filesToDownload.isEmpty()) {
return false;
}
- return !Collections.disjoint(outputs, topLevelOutputs);
+ return !Collections.disjoint(outputs, filesToDownload);
}
public static String grpcAwareErrorMessage(IOException e) {
diff --git a/src/test/shell/bazel/remote/remote_execution_test.sh b/src/test/shell/bazel/remote/remote_execution_test.sh
index 2f0e002..3fcedff 100755
--- a/src/test/shell/bazel/remote/remote_execution_test.sh
+++ b/src/test/shell/bazel/remote/remote_execution_test.sh
@@ -61,6 +61,23 @@
rm -rf "${cas_path}"
}
+case "$(uname -s | tr [:upper:] [:lower:])" in
+msys*|mingw*|cygwin*)
+ declare -r is_windows=true
+ ;;
+*)
+ declare -r is_windows=false
+ ;;
+esac
+
+if "$is_windows"; then
+ export MSYS_NO_PATHCONV=1
+ export MSYS2_ARG_CONV_EXCL="*"
+ declare -r EXE_EXT=".exe"
+else
+ declare -r EXE_EXT=""
+fi
+
function test_remote_grpc_cache_with_protocol() {
# Test that if 'grpc' is provided as a scheme for --remote_cache flag, remote cache works.
mkdir -p a
@@ -1042,6 +1059,78 @@
|| fail "Expected toplevel output bazel-bin/a/foobar.txt to be re-downloaded"
}
+function test_downloads_toplevel_runfiles() {
+ # Test that --experimental_remote_download_outputs=toplevel downloads the top level binary
+ # and generated runfiles.
+ mkdir -p a
+
+ cat > a/create_bar.tmpl <<'EOF'
+#!/bin/sh
+echo "bar runfiles"
+exit 0
+EOF
+
+ cat > a/foo.cc <<'EOF'
+#include <iostream>
+int main() { std::cout << "foo" << std::endl; return 0; }
+EOF
+
+ cat > a/BUILD <<'EOF'
+genrule(
+ name = "bar",
+ srcs = ["create_bar.tmpl"],
+ outs = ["create_bar.sh"],
+ cmd = "cat $(location create_bar.tmpl) > \"$@\"",
+)
+
+cc_binary(
+ name = "foo",
+ srcs = ["foo.cc"],
+ data = [":bar"],
+)
+EOF
+
+ bazel build \
+ --remote_executor=grpc://localhost:${worker_port} \
+ --experimental_remote_download_toplevel \
+ //a:foo || fail "Failed to build //a:foobar"
+
+ [[ -f bazel-bin/a/foo${EXE_EXT} ]] \
+ || fail "Expected toplevel output bazel-bin/a/foo${EXE_EXT} to be downloaded"
+
+ [[ -f bazel-bin/a/create_bar.sh ]] \
+ || fail "Expected runfile bazel-bin/a/create_bar.sh to be downloaded"
+}
+
+function test_downloads_toplevel_src_runfiles() {
+ # Test that using --experimental_remote_download_outputs=toplevel with a non-generated (source)
+ # runfile dependency works.
+ mkdir -p a
+ cat > a/create_foo.sh <<'EOF'
+#!/bin/sh
+echo "foo runfiles"
+exit 0
+EOF
+ chmod +x a/create_foo.sh
+ cat > a/BUILD <<'EOF'
+genrule(
+ name = "foo",
+ srcs = [],
+ tools = ["create_foo.sh"],
+ outs = ["foo.txt"],
+ cmd = "./$(location create_foo.sh) > \"$@\"",
+)
+EOF
+
+ bazel build \
+ --remote_executor=grpc://localhost:${worker_port} \
+ --experimental_remote_download_toplevel \
+ //a:foo || fail "Failed to build //a:foobar"
+
+ [[ -f bazel-bin/a/foo.txt ]] \
+ || fail "Expected toplevel output bazel-bin/a/foo.txt to be downloaded"
+}
+
function test_download_toplevel_test_rule() {
# Test that when using --experimental_remote_download_outputs=toplevel with bazel test only
# the test.log and test.xml file are downloaded but not the test binary. However when building