Write to ninja files via a standard API

This paves the way to have multiple targets in a single ninja file,
which in turn should allow us to shard targets without degrading build
performance.

Benchmarking reveals a statistically insignificant impact on
performance, and `siso dump` (crrev.com/c/8275910) reveals that the build graph
is identical to before.

Change-Id: I188fd8b075bb2f5cec9ff253c9220ae26a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/25740
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: Matt Stark <msta@google.com>
diff --git a/build/gen.py b/build/gen.py
index e4e377e..428c713 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -861,6 +861,7 @@
               'src/gn/ninja_c_binary_target_writer.cc',
               'src/gn/ninja_copy_target_writer.cc',
               'src/gn/ninja_create_bundle_target_writer.cc',
+              'src/gn/ninja_file.cc',
               'src/gn/ninja_generated_file_target_writer.cc',
               'src/gn/ninja_group_target_writer.cc',
               'src/gn/ninja_module_writer_util.cc',
diff --git a/integration_tests/simple/goldens/obj/hello.ninja b/integration_tests/simple/goldens/obj/hello.ninja
index b5f77f8..9ad97cf 100644
--- a/integration_tests/simple/goldens/obj/hello.ninja
+++ b/integration_tests/simple/goldens/obj/hello.ninja
@@ -4,6 +4,7 @@
 target_out_dir = obj
 target_output_name = hello
 
+
 build obj/hello.hello.o: cxx ../hello.cc
   source_file_part = hello.cc
   source_name_part = hello
diff --git a/integration_tests/simple/goldens/obj/hello_shared.ninja b/integration_tests/simple/goldens/obj/hello_shared.ninja
index 998ee5e..21ddb24 100644
--- a/integration_tests/simple/goldens/obj/hello_shared.ninja
+++ b/integration_tests/simple/goldens/obj/hello_shared.ninja
@@ -4,6 +4,7 @@
 target_out_dir = obj
 target_output_name = libhello_shared
 
+
 build obj/libhello_shared.hello_shared.o: cxx ../hello_shared.cc
   source_file_part = hello_shared.cc
   source_name_part = hello_shared
diff --git a/integration_tests/simple/goldens/obj/hello_static.ninja b/integration_tests/simple/goldens/obj/hello_static.ninja
index c554743..c2821dd 100644
--- a/integration_tests/simple/goldens/obj/hello_static.ninja
+++ b/integration_tests/simple/goldens/obj/hello_static.ninja
@@ -4,6 +4,7 @@
 target_out_dir = obj
 target_output_name = libhello_static
 
+
 build obj/libhello_static.hello_static.o: cxx ../hello_static.cc
   source_file_part = hello_static.cc
   source_name_part = hello_static
diff --git a/src/gn/ninja_action_target_writer.cc b/src/gn/ninja_action_target_writer.cc
index 0d01d07..b5490d3 100644
--- a/src/gn/ninja_action_target_writer.cc
+++ b/src/gn/ninja_action_target_writer.cc
@@ -26,7 +26,7 @@
 
 NinjaActionTargetWriter::~NinjaActionTargetWriter() = default;
 
-void NinjaActionTargetWriter::Run() {
+void NinjaActionTargetWriter::GenerateRules() {
   std::string custom_rule_name = WriteRuleDefinition();
 
   // Collect our deps to pass as additional "hard dependencies" for input deps.
@@ -78,7 +78,6 @@
   std::vector<OutputFile> input_deps = stamp_deps.implicit;
   input_deps.insert(input_deps.end(), stamp_deps.order_only.begin(),
                     stamp_deps.order_only.end());
-  out_ << std::endl;
 
   // Collects all output files for writing below.
   std::vector<OutputFile> output_files;
@@ -92,47 +91,33 @@
 
     // Write a rule that invokes the script once with the outputs as outputs,
     // and the data as inputs. It does not depend on the sources.
-    out_ << "build";
     SubstitutionWriter::GetListAsOutputFiles(
         settings_, target_->action_values().outputs(), &output_files);
-    WriteOutputs(output_files);
 
-    out_ << ": " << custom_rule_name;
-    if (!input_deps.empty()) {
-      // As in WriteSourceRules, we want to force this target to rebuild any
-      // time any of its dependencies change.
-      out_ << " |";
-      path_output_.WriteFiles(out_, input_deps);
-    }
-    if (!order_only_deps.empty()) {
-      // Write any order-only deps out for actions just like they are for
-      // binaries.
-      out_ << " ||";
-      path_output_.WriteFiles(out_, order_only_deps);
-    }
+    NinjaBuildEdge edge{
+        .rule = custom_rule_name,
+        .outputs = output_files,
+        .implicit_inputs = input_deps,
+        .order_only_inputs = order_only_deps,
+    };
+    AddValidationInputs(edge);
 
-    WriteValidations();
-    out_ << std::endl;
+    WriteNinjaVariablesForAction(edge.edge_vars);
 
-    if (target_->action_values().has_depfile()) {
-      WriteDepfile(SourceFile());
-    }
-
-    WriteNinjaVariablesForAction();
+    if (target_->action_values().has_depfile())
+      WriteDepfile(SourceFile(), edge.edge_vars);
 
     if (target_->pool().ptr) {
-      out_ << "  pool = ";
-      out_ << target_->pool().ptr->GetNinjaName(
-          settings_->default_toolchain_label());
-      out_ << std::endl;
+      edge.edge_vars.emplace_back("pool",
+                                  target_->pool().ptr->GetNinjaName(
+                                      settings_->default_toolchain_label()));
     }
+
+    AddEdge(std::move(edge));
   }
-  out_ << std::endl;
 
   // Write the phony, which doesn't need to depend on the data deps because they
   // have been added as order-only deps of the action output itself.
-  //
-  // TODO(thakis): If the action has just a single output, make things depend
   std::vector<OutputFile> stamp_file_order_only_deps;
   WriteStampOrPhonyForTarget(output_files, stamp_file_order_only_deps);
 }
@@ -152,7 +137,8 @@
   EscapeOptions args_escape_options;
   args_escape_options.mode = ESCAPE_NINJA_COMMAND;
 
-  out_ << "rule " << custom_rule_name << std::endl;
+  std::ostringstream rule_out;
+  rule_out << "rule " << custom_rule_name << std::endl;
 
   if (target_->action_values().uses_rsp_file()) {
     // Needs a response file. The unique_name part is for action_foreach so
@@ -163,17 +149,17 @@
     if (!target_->sources().empty())
       rspfile += ".$unique_name";
     rspfile += ".rsp";
-    out_ << "  rspfile = " << rspfile << std::endl;
+    rule_out << "  rspfile = " << rspfile << std::endl;
 
     // Response file contents.
-    out_ << "  rspfile_content =";
+    rule_out << "  rspfile_content =";
     for (const auto& arg :
          target_->action_values().rsp_file_contents().list()) {
-      out_ << " ";
+      rule_out << " ";
       SubstitutionWriter::WriteWithNinjaVariables(arg, args_escape_options,
-                                                  out_);
+                                                  rule_out);
     }
-    out_ << std::endl;
+    rule_out << std::endl;
   }
 
   // The command line requires shell escaping to properly handle filenames
@@ -182,29 +168,33 @@
                             settings_->build_settings()->root_path_utf8(),
                             ESCAPE_NINJA_COMMAND);
 
-  out_ << "  command = ";
-  command_output.WriteFile(out_, settings_->build_settings()->python_path());
-  out_ << " ";
-  command_output.WriteFile(out_, target_->action_values().script());
+  rule_out << "  command = ";
+  command_output.WriteFile(rule_out,
+                           settings_->build_settings()->python_path());
+  rule_out << " ";
+  command_output.WriteFile(rule_out, target_->action_values().script());
   for (const auto& arg : args.list()) {
-    out_ << " ";
-    SubstitutionWriter::WriteWithNinjaVariables(arg, args_escape_options, out_);
+    rule_out << " ";
+    SubstitutionWriter::WriteWithNinjaVariables(arg, args_escape_options,
+                                                rule_out);
   }
-  out_ << std::endl;
+  rule_out << std::endl;
   auto mnemonic = target_->action_values().mnemonic();
   if (mnemonic.empty())
     mnemonic = "ACTION";
-  out_ << "  description = " << mnemonic << " " << target_label << std::endl;
-  out_ << "  restat = 1" << std::endl;
+  rule_out << "  description = " << mnemonic << " " << target_label
+           << std::endl;
+  rule_out << "  restat = 1" << std::endl;
   const Tool* tool =
       target_->toolchain()->GetTool(GeneralTool::kGeneralToolAction);
   if (tool && tool->pool().ptr) {
-    out_ << "  pool = ";
-    out_ << tool->pool().ptr->GetNinjaName(
+    rule_out << "  pool = ";
+    rule_out << tool->pool().ptr->GetNinjaName(
         settings_->default_toolchain_label());
-    out_ << std::endl;
+    rule_out << std::endl;
   }
 
+  target_group_.custom_rules.push_back(rule_out.str());
   return custom_rule_name;
 }
 
@@ -221,31 +211,32 @@
 
   const Target::FileList& sources = target_->sources();
   for (size_t i = 0; i < sources.size(); i++) {
-    out_ << "build";
-    WriteOutputFilesForBuildLine(sources[i], output_files);
+    std::vector<OutputFile> cur_outputs;
+    SubstitutionWriter::ApplyListToSourceAsOutputFile(
+        target_, settings_, target_->action_values().outputs(), sources[i],
+        &cur_outputs);
+    output_files->insert(output_files->end(), cur_outputs.begin(),
+                         cur_outputs.end());
 
-    out_ << ": " << custom_rule_name << " ";
-    path_output_.WriteFile(out_, sources[i]);
-    if (!input_deps.empty()) {
-      // Using "|" for the dependencies forces all implicit dependencies to be
-      // fully up to date before running the action, and will re-run this
-      // action if any input dependencies change. This is important because
-      // this action may consume the outputs of previous steps.
-      out_ << " |";
-      path_output_.WriteFiles(out_, input_deps);
-    }
-    if (!order_only_deps.empty()) {
-      // Write any order-only deps out for actions just like they are written
-      // out for binaries.
-      out_ << " ||";
-      path_output_.WriteFiles(out_, order_only_deps);
-    }
-    WriteValidations();
-    out_ << std::endl;
+    // Using "|" for the dependencies forces all implicit dependencies to be
+    // fully up to date before running the action, and will re-run this
+    // action if any input dependencies change. This is important because
+    // this action may consume the outputs of previous steps.
+    // Write any order-only deps out for actions just like they are written
+    // out for binaries.
+    NinjaBuildEdge edge{
+        .rule = custom_rule_name,
+        .outputs = std::move(cur_outputs),
+        .explicit_inputs = {OutputFile(settings_->build_settings(),
+                                       sources[i])},
+        .implicit_inputs = input_deps,
+        .order_only_inputs = order_only_deps,
+    };
+    AddValidationInputs(edge);
 
     // Response files require a unique name be defined.
     if (target_->action_values().uses_rsp_file())
-      out_ << "  unique_name = " << i << std::endl;
+      edge.edge_vars.emplace_back("unique_name", std::to_string(i));
 
     // The required types is the union of the args and response file. This
     // might theoretically duplicate a definition if the same substitution is
@@ -255,60 +246,48 @@
     SubstitutionWriter::WriteNinjaVariablesForSource(
         target_, settings_, sources[i],
         target_->action_values().args().required_types(), args_escape_options,
-        out_);
+        edge.edge_vars);
     SubstitutionWriter::WriteNinjaVariablesForSource(
         target_, settings_, sources[i],
         target_->action_values().rsp_file_contents().required_types(),
-        args_escape_options, out_);
-    WriteNinjaVariablesForAction();
+        args_escape_options, edge.edge_vars);
+    WriteNinjaVariablesForAction(edge.edge_vars);
 
-    if (target_->action_values().has_depfile()) {
-      WriteDepfile(sources[i]);
-    }
+    if (target_->action_values().has_depfile())
+      WriteDepfile(sources[i], edge.edge_vars);
+
     if (target_->pool().ptr) {
-      out_ << "  pool = ";
-      out_ << target_->pool().ptr->GetNinjaName(
-          settings_->default_toolchain_label());
-      out_ << std::endl;
+      edge.edge_vars.emplace_back("pool",
+                                  target_->pool().ptr->GetNinjaName(
+                                      settings_->default_toolchain_label()));
     }
+
+    AddEdge(std::move(edge));
   }
 }
 
-void NinjaActionTargetWriter::WriteOutputFilesForBuildLine(
+void NinjaActionTargetWriter::WriteDepfile(
     const SourceFile& source,
-    std::vector<OutputFile>* output_files) {
-  size_t first_output_index = output_files->size();
-
-  SubstitutionWriter::ApplyListToSourceAsOutputFile(
-      target_, settings_, target_->action_values().outputs(), source,
-      output_files);
-
-  for (size_t i = first_output_index; i < output_files->size(); i++) {
-    out_ << " ";
-    WriteOutput((*output_files)[i]);
-  }
-}
-
-void NinjaActionTargetWriter::WriteDepfile(const SourceFile& source) {
-  out_ << "  depfile = ";
+    std::vector<NinjaVariable>& edge_vars) {
+  std::ostringstream ss;
   path_output_.WriteFile(
-      out_,
-      SubstitutionWriter::ApplyPatternToSourceAsOutputFile(
-          target_, settings_, target_->action_values().depfile(), source));
-  out_ << std::endl;
+      ss, SubstitutionWriter::ApplyPatternToSourceAsOutputFile(
+              target_, settings_, target_->action_values().depfile(), source));
+  edge_vars.emplace_back("depfile", ss.str());
   // Using "deps = gcc" allows Ninja to read and store the depfile content in
   // its internal database which improves performance, especially for large
   // depfiles. The use of this feature with depfiles that contain multiple
   // outputs require Ninja version 1.9.0 or newer.
   if (settings_->build_settings()->ninja_required_version() >=
       Version{1, 9, 0}) {
-    out_ << "  deps = gcc" << std::endl;
+    edge_vars.emplace_back("deps", "gcc");
   }
 }
 
-void NinjaActionTargetWriter::WriteNinjaVariablesForAction() {
+void NinjaActionTargetWriter::WriteNinjaVariablesForAction(
+    std::vector<NinjaVariable>& edge_vars) {
   SubstitutionBits subst;
   target_->action_values().args().FillRequiredTypes(&subst);
-  WriteRustCompilerVars(subst, /*indent=*/true, /*always_write=*/false);
-  WriteCCompilerVars(subst, /*indent=*/true, /*respect_source_types=*/false);
+  WriteRustCompilerVars(subst, /*always_write=*/false, edge_vars);
+  WriteCCompilerVars(subst, /*respect_source_used=*/false, edge_vars);
 }
diff --git a/src/gn/ninja_action_target_writer.h b/src/gn/ninja_action_target_writer.h
index eff087b..930a6df 100644
--- a/src/gn/ninja_action_target_writer.h
+++ b/src/gn/ninja_action_target_writer.h
@@ -18,13 +18,9 @@
   NinjaActionTargetWriter(const Target* target, std::ostream& out);
   ~NinjaActionTargetWriter() override;
 
-  void Run() override;
+  void GenerateRules() override;
 
  private:
-  FRIEND_TEST_ALL_PREFIXES(NinjaActionTargetWriter,
-                           WriteOutputFilesForBuildLine);
-  FRIEND_TEST_ALL_PREFIXES(NinjaActionTargetWriter,
-                           WriteOutputFilesForBuildLineWithDepfile);
   FRIEND_TEST_ALL_PREFIXES(NinjaActionTargetWriter, WriteArgsSubstitutions);
 
   // Writes the Ninja rule for invoking the script.
@@ -43,17 +39,12 @@
                         const std::vector<OutputFile>& order_only_deps,
                         std::vector<OutputFile>* output_files);
 
-  // Writes the output files generated by the output template for the given
-  // source file. This will start with a space and will not include a newline.
-  // Appends the output files to the given vector.
-  void WriteOutputFilesForBuildLine(const SourceFile& source,
-                                    std::vector<OutputFile>* output_files);
-
-  void WriteDepfile(const SourceFile& source);
+  void WriteDepfile(const SourceFile& source,
+                    std::vector<NinjaVariable>& edge_vars);
 
   // Writes variables that we make available to all actions, irrespective
   // of whether they're associated with a specific source file.
-  void WriteNinjaVariablesForAction();
+  void WriteNinjaVariablesForAction(std::vector<NinjaVariable>& edge_vars);
 
   // Path output writer that doesn't do any escaping or quoting. It does,
   // however, convert slashes.  Used for
diff --git a/src/gn/ninja_action_target_writer_unittest.cc b/src/gn/ninja_action_target_writer_unittest.cc
index e855582..cd3b0e7 100644
--- a/src/gn/ninja_action_target_writer_unittest.cc
+++ b/src/gn/ninja_action_target_writer_unittest.cc
@@ -14,29 +14,6 @@
 #include "util/build_config.h"
 #include "util/test/test.h"
 
