[gn] Add --export-compile-commands switch to gen

Adding a command line switch to generate a compilation database
(compile_commands.json) in the out/ directory. Compilation databases
are used by clang-based tools (e.g. clang-tidy, clangd, clang-rename).
CMake already does this with the -DEXPORT_COMPILE_COMMANDS option,
which regenerates the compdb whenever the ninja files are regenerated.

Use:
gn gen out/<builddir> --export-compile-commands

Setting this switch adds ~900ms to the `gn gen` command when run
on Chromium (wihtout --export-compile-commands == 2.7sec,
with --export-compile-commands == 3.6sec).

Change-Id: Ia3738e340b9de60da62557fb431a68a0618957fb
Reviewed-on: https://gn-review.googlesource.com/2040
Commit-Queue: Brett Wilson <brettw@chromium.org>
Reviewed-by: Brett Wilson <brettw@chromium.org>
diff --git a/build/gen.py b/build/gen.py
index 6fb6dfc..eceeafe 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -375,6 +375,7 @@
         'tools/gn/command_path.cc',
         'tools/gn/command_refs.cc',
         'tools/gn/commands.cc',
+        'tools/gn/compile_commands_writer.cc',
         'tools/gn/config.cc',
         'tools/gn/config_values.cc',
         'tools/gn/config_values_extractors.cc',
@@ -425,6 +426,7 @@
         'tools/gn/ninja_copy_target_writer.cc',
         'tools/gn/ninja_create_bundle_target_writer.cc',
         'tools/gn/ninja_group_target_writer.cc',
+        'tools/gn/ninja_target_command_util.cc',
         'tools/gn/ninja_target_writer.cc',
         'tools/gn/ninja_toolchain_writer.cc',
         'tools/gn/ninja_utils.cc',
diff --git a/tools/gn/command_gen.cc b/tools/gn/command_gen.cc
index eb1b4d2..d959fe4 100644
--- a/tools/gn/command_gen.cc
+++ b/tools/gn/command_gen.cc
@@ -11,6 +11,7 @@
 #include "base/timer/elapsed_timer.h"
 #include "tools/gn/build_settings.h"
 #include "tools/gn/commands.h"
+#include "tools/gn/compile_commands_writer.h"
 #include "tools/gn/eclipse_writer.h"
 #include "tools/gn/json_project_writer.h"
 #include "tools/gn/ninja_target_writer.h"
@@ -49,6 +50,7 @@
 const char kSwitchJsonFileName[] = "json-file-name";
 const char kSwitchJsonIdeScript[] = "json-ide-script";
 const char kSwitchJsonIdeScriptArgs[] = "json-ide-script-args";
+const char kSwitchExportCompileCommands[] = "export-compile-commands";
 
 // Collects Ninja rules for each toolchain. The lock protectes the rules.
 struct TargetWriteInfo {
@@ -279,6 +281,26 @@
   return false;
 }
 
+bool RunCompileCommandsWriter(const BuildSettings* build_settings,
+                              const Builder& builder,
+                              Err* err) {
+  const base::CommandLine* command_line =
+      base::CommandLine::ForCurrentProcess();
+  bool quiet = command_line->HasSwitch(switches::kQuiet);
+  base::ElapsedTimer timer;
+
+  std::string file_name = "compile_commands.json";
+
+  bool res = CompileCommandsWriter::RunAndWriteFiles(build_settings, builder,
+                                                     file_name, quiet, err);
+  if (res && !quiet) {
+    OutputString("Generating compile_commands took " +
+                 base::Int64ToString(timer.Elapsed().InMilliseconds()) +
+                 "ms\n");
+  }
+  return res;
+}
+
 }  // namespace
 
 const char kGen[] = "gen";
@@ -455,6 +477,13 @@
     return 1;
   }
 
