rust: Treat config inputs as implicit rebuild deps

NinjaRustBinaryTargetWriter previously placed the result of
WriteInputsStampOrPhonyAndGetDep() into order-only dependencies ("||")
while only adding target_->config_values().inputs() to implicit
dependencies ("|") and to the `sources` Ninja variable ("{{sources}}").
As a result, `inputs` specified on `config()` definitions (via
`configs`, `public_configs`, `all_dependent_configs`, or sub-configs)
were omitted from `sources` and treated only as order-only dependencies,
so changes to config inputs such as custom `rustc --target` JSON specs
failed to trigger incremental rebuilds (see https://fxrev.dev/1967814).

Refactor NinjaBinaryTargetWriter to share input and dependency
collection cleanly between NinjaCBinaryTargetWriter and
NinjaRustBinaryTargetWriter:
- Extract GetInputs() using UniqueVector<SourceFile> so duplicate
  SourceFile entries across targets and configs are deduplicated by
  value rather than element pointer address, and share it with
  NinjaRustBinaryTargetWriter::WriteSourcesAndInputs().
- Extract WriteCompilerInputDeps(num_output_uses) combining implicit
  `inputs`, `public_inputs`, and toolchain `deps` with order-only
  upstream hard dependencies.
- Pass num_output_uses = 1 in NinjaRustBinaryTargetWriter::Run() since
  Rust compiles a crate in a single build statement, avoiding redundant
  per-target `.inputs` / `.inputdeps` phony or stamp rules.
- Deduplicate `order_only_deps` with UniqueVector<OutputFile> and drop
  the duplicate tool_->inputs_phony_or_file() addition already handled
  by WriteCompilerBuildLine().

Change-Id: I89c123308a0c7f8f2f6844cc50a2dce0a0214cba
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/28740
Commit-Queue: Petr Hosek <phosek@google.com>
Reviewed-by: Petr Hosek <phosek@google.com>
Reviewed-by: Aaron Wood <aaronwood@google.com>
diff --git a/src/gn/ninja_binary_target_writer.cc b/src/gn/ninja_binary_target_writer.cc
index 420ac0a..e9e23cf 100644
--- a/src/gn/ninja_binary_target_writer.cc
+++ b/src/gn/ninja_binary_target_writer.cc
@@ -6,6 +6,7 @@
 
 #include <algorithm>
 #include <unordered_set>
+#include <utility>
 
 #include "base/strings/string_util.h"
 #include "gn/builtin_tool.h"
@@ -157,18 +158,23 @@
   out << "}\n";
 }
 
+UniqueVector<SourceFile> NinjaBinaryTargetWriter::GetInputs() const {
+  UniqueVector<SourceFile> inputs;
+  // ConfigValuesIterator yields all inputs from the target itself as well as
+  // from all its configs (configs, public_configs, all_dependent_configs).
+  for (ConfigValuesIterator iter(target_); !iter.done(); iter.Next()) {
+    inputs.Append(iter.cur().inputs());
+  }
+  return inputs;
+}
+
 std::vector<OutputFile>
 NinjaBinaryTargetWriter::WriteInputsStampOrPhonyAndGetDep(
     size_t num_output_uses) const {
   CHECK(target_->toolchain()) << "Toolchain not set on target "
                               << target_->label().GetUserVisibleName(true);
 
-  UniqueVector<const SourceFile*> inputs;
-  for (ConfigValuesIterator iter(target_); !iter.done(); iter.Next()) {
-    for (const auto& input : iter.cur().inputs()) {
-      inputs.push_back(&input);
-    }
-  }
+  UniqueVector<SourceFile> inputs = GetInputs();
 
   if (inputs.size() == 0)
     return std::vector<OutputFile>();  // No inputs
@@ -177,17 +183,18 @@
   // target for it.
   if (inputs.size() == 1) {
     return std::vector<OutputFile>{
-        OutputFile(settings_->build_settings(), *inputs[0])};
+        OutputFile(settings_->build_settings(), inputs[0])};
   }
 
-  std::vector<OutputFile> outs;
-  for (const SourceFile* source : inputs)
-    outs.push_back(OutputFile(settings_->build_settings(), *source));
-
   // If there are multiple inputs, but the phony target would be referenced only
   // once, don't write it but depend on the inputs directly.
-  if (num_output_uses == 1u)
+  if (num_output_uses == 1u) {
+    std::vector<OutputFile> outs;
+    outs.reserve(inputs.size());
+    for (const SourceFile& source : inputs)
+      outs.push_back(OutputFile(settings_->build_settings(), source));
     return outs;
+  }
 
   OutputFile stamp_or_phony;
   std::string tool;
@@ -211,15 +218,56 @@
   out_ << ": " << tool;
 
   // File inputs.
-  for (const auto* input : inputs) {
+  for (const auto& input : inputs) {
     out_ << " ";
-    path_output_.WriteFile(out_, *input);
+    path_output_.WriteFile(out_, input);
   }
 
   out_ << '\n';
   return {stamp_or_phony};
 }
 