-TEST(NinjaActionTargetWriter, WriteOutputFilesForBuildLine) {
-  Err err;
-  TestWithScope setup;
-
-  Target target(setup.settings(), Label(SourceDir("//foo/"), "bar"));
-  target.set_output_type(Target::ACTION_FOREACH);
-  target.action_values().outputs() =
-      SubstitutionList::MakeForTest("//out/Debug/gen/a b{{source_name_part}}.h",
-                                    "//out/Debug/gen/{{source_name_part}}.cc");
-
-  target.SetToolchain(setup.toolchain());
-  ASSERT_TRUE(target.OnResolved(&err));
-
-  std::ostringstream out;
-  NinjaActionTargetWriter writer(&target, out);
-
-  SourceFile source("//foo/bar.in");
-  std::vector<OutputFile> output_files;
-  writer.WriteOutputFilesForBuildLine(source, &output_files);
-
-  EXPECT_EQ(" gen/a$ bbar.h gen/bar.cc", out.str());
-}
-
 // Tests an action with no sources.
 TEST(NinjaActionTargetWriter, ActionNoSources) {
   Err err;
@@ -66,6 +43,7 @@
   description = ACTION //foo++:bar()
   restat = 1
 
+
 build foo.out: __foo___bar___rule | ../../foo++/script.py ../../foo++/included.txt
 
 build phony/foo++/bar: phony foo.out
@@ -110,6 +88,7 @@
   description = ACTION //foo:bar()
   restat = 1
 
+
 build foo.out: __foo_bar___rule | ../../foo/script.py ../../foo/included.txt
   pool = console
 
@@ -151,6 +130,7 @@
       "  description = ACTION //foo:bar()\n"
       "  restat = 1\n"
       "\n"
+      "\n"
       "build foo.out: __foo_bar___rule | ../../foo/script.py "
       "../../foo/included.txt ../../foo/source.txt\n"
       "\n"
@@ -208,6 +188,7 @@
       "  description = ACTION //foo:bar()\n"
       "  restat = 1\n"
       "\n"
+      "\n"
       "build foo.out: __foo_bar___rule | ../../foo/script.py "
       "../../foo/included.txt ../../foo/source.txt phony/foo/dep || "
       "phony/foo/datadep\n"
@@ -284,6 +265,8 @@
 #endif
       "  description = ACTION //foo:bar()\n"
       "  restat = 1\n"
+      "\n"
+      "\n"
       "build phony/foo/bar.inputdeps: phony ../../foo/script.py "
       "../../foo/included.txt phony/foo/dep\n"
       "\n"
@@ -291,6 +274,7 @@
       "phony/foo/bar.inputdeps || phony/foo/bundle_data_dep "
       "phony/foo/datadep\n"
       "  source_name_part = input1\n"
+      "\n"
       "build input2.out: __foo_bar___rule ../../foo/input2.txt | "
       "phony/foo/bar.inputdeps || phony/foo/bundle_data_dep "
       "phony/foo/datadep\n"
@@ -351,6 +335,8 @@
 #endif
       "  description = ACTION //foo:bar()\n"
       "  restat = 1\n"
+      "\n"
+      "\n"
       "build phony/foo/bar.inputdeps: phony ../../foo/script.py "
       "../../foo/included.txt\n"
       "\n"
@@ -359,6 +345,7 @@
       "  source_name_part = input1\n"
       "  depfile = gen/input1.d\n"
       "  deps = gcc\n"
+      "\n"
       "build input2.out: __foo_bar___rule ../../foo/input2.txt"
       " | phony/foo/bar.inputdeps\n"
       "  source_name_part = input2\n"
@@ -410,6 +397,7 @@
       "  description = ACTION //foo:bar()\n"
       "  restat = 1\n"
       "\n"
+      "\n"
       "build input1.out: __foo_bar___rule ../../foo/input1.txt"
       " | ../../foo/script.py\n"
       // Necessary for the rspfile defined in the rule.
@@ -464,6 +452,7 @@
       "  description = ACTION //foo:bar()\n"
       "  restat = 1\n"
       "\n"
+      "\n"
       "build input1.out: __foo_bar___rule ../../foo/input1.txt"
       " | ../../foo/script.py\n"
       // Substitution for the args.
@@ -510,6 +499,7 @@
         "  description = ACTION //foo:foo()\n"
         "  restat = 1\n"
         "\n"
+        "\n"
         "build foo.out: __foo_foo___rule | ../../foo/script.py"
         " ../../foo/input1.txt phony/foo/dep\n"
         "\n"
@@ -539,6 +529,7 @@
         "  description = ACTION //bar:bar()\n"
         "  restat = 1\n"
         "\n"
+        "\n"
         // Do not have obj/foo/dep.stamp as dependency.
         "build bar.out: __bar_bar___rule | ../../bar/script.py"
         " ../../bar/input1.txt phony/foo/foo\n"
@@ -595,6 +586,7 @@
         "  description = ACTION //foo:foo()\n"
         "  restat = 1\n"
         "\n"
+        "\n"
         "build foo.out: __foo_foo___rule | ../../foo/script.py"
         " ../../foo/input1.txt\n"
         "  rustenv = my_rustenv\n"
@@ -647,6 +639,7 @@
       R"(  description = ACTION //foo:bar()
   restat = 1
 
+
 build foo.out: __foo_bar___rule | ../../foo/my$ script.py ../../foo/input$ file.txt
 
 build phony/foo/bar: phony foo.out
@@ -691,6 +684,7 @@
       "  description = ACTION //foo:bar()\n"
       "  restat = 1\n"
       "\n"
+      "\n"
       "build foo.out: __foo_bar___rule | ../../foo/script.py |@ phony/foo/val\n"
       "\n"
       "build phony/foo/bar: phony foo.out |@ phony/foo/val\n";
diff --git a/src/gn/ninja_binary_target_writer.cc b/src/gn/ninja_binary_target_writer.cc
index 8c8d2a0..3e465bc 100644
--- a/src/gn/ninja_binary_target_writer.cc
+++ b/src/gn/ninja_binary_target_writer.cc
@@ -99,19 +99,17 @@
 
 NinjaBinaryTargetWriter::~NinjaBinaryTargetWriter() = default;
 
-void NinjaBinaryTargetWriter::Run() {
+void NinjaBinaryTargetWriter::GenerateRules() {
   if (target_->source_types_used().RustSourceUsed()) {
     NinjaRustBinaryTargetWriter writer(target_, out_);
     writer.SetResolvedTargetData(GetResolvedTargetData());
-    writer.SetNinjaOutputs(ninja_outputs_);
-    writer.Run();
+    target_group_ = writer.GenerateTargetGroup();
     return;
   }
 
   NinjaCBinaryTargetWriter writer(target_, out_);
   writer.SetResolvedTargetData(GetResolvedTargetData());
-  writer.SetNinjaOutputs(ninja_outputs_);
-  writer.Run();
+  target_group_ = writer.GenerateTargetGroup();
 }
 
 void NinjaBinaryTargetWriter::WritePublicModuleMap(std::ostream& out,
@@ -160,7 +158,7 @@
 
 std::vector<OutputFile>
 NinjaBinaryTargetWriter::WriteInputsStampOrPhonyAndGetDep(
-    size_t num_output_uses) const {
+    size_t num_output_uses) {
   CHECK(target_->toolchain()) << "Toolchain not set on target "
                               << target_->label().GetUserVisibleName(true);
 
@@ -183,7 +181,7 @@
 
   std::vector<OutputFile> outs;
   for (const SourceFile* source : inputs)
-    outs.push_back(OutputFile(settings_->build_settings(), *source));
+    outs.emplace_back(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.
@@ -207,17 +205,13 @@
            GeneralTool::kGeneralToolStamp;
   }
 
-  out_ << "build ";
-  WriteOutput(stamp_or_phony);
-  out_ << ": " << tool;
+  AddEdge(NinjaBuildEdge{
+      .rule = tool,
+      .outputs = {stamp_or_phony},
+      .explicit_inputs = outs,
+      .is_target_output = false,
+  });
 
-  // File inputs.
-  for (const auto* input : inputs) {
-    out_ << " ";
-    path_output_.WriteFile(out_, *input);
-  }
-
-  out_ << std::endl;
   return {stamp_or_phony};
 }
 
@@ -372,46 +366,44 @@
 
 void NinjaBinaryTargetWriter::WriteCompilerBuildLine(
     const std::vector<SourceFile>& sources,
-    const std::vector<OutputFile>& extra_deps,
-    const std::vector<OutputFile>& order_only_deps,
+    std::vector<OutputFile> extra_deps,
+    std::vector<OutputFile> order_only_deps,
     const Tool* tool,
-    const std::vector<OutputFile>& outputs,
+    std::vector<OutputFile> outputs,
     bool can_write_source_info,
     bool restat_output_allowed) {
-  out_ << "build";
-  WriteOutputs(outputs);
-
-  out_ << ": " << rule_prefix_ << tool->name();
-  path_output_.WriteFiles(out_, sources);
-
-  if (!extra_deps.empty() || !tool->inputs().empty()) {
-    out_ << " |";
-    path_output_.WriteFiles(out_, extra_deps);
-    if (auto phony = tool->inputs_phony_or_file(rule_prefix_,
-                                                *settings_->build_settings())) {
-      out_ << " ";
-      path_output_.WriteFile(out_, *phony);
-    }
+  if (auto phony = tool->inputs_phony_or_file(rule_prefix_,
+                                              *settings_->build_settings())) {
+    extra_deps.push_back(*phony);
   }
 
-  if (!order_only_deps.empty()) {
-    out_ << " ||";
-    path_output_.WriteFiles(out_, order_only_deps);
-  }
-  WriteValidations();
-  out_ << std::endl;
+  NinjaBuildEdge edge{
+      .rule = rule_prefix_ + tool->name(),
+      .outputs = std::move(outputs),
+      .explicit_inputs = ToOutputFiles(sources),
+      .implicit_inputs = std::move(extra_deps),
+      .order_only_inputs = std::move(order_only_deps),
+  };
+  AddValidationInputs(edge);
 
   if (!sources.empty() && can_write_source_info) {
-    out_ << "  " << "source_file_part = " << sources[0].GetName();
-    out_ << std::endl;
-    out_ << "  " << "source_name_part = "
-         << FindFilenameNoExtension(&sources[0].value());
-    out_ << std::endl;
+    edge.edge_vars.emplace_back("source_file_part", sources[0].GetName());
+    edge.edge_vars.emplace_back(
+        "source_name_part",
+        std::string(FindFilenameNoExtension(&sources[0].value())));
   }
 
   if (restat_output_allowed) {
-    out_ << "  restat = 1" << std::endl;
+    edge.edge_vars.emplace_back("restat", "1");
   }
+
+  if (target_->pool().ptr) {
+    edge.edge_vars.emplace_back("pool",
+                                target_->pool().ptr->GetNinjaName(
+                                    settings_->default_toolchain_label()));
+  }
+
+  AddEdge(std::move(edge));
 }
 
 void NinjaBinaryTargetWriter::WriteCustomLinkerFlags(std::ostream& out,
@@ -467,7 +459,7 @@
   WriteLibrarySearchPath(out, tool);
 
   if (optional_def_file) {
-    out_ << " /DEF:";
+    out << " /DEF:";
     path_output_.WriteFile(out, *optional_def_file);
   }
 }
diff --git a/src/gn/ninja_binary_target_writer.h b/src/gn/ninja_binary_target_writer.h
index 99b2b72..93774a6 100644
--- a/src/gn/ninja_binary_target_writer.h
+++ b/src/gn/ninja_binary_target_writer.h
@@ -20,7 +20,7 @@
   NinjaBinaryTargetWriter(const Target* target, std::ostream& out);
   ~NinjaBinaryTargetWriter() override;
 
-  void Run() override;
+  void GenerateRules() override;
   void WritePublicModuleMap(std::ostream& out, const SourceDir& out_dir);
   void WritePrivateModuleMap(std::ostream& out, const SourceDir& out_dir);
 
@@ -42,7 +42,7 @@
   // If there are no implicit dependencies and no extra target dependencies
   // are passed in, this returns an empty vector.
   std::vector<OutputFile> WriteInputsStampOrPhonyAndGetDep(
-      size_t num_phony_uses) const;
+      size_t num_phony_uses);
 
   // Gets all target dependencies and classifies them, as well as accumulates
   // object files from source sets we need to link.
@@ -60,10 +60,10 @@
                           ClassifiedDeps* classified_deps) const;
 
   void WriteCompilerBuildLine(const std::vector<SourceFile>& sources,
-                              const std::vector<OutputFile>& extra_deps,
-                              const std::vector<OutputFile>& order_only_deps,
+                              std::vector<OutputFile> extra_deps,
+                              std::vector<OutputFile> order_only_deps,
                               const Tool* tool,
-                              const std::vector<OutputFile>& outputs,
+                              std::vector<OutputFile> outputs,
                               bool can_write_source_info = true,
                               bool restat_output_allowed = false);
 
diff --git a/src/gn/ninja_binary_target_writer_unittest.cc b/src/gn/ninja_binary_target_writer_unittest.cc
index d8e12bc..1c34ff0 100644
--- a/src/gn/ninja_binary_target_writer_unittest.cc
+++ b/src/gn/ninja_binary_target_writer_unittest.cc
@@ -42,15 +42,18 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = bar\n"
       "\n"
+      "\n"
       "build obj/foo/bar.input1.o: cxx ../../foo/input1.cc\n"
       "  source_file_part = input1.cc\n"
       "  source_name_part = input1\n"
+      "\n"
       "build obj/foo/bar.input2.o: cxx ../../foo/input2.cc\n"
       "  source_file_part = input2.cc\n"
       "  source_name_part = input2\n"
       "\n"
       "build phony/foo/bar.linkdeps: phony obj/foo/bar.input1.o "
       "obj/foo/bar.input2.o ../../foo/input3.o ../../foo/input4.obj\n"
+      "\n"
       "build phony/foo/bar: phony phony/foo/bar.linkdeps\n";
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
@@ -143,12 +146,14 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build obj/foo/bar.source1.o: cxx ../../foo/source1.cc | "
         "../../foo/input1 ../../foo/input2\n"
         "  source_file_part = source1.cc\n"
         "  source_name_part = source1\n"
         "\n"
         "build phony/foo/bar.linkdeps: phony obj/foo/bar.source1.o\n"
+        "\n"
         "build phony/foo/bar: phony phony/foo/bar.linkdeps\n";
     std::string out_str = out.str();
     EXPECT_EQ(expected, out_str);
@@ -180,12 +185,15 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build phony/foo/bar.inputs: phony "
         "../../foo/input1 ../../foo/input2\n"
+        "\n"
         "build obj/foo/bar.source1.o: cxx ../../foo/source1.cc | "
         "phony/foo/bar.inputs\n"
         "  source_file_part = source1.cc\n"
         "  source_name_part = source1\n"
+        "\n"
         "build obj/foo/bar.source2.o: cxx ../../foo/source2.cc | "
         "phony/foo/bar.inputs\n"
         "  source_file_part = source2.cc\n"
@@ -193,6 +201,7 @@
         "\n"
         "build phony/foo/bar.linkdeps: phony obj/foo/bar.source1.o "
         "obj/foo/bar.source2.o\n"
+        "\n"
         "build phony/foo/bar: phony phony/foo/bar.linkdeps\n";
     std::string out_str = out.str();
     EXPECT_EQ(expected, out_str);
@@ -240,12 +249,14 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = b\n"
       "\n"
+      "\n"
       "build obj/foo/b.source1.o: cxx ../../foo/source1.cc | "
       "phony/foo/a.public_inputs || phony/foo/a\n"
       "  source_file_part = source1.cc\n"
       "  source_name_part = source1\n"
       "\n"
       "build phony/foo/b.linkdeps: phony obj/foo/b.source1.o || phony/foo/a\n"
+      "\n"
       "build phony/foo/b: phony phony/foo/b.linkdeps\n";
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
diff --git a/src/gn/ninja_bundle_data_target_writer.cc b/src/gn/ninja_bundle_data_target_writer.cc
index d6c2e56..01373b1 100644
--- a/src/gn/ninja_bundle_data_target_writer.cc
+++ b/src/gn/ninja_bundle_data_target_writer.cc
@@ -14,7 +14,7 @@
 
 NinjaBundleDataTargetWriter::~NinjaBundleDataTargetWriter() = default;
 
-void NinjaBundleDataTargetWriter::Run() {
+void NinjaBundleDataTargetWriter::GenerateRules() {
   std::vector<OutputFile> output_files;
   for (const SourceFile& source_file : target_->sources()) {
     output_files.push_back(
diff --git a/src/gn/ninja_bundle_data_target_writer.h b/src/gn/ninja_bundle_data_target_writer.h
index 720c593..be87dd8 100644
--- a/src/gn/ninja_bundle_data_target_writer.h
+++ b/src/gn/ninja_bundle_data_target_writer.h
@@ -13,7 +13,7 @@
   NinjaBundleDataTargetWriter(const Target* target, std::ostream& out);
   ~NinjaBundleDataTargetWriter() override;
 
-  void Run() override;
+  void GenerateRules() override;
 
  private:
   NinjaBundleDataTargetWriter(const NinjaBundleDataTargetWriter&) = delete;
diff --git a/src/gn/ninja_c_binary_target_writer.cc b/src/gn/ninja_c_binary_target_writer.cc
index a56f587..155d0de 100644
--- a/src/gn/ninja_c_binary_target_writer.cc
+++ b/src/gn/ninja_c_binary_target_writer.cc
@@ -11,15 +11,12 @@
 #include <set>
 #include <sstream>
 
-#include "base/strings/string_util.h"
 #include "gn/builtin_tool.h"
 #include "gn/c_substitution_type.h"
 #include "gn/config_values_extractors.h"
-#include "gn/deps_iterator.h"
 #include "gn/err.h"
 #include "gn/escape.h"
 #include "gn/filesystem_utils.h"
-#include "gn/general_tool.h"
 #include "gn/ninja_module_writer_util.h"
 #include "gn/ninja_target_command_util.h"
 #include "gn/ninja_utils.h"
@@ -63,7 +60,7 @@
 
 NinjaCBinaryTargetWriter::~NinjaCBinaryTargetWriter() = default;
 
-void NinjaCBinaryTargetWriter::Run() {
+void NinjaCBinaryTargetWriter::GenerateRules() {
   std::set<ClangModuleDep> module_dep_info =
       GetModuleDepsInformation(target_, resolved());
 
@@ -176,8 +173,7 @@
     const std::set<ClangModuleDep>& module_dep_info) {
   const SubstitutionBits& subst = target_->toolchain()->substitution_bits();
 
-  WriteCCompilerVars(subst, /*indent=*/false,
-                     /*respect_source_types_used=*/true);
+  WriteCCompilerVars(subst, /*respect_source_used=*/true);
 
   WriteModuleNameSubstitution();
 
@@ -198,11 +194,12 @@
 void NinjaCBinaryTargetWriter::WriteModuleNameSubstitution() {
   if (target_->toolchain()->substitution_bits().used.count(
           &CSubstitutionModuleName)) {
-    out_ << CSubstitutionModuleName.ninja_name << " = ";
     EscapeOptions options;
     options.mode = ESCAPE_NINJA;
-    EscapeStringToStream(out_, target_->module_name(), options);
-    out_ << std::endl;
+    std::ostringstream val;
+    EscapeStringToStream(val, target_->module_name(), options);
+    target_group_.target_vars.emplace_back(CSubstitutionModuleName.ninja_name,
+                                           val.str());
   }
 }
 
@@ -211,15 +208,11 @@
     const std::set<ClangModuleDep>& module_dep_info,
     bool include_self) {
   if (target_->toolchain()->substitution_bits().used.count(substitution)) {
-    EscapeOptions options;
-    options.mode = ESCAPE_NINJA_COMMAND;
-
-    out_ << substitution->ninja_name << " =";
+    std::ostringstream val;
     for (const auto& module_dep : module_dep_info) {
-      module_dep.Write(out_, path_output_, include_self);
+      module_dep.Write(val, path_output_, include_self);
     }
-
-    out_ << std::endl;
+    target_group_.target_vars.emplace_back(substitution->ninja_name, val.str());
   }
 }
 
@@ -312,7 +305,7 @@
 
   // This build line needs a custom language-specific flags value. Rule-specific
   // variables are just indented underneath the rule line.
-  out_ << "  " << flag_type->ninja_name << " =";
+  std::ostringstream flag_val;
 
   // Each substitution flag is overwritten in the target rule to replace the
   // implicitly generated -include flag with the -x <header lang> flag required
@@ -321,27 +314,26 @@
   if (tool_name == CTool::kCToolCc) {
     RecursiveTargetConfigStringsToStream(kRecursiveWriterKeepDuplicates,
                                          target_, &ConfigValues::cflags_c, opts,
-                                         out_);
+                                         flag_val);
   } else if (tool_name == CTool::kCToolCxx) {
     RecursiveTargetConfigStringsToStream(kRecursiveWriterKeepDuplicates,
                                          target_, &ConfigValues::cflags_cc,
-                                         opts, out_);
+                                         opts, flag_val);
   } else if (tool_name == CTool::kCToolObjC) {
     RecursiveTargetConfigStringsToStream(kRecursiveWriterKeepDuplicates,
                                          target_, &ConfigValues::cflags_objc,
-                                         opts, out_);
+                                         opts, flag_val);
   } else if (tool_name == CTool::kCToolObjCxx) {
     RecursiveTargetConfigStringsToStream(kRecursiveWriterKeepDuplicates,
                                          target_, &ConfigValues::cflags_objcc,
-                                         opts, out_);
+                                         opts, flag_val);
   }
 
   // Append the command to specify the language of the .gch file.
-  out_ << " -x " << GetPCHLangForToolType(tool_name);
+  flag_val << " -x " << GetPCHLangForToolType(tool_name);
 
-  // Write two blank lines to help separate the PCH build lines from the
-  // regular source build lines.
-  out_ << std::endl << std::endl;
+  target_group_.edges.back().edge_vars.emplace_back(flag_type->ninja_name,
+                                                    flag_val.str());
 }
 
 void NinjaCBinaryTargetWriter::WriteWindowsPCHCommand(
@@ -368,16 +360,15 @@
 
   // This build line needs a custom language-specific flags value. Rule-specific
   // variables are just indented underneath the rule line.
-  out_ << "  " << flag_type->ninja_name << " =";
+  std::ostringstream flag_val;
 
   // Append the command to generate the .pch file.
   // This adds the value to the existing flag instead of overwriting it.
-  out_ << " ${" << flag_type->ninja_name << "}";
-  out_ << " /Yc" << target_->config_values().precompiled_header();
+  flag_val << " ${" << flag_type->ninja_name << "} /Yc"
+           << target_->config_values().precompiled_header();
 
-  // Write two blank lines to help separate the PCH build lines from the
-  // regular source build lines.
-  out_ << std::endl << std::endl;
+  target_group_.edges.back().edge_vars.emplace_back(flag_type->ninja_name,
+                                                    flag_val.str());
 }
 
 void NinjaCBinaryTargetWriter::WriteSources(
@@ -475,7 +466,6 @@
 
       WriteCompilerBuildLine({source}, deps, order_only_deps, tool,
                              tool_outputs);
-      WritePool(out_);
     }
 
     // It's theoretically possible for a compiler to produce more than one
@@ -492,8 +482,6 @@
       extra_files->push_back(tool_outputs[i]);
     }
   }
-
-  out_ << std::endl;
 }
 
 void NinjaCBinaryTargetWriter::WriteSwiftSources(
@@ -529,8 +517,6 @@
                          swift_order_only_deps.vector(), tool, *output_files,
                          /*can_write_source_info=*/false,
                          /*restat_output_allowed=*/true);
-
-  out_ << std::endl;
 }
 
 void NinjaCBinaryTargetWriter::WriteSourceSetStamp(
@@ -569,53 +555,53 @@
   path.append(".linkdeps");
   link_phony = OutputFile(std::move(path));
 
-  out_ << "build ";
-  path_output_.WriteFile(out_, link_phony);
-  out_ << ": " << BuiltinTool::kBuiltinToolPhony;
-  path_output_.WriteFiles(out_, link_files);
-  if (!order_only_deps.empty()) {
-    out_ << " ||";
-    path_output_.WriteFiles(out_, order_only_deps);
-  }
-  out_ << std::endl;
+  NinjaBuildEdge link_edge{
+      .rule = BuiltinTool::kBuiltinToolPhony,
+      .outputs = {link_phony},
+      .explicit_inputs = link_files,
+      .order_only_inputs = order_only_deps,
+      .is_target_output = false,
+  };
+  AddEdge(std::move(link_edge));
 
   // 2. Default phony target containing all files (including additional
   // outputs). Depend on the .link target to avoid duplicating object files.
-  out_ << "build ";
-  path_output_.WriteFile(out_, target_->dependency_output());
-  out_ << ": " << BuiltinTool::kBuiltinToolPhony;
-  out_ << " ";
-  path_output_.WriteFile(out_, link_phony);
+  NinjaBuildEdge default_edge{
+      .rule = BuiltinTool::kBuiltinToolPhony,
+      .outputs = {target_->dependency_output()},
+      .explicit_inputs = {link_phony},
+      .is_target_output = false,
+  };
 
   // Collect non-object files (additional outputs) to add here.
-  std::vector<OutputFile> non_object_files;
   for (const auto& file : object_files) {
     if (!file.AsSourceFile(build_settings).IsObjectType()) {
-      non_object_files.push_back(file);
+      default_edge.explicit_inputs.push_back(file);
     }
   }
-  path_output_.WriteFiles(out_, non_object_files);
-  out_ << std::endl;
+  AddEdge(std::move(default_edge));
 }
 
 void NinjaCBinaryTargetWriter::WriteLinkerStuff(
-    const std::vector<OutputFile>& object_files,
+    std::vector<OutputFile> object_files,
     const std::vector<SourceFile>& other_files,
     const std::vector<OutputFile>& input_deps) {
   std::vector<OutputFile> output_files;
   SubstitutionWriter::ApplyListToLinkerAsOutputFile(
       target_, tool_, tool_->outputs(), &output_files);
 
-  out_ << "build";
-  WriteOutputs(output_files);
-
-  out_ << ": " << rule_prefix_ << tool_->name();
+  NinjaBuildEdge edge{
+      .rule = rule_prefix_ + tool_->name(),
+      .outputs = std::move(output_files),
+  };
 
   ClassifiedDeps classified_deps = GetClassifiedDeps();
 
   // Object files.
-  path_output_.WriteFiles(out_, object_files);
-  path_output_.WriteFiles(out_, classified_deps.extra_object_files);
+  edge.explicit_inputs = std::move(object_files);
+  edge.explicit_inputs.insert(edge.explicit_inputs.end(),
+                              classified_deps.extra_object_files.begin(),
+                              classified_deps.extra_object_files.end());
 
   // Dependencies.
   std::vector<OutputFile> implicit_deps;
@@ -638,8 +624,7 @@
       solibs.push_back(cur->link_output_file());
     } else {
       // Normal case, just link to this target.
-      out_ << " ";
-      path_output_.WriteFile(out_, cur->link_output_file());
+      edge.explicit_inputs.push_back(cur->link_output_file());
     }
   }
 
@@ -712,11 +697,7 @@
     }
   }
 
-  // Append implicit dependencies collected above.
-  if (!implicit_deps.empty()) {
-    out_ << " |";
-    path_output_.WriteFiles(out_, implicit_deps);
-  }
+  edge.implicit_inputs = std::move(implicit_deps);
 
   // Append data dependencies as order-only dependencies.
   //
@@ -731,87 +712,77 @@
   // on the sources, there is already an implicit order-only dependency.
   // However, it's extra work to separate these out and there's no disadvantage
   // to listing them again.
-  WriteOrderOnlyDependencies(classified_deps.non_linkable_deps);
+  edge.order_only_inputs =
+      GetOrderOnlyDepsFromNonLinkableDeps(classified_deps.non_linkable_deps);
 