+  if (command_line->HasSwitch(kSwitchExportCompileCommands) &&
+      !RunCompileCommandsWriter(&setup->build_settings(), setup->builder(),
+                                &err)) {
+    err.PrintToStdout();
+    return 1;
+  }
+
   TickDelta elapsed_time = timer.Elapsed();
 
   if (!command_line->HasSwitch(switches::kQuiet)) {
diff --git a/tools/gn/compile_commands_writer.cc b/tools/gn/compile_commands_writer.cc
new file mode 100644
index 0000000..772d8d2
--- /dev/null
+++ b/tools/gn/compile_commands_writer.cc
@@ -0,0 +1,284 @@
+// Copyright 2018 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 "tools/gn/compile_commands_writer.h"
+
+#include <sstream>
+
+#include "base/strings/stringprintf.h"
+#include "tools/gn/builder.h"
+#include "tools/gn/config_values_extractors.h"
+#include "tools/gn/escape.h"
+#include "tools/gn/filesystem_utils.h"
+#include "tools/gn/ninja_target_command_util.h"
+#include "tools/gn/path_output.h"
+#include "tools/gn/substitution_writer.h"
+
+// Structure of JSON output file
+// [
+//   {
+//      "directory": "The build directory."
+//      "file": "The main source file processed by this compilation step.
+//               Must be absolute or relative to the above build directory."
+//      "command": "The compile command executed."
+//   }
+//   ...
+// ]
+
+namespace {
+
+#if defined(OS_WIN)
+const char kPrettyPrintLineEnding[] = "\r\n";
+#else
+const char kPrettyPrintLineEnding[] = "\n";
+#endif
+
+struct CompileFlags {
+  std::string includes;
+  std::string defines;
+  std::string cflags;
+  std::string cflags_c;
+  std::string cflags_cc;
+  std::string cflags_objc;
+  std::string cflags_objcc;
+};
+
+void SetupCompileFlags(const Target* target,
+                       PathOutput& path_output,
+                       EscapeOptions opts,
+                       CompileFlags& flags) {
+  bool has_precompiled_headers =
+      target->config_values().has_precompiled_headers();
+
+  std::ostringstream defines_out;
+  RecursiveTargetConfigToStream<std::string>(
+      target, &ConfigValues::defines,
+      DefineWriter(ESCAPE_NINJA_PREFORMATTED_COMMAND, true), defines_out);
+  flags.defines = defines_out.str();
+
+  std::ostringstream includes_out;
+  RecursiveTargetConfigToStream<SourceDir>(target, &ConfigValues::include_dirs,
+                                           IncludeWriter(path_output),
+                                           includes_out);
+  flags.includes = includes_out.str();
+
+  std::ostringstream cflags_out;
+  WriteOneFlag(target, SUBSTITUTION_CFLAGS, false, Toolchain::TYPE_NONE,
+               &ConfigValues::cflags, opts, path_output, cflags_out,
+               /*write_substitution=*/false);
+  flags.cflags = cflags_out.str();
+
+  std::ostringstream cflags_c_out;
+  WriteOneFlag(target, SUBSTITUTION_CFLAGS_C, has_precompiled_headers,
+               Toolchain::TYPE_CC, &ConfigValues::cflags_c, opts, path_output,
+               cflags_c_out, /*write_substitution=*/false);
+  flags.cflags_c = cflags_c_out.str();
+
+  std::ostringstream cflags_cc_out;
+  WriteOneFlag(target, SUBSTITUTION_CFLAGS_CC, has_precompiled_headers,
+               Toolchain::TYPE_CXX, &ConfigValues::cflags_cc, opts, path_output,
+               cflags_cc_out, /*write_substitution=*/false);
+  flags.cflags_cc = cflags_cc_out.str();
+
+  std::ostringstream cflags_objc_out;
+  WriteOneFlag(target, SUBSTITUTION_CFLAGS_OBJC, has_precompiled_headers,
+               Toolchain::TYPE_OBJC, &ConfigValues::cflags_objc, opts,
+               path_output, cflags_objc_out, /*write_substitution=*/false);
+  flags.cflags_objc = cflags_objc_out.str();
+
+  std::ostringstream cflags_objcc_out;
+  WriteOneFlag(target, SUBSTITUTION_CFLAGS_OBJCC, has_precompiled_headers,
+               Toolchain::TYPE_OBJCXX, &ConfigValues::cflags_objcc, opts,
+               path_output, cflags_objcc_out, /*write_substitution=*/false);
+  flags.cflags_objcc = cflags_objcc_out.str();
+}
+
+void WriteFile(const SourceFile& source,
+               PathOutput& path_output,
+               std::string* compile_commands) {
+  std::ostringstream rel_source_path;
+  path_output.WriteFile(rel_source_path, source);
+  compile_commands->append("    \"file\": \"");
+  compile_commands->append(rel_source_path.str());
+}
+
+void WriteDirectory(std::string build_dir, std::string* compile_commands) {
+  compile_commands->append("\",");
+  compile_commands->append(kPrettyPrintLineEnding);
+  compile_commands->append("    \"directory\": \"");
+  compile_commands->append(build_dir);
+  compile_commands->append("\",");
+}
+
+void WriteCommand(const Target* target,
+                  const SourceFile& source,
+                  const CompileFlags& flags,
+                  std::vector<OutputFile>& tool_outputs,
+                  PathOutput& path_output,
+                  SourceFileType source_type,
+                  Toolchain::ToolType tool_type,
+                  EscapeOptions opts,
+                  std::string* compile_commands) {
+  EscapeOptions no_quoting(opts);
+  no_quoting.inhibit_quoting = true;
+  const Tool* tool = target->toolchain()->GetTool(tool_type);
+  std::ostringstream command_out;
+
+  for (const auto& range : tool->command().ranges()) {
+    // TODO: this is emitting a bonus space prior to each substitution.
+    switch (range.type) {
+      case SUBSTITUTION_LITERAL:
+        EscapeStringToStream(command_out, range.literal, no_quoting);
+        break;
+      case SUBSTITUTION_OUTPUT:
+        path_output.WriteFiles(command_out, tool_outputs);
+        break;
+      case SUBSTITUTION_DEFINES:
+        command_out << flags.defines;
+        break;
+      case SUBSTITUTION_INCLUDE_DIRS:
+        command_out << flags.includes;
+        break;
+      case SUBSTITUTION_CFLAGS:
+        command_out << flags.cflags;
+        break;
+      case SUBSTITUTION_CFLAGS_C:
+        if (source_type == SOURCE_C)
+          command_out << flags.cflags_c;
+        break;
+      case SUBSTITUTION_CFLAGS_CC:
+        if (source_type == SOURCE_CPP)
+          command_out << flags.cflags_cc;
+        break;
+      case SUBSTITUTION_CFLAGS_OBJC:
+        if (source_type == SOURCE_M)
+          command_out << flags.cflags_objc;
+        break;
+      case SUBSTITUTION_CFLAGS_OBJCC:
+        if (source_type == SOURCE_MM)
+          command_out << flags.cflags_objcc;
+        break;
+      case SUBSTITUTION_LABEL:
+      case SUBSTITUTION_LABEL_NAME:
+      case SUBSTITUTION_ROOT_GEN_DIR:
+      case SUBSTITUTION_ROOT_OUT_DIR:
+      case SUBSTITUTION_TARGET_GEN_DIR:
+      case SUBSTITUTION_TARGET_OUT_DIR:
+      case SUBSTITUTION_TARGET_OUTPUT_NAME:
+      case SUBSTITUTION_SOURCE:
+      case SUBSTITUTION_SOURCE_NAME_PART:
+      case SUBSTITUTION_SOURCE_FILE_PART:
+      case SUBSTITUTION_SOURCE_DIR:
+      case SUBSTITUTION_SOURCE_ROOT_RELATIVE_DIR:
+      case SUBSTITUTION_SOURCE_GEN_DIR:
+      case SUBSTITUTION_SOURCE_OUT_DIR:
+      case SUBSTITUTION_SOURCE_TARGET_RELATIVE:
+        EscapeStringToStream(command_out,
+                             SubstitutionWriter::GetCompilerSubstitution(
+                                 target, source, range.type),
+                             opts);
+        break;
+
+      // Other flags shouldn't be relevant to compiling C/C++/ObjC/ObjC++
+      // source files.
+      default:
+        NOTREACHED() << "Unsupported substitution for this type of target : "
+                     << kSubstitutionNames[range.type];
+        continue;
+    }
+  }
+  compile_commands->append(kPrettyPrintLineEnding);
+  compile_commands->append("    \"command\": \"");
+  compile_commands->append(command_out.str());
+}
+
+void RenderJSON(const BuildSettings* build_settings,
+                const Builder& builder,
+                std::vector<const Target*>& all_targets,
+                std::string* compile_commands) {
+  // TODO: Determine out an appropriate size to reserve.
+  compile_commands->reserve(all_targets.size() * 100);
+  compile_commands->append("[");
+  compile_commands->append(kPrettyPrintLineEnding);
+  bool first = true;
+  auto build_dir = build_settings->GetFullPath(build_settings->build_dir());
+  std::vector<OutputFile> tool_outputs;  // Prevent reallocation in loop.
+
+  EscapeOptions opts;
+  opts.mode = ESCAPE_NINJA_PREFORMATTED_COMMAND;
+
+  for (const auto* target : all_targets) {
+    if (!target->IsBinary())
+      continue;
+
+    // Precompute values that are the same for all sources in a target to avoid
+    // computing for every source.
+
+    PathOutput path_output(
+        target->settings()->build_settings()->build_dir(),
+        target->settings()->build_settings()->root_path_utf8(),
+        ESCAPE_NINJA_COMMAND);
+
+    CompileFlags flags;
+    SetupCompileFlags(target, path_output, opts, flags);
+
+    for (const auto& source : target->sources()) {
+      // If this source is not a C/C++/ObjC/ObjC++ source (not header) file,
+      // continue as it does not belong in the compilation database.
+      SourceFileType source_type = GetSourceFileType(source);
+      if (source_type != SOURCE_CPP && source_type != SOURCE_C &&
+          source_type != SOURCE_M && source_type != SOURCE_MM)
+        continue;
+
+      Toolchain::ToolType tool_type = Toolchain::TYPE_NONE;
+      if (!target->GetOutputFilesForSource(source, &tool_type, &tool_outputs))
+        continue;
+
+      if (!first) {
+        compile_commands->append(",");
+        compile_commands->append(kPrettyPrintLineEnding);
+      }
+      first = false;
+      compile_commands->append("  {");
+      compile_commands->append(kPrettyPrintLineEnding);
+
+      WriteFile(source, path_output, compile_commands);
+      WriteDirectory(base::StringPrintf("%" PRIsFP, build_dir.value().c_str()),
+                     compile_commands);
+      WriteCommand(target, source, flags, tool_outputs, path_output,
+                   source_type, tool_type, opts, compile_commands);
+      compile_commands->append("\"");
+      compile_commands->append(kPrettyPrintLineEnding);
+      compile_commands->append("  }");
+    }
+  }
+
+  compile_commands->append(kPrettyPrintLineEnding);
+  compile_commands->append("]");
+  compile_commands->append(kPrettyPrintLineEnding);
+}
+
+}  // namespace
+
+bool CompileCommandsWriter::RunAndWriteFiles(
+    const BuildSettings* build_settings,
+    const Builder& builder,
+    const std::string& file_name,
+    bool quiet,
+    Err* err) {
+  SourceFile output_file = build_settings->build_dir().ResolveRelativeFile(
+      Value(nullptr, file_name), err);
+  if (output_file.is_null())
+    return false;
+
+  base::FilePath output_path = build_settings->GetFullPath(output_file);
+
+  std::vector<const Target*> all_targets = builder.GetAllResolvedTargets();
+
+  std::string json;
+  RenderJSON(build_settings, builder, all_targets, &json);
+  if (!WriteFileIfChanged(output_path, json, err))
+    return false;
+  return true;
+}
diff --git a/tools/gn/compile_commands_writer.h b/tools/gn/compile_commands_writer.h
new file mode 100644
index 0000000..ad98dc2
--- /dev/null
+++ b/tools/gn/compile_commands_writer.h
@@ -0,0 +1,23 @@
+// Copyright 2018 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_COMPILE_COMMANDS_WRITER_H_
+#define TOOLS_GN_COMPILE_COMMANDS_WRITER_H_
+
+#include "tools/gn/err.h"
+#include "tools/gn/target.h"
+
+class Builder;
+class BuildSettings;
+
+class CompileCommandsWriter {
+ public:
+  static bool RunAndWriteFiles(const BuildSettings* build_setting,
+                               const Builder& builder,
+                               const std::string& file_name,
+                               bool quiet,
+                               Err* err);
+};
+
+#endif  // TOOLS_GN_COMPILE_COMMANDS_WRITER_H_
diff --git a/tools/gn/ninja_binary_target_writer.cc b/tools/gn/ninja_binary_target_writer.cc
index 48982d3..9e5632e 100644
--- a/tools/gn/ninja_binary_target_writer.cc
+++ b/tools/gn/ninja_binary_target_writer.cc
@@ -19,6 +19,7 @@
 #include "tools/gn/escape.h"
 #include "tools/gn/filesystem_utils.h"
 #include "tools/gn/ninja_utils.h"
+#include "tools/gn/ninja_target_command_util.h"
 #include "tools/gn/scheduler.h"
 #include "tools/gn/settings.h"
 #include "tools/gn/source_file_type.h"
@@ -50,78 +51,6 @@
   return opts;
 }
 
