Add NinjaOutputsWriter class

Implement a new class that can be used to generate a JSON
file containing an object mapping GN label strings to
list of corresponding Ninja output paths (including stamp
files).

This will be used to implement a new `gn gen` option.
The output can be used by GN clients to quickly convert
between GN labels and Ninja targets.

This is done through the following changes:

- NinjaTargetWriter gets new methods to write output
  files to the build plan, and record the
  corresponding OutputFile values (without Ninja
  escaping) separately if needed.

- All NinjaTargetWriter sub-classes are updated to
  call WriteOutput() or WriteOutputs() when adding
  Ninja output paths to the generated .ninja_files, instead
  of calling path_outputs_.WriteFile() directly.

Change-Id: Id6b1cc667e7bec56d37ededb75475fbc480eccaf
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/16761
Reviewed-by: Dirk Pranke <dpranke@google.com>
Commit-Queue: David Turner <digit@google.com>
diff --git a/build/gen.py b/build/gen.py
index 41d1571..e7037bb 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -706,6 +706,7 @@
         'src/gn/ninja_create_bundle_target_writer.cc',
         'src/gn/ninja_generated_file_target_writer.cc',
         'src/gn/ninja_group_target_writer.cc',
+        'src/gn/ninja_outputs_writer.cc',
         'src/gn/ninja_rust_binary_target_writer.cc',
         'src/gn/ninja_target_command_util.cc',
         'src/gn/ninja_target_writer.cc',
@@ -831,6 +832,7 @@
         'src/gn/ninja_create_bundle_target_writer_unittest.cc',
         'src/gn/ninja_generated_file_target_writer_unittest.cc',
         'src/gn/ninja_group_target_writer_unittest.cc',
+        'src/gn/ninja_outputs_writer_unittest.cc',
         'src/gn/ninja_rust_binary_target_writer_unittest.cc',
         'src/gn/ninja_target_command_util_unittest.cc',
         'src/gn/ninja_target_writer_unittest.cc',
diff --git a/src/gn/ninja_action_target_writer.cc b/src/gn/ninja_action_target_writer.cc
index d6d3aa8..06da244 100644
--- a/src/gn/ninja_action_target_writer.cc
+++ b/src/gn/ninja_action_target_writer.cc
@@ -87,7 +87,7 @@
     out_ << "build";
     SubstitutionWriter::GetListAsOutputFiles(
         settings_, target_->action_values().outputs(), &output_files);
-    path_output_.WriteFiles(out_, output_files);
+    WriteOutputs(output_files);
 
     out_ << ": " << custom_rule_name;
     if (!input_deps.empty()) {
@@ -275,7 +275,7 @@
 
   for (size_t i = first_output_index; i < output_files->size(); i++) {
     out_ << " ";
-    path_output_.WriteFile(out_, (*output_files)[i]);
+    WriteOutput((*output_files)[i]);
   }
 }
 
diff --git a/src/gn/ninja_binary_target_writer.cc b/src/gn/ninja_binary_target_writer.cc
index 3cfa584..62e01eb 100644
--- a/src/gn/ninja_binary_target_writer.cc
+++ b/src/gn/ninja_binary_target_writer.cc
@@ -44,12 +44,14 @@
   if (target_->source_types_used().RustSourceUsed()) {
     NinjaRustBinaryTargetWriter writer(target_, out_);
     writer.SetResolvedTargetData(GetResolvedTargetData());
+    writer.SetNinjaOutputs(ninja_outputs_);
     writer.Run();
     return;
   }
 
   NinjaCBinaryTargetWriter writer(target_, out_);
   writer.SetResolvedTargetData(GetResolvedTargetData());
+  writer.SetNinjaOutputs(ninja_outputs_);
   writer.Run();
 }
 
@@ -91,7 +93,8 @@
   stamp_file.value().append(".inputs.stamp");
 
   out_ << "build ";
-  path_output_.WriteFile(out_, stamp_file);
+  WriteOutput(stamp_file);
+
   out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
        << GeneralTool::kGeneralToolStamp;
 
