Add an "outputs" command.

"gn outputs ..." is a new command-line tool that will list the outputs
for a target or source file. It is mostly intended to help users find
how to express a compile command that just compiles what they're working
on (often the output file to write is difficult to figure out).

Do some refactoring work to share this computation with
get_target_outputs and the "gn desc" code (both of which duplicated it
before).

Change-Id: I5b4a0ef30b106598900a96d9eee1ba516f7ac1f7
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/7740
Commit-Queue: Brett Wilson <brettw@chromium.org>
Reviewed-by: Scott Graham <scottmg@chromium.org>
diff --git a/build/gen.py b/build/gen.py
index bcd9f15..95d9b44 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -466,6 +466,7 @@
         'src/gn/command_help.cc',
         'src/gn/command_ls.cc',
         'src/gn/command_meta.cc',
+        'src/gn/command_outputs.cc',
         'src/gn/command_path.cc',
         'src/gn/command_refs.cc',
         'src/gn/commands.cc',
diff --git a/docs/reference.md b/docs/reference.md
index c158fa4..2cdff65 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -15,6 +15,7 @@
     *   [help: Does what you think.](#cmd_help)
     *   [ls: List matching targets.](#cmd_ls)
     *   [meta: List target metadata collection results.](#cmd_meta)
+    *   [outputs: Which files a source/target make.](#cmd_outputs)
     *   [path: Find paths between two targets.](#cmd_path)
     *   [refs: Find stuff referencing a target or file.](#cmd_refs)
 *   [Target declarations](#targets)
@@ -971,6 +972,60 @@
       target and all of its dependency tree, rebasing the strings in the `files`
       key onto the source directory of the target's declaration relative to "/".
 ```
+### <a name="cmd_outputs"></a>**gn outputs &lt;out_dir&gt; &lt;list of target or file names...&gt; ***
+
+```
+  Lists the output files corresponding to the given target(s) or file name(s).
+  There can be multiple outputs because there can be more than one output
+  generated by a build step, and there can be more than one toolchain matched.
+  You can also list multiple inputs which will generate a union of all the
+  outputs from those inputs.
+
+   - The input target/file names are relative to the current directory.
+
+   - The output file names are relative to the root build directory.
+
+   This command is useful for finding a ninja command that will build only a
+   portion of the build.
+```
+
+#### **Target outputs**
+
+```
+  If the parameter is a target name that includes a toolchain, it will match
+  only that target in that toolchain. If no toolchain is specified, it will
+  match all targets with that name in any toolchain.
+
+  The result will be the outputs specified by that target which could be a
+  library, executable, output of an action, a stamp file, etc.
+```
+
+#### **File outputs**
+
+```
+  If the parameter is a file name it will compute the output for that compile
+  step for all targets in all toolchains that contain that file as a source
+  file.
+
+  If the source is not compiled (e.g. a header or text file), the command will
+  produce no output.
+
+  If the source is listed as an "input" to a binary target or action will
+  resolve to that target's outputs.
+```
+
+#### **Example**
+
+```
+  gn outputs out/debug some/directory:some_target
+      Find the outputs of a given target.
+
+  gn outputs out/debug src/project/my_file.cc | xargs ninja -C out/debug
+      Compiles just the given source file in all toolchains it's referenced in.
+
+  git diff --name-only | xargs gn outputs out/x64 | xargs ninja -C out/x64
+      Compiles all files changed in git.
+```
 ### <a name="cmd_path"></a>**gn path &lt;out_dir&gt; &lt;target_one&gt; &lt;target_two&gt;**
 
 ```
diff --git a/src/gn/command_outputs.cc b/src/gn/command_outputs.cc
new file mode 100644
index 0000000..376405b
--- /dev/null
+++ b/src/gn/command_outputs.cc
@@ -0,0 +1,155 @@
+// Copyright 2020 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 <stddef.h>
+
+#include <algorithm>
+
+#include "base/command_line.h"
+#include "base/strings/stringprintf.h"
+#include "gn/commands.h"
+#include "gn/setup.h"
+#include "gn/standard_out.h"
+
+namespace commands {
+
+const char kOutputs[] = "outputs";
+const char kOutputs_HelpShort[] = "outputs: Which files a source/target make.";
+const char kOutputs_Help[] =
+    R"(gn outputs <out_dir> <list of target or file names...> *
+
+  Lists the output files corresponding to the given target(s) or file name(s).
+  There can be multiple outputs because there can be more than one output
+  generated by a build step, and there can be more than one toolchain matched.
+  You can also list multiple inputs which will generate a union of all the
+  outputs from those inputs.
+
+   - The input target/file names are relative to the current directory.
+
+   - The output file names are relative to the root build directory.
+
+   This command is useful for finding a ninja command that will build only a
+   portion of the build.
+
+Target outputs
+
+  If the parameter is a target name that includes a toolchain, it will match
+  only that target in that toolchain. If no toolchain is specified, it will
+  match all targets with that name in any toolchain.
+
+  The result will be the outputs specified by that target which could be a
+  library, executable, output of an action, a stamp file, etc.
+
+File outputs
+
+  If the parameter is a file name it will compute the output for that compile
+  step for all targets in all toolchains that contain that file as a source
+  file.
+
+  If the source is not compiled (e.g. a header or text file), the command will
+  produce no output.
+
+  If the source is listed as an "input" to a binary target or action will
+  resolve to that target's outputs.
+
+Example
+
+  gn outputs out/debug some/directory:some_target
+      Find the outputs of a given target.
+
+  gn outputs out/debug src/project/my_file.cc | xargs ninja -C out/debug
+      Compiles just the given source file in all toolchains it's referenced in.
+
+  git diff --name-only | xargs gn outputs out/x64 | xargs ninja -C out/x64
+      Compiles all files changed in git.
+)";
+
+int RunOutputs(const std::vector<std::string>& args) {
+  if (args.size() < 2) {
+    Err(Location(),
+        "Expected a build dir and one or more input files or targets.\n"
+        "Usage: \"gn outputs <out_dir> <target-or-file>*\"")
+        .PrintToStdout();
+    return 1;
+  }
+
+  // Deliberately leaked to avoid expensive process teardown.
+  Setup* setup = new Setup;
+  if (!setup->DoSetup(args[0], false))
+    return 1;
+  if (!setup->Run())
+    return 1;
+
+  std::vector<std::string> inputs(args.begin() + 1, args.end());
+
+  UniqueVector<const Target*> target_matches;
+  UniqueVector<const Config*> config_matches;
+  UniqueVector<const Toolchain*> toolchain_matches;
+  UniqueVector<SourceFile> file_matches;
+  if (!ResolveFromCommandLineInput(setup, inputs, true, &target_matches,
+                                   &config_matches, &toolchain_matches,
+                                   &file_matches))
+    return 1;
+
+  // We only care about targets and files.
+  if (target_matches.empty() && file_matches.empty()) {
+    Err(Location(), "The input matched no targets or files.").PrintToStdout();
+    return 1;
+  }
+
+  // Resulting outputs.
+  std::vector<OutputFile> outputs;
+
+  // Files. This must go first because it may add to the "targets" list.
+  std::vector<const Target*> all_targets =
+      setup->builder().GetAllResolvedTargets();
+  for (const SourceFile& file : file_matches) {
+    std::vector<TargetContainingFile> targets;
+    GetTargetsContainingFile(setup, all_targets, file, true, &targets);
+    if (targets.empty()) {
+      Err(Location(), base::StringPrintf("No targets reference the file '%s'.",
+                                         file.value().c_str()))
+          .PrintToStdout();
+      return 1;
+    }
+
+    // There can be more than one target that references this file, evaluate the
+    // output name in all of them.
+    for (const TargetContainingFile& pair : targets) {
+      if (pair.second == HowTargetContainsFile::kInputs) {
+        // Inputs maps to the target itself. This will be evaluated below.
+        target_matches.push_back(pair.first);
+      } else if (pair.second == HowTargetContainsFile::kSources) {
+        // Source file, check it.
+        const char* computed_tool = nullptr;
+        std::vector<OutputFile> file_outputs;
+        pair.first->GetOutputFilesForSource(file, &computed_tool,
+                                            &file_outputs);
+        outputs.insert(outputs.end(), file_outputs.begin(), file_outputs.end());
+      }
+    }
+  }
+
+  // Targets.
+  for (const Target* target : target_matches) {
+    std::vector<SourceFile> output_files;
+    Err err;
+    if (!target->GetOutputsAsSourceFiles(LocationRange(), true, &output_files,
+                                         &err)) {
+      err.PrintToStdout();
+      return 1;
+    }
+
+    // Convert to OutputFiles.
+    for (const SourceFile& file : output_files)
+      outputs.emplace_back(&setup->build_settings(), file);
+  }
+
+  // Print.
+  for (const OutputFile& output_file : outputs)
+    printf("%s\n", output_file.value().c_str());
+  return 0;
+}
+
+}  // namespace commands
diff --git a/src/gn/command_refs.cc b/src/gn/command_refs.cc
index 843a9ba..4f9ba05 100644
--- a/src/gn/command_refs.cc
+++ b/src/gn/command_refs.cc
@@ -129,63 +129,6 @@
     RecursiveCollectRefs(dep_map, cur_dep->second, results);
 }
 
-bool TargetContainsFile(const Target* target, const SourceFile& file) {
-  for (const auto& cur_file : target->sources()) {
-    if (cur_file == file)
-      return true;
-  }
-  for (const auto& cur_file : target->public_headers()) {
-    if (cur_file == file)
-      return true;
-  }
-  for (ConfigValuesIterator iter(target); !iter.done(); iter.Next()) {
-    for (const auto& cur_file : iter.cur().inputs()) {
-      if (cur_file == file)
-        return true;
-    }
-  }
-  for (const auto& cur_file : target->data()) {
-    if (cur_file == file.value())
-      return true;
-    if (cur_file.back() == '/' &&
-        base::StartsWith(file.value(), cur_file, base::CompareCase::SENSITIVE))
-      return true;
-  }
-
-  if (target->action_values().script().value() == file.value())
-    return true;
-
-  std::vector<SourceFile> output_sources;
-  target->action_values().GetOutputsAsSourceFiles(target, &output_sources);
-  for (const auto& cur_file : output_sources) {
-    if (cur_file == file)
-      return true;
-  }
-
-  for (const auto& cur_file : target->computed_outputs()) {
-    if (cur_file.AsSourceFile(target->settings()->build_settings()) == file)
-      return true;
-  }
-  return false;
-}
-
-void GetTargetsContainingFile(Setup* setup,
-                              const std::vector<const Target*>& all_targets,
-                              const SourceFile& file,
-                              bool all_toolchains,
-                              UniqueVector<const Target*>* matches) {
-  Label default_toolchain = setup->loader()->default_toolchain_label();
-  for (auto* target : all_targets) {
-    if (!all_toolchains) {
-      // Only check targets in the default toolchain.
-      if (target->label().GetToolchainLabel() != default_toolchain)
-        continue;
-    }
-    if (TargetContainsFile(target, file))
-      matches->push_back(target);
-  }
-}
-
 bool TargetReferencesConfig(const Target* target, const Config* config) {
   for (const LabelConfigPair& cur : target->configs()) {
     if (cur.ptr == config)
@@ -457,8 +400,13 @@
       setup->builder().GetAllResolvedTargets();
   UniqueVector<const Target*> explicit_target_matches;
   for (const auto& file : file_matches) {
+    std::vector<TargetContainingFile> target_containing;
     GetTargetsContainingFile(setup, all_targets, file, all_toolchains,
-                             &explicit_target_matches);
+                             &target_containing);
+
+    // Extract just the Target*.
+    for (const TargetContainingFile& pair : target_containing)
+      explicit_target_matches.push_back(pair.first);
   }
   for (auto* config : config_matches) {
     GetTargetsReferencingConfig(setup, all_targets, config, all_toolchains,
diff --git a/src/gn/commands.cc b/src/gn/commands.cc
index 6dcf16a..1941cba 100644
--- a/src/gn/commands.cc
+++ b/src/gn/commands.cc
@@ -4,11 +4,15 @@
 
 #include "gn/commands.h"
 
+#include <optional>
+
 #include "base/command_line.h"
 #include "base/environment.h"
 #include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
 #include "base/values.h"
 #include "gn/builder.h"
+#include "gn/config_values_extractors.h"
 #include "gn/filesystem_utils.h"
 #include "gn/item.h"
 #include "gn/label.h"
@@ -362,6 +366,48 @@
 }
 #endif
 
+std::optional<HowTargetContainsFile> TargetContainsFile(
+    const Target* target,
+    const SourceFile& file) {
+  for (const auto& cur_file : target->sources()) {
+    if (cur_file == file)
+      return HowTargetContainsFile::kSources;
+  }
+  for (const auto& cur_file : target->public_headers()) {
+    if (cur_file == file)
+      return HowTargetContainsFile::kPublic;
+  }
+  for (ConfigValuesIterator iter(target); !iter.done(); iter.Next()) {
+    for (const auto& cur_file : iter.cur().inputs()) {
+      if (cur_file == file)
+        return HowTargetContainsFile::kInputs;
+    }
+  }
+  for (const auto& cur_file : target->data()) {
+    if (cur_file == file.value())
+      return HowTargetContainsFile::kData;
+    if (cur_file.back() == '/' &&
+        base::StartsWith(file.value(), cur_file, base::CompareCase::SENSITIVE))
+      return HowTargetContainsFile::kData;
+  }
+
+  if (target->action_values().script().value() == file.value())
+    return HowTargetContainsFile::kScript;
+
+  std::vector<SourceFile> output_sources;
+  target->action_values().GetOutputsAsSourceFiles(target, &output_sources);
+  for (const auto& cur_file : output_sources) {
+    if (cur_file == file)
+      return HowTargetContainsFile::kOutput;
+  }
+
+  for (const auto& cur_file : target->computed_outputs()) {
+    if (cur_file.AsSourceFile(target->settings()->build_settings()) == file)
+      return HowTargetContainsFile::kOutput;
+  }
+  return std::nullopt;
+}
+
 }  // namespace
 
 CommandInfo::CommandInfo()
@@ -388,6 +434,7 @@
     INSERT_COMMAND(Help)
     INSERT_COMMAND(Meta)
     INSERT_COMMAND(Ls)
+    INSERT_COMMAND(Outputs)
     INSERT_COMMAND(Path)
     INSERT_COMMAND(Refs)
 
@@ -558,4 +605,21 @@
   FilterAndPrintTargets(&target_vector, out);
 }
 
+void GetTargetsContainingFile(Setup* setup,
+                              const std::vector<const Target*>& all_targets,
+                              const SourceFile& file,
+                              bool all_toolchains,
+                              std::vector<TargetContainingFile>* matches) {
+  Label default_toolchain = setup->loader()->default_toolchain_label();
+  for (auto* target : all_targets) {
+    if (!all_toolchains) {
+      // Only check targets in the default toolchain.
+      if (target->label().GetToolchainLabel() != default_toolchain)
+        continue;
+    }
+    if (auto how = TargetContainsFile(target, file))
+      matches->emplace_back(target, *how);
+  }
+}
+
 }  // namespace commands
diff --git a/src/gn/commands.h b/src/gn/commands.h
index d9b6862..a3e6e42 100644
--- a/src/gn/commands.h
+++ b/src/gn/commands.h
@@ -79,6 +79,11 @@
 extern const char kLs_Help[];
 int RunLs(const std::vector<std::string>& args);
 
+extern const char kOutputs[];
+extern const char kOutputs_HelpShort[];
+extern const char kOutputs_Help[];
+int RunOutputs(const std::vector<std::string>& args);
+
 extern const char kPath[];
 extern const char kPath_HelpShort[];
 extern const char kPath_Help[];
@@ -118,6 +123,9 @@
 // Resolves a vector of command line inputs and figures out the full set of
 // things they resolve to.
 //
+// On success, returns true and populates the vectors. On failure, prints the
+// error and returns false.
+//
 // Patterns with wildcards will only match targets. The file_matches aren't
 // validated that they are real files or referenced by any targets. They're just
 // the set of things that didn't match anything else.
@@ -204,6 +212,23 @@
 void FilterAndPrintTargetSet(const std::set<const Target*>& targets,
                              base::ListValue* out);
 
+// Computes which targets reference the given file and also stores how the
+// target references the file.
+enum class HowTargetContainsFile {
+  kSources,
+  kPublic,
+  kInputs,
+  kData,
+  kScript,
+  kOutput,
+};
+using TargetContainingFile = std::pair<const Target*, HowTargetContainsFile>;
+void GetTargetsContainingFile(Setup* setup,
+                              const std::vector<const Target*>& all_targets,
+                              const SourceFile& file,
+                              bool all_toolchains,
+                              std::vector<TargetContainingFile>* matches);
+
 // Extra help from command_check.cc
 extern const char kNoGnCheck_Help[];
 
diff --git a/src/gn/desc_builder.cc b/src/gn/desc_builder.cc
index f2a57e4..8ef77a6 100644
--- a/src/gn/desc_builder.cc
+++ b/src/gn/desc_builder.cc
@@ -61,14 +61,20 @@
 // }
 //
 // Optionally, if "what" is specified while generating description, two other
-// properties can be requested that are not included by default
+// properties can be requested that are not included by default. First the
+// runtime dependendencies (see "gn help runtime_deps"):
 //
-// "runtime_deps" : [list of computed runtime dependencies]
-// "source_outputs" : {
-//    "source_file x" : [ list of outputs for source file x ]
-//    "source_file y" : [ list of outputs for source file y ]
-//    ...
-// }
+//   "runtime_deps" : [list of computed runtime dependencies]
+//
+// Second, for targets whose sources map to outputs (binary targets,
+// action_foreach, and copies with non-constant outputs), the "source_outputs"
+// indicates the mapping from source to output file(s):
+//
+//   "source_outputs" : {
+//      "source_file x" : [ list of outputs for source file x ]
+//      "source_file y" : [ list of outputs for source file y ]
+//      ...
+//   }
 
 namespace {
 
@@ -684,6 +690,19 @@
   }
 
   void FillInSourceOutputs(base::DictionaryValue* res) {
+    // Only include "source outputs" if there are sources that map to outputs.
+    // Things like actions have constant per-target outputs that don't depend on
+    // the list of sources. These don't need source outputs.
+    if (target_->output_type() != Target::ACTION_FOREACH &&
+        target_->output_type() != Target::COPY_FILES && !target_->IsBinary())
+      return;  // Everything else has constant outputs.
+
+    // "copy" targets may have patterns or not. If there's only one file, the
+    // user can specify a constant output name.
+    if (target_->output_type() == Target::COPY_FILES &&
+        target_->action_values().outputs().required_types().empty())
+      return;  // Constant output.
+
     auto dict = std::make_unique<base::DictionaryValue>();
     for (const auto& source : target_->sources()) {
       std::vector<OutputFile> outputs;
@@ -728,53 +747,28 @@
   }
 
   void FillInOutputs(base::DictionaryValue* res) {
-    if (target_->output_type() == Target::ACTION) {
-      auto list = std::make_unique<base::ListValue>();
-      for (const auto& elem : target_->action_values().outputs().list())
-        list->AppendString(elem.AsString());
+    std::vector<SourceFile> output_files;
+    Err err;
+    if (!target_->GetOutputsAsSourceFiles(LocationRange(), true, &output_files,
+                                          &err)) {
+      err.PrintToStdout();
+      return;
+    }
+    res->SetWithoutPathExpansion(variables::kOutputs,
+                                 RenderValue(output_files));
 
-      res->SetWithoutPathExpansion(variables::kOutputs, std::move(list));
-    } else if (target_->output_type() == Target::CREATE_BUNDLE ||
-               target_->output_type() == Target::GENERATED_FILE) {
-      Err err;
-      std::vector<SourceFile> output_files;
-      if (!target_->bundle_data().GetOutputsAsSourceFiles(
-              target_->settings(), target_, &output_files, &err)) {
-        err.PrintToStdout();
-      }
-      res->SetWithoutPathExpansion(variables::kOutputs,
-                                   RenderValue(output_files));
-    } else if (target_->output_type() == Target::ACTION_FOREACH ||
-               target_->output_type() == Target::COPY_FILES) {
+    // Write some extra data for certain output types.
+    if (target_->output_type() == Target::ACTION_FOREACH ||
+        target_->output_type() == Target::COPY_FILES) {
       const SubstitutionList& outputs = target_->action_values().outputs();
       if (!outputs.required_types().empty()) {
+        // Write out the output patterns if there are any.
         auto patterns = std::make_unique<base::ListValue>();
         for (const auto& elem : outputs.list())
           patterns->AppendString(elem.AsString());
 
         res->SetWithoutPathExpansion("output_patterns", std::move(patterns));
       }
-      std::vector<SourceFile> output_files;
-      SubstitutionWriter::ApplyListToSources(target_, target_->settings(),
-                                             outputs, target_->sources(),
-                                             &output_files);
-      res->SetWithoutPathExpansion(variables::kOutputs,
-                                   RenderValue(output_files));
-    } else {
-      DCHECK(target_->IsBinary());
-      const Tool* tool =
-          target_->toolchain()->GetToolForTargetFinalOutput(target_);
-
-      std::vector<OutputFile> output_files;
-      SubstitutionWriter::ApplyListToLinkerAsOutputFile(
-          target_, tool, tool->outputs(), &output_files);
-      std::vector<SourceFile> output_files_as_source_file;
-      for (const OutputFile& output_file : output_files)
-        output_files_as_source_file.push_back(
-            output_file.AsSourceFile(target_->settings()->build_settings()));
-
-      res->SetWithoutPathExpansion(variables::kOutputs,
-                                   RenderValue(output_files_as_source_file));
     }
   }
 
diff --git a/src/gn/function_get_target_outputs.cc b/src/gn/function_get_target_outputs.cc
index 7995a49..fa851f3 100644
--- a/src/gn/function_get_target_outputs.cc
+++ b/src/gn/function_get_target_outputs.cc
@@ -113,21 +113,19 @@
     return Value();
   }
 
-  // Compute the output list.
+  // Range for GetOutputsAsSourceFiles to blame for errors.
+  LocationRange arg_range;
+  if (args[0].origin())
+    arg_range = args[0].origin()->GetRange();
+
   std::vector<SourceFile> files;
-  if (target->output_type() == Target::ACTION ||
-      target->output_type() == Target::COPY_FILES ||
-      target->output_type() == Target::ACTION_FOREACH ||
-      target->output_type() == Target::GENERATED_FILE) {
-    target->action_values().GetOutputsAsSourceFiles(target, &files);
-  } else {
-    // Other types of targets are not supported.
-    *err =
-        Err(args[0],
-            "Target is not an action, action_foreach, generated_file, or copy.",
-            "Only these target types are supported by get_target_outputs.");
+
+  // The build is currently running so only non-binary targets (they don't
+  // depend on the toolchain definition which may not have been loaded yet) can
+  // be queried. Pass false for build_complete so it will flag such queries as
+  // an error.
+  if (!target->GetOutputsAsSourceFiles(arg_range, false, &files, err))
     return Value();
-  }
 
   // Convert to Values.
   Value ret(function, Value::LIST);
diff --git a/src/gn/json_project_writer_unittest.cc b/src/gn/json_project_writer_unittest.cc
index 8a29425..f642ea6 100644
--- a/src/gn/json_project_writer_unittest.cc
+++ b/src/gn/json_project_writer_unittest.cc
@@ -2,12 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "base/strings/string_util.h"
 #include "gn/json_project_writer.h"
+#include "base/strings/string_util.h"
 #include "gn/substitution_list.h"
 #include "gn/target.h"
-#include "gn/test_with_scope.h"
 #include "gn/test_with_scheduler.h"
+#include "gn/test_with_scope.h"
 #include "util/build_config.h"
 #include "util/test/test.h"
 
@@ -41,14 +41,17 @@
   std::vector<const Target*> targets;
   targets.push_back(&target);
 #if defined(OS_WIN)
-  base::FilePath root_path = base::FilePath(FILE_PATH_LITERAL("c:/path/to/src"));
+  base::FilePath root_path =
+      base::FilePath(FILE_PATH_LITERAL("c:/path/to/src"));
 #else
   base::FilePath root_path = base::FilePath(FILE_PATH_LITERAL("/path/to/src"));
 #endif
   setup.build_settings()->SetRootPath(root_path);
   g_scheduler->AddGenDependency(root_path.Append(FILE_PATH_LITERAL(".gn")));
-  g_scheduler->AddGenDependency(root_path.Append(FILE_PATH_LITERAL("BUILD.gn")));
-  g_scheduler->AddGenDependency(root_path.Append(FILE_PATH_LITERAL("build/BUILD.gn")));
+  g_scheduler->AddGenDependency(
+      root_path.Append(FILE_PATH_LITERAL("BUILD.gn")));
+  g_scheduler->AddGenDependency(
+      root_path.Append(FILE_PATH_LITERAL("build/BUILD.gn")));
   std::string out =
       JSONProjectWriter::RenderJSON(setup.build_settings(), targets);
 #if defined(OS_WIN)
@@ -59,7 +62,8 @@
       "   \"build_settings\": {\n"
       "      \"build_dir\": \"//out/Debug/\",\n"
       "      \"default_toolchain\": \"//toolchain:default\",\n"
-      "      \"gen_input_files\": [ \"//.gn\", \"//BUILD.gn\", \"//build/BUILD.gn\" ],\n"
+      "      \"gen_input_files\": [ \"//.gn\", \"//BUILD.gn\", "
+      "\"//build/BUILD.gn\" ],\n"
 #if defined(OS_WIN)
       "      \"root_path\": \"c:/path/to/src\"\n"
 #else
@@ -86,7 +90,7 @@
       "      }\n"
       "   }\n"
       "}\n";
-  EXPECT_EQ(expected_json, out);
+  EXPECT_EQ(expected_json, out) << out;
 }
 
 TEST_F(JSONWriter, RustTarget) {
@@ -172,13 +176,15 @@
   std::vector<const Target*> targets;
   targets.push_back(&target);
 #if defined(OS_WIN)
-  base::FilePath root_path = base::FilePath(FILE_PATH_LITERAL("c:/path/to/src"));
+  base::FilePath root_path =
+      base::FilePath(FILE_PATH_LITERAL("c:/path/to/src"));
 #else
   base::FilePath root_path = base::FilePath(FILE_PATH_LITERAL("/path/to/src"));
 #endif
   setup.build_settings()->SetRootPath(root_path);
   g_scheduler->AddGenDependency(root_path.Append(FILE_PATH_LITERAL(".gn")));
-  g_scheduler->AddGenDependency(root_path.Append(FILE_PATH_LITERAL("BUILD.gn")));
+  g_scheduler->AddGenDependency(
+      root_path.Append(FILE_PATH_LITERAL("BUILD.gn")));
   std::string out =
       JSONProjectWriter::RenderJSON(setup.build_settings(), targets);
 #if defined(OS_WIN)
@@ -211,6 +217,9 @@
       "         \"response_file_contents\": [ \"-j\", \"{{source_name_part}}\" "
       "],\n"
       "         \"script\": \"//foo/script.py\",\n"
+      "         \"source_outputs\": {\n"
+      "            \"//foo/input1.txt\": [ \"input1.out\" ]\n"
+      "         },\n"
       "         \"sources\": [ \"//foo/input1.txt\" ],\n"
       "         \"testonly\": false,\n"
       "         \"toolchain\": \"\",\n"
diff --git a/src/gn/target.cc b/src/gn/target.cc
index 2469fba..93ad7c0 100644
--- a/src/gn/target.cc
+++ b/src/gn/target.cc
@@ -486,36 +486,115 @@
   return false;
 }
 
+bool Target::GetOutputsAsSourceFiles(const LocationRange& loc_for_error,
+                                     bool build_complete,
+                                     std::vector<SourceFile>* outputs,
+                                     Err* err) const {
+  const static char kBuildIncompleteMsg[] =
+      "This target is a binary target which can't be queried for its "
+      "outputs\nduring the build. It will work for action, action_foreach, "
+      "generated_file,\nand copy targets.";
+
+  outputs->clear();
+
+  std::vector<SourceFile> files;
+  if (output_type() == Target::ACTION || output_type() == Target::COPY_FILES ||
+      output_type() == Target::ACTION_FOREACH ||
+      output_type() == Target::GENERATED_FILE) {
+    action_values().GetOutputsAsSourceFiles(this, outputs);
+  } else if (output_type() == Target::CREATE_BUNDLE ||
+             output_type() == Target::GENERATED_FILE) {
+    if (!bundle_data().GetOutputsAsSourceFiles(settings(), this, outputs, err))
+      return false;
+  } else if (IsBinary() && output_type() != Target::SOURCE_SET) {
+    // Binary target with normal outputs (source sets have stamp outputs like
+    // groups).
+    DCHECK(IsBinary()) << static_cast<int>(output_type());
+    if (!build_complete) {
+      // Can't access the toolchain for a target before the build is complete.
+      // Otherwise it will race with loading and setting the toolchain
+      // definition.
+      *err = Err(loc_for_error, kBuildIncompleteMsg);
+      return false;
+    }
+
+    const Tool* tool = toolchain()->GetToolForTargetFinalOutput(this);
+
+    std::vector<OutputFile> output_files;
+    SubstitutionWriter::ApplyListToLinkerAsOutputFile(
+        this, tool, tool->outputs(), &output_files);
+    for (const OutputFile& output_file : output_files) {
+      outputs->push_back(
+          output_file.AsSourceFile(settings()->build_settings()));
+    }
+  } else {
+    // Everything else (like a group or something) has a stamp output. The
+    // dependency output file should have computed what this is. This won't be
+    // valid unless the build is complete.
+    if (!build_complete) {
+      *err = Err(loc_for_error, kBuildIncompleteMsg);
+      return false;
+    }
+    outputs->push_back(
+        dependency_output_file().AsSourceFile(settings()->build_settings()));
+  }
+  return true;
+}
+
 bool Target::GetOutputFilesForSource(const SourceFile& source,
                                      const char** computed_tool_type,
                                      std::vector<OutputFile>* outputs) const {
+  DCHECK(toolchain());  // Should be resolved before calling.
+
   outputs->clear();
   *computed_tool_type = Tool::kToolNone;
 
-  SourceFile::Type file_type = source.type();
-  if (file_type == SourceFile::SOURCE_UNKNOWN)
-    return false;
-  if (file_type == SourceFile::SOURCE_O) {
-    // Object files just get passed to the output and not compiled.
-    outputs->push_back(OutputFile(settings()->build_settings(), source));
-    return true;
+  if (output_type() == Target::COPY_FILES ||
+      output_type() == Target::ACTION_FOREACH) {
+    // These target types apply the output pattern to the input.
+    std::vector<SourceFile> output_files;
+    SubstitutionWriter::ApplyListToSourceAsOutputFile(
+        this, settings(), action_values().outputs(), source, outputs);
+  } else if (!IsBinary()) {
+    // All other non-binary target types just return the target outputs. We
+    // don't know if the build is complete and it doesn't matter for non-binary
+    // targets, so just assume it's not and pass "false".
+    std::vector<SourceFile> outputs_as_source_files;
+    Err err;  // We can ignore the error and return empty for failure.
+    GetOutputsAsSourceFiles(LocationRange(), false, &outputs_as_source_files,
+                            &err);
+
+    // Convert to output files.
+    for (const auto& cur : outputs_as_source_files)
+      outputs->emplace_back(OutputFile(settings()->build_settings(), cur));
+  } else {
+    // All binary targets do a tool lookup.
+    DCHECK(IsBinary());
+
+    SourceFile::Type file_type = source.type();
+    if (file_type == SourceFile::SOURCE_UNKNOWN)
+      return false;
+    if (file_type == SourceFile::SOURCE_O) {
+      // Object files just get passed to the output and not compiled.
+      outputs->emplace_back(OutputFile(settings()->build_settings(), source));
+      return true;
+    }
+
+    // Rust generates on a module level, not source.
+    if (file_type == SourceFile::SOURCE_RS)
+      return false;
+
+    *computed_tool_type = Tool::GetToolTypeForSourceType(file_type);
+    if (*computed_tool_type == Tool::kToolNone)
+      return false;  // No tool for this file (it's a header file or something).
+    const Tool* tool = toolchain_->GetTool(*computed_tool_type);
+    if (!tool)
+      return false;  // Tool does not apply for this toolchain.file.
+
+    // Figure out what output(s) this compiler produces.
+    SubstitutionWriter::ApplyListToCompilerAsOutputFile(
+        this, source, tool->outputs(), outputs);
   }
-
-  // Rust generates on a module level, not source.
-  if (file_type == SourceFile::SOURCE_RS) {
-    return false;
-  }
-
-  *computed_tool_type = Tool::GetToolTypeForSourceType(file_type);
-  if (*computed_tool_type == Tool::kToolNone)
-    return false;  // No tool for this file (it's a header file or something).
-  const Tool* tool = toolchain_->GetTool(*computed_tool_type);
-  if (!tool)
-    return false;  // Tool does not apply for this toolchain.file.
-
-  // Figure out what output(s) this compiler produces.
-  SubstitutionWriter::ApplyListToCompilerAsOutputFile(this, source,
-                                                      tool->outputs(), outputs);
   return !outputs->empty();
 }
 
diff --git a/src/gn/target.h b/src/gn/target.h
index 3c7ebf9..700859c 100644
--- a/src/gn/target.h
+++ b/src/gn/target.h
@@ -340,10 +340,41 @@
     return runtime_outputs_;
   }
 
+  // Computes and returns the outputs of this target expressed as SourceFiles.
+  //
+  // For binary target this depends on the tool for this target so the toolchain
+  // must have been loaded beforehand. This will happen asynchronously so
+  // calling this on a binary target before the build is complete will produce a
+  // race condition.
+  //
+  // To resolve this, the caller passes in whether the entire build is complete
+  // (this is used for the introspection commands which run after everything
+  // else).
+  //
+  // If the build is complete, the toolchain will be used for binary targets to
+  // compute the outputs. If the build is not complete, calling this function
+  // for binary targets will produce an error.
+  //
+  // The |loc_for_error| is used to blame a location for any errors produced. It
+  // can be empty if there is no range (like this is being called based on the
+  // command-line.
+  bool GetOutputsAsSourceFiles(const LocationRange& loc_for_error,
+                               bool build_complete,
+                               std::vector<SourceFile>* outputs,
+                               Err* err) const;
+
   // Computes the set of output files resulting from compiling the given source
-  // file. If the file can be compiled and the tool exists, fills the outputs
-  // in and writes the tool type to computed_tool_type. If the file is not
-  // compilable, returns false.
+  // file.
+  //
+  // For binary targets, if the file can be compiled and the tool exists, fills
+  // the outputs in and writes the tool type to computed_tool_type. If the file
+  // is not compilable, returns false.
+  //
+  // For action_foreach and copy targets, applies the output pattern to the
+  // given file name to compute the outputs.
+  //
+  // For all other target types, just returns the target outputs because such
+  // targets conceptually process all of their inputs as one step.
   //
   // The function can succeed with a "NONE" tool type for object files which
   // are just passed to the output. The output will always be overwritten, not
diff --git a/src/gn/target_unittest.cc b/src/gn/target_unittest.cc
index 3844d04..09d7a1a 100644
--- a/src/gn/target_unittest.cc
+++ b/src/gn/target_unittest.cc
@@ -719,6 +719,8 @@
 
 // Tests that runtime_outputs works without an explicit link_output for
 // solink tools.
+//
+// Also tests GetOutputsAsSourceFiles() for binaries (the setup is the same).
 TEST_F(TargetTest, RuntimeOuputs) {
   TestWithScope setup;
   Err err;
@@ -760,9 +762,146 @@
   ASSERT_EQ(2u, target.runtime_outputs().size());
   EXPECT_EQ("./a.dll", target.runtime_outputs()[0].value());
   EXPECT_EQ("./a.pdb", target.runtime_outputs()[1].value());
+
+  // Test GetOutputsAsSourceFiles().
+  std::vector<SourceFile> computed_outputs;
+  EXPECT_TRUE(target.GetOutputsAsSourceFiles(LocationRange(), true,
+                                             &computed_outputs, &err));
+  ASSERT_EQ(3u, computed_outputs.size());
+  EXPECT_EQ("//out/Debug/a.dll.lib", computed_outputs[0].value());
+  EXPECT_EQ("//out/Debug/a.dll", computed_outputs[1].value());
+  EXPECT_EQ("//out/Debug/a.pdb", computed_outputs[2].value());
 }
 
-// Shared libraries should be inherited across public shared liobrary
+// Tests Target::GetOutputFilesForSource for binary targets (these require a
+// tool definition). Also tests GetOutputsAsSourceFiles() for source sets.
+TEST_F(TargetTest, GetOutputFilesForSource_Binary) {
+  TestWithScope setup;
+
+  Toolchain toolchain(setup.settings(), Label(SourceDir("//tc/"), "tc"));
+
+  std::unique_ptr<Tool> tool = Tool::CreateTool(CTool::kCToolCxx);
+  CTool* cxx = tool->AsC();
+  cxx->set_outputs(SubstitutionList::MakeForTest("{{source_file_part}}.o"));
+  toolchain.SetTool(std::move(tool));
+
+  Target target(setup.settings(), Label(SourceDir("//a/"), "a"));
+  target.set_output_type(Target::SOURCE_SET);
+  target.SetToolchain(&toolchain);
+  Err err;
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  const char* computed_tool_type = nullptr;
+  std::vector<OutputFile> output;
+  bool result = target.GetOutputFilesForSource(SourceFile("//source/input.cc"),
+                                               &computed_tool_type, &output);
+  ASSERT_TRUE(result);
+  EXPECT_EQ(std::string("cxx"), std::string(computed_tool_type));
+
+  // Outputs are relative to the build directory "//out/Debug/".
+  ASSERT_EQ(1u, output.size());
+  EXPECT_EQ("input.cc.o", output[0].value()) << output[0].value();
+
+  // Test GetOutputsAsSourceFiles(). Since this is a source set it should give a
+  // stamp file.
+  std::vector<SourceFile> computed_outputs;
+  EXPECT_TRUE(target.GetOutputsAsSourceFiles(LocationRange(), true,
+                                             &computed_outputs, &err));
+  ASSERT_EQ(1u, computed_outputs.size());
+  EXPECT_EQ("//out/Debug/obj/a/a.stamp", computed_outputs[0].value());
+}
+
+// Tests Target::GetOutputFilesForSource for action_foreach targets (these, like
+// copy targets, apply a pattern to the source file). Also tests
+// GetOutputsAsSourceFiles() for action_foreach().
+TEST_F(TargetTest, GetOutputFilesForSource_ActionForEach) {
+  TestWithScope setup;
+
+  TestTarget target(setup, "//a:a", Target::ACTION_FOREACH);
+  target.sources().push_back(SourceFile("//a/source_file1.txt"));
+  target.sources().push_back(SourceFile("//a/source_file2.txt"));
+  target.action_values().outputs() =
+      SubstitutionList::MakeForTest("//out/Debug/{{source_file_part}}.one",
+                                    "//out/Debug/{{source_file_part}}.two");
+  Err err;
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  const char* computed_tool_type = nullptr;
+  std::vector<OutputFile> output;
+  bool result = target.GetOutputFilesForSource(SourceFile("//source/input.txt"),
+                                               &computed_tool_type, &output);
+  ASSERT_TRUE(result);
+
+  // Outputs are relative to the build directory "//out/Debug/".
+  ASSERT_EQ(2u, output.size());
+  EXPECT_EQ("input.txt.one", output[0].value());
+  EXPECT_EQ("input.txt.two", output[1].value());
+
+  // Test GetOutputsAsSourceFiles(). It should give both outputs for each of the
+  // two inputs.
+  std::vector<SourceFile> computed_outputs;
+  EXPECT_TRUE(target.GetOutputsAsSourceFiles(LocationRange(), true,
+                                             &computed_outputs, &err));
+  ASSERT_EQ(4u, computed_outputs.size());
+  EXPECT_EQ("//out/Debug/source_file1.txt.one", computed_outputs[0].value());
+  EXPECT_EQ("//out/Debug/source_file1.txt.two", computed_outputs[1].value());
+  EXPECT_EQ("//out/Debug/source_file2.txt.one", computed_outputs[2].value());
+  EXPECT_EQ("//out/Debug/source_file2.txt.two", computed_outputs[3].value());
+}
+
+// Tests Target::GetOutputFilesForSource for action targets (these just list the
+// output of the action as the result of all possible inputs). This should also
+// cover everything other than binary, action_foreach, and copy targets.
+TEST_F(TargetTest, GetOutputFilesForSource_Action) {
+  TestWithScope setup;
+
+  TestTarget target(setup, "//a:a", Target::ACTION);
+  target.sources().push_back(SourceFile("//a/source_file1.txt"));
+  target.action_values().outputs() =
+      SubstitutionList::MakeForTest("//out/Debug/one", "//out/Debug/two");
+  Err err;
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  const char* computed_tool_type = nullptr;
+  std::vector<OutputFile> output;
+  bool result = target.GetOutputFilesForSource(SourceFile("//source/input.txt"),
+                                               &computed_tool_type, &output);
+  ASSERT_TRUE(result);
+
+  // Outputs are relative to the build directory "//out/Debug/".
+  ASSERT_EQ(2u, output.size());
+  EXPECT_EQ("one", output[0].value());
+  EXPECT_EQ("two", output[1].value());
+
+  // Test GetOutputsAsSourceFiles(). It should give the listed outputs.
+  std::vector<SourceFile> computed_outputs;
+  EXPECT_TRUE(target.GetOutputsAsSourceFiles(LocationRange(), true,
+                                             &computed_outputs, &err));
+  ASSERT_EQ(2u, computed_outputs.size());
+  EXPECT_EQ("//out/Debug/one", computed_outputs[0].value());
+  EXPECT_EQ("//out/Debug/two", computed_outputs[1].value());
+
+  // Test that the copy target type behaves the same. This target requires only
+  // one output.
+  target.action_values().outputs() =
+      SubstitutionList::MakeForTest("//out/Debug/one");
+  target.set_output_type(Target::COPY_FILES);
+
+  // Outputs are relative to the build directory "//out/Debug/".
+  result = target.GetOutputFilesForSource(SourceFile("//source/input.txt"),
+                                          &computed_tool_type, &output);
+  ASSERT_TRUE(result);
+  ASSERT_EQ(1u, output.size());
+  EXPECT_EQ("one", output[0].value());
+
+  // Test GetOutputsAsSourceFiles() for the copy case.
+  EXPECT_TRUE(target.GetOutputsAsSourceFiles(LocationRange(), true,
+                                             &computed_outputs, &err));
+  ASSERT_EQ(1u, computed_outputs.size()) << computed_outputs.size();
+  EXPECT_EQ("//out/Debug/one", computed_outputs[0].value());
+}
+
+// Shared libraries should be inherited across public shared library
 // boundaries.
 TEST_F(TargetTest, SharedInheritance) {
   TestWithScope setup;
@@ -1234,8 +1373,7 @@
       std::pair<std::string_view, Value>("a", a_expected));
 
   Value walk_expected(nullptr, Value::LIST);
-  walk_expected.list_value().push_back(
-      Value(nullptr, "two"));
+  walk_expected.list_value().push_back(Value(nullptr, "two"));
   one.metadata().contents().insert(
       std::pair<std::string_view, Value>("walk", walk_expected));