-struct DefineWriter {
-  DefineWriter() { options.mode = ESCAPE_NINJA_COMMAND; }
-
-  void operator()(const std::string& s, std::ostream& out) const {
-    out << " ";
-    EscapeStringToStream(out, "-D" + s, options);
-  }
-
-  EscapeOptions options;
-};
-
-struct IncludeWriter {
-  explicit IncludeWriter(PathOutput& path_output) : path_output_(path_output) {}
-  ~IncludeWriter() = default;
-
-  void operator()(const SourceDir& d, std::ostream& out) const {
-    std::ostringstream path_out;
-    path_output_.WriteDir(path_out, d, PathOutput::DIR_NO_LAST_SLASH);
-    const std::string& path = path_out.str();
-    if (path[0] == '"')
-      out << " \"-I" << path.substr(1);
-    else
-      out << " -I" << path;
-  }
-
-  PathOutput& path_output_;
-};
-
-// Returns the language-specific suffix for precompiled header files.
-const char* GetPCHLangSuffixForToolType(Toolchain::ToolType type) {
-  switch (type) {
-    case Toolchain::TYPE_CC:
-      return "c";
-    case Toolchain::TYPE_CXX:
-      return "cc";
-    case Toolchain::TYPE_OBJC:
-      return "m";
-    case Toolchain::TYPE_OBJCXX:
-      return "mm";
-    default:
-      NOTREACHED() << "Not a valid PCH tool type: " << type;
-      return "";
-  }
-}
-
-std::string GetWindowsPCHObjectExtension(Toolchain::ToolType tool_type,
-                                         const std::string& obj_extension) {
-  const char* lang_suffix = GetPCHLangSuffixForToolType(tool_type);
-  std::string result = ".";
-  // For MSVC, annotate the obj files with the language type. For example:
-  //   obj/foo/target_name.precompile.obj ->
-  //   obj/foo/target_name.precompile.cc.obj
-  result += lang_suffix;
-  result += obj_extension;
-  return result;
-}
-
-std::string GetGCCPCHOutputExtension(Toolchain::ToolType tool_type) {
-  const char* lang_suffix = GetPCHLangSuffixForToolType(tool_type);
-  std::string result = ".";
-  // For GCC, the output name must have a .gch suffix and be annotated with
-  // the language type. For example:
-  //   obj/foo/target_name.header.h ->
-  //   obj/foo/target_name.header.h-cc.gch
-  // In order for the compiler to pick it up, the output name (minus the .gch
-  // suffix MUST match whatever is passed to the -include flag).
-  result += "h-";
-  result += lang_suffix;
-  result += ".gch";
-  return result;
-}
-
 // Returns the language-specific lang recognized by gcc’s -x flag for
 // precompiled header files.
 const char* GetPCHLangForToolType(Toolchain::ToolType type) {
@@ -140,55 +69,6 @@
   }
 }
 