-  WriteValidations();
-
-  // End of the link "build" line.
-  out_ << std::endl;
+  AddValidationInputs(edge);
 
   // The remaining things go in the inner scope of the link line.
   if (target_->output_type() == Target::EXECUTABLE ||
       target_->output_type() == Target::SHARED_LIBRARY ||
       target_->output_type() == Target::LOADABLE_MODULE) {
-    out_ << "  ldflags =";
-    WriteLinkerFlags(out_, tool_, optional_def_file);
-    out_ << std::endl;
-    out_ << "  libs =";
-    WriteLibs(out_, tool_);
-    out_ << std::endl;
-    out_ << "  frameworks =";
-    WriteFrameworks(out_, tool_);
-    out_ << std::endl;
-    out_ << "  swiftmodules =";
-    WriteSwiftModules(out_, tool_, swiftmodules);
-    out_ << std::endl;
+    {
+      std::ostringstream ss;
+      WriteLinkerFlags(ss, tool_, optional_def_file);
+      edge.edge_vars.emplace_back("ldflags", ss.str());
+    }
+    {
+      std::ostringstream ss;
+      WriteLibs(ss, tool_);
+      edge.edge_vars.emplace_back("libs", ss.str());
+    }
+    {
+      std::ostringstream ss;
+      WriteFrameworks(ss, tool_);
+      edge.edge_vars.emplace_back("frameworks", ss.str());
+    }
+    {
+      std::ostringstream ss;
+      WriteSwiftModules(ss, tool_, swiftmodules);
+      edge.edge_vars.emplace_back("swiftmodules", ss.str());
+    }
   } else if (target_->output_type() == Target::STATIC_LIBRARY) {
-    out_ << "  arflags =";
+    std::ostringstream ss;
     RecursiveTargetConfigStringsToStream(kRecursiveWriterKeepDuplicates,
                                          target_, &ConfigValues::arflags,
-                                         GetFlagOptions(), out_);
-    out_ << std::endl;
+                                         GetFlagOptions(), ss);
+    edge.edge_vars.emplace_back("arflags", ss.str());
   }
-  WriteOutputSubstitutions();
-  WriteLibsList("solibs", solibs);
-  WriteLibsList("rlibs", transitive_rustlibs);
-  WritePool(out_);
-}
 
