Blaze actions: add support for dependency pruning.
This adds the parameter "unused_inputs_list" to Skylark "actions.run" method.
This parameter take a file that contains a list of files that were present in the inputs but were not used.
This allows the action to not check for changes in those inputs.
RELNOTES:
PiperOrigin-RevId: 250275882
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/StarlarkAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/StarlarkAction.java
index d620d58..83a9971 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/actions/StarlarkAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/StarlarkAction.java
@@ -13,9 +13,14 @@
// limitations under the License.
package com.google.devtools.build.lib.analysis.actions;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.actions.ActionEnvironment;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.CommandLines;
@@ -23,16 +28,19 @@
import com.google.devtools.build.lib.actions.ResourceSet;
import com.google.devtools.build.lib.actions.RunfilesSupplier;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
-/**
- * A Starlark specific SpawnAction.
- *
- * <p>Note: current implementation is empty: StarlarkAction is equivalent to SpawnAction. This
- * refactoring will help isolate Starlark specific implementation without impacting other classes
- * derived from SpawnAction.
- */
+/** A Starlark specific SpawnAction. */
public final class StarlarkAction extends SpawnAction {
+ private final Optional<Artifact> unusedInputsList;
+ private final Iterable<Artifact> allInputs;
+
/**
* Constructs a StarlarkAction using direct initialization arguments.
*
@@ -56,6 +64,7 @@
* @param progressMessage the message printed during the progression of the build
* @param runfilesSupplier {@link RunfilesSupplier}s describing the runfiles for the action
* @param mnemonic the mnemonic that is reported in the master log
+ * @param unusedInputsList file containing the list of inputs that were not used by the action.
*/
public StarlarkAction(
ActionOwner owner,
@@ -71,7 +80,8 @@
ImmutableMap<String, String> executionInfo,
CharSequence progressMessage,
RunfilesSupplier runfilesSupplier,
- String mnemonic) {
+ String mnemonic,
+ Optional<Artifact> unusedInputsList) {
super(
owner,
tools,
@@ -89,11 +99,68 @@
mnemonic,
/* executeUnconditionally */ false,
/* extraActionInfoSupplier */ null);
+ this.allInputs = inputs;
+ this.unusedInputsList = unusedInputsList;
+ }
+
+ @VisibleForTesting
+ public Optional<Artifact> getUnusedInputsList() {
+ return unusedInputsList;
+ }
+
+ @Override
+ public boolean discoversInputs() {
+ return unusedInputsList.isPresent();
+ }
+
+ @Override
+ public Iterable<Artifact> getAllowedDerivedInputs() {
+ return getInputs();
+ }
+
+ @Override
+ public Iterable<Artifact> discoverInputs(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ // We need to "re-discover" all the original inputs: the unused ones that were removed
+ // might now be needed.
+ updateInputs(allInputs);
+ return allInputs;
+ }
+
+ @Override
+ protected void afterExecute(ActionExecutionContext actionExecutionContext) throws IOException {
+ if (!unusedInputsList.isPresent()) {
+ return;
+ }
+ Map<String, Artifact> usedInputs = new HashMap<>();
+ for (Artifact input : allInputs) {
+ usedInputs.put(input.getExecPathString(), input);
+ }
+ try (BufferedReader br =
+ new BufferedReader(
+ new InputStreamReader(unusedInputsList.get().getPath().getInputStream(), UTF_8))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ line = line.trim();
+ if (line.isEmpty()) {
+ continue;
+ }
+ usedInputs.remove(line);
+ }
+ }
+ updateInputs(usedInputs.values());
}
/** Builder class to construct {@link StarlarkAction} instances. */
public static class Builder extends SpawnAction.Builder {
+ private Optional<Artifact> unusedInputsList = Optional.empty();
+
+ public Builder setUnusedInputsList(Optional<Artifact> unusedInputsList) {
+ this.unusedInputsList = unusedInputsList;
+ return this;
+ }
+
/** Creates a SpawnAction. */
@Override
protected SpawnAction createSpawnAction(
@@ -125,7 +192,8 @@
executionInfo,
progressMessage,
runfilesSupplier,
- mnemonic);
+ mnemonic,
+ unusedInputsList);
}
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkActionFactory.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkActionFactory.java
index 5893524..35d4bbe 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkActionFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkActionFactory.java
@@ -75,6 +75,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Nullable;
@@ -214,6 +215,7 @@
public void run(
SkylarkList outputs,
Object inputs,
+ Object unusedInputsList,
Object executableUnchecked,
Object toolsUnchecked,
Object arguments,
@@ -249,6 +251,7 @@
registerStarlarkAction(
outputs,
inputs,
+ unusedInputsList,
toolsUnchecked,
mnemonicUnchecked,
progressMessage,
@@ -359,6 +362,7 @@
registerStarlarkAction(
outputs,
inputs,
+ /*unusedInputsList=*/ Runtime.NONE,
toolsUnchecked,
mnemonicUnchecked,
progressMessage,
@@ -412,6 +416,7 @@
private void registerStarlarkAction(
SkylarkList outputs,
Object inputs,
+ Object unusedInputsList,
Object toolsUnchecked,
Object mnemonicUnchecked,
Object progressMessage,
@@ -433,6 +438,26 @@
}
builder.addOutputs(outputs.getContents(Artifact.class, "outputs"));
+ if (unusedInputsList != Runtime.NONE) {
+ if (!starlarkSemantics.experimentalStarlarkUnusedInputsList()) {
+ throw new EvalException(
+ location,
+ "'unused_inputs_list' attribute is experimental and disabled by default. "
+ + "This API is in development and subject to change at any time. "
+ + "Use --experimental_starlark_unused_inputs_list to use this experimental API.");
+ }
+ if (unusedInputsList instanceof Artifact) {
+ builder.setUnusedInputsList(Optional.of((Artifact) unusedInputsList));
+ } else {
+ throw new EvalException(
+ location,
+ "expected value of type 'File' for "
+ + "a member of parameter 'unused_inputs_list' but got "
+ + EvalUtils.getDataTypeName(unusedInputsList)
+ + " instead");
+ }
+ }
+
if (toolsUnchecked != Runtime.UNBOUND) {
@SuppressWarnings("unchecked")
Iterable<Object> toolsIterable;
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java
index a0ff5a3..8891297 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java
@@ -895,6 +895,7 @@
.run(
outputs,
inputs,
+ /*unusedInputsList=*/ Runtime.NONE,
executableUnchecked,
toolsUnchecked,
arguments,
diff --git a/src/main/java/com/google/devtools/build/lib/packages/StarlarkSemanticsOptions.java b/src/main/java/com/google/devtools/build/lib/packages/StarlarkSemanticsOptions.java
index e9b26c2..d172331 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/StarlarkSemanticsOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/StarlarkSemanticsOptions.java
@@ -154,6 +154,15 @@
public boolean experimentalStarlarkConfigTransitions;
@Option(
+ name = "experimental_starlark_unused_inputs_list",
+ defaultValue = "false",
+ documentationCategory = OptionDocumentationCategory.STARLARK_SEMANTICS,
+ effectTags = {OptionEffectTag.CHANGES_INPUTS},
+ metadataTags = {OptionMetadataTag.EXPERIMENTAL},
+ help = "If set to true, enables use of 'unused_inputs_list' in starlark action.run().")
+ public boolean experimentalStarlarkUnusedInputsList;
+
+ @Option(
name = "incompatible_bzl_disallow_load_after_statement",
defaultValue = "true",
documentationCategory = OptionDocumentationCategory.STARLARK_SEMANTICS,
@@ -619,6 +628,7 @@
experimentalJavaCommonCreateProviderEnabledPackages)
.experimentalPlatformsApi(experimentalPlatformsApi)
.experimentalStarlarkConfigTransitions(experimentalStarlarkConfigTransitions)
+ .experimentalStarlarkUnusedInputsList(experimentalStarlarkUnusedInputsList)
.incompatibleBzlDisallowLoadAfterStatement(incompatibleBzlDisallowLoadAfterStatement)
.incompatibleDepsetIsNotIterable(incompatibleDepsetIsNotIterable)
.incompatibleDepsetUnion(incompatibleDepsetUnion)
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/SkylarkActionFactoryApi.java b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/SkylarkActionFactoryApi.java
index f2a4252..d1f8fc5 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/SkylarkActionFactoryApi.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/SkylarkActionFactoryApi.java
@@ -184,6 +184,23 @@
positional = false,
doc = "List or depset of the input files of the action."),
@Param(
+ name = "unused_inputs_list",
+ type = Object.class,
+ allowedTypes = {
+ @ParamType(type = FileApi.class),
+ },
+ named = true,
+ noneable = true,
+ defaultValue = "None",
+ positional = false,
+ doc =
+ "File containing list of inputs unused by the action. "
+ + ""
+ + "<p>The content of this file (generally one of the outputs of the action) "
+ + "corresponds to the list of input files that were not used during the whole "
+ + "action execution. Any change in those files must not affect in any way the "
+ + "outputs of the action."),
+ @Param(
name = "executable",
type = Object.class,
allowedTypes = {
@@ -282,6 +299,7 @@
public void run(
SkylarkList outputs,
Object inputs,
+ Object unusedInputsList,
Object executableUnchecked,
Object toolsUnchecked,
Object arguments,
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/StarlarkSemantics.java b/src/main/java/com/google/devtools/build/lib/syntax/StarlarkSemantics.java
index 183fe55..d136bdc 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/StarlarkSemantics.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/StarlarkSemantics.java
@@ -47,6 +47,8 @@
EXPERIMENTAL_PLATFORM_API(StarlarkSemantics::experimentalPlatformsApi),
EXPERIMENTAL_STARLARK_CONFIG_TRANSITION(
StarlarkSemantics::experimentalStarlarkConfigTransitions),
+ EXPERIMENTAL_STARLARK_UNUSED_INPUTS_LIST(
+ StarlarkSemantics::experimentalStarlarkUnusedInputsList),
INCOMPATIBLE_DISABLE_OBJC_PROVIDER_RESOURCES(
StarlarkSemantics::incompatibleDisableObjcProviderResources),
INCOMPATIBLE_NO_OUTPUT_ATTR_DEFAULT(StarlarkSemantics::incompatibleNoOutputAttrDefault),
@@ -133,6 +135,8 @@
public abstract boolean experimentalStarlarkConfigTransitions();
+ public abstract boolean experimentalStarlarkUnusedInputsList();
+
public abstract boolean incompatibleBzlDisallowLoadAfterStatement();
public abstract boolean incompatibleDepsetIsNotIterable();
@@ -222,6 +226,7 @@
.experimentalJavaCommonCreateProviderEnabledPackages(ImmutableList.of())
.experimentalPlatformsApi(false)
.experimentalStarlarkConfigTransitions(false)
+ .experimentalStarlarkUnusedInputsList(false)
.incompatibleBzlDisallowLoadAfterStatement(true)
.incompatibleDepsetIsNotIterable(false)
.incompatibleDepsetUnion(true)
@@ -278,6 +283,8 @@
public abstract Builder experimentalStarlarkConfigTransitions(boolean value);
+ public abstract Builder experimentalStarlarkUnusedInputsList(boolean value);
+
public abstract Builder incompatibleBzlDisallowLoadAfterStatement(boolean value);
public abstract Builder incompatibleDepsetIsNotIterable(boolean value);
diff --git a/src/test/java/com/google/devtools/build/lib/packages/SkylarkSemanticsConsistencyTest.java b/src/test/java/com/google/devtools/build/lib/packages/SkylarkSemanticsConsistencyTest.java
index bdd8af9..2f3c401 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/SkylarkSemanticsConsistencyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/SkylarkSemanticsConsistencyTest.java
@@ -132,6 +132,7 @@
+ rand.nextDouble(),
"--experimental_platforms_api=" + rand.nextBoolean(),
"--experimental_starlark_config_transitions=" + rand.nextBoolean(),
+ "--experimental_starlark_unused_inputs_list=" + rand.nextBoolean(),
"--incompatible_bzl_disallow_load_after_statement=" + rand.nextBoolean(),
"--incompatible_depset_for_libraries_to_link_getter=" + rand.nextBoolean(),
"--incompatible_depset_is_not_iterable=" + rand.nextBoolean(),
@@ -184,6 +185,7 @@
ImmutableList.of(String.valueOf(rand.nextDouble()), String.valueOf(rand.nextDouble())))
.experimentalPlatformsApi(rand.nextBoolean())
.experimentalStarlarkConfigTransitions(rand.nextBoolean())
+ .experimentalStarlarkUnusedInputsList(rand.nextBoolean())
.incompatibleBzlDisallowLoadAfterStatement(rand.nextBoolean())
.incompatibleDepsetForLibrariesToLinkGetter(rand.nextBoolean())
.incompatibleDepsetIsNotIterable(rand.nextBoolean())
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/BUILD b/src/test/java/com/google/devtools/build/lib/skylark/BUILD
index 6f3d861..399d0c4 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/skylark/BUILD
@@ -86,6 +86,7 @@
"//third_party:jsr305",
"//third_party:junit4",
"//third_party:truth",
+ "//third_party:truth8",
],
)
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java
index 7e2428b..907f941 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java
@@ -16,6 +16,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.Truth8.assertThat;
import static com.google.devtools.build.lib.packages.Attribute.attr;
import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST;
import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
@@ -31,7 +32,7 @@
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
-import com.google.devtools.build.lib.analysis.actions.SpawnAction;
+import com.google.devtools.build.lib.analysis.actions.StarlarkAction;
import com.google.devtools.build.lib.analysis.configuredtargets.FileConfiguredTarget;
import com.google.devtools.build.lib.analysis.skylark.SkylarkRuleContext;
import com.google.devtools.build.lib.analysis.util.MockRule;
@@ -679,19 +680,57 @@
SkylarkRuleContext ruleContext = createRuleContext("//foo:androidlib");
evalRuleContextCode(
ruleContext,
- "ruleContext.actions.run(\n"
- + " inputs = ruleContext.files.srcs,\n"
- + " outputs = ruleContext.files.srcs,\n"
- + " arguments = ['--a','--b'],\n"
- + " executable = ruleContext.executable._idlclass)\n");
- SpawnAction action =
- (SpawnAction)
+ "ruleContext.actions.run(",
+ " inputs = ruleContext.files.srcs,",
+ " outputs = ruleContext.files.srcs,",
+ " arguments = ['--a','--b'],",
+ " executable = ruleContext.executable._idlclass)");
+ StarlarkAction action =
+ (StarlarkAction)
Iterables.getOnlyElement(
ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
assertThat(action.getCommandFilename()).matches("^.*/IdlClass(\\.exe){0,1}$");
}
@Test
+ public void testCreateStarlarkActionArgumentsWithUnusedInputsList() throws Exception {
+ setSkylarkSemanticsOptions("--experimental_starlark_unused_inputs_list=True");
+ SkylarkRuleContext ruleContext = createRuleContext("//foo:foo");
+ evalRuleContextCode(
+ ruleContext,
+ "ruleContext.actions.run(",
+ " inputs = ruleContext.files.srcs,",
+ " outputs = ruleContext.files.srcs,",
+ " executable = 'executable',",
+ " unused_inputs_list = ruleContext.files.srcs[0])");
+ StarlarkAction action =
+ (StarlarkAction)
+ Iterables.getOnlyElement(
+ ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+ assertThat(action.getUnusedInputsList()).isPresent();
+ assertThat(action.getUnusedInputsList().get().getFilename()).isEqualTo("a.txt");
+ assertThat(action.discoversInputs()).isTrue();
+ }
+
+ @Test
+ public void testCreateStarlarkActionArgumentsWithoutUnusedInputsList() throws Exception {
+ SkylarkRuleContext ruleContext = createRuleContext("//foo:foo");
+ evalRuleContextCode(
+ ruleContext,
+ "ruleContext.actions.run(",
+ " inputs = ruleContext.files.srcs,",
+ " outputs = ruleContext.files.srcs,",
+ " executable = 'executable',",
+ " unused_inputs_list = None)");
+ StarlarkAction action =
+ (StarlarkAction)
+ Iterables.getOnlyElement(
+ ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+ assertThat(action.getUnusedInputsList()).isEmpty();
+ assertThat(action.discoversInputs()).isFalse();
+ }
+
+ @Test
public void testOutputs() throws Exception {
SkylarkRuleContext ruleContext = createRuleContext("//foo:bar");
Iterable<?> result = (Iterable<?>) evalRuleContextCode(ruleContext, "ruleContext.outputs.outs");
diff --git a/src/test/shell/integration/skylark_dependency_pruning_test.sh b/src/test/shell/integration/skylark_dependency_pruning_test.sh
new file mode 100755
index 0000000..e8a29e6
--- /dev/null
+++ b/src/test/shell/integration/skylark_dependency_pruning_test.sh
@@ -0,0 +1,278 @@
+#!/bin/bash
+#
+# Copyright 2019 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -euo pipefail
+# Load the test setup defined in the parent directory
+CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "${CURRENT_DIR}/../integration_test_setup.sh" \
+ || { echo "integration_test_setup.sh not found!" >&2; exit 1; }
+
+
+#### HELPER FUNCTIONS ##################################################
+
+function set_up() {
+ mkdir -p pkg
+
+ cat > pkg/BUILD << 'EOF'
+load(":build.bzl", "build_rule")
+
+filegroup(
+ name = "all_inputs",
+ srcs = glob(["*.input"]),
+)
+
+sh_binary(
+ name = "cat_unused",
+ srcs = ["cat_unused.sh"],
+)
+
+build_rule(
+ name = "output",
+ out = "output.out",
+ executable = ":cat_unused",
+ inputs = ":all_inputs",
+)
+EOF
+
+ cat > pkg/build.bzl << 'EOF'
+def _impl(ctx):
+ inputs = ctx.attr.inputs.files
+ output = ctx.outputs.out
+ unused_inputs_list = ctx.actions.declare_file(ctx.label.name + ".unused")
+ arguments = []
+ arguments += [output.path]
+ arguments += [unused_inputs_list.path]
+ for input in inputs:
+ arguments += [input.path]
+ ctx.actions.run(
+ inputs = inputs,
+ outputs = [output, unused_inputs_list],
+ arguments = arguments,
+ executable = ctx.executable.executable,
+ unused_inputs_list = unused_inputs_list,
+ )
+
+build_rule = rule(
+ attrs = {
+ "inputs": attr.label(),
+ "executable": attr.label(executable = True, cfg = "host"),
+ "out": attr.output(),
+ },
+ implementation = _impl,
+)
+EOF
+
+ cat > pkg/cat_unused.sh << 'EOF'
+#!/bin/sh
+#
+# Usage: cat_unused.sh output_file unused_file input...
+# "Magic" input content values:
+# - 'unused': mark the file unused, skip its content.
+# - 'invalidUnused': produce an invalid unused file.
+#
+set -eu
+
+output_file="$1"
+shift
+unused_file="$1"
+shift
+
+output=""
+unused=""
+for input in "$@"; do
+ if grep -q "invalidUnused" "${input}"; then
+ unused+="${input}_invalid\n"
+ elif grep -q "unused" "${input}"; then
+ unused+="${input}\n"
+ else
+ output+="$(cat "${input}") "
+ fi
+done
+
+echo -n -e "${output}" > "${output_file}"
+echo -n -e "${unused}" > "${unused_file}"
+EOF
+
+ chmod +x pkg/cat_unused.sh
+
+ echo "contentA" > pkg/a.input
+ echo "contentB" > pkg/b.input
+ echo "contentC" > pkg/c.input
+}
+
+function tear_down() {
+ bazel clean
+ bazel shutdown
+ rm -rf pkg
+}
+
+# ----------------------------------------------------------------------
+# HELPER FUNCTIONS
+# ----------------------------------------------------------------------
+
+# Checks that the unused file contains exactly the list of files passed
+# as parameters.
+function check_unused_content() {
+ unused_file="${PRODUCT_NAME}-bin/pkg/output.unused"
+ expected=""
+ for input in "$@"; do
+ expected+="${input}"
+ expected+=$'\n'
+ done
+ expected="$(echo "${expected}")" # Trimmed.
+ actual="$(cat ${unused_file})"
+ assert_equals "$expected" "$actual"
+}
+
+# Checks the content of the output.
+function check_output_content() {
+ output_file="${PRODUCT_NAME}-bin/pkg/output.out"
+ actual="$(echo $(cat ${output_file}))" # Trimmed.
+ assert_equals "$@" "$actual"
+}
+
+# ----------------------------------------------------------------------
+# TESTS
+# ----------------------------------------------------------------------
+
+# Idea of the tests:
+# - "cat_unused.sh" cats the lists of inputs.
+# - if an input contains "unused", it is added to the "unused_list"
+# - otherwise, its content is concatenated to the output.
+# As a result, any input file that contains "unused" will be considered as
+# unused by the build system..
+#
+# Note: this is not a valid use of "unused_inputs_list" as all input files do
+# actually influence the build output, making this build rule
+# non-deterministic.
+# However, the goal of this test is to check the behavior of the build system
+# with regard to the "unused_inputs_list" attribute.
+
+# Typical "rebuild" scenario.
+function test_dependency_pruning_scenario() {
+ # Initial build.
+ bazel build --experimental_starlark_unused_inputs_list //pkg:output \
+ || fail "build failed"
+ check_output_content "contentA contentB contentC"
+ check_unused_content
+
+ # Mark "b" as unused.
+ echo "unused" > pkg/b.input
+ bazel build --experimental_starlark_unused_inputs_list //pkg:output \
+ || fail "build failed"
+ check_output_content "contentA contentC"
+ check_unused_content "pkg/b.input"
+
+ # Change "b" again:
+ # This time it should be used. But given that it was marked "unused"
+ # the build should not trigger: "b" should still be considered unused.
+ echo "newContentB" > pkg/b.input
+ bazel build --experimental_starlark_unused_inputs_list //pkg:output \
+ || fail "build failed"
+ check_output_content "contentA contentC"
+ check_unused_content "pkg/b.input"
+
+ # Change c:
+ # The build should be triggered, and the newer version of "b" should be used.
+ echo "unused" > pkg/c.input
+ bazel build --experimental_starlark_unused_inputs_list //pkg:output \
+ || fail "build failed"
+ check_output_content "contentA newContentB"
+ check_unused_content "pkg/c.input"
+}
+
+# Verify that the state of the local action cache survives server shutdown.
+function test_unused_shutdown() {
+ # Mark "b" as unused + initial build
+ echo "unused" > pkg/b.input
+ bazel build --experimental_starlark_unused_inputs_list //pkg:output \
+ || fail "build failed"
+ check_output_content "contentA contentC"
+ check_unused_content "pkg/b.input"
+
+ # Shutdown.
+ bazel shutdown
+
+ # Change "b" again:
+ # Check that the action is still cached, although b changed.
+ echo "newContentB" > pkg/b.input
+ bazel build --experimental_starlark_unused_inputs_list //pkg:output \
+ || fail "build failed"
+ check_output_content "contentA contentC"
+ check_unused_content "pkg/b.input"
+
+ # Change c:
+ # The build should be trigerred, and the newer version of "b" should be used.
+ echo "unused" > pkg/c.input
+ bazel build --experimental_starlark_unused_inputs_list //pkg:output \
+ || fail "build failed"
+ check_output_content "contentA newContentB"
+ check_unused_content "pkg/c.input"
+}
+
+# Verify that actually used input files stay on the set ot inputs after a server
+# shutdown.
+function test_used_shutdown() {
+ # Mark "b" as unused + initial build
+ echo "unused" > pkg/b.input
+ bazel build --experimental_starlark_unused_inputs_list //pkg:output \
+ || fail "build failed"
+ check_output_content "contentA contentC"
+ check_unused_content "pkg/b.input"
+
+ # Shutdown.
+ bazel shutdown
+
+ # Change "c", which is used.
+ echo "newContentC" > pkg/c.input
+ bazel build --experimental_starlark_unused_inputs_list //pkg:output \
+ || fail "build failed"
+ check_output_content "contentA newContentC"
+ check_unused_content "pkg/b.input"
+}
+
+# Verify that file names that are not actually inputs in the unused file are
+# ignored.
+function test_invalid_unused() {
+ # Mark "b" as producing an invalid unused file + initial build
+ echo "invalidUnused" > pkg/b.input
+ bazel build --experimental_starlark_unused_inputs_list //pkg:output \
+ || fail "build failed"
+ # Note: build should not fail: it is OK for unused file to contain
+ # non-existing files.
+ check_output_content "contentA contentC"
+ check_unused_content "pkg/b.input_invalid"
+
+ # Change "b" again:
+ # It should just be picked-up, as it was not "unused".
+ echo "newContentB" > pkg/b.input
+ bazel build --experimental_starlark_unused_inputs_list //pkg:output \
+ || fail "build failed"
+ check_output_content "contentA newContentB contentC"
+ check_unused_content
+}
+
+# Verify that the flag '--experimental_starlark_unused_inputs_list' is required
+# for 'unused_inputs_list' usage.
+function test_experiment_flag_required() {
+ # This should fail.
+ bazel build //pkg:output >& $TEST_log && fail "Expected failure"
+ exitcode=$?
+ assert_equals 1 "$exitcode"
+ expect_log "Use --experimental_starlark_unused_inputs_list"
+}
+
+run_suite "Tests Skylark dependency pruning"