-// Fills |outputs| with the object or gch file for the precompiled header of the
-// given type (flag type and tool type must match).
-void GetPCHOutputFiles(const Target* target,
-                       Toolchain::ToolType tool_type,
-                       std::vector<OutputFile>* outputs) {
-  outputs->clear();
-
-  // Compute the tool. This must use the tool type passed in rather than the
-  // detected file type of the precompiled source file since the same
-  // precompiled source file will be used for separate C/C++ compiles.
-  const Tool* tool = target->toolchain()->GetTool(tool_type);
-  if (!tool)
-    return;
-  SubstitutionWriter::ApplyListToCompilerAsOutputFile(
-      target, target->config_values().precompiled_source(), tool->outputs(),
-      outputs);
-
-  if (outputs->empty())
-    return;
-  if (outputs->size() > 1)
-    outputs->resize(1);  // Only link the first output from the compiler tool.
-
-  std::string& output_value = (*outputs)[0].value();
-  size_t extension_offset = FindExtensionOffset(output_value);
-  if (extension_offset == std::string::npos) {
-    // No extension found.
-    return;
-  }
-  DCHECK(extension_offset >= 1);
-  DCHECK(output_value[extension_offset - 1] == '.');
-
-  std::string output_extension;
-  Tool::PrecompiledHeaderType header_type = tool->precompiled_header_type();
-  switch (header_type) {
-    case Tool::PCH_MSVC:
-      output_extension = GetWindowsPCHObjectExtension(
-          tool_type, output_value.substr(extension_offset - 1));
-      break;
-    case Tool::PCH_GCC:
-      output_extension = GetGCCPCHOutputExtension(tool_type);
-      break;
-    case Tool::PCH_NONE:
-      NOTREACHED() << "No outputs for no PCH type.";
-      break;
-  }
-  output_value.replace(extension_offset - 1, std::string::npos,
-                       output_extension);
-}
-
 // Appends the object files generated by the given source set to the given
 // output vector.
 void AddSourceSetObjectFiles(const Target* source_set,
@@ -372,29 +252,29 @@
 
   EscapeOptions opts = GetFlagOptions();
   if (used_types.Get(SOURCE_S) || used_types.Get(SOURCE_ASM)) {
-    WriteOneFlag(SUBSTITUTION_ASMFLAGS, false, Toolchain::TYPE_NONE,
-                 &ConfigValues::asmflags, opts);
+    WriteOneFlag(target_, SUBSTITUTION_ASMFLAGS, false, Toolchain::TYPE_NONE,
+                 &ConfigValues::asmflags, opts, path_output_, out_);
   }
   if (used_types.Get(SOURCE_C) || used_types.Get(SOURCE_CPP) ||
       used_types.Get(SOURCE_M) || used_types.Get(SOURCE_MM)) {
-    WriteOneFlag(SUBSTITUTION_CFLAGS, false, Toolchain::TYPE_NONE,
-                 &ConfigValues::cflags, opts);
+    WriteOneFlag(target_, SUBSTITUTION_CFLAGS, false, Toolchain::TYPE_NONE,
+                 &ConfigValues::cflags, opts, path_output_, out_);
   }
   if (used_types.Get(SOURCE_C)) {
-    WriteOneFlag(SUBSTITUTION_CFLAGS_C, has_precompiled_headers,
-                 Toolchain::TYPE_CC, &ConfigValues::cflags_c, opts);
+    WriteOneFlag(target_, SUBSTITUTION_CFLAGS_C, has_precompiled_headers,
+                 Toolchain::TYPE_CC, &ConfigValues::cflags_c, opts, path_output_, out_);
   }
   if (used_types.Get(SOURCE_CPP)) {
-    WriteOneFlag(SUBSTITUTION_CFLAGS_CC, has_precompiled_headers,
-                 Toolchain::TYPE_CXX, &ConfigValues::cflags_cc, opts);
+    WriteOneFlag(target_, SUBSTITUTION_CFLAGS_CC, has_precompiled_headers,
+                 Toolchain::TYPE_CXX, &ConfigValues::cflags_cc, opts, path_output_, out_);
   }
   if (used_types.Get(SOURCE_M)) {
-    WriteOneFlag(SUBSTITUTION_CFLAGS_OBJC, has_precompiled_headers,
-                 Toolchain::TYPE_OBJC, &ConfigValues::cflags_objc, opts);
+    WriteOneFlag(target_, SUBSTITUTION_CFLAGS_OBJC, has_precompiled_headers,
+                 Toolchain::TYPE_OBJC, &ConfigValues::cflags_objc, opts, path_output_, out_);
   }
   if (used_types.Get(SOURCE_MM)) {
-    WriteOneFlag(SUBSTITUTION_CFLAGS_OBJCC, has_precompiled_headers,
-                 Toolchain::TYPE_OBJCXX, &ConfigValues::cflags_objcc, opts);
+    WriteOneFlag(target_, SUBSTITUTION_CFLAGS_OBJCC, has_precompiled_headers,
+                 Toolchain::TYPE_OBJCXX, &ConfigValues::cflags_objcc, opts, path_output_, out_);
   }
 
   WriteSharedVars(subst);
@@ -440,59 +320,6 @@
   return input_stamp_file;
 }
 