+NinjaTargetWriter::InputDeps NinjaBinaryTargetWriter::WriteCompilerInputDeps(
+    size_t num_output_uses) const {
+  InputDeps deps;
+  deps.implicit = WriteInputsStampOrPhonyAndGetDep(num_output_uses);
+
+  // Upstream hard dependencies will be an order-only dependency. This will
+  // cause Ninja to make sure the inputs are up to date before compiling this
+  // source, but changes in the input deps won't cause the file to be
+  // recompiled.
+  //
+  // This is important to prevent changes in unrelated actions that are
+  // upstream of this target from causing everything to be recompiled.
+  //
+  // Why can we get away with this for upstream target actions rather than using
+  // implicit deps ("|", which will force rebuilds when the inputs change)? For
+  // source code, the computed dependencies of all headers and included files
+  // will be computed by the compiler via depfiles, which will cause source
+  // rebuilds if any "real" upstream dependencies change. Explicit `inputs` (on
+  // the target or any applied configs), `public_inputs`, and toolchain `deps`
+  // are placed in `deps.implicit` ("|") so changes to them always force
+  // recompilation (for instance, `rustc --emit=dep-info` omits command-line
+  // files such as custom target JSON specs).
+  //
+  // If a source file is generated by an input dependency, Ninja will see the
+  // input to the build rule doesn't exist, and that it is an output from a
+  // previous step, and build the previous step first. This is a "real"
+  // dependency and doesn't need | or || to express.
+  //
+  // The only case where this order-only rule matters is for the first build
+  // where no .d files exist, and Ninja doesn't know what that source file
+  // depends on. In this case it's sufficient to ensure that the upstream
+  // dependencies are built first. This is exactly what Ninja's order-only
+  // dependencies express.
+  InputDeps stamp_deps = WriteInputDepsStampOrPhonyAndGetDep(
+      std::vector<const Target*>(), num_output_uses);
+  deps.implicit.insert(deps.implicit.end(), stamp_deps.implicit.begin(),
+                       stamp_deps.implicit.end());
+  deps.order_only = std::move(stamp_deps.order_only);
+  return deps;
+}
+
 NinjaBinaryTargetWriter::ClassifiedDeps
 NinjaBinaryTargetWriter::GetClassifiedDeps() const {
   ClassifiedDeps classified_deps;
diff --git a/src/gn/ninja_binary_target_writer.h b/src/gn/ninja_binary_target_writer.h
index f942297..7166589 100644
--- a/src/gn/ninja_binary_target_writer.h
+++ b/src/gn/ninja_binary_target_writer.h
@@ -34,15 +34,21 @@
     UniqueVector<const Target*> swiftmodule_deps;
   };
 
-  // Writes to the output stream a phony rule for inputs, and
-  // returns the target to be appended to source rules that encodes the
-  // implicit dependencies for the current target.
-  // If num_output_uses is small, this might return all input dependencies
-  // directly, without writing a phony rule.
-  // If there are no implicit dependencies and no extra target dependencies
-  // are passed in, this returns an empty vector.
+  // Collects all deduplicated input files from the target and its configs.
+  UniqueVector<SourceFile> GetInputs() const;
+
+  // Writes to the output stream a stamp or phony rule for inputs (if needed),
+  // and returns the files to be appended to source rules that encode the
+  // implicit `inputs` dependencies for the current target.
+  // If num_output_uses is 1 or there is at most one input, this returns the
+  // input dependencies directly without writing an intermediate rule.
   std::vector<OutputFile> WriteInputsStampOrPhonyAndGetDep(
-      size_t num_phony_uses) const;
+      size_t num_output_uses) const;
+
+  // Combines implicit dependencies from target/config `inputs`,
+  // `public_inputs`, and toolchain `deps` with order-only dependencies from
+  // upstream hard deps.
+  InputDeps WriteCompilerInputDeps(size_t num_output_uses) const;
 
   // Gets all target dependencies and classifies them, as well as accumulates
   // object files from source sets we need to link.
diff --git a/src/gn/ninja_c_binary_target_writer.cc b/src/gn/ninja_c_binary_target_writer.cc
index b750cda..027ba06 100644
--- a/src/gn/ninja_c_binary_target_writer.cc
+++ b/src/gn/ninja_c_binary_target_writer.cc
@@ -68,54 +68,22 @@
 
   WriteCompilerVars(module_dep_info);
 
-  size_t num_output_uses = target_->sources().size();
-
-  std::vector<OutputFile> input_deps =
-      WriteInputsStampOrPhonyAndGetDep(num_output_uses);
-
-  // The input dependencies will be an order-only dependency. This will cause
-  // Ninja to make sure the inputs are up to date before compiling this source,
-  // but changes in the inputs deps won't cause the file to be recompiled.
-  //
-  // This is important to prevent changes in unrelated actions that are
-  // upstream of this target from causing everything to be recompiled.
-  //
-  // Why can we get away with this rather than using implicit deps ("|", which
-  // will force rebuilds when the inputs change)? For source code, the
-  // computed dependencies of all headers will be computed by the compiler,
-  // which will cause source rebuilds if any "real" upstream dependencies
-  // change.
-  //
-  // If a .cc file is generated by an input dependency, Ninja will see the
-  // input to the build rule doesn't exist, and that it is an output from a
-  // previous step, and build the previous step first. This is a "real"
-  // dependency and doesn't need | or || to express.
-  //
-  // The only case where this rule matters is for the first build where no .d
-  // files exist, and Ninja doesn't know what that source file depends on. In
-  // this case it's sufficient to ensure that the upstream dependencies are
-  // built first. This is exactly what Ninja's order-only dependencies
-  // expresses.
-  //
-  // The order only deps are referenced by each source file compile,
+  // The order-only deps are referenced by each source file compile,
   // but also by PCH compiles.  The latter are annoying to count, so omit
   // them here.  This means that binary targets with a single source file
   // that also use PCH files won't have a phony target even though having
   // one would make output ninja file size a bit lower. That's ok, binary
   // targets with a single source are rare.