@@ -263,7 +266,7 @@
     const std::vector<OutputFile>& outputs,
     bool can_write_source_info) {
   out_ << "build";
-  path_output_.WriteFiles(out_, outputs);
+  WriteOutputs(outputs);
 
   out_ << ": " << rule_prefix_ << tool_name;
   path_output_.WriteFiles(out_, sources);
diff --git a/src/gn/ninja_c_binary_target_writer.cc b/src/gn/ninja_c_binary_target_writer.cc
index f97c222..eada97d 100644
--- a/src/gn/ninja_c_binary_target_writer.cc
+++ b/src/gn/ninja_c_binary_target_writer.cc
@@ -612,7 +612,7 @@
       target_, tool_, tool_->outputs(), &output_files);
 
   out_ << "build";
-  path_output_.WriteFiles(out_, output_files);
+  WriteOutputs(output_files);
 
   out_ << ": " << rule_prefix_
        << Tool::GetToolTypeForTargetFinalOutput(target_);
diff --git a/src/gn/ninja_copy_target_writer.cc b/src/gn/ninja_copy_target_writer.cc
index 157b612..eca56d0 100644
--- a/src/gn/ninja_copy_target_writer.cc
+++ b/src/gn/ninja_copy_target_writer.cc
@@ -113,7 +113,8 @@
     output_files->push_back(output_file);
 
     out_ << "build ";