-void NinjaBinaryTargetWriter::WriteOneFlag(
-    SubstitutionType subst_enum,
-    bool has_precompiled_headers,
-    Toolchain::ToolType tool_type,
-    const std::vector<std::string>& (ConfigValues::*getter)() const,
-    EscapeOptions flag_escape_options) {
-  if (!target_->toolchain()->substitution_bits().used[subst_enum])
-    return;
-
-  out_ << kSubstitutionNinjaNames[subst_enum] << " =";
-
-  if (has_precompiled_headers) {
-    const Tool* tool = target_->toolchain()->GetTool(tool_type);
-    if (tool && tool->precompiled_header_type() == Tool::PCH_MSVC) {
-      // Name the .pch file.
-      out_ << " /Fp";
-      path_output_.WriteFile(out_, GetWindowsPCHFile(tool_type));
-
-      // Enables precompiled headers and names the .h file. It's a string
-      // rather than a file name (so no need to rebase or use path_output_).
-      out_ << " /Yu" << target_->config_values().precompiled_header();
-      RecursiveTargetConfigStringsToStream(target_, getter, flag_escape_options,
-                                           out_);
-    } else if (tool && tool->precompiled_header_type() == Tool::PCH_GCC) {
-      // The targets to build the .gch files should omit the -include flag
-      // below. To accomplish this, each substitution flag is overwritten in the
-      // target rule and these values are repeated. The -include flag is omitted
-      // in place of the required -x <header lang> flag for .gch targets.
-      RecursiveTargetConfigStringsToStream(target_, getter, flag_escape_options,
-                                           out_);
-
-      // Compute the gch file (it will be language-specific).
-      std::vector<OutputFile> outputs;
-      GetPCHOutputFiles(target_, tool_type, &outputs);
-      if (!outputs.empty()) {
-        // Trim the .gch suffix for the -include flag.
-        // e.g. for gch file foo/bar/target.precompiled.h.gch:
-        //          -include foo/bar/target.precompiled.h
-        std::string pch_file = outputs[0].value();
-        pch_file.erase(pch_file.length() - 4);
-        out_ << " -include " << pch_file;
-      }
-    } else {
-      RecursiveTargetConfigStringsToStream(target_, getter, flag_escape_options,
-                                           out_);
-    }
-  } else {
-    RecursiveTargetConfigStringsToStream(target_, getter, flag_escape_options,
-                                         out_);
-  }
-  out_ << std::endl;
-}
-
 void NinjaBinaryTargetWriter::WritePCHCommands(
     const SourceFileTypeSet& used_types,
     const OutputFile& input_dep,
@@ -1033,19 +860,6 @@
   }
 }
 