-void NinjaCBinaryTargetWriter::WriteOutputSubstitutions() {
   const std::string output_extension =
       SubstitutionWriter::GetLinkerSubstitution(target_, tool_,
                                                 &SubstitutionOutputExtension);
-  out_ << "  output_extension =";
-  if (!output_extension.empty()) {
-    out_ << " " << output_extension;
-  }
-  out_ << std::endl;
+  edge.edge_vars.emplace_back("output_extension", output_extension);
 
   const std::string output_dir = SubstitutionWriter::GetLinkerSubstitution(
       target_, tool_, &SubstitutionOutputDir);
-  out_ << "  output_dir =";
-  if (!output_dir.empty()) {
-    out_ << " " << output_dir;
+  edge.edge_vars.emplace_back("output_dir", output_dir);
+
+  if (!solibs.empty()) {
+    std::ostringstream ss;
+    PathOutput output(path_output_.current_dir(),
+                      settings_->build_settings()->root_path_utf8(),
+                      ESCAPE_NINJA_COMMAND);
+    output.WriteFiles(ss, solibs);
+    edge.edge_vars.emplace_back("solibs", ss.str());
   }
-  out_ << std::endl;
-}
 
-void NinjaCBinaryTargetWriter::WriteLibsList(
-    const std::string& label,
-    const std::vector<OutputFile>& libs) {
-  if (libs.empty())
-    return;
-
-  out_ << "  " << label << " =";
-  PathOutput output(path_output_.current_dir(),
-                    settings_->build_settings()->root_path_utf8(),
-                    ESCAPE_NINJA_COMMAND);
-  output.WriteFiles(out_, libs);
-  out_ << std::endl;
-}
-
-void NinjaCBinaryTargetWriter::WriteOrderOnlyDependencies(
-    const UniqueVector<const Target*>& non_linkable_deps) {
-  std::vector<OutputFile> outputs_to_write =
-      GetOrderOnlyDepsFromNonLinkableDeps(non_linkable_deps);
-
-  if (!outputs_to_write.empty()) {
-    out_ << " ||";
-    for (const auto& output : outputs_to_write) {
-      out_ << " ";
-      path_output_.WriteFile(out_, output);
-    }
+  if (!transitive_rustlibs.empty()) {
+    std::ostringstream ss;
+    PathOutput output(path_output_.current_dir(),
+                      settings_->build_settings()->root_path_utf8(),
+                      ESCAPE_NINJA_COMMAND);
+    output.WriteFiles(ss, transitive_rustlibs);
+    edge.edge_vars.emplace_back("rlibs", ss.str());
   }
+
+  if (target_->pool().ptr) {
+    edge.edge_vars.emplace_back("pool",
+                                target_->pool().ptr->GetNinjaName(
+                                    settings_->default_toolchain_label()));
+  }
+
+  AddEdge(std::move(edge));
 }
 
 bool NinjaCBinaryTargetWriter::CheckForDuplicateObjectFiles(
diff --git a/src/gn/ninja_c_binary_target_writer.h b/src/gn/ninja_c_binary_target_writer.h
index 3353a1b..7ba7840 100644
--- a/src/gn/ninja_c_binary_target_writer.h
+++ b/src/gn/ninja_c_binary_target_writer.h
@@ -20,7 +20,7 @@
   NinjaCBinaryTargetWriter(const Target* target, std::ostream& out);
   ~NinjaCBinaryTargetWriter() override;
 
-  void Run() override;
+  void GenerateRules() override;
 
  private:
   using OutputFileSet = std::set<OutputFile>;
@@ -92,20 +92,9 @@
   // Writes the stamp line for a source set. These are not linked.
   void WriteSourceSetStamp(const std::vector<OutputFile>& object_files);
 
-  void WriteLinkerStuff(const std::vector<OutputFile>& object_files,
+  void WriteLinkerStuff(std::vector<OutputFile> object_files,
                         const std::vector<SourceFile>& other_files,
                         const std::vector<OutputFile>& input_deps);
-  void WriteOutputSubstitutions();
-  void WriteLibsList(const std::string& label,
-                     const std::vector<OutputFile>& libs);
-
-  // Writes the implicit dependencies for the link or stamp line. This is
-  // the "||" and everything following it on the ninja line.
-  //
-  // The order-only dependencies are the non-linkable deps passed in as an
-  // argument, plus the data file dependencies in the target.
-  void WriteOrderOnlyDependencies(
-      const UniqueVector<const Target*>& non_linkable_deps);
 
   // Checks for duplicates in the given list of output files. If any duplicates
   // are found, throws an error and return false.
diff --git a/src/gn/ninja_c_binary_target_writer_unittest.cc b/src/gn/ninja_c_binary_target_writer_unittest.cc
index 34a473f..3d051ee 100644
--- a/src/gn/ninja_c_binary_target_writer_unittest.cc
+++ b/src/gn/ninja_c_binary_target_writer_unittest.cc
@@ -60,15 +60,18 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build obj/foo/bar.input1.o: cxx ../../foo/input1.cc\n"
         "  source_file_part = input1.cc\n"
         "  source_name_part = input1\n"
+        "\n"
         "build obj/foo/bar.input2.o: cxx ../../foo/input2.cc\n"
         "  source_file_part = input2.cc\n"
         "  source_name_part = input2\n"
         "\n"
         "build phony/foo/bar.linkdeps: phony obj/foo/bar.input1.o "
         "obj/foo/bar.input2.o ../../foo/input3.o ../../foo/input4.obj\n"
+        "\n"
         "build phony/foo/bar: phony phony/foo/bar.linkdeps\n";
     std::string out_str = out.str();
     EXPECT_EQ(expected, out_str);
@@ -215,12 +218,14 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = bar\n"
       "\n"
+      "\n"
       "build obj/foo/bar.input1.o obj/foo/input1.dwo: cxx "
       "../../foo/input1.cc\n"
       "  source_file_part = input1.cc\n"
       "  source_name_part = input1\n"
       "\n"
       "build phony/foo/bar.linkdeps: phony obj/foo/bar.input1.o\n"
+      "\n"
       "build phony/foo/bar: phony phony/foo/bar.linkdeps obj/foo/input1.dwo\n";
 
   EXPECT_EQ(expected, out.str());
@@ -269,12 +274,14 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = bar\n"
       "\n"
+      "\n"
       "build obj/foo/bar.input1.o obj/foo/input1.dwo: cxx "
       "../../foo/input1.cc\n"
       "  source_file_part = input1.cc\n"
       "  source_name_part = input1\n"
       "\n"
       "build phony/foo/bar.linkdeps: phony obj/foo/bar.input1.o\n"
+      "\n"
       "build phony/foo/bar: phony phony/foo/bar.linkdeps obj/foo/input1.dwo\n";
 
   EXPECT_EQ(expected, out.str());
@@ -337,14 +344,17 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = b\n"
         "\n"
+        "\n"
         "build obj/foo/b.b.o obj/foo/b.dwo: cxx ../../foo/b.cc\n"
         "  source_file_part = b.cc\n"
         "  source_name_part = b\n"
+        "\n"
         "build obj/foo/b.b2.o obj/foo/b2.dwo: cxx ../../foo/b2.cc\n"
         "  source_file_part = b2.cc\n"
         "  source_name_part = b2\n"
         "\n"
         "build phony/foo/b.linkdeps: phony obj/foo/b.b.o obj/foo/b.b2.o\n"
+        "\n"
         "build phony/foo/b: phony phony/foo/b.linkdeps obj/foo/b.dwo "
         "obj/foo/b2.dwo\n";
 
@@ -367,6 +377,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = liba\n"
         "\n"
+        "\n"
         "build obj/foo/liba.a.o: cxx ../../foo/a.cc\n"
         "  source_file_part = a.cc\n"
         "  source_name_part = a\n"
@@ -459,6 +470,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = libf\n"
         "\n"
+        "\n"
         "build obj/foo/libf.f.o obj/foo/f.dwo: cxx ../../foo/f.cc\n"
         "  source_file_part = f.cc\n"
         "  source_name_part = f\n"
@@ -520,6 +532,7 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = libbar\n"
       "\n"
+      "\n"
       "build obj/foo/libbar.input1.o: cxx ../../foo/input1.cc\n"
       "  source_file_part = input1.cc\n"
       "  source_name_part = input1\n"
@@ -569,6 +582,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = libbar\n"
         "\n"
+        "\n"
         "build obj/foo/libbar.input1.o: cxx ../../foo/input1.cc\n"
         "  source_file_part = input1.cc\n"
         "  source_name_part = input1\n"
@@ -601,6 +615,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = libbar\n"
         "\n"
+        "\n"
         "build obj/foo/libbar.input1.o: cxx ../../foo/input1.cc\n"
         "  source_file_part = input1.cc\n"
         "  source_name_part = input1\n"
@@ -652,6 +667,7 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = libbar\n"
       "\n"
+      "\n"
       "build obj/foo/libbar.input1.o: cxx ../../foo/input1.cc\n"
       "  source_file_part = input1.cc\n"
       "  source_name_part = input1\n"
@@ -713,6 +729,7 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = main\n"
       "\n"
+      "\n"
       "build obj/foo/main.main.o: cxx ../../foo/main.cc\n"
       "  source_file_part = main.cc\n"
       "  source_name_part = main\n"
@@ -769,10 +786,12 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = libshlib\n"
       "\n"
+      "\n"
       "build obj/foo/libshlib.input1.o: cxx ../../foo/input1.cc"
       " || phony/foo/action\n"
       "  source_file_part = input1.cc\n"
       "  source_name_part = input1\n"
+      "\n"
       "build obj/foo/libshlib.input2.o: cxx ../../foo/input2.cc"
       " || phony/foo/action\n"
       "  source_file_part = input2.cc\n"
@@ -837,6 +856,7 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = gen_obj\n"
       "\n"
+      "\n"
       "build obj/BUILD_DIR/gen_obj.generated.o: cxx generated.cc"
       " || phony/foo/generate\n"
       "  source_file_part = generated.cc\n"
@@ -847,6 +867,7 @@
       // The order-only dependency here is strictly unnecessary since the
       // sources list this as an order-only dep.
       " || phony/foo/generate\n"
+      "\n"
       "build phony/foo/gen_obj: phony phony/foo/gen_obj.linkdeps\n";
 
   std::string obj_str = obj_out.str();
@@ -918,6 +939,7 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = final_target\n"
       "\n"
+      "\n"
       "build obj/foo/final_target.main.o: cxx ../../foo/main.cc\n"
       "  source_file_part = main.cc\n"
       "  source_name_part = main\n"
@@ -1080,9 +1102,11 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = shlib\n"
       "\n"
+      "\n"
       "build obj/foo/shlib.input1.o: cxx ../../foo/input1.cc\n"
       "  source_file_part = input1.cc\n"
       "  source_name_part = input1\n"
+      "\n"
       "build obj/foo/shlib.input2.o: cxx ../../foo/input2.cc\n"
       "  source_file_part = input2.cc\n"
       "  source_name_part = input2\n"
@@ -1139,12 +1163,14 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = inter\n"
       "\n"
+      "\n"
       "build obj/foo/inter.inter.o: cxx ../../foo/inter.cc\n"
       "  source_file_part = inter.cc\n"
       "  source_name_part = inter\n"
       "\n"
       "build phony/foo/inter.linkdeps: phony obj/foo/inter.inter.o || "
       "./data_target\n"
+      "\n"
       "build phony/foo/inter: phony phony/foo/inter.linkdeps\n";
   EXPECT_EQ(inter_expected, inter_out.str());
 
@@ -1176,6 +1202,7 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = exe\n"
       "\n"
+      "\n"
       "build obj/foo/exe.final.o: cxx ../../foo/final.cc\n"
       "  source_file_part = final.cc\n"
       "  source_name_part = final\n"
@@ -1218,6 +1245,7 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = libbar\n"
       "\n"
+      "\n"
       "build obj/foo/libbar.sources.o: cxx ../../foo/sources.cc\n"
       "  source_file_part = sources.cc\n"
       "  source_name_part = sources\n"
@@ -1258,6 +1286,7 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = libbar\n"
       "\n"
+      "\n"
       "build obj/foo/libbar.sources.o: cxx ../../foo/sources.cc\n"
       "  source_file_part = sources.cc\n"
       "  source_name_part = sources\n"
@@ -1296,6 +1325,7 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = exe\n"
       "\n"
+      "\n"
       "build obj/foo/exe.final.o: cxx ../../foo/final.cc\n"
       "  source_file_part = final.cc\n"
       "  source_name_part = final\n"
@@ -1374,10 +1404,12 @@
         "cflags_cc =\n"
         "target_output_name = no_pch_target\n"
         "\n"
+        "\n"
         "build withpch/obj/foo/no_pch_target.input1.o: "
         "withpch_cxx ../../foo/input1.cc\n"
         "  source_file_part = input1.cc\n"
         "  source_name_part = input1\n"
+        "\n"
         "build withpch/obj/foo/no_pch_target.input2.o: "
         "withpch_cc ../../foo/input2.c\n"
         "  source_file_part = input2.c\n"
@@ -1386,6 +1418,7 @@
         "build withpch/phony/foo/no_pch_target.linkdeps: "
         "phony withpch/obj/foo/no_pch_target.input1.o "
         "withpch/obj/foo/no_pch_target.input2.o\n"
+        "\n"
         "build withpch/phony/foo/no_pch_target: "
         "phony withpch/phony/foo/no_pch_target.linkdeps\n";
     EXPECT_EQ(no_pch_expected, out.str());
@@ -1421,6 +1454,7 @@
         "/Yubuild/precompile.h\n"
         "target_output_name = pch_target\n"
         "\n"
+        "\n"
         // Compile the precompiled source files with /Yc.
         "build withpch/obj/build/pch_target.precompile.c.o: "
         "withpch_cc ../../build/precompile.cc\n"
@@ -1440,6 +1474,7 @@
         "withpch/obj/build/pch_target.precompile.cc.o\n"
         "  source_file_part = input1.cc\n"
         "  source_name_part = input1\n"
+        "\n"
         "build withpch/obj/foo/pch_target.input2.o: "
         "withpch_cc ../../foo/input2.c | "
         // Explicit dependency on the PCH build step.
@@ -1453,6 +1488,7 @@
         // The precompiled object files were added to the outputs.
         "withpch/obj/build/pch_target.precompile.c.o "
         "withpch/obj/build/pch_target.precompile.cc.o\n"
+        "\n"
         "build withpch/phony/foo/pch_target: phony "
         "withpch/phony/foo/pch_target.linkdeps\n";
     EXPECT_EQ(pch_win_expected, out.str());
@@ -1524,10 +1560,12 @@
         "cflags_cc =\n"
         "target_output_name = no_pch_target\n"
         "\n"
+        "\n"
         "build withpch/obj/foo/no_pch_target.input1.o: "
         "withpch_cxx ../../foo/input1.cc\n"
         "  source_file_part = input1.cc\n"
         "  source_name_part = input1\n"
+        "\n"
         "build withpch/obj/foo/no_pch_target.input2.o: "
         "withpch_cc ../../foo/input2.c\n"
         "  source_file_part = input2.c\n"
@@ -1536,6 +1574,7 @@
         "build withpch/phony/foo/no_pch_target.linkdeps: "
         "phony withpch/obj/foo/no_pch_target.input1.o "
         "withpch/obj/foo/no_pch_target.input2.o\n"
+        "\n"
         "build withpch/phony/foo/no_pch_target: "
         "phony withpch/phony/foo/no_pch_target.linkdeps\n";
     EXPECT_EQ(no_pch_expected, out.str());
@@ -1569,6 +1608,7 @@
         "cflags_cc = -include withpch/obj/build/pch_target.precompile.h-cc\n"
         "target_output_name = pch_target\n"
         "\n"
+        "\n"
         // Compile the precompiled sources with -x <lang>.
         "build withpch/obj/build/pch_target.precompile.h-c.gch: "
         "withpch_cc ../../build/precompile.h\n"
@@ -1588,6 +1628,7 @@
         "withpch/obj/build/pch_target.precompile.h-cc.gch\n"
         "  source_file_part = input1.cc\n"
         "  source_name_part = input1\n"
+        "\n"
         "build withpch/obj/foo/pch_target.input2.o: "
         "withpch_cc ../../foo/input2.c | "
         // Explicit dependency on the PCH build step.
@@ -1598,6 +1639,7 @@
         "build withpch/phony/foo/pch_target.linkdeps: "
         "phony withpch/obj/foo/pch_target.input1.o "
         "withpch/obj/foo/pch_target.input2.o\n"
+        "\n"
         "build withpch/phony/foo/pch_target: "
         "phony withpch/phony/foo/pch_target.linkdeps\n";
     EXPECT_EQ(pch_gcc_expected, out.str());
@@ -1659,10 +1701,12 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build obj/foo/bar.input1.o: cxx ../../foo/input1.cc"
         " | ../../foo/input.data\n"
         "  source_file_part = input1.cc\n"
         "  source_name_part = input1\n"
+        "\n"
         "build obj/foo/bar.input2.o: cxx ../../foo/input2.cc"
         " | ../../foo/input.data\n"
         "  source_file_part = input2.cc\n"
@@ -1670,6 +1714,7 @@
         "\n"
         "build phony/foo/bar.linkdeps: phony obj/foo/bar.input1.o "
         "obj/foo/bar.input2.o\n"
+        "\n"
         "build phony/foo/bar: phony phony/foo/bar.linkdeps\n";
 
     EXPECT_EQ(expected, out.str());
@@ -1735,12 +1780,15 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build phony/foo/bar.inputs: phony"
         " ../../foo/input1.data ../../foo/input2.data\n"
+        "\n"
         "build obj/foo/bar.input1.o: cxx ../../foo/input1.cc"
         " | phony/foo/bar.inputs\n"
         "  source_file_part = input1.cc\n"
         "  source_name_part = input1\n"
+        "\n"
         "build obj/foo/bar.input2.o: cxx ../../foo/input2.cc"
         " | phony/foo/bar.inputs\n"
         "  source_file_part = input2.cc\n"
@@ -1748,6 +1796,7 @@
         "\n"
         "build phony/foo/bar.linkdeps: phony obj/foo/bar.input1.o "
         "obj/foo/bar.input2.o\n"
+        "\n"
         "build phony/foo/bar: phony phony/foo/bar.linkdeps\n";
 
     EXPECT_EQ(expected, out.str());
@@ -1791,12 +1840,15 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build phony/foo/bar.inputs: phony"
         " ../../foo/input1.data ../../foo/input2.data ../../foo/input3.data\n"
+        "\n"
         "build obj/foo/bar.input1.o: cxx ../../foo/input1.cc"
         " | phony/foo/bar.inputs\n"
         "  source_file_part = input1.cc\n"
         "  source_name_part = input1\n"
+        "\n"
         "build obj/foo/bar.input2.o: cxx ../../foo/input2.cc"
         " | phony/foo/bar.inputs\n"
         "  source_file_part = input2.cc\n"
@@ -1804,6 +1856,7 @@
         "\n"
         "build phony/foo/bar.linkdeps: phony obj/foo/bar.input1.o "
         "obj/foo/bar.input2.o\n"
+        "\n"
         "build phony/foo/bar: phony phony/foo/bar.linkdeps\n";
 
     EXPECT_EQ(expected, out.str());
@@ -1849,6 +1902,7 @@
       "target_out_dir = obj/bar\n"
       "target_output_name = bar\n"
       "\n"
+      "\n"
       "build obj/bar/bar.bar.o: cxx ../../bar/bar.cc\n"
       "  source_file_part = bar.cc\n"
       "  source_name_part = bar\n"
@@ -2026,6 +2080,7 @@
       "target_out_dir = obj/exe\n"
       "target_output_name = exe\n"
       "\n"
+      "\n"
       "build obj/exe/exe.main.o: cxx ../../exe/main.cc\n"
       "  source_file_part = main.cc\n"
       "  source_name_part = main\n"
@@ -2194,6 +2249,7 @@
       "target_out_dir = obj/exe\n"
       "target_output_name = exe\n"
       "\n"
+      "\n"
       "build obj/exe/exe.main.o: cxx ../../exe/main.cc\n"
       "  source_file_part = main.cc\n"
       "  source_name_part = main\n"
@@ -2271,6 +2327,7 @@
       "target_out_dir = obj/bar\n"
       "target_output_name = bar\n"
       "\n"
+      "\n"
       "build obj/bar/bar.bar.o: cxx ../../bar/bar.cc\n"
       "  source_file_part = bar.cc\n"
       "  source_name_part = bar\n"
@@ -2374,6 +2431,7 @@
       "target_out_dir = obj/exe\n"
       "target_output_name = binary\n"
       "\n"
+      "\n"
       "build obj/exe/binary.main.o: cxx ../../exe/main.cc\n"
       "  source_file_part = main.cc\n"
       "  source_name_part = main\n"
@@ -2470,6 +2528,7 @@
       "target_out_dir = obj/exe\n"
       "target_output_name = binary\n"
       "\n"
+      "\n"
       "build obj/exe/binary.main.o: cxx ../../exe/main.cc\n"
       "  source_file_part = main.cc\n"
       "  source_name_part = main\n"
@@ -2568,6 +2627,7 @@
       "target_out_dir = obj/exe\n"
       "target_output_name = binary\n"
       "\n"
+      "\n"
       "build obj/exe/binary.main.o: cxx ../../exe/main.cc\n"
       "  source_file_part = main.cc\n"
       "  source_name_part = main\n"
@@ -2678,6 +2738,7 @@
       "target_out_dir = obj/exe\n"
       "target_output_name = binary\n"
       "\n"
+      "\n"
       "build obj/exe/binary.main.o: cxx ../../exe/main.cc\n"
       "  source_file_part = main.cc\n"
       "  source_name_part = main\n"
@@ -2726,10 +2787,12 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = libbar\n"
       "\n"
+      "\n"
       "build obj/foo/libbar.bar.o: cxx ../../foo/bar.cc | "
       "obj/foo/libbar.bar.pcm\n"
       "  source_file_part = bar.cc\n"
       "  source_name_part = bar\n"
+      "\n"
       "build obj/foo/libbar.bar.pcm: cxx_module ../../foo/bar.modulemap\n"
       "  source_file_part = bar.modulemap\n"
       "  source_name_part = bar\n"
@@ -2771,15 +2834,18 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = bar\n"
       "\n"
+      "\n"
       "build obj/foo/bar.bar.o: cxx ../../foo/bar.cc | "
       "obj/foo/bar.bar.pcm\n"
       "  source_file_part = bar.cc\n"
       "  source_name_part = bar\n"
+      "\n"
       "build obj/foo/bar.bar.pcm: cxx_module ../../foo/bar.modulemap\n"
       "  source_file_part = bar.modulemap\n"
       "  source_name_part = bar\n"
       "\n"
       "build phony/foo/bar.linkdeps: phony obj/foo/bar.bar.o\n"
+      "\n"
       "build phony/foo/bar: phony phony/foo/bar.linkdeps obj/foo/bar.bar.pcm\n";
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
@@ -2816,11 +2882,13 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = foo\n"
         "\n"
+        "\n"
         "build gen/foo/foo.h obj/foo/Foo.swiftmodule obj/foo/file1.o "
         "obj/foo/file2.o: swift ../../foo/file1.swift ../../foo/file2.swift\n"
         "  restat = 1\n"
         "\n"
         "build phony/foo/foo.linkdeps: phony obj/foo/file1.o obj/foo/file2.o\n"
+        "\n"
         "build phony/foo/foo: phony phony/foo/foo.linkdeps gen/foo/foo.h "
         "obj/foo/Foo.swiftmodule\n";
 
@@ -2854,12 +2922,14 @@
         "target_out_dir = obj/bar\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build gen/bar/bar.h obj/bar/Bar.swiftmodule obj/bar/bar.o: swift "
         "../../bar/bar.swift || phony/foo/foo\n"
         "  restat = 1\n"
         "\n"
         "build phony/bar/bar.linkdeps: phony obj/bar/bar.o || "
         "phony/foo/foo.linkdeps\n"
+        "\n"
         "build phony/bar/bar: phony phony/bar/bar.linkdeps gen/bar/bar.h "
         "obj/bar/Bar.swiftmodule\n";
 
@@ -2901,12 +2971,14 @@
         "target_out_dir = obj/bar\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build gen/bar/bar.h obj/bar/Bar.swiftmodule obj/bar/bar.o: swift "
         "../../bar/bar.swift || phony/bar/group phony/foo/foo\n"
         "  restat = 1\n"
         "\n"
         "build phony/bar/bar.linkdeps: phony obj/bar/bar.o || "
         "phony/foo/foo.linkdeps\n"
+        "\n"
         "build phony/bar/bar: phony phony/bar/bar.linkdeps gen/bar/bar.h "
         "obj/bar/Bar.swiftmodule\n";
 
@@ -3039,9 +3111,11 @@
 target_out_dir = obj/blah
 target_output_name = liba
 
+
 build obj/blah/liba.a.pcm: cxx_module ../../blah/a.modulemap
   source_file_part = a.modulemap
   source_name_part = a
+
 build obj/blah/liba.a.o: cxx ../../blah/a.cc | obj/blah/liba.a.pcm
   source_file_part = a.cc
   source_name_part = a
@@ -3088,9 +3162,11 @@
 target_out_dir = obj/stuff
 target_output_name = libb
 
+
 build obj/stuff/libb.b.pcm: cxx_module ../../stuff/b.modulemap | obj/blah/liba.a.pcm
   source_file_part = b.modulemap
   source_name_part = b
+
 build obj/stuff/libb.b.o: cxx ../../stuff/b.cc | obj/stuff/libb.b.pcm obj/blah/liba.a.pcm
   source_file_part = b.cc
   source_name_part = b
@@ -3135,6 +3211,7 @@
 target_out_dir = obj/things
 target_output_name = libc
 
+
 build obj/stuff/libc.c.pcm: cxx_module ../../stuff/c.modulemap | obj/stuff/libb.b.pcm obj/blah/liba.a.pcm
   source_file_part = c.modulemap
   source_name_part = c
@@ -3177,9 +3254,11 @@
 target_out_dir = obj/zap
 target_output_name = c
 
+
 build obj/zap/c.x.o: cxx ../../zap/x.cc | obj/stuff/libb.b.pcm obj/blah/liba.a.pcm
   source_file_part = x.cc
   source_name_part = x
+
 build obj/zap/c.y.o: cxx ../../zap/y.cc | obj/stuff/libb.b.pcm obj/blah/liba.a.pcm
   source_file_part = y.cc
   source_name_part = y
@@ -3235,6 +3314,7 @@
 target_out_dir = obj/launchpad
 target_output_name = main
 
+
 build obj/launchpad/main.main.o: cxx ../../launchpad/main.cc
   source_file_part = main.cc
   source_name_part = main
@@ -3286,6 +3366,7 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = bar\n"
       "\n"
+      "\n"
       "build obj/foo/bar.source.o: cxx ../../foo/source.cc\n"
       "  source_file_part = source.cc\n"
       "  source_name_part = source\n"
@@ -3350,6 +3431,7 @@
       "root_out_dir = .\n"
       "target_output_name = bar\n"
       "\n"
+      "\n"
       "build obj/foo/bar.source.o: cxx ../../foo/source.cc | "
       "../../bin/clang++\n"
       "  source_file_part = source.cc\n"
@@ -3443,11 +3525,13 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = bar\n"
       "\n"
+      "\n"
       "build obj/foo/bar.source1.o: cxx ../../foo/source1.cc\n"
       "  source_file_part = source1.cc\n"
       "  source_name_part = source1\n"
       "\n"
       "build phony/foo/bar.linkdeps: phony obj/foo/bar.source1.o\n"
+      "\n"
       "build phony/foo/bar: phony phony/foo/bar.linkdeps\n";
   writer.Run();
   std::string ninja_str = ninja_out.str();
@@ -3590,11 +3674,13 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = root\n"
       "\n"
+      "\n"
       "build obj/foo/root.root.o: cxx ../../foo/root.cc\n"
       "  source_file_part = root.cc\n"
       "  source_name_part = root\n"
       "\n"
       "build phony/foo/root.linkdeps: phony obj/foo/root.root.o\n"
+      "\n"
       "build phony/foo/root: phony phony/foo/root.linkdeps\n";
 
   EXPECT_EQ(expected_root_ninja, root_ninja_str);
@@ -3664,12 +3750,14 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = a\n"
         "\n"
+        "\n"
         "build obj/foo/a.a.o: cxx ../../foo/a.cc\n"
         "  source_file_part = a.cc\n"
         "  source_name_part = a\n"
         "\n"
         "build phony/foo/a.linkdeps: phony obj/foo/a.a.o"
         " || phony/foo/b.linkdeps\n"
+        "\n"
         "build phony/foo/a: phony phony/foo/a.linkdeps\n";
 
     EXPECT_EQ(expected, out.str());
diff --git a/src/gn/ninja_copy_target_writer.cc b/src/gn/ninja_copy_target_writer.cc
index 16fca06..3c26e78 100644
--- a/src/gn/ninja_copy_target_writer.cc
+++ b/src/gn/ninja_copy_target_writer.cc
@@ -4,7 +4,6 @@
 
 #include "gn/ninja_copy_target_writer.h"
 
-#include "base/strings/string_util.h"
 #include "gn/general_tool.h"
 #include "gn/ninja_utils.h"
 #include "gn/output_file.h"
@@ -21,7 +20,7 @@
 
 NinjaCopyTargetWriter::~NinjaCopyTargetWriter() = default;
 
-void NinjaCopyTargetWriter::Run() {
+void NinjaCopyTargetWriter::GenerateRules() {
   const Tool* copy_tool =
       target_->toolchain()->GetTool(GeneralTool::kGeneralToolCopy);
   if (!copy_tool) {
@@ -55,7 +54,6 @@
 
   std::vector<OutputFile> output_files;
   WriteCopyRules(&output_files);
-  out_ << std::endl;
   WriteStampOrPhonyForTarget(output_files, std::vector<OutputFile>());
 }
 
@@ -115,20 +113,15 @@
             target_, target_->settings(), output_subst, input_file);
     output_files->push_back(output_file);
 
-    out_ << "build ";
-    WriteOutput(std::move(output_file));
-
-    out_ << ": " << tool_name << " ";
-    path_output_.WriteFile(out_, input_file);
-    if (!implicit_deps.empty()) {
-      out_ << " |";
-      path_output_.WriteFiles(out_, implicit_deps);
-    }
-    if (!order_only_deps.empty()) {
-      out_ << " ||";
-      path_output_.WriteFiles(out_, order_only_deps);
-    }
-    WriteValidations();
-    out_ << std::endl;
+    NinjaBuildEdge edge{
+        .rule = tool_name,
+        .outputs = {output_file},
+        .explicit_inputs = {OutputFile(settings_->build_settings(),
+                                       input_file)},
+        .implicit_inputs = implicit_deps,
+        .order_only_inputs = order_only_deps,
+    };
+    AddValidationInputs(edge);
+    AddEdge(std::move(edge));
   }
 }
diff --git a/src/gn/ninja_copy_target_writer.h b/src/gn/ninja_copy_target_writer.h
index 95d50b4..1016f2c 100644
--- a/src/gn/ninja_copy_target_writer.h
+++ b/src/gn/ninja_copy_target_writer.h
@@ -13,7 +13,7 @@
   NinjaCopyTargetWriter(const Target* target, std::ostream& out);
   ~NinjaCopyTargetWriter() override;
 
-  void Run() override;
+  void GenerateRules() override;
 
  private:
   // Writes the rules top copy the file(s), putting the computed output file
diff --git a/src/gn/ninja_copy_target_writer_unittest.cc b/src/gn/ninja_copy_target_writer_unittest.cc
index 6fc5e43..0199b50 100644
--- a/src/gn/ninja_copy_target_writer_unittest.cc
+++ b/src/gn/ninja_copy_target_writer_unittest.cc
@@ -34,6 +34,7 @@
 
   const char expected_linux[] =
       "build input1.out: copy ../../foo/input1.txt\n"
+      "\n"
       "build input2.out: copy ../../foo/input2.txt\n"
       "\n"
       "build phony/foo/bar: phony input1.out input2.out\n";
@@ -210,7 +211,9 @@
     const char expected_linux[] =
         "build phony/foo/bar.inputdeps: phony || phony/foo/action1 "
         "phony/foo/action2\n"
+        "\n"
         "build action1.copy: copy action1.out | phony/foo/bar.inputdeps\n"
+        "\n"
         "build action2.copy: copy action2.out | phony/foo/bar.inputdeps\n"
         "\n"
         "build phony/foo/bar: phony action1.copy action2.copy\n";
diff --git a/src/gn/ninja_create_bundle_target_writer.cc b/src/gn/ninja_create_bundle_target_writer.cc
index c91d41c..bf45ad4 100644
--- a/src/gn/ninja_create_bundle_target_writer.cc
+++ b/src/gn/ninja_create_bundle_target_writer.cc
@@ -74,7 +74,7 @@
 
 NinjaCreateBundleTargetWriter::~NinjaCreateBundleTargetWriter() = default;
 
-void NinjaCreateBundleTargetWriter::Run() {
+void NinjaCreateBundleTargetWriter::GenerateRules() {
   if (!EnsureAllToolsAvailable(target_))
     return;
 
@@ -109,14 +109,13 @@
   // Write a phony target for the outer bundle directory. This allows other
   // targets to treat the entire bundle as a single unit, even though it is
   // a directory, so that it can be depended upon as a discrete build edge.
-  out_ << "build ";
-
-  WriteOutput(
-      OutputFile(settings_->build_settings(),
-                 target_->bundle_data().GetBundleRootDirOutput(settings_)));
-  out_ << ": " << BuiltinTool::kBuiltinToolPhony << " ";
-  out_ << target_->dependency_output().value();
-  out_ << std::endl;
+  AddEdge(NinjaBuildEdge{
+      .rule = BuiltinTool::kBuiltinToolPhony,
+      .outputs = {OutputFile(
+          settings_->build_settings(),
+          target_->bundle_data().GetBundleRootDirOutput(settings_))},
+      .explicit_inputs = {target_->dependency_output()},
+  });
 }
 
 std::string NinjaCreateBundleTargetWriter::WritePostProcessingRuleDefinition() {
@@ -128,24 +127,28 @@
   base::ReplaceChars(custom_rule_name, ":/()", "_", &custom_rule_name);
   custom_rule_name.append("_post_processing_rule");
 
-  out_ << "rule " << custom_rule_name << std::endl;
-  out_ << "  command = ";
-  path_output_.WriteFile(out_, settings_->build_settings()->python_path());
-  out_ << " ";
-  path_output_.WriteFile(out_, target_->bundle_data().post_processing_script());
+  std::ostringstream rule_out;
+  rule_out << "rule " << custom_rule_name << std::endl;
+  rule_out << "  command = ";
+  path_output_.WriteFile(rule_out, settings_->build_settings()->python_path());
+  rule_out << " ";
+  path_output_.WriteFile(rule_out,
+                         target_->bundle_data().post_processing_script());
 
   const SubstitutionList& args = target_->bundle_data().post_processing_args();
   EscapeOptions args_escape_options;
   args_escape_options.mode = ESCAPE_NINJA_COMMAND;
 
   for (const auto& arg : args.list()) {
-    out_ << " ";
-    SubstitutionWriter::WriteWithNinjaVariables(arg, args_escape_options, out_);
+    rule_out << " ";
+    SubstitutionWriter::WriteWithNinjaVariables(arg, args_escape_options,
+                                                rule_out);
   }
-  out_ << std::endl;
-  out_ << "  description = POST PROCESSING " << target_label << std::endl;
-  out_ << "  restat = 1" << std::endl;
-  out_ << std::endl;
+  rule_out << std::endl;
+  rule_out << "  description = POST PROCESSING " << target_label << std::endl;
+  rule_out << "  restat = 1" << std::endl;
+
+  target_group_.custom_rules.push_back(rule_out.str());
 
   WritePostProcessingManifestFile();
   return custom_rule_name;
@@ -208,22 +211,15 @@
         /*err=*/nullptr);
     output_files->push_back(expanded_output_file);
 
-    out_ << "build ";
-    WriteOutput(std::move(expanded_output_file));
-    out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
-         << GeneralTool::kGeneralToolCopyBundleData << " ";
-    path_output_.WriteFile(out_, source_file);
-
-    if (!implicit_deps.empty()) {
-      out_ << " |";
-      path_output_.WriteFiles(out_, implicit_deps);
-    }
-    if (!order_only_deps.empty()) {
-      out_ << " ||";
-      path_output_.WriteFiles(out_, order_only_deps);
-    }
-
-    out_ << std::endl;
+    AddEdge(NinjaBuildEdge{
+        .rule = GetNinjaRulePrefixForToolchain(settings_) +
+                GeneralTool::kGeneralToolCopyBundleData,
+        .outputs = {expanded_output_file},
+        .explicit_inputs = {OutputFile(settings_->build_settings(),
+                                       source_file)},
+        .implicit_inputs = implicit_deps,
+        .order_only_inputs = order_only_deps,
+    });
   }
 }
 
@@ -257,80 +253,68 @@
   if (target_->bundle_data().assets_catalog_sources().empty()) {
     DCHECK(!target_->bundle_data().partial_info_plist().is_null());
 
-    out_ << "build ";
-    WriteOutput(partial_info_plist);
-    out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
-         << GeneralTool::kGeneralToolStamp;
-    if (!implicit_deps.empty()) {
-      out_ << " |";
-      path_output_.WriteFiles(out_, implicit_deps);
-    }
-    if (!order_only_deps.empty()) {
-      out_ << " ||";
-      path_output_.WriteFiles(out_, order_only_deps);
-    }
-    out_ << std::endl;
+    AddEdge(NinjaBuildEdge{
+        .rule = GetNinjaRulePrefixForToolchain(settings_) +
+                GeneralTool::kGeneralToolStamp,
+        .outputs = {partial_info_plist},
+        .implicit_inputs = implicit_deps,
+        .order_only_inputs = order_only_deps,
+    });
     return;
   }
 
   OutputFile input_dep = WriteCompileAssetsCatalogInputDepsStampOrPhony(
       target_->bundle_data().assets_catalog_deps());
-  DCHECK(!input_dep.value().empty());
 
-  out_ << "build ";
-  WriteOutput(std::move(compiled_catalog));
+  std::vector<OutputFile> implicit_outputs;
   if (partial_info_plist != OutputFile()) {
     // If "partial_info_plist" is non-empty, then add it to list of implicit
     // outputs of the asset catalog compilation, so that target can use it
     // without getting the ninja error "'foo', needed by 'bar', missing and
     // no known rule to make it".
-    out_ << " | ";
-    WriteOutput(partial_info_plist);
+    implicit_outputs.push_back(partial_info_plist);
   }
 
-  out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
-       << GeneralTool::kGeneralToolCompileXCAssets;
+  std::vector<OutputFile> implicit_inputs;
+  implicit_inputs.push_back(input_dep);
+  implicit_inputs.insert(implicit_inputs.end(), implicit_deps.begin(),
+                         implicit_deps.end());
 
-  SourceFileSet asset_catalog_bundles;
-  for (const auto& source : target_->bundle_data().assets_catalog_sources()) {
-    out_ << " ";
-    path_output_.WriteFile(out_, source);
-    asset_catalog_bundles.insert(source);
-  }
-
-  out_ << " | ";
-  path_output_.WriteFile(out_, input_dep);
-  path_output_.WriteFiles(out_, implicit_deps);
-
-  if (!order_only_deps.empty()) {
-    out_ << " ||";
-    path_output_.WriteFiles(out_, order_only_deps);
-  }
-
-  out_ << std::endl;
-
-  out_ << "  product_type = " << target_->bundle_data().product_type()
-       << std::endl;
+  std::vector<NinjaVariable> edge_vars;
+  edge_vars.emplace_back("product_type", target_->bundle_data().product_type());
 
   if (partial_info_plist != OutputFile()) {
-    out_ << "  partial_info_plist = ";
-    path_output_.WriteFile(out_, partial_info_plist);
-    out_ << std::endl;
+    std::ostringstream ss;
+    path_output_.WriteFile(ss, partial_info_plist);
+    edge_vars.emplace_back("partial_info_plist", ss.str());
   }
 
   const std::vector<SubstitutionPattern>& flags =
       target_->bundle_data().xcasset_compiler_flags().list();
   if (!flags.empty()) {
-    out_ << "  " << SubstitutionXcassetsCompilerFlags.ninja_name << " =";
+    std::ostringstream ss;
     EscapeOptions args_escape_options;
     args_escape_options.mode = ESCAPE_NINJA_COMMAND;
     for (const auto& flag : flags) {
-      out_ << " ";
+      ss << " ";
       SubstitutionWriter::WriteWithNinjaVariables(flag, args_escape_options,
-                                                  out_);
+                                                  ss);
     }
-    out_ << std::endl;
+    edge_vars.emplace_back(SubstitutionXcassetsCompilerFlags.ninja_name,
+                           ss.str());
   }
+
+  AddEdge(NinjaBuildEdge{
+      .rule = GetNinjaRulePrefixForToolchain(settings_) +
+              GeneralTool::kGeneralToolCompileXCAssets,
+      .outputs = {compiled_catalog},
+      .implicit_outputs = std::move(implicit_outputs),
+      .explicit_inputs =
+          ToOutputFiles(target_->bundle_data().assets_catalog_sources()),
+      .implicit_inputs = std::move(implicit_inputs),
+      .order_only_inputs = order_only_deps,
+      .edge_vars = std::move(edge_vars),
+  });
 }
 
 OutputFile
@@ -358,17 +342,18 @@
            GeneralTool::kGeneralToolStamp;
   }
 
-  out_ << "build ";
-  WriteOutput(xcassets_input_stamp_or_phony);
-  out_ << ": " << tool;
-
+  std::vector<OutputFile> explicit_inputs;
   for (const Target* target : dependencies) {
     if (target->has_dependency_output()) {
-      out_ << " ";
-      path_output_.WriteFile(out_, target->dependency_output());
+      explicit_inputs.push_back(target->dependency_output());
     }
   }
-  out_ << std::endl;
+
+  AddEdge(NinjaBuildEdge{
+      .rule = tool,
+      .outputs = {xcassets_input_stamp_or_phony},
+      .explicit_inputs = std::move(explicit_inputs),
+  });
   return xcassets_input_stamp_or_phony;
 }
 
@@ -385,22 +370,21 @@
                                                output_files);
   DCHECK(!post_processing_input_stamp_file.value().empty());
 
-  out_ << "build";
   std::vector<OutputFile> post_processing_output_files;
   SubstitutionWriter::GetListAsOutputFiles(
       settings_, target_->bundle_data().post_processing_outputs(),
       &post_processing_output_files);
-  WriteOutputs(post_processing_output_files);
 
   // Since the post-processing step depends on all the files from the bundle,
   // the create_bundle stamp can just depends on the output of the signature
   // script (dependencies are transitive).
-  *output_files = std::move(post_processing_output_files);
+  *output_files = post_processing_output_files;
 