-    path_output_.WriteFile(out_, output_file);
+    WriteOutput(std::move(output_file));
+
     out_ << ": " << tool_name << " ";
     path_output_.WriteFile(out_, input_file);
     if (!input_deps.empty() || !data_outs.empty()) {
diff --git a/src/gn/ninja_create_bundle_target_writer.cc b/src/gn/ninja_create_bundle_target_writer.cc
index 23936ab..efa950e 100644
--- a/src/gn/ninja_create_bundle_target_writer.cc
+++ b/src/gn/ninja_create_bundle_target_writer.cc
@@ -97,10 +97,11 @@
   // 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 ";
-  path_output_.WriteFile(
-      out_,
+
+  WriteOutput(
       OutputFile(settings_->build_settings(),
                  target_->bundle_data().GetBundleRootDirOutput(settings_)));
+
   out_ << ": phony " << target_->dependency_output_file().value();
   out_ << std::endl;
 }
@@ -162,7 +163,7 @@
     output_files->push_back(expanded_output_file);
 
     out_ << "build ";
-    path_output_.WriteFile(out_, expanded_output_file);
+    WriteOutput(std::move(expanded_output_file));
     out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
          << GeneralTool::kGeneralToolCopyBundleData << " ";
     path_output_.WriteFile(out_, source_file);
@@ -206,7 +207,7 @@
     DCHECK(!target_->bundle_data().partial_info_plist().is_null());
 
     out_ << "build ";
-    path_output_.WriteFile(out_, partial_info_plist);
+    WriteOutput(partial_info_plist);
     out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
          << GeneralTool::kGeneralToolStamp;
     if (!order_only_deps.empty()) {
@@ -222,14 +223,14 @@
   DCHECK(!input_dep.value().empty());
 
   out_ << "build ";
-  path_output_.WriteFile(out_, compiled_catalog);
+  WriteOutput(std::move(compiled_catalog));
   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_ << " | ";
-    path_output_.WriteFile(out_, partial_info_plist);
+    WriteOutput(partial_info_plist);
   }
 
   out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
@@ -289,7 +290,7 @@
   xcassets_input_stamp_file.value().append(".xcassets.inputdeps.stamp");
 
   out_ << "build ";
-  path_output_.WriteFile(out_, xcassets_input_stamp_file);
+  WriteOutput(xcassets_input_stamp_file);
   out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
        << GeneralTool::kGeneralToolStamp;
 
@@ -317,7 +318,7 @@
   SubstitutionWriter::GetListAsOutputFiles(
       settings_, target_->bundle_data().code_signing_outputs(),
       &code_signing_output_files);
-  path_output_.WriteFiles(out_, code_signing_output_files);
+  WriteOutputs(code_signing_output_files);
 
   // Since the code signature step depends on all the files from the bundle,
   // the create_bundle stamp can just depends on the output of the signature
@@ -355,7 +356,7 @@
   code_signing_input_stamp_file.value().append(".codesigning.inputdeps.stamp");
 
   out_ << "build ";
-  path_output_.WriteFile(out_, code_signing_input_stamp_file);
+  WriteOutput(code_signing_input_stamp_file);
   out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
        << GeneralTool::kGeneralToolStamp;
 
diff --git a/src/gn/ninja_outputs_writer.cc b/src/gn/ninja_outputs_writer.cc
new file mode 100644
index 0000000..4f9309f
--- /dev/null
+++ b/src/gn/ninja_outputs_writer.cc
@@ -0,0 +1,157 @@
+// Copyright (c) 2023 The Chromium 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_outputs_writer.h"
+
+#include <algorithm>
+#include <memory>
+
+#include "base/command_line.h"
+#include "base/files/file_path.h"
+#include "base/json/string_escape.h"
+#include "gn/builder.h"
+#include "gn/commands.h"
+#include "gn/filesystem_utils.h"
+#include "gn/invoke_python.h"
+#include "gn/settings.h"
+#include "gn/string_output_buffer.h"
+
+// NOTE: Intentional macro definition allows compile-time string concatenation.
+// (see usage below).
+#if defined(OS_WINDOWS)
+#define LINE_ENDING "\r\n"
+#else
+#define LINE_ENDING "\n"
+#endif
+
+namespace {
+
+using MapType = NinjaOutputsWriter::MapType;
+
+// Sort the targets according to their human visible labels first.
+struct TargetLabelPair {
+  TargetLabelPair(const Target* target, const Label& default_toolchain_label)
+      : target(target),
+        label(std::make_unique<std::string>(
+            target->label().GetUserVisibleName(default_toolchain_label))) {}
+
+  const Target* target;
+  std::unique_ptr<std::string> label;
+
+  bool operator<(const TargetLabelPair& other) const {
+    return *label < *other.label;
+  }
+
+  using List = std::vector<TargetLabelPair>;
+
+  // Create list of TargetLabelPairs sorted by their target labels.
+  static List CreateSortedList(const MapType& outputs_map,
+                               const Label& default_toolchain_label) {
+    List result;
+    result.reserve(outputs_map.size());
+
+    for (const auto& output_pair : outputs_map)
+      result.emplace_back(output_pair.first, default_toolchain_label);
+
+    std::sort(result.begin(), result.end());
+    return result;
+  }
+};
+
+}  // namespace
+
+// static
+StringOutputBuffer NinjaOutputsWriter::GenerateJSON(
+    const MapType& outputs_map) {
+  Label default_toolchain_label;
+  if (!outputs_map.empty()) {
+    default_toolchain_label =
+        outputs_map.begin()->first->settings()->default_toolchain_label();
+  }
+
+  auto sorted_pairs =
+      TargetLabelPair::CreateSortedList(outputs_map, default_toolchain_label);
+
+  StringOutputBuffer out;
+  out.Append('{');
+
+  auto escape = [](std::string_view str) -> std::string {
+    std::string result;
+    base::EscapeJSONString(str, true, &result);
+    return result;
+  };
+
+  bool first_label = true;
+  for (const auto& pair : sorted_pairs) {
+    const Target* target = pair.target;
+    const std::string& label = *pair.label;
+
+    auto it = outputs_map.find(target);
+    CHECK(it != outputs_map.end());
+
+    if (!first_label)
+      out.Append(',');
+    first_label = false;
+
+    out.Append("\n  ");
+    out.Append(escape(label));
+    out.Append(": [");
+    bool first_path = true;
+    for (const auto& output : it->second) {
+      if (!first_path)
+        out.Append(',');
+      first_path = false;
+      out.Append("\n    ");
+      out.Append(escape(output.value()));
+    }
+    out.Append("\n  ]");
+  }
+
+  out.Append("\n}");
+  return out;
+}
+
+bool NinjaOutputsWriter::RunAndWriteFiles(
+    const MapType& outputs_map,
+    const BuildSettings* build_settings,
+    const std::string& file_name,
+    const std::string& exec_script,
+    const std::string& exec_script_extra_args,
+    bool quiet,
+    Err* err) {
+  SourceFile output_file = build_settings->build_dir().ResolveRelativeFile(
+      Value(nullptr, file_name), err);
+  if (output_file.is_null()) {
+    return false;
+  }
+
+  StringOutputBuffer outputs = GenerateJSON(outputs_map);
+
+  base::FilePath output_path = build_settings->GetFullPath(output_file);
+  if (!outputs.ContentsEqual(output_path)) {
+    if (!outputs.WriteToFile(output_path, err)) {
+      return false;
+    }
+
+    if (!exec_script.empty()) {
+      SourceFile script_file;
+      if (exec_script[0] != '/') {
+        // Relative path, assume the base is in build_dir.
+        script_file = build_settings->build_dir().ResolveRelativeFile(
+            Value(nullptr, exec_script), err);
+        if (script_file.is_null()) {
+          return false;
+        }
+      } else {
+        script_file = SourceFile(exec_script);
+      }
+      base::FilePath script_path = build_settings->GetFullPath(script_file);
+      return internal::InvokePython(build_settings, script_path,
+                                    exec_script_extra_args, output_path, quiet,
+                                    err);
+    }
+  }
+
+  return true;
+}
diff --git a/src/gn/ninja_outputs_writer.h b/src/gn/ninja_outputs_writer.h
new file mode 100644
index 0000000..c81a3ed
--- /dev/null
+++ b/src/gn/ninja_outputs_writer.h
@@ -0,0 +1,41 @@
+// Copyright (c) 2023 The Chromium 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_OUTPUTS_WRITER_H_
+#define TOOLS_GN_NINJA_OUTPUTS_WRITER_H_
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "gn/err.h"
+#include "gn/output_file.h"
+#include "gn/string_output_buffer.h"
+#include "gn/target.h"
+
+class Builder;
+class BuildSettings;
+class StringOutputBuffer;
+
+// Generates the --ninja-outputs-file content
+class NinjaOutputsWriter {
+ public:
+  // A map from targets to list of corresponding Ninja output paths.
+  using MapType = std::unordered_map<const Target*, std::vector<OutputFile>>;
+
+  static bool RunAndWriteFiles(const MapType& outputs_map,
+                               const BuildSettings* build_setting,
+                               const std::string& file_name,
+                               const std::string& exec_script,
+                               const std::string& exec_script_extra_args,
+                               bool quiet,
+                               Err* err);
+
+ private:
+  FRIEND_TEST_ALL_PREFIXES(NinjaOutputsWriterTest, OutputsFile);
+
+  static StringOutputBuffer GenerateJSON(const MapType& outputs_map);
+};
+
+#endif
diff --git a/src/gn/ninja_outputs_writer_unittest.cc b/src/gn/ninja_outputs_writer_unittest.cc
new file mode 100644
index 0000000..52354c6
--- /dev/null
+++ b/src/gn/ninja_outputs_writer_unittest.cc
@@ -0,0 +1,159 @@
+// Copyright 2024 The Chromium 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_outputs_writer.h"
+
+#include "base/command_line.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "gn/builder_record.h"
+#include "gn/filesystem_utils.h"
+#include "gn/ninja_target_writer.h"
+#include "gn/setup.h"
+#include "gn/switches.h"
+#include "gn/test_with_scheduler.h"
+#include "util/test/test.h"
+
+using NinjaOutputsWriterTest = TestWithScheduler;
+using NinjaOutputsMap = NinjaOutputsWriter::MapType;
+
+static void WriteFile(const base::FilePath& file, const std::string& data) {
+  CHECK_EQ(static_cast<int>(data.size()),  // Way smaller than INT_MAX.
+           base::WriteFile(file, data.data(), data.size()));
+}
+
+// Collects Ninja outputs for each target. Used by multiple background threads.
+struct TargetWriteInfo {
+  std::mutex lock;
+  NinjaOutputsMap ninja_outputs_map;
+};
+
+// Called on worker thread to write the ninja file.
+void BackgroundDoWrite(TargetWriteInfo* write_info, const Target* target) {
+  std::vector<OutputFile> target_ninja_outputs;
+  std::string rule = NinjaTargetWriter::RunAndWriteFile(target, nullptr,
+                                                        &target_ninja_outputs);
+
+  DCHECK(!rule.empty());
+
+  std::lock_guard<std::mutex> lock(write_info->lock);
+  write_info->ninja_outputs_map.emplace(target,
+                                        std::move(target_ninja_outputs));
+}
+
+static void ItemResolvedAndGeneratedCallback(TargetWriteInfo* write_info,
+                                             const BuilderRecord* record) {
+  const Item* item = record->item();
+  const Target* target = item->AsTarget();
+  if (target) {
+    g_scheduler->ScheduleWork(
+        [write_info, target]() { BackgroundDoWrite(write_info, target); });
+  }
+}
+
+TEST_F(NinjaOutputsWriterTest, OutputsFile) {
+  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);
+
+  const char kDotfileContents[] = R"(
+buildconfig = "//BUILDCONFIG.gn"
+)";
+
+  const char kBuildConfigContents[] = R"(
+set_default_toolchain("//toolchain:default")
+)";
+
+  const char kToolchainBuildContents[] = R"##(
+toolchain("default") {
+  tool("stamp") {
+    command = "stamp"
+  }
+}
+
+toolchain("secondary") {
+  tool("stamp") {
+    command = "stamp2"
+  }
+}
+)##";
+
+  const char kBuildGnContents[] = R"##(
+group("foo") {
+  deps = [ ":bar", ":zoo(//toolchain:secondary)" ]
+}
+
+action("bar") {
+  script = "//:run_bar_script.py"
+  outputs = [ "$root_build_dir/bar.output" ]
+  args = []
+}
+
+group("zoo") {
+}
+)##";
+
+  // Create a temp directory containing the build.
+  base::ScopedTempDir in_temp_dir;
+  ASSERT_TRUE(in_temp_dir.CreateUniqueTempDir());
+  base::FilePath in_path = in_temp_dir.GetPath();
+
+  WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILD.gn")), kBuildGnContents);
+  WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILDCONFIG.gn")),
+            kBuildConfigContents);
+  WriteFile(in_path.Append(FILE_PATH_LITERAL(".gn")), kDotfileContents);
+
+  EXPECT_TRUE(
+      base::CreateDirectory(in_path.Append(FILE_PATH_LITERAL("toolchain"))));
+
+  WriteFile(in_path.Append(FILE_PATH_LITERAL("toolchain/BUILD.gn")),
+            kToolchainBuildContents);
+
+  cmdline.AppendSwitch(switches::kRoot, FilePathToUTF8(in_path));
+
+  base::FilePath outputs_json_path(FILE_PATH_LITERAL("ninja_outputs.json"));
+  cmdline.AppendSwitch("--ninja-outputs-file",
+                       FilePathToUTF8(outputs_json_path));
+
+  // Create another temp dir for writing the generated files to.
+  base::ScopedTempDir build_temp_dir;
+  ASSERT_TRUE(build_temp_dir.CreateUniqueTempDir());
+
+  // Run setup
+  Setup setup;
+  EXPECT_TRUE(
+      setup.DoSetup(FilePathToUTF8(build_temp_dir.GetPath()), true, cmdline));
+
+  TargetWriteInfo write_info;
+
+  setup.builder().set_resolved_and_generated_callback(
+      [&write_info](const BuilderRecord* record) {
+        ItemResolvedAndGeneratedCallback(&write_info, record);
+      });
+
+  // Do the actual load.
+  ASSERT_TRUE(setup.Run());
+
+  StringOutputBuffer out =
+      NinjaOutputsWriter::GenerateJSON(write_info.ninja_outputs_map);
+
+  // Verify that the generated file is here.
+  std::string generated = out.str();
+  std::string expected = R"##({
+  "//:bar": [
+    "bar.output",
+    "obj/bar.stamp"
+  ],
+  "//:foo": [
+    "obj/foo.stamp"
+  ],
+  "//:zoo": [
+    "obj/zoo.stamp"
+  ],
+  "//:zoo(//toolchain:secondary)": [
+    "secondary/obj/zoo.stamp"
+  ]
+})##";
+
+  EXPECT_EQ(generated, expected);
+}
diff --git a/src/gn/ninja_target_writer.cc b/src/gn/ninja_target_writer.cc
index 6ecbaa7..b8b156f 100644
--- a/src/gn/ninja_target_writer.cc
+++ b/src/gn/ninja_target_writer.cc
@@ -47,6 +47,11 @@
   }
 }
 