-  NinjaTargetWriter::InputDeps stamp_deps = WriteInputDepsStampOrPhonyAndGetDep(
-      std::vector<const Target*>(), num_output_uses);
-  input_deps.insert(input_deps.end(), stamp_deps.implicit.begin(),
-                    stamp_deps.implicit.end());
-  std::vector<OutputFile> order_only_deps = stamp_deps.order_only;
+  InputDeps compiler_input_deps =
+      WriteCompilerInputDeps(target_->sources().size());
 
   // For GCC builds, the .gch files are not object files, but still need to be
   // added as explicit dependencies below. The .gch output files are placed in
   // |pch_other_files|. This is to prevent linking against them.
   std::vector<OutputFile> pch_obj_files;
   std::vector<OutputFile> pch_other_files;
-  WritePCHCommands(input_deps, order_only_deps, &pch_obj_files,
-                   &pch_other_files);
+  WritePCHCommands(compiler_input_deps.implicit, compiler_input_deps.order_only,
+                   &pch_obj_files, &pch_other_files);
   std::vector<OutputFile>* pch_files =
       !pch_obj_files.empty() ? &pch_obj_files : &pch_other_files;
 
@@ -136,11 +104,13 @@
   std::vector<SourceFile> other_files;
   std::vector<OutputFile>* stamp_files = &obj_files;  // default
   if (!target_->source_types_used().SwiftSourceUsed()) {
-    WriteSources(*pch_files, input_deps, order_only_deps, module_dep_info,
-                 &obj_files, &extra_files, &other_files);
+    WriteSources(*pch_files, compiler_input_deps.implicit,
+                 compiler_input_deps.order_only, module_dep_info, &obj_files,
+                 &extra_files, &other_files);
   } else {
     stamp_files = &extra_files;  // Swift generates more than object files
-    WriteSwiftSources(input_deps, order_only_deps, &obj_files, &extra_files);
+    WriteSwiftSources(compiler_input_deps.implicit,
+                      compiler_input_deps.order_only, &obj_files, &extra_files);
   }
 
   // Link all MSVC pch object files. The vector will be empty on GCC toolchains.
@@ -167,7 +137,7 @@
     }
     WriteSourceSetStamp(*stamp_files);
   } else {
-    WriteLinkerStuff(obj_files, other_files, input_deps);
+    WriteLinkerStuff(obj_files, other_files, compiler_input_deps.implicit);
   }
 }
 