-OutputFile NinjaBinaryTargetWriter::GetWindowsPCHFile(
-    Toolchain::ToolType tool_type) const {
-  // Use "obj/{dir}/{target_name}_{lang}.pch" which ends up
-  // looking like "obj/chrome/browser/browser_cc.pch"
-  OutputFile ret = GetBuildDirForTargetAsOutputFile(target_, BuildDirType::OBJ);
-  ret.value().append(target_->label().name());
-  ret.value().push_back('_');
-  ret.value().append(GetPCHLangSuffixForToolType(tool_type));
-  ret.value().append(".pch");
-
-  return ret;
-}
-
 bool NinjaBinaryTargetWriter::CheckForDuplicateObjectFiles(
     const std::vector<OutputFile>& files) const {
   std::unordered_set<std::string> set;
diff --git a/tools/gn/ninja_binary_target_writer.h b/tools/gn/ninja_binary_target_writer.h
index 4bc5b44..5cb7ab5 100644
--- a/tools/gn/ninja_binary_target_writer.h
+++ b/tools/gn/ninja_binary_target_writer.h
@@ -37,21 +37,6 @@
   // will be empty if there are no inputs.
   OutputFile WriteInputsStampAndGetDep() const;
 
-  // has_precompiled_headers is set when this substitution matches a tool type
-  // that supports precompiled headers, and this target supports precompiled
-  // headers. It doesn't indicate if the tool has precompiled headers (this
-  // will be looked up by this function).
-  //
-  // The tool_type indicates the corresponding tool for flags that are
-  // tool-specific (e.g. "cflags_c"). For non-tool-specific flags (e.g.
-  // "defines") tool_type should be TYPE_NONE.
-  void WriteOneFlag(SubstitutionType subst_enum,
-                    bool has_precompiled_headers,
-                    Toolchain::ToolType tool_type,
-                    const std::vector<std::string>& (ConfigValues::*getter)()
-                        const,
-                    EscapeOptions flag_escape_options);
-
   // Writes build lines required for precompiled headers. Any generated
   // object files will be appended to the |object_files|. Any generated
   // non-object files (for instance, .gch files from a GCC toolchain, are
@@ -140,9 +125,6 @@
   void WriteOrderOnlyDependencies(
       const UniqueVector<const Target*>& non_linkable_deps);
 
-  // Returns the computed name of the Windows .pch file for the given
-  // tool type. The tool must support precompiled headers.
-  OutputFile GetWindowsPCHFile(Toolchain::ToolType tool_type) const;
 
   // Checks for duplicates in the given list of output files. If any duplicates
   // are found, throws an error and return false.
diff --git a/tools/gn/ninja_binary_target_writer_unittest.cc b/tools/gn/ninja_binary_target_writer_unittest.cc
index 6d02a8b..a0a7ce8 100644
--- a/tools/gn/ninja_binary_target_writer_unittest.cc
+++ b/tools/gn/ninja_binary_target_writer_unittest.cc
@@ -9,6 +9,7 @@
 #include <utility>
 
 #include "tools/gn/config.h"
+#include "tools/gn/ninja_target_command_util.h"
 #include "tools/gn/scheduler.h"
 #include "tools/gn/target.h"
 #include "tools/gn/test_with_scheduler.h"
diff --git a/tools/gn/ninja_target_command_util.cc b/tools/gn/ninja_target_command_util.cc
new file mode 100644
index 0000000..29662cd
--- /dev/null
+++ b/tools/gn/ninja_target_command_util.cc
@@ -0,0 +1,179 @@
+// Copyright 2018 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 "tools/gn/ninja_target_command_util.h"
+
+#include <string.h>
+
+#include "tools/gn/substitution_writer.h"
+
+namespace {
+
+// Returns the language-specific suffix for precompiled header files.
+const char* GetPCHLangSuffixForToolType(Toolchain::ToolType type) {
+  switch (type) {
+    case Toolchain::TYPE_CC:
+      return "c";
+    case Toolchain::TYPE_CXX:
+      return "cc";
+    case Toolchain::TYPE_OBJC:
+      return "m";
+    case Toolchain::TYPE_OBJCXX:
+      return "mm";
+    default:
+      NOTREACHED() << "Not a valid PCH tool type: " << type;
+      return "";
+  }
+}
+
+}  // namespace
+
+// Returns the computed name of the Windows .pch file for the given
+// tool type. The tool must support precompiled headers.
+OutputFile GetWindowsPCHFile(const Target* target,
+                             Toolchain::ToolType tool_type) {
+  // Use "obj/{dir}/{target_name}_{lang}.pch" which ends up
+  // looking like "obj/chrome/browser/browser_cc.pch"
+  OutputFile ret = GetBuildDirForTargetAsOutputFile(target, BuildDirType::OBJ);
+  ret.value().append(target->label().name());
+  ret.value().push_back('_');
+  ret.value().append(GetPCHLangSuffixForToolType(tool_type));
+  ret.value().append(".pch");
+
+  return ret;
+}
+
+void WriteOneFlag(const Target* target,
+                  SubstitutionType subst_enum,
+                  bool has_precompiled_headers,
+                  Toolchain::ToolType tool_type,
+                  const std::vector<std::string>& (ConfigValues::*getter)()
+                      const,
+                  EscapeOptions flag_escape_options,
+                  PathOutput& path_output,
+                  std::ostream& out,
+                  bool write_substitution) {
+  if (!target->toolchain()->substitution_bits().used[subst_enum])
+    return;
+
+  if (write_substitution)
+    out << kSubstitutionNinjaNames[subst_enum] << " =";
+
+  if (has_precompiled_headers) {
+    const Tool* tool = target->toolchain()->GetTool(tool_type);
+    if (tool && tool->precompiled_header_type() == Tool::PCH_MSVC) {
+      // Name the .pch file.
+      out << " /Fp";
+      path_output.WriteFile(out, GetWindowsPCHFile(target, tool_type));
+
+      // Enables precompiled headers and names the .h file. It's a string
+      // rather than a file name (so no need to rebase or use path_output).
+      out << " /Yu" << target->config_values().precompiled_header();
+      RecursiveTargetConfigStringsToStream(target, getter, flag_escape_options,
+                                           out);
+    } else if (tool && tool->precompiled_header_type() == Tool::PCH_GCC) {
+      // The targets to build the .gch files should omit the -include flag
+      // below. To accomplish this, each substitution flag is overwritten in the
+      // target rule and these values are repeated. The -include flag is omitted
+      // in place of the required -x <header lang> flag for .gch targets.
+      RecursiveTargetConfigStringsToStream(target, getter, flag_escape_options,
+                                           out);
+
+      // Compute the gch file (it will be language-specific).
+      std::vector<OutputFile> outputs;
+      GetPCHOutputFiles(target, tool_type, &outputs);
+      if (!outputs.empty()) {
+        // Trim the .gch suffix for the -include flag.
+        // e.g. for gch file foo/bar/target.precompiled.h.gch:
+        //          -include foo/bar/target.precompiled.h
+        std::string pch_file = outputs[0].value();
+        pch_file.erase(pch_file.length() - 4);
+        out << " -include " << pch_file;
+      }
+    } else {
+      RecursiveTargetConfigStringsToStream(target, getter, flag_escape_options,
+                                           out);
+    }
+  } else {
+    RecursiveTargetConfigStringsToStream(target, getter, flag_escape_options,
+                                         out);
+  }
+
+  if (write_substitution)
+    out << std::endl;
+}
+
+void GetPCHOutputFiles(const Target* target,
+                       Toolchain::ToolType tool_type,
+                       std::vector<OutputFile>* outputs) {
+  outputs->clear();
+
+  // Compute the tool. This must use the tool type passed in rather than the
+  // detected file type of the precompiled source file since the same
+  // precompiled source file will be used for separate C/C++ compiles.
+  const Tool* tool = target->toolchain()->GetTool(tool_type);
+  if (!tool)
+    return;
+  SubstitutionWriter::ApplyListToCompilerAsOutputFile(
+      target, target->config_values().precompiled_source(), tool->outputs(),
+      outputs);
+
+  if (outputs->empty())
+    return;
+  if (outputs->size() > 1)
+    outputs->resize(1);  // Only link the first output from the compiler tool.
+
+  std::string& output_value = (*outputs)[0].value();
+  size_t extension_offset = FindExtensionOffset(output_value);
+  if (extension_offset == std::string::npos) {
+    // No extension found.
+    return;
+  }
+  DCHECK(extension_offset >= 1);
+  DCHECK(output_value[extension_offset - 1] == '.');
+
+  std::string output_extension;
+  Tool::PrecompiledHeaderType header_type = tool->precompiled_header_type();
+  switch (header_type) {
+    case Tool::PCH_MSVC:
+      output_extension = GetWindowsPCHObjectExtension(
+          tool_type, output_value.substr(extension_offset - 1));
+      break;
+    case Tool::PCH_GCC:
+      output_extension = GetGCCPCHOutputExtension(tool_type);
+      break;
+    case Tool::PCH_NONE:
+      NOTREACHED() << "No outputs for no PCH type.";
+      break;
+  }
+  output_value.replace(extension_offset - 1, std::string::npos,
+                       output_extension);
+}
+
+std::string GetGCCPCHOutputExtension(Toolchain::ToolType tool_type) {
+  const char* lang_suffix = GetPCHLangSuffixForToolType(tool_type);
+  std::string result = ".";
+  // For GCC, the output name must have a .gch suffix and be annotated with
+  // the language type. For example:
+  //   obj/foo/target_name.header.h ->
+  //   obj/foo/target_name.header.h-cc.gch
+  // In order for the compiler to pick it up, the output name (minus the .gch
+  // suffix MUST match whatever is passed to the -include flag).
+  result += "h-";
+  result += lang_suffix;
+  result += ".gch";
+  return result;
+}
+
+std::string GetWindowsPCHObjectExtension(Toolchain::ToolType tool_type,
+                                         const std::string& obj_extension) {
+  const char* lang_suffix = GetPCHLangSuffixForToolType(tool_type);
+  std::string result = ".";
+  // For MSVC, annotate the obj files with the language type. For example:
+  //   obj/foo/target_name.precompile.obj ->
+  //   obj/foo/target_name.precompile.cc.obj
+  result += lang_suffix;
+  result += obj_extension;
+  return result;
+}
diff --git a/tools/gn/ninja_target_command_util.h b/tools/gn/ninja_target_command_util.h
new file mode 100644
index 0000000..619add3
--- /dev/null
+++ b/tools/gn/ninja_target_command_util.h
@@ -0,0 +1,84 @@
+// Copyright 2018 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_TARGET_COMMAND_WRITER_H_
+#define TOOLS_GN_NINJA_TARGET_COMMAND_WRITER_H_
+
+#include "base/json/string_escape.h"
+#include "tools/gn/config_values_extractors.h"
+#include "tools/gn/escape.h"
+#include "tools/gn/filesystem_utils.h"
+#include "tools/gn/path_output.h"
+#include "tools/gn/target.h"
+#include "tools/gn/toolchain.h"
+
+struct DefineWriter {
+  DefineWriter() { options.mode = ESCAPE_NINJA_COMMAND; }
+  DefineWriter(EscapingMode mode, bool escape_strings)
+      : escape_strings(escape_strings) {
+    options.mode = mode;
+  }
+
+  void operator()(const std::string& s, std::ostream& out) const {
+    out << " ";
+    if (escape_strings) {
+      std::string dest;
+      base::EscapeJSONString(s, false, &dest);
+      EscapeStringToStream(out, "-D" + dest, options);
+      return;
+    }
+    EscapeStringToStream(out, "-D" + s, options);
+  }
+
+  EscapeOptions options;
+  bool escape_strings = false;
+};
+
+struct IncludeWriter {
+  explicit IncludeWriter(PathOutput& path_output) : path_output_(path_output) {}
+  ~IncludeWriter() = default;
+
+  void operator()(const SourceDir& d, std::ostream& out) const {
+    std::ostringstream path_out;
+    path_output_.WriteDir(path_out, d, PathOutput::DIR_NO_LAST_SLASH);
+    const std::string& path = path_out.str();
+    if (path[0] == '"')
+      out << " \"-I" << path.substr(1);
+    else
+      out << " -I" << path;
+  }
+
+  PathOutput& path_output_;
+};
+
+// has_precompiled_headers is set when this substitution matches a tool type
+// that supports precompiled headers, and this target supports precompiled
+// headers. It doesn't indicate if the tool has precompiled headers (this
+// will be looked up by this function).
+//
+// The tool_type indicates the corresponding tool for flags that are
+// tool-specific (e.g. "cflags_c"). For non-tool-specific flags (e.g.
+// "defines") tool_type should be TYPE_NONE.
+void WriteOneFlag(const Target* target,
+                  SubstitutionType subst_enum,
+                  bool has_precompiled_headers,
+                  Toolchain::ToolType tool_type,
+                  const std::vector<std::string>& (ConfigValues::*getter)()
+                      const,
+                  EscapeOptions flag_escape_options,
+                  PathOutput& path_output,
+                  std::ostream& out,
+                  bool write_substitution = true);
+
+// Fills |outputs| with the object or gch file for the precompiled header of the
+// given type (flag type and tool type must match).
+void GetPCHOutputFiles(const Target* target,
+                       Toolchain::ToolType tool_type,
+                       std::vector<OutputFile>* outputs);
+
+std::string GetGCCPCHOutputExtension(Toolchain::ToolType tool_type);
+std::string GetWindowsPCHObjectExtension(Toolchain::ToolType tool_type,
+                                         const std::string& obj_extension);
+
+#endif  // TOOLS_GN_NINJA_TARGET_COMMAND_WRITER_H_