Do not re-run Ninja actions when only order-only inputs change.

RELNOTES: None.
PiperOrigin-RevId: 312347597
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaAction.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaAction.java
index 644078d..19cb6a6 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaAction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaAction.java
@@ -30,6 +30,7 @@
 import com.google.devtools.build.lib.actions.RunfilesSupplier;
 import com.google.devtools.build.lib.actions.SpawnResult;
 import com.google.devtools.build.lib.analysis.actions.SpawnAction;
+import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaTarget;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
@@ -53,12 +54,15 @@
   @Nullable private final Artifact depFile;
   private final ImmutableMap<PathFragment, Artifact> allowedDerivedInputs;
   private final ArtifactRoot derivedOutputRoot;
+  private final NestedSet<Artifact> originalInputs; // This does not include order-only inputs.
+  private final NestedSet<Artifact> orderOnlyInputs;
 
   public NinjaAction(
       ActionOwner owner,
       Root sourceRoot,
       NestedSet<Artifact> tools,
       NestedSet<Artifact> inputs,
+      NestedSet<Artifact> orderOnlyInputs,
       List<? extends Artifact> outputs,
       CommandLines commandLines,
       ActionEnvironment env,
@@ -88,6 +92,7 @@
         /* resultConsumer= */ null);
     this.sourceRoot = sourceRoot;
     this.depFile = depFile;
+
     ImmutableMap.Builder<PathFragment, Artifact> allowedDerivedInputsBuilder =
         ImmutableMap.builder();
     for (Artifact input : inputs.toList()) {
@@ -96,7 +101,10 @@
       }
     }
     this.allowedDerivedInputs = allowedDerivedInputsBuilder.build();
+
     this.derivedOutputRoot = derivedOutputRoot;
+    this.originalInputs = inputs;
+    this.orderOnlyInputs = orderOnlyInputs;
   }
 
   @Override
@@ -113,18 +121,31 @@
       throws EnvironmentalExecException {
     checkOutputsForDirectories(actionExecutionContext);
 
-    if (depFile != null) {
+    if (depFile == null) {
+      // The inputs may have been modified during input discovery to include order-only inputs.
+      // Restore the original inputs to exclude those order-only inputs, so that order-only inputs
+      // do not cause the action to be rerun. This needs to be done when there is no dep file
+      // because updateInputsFromDepfile() will recalculate all the dependencies, and even if an
+      // input is order-only, it will cause a rebuild if it's listed in the depfile.
+      // Note also that this must check if the order-only inputs are empty, because if they are,
+      // discoversInputs() will return false, causing updateInputs() to throw an error.
+      if (!orderOnlyInputs.isEmpty()) {
+        updateInputs(originalInputs);
+      }
+    } else {
       updateInputsFromDepfile(actionExecutionContext);
     }
   }
 
   private void updateInputsFromDepfile(ActionExecutionContext actionExecutionContext)
       throws EnvironmentalExecException {
+
     boolean siblingRepositoryLayout =
         actionExecutionContext
             .getOptions()
             .getOptions(StarlarkSemanticsOptions.class)
             .experimentalSiblingRepositoryLayout;
+
     CppIncludeExtractionContext scanningContext =
         actionExecutionContext.getContext(CppIncludeExtractionContext.class);
     ArtifactResolver artifactResolver = scanningContext.getArtifactResolver();
@@ -142,6 +163,7 @@
         } else {
           execRelativePath = inputPath.asFragment().relativeTo(execRoot.asFragment());
         }
+
         Artifact inputArtifact = null;
         if (allowedDerivedInputs.containsKey(execRelativePath)) {
           // Predeclared generated input.
@@ -169,6 +191,7 @@
                       + "a source input, or a pre-declared generated input",
                   execRelativePath));
         }
+
         inputsBuilder.add(inputArtifact);
       }
       updateInputs(inputsBuilder.build());
@@ -180,7 +203,7 @@
 
   @Override
   public boolean discoversInputs() {
-    return depFile != null;
+    return depFile != null || !orderOnlyInputs.isEmpty();
   }
 
   @Override
