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"