-  out_ << ": " << post_processing_rule_name;
-  out_ << " | ";
-  path_output_.WriteFile(out_, post_processing_input_stamp_file);
-  out_ << std::endl;
+  AddEdge(NinjaBuildEdge{
+      .rule = post_processing_rule_name,
+      .outputs = std::move(post_processing_output_files),
+      .implicit_inputs = {post_processing_input_stamp_file},
+  });
 }
 
 OutputFile
@@ -444,22 +428,12 @@
            GeneralTool::kGeneralToolStamp;
   }
 
-  out_ << "build ";
-  WriteOutput(stamp_or_phony);
-  out_ << ": " << tool;
-
-  for (const SourceFile& source : post_processing_input_files) {
-    out_ << " ";
-    path_output_.WriteFile(out_, source);
-  }
-  if (!implicit_deps.empty()) {
-    out_ << " |";
-    path_output_.WriteFiles(out_, implicit_deps);
-  }
-  if (!order_only_deps.empty()) {
-    out_ << " ||";
-    path_output_.WriteFiles(out_, order_only_deps);
-  }
-  out_ << std::endl;
+  AddEdge(NinjaBuildEdge{
+      .rule = tool,
+      .outputs = {stamp_or_phony},
+      .explicit_inputs = ToOutputFiles(post_processing_input_files),
+      .implicit_inputs = implicit_deps,
+      .order_only_inputs = order_only_deps,
+  });
   return stamp_or_phony;
 }
diff --git a/src/gn/ninja_create_bundle_target_writer.h b/src/gn/ninja_create_bundle_target_writer.h
index 7d603a0..6c4d746 100644
--- a/src/gn/ninja_create_bundle_target_writer.h
+++ b/src/gn/ninja_create_bundle_target_writer.h
@@ -15,7 +15,7 @@
   NinjaCreateBundleTargetWriter(const Target* target, std::ostream& out);
   ~NinjaCreateBundleTargetWriter() override;
 
-  void Run() override;
+  void GenerateRules() override;
 
  private:
   // Writes the Ninja rule for invoking the post-processing script.
diff --git a/src/gn/ninja_create_bundle_target_writer_unittest.cc b/src/gn/ninja_create_bundle_target_writer_unittest.cc
index 2d23969..d0b9d02 100644
--- a/src/gn/ninja_create_bundle_target_writer_unittest.cc
+++ b/src/gn/ninja_create_bundle_target_writer_unittest.cc
@@ -77,13 +77,17 @@
   const char expected[] =
       "build phony/baz/bar.inputdeps: phony || phony/foo/bar "
       "phony/foo/data\n"
+      "\n"
       "build bar.bundle/Contents/Resources/input1.txt: copy_bundle_data "
       "../../foo/input1.txt | phony/baz/bar.inputdeps\n"
+      "\n"
       "build bar.bundle/Contents/Resources/input2.txt: copy_bundle_data "
       "../../foo/input2.txt | phony/baz/bar.inputdeps\n"
+      "\n"
       "build phony/baz/bar: phony "
       "bar.bundle/Contents/Resources/input1.txt "
       "bar.bundle/Contents/Resources/input2.txt\n"
+      "\n"
       "build bar.bundle: phony phony/baz/bar\n";
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
@@ -125,13 +129,17 @@
   const char expected[] =
       "build phony/baz/bar.inputdeps: phony || phony/foo/bar "
       "phony/foo/data\n"
+      "\n"
       "build gen/bar.bundle/Contents/Resources/input1.txt: copy_bundle_data "
       "../../foo/input1.txt | phony/baz/bar.inputdeps\n"
+      "\n"
       "build gen/bar.bundle/Contents/Resources/input2.txt: copy_bundle_data "
       "../../foo/input2.txt | phony/baz/bar.inputdeps\n"
+      "\n"
       "build phony/baz/bar: phony "
       "gen/bar.bundle/Contents/Resources/input1.txt "
       "gen/bar.bundle/Contents/Resources/input2.txt\n"
+      "\n"
       "build gen/bar.bundle: phony phony/baz/bar\n";
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
@@ -164,8 +172,10 @@
 
   const char expected[] =
       "build baz/bar/bar_partial_info.plist: stamp || phony/foo/bar\n"
+      "\n"
       "build phony/baz/bar: phony "
       "baz/bar/bar_partial_info.plist || phony/foo/bar\n"
+      "\n"
       "build bar.bundle: phony phony/baz/bar\n";
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
@@ -229,12 +239,15 @@
   const char expected[] =
       "build phony/baz/bar.inputdeps: phony || phony/foo/bar "
       "phony/foo/data\n"
+      "\n"
       "build bar.bundle/Contents/Resources/Assets.car: compile_xcassets "
       "../../foo/Foo.xcassets | phony/foo/data phony/baz/bar.inputdeps\n"
       "  product_type = com.apple.product-type\n"
       "  xcasset_compiler_flags = --app-icon foo\n"
+      "\n"
       "build phony/baz/bar: phony "
       "bar.bundle/Contents/Resources/Assets.car\n"
+      "\n"
       "build bar.bundle: phony phony/baz/bar\n";
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
@@ -270,6 +283,7 @@
 
   const char expected[] =
       "build phony/baz/bar: phony || phony/foo/action\n"
+      "\n"
       "build bar.bundle: phony phony/baz/bar\n";
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
@@ -392,15 +406,20 @@
       "build phony/baz/bar.inputdeps: phony || phony/biz/assets "
       "phony/foo/assets phony/foo/bar phony/foo/data "
       "phony/qux/info_plist phony/quz/assets\n"
+      "\n"
       "build bar.bundle/Contents/Info.plist: copy_bundle_data "
       "../../qux/qux-Info.plist | phony/baz/bar.inputdeps\n"
+      "\n"
       "build bar.bundle/Contents/Resources/input1.txt: copy_bundle_data "
       "../../foo/input1.txt | phony/baz/bar.inputdeps\n"
+      "\n"
       "build bar.bundle/Contents/Resources/input2.txt: copy_bundle_data "
       "../../foo/input2.txt | phony/baz/bar.inputdeps\n"
+      "\n"
       "build phony/baz/bar.xcassets.inputdeps: phony "
       "phony/foo/assets "
       "phony/quz/assets phony/biz/assets\n"
+      "\n"
       "build bar.bundle/Contents/Resources/Assets.car | "
       "baz/bar/bar_partial_info.plist: compile_xcassets "
       "../../foo/Foo.xcassets ../../quz/Quz.xcassets "
@@ -408,12 +427,14 @@
       "phony/baz/bar.inputdeps\n"
       "  product_type = com.apple.product-type\n"
       "  partial_info_plist = baz/bar/bar_partial_info.plist\n"
+      "\n"
       "build phony/baz/bar: phony "
       "bar.bundle/Contents/Info.plist "
       "bar.bundle/Contents/Resources/input1.txt "
       "bar.bundle/Contents/Resources/input2.txt "
       "bar.bundle/Contents/Resources/Assets.car "
       "baz/bar/bar_partial_info.plist\n"
+      "\n"
       "build bar.bundle: phony phony/baz/bar\n";
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
@@ -471,29 +492,36 @@
   writer.Run();
 
   const char expected[] =
-      "build phony/baz/bar.inputdeps: phony || ./quz phony/foo/bar "
-      "phony/foo/data\n"
       "rule __baz_bar___toolchain_default__post_processing_rule\n"
       "  command =  ../../build/codesign.py -b=quz bar.bundle\n"
       "  description = POST PROCESSING //baz:bar(//toolchain:default)\n"
       "  restat = 1\n"
       "\n"
+      "\n"
+      "build phony/baz/bar.inputdeps: phony || ./quz phony/foo/bar "
+      "phony/foo/data\n"
+      "\n"
       "build bar.bundle/Contents/Resources/input1.txt: copy_bundle_data "
       "../../foo/input1.txt | phony/baz/bar.inputdeps\n"
+      "\n"
       "build bar.bundle/Contents/Resources/input2.txt: copy_bundle_data "
       "../../foo/input2.txt | phony/baz/bar.inputdeps\n"
+      "\n"
       "build phony/baz/bar.postprocessing.inputdeps: phony "
       "../../build/codesign.py "
       "quz "
       "bar.bundle/Contents/Resources/input1.txt "
       "bar.bundle/Contents/Resources/input2.txt | "
       "phony/baz/bar.inputdeps\n"
+      "\n"
       "build bar.bundle/Contents/quz bar.bundle/_CodeSignature/CodeResources: "
       "__baz_bar___toolchain_default__post_processing_rule "
       "| phony/baz/bar.postprocessing.inputdeps\n"
+      "\n"
       "build phony/baz/bar: phony "
       "bar.bundle/Contents/quz "
       "bar.bundle/_CodeSignature/CodeResources ./quz\n"
+      "\n"
       "build bar.bundle: phony phony/baz/bar\n";
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
@@ -554,32 +582,39 @@
   writer.Run();
 
   const char expected[] =
-      "build toolchain/phony/baz/bar.inputdeps: phony || ./quz "
-      "toolchain/phony/foo/bar "
-      "toolchain/phony/foo/data\n"
       "rule __baz_bar___toolchain_default__post_processing_rule\n"
       "  command =  ../../build/codesign.py -b=quz bar.bundle\n"
       "  description = POST PROCESSING //baz:bar(//toolchain:default)\n"
       "  restat = 1\n"
       "\n"
+      "\n"
+      "build toolchain/phony/baz/bar.inputdeps: phony || ./quz "
+      "toolchain/phony/foo/bar "
+      "toolchain/phony/foo/data\n"
+      "\n"
       "build bar.bundle/Contents/Resources/input1.txt: "
       "toolchain_copy_bundle_data "
       "../../foo/input1.txt | toolchain/phony/baz/bar.inputdeps\n"
+      "\n"
       "build bar.bundle/Contents/Resources/input2.txt: "
       "toolchain_copy_bundle_data "
       "../../foo/input2.txt | toolchain/phony/baz/bar.inputdeps\n"
+      "\n"
       "build toolchain/phony/baz/bar.postprocessing.inputdeps: phony "
       "../../build/codesign.py "
       "quz "
       "bar.bundle/Contents/Resources/input1.txt "
       "bar.bundle/Contents/Resources/input2.txt | "
       "toolchain/phony/baz/bar.inputdeps\n"
+      "\n"
       "build bar.bundle/Contents/quz bar.bundle/_CodeSignature/CodeResources: "
       "__baz_bar___toolchain_default__post_processing_rule "
       "| toolchain/phony/baz/bar.postprocessing.inputdeps\n"
+      "\n"
       "build toolchain/phony/baz/bar: phony "
       "bar.bundle/Contents/quz "
       "bar.bundle/_CodeSignature/CodeResources ./quz\n"
+      "\n"
       "build bar.bundle: phony toolchain/phony/baz/bar\n";
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
diff --git a/src/gn/ninja_file.cc b/src/gn/ninja_file.cc
new file mode 100644
index 0000000..cacb7f6
--- /dev/null
+++ b/src/gn/ninja_file.cc
@@ -0,0 +1,127 @@
+// Copyright 2026 The GN Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "gn/ninja_file.h"
+
+#include <ostream>
+#include <string>
+
+#include "base/logging.h"
+#include "gn/escape.h"
+#include "gn/string_output_buffer.h"
+
+namespace {
+
+void WriteOutputFile(std::ostream& out, const OutputFile& file) {
+  EscapeOptions opts;
+  opts.mode = ESCAPE_NINJA;
+  out << " ";
+  EscapeStringToStream(out, file.value(), opts);
+}
+
+void WriteVariable(std::ostream& out,
+                   std::string_view name,
+                   const std::string& value,
+                   bool indent = false) {
+  if (indent)
+    out << "  ";
+  out << name << " =";
+  if (!value.empty()) {
+    if (!value.starts_with(' '))
+      out << " ";
+    out << value;
+  }
+  out << '\n';
+}
+
+void WriteRules(std::ostream& out, const std::vector<std::string>& rules) {
+  if (!rules.empty()) {
+    for (const auto& rule : rules) {
+      out << rule << '\n';
+    }
+    out << '\n';
+  }
+}
+
+}  // namespace
+
+void NinjaFile::AddTargetGroup(NinjaTargetGroup group) {
+  targets.push_back(std::move(group));
+}
+
+void NinjaFile::Hoist() {
+  if (targets.empty())
+    return;
+
+  if (targets.size() == 1) {
+    file_vars = std::move(targets.front().target_vars);
+    targets.front().target_vars.clear();
+    return;
+  }
+
+  CHECK(false) << "Multiple targets per ninja file not yet implemented";
+}
+
+void NinjaFile::Serialize(std::ostream& out) {
+  Hoist();
+
+  for (const auto& var : file_vars) {
+    WriteVariable(out, var.name, var.value, /*indent=*/false);
+  }
+  if (!file_vars.empty())
+    out << "\n\n";
+
+  WriteRules(out, custom_rules);
+
+  for (size_t i = 0; i < targets.size(); ++i) {
+    if (i > 0)
+      out << "\n\n";
+    const auto& target = targets[i];
+    WriteRules(out, target.custom_rules);
+    for (size_t e = 0; e < target.edges.size(); ++e) {
+      if (e > 0)
+        out << "\n";
+      const auto& edge = target.edges[e];
+      out << "build";
+      for (const auto& output : edge.outputs)
+        WriteOutputFile(out, output);
+      if (!edge.implicit_outputs.empty()) {
+        out << " |";
+        for (const auto& out_file : edge.implicit_outputs)
+          WriteOutputFile(out, out_file);
+      }
+      out << ": " << edge.rule;
+      for (const auto& input : edge.explicit_inputs)
+        WriteOutputFile(out, input);
+      if (!edge.implicit_inputs.empty()) {
+        out << " |";
+        for (const auto& in_file : edge.implicit_inputs)
+          WriteOutputFile(out, in_file);
+      }
+      if (!edge.order_only_inputs.empty()) {
+        out << " ||";
+        for (const auto& in_file : edge.order_only_inputs)
+          WriteOutputFile(out, in_file);
+      }
+      if (!edge.validation_inputs.empty()) {
+        out << " |@";
+        for (const auto& in_file : edge.validation_inputs)
+          WriteOutputFile(out, in_file);
+      }
+      out << "\n";
+
+      for (const auto& var : target.target_vars) {
+        WriteVariable(out, var.name, var.value, /*indent=*/true);
+      }
+      for (const auto& var : edge.edge_vars) {
+        WriteVariable(out, var.name, var.value, /*indent=*/true);
+      }
+    }
+  }
+}
+
+void NinjaFile::Serialize(StringOutputBuffer& out) {
+  std::ostream os(&out);
+  Serialize(os);
+}
diff --git a/src/gn/ninja_file.h b/src/gn/ninja_file.h
new file mode 100644
index 0000000..c2b3de9
--- /dev/null
+++ b/src/gn/ninja_file.h
@@ -0,0 +1,85 @@
+// Copyright 2026 The GN Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef TOOLS_GN_NINJA_FILE_H_
+#define TOOLS_GN_NINJA_FILE_H_
+
+#include <iosfwd>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "gn/output_file.h"
+
+class StringOutputBuffer;
+class Target;
+
+// A single variable assignment in Ninja syntax (e.g. cflags = -fPIC).
+struct NinjaVariable {
+  std::string_view name;
+  std::string value;
+
+  bool operator==(const NinjaVariable& other) const = default;
+};
+
+// Represents a single 'build <outputs>: <rule> <inputs>' edge.
+struct NinjaBuildEdge {
+  std::string rule;
+  std::vector<OutputFile> outputs;
+  std::vector<OutputFile> implicit_outputs = {};
+
+  std::vector<OutputFile> explicit_inputs = {};
+  std::vector<OutputFile> implicit_inputs = {};
+  std::vector<OutputFile> order_only_inputs = {};
+  std::vector<OutputFile> validation_inputs = {};
+
+  // Variables specific to this edge (e.g. source_file_part, source_name_part).
+  std::vector<NinjaVariable> edge_vars = {};
+
+  // Whether the outputs of this edge are target outputs for
+  // --ide=ninja_outputs. Intermediate input dependency stamps/phonies are not
+  // target outputs. See https://gn.issues.chromium.org/448860851.
+  bool is_target_output = true;
+};
+
+// Represents a group of build edges and shared variables for a target.
+struct NinjaTargetGroup {
+  const Target* target = nullptr;
+
+  // Custom rule definitions specific to this target (e.g. for action targets).
+  std::vector<std::string> custom_rules;
+
+  // Variables shared across all edges in this target (e.g. cflags, defines,
+  // target_out_dir).
+  std::vector<NinjaVariable> target_vars;
+
+  // Compilation, link, or action build edges for this target.
+  std::vector<NinjaBuildEdge> edges;
+};
+
+// Represents a complete Ninja build file.
+class NinjaFile {
+ public:
+  // Top-level / hoisted variables.
+  std::vector<NinjaVariable> file_vars;
+
+  // Custom rule definitions.
+  std::vector<std::string> custom_rules;
+
+  // Target groups contained in this file.
+  std::vector<NinjaTargetGroup> targets;
+
+  // Adds a target group to this file.
+  void AddTargetGroup(NinjaTargetGroup group);
+
+  // Serializes the Ninja AST to an output stream or StringOutputBuffer.
+  void Serialize(std::ostream& out);
+  void Serialize(StringOutputBuffer& out);
+
+ private:
+  // Hoists identical target variables across all targets into file_vars.
+  void Hoist();
+};
+
+#endif  // TOOLS_GN_NINJA_FILE_H_
diff --git a/src/gn/ninja_generated_file_target_writer.cc b/src/gn/ninja_generated_file_target_writer.cc
index 6de8207..9fd07f2 100644
--- a/src/gn/ninja_generated_file_target_writer.cc
+++ b/src/gn/ninja_generated_file_target_writer.cc
@@ -20,7 +20,7 @@
 
 NinjaGeneratedFileTargetWriter::~NinjaGeneratedFileTargetWriter() = default;
 
-void NinjaGeneratedFileTargetWriter::Run() {
+void NinjaGeneratedFileTargetWriter::GenerateRules() {
   // Write the file.
   GenerateFile();
 
diff --git a/src/gn/ninja_generated_file_target_writer.h b/src/gn/ninja_generated_file_target_writer.h
index 3103388..dc18fe2 100644
--- a/src/gn/ninja_generated_file_target_writer.h
+++ b/src/gn/ninja_generated_file_target_writer.h
@@ -13,7 +13,7 @@
   NinjaGeneratedFileTargetWriter(const Target* target, std::ostream& out);
   ~NinjaGeneratedFileTargetWriter() override;
 
-  void Run() override;
+  void GenerateRules() override;
 
  private:
   void GenerateFile();
diff --git a/src/gn/ninja_group_target_writer.cc b/src/gn/ninja_group_target_writer.cc
index 7db1a3a..c936a12 100644
--- a/src/gn/ninja_group_target_writer.cc
+++ b/src/gn/ninja_group_target_writer.cc
@@ -16,7 +16,7 @@
 
 NinjaGroupTargetWriter::~NinjaGroupTargetWriter() = default;
 
-void NinjaGroupTargetWriter::Run() {
+void NinjaGroupTargetWriter::GenerateRules() {
   // A group rule just generates a phony target with dependencies on each of
   // the deps and data_deps in the group.
   std::vector<OutputFile> output_files;
diff --git a/src/gn/ninja_group_target_writer.h b/src/gn/ninja_group_target_writer.h
index 7a3f211..663cc45 100644
--- a/src/gn/ninja_group_target_writer.h
+++ b/src/gn/ninja_group_target_writer.h
@@ -13,7 +13,7 @@
   NinjaGroupTargetWriter(const Target* target, std::ostream& out);
   ~NinjaGroupTargetWriter() override;
 
-  void Run() override;
+  void GenerateRules() override;
 
  private:
   NinjaGroupTargetWriter(const NinjaGroupTargetWriter&) = delete;
diff --git a/src/gn/ninja_rust_binary_target_writer.cc b/src/gn/ninja_rust_binary_target_writer.cc
index a8bea8f..d0651bf 100644
--- a/src/gn/ninja_rust_binary_target_writer.cc
+++ b/src/gn/ninja_rust_binary_target_writer.cc
@@ -27,21 +27,21 @@
   return opts;
 }
 
-void WriteVar(const char* name,
+void WriteVar(std::string_view name,
               const std::string& value,
               EscapeOptions opts,
-              std::ostream& out) {
-  out << name << " = ";
-  EscapeStringToStream(out, value, opts);
-  out << std::endl;
+              std::vector<NinjaVariable>& target_vars) {
+  std::ostringstream ss;
+  EscapeStringToStream(ss, value, opts);
+  target_vars.emplace_back(name, ss.str());
 }
 
 void WriteCrateVars(const Target* target,
                     const Tool* tool,
                     EscapeOptions opts,
-                    std::ostream& out) {
+                    std::vector<NinjaVariable>& target_vars) {
   WriteVar(kRustSubstitutionCrateName.ninja_name,
-           target->rust_values().crate_name(), opts, out);
+           target->rust_values().crate_name(), opts, target_vars);
 
   std::string crate_type;
   switch (target->rust_values().crate_type()) {
@@ -86,16 +86,17 @@
     default:
       NOTREACHED();
   }
-  WriteVar(kRustSubstitutionCrateType.ninja_name, crate_type, opts, out);
+  WriteVar(kRustSubstitutionCrateType.ninja_name, crate_type, opts,
+           target_vars);
 
   WriteVar(SubstitutionOutputExtension.ninja_name,
            SubstitutionWriter::GetLinkerSubstitution(
                target, tool, &SubstitutionOutputExtension),
-           opts, out);
+           opts, target_vars);
   WriteVar(SubstitutionOutputDir.ninja_name,
            SubstitutionWriter::GetLinkerSubstitution(target, tool,
                                                      &SubstitutionOutputDir),
-           opts, out);
+           opts, target_vars);
 }
 
 }  // namespace
@@ -109,7 +110,7 @@
 
 // TODO(juliehockett): add inherited library support? and IsLinkable support?
 // for c-cross-compat