+void NinjaTargetWriter::SetNinjaOutputs(
+    std::vector<OutputFile>* ninja_outputs) {
+  ninja_outputs_ = ninja_outputs;
+}
+
 ResolvedTargetData* NinjaTargetWriter::GetResolvedTargetData() {
   return const_cast<ResolvedTargetData*>(&resolved());
 }
@@ -61,9 +66,39 @@
 
 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,
-                                               ResolvedTargetData* resolved) {
+std::string NinjaTargetWriter::RunAndWriteFile(
+    const Target* target,
+    ResolvedTargetData* resolved,
+    std::vector<OutputFile>* ninja_outputs) {
   const Settings* settings = target->settings();
 
   ScopedTrace trace(TraceItem::TRACE_FILE_WRITE_NINJA,
@@ -97,32 +132,39 @@
   if (target->output_type() == Target::BUNDLE_DATA) {
     NinjaBundleDataTargetWriter writer(target, rules);
     writer.SetResolvedTargetData(resolved);
+    writer.SetNinjaOutputs(ninja_outputs);
     writer.Run();
   } else if (target->output_type() == Target::CREATE_BUNDLE) {
     NinjaCreateBundleTargetWriter writer(target, rules);
     writer.SetResolvedTargetData(resolved);
+    writer.SetNinjaOutputs(ninja_outputs);
     writer.Run();
   } else if (target->output_type() == Target::COPY_FILES) {
     NinjaCopyTargetWriter writer(target, rules);
     writer.SetResolvedTargetData(resolved);
+    writer.SetNinjaOutputs(ninja_outputs);
     writer.Run();
   } else if (target->output_type() == Target::ACTION ||
              target->output_type() == Target::ACTION_FOREACH) {
     NinjaActionTargetWriter writer(target, rules);
     writer.SetResolvedTargetData(resolved);
+    writer.SetNinjaOutputs(ninja_outputs);
     writer.Run();
   } else if (target->output_type() == Target::GROUP) {
     NinjaGroupTargetWriter writer(target, rules);
     writer.SetResolvedTargetData(resolved);
+    writer.SetNinjaOutputs(ninja_outputs);
     writer.Run();
   } else if (target->output_type() == Target::GENERATED_FILE) {
     NinjaGeneratedFileTargetWriter writer(target, rules);
     writer.SetResolvedTargetData(resolved);
+    writer.SetNinjaOutputs(ninja_outputs);
     writer.Run();
   } else if (target->IsBinary()) {
     needs_file_write = true;
     NinjaBinaryTargetWriter writer(target, rules);
     writer.SetResolvedTargetData(resolved);
+    writer.SetNinjaOutputs(ninja_outputs);
     writer.Run();
   } else {
     CHECK(0) << "Output type of target not handled.";
@@ -508,7 +550,8 @@
   input_stamp_file.value().append(".inputdeps.stamp");
 
   out_ << "build ";
-  path_output_.WriteFile(out_, input_stamp_file);
+  WriteOutput(input_stamp_file);
+
   out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
        << GeneralTool::kGeneralToolStamp;
   path_output_.WriteFiles(out_, outs);
@@ -529,7 +572,7 @@
       << "\"" << stamp_file.value() << "\"";
 
   out_ << "build ";
-  path_output_.WriteFile(out_, stamp_file);
+  WriteOutput(std::move(stamp_file));
 
   out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
        << GeneralTool::kGeneralToolStamp;
diff --git a/src/gn/ninja_target_writer.h b/src/gn/ninja_target_writer.h
index 74378da..9655c72 100644
--- a/src/gn/ninja_target_writer.h
+++ b/src/gn/ninja_target_writer.h
@@ -34,6 +34,11 @@
   // instances to share the same cached information.
   void SetResolvedTargetData(ResolvedTargetData* resolved);
 
+  // Set the vector that will receive the Ninja output file paths generated
+  // by this writer. A nullptr value means no output files needs to be
+  // collected.
+  void SetNinjaOutputs(std::vector<OutputFile>* ninja_outputs);
+
   // Returns the build line to be written to the toolchain build file.
   //
   // Some targets have their rules written to separate files, and some can have
@@ -41,8 +46,13 @@
   // function will return the rules as a string. For the separate file case,
   // the separate ninja file will be written and the return string will be the
   // subninja command to load that file.
-  static std::string RunAndWriteFile(const Target* target,
-                                     ResolvedTargetData* resolved = nullptr);
+  //
+  // If |ninja_outputs| is not nullptr, it will be set with the list of
+  // Ninja output paths generated by the corresponding writer.
+  static std::string RunAndWriteFile(
+      const Target* target,
+      ResolvedTargetData* resolved = nullptr,
+      std::vector<OutputFile>* ninja_outputs = nullptr);
 
   virtual void Run() = 0;
 
@@ -94,6 +104,21 @@
   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
+  // be const.
+  mutable std::vector<OutputFile>* ninja_outputs_ = nullptr;
+
   // The ResolvedTargetData instance can be set through SetResolvedTargetData()
   // or it will be created lazily when resolved() is called, hence the need
   // for 'mutable' here.