@@ -190,7 +213,12 @@
 
   @Override
   public NestedSet<Artifact> discoverInputs(ActionExecutionContext actionExecutionContext) {
-    updateInputs(getInputs());
-    return getInputs();
+    NestedSet<Artifact> inputs =
+        NestedSetBuilder.<Artifact>stableOrder()
+            .addTransitive(getInputs())
+            .addTransitive(orderOnlyInputs)
+            .build();
+    updateInputs(inputs);
+    return inputs;
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaActionsHelper.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaActionsHelper.java
index 4945775..f83a22d 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaActionsHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaActionsHelper.java
@@ -32,6 +32,7 @@
 import com.google.devtools.build.lib.bazel.rules.ninja.file.GenericParsingException;
 import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaRuleVariable;
 import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaTarget;
+import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaTarget.InputKind;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.Order;
 import com.google.devtools.build.lib.packages.TargetUtils;
@@ -40,6 +41,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
 import javax.annotation.Nullable;
@@ -157,8 +159,10 @@
 
   private void createNinjaAction(NinjaTarget target) throws GenericParsingException {
     NestedSetBuilder<Artifact> inputsBuilder = NestedSetBuilder.stableOrder();
+    NestedSetBuilder<Artifact> orderOnlyInputsBuilder = NestedSetBuilder.stableOrder();
     ImmutableList.Builder<Artifact> outputsBuilder = ImmutableList.builder();
-    boolean isAlwaysDirty = fillArtifacts(target, inputsBuilder, outputsBuilder);
+    boolean isAlwaysDirty =
+        fillArtifacts(target, inputsBuilder, orderOnlyInputsBuilder, outputsBuilder);
 
     ImmutableSortedMap<NinjaRuleVariable, String> resolvedMap = target.computeRuleVariables();
     String command = resolvedMap.get(NinjaRuleVariable.COMMAND);
@@ -182,6 +186,7 @@
             artifactsHelper.getSourceRoot(),
             NestedSetBuilder.emptySet(Order.STABLE_ORDER),
             inputsBuilder.build(),
+            orderOnlyInputsBuilder.build(),
             outputs,
             commandLines,
             Preconditions.checkNotNull(ruleContext.getConfiguration()).getActionEnvironment(),
@@ -228,17 +233,25 @@
   private boolean fillArtifacts(
       NinjaTarget target,
       NestedSetBuilder<Artifact> inputsBuilder,
+      NestedSetBuilder<Artifact> orderOnlyInputsBuilder,
       ImmutableList.Builder<Artifact> outputsBuilder)
       throws GenericParsingException {
+
     boolean isAlwaysDirty = false;
-    for (PathFragment input : target.getAllInputs()) {
+    for (Map.Entry<InputKind, PathFragment> entry : target.getAllInputsAndKind()) {
+
+      InputKind kind = entry.getKey();
+      boolean isOrderOnly = (kind == InputKind.ORDER_ONLY);
+      NestedSetBuilder<Artifact> builder = isOrderOnly ? orderOnlyInputsBuilder : inputsBuilder;
+
+      PathFragment input = entry.getValue();
       PhonyTarget phonyTarget = this.phonyTargets.get(input);
       if (phonyTarget != null) {
-        inputsBuilder.addTransitive(phonyTargetArtifacts.getPhonyTargetArtifacts(input));
-        isAlwaysDirty |= phonyTarget.isAlwaysDirty();
+        builder.addTransitive(phonyTargetArtifacts.getPhonyTargetArtifacts(input));
+        isAlwaysDirty |= (phonyTarget.isAlwaysDirty() && !isOrderOnly);
       } else {
         Artifact artifact = artifactsHelper.getInputArtifact(input);
-        inputsBuilder.add(artifact);
+        builder.add(artifact);
       }
     }
 
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaTarget.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaTarget.java
index 33956e6..b8a3a27 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaTarget.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaTarget.java
@@ -193,7 +193,7 @@
    */
   private final ImmutableSortedMap<NinjaRuleVariable, NinjaVariableValue> ruleVariables;
 
-  public NinjaTarget(
+  private NinjaTarget(
       String ruleName,
       ImmutableSortedKeyListMultimap<InputKind, PathFragment> inputs,
       ImmutableSortedKeyListMultimap<OutputKind, PathFragment> outputs,
@@ -226,6 +226,10 @@
     return inputs.values();
   }
 
+  public Collection<Map.Entry<InputKind, PathFragment>> getAllInputsAndKind() {
+    return inputs.entries();
+  }
+
   public Collection<PathFragment> getExplicitInputs() {
     return inputs.get(InputKind.EXPLICIT);
   }
diff --git a/src/test/java/com/google/devtools/build/lib/blackbox/tests/NinjaBlackBoxTest.java b/src/test/java/com/google/devtools/build/lib/blackbox/tests/NinjaBlackBoxTest.java
index c427461..10abca8 100644
--- a/src/test/java/com/google/devtools/build/lib/blackbox/tests/NinjaBlackBoxTest.java
+++ b/src/test/java/com/google/devtools/build/lib/blackbox/tests/NinjaBlackBoxTest.java
@@ -608,4 +608,97 @@
     assertThat(Files.exists(rspFile)).isTrue();
     assertThat(Files.readAllLines(rspFile)).containsExactly("../input.txt");
   }
+
+  @Test
+  public void testOrderOnlyInputsDoNotCauseRebuild() throws Exception {
+    context().write("input.txt", "abc");
+    context().write("order-only-input.txt", "123");
+    context()
+        .write(
+            "build_dir/build.ninja",
+            "rule cat",
+            "  command = cat ${in} ${order_only_input} > ${out}",
+            "build output.txt: cat ../input.txt || ../order-only-input.txt",
+            "  order_only_input = ../order-only-input.txt");
+
+    context()
+        .write(
+            "BUILD",
+            "ninja_graph(name = 'graph', output_root = 'build_dir',",
+            " working_directory = 'build_dir',",
+            " main = 'build_dir/build.ninja')",
+            "ninja_build(name = 'ninja_target', ninja_graph = 'graph',",
+            " output_groups = {'main': ['output.txt']})");
+
+    BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
+
+    assertConfigured(bazel.build("//:ninja_target"));
+    Path outputPath = context().resolveExecRootPath(bazel, "build_dir/output.txt");
+    assertThat(outputPath.toFile().exists()).isTrue();
+    assertThat(Files.readAllLines(outputPath)).containsExactly("abc", "123");
+
+    // Change the order-only input...
+    context().write("order-only-input.txt", "456");
+    // ...but nothing should rerun....
+    assertNothingConfigured(bazel.build("//:ninja_target"));
+    // ...and the output should remain the same.
+    assertThat(Files.readAllLines(outputPath)).containsExactly("abc", "123");
+
+    // Change the main input...
+    context().write("input.txt", "xyz");
+    bazel.build("//:ninja_target");
+    // ...and the output should have the new content from the order-only input.
+    assertThat(Files.readAllLines(outputPath)).containsExactly("xyz", "456");
+  }
+
+  @Test
+  public void testOrderOnlyInputsInDepFileCauseRebuild() throws Exception {
+    context().write("build_dir/in.txt", "abc");
+    context().write("build_dir/order-only-input1.txt", "123");
+    context().write("build_dir/order-only-input2.txt", "456");
+    context()
+        .write(
+            "build_dir/build.ninja",
+            "rule cat",
+            "  command = cat ${in} ${order_only_input1} ${order_only_input2} > ${out} ; "
+                // TODO(b/156961649): Having to put "build_dir/" in front of ${order_only_input2}
+                // might be a bug
+                + "echo \"${out} : build_dir/${order_only_input2}\" > ${depfile}",
+            "  depfile = df",
+            "build output.txt: cat in.txt || order-only-input1.txt order-only-input2.txt",
+            "  order_only_input1 = order-only-input1.txt",
+            "  order_only_input2 = order-only-input2.txt");
+
+    context()
+        .write(
+            "BUILD",
+            "ninja_graph(name = 'graph', output_root = 'build_dir',",
+            " working_directory = 'build_dir',",
+            " main = 'build_dir/build.ninja',",
+            " output_root_inputs = ['in.txt', 'order-only-input1.txt', 'order-only-input2.txt'],)",
+            "ninja_build(name = 'ninja_target', ninja_graph = 'graph',",
+            " output_groups = {'main': ['output.txt']})");
+
+    BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
+
+    ProcessResult pr = bazel.build("//:ninja_target");
+    System.out.println(pr.outString());
+    assertConfigured(pr);
+    Path outputPath = context().resolveExecRootPath(bazel, "build_dir/output.txt");
+    assertThat(outputPath.toFile().exists()).isTrue();
+    assertThat(Files.readAllLines(outputPath)).containsExactly("abc", "123", "456");
+
+    // Change the order-only input...
+    context().write("build_dir/order-only-input1.txt", "789");
+    // ...but nothing should rerun....
+    assertNothingConfigured(bazel.build("//:ninja_target"));
+    // ...and the output should remain the same.
+    assertThat(Files.readAllLines(outputPath)).containsExactly("abc", "123", "456");
+
+    // But change the order-only input that's listed in the depfile...
+    context().write("build_dir/order-only-input2.txt", "xyz");
+    bazel.build("//:ninja_target");
+    // ...and the output should have the new content from both order-only inputs.
+    assertThat(Files.readAllLines(outputPath)).containsExactly("abc", "789", "xyz");
+  }
 }