-void NinjaRustBinaryTargetWriter::Run() {
+void NinjaRustBinaryTargetWriter::GenerateRules() {
   DCHECK(target_->output_type() != Target::SOURCE_SET);
 
   size_t num_output_uses = target_->sources().size();
@@ -290,16 +291,15 @@
   WriteExternsAndDeps(extern_deps, transitive_crates, rustdeps, nonrustdeps,
                       swiftmodules);
   WriteSourcesAndInputs();
-  WritePool(out_);
 }
 
 void NinjaRustBinaryTargetWriter::WriteCompilerVars() {
   const SubstitutionBits& subst = target_->toolchain()->substitution_bits();
 
   EscapeOptions opts = GetFlagOptions();
-  WriteCrateVars(target_, tool_, opts, out_);
+  WriteCrateVars(target_, tool_, opts, target_group_.target_vars);
 
-  WriteRustCompilerVars(subst, /*indent=*/false, /*always_write=*/true);
+  WriteRustCompilerVars(subst, /*always_write=*/true);
 
   WriteSharedVars(subst);
 }
@@ -312,25 +312,25 @@
   // 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));
+    deps->emplace_back(settings_->build_settings(), source);
   }
   for (const auto& data : target_->config_values().inputs()) {
-    deps->push_back(OutputFile(settings_->build_settings(), data));
+    deps->emplace_back(settings_->build_settings(), data);
   }
 }
 
 void NinjaRustBinaryTargetWriter::WriteSourcesAndInputs() {
-  out_ << "  sources =";
+  std::ostringstream val;
   for (const auto& source : target_->sources()) {
-    out_ << " ";
-    path_output_.WriteFile(out_,
+    val << " ";
+    path_output_.WriteFile(val,
                            OutputFile(settings_->build_settings(), source));
   }
   for (const auto& data : target_->config_values().inputs()) {
-    out_ << " ";
-    path_output_.WriteFile(out_, OutputFile(settings_->build_settings(), data));
+    val << " ";
+    path_output_.WriteFile(val, OutputFile(settings_->build_settings(), data));
   }
-  out_ << std::endl;
+  target_group_.edges.back().edge_vars.emplace_back("sources", val.str());
 }
 
 void NinjaRustBinaryTargetWriter::WriteExternsAndDeps(
@@ -339,24 +339,25 @@
     const std::vector<OutputFile>& rustdeps,
     const std::vector<OutputFile>& nonrustdeps,
     const std::vector<OutputFile>& swiftmodules) {
+  std::ostringstream externs_val;
   // Writes a external LibFile which comes from user-specified externs, and may
   // be either a string or a SourceFile.
-  auto write_extern_lib_file = [this](std::string_view crate_name,
-                                      LibFile lib_file) {
-    out_ << " --extern ";
-    out_ << crate_name;
-    out_ << "=";
+  auto write_extern_lib_file = [this, &externs_val](std::string_view crate_name,
+                                                    LibFile lib_file) {
+    externs_val << " --extern ";
+    externs_val << crate_name;
+    externs_val << "=";
     if (lib_file.is_source_file()) {
-      path_output_.WriteFile(out_, lib_file.source_file());
+      path_output_.WriteFile(externs_val, lib_file.source_file());
     } else {
       EscapeOptions escape_opts_command;
       escape_opts_command.mode = ESCAPE_NINJA_COMMAND;
-      EscapeStringToStream(out_, lib_file.value(), escape_opts_command);
+      EscapeStringToStream(externs_val, lib_file.value(), escape_opts_command);
     }
   };
   // Writes an external OutputFile which comes from a dependency of the current
   // target.
-  auto write_extern_target = [this](const Target& dep) {
+  auto write_extern_target = [this, &externs_val](const Target& dep) {
     std::string_view crate_name;
     const auto& aliased_deps = target_->rust_values().aliased_deps();
     if (auto it = aliased_deps.find(dep.label()); it != aliased_deps.end()) {
@@ -365,15 +366,12 @@
       crate_name = dep.rust_values().crate_name();
     }
 
-    out_ << " --extern ";
-    out_ << crate_name;
-    out_ << "=";
-    path_output_.WriteFile(out_, dep.dependency_output_file());
+    externs_val << " --extern ";
+    externs_val << crate_name;
+    externs_val << "=";
+    path_output_.WriteFile(externs_val, dep.dependency_output_file());
   };
 
-  // Write accessible crates with `--extern` to add them to the extern prelude.
-  out_ << "  externs =";
-
   // Tracking to avoid emitted the same lib twice. We track it instead of
   // pre-emptively constructing a UniqueVector since we would have to also store
   // the crate name, and in the future the public-ness.
@@ -418,13 +416,14 @@
     }
   }
 
-  out_ << std::endl;
-  out_ << "  rustdeps =";
+  target_group_.edges.back().edge_vars.emplace_back("externs",
+                                                    externs_val.str());
 
+  std::ostringstream rustdeps_val;
   for (const SourceDir& dir : private_extern_dirs) {
     // TODO: switch to using `--extern priv:name` after stabilization.
-    out_ << " -Ldependency=";
-    path_output_.WriteDir(out_, dir, PathOutput::DIR_NO_LAST_SLASH);
+    rustdeps_val << " -Ldependency=";
+    path_output_.WriteDir(rustdeps_val, dir, PathOutput::DIR_NO_LAST_SLASH);
   }
 
   // If rustc will invoke a linker, then pass linker arguments to include those
@@ -434,27 +433,30 @@
   // that allows dynamic linking, as rustc may have previously put it into
   // static-only mode.
   if (nonrustdeps.size() > 0) {
-    out_ << " " << tool_->dynamic_link_switch();
+    rustdeps_val << " " << tool_->dynamic_link_switch();
   }
   for (const auto& nonrustdep : nonrustdeps) {
-    out_ << " -Clink-arg=";
-    path_output_.WriteFile(out_, nonrustdep);
+    rustdeps_val << " -Clink-arg=";
+    path_output_.WriteFile(rustdeps_val, nonrustdep);
   }
 
   // Library search paths are required to find system libraries named in #[link]
   // directives, which will not be specified in non-Rust native dependencies.
-  WriteLibrarySearchPath(out_, tool_);
+  WriteLibrarySearchPath(rustdeps_val, tool_);
   // If rustc will invoke a linker, all libraries need the passed through to the
   // linker.
-  WriteLibs(out_, tool_);
-  WriteFrameworks(out_, tool_);
-  WriteSwiftModules(out_, tool_, swiftmodules);
+  WriteLibs(rustdeps_val, tool_);
+  WriteFrameworks(rustdeps_val, tool_);
+  WriteSwiftModules(rustdeps_val, tool_, swiftmodules);
 
-  out_ << std::endl;
-  out_ << "  ldflags =";
+  target_group_.edges.back().edge_vars.emplace_back("rustdeps",
+                                                    rustdeps_val.str());
+
+  std::ostringstream ldflags_val;
   // If rustc will invoke a linker, linker flags need to be forwarded through to
   // the linker.
-  WriteCustomLinkerFlags(out_, tool_);
+  WriteCustomLinkerFlags(ldflags_val, tool_);
 
-  out_ << std::endl;
+  target_group_.edges.back().edge_vars.emplace_back("ldflags",
+                                                    ldflags_val.str());
 }
diff --git a/src/gn/ninja_rust_binary_target_writer.h b/src/gn/ninja_rust_binary_target_writer.h
index 83e1203..0b08b0d 100644
--- a/src/gn/ninja_rust_binary_target_writer.h
+++ b/src/gn/ninja_rust_binary_target_writer.h
@@ -17,7 +17,7 @@
   NinjaRustBinaryTargetWriter(const Target* target, std::ostream& out);
   ~NinjaRustBinaryTargetWriter() override;
 
-  void Run() override;
+  void GenerateRules() override;
 
  private:
   struct ExternCrate {
diff --git a/src/gn/ninja_rust_binary_target_writer_unittest.cc b/src/gn/ninja_rust_binary_target_writer_unittest.cc
index 3f20c5a..201e1e7 100644
--- a/src/gn/ninja_rust_binary_target_writer_unittest.cc
+++ b/src/gn/ninja_rust_binary_target_writer_unittest.cc
@@ -56,8 +56,8 @@
     const char expected[] =
         "crate_name = foo_bar\n"
         "crate_type = bin\n"
-        "output_extension = \n"
-        "output_dir = \n"
+        "output_extension =\n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -65,6 +65,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build ./foo_bar: rust_bin ../../foo/main.rs | ../../foo/input3.rs "
         "../../foo/main.rs\n"
         "  source_file_part = main.rs\n"
@@ -112,7 +113,7 @@
         "crate_name = privatecrate\n"
         "crate_type = rlib\n"
         "output_extension = .rlib\n"
-        "output_dir = \n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -120,6 +121,7 @@
         "target_out_dir = obj/baz\n"
         "target_output_name = libprivatelib\n"
         "\n"
+        "\n"
         "build obj/baz/libprivatelib.rlib: rust_rlib ../../baz/lib.rs | "
         "../../baz/privatelib.rs ../../baz/lib.rs\n"
         "  source_file_part = lib.rs\n"
@@ -154,7 +156,7 @@
         "crate_name = farcrate\n"
         "crate_type = rlib\n"
         "output_extension = .rlib\n"
-        "output_dir = \n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -162,6 +164,7 @@
         "target_out_dir = obj/far\n"
         "target_output_name = libfarlib\n"
         "\n"
+        "\n"
         "build obj/far/libfarlib.rlib: rust_rlib ../../far/lib.rs | "
         "../../far/farlib.rs ../../far/lib.rs\n"
         "  source_file_part = lib.rs\n"
@@ -196,7 +199,7 @@
         "crate_name = publiccrate\n"
         "crate_type = rlib\n"
         "output_extension = .rlib\n"
-        "output_dir = \n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -204,6 +207,7 @@
         "target_out_dir = obj/bar\n"
         "target_output_name = libpubliclib\n"
         "\n"
+        "\n"
         "build obj/bar/libpubliclib.rlib: rust_rlib ../../bar/lib.rs | "
         "../../bar/publiclib.rs ../../bar/lib.rs obj/far/libfarlib.rlib\n"
         "  source_file_part = lib.rs\n"
@@ -251,8 +255,8 @@
     const char expected[] =
         "crate_name = main_crate\n"
         "crate_type = bin\n"
-        "output_extension = \n"
-        "output_dir = \n"
+        "output_extension =\n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -260,6 +264,7 @@
         "target_out_dir = obj/main\n"
         "target_output_name = main\n"
         "\n"
+        "\n"
         "build ./main_crate: rust_bin ../../main/main.rs | "
         "../../main/source.rs ../../main/main.rs obj/foo/libdirect.rlib "
         "obj/bar/libpubliclib.rlib obj/far/libfarlib.rlib "
@@ -305,7 +310,7 @@
         "crate_name = private_inside\n"
         "crate_type = rlib\n"
         "output_extension = .rlib\n"
-        "output_dir = \n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -313,6 +318,7 @@
         "target_out_dir = obj/faz\n"
         "target_output_name = libprivate_inside\n"
         "\n"
+        "\n"
         "build obj/faz/libprivate_inside.rlib: rust_rlib ../../faz/lib.rs | "
         "../../faz/private_inside.rs ../../faz/lib.rs\n"
         "  source_file_part = lib.rs\n"
@@ -346,7 +352,7 @@
         "crate_name = inside\n"
         "crate_type = rlib\n"
         "output_extension = .rlib\n"
-        "output_dir = \n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -354,6 +360,7 @@
         "target_out_dir = obj/baz\n"
         "target_output_name = libinside\n"
         "\n"
+        "\n"
         "build obj/baz/libinside.rlib: rust_rlib ../../baz/lib.rs | "
         "../../baz/inside.rs ../../baz/lib.rs\n"
         "  source_file_part = lib.rs\n"
@@ -390,7 +397,7 @@
         "crate_name = mylib\n"
         "crate_type = dylib\n"
         "output_extension = .so\n"
-        "output_dir = \n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -398,6 +405,7 @@
         "target_out_dir = obj/bar\n"
         "target_output_name = libmylib\n"
         "\n"
+        "\n"
         "build obj/bar/libmylib.so: rust_dylib ../../bar/lib.rs | "
         "../../bar/mylib.rs ../../bar/lib.rs "
         "obj/baz/libinside.rlib obj/faz/libprivate_inside.rlib\n"
@@ -462,8 +470,8 @@
     const char expected[] =
         "crate_name = foo_bar\n"
         "crate_type = bin\n"
-        "output_extension = \n"
-        "output_dir = \n"
+        "output_extension =\n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -471,6 +479,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build ./foo_bar: rust_bin ../../foo/main.rs | ../../foo/source.rs "
         "../../foo/main.rs obj/foo/libdirect.so obj/bar/libmylib.so "
         "obj/baz/libinside.rlib\n"
@@ -515,7 +524,7 @@
         "crate_name = mymacro\n"
         "crate_type = proc-macro\n"
         "output_extension = .so\n"
-        "output_dir = \n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -523,6 +532,7 @@
         "target_out_dir = obj/bar\n"
         "target_output_name = libmymacro\n"
         "\n"
+        "\n"
         "build obj/bar/libmymacro.so: rust_macro ../../bar/lib.rs | "
         "../../bar/mylib.rs ../../bar/lib.rs\n"
         "  source_file_part = lib.rs\n"
@@ -575,7 +585,7 @@
         "crate_name = mylib\n"
         "crate_type = rlib\n"
         "output_extension = .rlib\n"
-        "output_dir = \n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -583,6 +593,7 @@
         "target_out_dir = obj/bar\n"
         "target_output_name = libmylib\n"
         "\n"
+        "\n"
         "build obj/bar/libmylib.rlib: rust_rlib ../../bar/lib.rs | "
         "../../bar/mylib.rs ../../bar/lib.rs obj/bar/libmymacro.so || "
         "obj/bar/libmymacro.so\n"
@@ -617,8 +628,8 @@
     const char expected[] =
         "crate_name = foo_bar\n"
         "crate_type = bin\n"
-        "output_extension = \n"
-        "output_dir = \n"
+        "output_extension =\n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -626,6 +637,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build ./foo_bar: rust_bin ../../foo/main.rs | "
         "../../foo/source.rs ../../foo/main.rs "
         "obj/bar/libmylib.rlib obj/bar/libmymacro.so\n"
@@ -702,8 +714,8 @@
     const char expected[] =
         "crate_name = f_crate\n"
         "crate_type = bin\n"
-        "output_extension = \n"
-        "output_dir = \n"
+        "output_extension =\n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -711,6 +723,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = f\n"
         "\n"
+        "\n"
         "build ./f_crate: rust_bin ../../foo/main.rs | ../../foo/main.rs "
         "obj/foo/b.b.o || phony/foo/b.linkdeps\n"
         "  source_file_part = main.rs\n"
@@ -792,8 +805,8 @@
     const char expected[] =
         "crate_name = foo_bar\n"
         "crate_type = bin\n"
-        "output_extension = \n"
-        "output_dir = \n"
+        "output_extension =\n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -801,6 +814,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build ./foo_bar: rust_bin ../../foo/main.rs | ../../foo/source.rs "
         "../../foo/main.rs obj/bar/libdirect.rlib obj/baz/libmylib.rlib "
         "obj/faz/libtransitive.rlib\n"
@@ -895,8 +909,8 @@
     const char expected[] =
         "crate_name = foo_bar\n"
         "crate_type = bin\n"
-        "output_extension = \n"
-        "output_dir = \n"
+        "output_extension =\n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -904,6 +918,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build ./foo_bar: rust_bin ../../foo/main.rs | ../../foo/source.rs "
         "../../foo/main.rs obj/baz/sourceset.csourceset.o "
         "obj/bar/libmylib.rlib "
@@ -942,8 +957,8 @@
     const char expected[] =
         "crate_name = foo_bar\n"
         "crate_type = bin\n"
-        "output_extension = \n"
-        "output_dir = \n"
+        "output_extension =\n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -951,6 +966,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build ./foo_bar: rust_bin ../../foo/main.rs | ../../foo/source.rs "
         "../../foo/main.rs obj/foo/libstatic.a\n"
         "  source_file_part = main.rs\n"
@@ -984,7 +1000,7 @@
         "crate_name = baz\n"
         "crate_type = staticlib\n"
         "output_extension = .a\n"
-        "output_dir = \n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -992,6 +1008,7 @@
         "target_out_dir = obj/baz\n"
         "target_output_name = libbaz\n"
         "\n"
+        "\n"
         "build obj/baz/libbaz.a: rust_staticlib ../../baz/lib.rs | "
         "../../baz/lib.rs "
         "obj/foo/libstatic.a\n"
@@ -1155,8 +1172,8 @@
   const char expected[] =
       "crate_name = exe\n"
       "crate_type = bin\n"
-      "output_extension = \n"
-      "output_dir = \n"
+      "output_extension =\n"
+      "output_dir =\n"
       "rustflags =\n"
       "rustenv =\n"
       "root_out_dir = .\n"
@@ -1164,6 +1181,7 @@
       "target_out_dir = obj/exe\n"
       "target_output_name = exe\n"
       "\n"
+      "\n"
       "build ./exe: rust_bin ../../exe/main.rs | "
       "../../exe/main.rs "
       "obj/pub_sset_in_staticlib/pub_sset_in_staticlib.lib.o "
@@ -1231,6 +1249,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build ./foo_bar.exe: rust_bin ../../foo/main.rs | ../../foo/input3.rs "
         "../../foo/main.rs\n"
         "  source_file_part = main.rs\n"
@@ -1289,7 +1308,7 @@
     const char expected[] =
         "crate_name = foo_bar\n"
         "crate_type = bin\n"
-        "output_extension = \n"
+        "output_extension =\n"
         "output_dir = foo\n"
         "rustflags =\n"
         "rustenv =\n"
@@ -1298,6 +1317,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build ./foo_bar: rust_bin ../../foo/main.rs | ../../foo/input.rs "
         "../../foo/main.rs obj/bar/librlib.rlib\n"
         "  source_file_part = main.rs\n"
@@ -1390,7 +1410,7 @@
         "crate_name = rlibcrate\n"
         "crate_type = rlib\n"
         "output_extension = .rlib\n"
-        "output_dir = \n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -1398,6 +1418,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = librlibcrate\n"
         "\n"
+        "\n"
         "build obj/foo/librlibcrate.rlib: rust_rlib ../../foo/input.rs | "
         "../../foo/input.rs obj/bar/libpubliclib.rlib obj/clib/libstatic.a\n"
         "  source_file_part = input.rs\n"
@@ -1474,7 +1495,7 @@
         "crate_name = mymacro\n"
         "crate_type = proc-macro\n"
         "output_extension = .so\n"
-        "output_dir = \n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -1482,6 +1503,7 @@
         "target_out_dir = obj/bar\n"
         "target_output_name = libmymacro\n"
         "\n"
+        "\n"
         "build obj/bar/libmymacro.so: rust_macro ../../bar/lib.rs | "
         "../../bar/mylib.rs ../../bar/lib.rs "
         "obj/baz/public/libmymacropublicdep.rlib "
@@ -1520,8 +1542,8 @@
     const char expected[] =
         "crate_name = foo_bar\n"
         "crate_type = bin\n"
-        "output_extension = \n"
-        "output_dir = \n"
+        "output_extension =\n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -1529,6 +1551,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build ./foo_bar: rust_bin ../../foo/main.rs | ../../foo/source.rs "
         "../../foo/main.rs obj/bar/libmymacro.so\n"
         "  source_file_part = main.rs\n"
@@ -1567,7 +1590,7 @@
         "crate_name = mylib\n"
         "crate_type = rlib\n"
         "output_extension = .rlib\n"
-        "output_dir = \n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -1575,6 +1598,7 @@
         "target_out_dir = obj/bar\n"
         "target_output_name = libmylib\n"
         "\n"
+        "\n"
         "build obj/bar/libmylib.rlib: rust_rlib ../../bar/lib.rs | "
         "../../bar/mylib.rs ../../bar/lib.rs\n"
         "  source_file_part = lib.rs\n"
@@ -1615,8 +1639,8 @@
     const char expected[] =
         "crate_name = foo_bar\n"
         "crate_type = bin\n"
-        "output_extension = \n"
-        "output_dir = \n"
+        "output_extension =\n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -1624,6 +1648,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build ./foo_bar: rust_bin ../../foo/main.rs | "
         "../../foo/source.rs ../../foo/main.rs obj/bar/libmylib.rlib || "
         "obj/bar/libmylib.rlib\n"
@@ -1670,8 +1695,8 @@
     const char expected[] =
         "crate_name = foo_bar\n"
         "crate_type = bin\n"
-        "output_extension = \n"
-        "output_dir = \n"
+        "output_extension =\n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -1679,6 +1704,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build ./foo_bar: rust_bin ../../foo/main.rs | ../../foo/source.rs "
         "../../foo/main.rs ../../foo/lib1.rlib\n"
         "  source_file_part = main.rs\n"
@@ -1717,12 +1743,10 @@
     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"
-        "output_dir = \n"
+        "output_extension =\n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -1730,6 +1754,10 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
+        "build phony/foo/bar.inputs: phony ../../foo/config.json "
+        "../../foo/template.h\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"
@@ -1767,7 +1795,7 @@
         "crate_name = mylib\n"
         "crate_type = cdylib\n"
         "output_extension = .so\n"
-        "output_dir = \n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -1775,6 +1803,7 @@
         "target_out_dir = obj/bar\n"
         "target_output_name = libmylib\n"
         "\n"
+        "\n"
         "build obj/bar/libmylib.so: rust_cdylib ../../bar/lib.rs | "
         "../../bar/lib.rs\n"
         "  source_file_part = lib.rs\n"
@@ -1807,8 +1836,8 @@
     const char expected[] =
         "crate_name = foo_bar\n"
         "crate_type = bin\n"
-        "output_extension = \n"
-        "output_dir = \n"
+        "output_extension =\n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -1816,6 +1845,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build ./foo_bar: rust_bin ../../foo/main.rs | ../../foo/source.rs "
         "../../foo/main.rs obj/bar/libmylib.so\n"
         "  source_file_part = main.rs\n"
@@ -1884,8 +1914,8 @@
     const char expected[] =
         "crate_name = foo_bar\n"
         "crate_type = bin\n"
-        "output_extension = \n"
-        "output_dir = \n"
+        "output_extension =\n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -1893,6 +1923,7 @@
         "target_out_dir = obj/foo\n"
         "target_output_name = bar\n"
         "\n"
+        "\n"
         "build ./foo_bar: rust_bin ../../foo/main.rs | ../../foo/main.rs "
         "obj/bar/libmylib.rlib ./libshared.so ./libimplicit.so\n"
         "  source_file_part = main.rs\n"
@@ -1978,8 +2009,8 @@
     const char expected[] =
         "crate_name = exe\n"
         "crate_type = bin\n"
-        "output_extension = \n"
-        "output_dir = \n"
+        "output_extension =\n"
+        "output_dir =\n"
         "rustflags =\n"
         "rustenv =\n"
         "root_out_dir = .\n"
@@ -1987,6 +2018,7 @@
         "target_out_dir = obj/linked\n"
         "target_output_name = exe\n"
         "\n"
+        "\n"
         "build ./exe: rust_bin ../../linked/exe.rs | ../../linked/exe.rs "
         "obj/sset/bar.input1.o obj/public/libbehind_sourceset_public.rlib "
         "obj/private/libbehind_sourceset_private.rlib || "
@@ -2035,8 +2067,8 @@
   const char expected[] =
       "crate_name = bar\n"
       "crate_type = bin\n"
-      "output_extension = \n"
-      "output_dir = \n"
+      "output_extension =\n"
+      "output_dir =\n"
       "rustflags =\n"
       "rustenv =\n"
       "root_out_dir = .\n"
@@ -2044,14 +2076,15 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = bar\n"
       "\n"
+      "\n"
       "build ./bar: rust_bin ../../foo/source.rs | ../../foo/source.rs\n"
       "  source_file_part = source.rs\n"
       "  source_name_part = source\n"
+      "  pool = foo_pool\n"
       "  externs =\n"
       "  rustdeps =\n"
       "  ldflags =\n"
-      "  sources = ../../foo/source.rs\n"
-      "  pool = foo_pool\n";
+      "  sources = ../../foo/source.rs\n";
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
 }
@@ -2108,8 +2141,8 @@
   const char expected[] =
       "crate_name = exe\n"
       "crate_type = bin\n"
-      "output_extension = \n"
-      "output_dir = \n"
+      "output_extension =\n"
+      "output_dir =\n"
       "rustflags =\n"
       "rustenv =\n"
       "root_out_dir = .\n"
@@ -2117,6 +2150,7 @@
       "target_out_dir = obj/linked\n"
       "target_output_name = exe\n"
       "\n"
+      "\n"
       "build ./exe: rust_bin ../../linked/exe.rs | ../../linked/exe.rs || "
       "phony/bar/framework\n"
       "  source_file_part = exe.rs\n"
@@ -2166,8 +2200,8 @@
   const char expected[] =
       "crate_name = exe\n"
       "crate_type = bin\n"
-      "output_extension = \n"
-      "output_dir = \n"
+      "output_extension =\n"
+      "output_dir =\n"
       "rustflags =\n"
       "rustenv =\n"
       "root_out_dir = .\n"
@@ -2175,6 +2209,7 @@
       "target_out_dir = obj/linked\n"
       "target_output_name = exe\n"
       "\n"
+      "\n"
       "build ./exe: rust_bin ../../linked/exe.rs | ../../linked/exe.rs "
       "obj/foo/file1.o obj/foo/file2.o || "
       "phony/foo/foo obj/foo/Foo.swiftmodule phony/foo/foo.linkdeps\n"
diff --git a/src/gn/ninja_target_writer.cc b/src/gn/ninja_target_writer.cc
index d3d1eb1..e35d39a 100644
--- a/src/gn/ninja_target_writer.cc
+++ b/src/gn/ninja_target_writer.cc
@@ -7,12 +7,10 @@
 #include <algorithm>
 #include <sstream>
 
-#include "base/files/file_util.h"
 #include "base/strings/string_util.h"
 #include "gn/builtin_tool.h"
 #include "gn/c_substitution_type.h"
 #include "gn/config_values_extractors.h"
-#include "gn/err.h"
 #include "gn/escape.h"
 #include "gn/filesystem_utils.h"
 #include "gn/general_tool.h"
@@ -29,7 +27,6 @@
 #include "gn/rust_substitution_type.h"
 #include "gn/scheduler.h"
 #include "gn/string_output_buffer.h"
-#include "gn/string_utils.h"
 #include "gn/substitution_writer.h"
 #include "gn/target.h"
 #include "gn/trace.h"
@@ -40,7 +37,55 @@
       out_(out),
       path_output_(settings_->build_settings()->build_dir(),
                    settings_->build_settings()->root_path_utf8(),
-                   ESCAPE_NINJA) {}
+                   ESCAPE_NINJA) {
+  target_group_.target = target;
+}
+
+void NinjaTargetWriter::AddTargetVar(std::string_view name, std::string value) {
+  target_group_.target_vars.emplace_back(name, std::move(value));
+}
+
+void NinjaTargetWriter::AddEdge(NinjaBuildEdge edge) {
+  target_group_.edges.push_back(std::move(edge));
+}
+
+NinjaTargetGroup NinjaTargetWriter::GenerateTargetGroup() {
+  GenerateRules();
+  return std::move(target_group_);
+}
+
+void NinjaTargetWriter::SetNinjaOutputs(
+    std::vector<OutputFile>* ninja_outputs) {
+  ninja_outputs_ = ninja_outputs;
+}
+
+void NinjaTargetWriter::Run() {
+  NinjaTargetGroup group = GenerateTargetGroup();
+  if (ninja_outputs_) {
+    for (const auto& edge : group.edges) {
+      if (edge.is_target_output) {
+        ninja_outputs_->insert(ninja_outputs_->end(), edge.outputs.begin(),
+                               edge.outputs.end());
+        ninja_outputs_->insert(ninja_outputs_->end(),
+                               edge.implicit_outputs.begin(),
+                               edge.implicit_outputs.end());
+      }
+    }
+  }
+  NinjaFile file;
+  file.AddTargetGroup(std::move(group));
+  file.Serialize(out_);
+}
+
+std::vector<OutputFile> NinjaTargetWriter::ToOutputFiles(
+    const std::vector<SourceFile>& sources) const {
+  std::vector<OutputFile> outputs;
+  outputs.reserve(sources.size());
+  for (const auto& source : sources) {
+    outputs.emplace_back(settings_->build_settings(), source);
+  }
+  return outputs;
+}
 
 void NinjaTargetWriter::SetResolvedTargetData(ResolvedTargetData* resolved) {
   if (resolved) {
@@ -49,11 +94,6 @@
   }
 }
 