diff --git a/src/gn/ninja_c_binary_target_writer_unittest.cc b/src/gn/ninja_c_binary_target_writer_unittest.cc
index edaf89f..e446574 100644
--- a/src/gn/ninja_c_binary_target_writer_unittest.cc
+++ b/src/gn/ninja_c_binary_target_writer_unittest.cc
@@ -1747,6 +1747,8 @@
   {
     Config far_config(setup.settings(), Label(SourceDir("//foo/"), "qux"));
     far_config.own_values().inputs().push_back(SourceFile("//foo/input3.data"));
+    // Duplicate of target's input1.data to verify value-based deduplication.
+    far_config.own_values().inputs().push_back(SourceFile("//foo/input1.data"));
     ASSERT_TRUE(far_config.OnResolved(&err));
 
     Config config(setup.settings(), Label(SourceDir("//foo/"), "baz"));
diff --git a/src/gn/ninja_rust_binary_target_writer.cc b/src/gn/ninja_rust_binary_target_writer.cc
index 7cf7256..c263c75 100644
--- a/src/gn/ninja_rust_binary_target_writer.cc
+++ b/src/gn/ninja_rust_binary_target_writer.cc
@@ -5,6 +5,7 @@
 #include "gn/ninja_rust_binary_target_writer.h"
 
 #include <unordered_set>
+#include <utility>
 
 #include "base/strings/string_util.h"
 #include "gn/deps_iterator.h"
@@ -113,25 +114,17 @@
 void NinjaRustBinaryTargetWriter::Run() {
   DCHECK(target_->output_type() != Target::SOURCE_SET);
 
-  size_t num_output_uses = target_->sources().size();
-
-  std::vector<OutputFile> input_deps =
-      WriteInputsStampOrPhonyAndGetDep(num_output_uses);
-
   WriteCompilerVars();
 
   // Classify our dependencies.
   ClassifiedDeps classified_deps = GetClassifiedDeps();
 
-  // The input dependencies will be an order-only dependency. This will cause
-  // Ninja to make sure the inputs are up to date before compiling this source,
-  // but changes in the inputs deps won't cause the file to be recompiled. See
-  // the comment on NinjaCBinaryTargetWriter::Run for more detailed explanation.
-  NinjaTargetWriter::InputDeps stamp_deps = WriteInputDepsStampOrPhonyAndGetDep(
-      std::vector<const Target*>(), num_output_uses);
-  std::vector<OutputFile> order_only_deps = stamp_deps.order_only;
-  std::copy(input_deps.begin(), input_deps.end(),
-            std::back_inserter(order_only_deps));
+  // Unlike C/C++, Rust compiles all sources of a crate in one command, so
+  // input dependencies are referenced only once in this ninja file.
+  InputDeps compiler_input_deps =
+      WriteCompilerInputDeps(/*num_output_uses=*/1u);
+  UniqueVector<OutputFile> order_only_deps;
+  order_only_deps.Append(std::move(compiler_input_deps.order_only));
 
   // rustc does not invoke a linker when building an rlib, so non-Rust native
   // dependencies (object files, static and shared libraries) are not used to
@@ -147,19 +140,18 @@
   // -Ldependency. Also assemble a list of extra (i.e. implicit) deps
   // for ninja dependency tracking.
   UniqueVector<OutputFile> implicit_deps;
-  implicit_deps.Append(stamp_deps.implicit.begin(), stamp_deps.implicit.end());
-  AppendSourcesAndInputsToImplicitDeps(&implicit_deps);
+  // Only the crate_root file needs to be given to rustc as explicit input.
+  // Any other 'sources' are implicit deps.
+  for (const auto& source : target_->sources()) {
+    implicit_deps.push_back(OutputFile(settings_->build_settings(), source));
+  }
+  implicit_deps.Append(compiler_input_deps.implicit.begin(),
+                       compiler_input_deps.implicit.end());
   if (!is_rlib) {
     implicit_deps.Append(classified_deps.extra_object_files.begin(),
                          classified_deps.extra_object_files.end());
   }
 
-  if (auto phony = tool_->inputs_phony_or_file(rule_prefix_,
-                                               *settings_->build_settings());
-      phony) {
-    implicit_deps.emplace_back(std::move(*phony));
-  }
-
   std::vector<OutputFile> rustdeps;
   std::vector<OutputFile> nonrustdeps;
   std::vector<OutputFile> swiftmodules;
@@ -190,8 +182,7 @@
 
   std::vector<OutputFile> group_order_only_deps =
       GetOrderOnlyDepsFromNonLinkableDeps(classified_deps.non_linkable_deps);
-  order_only_deps.insert(order_only_deps.end(), group_order_only_deps.begin(),
-                         group_order_only_deps.end());
+  order_only_deps.Append(std::move(group_order_only_deps));
   for (const auto* linkable_dep : classified_deps.linkable_deps) {
     // Rust cdylibs are treated as non-Rust dependencies for linking purposes.
     if (linkable_dep->source_types_used().RustSourceUsed() &&
@@ -295,8 +286,8 @@
   SubstitutionWriter::ApplyListToLinkerAsOutputFile(
       target_, tool_, tool_->outputs(), &tool_outputs);
   WriteCompilerBuildLine({target_->rust_values().crate_root()},
-                         implicit_deps.vector(), order_only_deps, tool_,
-                         tool_outputs);
+                         implicit_deps.vector(), order_only_deps.vector(),
+                         tool_, tool_outputs);
 
   std::vector<const Target*> extern_deps(
       classified_deps.linkable_deps.vector());
@@ -321,31 +312,15 @@
   WriteSharedVars(subst);
 }
 
-void NinjaRustBinaryTargetWriter::AppendSourcesAndInputsToImplicitDeps(
-    UniqueVector<OutputFile>* deps) const {
-  // Only the crate_root file needs to be given to rustc as input.
-  // Any other 'sources' are just implicit deps.
-  // Most Rust targets won't bother specifying the "sources =" line
-  // because it is handled sufficiently by crate_root and the generation
-  // of depfiles by rustc. But for those which do...
-  for (const auto& source : target_->sources()) {
-    deps->push_back(OutputFile(settings_->build_settings(), source));
-  }
-  for (const auto& data : target_->config_values().inputs()) {
-    deps->push_back(OutputFile(settings_->build_settings(), data));
-  }
-}
-
 void NinjaRustBinaryTargetWriter::WriteSourcesAndInputs() {
+  UniqueVector<SourceFile> files;
+  files.Append(target_->sources());
+  files.Append(GetInputs());
+
   out_ << "  sources =";
-  for (const auto& source : target_->sources()) {
+  for (const auto& file : files) {
     out_ << " ";
-    path_output_.WriteFile(out_,
-                           OutputFile(settings_->build_settings(), source));
-  }
-  for (const auto& data : target_->config_values().inputs()) {
-    out_ << " ";
-    path_output_.WriteFile(out_, OutputFile(settings_->build_settings(), data));
+    path_output_.WriteFile(out_, OutputFile(settings_->build_settings(), file));
   }
   out_ << '\n';
 }
diff --git a/src/gn/ninja_rust_binary_target_writer.h b/src/gn/ninja_rust_binary_target_writer.h
index 99f95fe..44e598a 100644
--- a/src/gn/ninja_rust_binary_target_writer.h
+++ b/src/gn/ninja_rust_binary_target_writer.h
@@ -26,8 +26,6 @@
   };
 
   void WriteCompilerVars();
-  void WriteSources(const OutputFile& input_dep,
-                    const std::vector<OutputFile>& order_only_deps);
   void WriteExternsAndDeps(const std::vector<const Target*>& deps,
                            const std::vector<ExternCrate>& transitive_rust_deps,
                            const std::vector<OutputFile>& rustdeps,
@@ -36,9 +34,6 @@
   // Unlike C/C++, Rust compiles all sources of a crate in one command.
   // Write a ninja variable `sources` that contains all sources and input files.
   void WriteSourcesAndInputs();
-  void WriteEdition();
-  void AppendSourcesAndInputsToImplicitDeps(
-      UniqueVector<OutputFile>* deps) const;
 
   const RustTool* tool_;
 
diff --git a/src/gn/ninja_rust_binary_target_writer_unittest.cc b/src/gn/ninja_rust_binary_target_writer_unittest.cc
index 3d0733a..4f5282e 100644
--- a/src/gn/ninja_rust_binary_target_writer_unittest.cc
+++ b/src/gn/ninja_rust_binary_target_writer_unittest.cc
@@ -1788,8 +1788,6 @@
     writer.Run();
 
     const char expected[] =
-        "build phony/foo/bar.inputs: phony ../../foo/config.json "
-        "../../foo/template.h\n"
         "crate_name = foo_bar\n"
         "crate_type = bin\n"
         "output_extension = \n"
@@ -1802,8 +1800,7 @@
         "target_output_name = bar\n"
         "\n"
         "build ./foo_bar: rust_bin ../../foo/main.rs | ../../foo/source.rs "
-        "../../foo/main.rs ../../foo/config.json ../../foo/template.h "
-        "|| phony/foo/bar.inputs\n"
+        "../../foo/main.rs ../../foo/config.json ../../foo/template.h\n"
         "  source_file_part = main.rs\n"
         "  source_name_part = main\n"
         "  externs =\n"
@@ -1815,6 +1812,391 @@
   }
 }
 
+TEST_F(NinjaRustBinaryTargetWriterTest, ConfigInputsAndOrderOnlyDeps) {
+  Err err;
+  TestWithScope setup;
+
+  // Sub-config attached to a direct config (e.g. a custom target JSON spec).
+  Config sub_config(setup.settings(), Label(SourceDir("//cfg/"), "sub_cfg"));
+  sub_config.visibility().SetPublic();
+  sub_config.own_values().rustflags().push_back("--target=gen/cfg/target.json");
+  sub_config.own_values().inputs().push_back(
+      SourceFile("//out/Debug/gen/cfg/target.json"));
+  // Duplicate of direct_config's input to verify value-based deduplication.
+  sub_config.own_values().inputs().push_back(SourceFile("//cfg/direct.json"));
+  ASSERT_TRUE(sub_config.OnResolved(&err));
+
+  Config direct_config(setup.settings(), Label(SourceDir("//cfg/"), "direct"));
+  direct_config.visibility().SetPublic();
+  direct_config.own_values().inputs().push_back(
+      SourceFile("//cfg/direct.json"));
+  direct_config.configs().push_back(LabelConfigPair(&sub_config));
+  ASSERT_TRUE(direct_config.OnResolved(&err));
+
+  Config pub_config(setup.settings(), Label(SourceDir("//cfg/"), "pub"));
+  pub_config.visibility().SetPublic();
+  pub_config.own_values().inputs().push_back(SourceFile("//cfg/public.json"));
+  ASSERT_TRUE(pub_config.OnResolved(&err));
+
+  Config all_dep_config(setup.settings(), Label(SourceDir("//cfg/"), "all"));
+  all_dep_config.visibility().SetPublic();
+  all_dep_config.own_values().inputs().push_back(SourceFile("//cfg/all.json"));
+  ASSERT_TRUE(all_dep_config.OnResolved(&err));
+
+  // Upstream action target: its output should be an order-only dependency
+  // ("||"), while its public_configs / all_dependent_configs `inputs` and its
+  // `public_inputs` must be implicit rebuild dependencies ("|").
+  Target gen_action(setup.settings(), Label(SourceDir("//cfg/"), "gen_target"));
+  gen_action.set_output_type(Target::ACTION);
+  gen_action.visibility().SetPublic();
+  gen_action.action_values().set_script(SourceFile("//cfg/gen.py"));
+  gen_action.action_values().outputs() =
+      SubstitutionList::MakeForTest("//out/Debug/gen/cfg/target.json");
+  gen_action.public_configs().push_back(LabelConfigPair(&pub_config));
+  gen_action.all_dependent_configs().push_back(
+      LabelConfigPair(&all_dep_config));
+  gen_action.public_inputs().push_back(SourceFile("//cfg/pub_input.txt"));
+  gen_action.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(gen_action.OnResolved(&err));
+
+  // 1. Test RUST_LIBRARY (rlib) with target inputs, config inputs,
+  // public_inputs, and an order-only action dependency.
+  Target rlib(setup.settings(), Label(SourceDir("//foo/"), "mylib"));
+  rlib.set_output_type(Target::RUST_LIBRARY);
+  rlib.visibility().SetPublic();
+  SourceFile lib_rs("//foo/lib.rs");
+  rlib.sources().push_back(SourceFile("//foo/extra.rs"));
+  rlib.sources().push_back(lib_rs);
+  rlib.source_types_used().Set(SourceFile::SOURCE_RS);
+  rlib.rust_values().set_crate_root(lib_rs);
+  rlib.rust_values().crate_name() = "mylib";
+  rlib.config_values().inputs().push_back(SourceFile("//foo/own.json"));
+  // Also duplicate //cfg/direct.json on the target itself to test deduping.
+  rlib.config_values().inputs().push_back(SourceFile("//cfg/direct.json"));
+  rlib.configs().push_back(LabelConfigPair(&direct_config));
+  rlib.public_deps().push_back(LabelTargetPair(&gen_action));
+  rlib.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(rlib.OnResolved(&err));
+
+  {
+    StringOutputBuffer out;
+    NinjaRustBinaryTargetWriter writer(&rlib, out);
+    writer.Run();
+
+    const char expected[] =
+        "crate_name = mylib\n"
+        "crate_type = rlib\n"
+        "output_extension = .rlib\n"
+        "output_dir = \n"
+        "rustflags = --target=gen/cfg/target.json\n"
+        "rustenv =\n"
+        "root_out_dir = .\n"
+        "target_gen_dir = gen/foo\n"
+        "target_out_dir = obj/foo\n"
+        "target_output_name = libmylib\n"
+        "\n"
+        "build obj/foo/libmylib.rlib: rust_rlib ../../foo/lib.rs | "
+        "../../foo/extra.rs ../../foo/lib.rs ../../foo/own.json "
+        "../../cfg/direct.json gen/cfg/target.json ../../cfg/all.json "
+        "../../cfg/public.json phony/cfg/gen_target.public_inputs || "
+        "phony/cfg/gen_target\n"
+        "  source_file_part = lib.rs\n"
+        "  source_name_part = lib\n"
+        "  externs =\n"
+        "  rustdeps =\n"
+        "  ldflags =\n"
+        "  sources = ../../foo/extra.rs ../../foo/lib.rs ../../foo/own.json "
+        "../../cfg/direct.json gen/cfg/target.json ../../cfg/all.json "
+        "../../cfg/public.json\n";
+    EXPECT_EQ(expected, out);
+  }
+
+  // 2. Test RUST_PROC_MACRO, SHARED_LIBRARY (dylib), LOADABLE_MODULE (cdylib),
+  // and STATIC_LIBRARY receiving inherited configs (all_dependent_configs +
+  // transitive public_configs via a group), toolchain deps, and multiple
+  // upstream action hard_deps, with stamp files enabled.
+  {
+    TestWithScope stamp_setup;
+    stamp_setup.build_settings()->set_no_stamp_files(false);
+
+    Target tc_dep(stamp_setup.settings(), Label(SourceDir("//tc/"), "setup"));
+    tc_dep.set_output_type(Target::ACTION);
+    tc_dep.visibility().SetPublic();
+    tc_dep.action_values().set_script(SourceFile("//tc/setup.py"));
+    tc_dep.SetToolchain(stamp_setup.toolchain());
+    ASSERT_TRUE(tc_dep.OnResolved(&err));
+    stamp_setup.toolchain()->deps().push_back(LabelTargetPair(&tc_dep));
+
+    Config transitive_pub_cfg(stamp_setup.settings(),
+                              Label(SourceDir("//cfg/"), "trans_pub"));
+    transitive_pub_cfg.visibility().SetPublic();
+    transitive_pub_cfg.own_values().inputs().push_back(
+        SourceFile("//cfg/spec1.json"));
+    ASSERT_TRUE(transitive_pub_cfg.OnResolved(&err));
+
+    Config all_dep_spec_cfg(stamp_setup.settings(),
+                            Label(SourceDir("//cfg/"), "all_spec"));
+    all_dep_spec_cfg.visibility().SetPublic();
+    all_dep_spec_cfg.own_values().inputs().push_back(
+        SourceFile("//cfg/spec2.json"));
+    ASSERT_TRUE(all_dep_spec_cfg.OnResolved(&err));
+
+    Target action_dep(stamp_setup.settings(),
+                      Label(SourceDir("//cfg/"), "act"));
+    action_dep.set_output_type(Target::ACTION);
+    action_dep.visibility().SetPublic();
+    action_dep.action_values().set_script(SourceFile("//cfg/gen.py"));
+    action_dep.action_values().outputs() =
+        SubstitutionList::MakeForTest("//out/Debug/gen/cfg/out.txt");
+    action_dep.public_configs().push_back(LabelConfigPair(&transitive_pub_cfg));
+    action_dep.all_dependent_configs().push_back(
+        LabelConfigPair(&all_dep_spec_cfg));
+    action_dep.SetToolchain(stamp_setup.toolchain());
+    ASSERT_TRUE(action_dep.OnResolved(&err));
+
+    Target action_dep2(stamp_setup.settings(),
+                       Label(SourceDir("//cfg/"), "act2"));
+    action_dep2.set_output_type(Target::ACTION);
+    action_dep2.visibility().SetPublic();
+    action_dep2.action_values().set_script(SourceFile("//cfg/gen2.py"));
+    action_dep2.action_values().outputs() =
+        SubstitutionList::MakeForTest("//out/Debug/gen/cfg/out2.txt");
+    action_dep2.SetToolchain(stamp_setup.toolchain());
+    ASSERT_TRUE(action_dep2.OnResolved(&err));
+
+    Target cfg_group(stamp_setup.settings(), Label(SourceDir("//cfg/"), "grp"));
+    cfg_group.set_output_type(Target::GROUP);
+    cfg_group.visibility().SetPublic();
+    cfg_group.public_deps().push_back(LabelTargetPair(&action_dep));
+    cfg_group.public_deps().push_back(LabelTargetPair(&action_dep2));
+    cfg_group.SetToolchain(stamp_setup.toolchain());
+    ASSERT_TRUE(cfg_group.OnResolved(&err));
+
+    Target procmacro(stamp_setup.settings(),
+                     Label(SourceDir("//macro/"), "pm"));
+    procmacro.set_output_type(Target::RUST_PROC_MACRO);
+    procmacro.visibility().SetPublic();
+    SourceFile pm_root("//macro/lib.rs");
+    procmacro.sources().push_back(SourceFile("//macro/helper.rs"));
+    procmacro.sources().push_back(pm_root);
+    procmacro.source_types_used().Set(SourceFile::SOURCE_RS);
+    procmacro.rust_values().set_crate_root(pm_root);
+    procmacro.rust_values().crate_name() = "pm";
+    procmacro.private_deps().push_back(LabelTargetPair(&cfg_group));
+    procmacro.SetToolchain(stamp_setup.toolchain());
+    ASSERT_TRUE(procmacro.OnResolved(&err));
+
+    StringOutputBuffer pm_out;
+    NinjaRustBinaryTargetWriter pm_writer(&procmacro, pm_out);
+    pm_writer.Run();
+
+    const char pm_expected[] =
+        "crate_name = pm\n"
+        "crate_type = proc-macro\n"
+        "output_extension = .so\n"
+        "output_dir = \n"
+        "rustflags =\n"
+        "rustenv =\n"
+        "root_out_dir = .\n"
+        "target_gen_dir = gen/macro\n"
+        "target_out_dir = obj/macro\n"
+        "target_output_name = libpm\n"
+        "\n"
+        "build obj/macro/libpm.so: rust_macro ../../macro/lib.rs | "
+        "../../macro/helper.rs ../../macro/lib.rs ../../cfg/spec2.json "
+        "../../cfg/spec1.json obj/tc/setup.stamp || obj/cfg/grp.harddeps.stamp "
+        "obj/cfg/act.stamp obj/cfg/act2.stamp\n"
+        "  source_file_part = lib.rs\n"
+        "  source_name_part = lib\n"
+        "  externs =\n"
+        "  rustdeps =\n"
+        "  ldflags =\n"
+        "  sources = ../../macro/helper.rs ../../macro/lib.rs "
+        "../../cfg/spec2.json ../../cfg/spec1.json\n";
+    EXPECT_EQ(pm_expected, pm_out);
+
+    Target dylib(stamp_setup.settings(), Label(SourceDir("//dylib/"), "dy"));
+    dylib.set_output_type(Target::SHARED_LIBRARY);
+    dylib.visibility().SetPublic();
+    SourceFile dy_root("//dylib/lib.rs");
+    dylib.sources().push_back(dy_root);
+    dylib.source_types_used().Set(SourceFile::SOURCE_RS);
+    dylib.rust_values().set_crate_root(dy_root);
+    dylib.rust_values().set_crate_type(RustValues::CRATE_DYLIB);
+    dylib.rust_values().crate_name() = "dy";
+    dylib.private_deps().push_back(LabelTargetPair(&cfg_group));
+    dylib.SetToolchain(stamp_setup.toolchain());
+    ASSERT_TRUE(dylib.OnResolved(&err));
+
+    StringOutputBuffer dy_out;
+    NinjaRustBinaryTargetWriter dy_writer(&dylib, dy_out);
+    dy_writer.Run();
+
+    const char dy_expected[] =
+        "crate_name = dy\n"
+        "crate_type = dylib\n"
+        "output_extension = .so\n"
+        "output_dir = \n"
+        "rustflags =\n"
+        "rustenv =\n"
+        "root_out_dir = .\n"
+        "target_gen_dir = gen/dylib\n"
+        "target_out_dir = obj/dylib\n"
+        "target_output_name = libdy\n"
+        "\n"
+        "build obj/dylib/libdy.so: rust_dylib ../../dylib/lib.rs | "
+        "../../dylib/lib.rs ../../cfg/spec2.json ../../cfg/spec1.json "
+        "obj/tc/setup.stamp || obj/cfg/grp.harddeps.stamp obj/cfg/act.stamp "
+        "obj/cfg/act2.stamp\n"
+        "  source_file_part = lib.rs\n"
+        "  source_name_part = lib\n"
+        "  externs =\n"
+        "  rustdeps =\n"
+        "  ldflags =\n"
+        "  sources = ../../dylib/lib.rs ../../cfg/spec2.json "
+        "../../cfg/spec1.json\n";
+    EXPECT_EQ(dy_expected, dy_out);
+
+    Target cdylib(stamp_setup.settings(), Label(SourceDir("//cdylib/"), "cd"));
+    cdylib.set_output_type(Target::LOADABLE_MODULE);
+    cdylib.visibility().SetPublic();
+    SourceFile cd_root("//cdylib/lib.rs");
+    cdylib.sources().push_back(cd_root);
+    cdylib.source_types_used().Set(SourceFile::SOURCE_RS);
+    cdylib.rust_values().set_crate_root(cd_root);
+    cdylib.rust_values().set_crate_type(RustValues::CRATE_CDYLIB);
+    cdylib.rust_values().crate_name() = "cd";
+    cdylib.private_deps().push_back(LabelTargetPair(&cfg_group));
+    cdylib.SetToolchain(stamp_setup.toolchain());
+    ASSERT_TRUE(cdylib.OnResolved(&err));
+
+    StringOutputBuffer cd_out;
+    NinjaRustBinaryTargetWriter cd_writer(&cdylib, cd_out);
+    cd_writer.Run();
+
+    const char cd_expected[] =
+        "crate_name = cd\n"
+        "crate_type = cdylib\n"
+        "output_extension = .so\n"
+        "output_dir = \n"
+        "rustflags =\n"
+        "rustenv =\n"
+        "root_out_dir = .\n"
+        "target_gen_dir = gen/cdylib\n"
+        "target_out_dir = obj/cdylib\n"
+        "target_output_name = libcd\n"
+        "\n"
+        "build obj/cdylib/libcd.so: rust_cdylib ../../cdylib/lib.rs | "
+        "../../cdylib/lib.rs ../../cfg/spec2.json ../../cfg/spec1.json "
+        "obj/tc/setup.stamp || obj/cfg/grp.harddeps.stamp obj/cfg/act.stamp "
+        "obj/cfg/act2.stamp\n"
+        "  source_file_part = lib.rs\n"
+        "  source_name_part = lib\n"
+        "  externs =\n"
+        "  rustdeps =\n"
+        "  ldflags =\n"
+        "  sources = ../../cdylib/lib.rs ../../cfg/spec2.json "
+        "../../cfg/spec1.json\n";
+    EXPECT_EQ(cd_expected, cd_out);
+
+    Target staticlib(stamp_setup.settings(),
+                     Label(SourceDir("//static/"), "st"));
+    staticlib.set_output_type(Target::STATIC_LIBRARY);
+    staticlib.visibility().SetPublic();
+    SourceFile st_root("//static/lib.rs");
+    staticlib.sources().push_back(st_root);
+    staticlib.source_types_used().Set(SourceFile::SOURCE_RS);
+    staticlib.rust_values().set_crate_root(st_root);
+    staticlib.rust_values().crate_name() = "st";
+    staticlib.private_deps().push_back(LabelTargetPair(&cfg_group));
+    staticlib.SetToolchain(stamp_setup.toolchain());
+    ASSERT_TRUE(staticlib.OnResolved(&err));
+
+    StringOutputBuffer st_out;
+    NinjaRustBinaryTargetWriter st_writer(&staticlib, st_out);
+    st_writer.Run();
+
+    const char st_expected[] =
+        "crate_name = st\n"
+        "crate_type = staticlib\n"
+        "output_extension = .a\n"
+        "output_dir = \n"
+        "rustflags =\n"
+        "rustenv =\n"
+        "root_out_dir = .\n"
+        "target_gen_dir = gen/static\n"
+        "target_out_dir = obj/static\n"
+        "target_output_name = libst\n"
+        "\n"
+        "build obj/static/libst.a: rust_staticlib ../../static/lib.rs | "
+        "../../static/lib.rs ../../cfg/spec2.json ../../cfg/spec1.json "
+        "obj/tc/setup.stamp || obj/cfg/grp.harddeps.stamp obj/cfg/act.stamp "
+        "obj/cfg/act2.stamp\n"
+        "  source_file_part = lib.rs\n"
+        "  source_name_part = lib\n"
+        "  externs =\n"
+        "  rustdeps =\n"
+        "  ldflags =\n"
+        "  sources = ../../static/lib.rs ../../cfg/spec2.json "
+        "../../cfg/spec1.json\n";
+    EXPECT_EQ(st_expected, st_out);
+  }
+}
+
+TEST_F(NinjaRustBinaryTargetWriterTest, ToolInputs) {
+  Err err;
+  TestWithScope setup;
+
+  Toolchain toolchain_with_inputs(
+      setup.settings(),
+      Label(SourceDir("//toolchain_with_inputs/"), "with_inputs"));
+  std::unique_ptr<Tool> rust_bin = Tool::CreateTool(RustTool::kRsToolBin);
+  TestWithScope::SetCommandForTool(
+      "{{rustenv}} rustc --crate-name {{crate_name}} {{source}} "
+      "--crate-type {{crate_type}} {{rustflags}} -o {{output}} "
+      "{{rustdeps}} {{externs}}",
+      rust_bin.get());
+  rust_bin->set_outputs(SubstitutionList::MakeForTest(
+      "{{root_out_dir}}/{{crate_name}}{{output_extension}}"));
+  rust_bin->set_inputs({SourceFile("//bin/rustc")});
+  toolchain_with_inputs.SetTool(std::move(rust_bin));
+  toolchain_with_inputs.ToolchainSetupComplete();
+
+  Target target(setup.settings(), Label(SourceDir("//foo/"), "bar"));
+  target.set_output_type(Target::EXECUTABLE);
+  target.visibility().SetPublic();
+  SourceFile main("//foo/main.rs");
+  target.sources().push_back(main);
+  target.source_types_used().Set(SourceFile::SOURCE_RS);
+  target.rust_values().set_crate_root(main);
+  target.rust_values().crate_name() = "foo_bar";
+  target.SetToolchain(&toolchain_with_inputs);
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  StringOutputBuffer out;
+  NinjaRustBinaryTargetWriter writer(&target, out);
+  writer.Run();
+
+  const char expected[] =
+      "crate_name = foo_bar\n"
+      "crate_type = bin\n"
+      "output_extension = \n"
+      "output_dir = \n"
+      "rustflags =\n"
+      "rustenv =\n"
+      "root_out_dir = .\n"
+      "\n"
+      "build ./foo_bar: rust_bin ../../foo/main.rs | ../../foo/main.rs "
+      "../../bin/rustc\n"
+      "  source_file_part = main.rs\n"
+      "  source_name_part = main\n"
+      "  externs =\n"
+      "  rustdeps =\n"
+      "  ldflags =\n"
+      "  sources = ../../foo/main.rs\n";
+  EXPECT_EQ(expected, out);
+}
+
 TEST_F(NinjaRustBinaryTargetWriterTest, CdylibDeps) {
   Err err;
   TestWithScope setup;