-void NinjaTargetWriter::SetNinjaOutputs(
-    std::vector<OutputFile>* ninja_outputs) {
-  ninja_outputs_ = ninja_outputs;
-}
-
 ResolvedTargetData* NinjaTargetWriter::GetResolvedTargetData() {
   return const_cast<ResolvedTargetData*>(&resolved());
 }
@@ -68,34 +108,6 @@
 
 NinjaTargetWriter::~NinjaTargetWriter() = default;
 
-void NinjaTargetWriter::WriteOutput(const OutputFile& output) const {
-  path_output_.WriteFile(out_, output);
-  if (ninja_outputs_)
-    ninja_outputs_->push_back(output);
-}
-
-void NinjaTargetWriter::WriteOutput(OutputFile&& output) const {
-  path_output_.WriteFile(out_, output);
-  if (ninja_outputs_)
-    ninja_outputs_->push_back(std::move(output));
-}
-
-void NinjaTargetWriter::WriteOutputs(
-    const std::vector<OutputFile>& outputs) const {
-  path_output_.WriteFiles(out_, outputs);
-  if (ninja_outputs_)
-    ninja_outputs_->insert(ninja_outputs_->end(), outputs.begin(),
-                           outputs.end());
-}
-
-void NinjaTargetWriter::WriteOutputs(std::vector<OutputFile>&& outputs) const {
-  path_output_.WriteFiles(out_, outputs);
-  if (ninja_outputs_) {
-    for (auto& output : outputs)
-      ninja_outputs_->push_back(std::move(output));
-  }
-}
-
 // static
 std::string NinjaTargetWriter::RunAndWriteFile(
     const Target* target,
@@ -110,10 +122,8 @@
   if (g_scheduler->verbose_logging())
     g_scheduler->Log("Computing", target->label().GetUserVisibleName(true));
 
-  // It's ridiculously faster to write to a string and then write that to
-  // disk in one operation than to use an fstream here.
-  StringOutputBuffer storage;
-  std::ostream rules(&storage);
+  StringOutputBuffer dummy_storage;
+  std::ostream dummy_rules(&dummy_storage);
 
   // Call out to the correct sub-type of writer. Binary targets need to be
   // written to separate files for compiler flag scoping, but other target
@@ -130,43 +140,37 @@
   // Groups and actions don't use this type of flag, they make unique rules
   // or write variables scoped under each build line. As a result, they don't
   // need the separate files.
+  NinjaTargetGroup group;
   bool needs_file_write = false;
   if (target->output_type() == Target::BUNDLE_DATA) {
-    NinjaBundleDataTargetWriter writer(target, rules);
+    NinjaBundleDataTargetWriter writer(target, dummy_rules);
     writer.SetResolvedTargetData(resolved);
-    writer.SetNinjaOutputs(ninja_outputs);
-    writer.Run();
+    group = writer.GenerateTargetGroup();
   } else if (target->output_type() == Target::CREATE_BUNDLE) {
-    NinjaCreateBundleTargetWriter writer(target, rules);
+    NinjaCreateBundleTargetWriter writer(target, dummy_rules);
     writer.SetResolvedTargetData(resolved);
-    writer.SetNinjaOutputs(ninja_outputs);
-    writer.Run();
+    group = writer.GenerateTargetGroup();
   } else if (target->output_type() == Target::COPY_FILES) {
-    NinjaCopyTargetWriter writer(target, rules);
+    NinjaCopyTargetWriter writer(target, dummy_rules);
     writer.SetResolvedTargetData(resolved);
-    writer.SetNinjaOutputs(ninja_outputs);
-    writer.Run();
+    group = writer.GenerateTargetGroup();
   } else if (target->output_type() == Target::ACTION ||
              target->output_type() == Target::ACTION_FOREACH) {
-    NinjaActionTargetWriter writer(target, rules);
+    NinjaActionTargetWriter writer(target, dummy_rules);
     writer.SetResolvedTargetData(resolved);
-    writer.SetNinjaOutputs(ninja_outputs);
-    writer.Run();
+    group = writer.GenerateTargetGroup();
   } else if (target->output_type() == Target::GROUP) {
-    NinjaGroupTargetWriter writer(target, rules);
+    NinjaGroupTargetWriter writer(target, dummy_rules);
     writer.SetResolvedTargetData(resolved);
-    writer.SetNinjaOutputs(ninja_outputs);
-    writer.Run();
+    group = writer.GenerateTargetGroup();
   } else if (target->output_type() == Target::GENERATED_FILE) {
-    NinjaGeneratedFileTargetWriter writer(target, rules);
+    NinjaGeneratedFileTargetWriter writer(target, dummy_rules);
     writer.SetResolvedTargetData(resolved);
-    writer.SetNinjaOutputs(ninja_outputs);
-    writer.Run();
+    group = writer.GenerateTargetGroup();
   } else if (target->IsBinary()) {
     needs_file_write = true;
-    NinjaBinaryTargetWriter writer(target, rules);
+    NinjaBinaryTargetWriter writer(target, dummy_rules);
     writer.SetResolvedTargetData(resolved);
-    writer.SetNinjaOutputs(ninja_outputs);
     if (target->module_type().test(Target::MODULEMAP_IS_GENERATED)) {
       const SourceFile* modulemap = target->modulemap_file();
       CHECK(modulemap);
@@ -187,12 +191,32 @@
           *target->private_modulemap_file());
       private_storage.WriteToFileIfChanged(private_path, nullptr);
     }
-    writer.Run();
+    group = writer.GenerateTargetGroup();
   } else {
     CHECK(0) << "Output type of target not handled.";
   }
 
-  WritePublicInputsStampOrPhony(target, resolved, rules);
+  if (ninja_outputs) {
+    for (const auto& edge : group.edges) {
+      if (edge.is_target_output) {
+        ninja_outputs->insert(ninja_outputs->end(), edge.outputs.begin(),
+                              edge.outputs.end());
+        ninja_outputs->insert(ninja_outputs->end(),
+                              edge.implicit_outputs.begin(),
+                              edge.implicit_outputs.end());
+      }
+    }
+  }
+
+  WritePublicInputsStampOrPhony(target, resolved, group);
+
+  NinjaFile file;
+  file.AddTargetGroup(std::move(group));
+
+  // It's ridiculously faster to write to a string and then write that to
+  // disk in one operation than to use an fstream here.
+  StringOutputBuffer storage;
+  file.Serialize(storage);
 
   if (needs_file_write) {
     // Write the ninja file.
@@ -221,7 +245,7 @@
 void NinjaTargetWriter::WritePublicInputsStampOrPhony(
     const Target* target,
     ResolvedTargetData* resolved,
-    std::ostream& out) {
+    NinjaTargetGroup& group) {
   DCHECK(resolved);
   if (!resolved->ExportsPublicInputs(target))
     return;
@@ -231,7 +255,7 @@
 
   std::vector<OutputFile> deps;
   for (const auto& file : target->public_inputs()) {
-    deps.push_back(OutputFile(build_settings, file));
+    deps.emplace_back(build_settings, file);
   }
   for (const auto& dep : target->public_deps()) {
     if (resolved->ExportsPublicInputs(dep.ptr)) {
@@ -239,111 +263,91 @@
     }
   }
 
-  PathOutput path_output(build_settings->build_dir(),
-                         build_settings->root_path_utf8(), ESCAPE_NINJA);
-
-  out << "build ";
-  path_output.WriteFile(out, output);
-
+  std::string rule;
   if (build_settings->no_stamp_files()) {
-    out << ": " << BuiltinTool::kBuiltinToolPhony;
+    rule = BuiltinTool::kBuiltinToolPhony;
   } else {
-    out << ": " << GetNinjaRulePrefixForToolchain(target->settings())
-        << GeneralTool::kGeneralToolStamp;
+    rule = GetNinjaRulePrefixForToolchain(target->settings()) +
+           GeneralTool::kGeneralToolStamp;
   }
 
-  path_output.WriteFiles(out, deps);
-  out << std::endl << std::endl;
+  group.edges.push_back(NinjaBuildEdge{
+      .rule = std::move(rule),
+      .outputs = {output},
+      .explicit_inputs = std::move(deps),
+      .is_target_output = false,
+  });
 }
 
 void NinjaTargetWriter::WriteEscapedSubstitution(const Substitution* type) {
   EscapeOptions opts;
   opts.mode = ESCAPE_NINJA;
 
-  out_ << type->ninja_name << " = ";
+  std::ostringstream val;
   EscapeStringToStream(
-      out_, SubstitutionWriter::GetTargetSubstitution(target_, type), opts);
-  out_ << std::endl;
+      val, SubstitutionWriter::GetTargetSubstitution(target_, type), opts);
+  target_group_.target_vars.emplace_back(type->ninja_name, val.str());
 }
 
 void NinjaTargetWriter::WriteSharedVars(const SubstitutionBits& bits) {
-  bool written_anything = false;
-
   // Target label.
   if (bits.used.count(&SubstitutionLabel)) {
     WriteEscapedSubstitution(&SubstitutionLabel);
-    written_anything = true;
   }
 
   // Target label name.
   if (bits.used.count(&SubstitutionLabelName)) {
     WriteEscapedSubstitution(&SubstitutionLabelName);
-    written_anything = true;
   }
 
   // Target label name without toolchain.
   if (bits.used.count(&SubstitutionLabelNoToolchain)) {
     WriteEscapedSubstitution(&SubstitutionLabelNoToolchain);
-    written_anything = true;
   }
 
   // Root gen dir.
   if (bits.used.count(&SubstitutionRootGenDir)) {
     WriteEscapedSubstitution(&SubstitutionRootGenDir);
-    written_anything = true;
   }
 
   // Root out dir.
   if (bits.used.count(&SubstitutionRootOutDir)) {
     WriteEscapedSubstitution(&SubstitutionRootOutDir);
-    written_anything = true;
   }
 
   // Target gen dir.
   if (bits.used.count(&SubstitutionTargetGenDir)) {
     WriteEscapedSubstitution(&SubstitutionTargetGenDir);
-    written_anything = true;
   }
 
   // Target out dir.
   if (bits.used.count(&SubstitutionTargetOutDir)) {
     WriteEscapedSubstitution(&SubstitutionTargetOutDir);
-    written_anything = true;
   }
 
   // Target output name.
   if (bits.used.count(&SubstitutionTargetOutputName)) {
     WriteEscapedSubstitution(&SubstitutionTargetOutputName);
-    written_anything = true;
   }
-
-  // If we wrote any vars, separate them from the rest of the file that follows
-  // with a blank line.
-  if (written_anything)
-    out_ << std::endl;
 }
 
-void NinjaTargetWriter::WriteCCompilerVars(const SubstitutionBits& bits,
-                                           bool indent,
-                                           bool respect_source_used) {
+void NinjaTargetWriter::WriteCCompilerVars(
+    const SubstitutionBits& bits,
+    bool respect_source_used,
+    std::vector<NinjaVariable>& target_vars) {
   // Defines.
   if (bits.used.count(&CSubstitutionDefines)) {
-    if (indent)
-      out_ << "  ";
-    out_ << CSubstitutionDefines.ninja_name << " =";
+    std::ostringstream val;
     RecursiveTargetConfigToStream<std::string>(kRecursiveWriterSkipDuplicates,
                                                target_, &ConfigValues::defines,
-                                               DefineWriter(), out_);
-    out_ << std::endl;
+                                               DefineWriter(), val);
+    target_vars.emplace_back(CSubstitutionDefines.ninja_name, val.str());
   }
 
   // Framework search path.
   if (bits.used.count(&CSubstitutionFrameworkDirs)) {
     const Tool* tool = target_->toolchain()->GetTool(CTool::kCToolLink);
-
-    if (indent)
-      out_ << "  ";
-    out_ << CSubstitutionFrameworkDirs.ninja_name << " =";
+    std::ostringstream val;
     PathOutput framework_dirs_output(
         path_output_.current_dir(),
         settings_->build_settings()->root_path_utf8(), ESCAPE_NINJA_COMMAND);
@@ -351,22 +355,20 @@
         kRecursiveWriterSkipDuplicates, target_, &ConfigValues::framework_dirs,
         FrameworkDirsWriter(framework_dirs_output,
                             tool->framework_dir_switch()),
-        out_);
-    out_ << std::endl;
+        val);
+    target_vars.emplace_back(CSubstitutionFrameworkDirs.ninja_name, val.str());
   }
 
   // Include directories.
   if (bits.used.count(&CSubstitutionIncludeDirs)) {
-    if (indent)
-      out_ << "  ";
-    out_ << CSubstitutionIncludeDirs.ninja_name << " =";
+    std::ostringstream val;
     PathOutput include_path_output(
         path_output_.current_dir(),
         settings_->build_settings()->root_path_utf8(), ESCAPE_NINJA_COMMAND);
     RecursiveTargetConfigToStream<SourceDir>(
         kRecursiveWriterSkipDuplicates, target_, &ConfigValues::include_dirs,
-        IncludeWriter(include_path_output), out_);
-    out_ << std::endl;
+        IncludeWriter(include_path_output), val);
+    target_vars.emplace_back(CSubstitutionIncludeDirs.ninja_name, val.str());
   }
 
   bool has_precompiled_headers =
@@ -374,13 +376,24 @@
 
   EscapeOptions opts;
   opts.mode = ESCAPE_NINJA_COMMAND;
+
+  auto write_flag =
+      [&](const Substitution* subst, bool has_pch, const char* tool_name,
+          const std::vector<std::string>& (ConfigValues::*getter)() const) {
+        if (!target_->toolchain()->substitution_bits().used.count(subst))
+          return;
+        std::ostringstream val;
+        WriteOneFlag(kRecursiveWriterKeepDuplicates, target_, subst, has_pch,
+                     tool_name, getter, opts, path_output_, val,
+                     /*write_substitution=*/false, /*indent=*/false);
+        target_vars.emplace_back(subst->ninja_name, val.str());
+      };
+
   if (respect_source_used
           ? target_->source_types_used().Get(SourceFile::SOURCE_S)
           : bits.used.count(&CSubstitutionAsmFlags)) {
-    WriteOneFlag(kRecursiveWriterKeepDuplicates, target_,
-                 &CSubstitutionAsmFlags, false, Tool::kToolNone,
-                 &ConfigValues::asmflags, opts, path_output_, out_, true,
-                 indent);
+    write_flag(&CSubstitutionAsmFlags, false, Tool::kToolNone,
+               &ConfigValues::asmflags);
   }
   if (respect_source_used
           ? (target_->source_types_used().Get(SourceFile::SOURCE_C) ||
@@ -389,62 +402,51 @@
              target_->source_types_used().Get(SourceFile::SOURCE_MM) ||
              target_->source_types_used().Get(SourceFile::SOURCE_MODULEMAP))
           : bits.used.count(&CSubstitutionCFlags)) {
-    WriteOneFlag(kRecursiveWriterKeepDuplicates, target_, &CSubstitutionCFlags,
-                 false, Tool::kToolNone, &ConfigValues::cflags, opts,
-                 path_output_, out_, true, indent);
+    write_flag(&CSubstitutionCFlags, false, Tool::kToolNone,
+               &ConfigValues::cflags);
   }
   if (respect_source_used
           ? target_->source_types_used().Get(SourceFile::SOURCE_C)
           : bits.used.count(&CSubstitutionCFlagsC)) {
-    WriteOneFlag(kRecursiveWriterKeepDuplicates, target_, &CSubstitutionCFlagsC,
-                 has_precompiled_headers, CTool::kCToolCc,
-                 &ConfigValues::cflags_c, opts, path_output_, out_, true,
-                 indent);
+    write_flag(&CSubstitutionCFlagsC, has_precompiled_headers, CTool::kCToolCc,
+               &ConfigValues::cflags_c);
   }
   if (respect_source_used
           ? (target_->source_types_used().Get(SourceFile::SOURCE_CPP) ||
              target_->source_types_used().Get(SourceFile::SOURCE_MODULEMAP))
           : bits.used.count(&CSubstitutionCFlagsCc)) {
-    WriteOneFlag(kRecursiveWriterKeepDuplicates, target_,
-                 &CSubstitutionCFlagsCc, has_precompiled_headers,
-                 CTool::kCToolCxx, &ConfigValues::cflags_cc, opts, path_output_,
-                 out_, true, indent);
+    write_flag(&CSubstitutionCFlagsCc, has_precompiled_headers,
+               CTool::kCToolCxx, &ConfigValues::cflags_cc);
   }
   if (respect_source_used
           ? target_->source_types_used().Get(SourceFile::SOURCE_M)
           : bits.used.count(&CSubstitutionCFlagsObjC)) {
-    WriteOneFlag(kRecursiveWriterKeepDuplicates, target_,
-                 &CSubstitutionCFlagsObjC, has_precompiled_headers,
-                 CTool::kCToolObjC, &ConfigValues::cflags_objc, opts,
-                 path_output_, out_, true, indent);
+    write_flag(&CSubstitutionCFlagsObjC, has_precompiled_headers,
+               CTool::kCToolObjC, &ConfigValues::cflags_objc);
   }
   if (respect_source_used
           ? target_->source_types_used().Get(SourceFile::SOURCE_MM)
           : bits.used.count(&CSubstitutionCFlagsObjCc)) {
-    WriteOneFlag(kRecursiveWriterKeepDuplicates, target_,
-                 &CSubstitutionCFlagsObjCc, has_precompiled_headers,
-                 CTool::kCToolObjCxx, &ConfigValues::cflags_objcc, opts,
-                 path_output_, out_, true, indent);
+    write_flag(&CSubstitutionCFlagsObjCc, has_precompiled_headers,
+               CTool::kCToolObjCxx, &ConfigValues::cflags_objcc);
   }
   if (target_->source_types_used().SwiftSourceUsed() || !respect_source_used) {
     if (bits.used.count(&CSubstitutionSwiftModuleName)) {
-      if (indent)
-        out_ << "  ";
-      out_ << CSubstitutionSwiftModuleName.ninja_name << " = ";
-      EscapeStringToStream(out_, target_->swift_values().module_name(), opts);
-      out_ << std::endl;
+      std::ostringstream val;
+      EscapeStringToStream(val, target_->swift_values().module_name(), opts);
+      target_vars.emplace_back(CSubstitutionSwiftModuleName.ninja_name,
+                               val.str());
     }
 
     if (bits.used.count(&CSubstitutionSwiftBridgeHeader)) {
-      if (indent)
-        out_ << "  ";
-      out_ << CSubstitutionSwiftBridgeHeader.ninja_name << " = ";
+      std::ostringstream val;
       if (!target_->swift_values().bridge_header().is_null()) {
-        path_output_.WriteFile(out_, target_->swift_values().bridge_header());
+        path_output_.WriteFile(val, target_->swift_values().bridge_header());
       } else {
-        out_ << R"("")";
+        val << R"("")";
       }
-      out_ << std::endl;
+      target_vars.emplace_back(CSubstitutionSwiftBridgeHeader.ninja_name,
+                               val.str());
     }
 
     if (bits.used.count(&CSubstitutionSwiftModuleDirs)) {
@@ -454,51 +456,60 @@
       for (const Target* dep : resolved().GetSwiftModuleDependencies(target_))
         swiftmodule_dirs.push_back(dep->swift_values().module_output_dir());
 
-      if (indent)
-        out_ << "  ";
-      out_ << CSubstitutionSwiftModuleDirs.ninja_name << " =";
+      std::ostringstream val;
       PathOutput swiftmodule_path_output(
           path_output_.current_dir(),
           settings_->build_settings()->root_path_utf8(), ESCAPE_NINJA_COMMAND);
       IncludeWriter swiftmodule_path_writer(swiftmodule_path_output);
       for (const SourceDir& swiftmodule_dir : swiftmodule_dirs) {
-        swiftmodule_path_writer(swiftmodule_dir, out_);
+        swiftmodule_path_writer(swiftmodule_dir, val);
       }
-      out_ << std::endl;
+      target_vars.emplace_back(CSubstitutionSwiftModuleDirs.ninja_name,
+                               val.str());
     }
+  }
+}
 
-    WriteOneFlag(kRecursiveWriterKeepDuplicates, target_,
-                 &CSubstitutionSwiftFlags, false, CTool::kCToolSwift,
-                 &ConfigValues::swiftflags, opts, path_output_, out_, true,
-                 indent);
+void NinjaTargetWriter::WriteCCompilerVars(const SubstitutionBits& bits,
+                                           bool respect_source_used) {
+  WriteCCompilerVars(bits, respect_source_used, target_group_.target_vars);
+}
+
+void NinjaTargetWriter::WriteRustCompilerVars(
+    const SubstitutionBits& bits,
+    bool always_write,
+    std::vector<NinjaVariable>& target_vars) {
+  EscapeOptions opts;
+  opts.mode = ESCAPE_NINJA_COMMAND;
+
+  auto write_flag =
+      [&](const Substitution* subst,
+          const std::vector<std::string>& (ConfigValues::*getter)() const) {
+        std::ostringstream val;
+        WriteOneFlag(kRecursiveWriterKeepDuplicates, target_, subst, false,
+                     Tool::kToolNone, getter, opts, path_output_, val,
+                     /*write_substitution=*/false, /*indent=*/false);
+        target_vars.emplace_back(subst->ninja_name, val.str());
+      };
+
+  if (bits.used.count(&kRustSubstitutionRustFlags) || always_write) {
+    write_flag(&kRustSubstitutionRustFlags, &ConfigValues::rustflags);
+  }
+
+  if (bits.used.count(&kRustSubstitutionRustEnv) || always_write) {
+    write_flag(&kRustSubstitutionRustEnv, &ConfigValues::rustenv);
   }
 }
 
 void NinjaTargetWriter::WriteRustCompilerVars(const SubstitutionBits& bits,
-                                              bool indent,
                                               bool always_write) {
-  EscapeOptions opts;
-  opts.mode = ESCAPE_NINJA_COMMAND;
-
-  if (bits.used.count(&kRustSubstitutionRustFlags) || always_write) {
-    WriteOneFlag(kRecursiveWriterKeepDuplicates, target_,
-                 &kRustSubstitutionRustFlags, false, Tool::kToolNone,
-                 &ConfigValues::rustflags, opts, path_output_, out_, true,
-                 indent);
-  }
-
-  if (bits.used.count(&kRustSubstitutionRustEnv) || always_write) {
-    WriteOneFlag(kRecursiveWriterKeepDuplicates, target_,
-                 &kRustSubstitutionRustEnv, false, Tool::kToolNone,
-                 &ConfigValues::rustenv, opts, path_output_, out_, true,
-                 indent);
-  }
+  WriteRustCompilerVars(bits, always_write, target_group_.target_vars);
 }
 
 NinjaTargetWriter::InputDeps
 NinjaTargetWriter::WriteInputDepsStampOrPhonyAndGetDep(
     const std::vector<const Target*>& additional_hard_deps,
-    size_t num_output_uses) const {
+    size_t num_output_uses) {
   CHECK(target_->toolchain()) << "Toolchain not set on target "
                               << target_->label().GetUserVisibleName(true);
 
@@ -606,7 +617,7 @@
 
   // File input deps.
   for (const SourceFile* source : input_deps_sources)
-    deps.order_only.push_back(OutputFile(settings_->build_settings(), *source));
+    deps.order_only.emplace_back(settings_->build_settings(), *source);
   // Target input deps. Sort by label so the output is deterministic (otherwise
   // some of the targets will have gone through std::sets which will have
   // sorted them by pointer).
@@ -663,17 +674,15 @@
            GeneralTool::kGeneralToolStamp;
   }
 
-  // These are not real outputs, so do not use WriteOutput() here.
+  // These are not real outputs, so do not mark as target output.
   // See https://gn.issues.chromium.org/448860851.
-  out_ << "build ";
-  path_output_.WriteFile(out_, input_stamp_or_phony);
-  out_ << ": " << tool;
-  path_output_.WriteFiles(out_, deps.implicit);
-  if (!deps.order_only.empty()) {
-    out_ << " ||";
-    path_output_.WriteFiles(out_, deps.order_only);
-  }
-  out_ << "\n";
+  AddEdge(NinjaBuildEdge{
+      .rule = tool,
+      .outputs = {input_stamp_or_phony},
+      .explicit_inputs = deps.implicit,
+      .order_only_inputs = deps.order_only,
+      .is_target_output = false,
+  });
 
   InputDeps result;
   result.implicit.push_back(input_stamp_or_phony);
@@ -699,35 +708,30 @@
     }
   }
 
+  OutputFile output_file;
+  std::string rule;
   // We should have already discerned whether this target is a stamp or a phony.
   // If there's a dependency_output_file, it should be a stamp. Else is a phony
   // or omitted phony (in which case, we don't write it).
   if (target_->has_dependency_output_file()) {
     // Make a stamp target.
-    const OutputFile& stamp_file = target_->dependency_output_file();
+    output_file = target_->dependency_output_file();
 
     // First validate that the target's dependency is a stamp file. Otherwise,
     // we shouldn't have gotten here!
-    CHECK(base::EndsWithCaseInsensitiveASCII(stamp_file.value(), ".stamp"))
+    CHECK(base::EndsWithCaseInsensitiveASCII(output_file.value(), ".stamp"))
         << "Output should end in \".stamp\" for stamp file output. Instead "
            "got: "
-        << "\"" << stamp_file.value() << "\"";
+        << "\"" << output_file.value() << "\"";
 
-    out_ << "build ";
-    WriteOutput(stamp_file);
-
-    out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
-         << GeneralTool::kGeneralToolStamp;
+    rule = GetNinjaRulePrefixForToolchain(settings_) +
+           GeneralTool::kGeneralToolStamp;
   } else if (target_->has_dependency_output_alias()) {
     // Make a phony target.
-    const OutputFile& phony_target = target_->dependency_output_alias();
-    CHECK(!phony_target.value().empty());
+    output_file = target_->dependency_output_alias();
+    CHECK(!output_file.value().empty());
 
-    out_ << "build ";
-    WriteOutput(phony_target);
-
-    out_ << ": " << BuiltinTool::kBuiltinToolPhony;
-
+    rule = BuiltinTool::kBuiltinToolPhony;
   } else {
     // This is the omitted phony case. We should not get here if there were any
     // dependencies, so ensure that none got added.
@@ -736,32 +740,21 @@
     return;
   }
 
-  path_output_.WriteFiles(out_, all_files);
-
-  if (!order_only_deps.empty()) {
-    out_ << " ||";
-    path_output_.WriteFiles(out_, order_only_deps);
-  }
-  WriteValidations();
-  out_ << std::endl;
+  NinjaBuildEdge edge{
+      .rule = std::move(rule),
+      .outputs = {output_file},
+      .explicit_inputs = std::move(all_files),
+      .order_only_inputs = order_only_deps,
+  };
+  AddValidationInputs(edge);
+  AddEdge(std::move(edge));
 }
 
-void NinjaTargetWriter::WriteValidations() {
-  const LabelTargetVector& validations = target_->validations();
-  if (validations.empty())
-    return;
-
-  bool first = true;
-  for (const auto& pair : validations) {
+void NinjaTargetWriter::AddValidationInputs(NinjaBuildEdge& edge) const {
+  for (const auto& pair : target_->validations()) {
     // This check is needed because empty groups have no output.
-    if (!pair.ptr->has_dependency_output()) {
-      continue;
+    if (pair.ptr->has_dependency_output()) {
+      edge.validation_inputs.push_back(pair.ptr->dependency_output());
     }
-    if (first) {
-      out_ << " |@";
-      first = false;
-    }
-    out_ << " ";
-    WriteOutput(pair.ptr->dependency_output());
   }
 }
diff --git a/src/gn/ninja_target_writer.h b/src/gn/ninja_target_writer.h
index d7b2883..a29a590 100644
--- a/src/gn/ninja_target_writer.h
+++ b/src/gn/ninja_target_writer.h
@@ -7,6 +7,7 @@
 
 #include <iosfwd>
 
+#include "gn/ninja_file.h"
 #include "gn/path_output.h"
 #include "gn/resolved_target_data.h"
 #include "gn/substitution_type.h"
@@ -23,6 +24,12 @@
   NinjaTargetWriter(const Target* target, std::ostream& out);
   virtual ~NinjaTargetWriter();
 
+  NinjaTargetGroup& target_group() { return target_group_; }
+  const NinjaTargetGroup& target_group() const { return target_group_; }
+
+  void AddTargetVar(std::string_view name, std::string value);
+  void AddEdge(NinjaBuildEdge edge);
+
   // Returns a ResolvedTargetData that can be used to retrieve information
   // from targets. The instance can be set through SetResolvedTargetData()
   // or may be created on demand otherwise (which is useful to keep unit-tests
@@ -59,11 +66,19 @@
   // false.
   static void WritePublicInputsStampOrPhony(const Target* target,
                                             ResolvedTargetData* resolved,
-                                            std::ostream& out);
+                                            NinjaTargetGroup& group);
 
-  virtual void Run() = 0;
+  // Generates and returns the populated NinjaTargetGroup by value.
+  NinjaTargetGroup GenerateTargetGroup();
+
+  // Runs rule generation and serializes the target's rules to out_.
+  void Run();
 
  protected:
+  // Generates the NinjaTargetGroup (target variables and edges) without
+  // serializing.
+  virtual void GenerateRules() = 0;
+
   // Returns a writable pointer to resolved(). Only used internally.
   ResolvedTargetData* GetResolvedTargetData();
 
@@ -79,15 +94,18 @@
   // respect the types of source code used; otherwise they will respect the bits
   // passed in.
   void WriteCCompilerVars(const SubstitutionBits& bits,
-                          bool indent,
+                          bool respect_source_used,
+                          std::vector<NinjaVariable>& target_vars);
+  void WriteCCompilerVars(const SubstitutionBits& bits,
                           bool respect_source_used);
 
   // Writes out the substitution values that are shared between Rust tools
   // and action tools. Only the substitutions identified by the given bits will
   // be written, unless 'always_write' is specified.
   void WriteRustCompilerVars(const SubstitutionBits& bits,
-                             bool indent,
-                             bool always_write);
+                             bool always_write,
+                             std::vector<NinjaVariable>& target_vars);
+  void WriteRustCompilerVars(const SubstitutionBits& bits, bool always_write);
 
   struct InputDeps {
     std::vector<OutputFile> implicit;
@@ -103,7 +121,7 @@
   // are passed in, this returns an empty struct.
   InputDeps WriteInputDepsStampOrPhonyAndGetDep(
       const std::vector<const Target*>& additional_hard_deps,
-      size_t num_output_uses) const;
+      size_t num_output_uses);
 
   // Writes to the output file a final phony rule for the target that aliases
   // the given list of files.
@@ -111,23 +129,18 @@
       const std::vector<OutputFile>& deps,
       const std::vector<OutputFile>& order_only_deps);
 
-  // Writes the validation dependencies to the output stream.
-  void WriteValidations();
+  // Appends the target's validation dependencies to the given edge.
+  void AddValidationInputs(NinjaBuildEdge& edge) const;
+
+  // Converts a list of SourceFiles to OutputFiles relative to the build dir.
+  std::vector<OutputFile> ToOutputFiles(
+      const std::vector<SourceFile>& sources) const;
 
   const Settings* settings_;  // Non-owning.
   const Target* target_;      // Non-owning.
   std::ostream& out_;
   PathOutput path_output_;
 
-  // Write a Ninja output file to out_, and also add it to |*ninja_outputs_|
-  // if needed.
-  void WriteOutput(const OutputFile& output) const;
-  void WriteOutput(OutputFile&& output) const;
-
-  // Same as WriteOutput() for a list of Ninja output file paths.
-  void WriteOutputs(const std::vector<OutputFile>& outputs) const;
-  void WriteOutputs(std::vector<OutputFile>&& outputs) const;
-
   // The list of all Ninja output file paths generated by this writer for
   // this target. Used to implement the --ide=ninja_outputs `gn gen` flag.
   // Needs to be mutable because WriteOutput() and WriteOutputs() need to
@@ -140,6 +153,8 @@
   mutable ResolvedTargetData* resolved_ptr_ = nullptr;
   mutable std::unique_ptr<ResolvedTargetData> resolved_owned_;
 
+  NinjaTargetGroup target_group_;
+
  private:
   void WriteCopyRules();
   void WriteEscapedSubstitution(const Substitution* type);
diff --git a/src/gn/ninja_target_writer_unittest.cc b/src/gn/ninja_target_writer_unittest.cc
index 53b1a4d..10bd166 100644
--- a/src/gn/ninja_target_writer_unittest.cc
+++ b/src/gn/ninja_target_writer_unittest.cc
@@ -20,20 +20,27 @@
                            std::ostream& out)
       : NinjaTargetWriter(target, out) {}
 
-  void Run() override {}
+  void GenerateRules() override {}
 
   // Make this public so the test can call it.
   NinjaTargetWriter::InputDeps WriteInputDepsStampOrPhonyAndGetDep(
       const std::vector<const Target*>& additional_hard_deps,
       size_t num_stamp_uses) {
-    return NinjaTargetWriter::WriteInputDepsStampOrPhonyAndGetDep(
+    auto deps = NinjaTargetWriter::WriteInputDepsStampOrPhonyAndGetDep(
         additional_hard_deps, num_stamp_uses);
+    NinjaFile file;
+    file.AddTargetGroup(std::move(target_group_));
+    file.Serialize(out_);
+    return deps;
   }
 
   void WriteStampOrPhonyForTarget(
       const std::vector<OutputFile>& deps,
       const std::vector<OutputFile>& order_only_deps) {
     NinjaTargetWriter::WriteStampOrPhonyForTarget(deps, order_only_deps);
+    NinjaFile file;
+    file.AddTargetGroup(std::move(target_group_));
+    file.Serialize(out_);
   }
 };
 
@@ -155,6 +162,7 @@
         "  description = ACTION //foo:action()\n"
         "  restat = 1\n"
         "\n"
+        "\n"
         "build: __foo_action___rule | ../../foo/script.py"
         " ../../foo/action_source.txt ./target\n"
         "\n"
@@ -255,6 +263,7 @@
         "  description = ACTION //foo:action()\n"
         "  restat = 1\n"
         "\n"
+        "\n"
         "build: __foo_action___rule | ../../foo/script.py"
         " ../../foo/action_source.txt ./target\n"
         "\n"
@@ -540,6 +549,7 @@
         "  description = ACTION //foo:b_pub()\n"
         "  restat = 1\n"
         "\n"
+        "\n"
         "build b_pub.out: __foo_b_pub___rule | ../../foo/script.py "
         "phony/foo/a\n"
         "\n"
@@ -558,6 +568,7 @@
         "  description = ACTION //foo:b_priv()\n"
         "  restat = 1\n"
         "\n"
+        "\n"
         "build b_priv.out: __foo_b_priv___rule | ../../foo/script.py "
         "phony/foo/a\n"
         "\n"
@@ -607,6 +618,7 @@
         "  description = ACTION //foo:b_data()\n"
         "  restat = 1\n"
         "\n"
+        "\n"
         "build b_data.out: __foo_b_data___rule | ../../foo/script.py || "
         "phony/foo/a\n"
         "\n"
@@ -625,6 +637,7 @@
         "  description = ACTION //foo:c_data()\n"
         "  restat = 1\n"
         "\n"
+        "\n"
         "build c_data.out: __foo_c_data___rule | ../../foo/script.py "
         "phony/foo/b_data\n"
         "\n"
@@ -675,9 +688,13 @@
   {
     std::ostringstream stream;
     ResolvedTargetData resolved;
+    NinjaTargetGroup group;
     TestingNinjaTargetWriter::WritePublicInputsStampOrPhony(&a, &resolved,
-                                                            stream);
-    EXPECT_EQ("build phony/foo/a.public_inputs: phony ../../foo/a.in\n\n",
+                                                            group);
+    NinjaFile file;
+    file.AddTargetGroup(std::move(group));
+    file.Serialize(stream);
+    EXPECT_EQ("build phony/foo/a.public_inputs: phony ../../foo/a.in\n",
               stream.str());
   }
 
@@ -739,9 +756,13 @@
   {
     std::ostringstream stream;
     ResolvedTargetData resolved;
+    NinjaTargetGroup group;
     TestingNinjaTargetWriter::WritePublicInputsStampOrPhony(&g, &resolved,
-                                                            stream);
-    EXPECT_EQ("build phony/foo/g.public_inputs: phony ../../foo/g.in\n\n",
+                                                            group);
+    NinjaFile file;
+    file.AddTargetGroup(std::move(group));
+    file.Serialize(stream);
+    EXPECT_EQ("build phony/foo/g.public_inputs: phony ../../foo/g.in\n",
               stream.str());
   }
 
diff --git a/src/gn/substitution_writer.cc b/src/gn/substitution_writer.cc
index b6d0c48..23ea92b 100644
--- a/src/gn/substitution_writer.cc
+++ b/src/gn/substitution_writer.cc
@@ -8,6 +8,7 @@
 #include "gn/c_substitution_type.h"
 #include "gn/escape.h"
 #include "gn/filesystem_utils.h"
+#include "gn/ninja_file.h"
 #include "gn/output_file.h"
 #include "gn/rust_substitution_type.h"
 #include "gn/rust_tool.h"
@@ -322,6 +323,31 @@
     const SourceFile& source,
     const std::vector<const Substitution*>& types,
     const EscapeOptions& escape_options,
+    std::vector<NinjaVariable>& edge_vars) {
+  for (const auto& type : types) {
+    // Don't write SOURCE since that just maps to Ninja's $in variable, which
+    // is implicit in the rule. RESPONSE_FILE_NAME is written separately
+    // only when writing target rules since it can never be used in any
+    // other context (like process_file_template).
+    if (type != &SubstitutionSource && type != &SubstitutionRspFileName) {
+      std::ostringstream val;
+      EscapeStringToStream(
+          val,
+          GetSourceSubstitution(target, settings, source, type, OUTPUT_RELATIVE,
+                                settings->build_settings()->build_dir()),
+          escape_options);
+      edge_vars.emplace_back(type->ninja_name, val.str());
+    }
+  }
+}
+
+// static
+void SubstitutionWriter::WriteNinjaVariablesForSource(
+    const Target* target,
+    const Settings* settings,
+    const SourceFile& source,
+    const std::vector<const Substitution*>& types,
+    const EscapeOptions& escape_options,
     std::ostream& out) {
   for (const auto& type : types) {
     // Don't write SOURCE since that just maps to Ninja's $in variable, which
diff --git a/src/gn/substitution_writer.h b/src/gn/substitution_writer.h
index 95e4b94..c7da823 100644
--- a/src/gn/substitution_writer.h
+++ b/src/gn/substitution_writer.h
@@ -9,6 +9,7 @@
 #include <string>
 #include <vector>
 
+#include "gn/ninja_file.h"
 #include "gn/substitution_type.h"
 
 struct EscapeOptions;
@@ -157,6 +158,13 @@
       const SourceFile& source,
       const std::vector<const Substitution*>& types,
       const EscapeOptions& escape_options,
+      std::vector<NinjaVariable>& edge_vars);
+  static void WriteNinjaVariablesForSource(
+      const Target* target,
+      const Settings* settings,
+      const SourceFile& source,
+      const std::vector<const Substitution*>& types,
+      const EscapeOptions& escape_options,
       std::ostream& out);
 
   // Extracts the given type of substitution related to a source file from the