clang: Add support for C++ modules in deps

This adds support for {{module_deps}}. This expansion is to be used by
the "cxx" tool. When the deps of a target include dependencies on
modules (defined as a target that includes a .modulemap file, which is
built to a .pcm), module_deps will be filled out with the required flags
to depend on those modules.

The targets that are depended upon must currently have a (handwritten)
.modulemap file in their sources.

The dependency semantics (as described in NinjaCBinaryTargetWriterTest)
are:
- .modulemap are built to .pcm by cxx_module
- .pcm aren't linked against
- The .cc of a target that uses modules depend on the .pcm ("implicit"
dependency in ninja terminology)
- The .cc sources of targets that depend on a module have implicit
dependencies on the pcm of the dependent modules target as well.
- A .a or executable does not depend on the .pcm.

Bug: fuchsia:27276
Change-Id: I84c26975b93db71e5309ad607fce900fe2705f90
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/9602
Commit-Queue: Scott Graham <scottmg@chromium.org>
Reviewed-by: Brett Wilson <brettw@chromium.org>
Reviewed-by: Petr Hosek <phosek@google.com>
diff --git a/docs/reference.md b/docs/reference.md
index 168d64e..257f474 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -3674,6 +3674,11 @@
         {{target_output_name}}, this is not affected by the "output_prefix" in
         the tool or the "output_name" set on the target.
 
+    {{label_no_toolchain}}
+        The label of the current target, never including the toolchain
+        (otherwise, this is identical to {{label}}). This is used as the module
+        name when using .modulemap files.
+
     {{output}}
         The relative path and name of the output(s) of the current build step.
         If there is more than one output, this will expand to a list of all of
diff --git a/src/gn/c_substitution_type.cc b/src/gn/c_substitution_type.cc
index d0c04c8..618e846 100644
--- a/src/gn/c_substitution_type.cc
+++ b/src/gn/c_substitution_type.cc
@@ -14,7 +14,8 @@
     &CSubstitutionCFlagsC,         &CSubstitutionCFlagsCc,
     &CSubstitutionCFlagsObjC,      &CSubstitutionCFlagsObjCc,
     &CSubstitutionDefines,         &CSubstitutionFrameworkDirs,
-    &CSubstitutionIncludeDirs,     &CSubstitutionSwiftModules,
+    &CSubstitutionIncludeDirs,     &CSubstitutionModuleDeps,
+    &CSubstitutionSwiftModules,
 
     &CSubstitutionLinkerInputs,    &CSubstitutionLinkerInputsNewline,
     &CSubstitutionLdFlags,         &CSubstitutionLibs,
@@ -40,11 +41,12 @@
                                                  "framework_dirs"};
 const Substitution CSubstitutionIncludeDirs = {"{{include_dirs}}",
                                               "include_dirs"};
+const Substitution CSubstitutionModuleDeps = {"{{module_deps}}", "module_deps"};
 
 // Valid for linker tools.
 const Substitution CSubstitutionLinkerInputs = {"{{inputs}}", "in"};
 const Substitution CSubstitutionLinkerInputsNewline = {"{{inputs_newline}}",
-                                                      "in_newline"};
+                                                       "in_newline"};
 const Substitution CSubstitutionLdFlags = {"{{ldflags}}", "ldflags"};
 const Substitution CSubstitutionLibs = {"{{libs}}", "libs"};
 const Substitution CSubstitutionSoLibs = {"{{solibs}}", "solibs"};
@@ -72,7 +74,7 @@
          type == &CSubstitutionCFlagsCc || type == &CSubstitutionCFlagsObjC ||
          type == &CSubstitutionCFlagsObjCc || type == &CSubstitutionDefines ||
          type == &CSubstitutionFrameworkDirs ||
-         type == &CSubstitutionIncludeDirs;
+         type == &CSubstitutionIncludeDirs || type == &CSubstitutionModuleDeps;
 }
 
 bool IsValidCompilerOutputsSubstitution(const Substitution* type) {
diff --git a/src/gn/c_substitution_type.h b/src/gn/c_substitution_type.h
index 63eb627..e4255fd 100644
--- a/src/gn/c_substitution_type.h
+++ b/src/gn/c_substitution_type.h
@@ -23,6 +23,7 @@
 extern const Substitution CSubstitutionDefines;
 extern const Substitution CSubstitutionFrameworkDirs;
 extern const Substitution CSubstitutionIncludeDirs;
+extern const Substitution CSubstitutionModuleDeps;
 
 // Valid for linker tools.
 extern const Substitution CSubstitutionLinkerInputs;
diff --git a/src/gn/compile_commands_writer.cc b/src/gn/compile_commands_writer.cc
index 05e78d0..9f8ef63 100644
--- a/src/gn/compile_commands_writer.cc
+++ b/src/gn/compile_commands_writer.cc
@@ -187,6 +187,7 @@
         out << flags.cflags_objcc;
     } else if (range.type == &SubstitutionLabel ||
                range.type == &SubstitutionLabelName ||
+               range.type == &SubstitutionLabelNoToolchain ||
                range.type == &SubstitutionRootGenDir ||
                range.type == &SubstitutionRootOutDir ||
                range.type == &SubstitutionTargetGenDir ||
diff --git a/src/gn/function_toolchain.cc b/src/gn/function_toolchain.cc
index 1288511..52ac8f7 100644
--- a/src/gn/function_toolchain.cc
+++ b/src/gn/function_toolchain.cc
@@ -603,6 +603,11 @@
         {{target_output_name}}, this is not affected by the "output_prefix" in
         the tool or the "output_name" set on the target.
 
+    {{label_no_toolchain}}
+        The label of the current target, never including the toolchain
+        (otherwise, this is identical to {{label}}). This is used as the module
+        name when using .modulemap files.
+
     {{output}}
         The relative path and name of the output(s) of the current build step.
         If there is more than one output, this will expand to a list of all of
diff --git a/src/gn/ninja_c_binary_target_writer.cc b/src/gn/ninja_c_binary_target_writer.cc
index 62edd4f..5f4a14b 100644
--- a/src/gn/ninja_c_binary_target_writer.cc
+++ b/src/gn/ninja_c_binary_target_writer.cc
@@ -28,6 +28,22 @@
 #include "gn/substitution_writer.h"
 #include "gn/target.h"
 
+struct ModuleDep {
+  ModuleDep(const SourceFile* modulemap,
+            const std::string& module_name,
+            const OutputFile& pcm)
+      : modulemap(modulemap), module_name(module_name), pcm(pcm) {}
+
+  // The input module.modulemap source file.
+  const SourceFile* modulemap;
+
+  // The internal module name, in GN this is the target's label.
+  std::string module_name;
+
+  // The compiled version of the module.
+  OutputFile pcm;
+};
+
 namespace {
 
 // Returns the proper escape options for writing compiler and linker flags.
@@ -52,6 +68,49 @@
   return "";
 }
 
+const SourceFile* GetModuleMapFromTargetSources(const Target* target) {
+  for (const SourceFile& sf : target->sources()) {
+    if (sf.type() == SourceFile::SOURCE_MODULEMAP) {
+      return &sf;
+    }
+  }
+  return nullptr;
+}
+
+std::vector<ModuleDep> GetModuleDepsInformation(const Target* target) {
+  std::vector<ModuleDep> ret;
+
+  auto add = [&ret](const Target* t) {
+    const SourceFile* modulemap = GetModuleMapFromTargetSources(t);
+    CHECK(modulemap);
+
+    std::string label;
+    CHECK(SubstitutionWriter::GetTargetSubstitution(
+        t, &SubstitutionLabelNoToolchain, &label));
+
+    const char* tool_type;
+    std::vector<OutputFile> modulemap_outputs;
+    CHECK(
+        t->GetOutputFilesForSource(*modulemap, &tool_type, &modulemap_outputs));
+    // Must be only one .pcm from .modulemap.
+    CHECK(modulemap_outputs.size() == 1u);
+    ret.emplace_back(modulemap, label, modulemap_outputs[0]);
+  };
+
+  if (target->source_types_used().Get(SourceFile::SOURCE_MODULEMAP)) {
+    add(target);
+  }
+
+  for (const auto& pair: target->GetDeps(Target::DEPS_LINKED)) {
+    // Having a .modulemap source means that the dependency is modularized.
+    if (pair.ptr->source_types_used().Get(SourceFile::SOURCE_MODULEMAP)) {
+      add(pair.ptr);
+    }
+  }
+
+  return ret;
+}
+
 }  // namespace
 
 NinjaCBinaryTargetWriter::NinjaCBinaryTargetWriter(const Target* target,
@@ -62,7 +121,9 @@
 NinjaCBinaryTargetWriter::~NinjaCBinaryTargetWriter() = default;
 
 void NinjaCBinaryTargetWriter::Run() {
-  WriteCompilerVars();
+  std::vector<ModuleDep> module_dep_info = GetModuleDepsInformation(target_);
+
+  WriteCompilerVars(module_dep_info);
 
   size_t num_stamp_uses = target_->sources().size();
 
@@ -127,8 +188,8 @@
   std::vector<OutputFile> obj_files;
   std::vector<SourceFile> other_files;
   if (!target_->source_types_used().SwiftSourceUsed()) {
-    WriteSources(*pch_files, input_deps, order_only_deps, &obj_files,
-                 &other_files);
+    WriteSources(*pch_files, input_deps, order_only_deps, module_dep_info,
+                 &obj_files, &other_files);
   } else {
     WriteSwiftSources(input_deps, order_only_deps, &obj_files);
   }
@@ -154,7 +215,8 @@
   }
 }
 
-void NinjaCBinaryTargetWriter::WriteCompilerVars() {
+void NinjaCBinaryTargetWriter::WriteCompilerVars(
+    const std::vector<ModuleDep>& module_dep_info) {
   const SubstitutionBits& subst = target_->toolchain()->substitution_bits();
 
   // Defines.
@@ -193,6 +255,29 @@
     out_ << std::endl;
   }
 
+  if (!module_dep_info.empty()) {
+    // TODO(scottmg): Currently clang modules only supported for C++.
+    if (target_->source_types_used().Get(SourceFile::SOURCE_CPP)) {
+      if (target_->toolchain()->substitution_bits().used.count(
+              &CSubstitutionModuleDeps)) {
+        EscapeOptions options;
+        options.mode = ESCAPE_NINJA_COMMAND;
+
+        out_ << CSubstitutionModuleDeps.ninja_name << " = ";
+        EscapeStringToStream(out_, "-fmodules-embed-all-files", options);
+
+        for (const auto& module_dep : module_dep_info) {
+          out_ << " ";
+          EscapeStringToStream(
+              out_, "-fmodule-file=" + module_dep.module_name + "=", options);
+          path_output_.WriteFile(out_, module_dep.pcm);
+        }
+
+        out_ << std::endl;
+      }
+    }
+  }
+
   bool has_precompiled_headers =
       target_->config_values().has_precompiled_headers();
 
@@ -427,6 +512,7 @@
     const std::vector<OutputFile>& pch_deps,
     const std::vector<OutputFile>& input_deps,
     const std::vector<OutputFile>& order_only_deps,
+    const std::vector<ModuleDep>& module_dep_info,
     std::vector<OutputFile>* object_files,
     std::vector<SourceFile>* other_files) {
   DCHECK(!target_->source_types_used().SwiftSourceUsed());
@@ -446,7 +532,6 @@
       continue;  // No output for this source.
     }
 
-
     std::copy(input_deps.begin(), input_deps.end(), std::back_inserter(deps));
 
     if (tool_name != Tool::kToolNone) {
@@ -478,13 +563,21 @@
           }
         }
       }
+
+      for (const auto& module_dep : module_dep_info) {
+        if (tool_outputs[0] != module_dep.pcm)
+          deps.push_back(module_dep.pcm);
+      }
+
       WriteCompilerBuildLine({source}, deps, order_only_deps, tool_name,
                              tool_outputs);
     }
 
     // It's theoretically possible for a compiler to produce more than one
     // output, but we'll only link to the first output.
-    object_files->push_back(tool_outputs[0]);
+    if (source.type() != SourceFile::SOURCE_MODULEMAP) {
+      object_files->push_back(tool_outputs[0]);
+    }
   }
 
   out_ << std::endl;
diff --git a/src/gn/ninja_c_binary_target_writer.h b/src/gn/ninja_c_binary_target_writer.h
index 695c400..c2e63e4 100644
--- a/src/gn/ninja_c_binary_target_writer.h
+++ b/src/gn/ninja_c_binary_target_writer.h
@@ -12,6 +12,7 @@
 #include "gn/unique_vector.h"
 
 struct EscapeOptions;
+struct ModuleDep;
 
 // Writes a .ninja file for a binary target type (an executable, a shared
 // library, or a static library).
@@ -26,7 +27,7 @@
   using OutputFileSet = std::set<OutputFile>;
 
   // Writes all flags for the compiler: includes, defines, cflags, etc.
-  void WriteCompilerVars();
+  void WriteCompilerVars(const std::vector<ModuleDep>& module_dep_info);
 
   // Writes build lines required for precompiled headers. Any generated
   // object files will be appended to the |object_files|. Any generated
@@ -71,6 +72,7 @@
   void WriteSources(const std::vector<OutputFile>& pch_deps,
                     const std::vector<OutputFile>& input_deps,
                     const std::vector<OutputFile>& order_only_deps,
+                    const std::vector<ModuleDep>& module_dep_info,
                     std::vector<OutputFile>* object_files,
                     std::vector<SourceFile>* other_files);
   void WriteSwiftSources(const std::vector<OutputFile>& input_deps,
diff --git a/src/gn/ninja_c_binary_target_writer_unittest.cc b/src/gn/ninja_c_binary_target_writer_unittest.cc
index f650e44..391d1d7 100644
--- a/src/gn/ninja_c_binary_target_writer_unittest.cc
+++ b/src/gn/ninja_c_binary_target_writer_unittest.cc
@@ -1504,7 +1504,9 @@
   setup.toolchain()->SetTool(std::move(cxx_module_tool));
 
   TestTarget target(setup, "//foo:bar", Target::STATIC_LIBRARY);
+  target.sources().push_back(SourceFile("//foo/bar.cc"));
   target.sources().push_back(SourceFile("//foo/bar.modulemap"));
+  target.source_types_used().Set(SourceFile::SOURCE_CPP);
   target.source_types_used().Set(SourceFile::SOURCE_MODULEMAP);
   ASSERT_TRUE(target.OnResolved(&err));
 
@@ -1515,13 +1517,16 @@
   const char expected[] =
       "defines =\n"
       "include_dirs =\n"
+      "cflags =\n"
+      "cflags_cc =\n"
       "root_out_dir = .\n"
       "target_out_dir = obj/foo\n"
       "target_output_name = libbar\n"
       "\n"
+      "build obj/foo/libbar.bar.o: cxx ../../foo/bar.cc | obj/foo/libbar.bar.pcm\n"
       "build obj/foo/libbar.bar.pcm: cxx_module ../../foo/bar.modulemap\n"
       "\n"
-      "build obj/foo/libbar.a: alink obj/foo/libbar.bar.pcm\n"
+      "build obj/foo/libbar.a: alink obj/foo/libbar.bar.o\n"
       "  arflags =\n"
       "  output_extension = \n"
       "  output_dir = \n";
@@ -1690,3 +1695,183 @@
     EXPECT_EQ(expected, out_str) << expected << "\n" << out_str;
   }
 }
+
+TEST_F(NinjaCBinaryTargetWriterTest, DependOnModule) {
+  TestWithScope setup;
+  Err err;
+
+  // There's no cxx_module or flags in the test toolchain, set up a
+  // custom one here.
+  Settings module_settings(setup.build_settings(), "withmodules/");
+  Toolchain module_toolchain(&module_settings,
+                             Label(SourceDir("//toolchain/"), "withmodules"));
+  module_settings.set_toolchain_label(module_toolchain.label());
+  module_settings.set_default_toolchain_label(module_toolchain.label());
+
+  std::unique_ptr<Tool> cxx = std::make_unique<CTool>(CTool::kCToolCxx);
+  CTool* cxx_tool = cxx->AsC();
+  TestWithScope::SetCommandForTool(
+      "c++ {{source}} {{cflags}} {{cflags_cc}} {{module_deps}} "
+      "{{defines}} {{include_dirs}} -o {{output}}",
+      cxx_tool);
+  cxx_tool->set_outputs(SubstitutionList::MakeForTest(
+      "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o"));
+  cxx_tool->set_precompiled_header_type(CTool::PCH_GCC);
+  module_toolchain.SetTool(std::move(cxx));
+
+  std::unique_ptr<Tool> cxx_module_tool =
+      Tool::CreateTool(CTool::kCToolCxxModule);
+  TestWithScope::SetCommandForTool(
+      "c++ {{source}} {{cflags}} {{cflags_cc}} {{defines}} {{include_dirs}} "
+      "-fmodule-name={{label}} -c -x c++ -Xclang -emit-module -o {{output}}",
+      cxx_module_tool.get());
+  cxx_module_tool->set_outputs(SubstitutionList::MakeForTest(
+      "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.pcm"));
+  module_toolchain.SetTool(std::move(cxx_module_tool));
+
+  std::unique_ptr<Tool> alink = Tool::CreateTool(CTool::kCToolAlink);
+  CTool* alink_tool = alink->AsC();
+  TestWithScope::SetCommandForTool("ar {{output}} {{source}}", alink_tool);
+  alink_tool->set_lib_switch("-l");
+  alink_tool->set_lib_dir_switch("-L");
+  alink_tool->set_output_prefix("lib");
+  alink_tool->set_outputs(SubstitutionList::MakeForTest(
+      "{{target_out_dir}}/{{target_output_name}}.a"));
+  module_toolchain.SetTool(std::move(alink));
+
+  std::unique_ptr<Tool> link = Tool::CreateTool(CTool::kCToolLink);
+  CTool* link_tool = link->AsC();
+  TestWithScope::SetCommandForTool(
+      "ld -o {{target_output_name}} {{source}} "
+      "{{ldflags}} {{libs}}",
+      link_tool);
+  link_tool->set_lib_switch("-l");
+  link_tool->set_lib_dir_switch("-L");
+  link_tool->set_outputs(
+      SubstitutionList::MakeForTest("{{root_out_dir}}/{{target_output_name}}"));
+  module_toolchain.SetTool(std::move(link));
+
+  module_toolchain.ToolchainSetupComplete();
+
+  Target target(&module_settings, Label(SourceDir("//blah/"), "a"));
+  target.set_output_type(Target::STATIC_LIBRARY);
+  target.visibility().SetPublic();
+  target.sources().push_back(SourceFile("//blah/a.modulemap"));
+  target.sources().push_back(SourceFile("//blah/a.cc"));
+  target.sources().push_back(SourceFile("//blah/a.h"));
+  target.source_types_used().Set(SourceFile::SOURCE_CPP);
+  target.source_types_used().Set(SourceFile::SOURCE_MODULEMAP);
+  target.SetToolchain(&module_toolchain);
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  // The library first.
+  {
+    std::ostringstream out;
+    NinjaCBinaryTargetWriter writer(&target, out);
+    writer.Run();
+
+    const char expected[] = R"(defines =
+include_dirs =
+module_deps = -fmodules-embed-all-files -fmodule-file=//blah$:a=obj/blah/liba.a.pcm
+cflags =
+cflags_cc =
+label = //blah$:a
+root_out_dir = withmodules
+target_out_dir = obj/blah
+target_output_name = liba
+
+build obj/blah/liba.a.pcm: cxx_module ../../blah/a.modulemap
+build obj/blah/liba.a.o: cxx ../../blah/a.cc | obj/blah/liba.a.pcm
+
+build obj/blah/liba.a: alink obj/blah/liba.a.o
+  arflags =
+  output_extension = 
+  output_dir = 
+)";
+
+    std::string out_str = out.str();
+    EXPECT_EQ(expected, out_str) << expected << "\n" << out_str;
+  }
+
+  Target target2(&module_settings, Label(SourceDir("//stuff/"), "b"));
+  target2.set_output_type(Target::STATIC_LIBRARY);
+  target2.visibility().SetPublic();
+  target2.sources().push_back(SourceFile("//stuff/b.modulemap"));
+  target2.sources().push_back(SourceFile("//stuff/b.cc"));
+  target2.sources().push_back(SourceFile("//stuff/b.h"));
+  target2.source_types_used().Set(SourceFile::SOURCE_CPP);
+  target2.source_types_used().Set(SourceFile::SOURCE_MODULEMAP);
+  target2.SetToolchain(&module_toolchain);
+  ASSERT_TRUE(target2.OnResolved(&err));
+
+  // A second library to make sure the depender includes both.
+  {
+    std::ostringstream out;
+    NinjaCBinaryTargetWriter writer(&target2, out);
+    writer.Run();
+
+    const char expected[] = R"(defines =
+include_dirs =
+module_deps = -fmodules-embed-all-files -fmodule-file=//stuff$:b=obj/stuff/libb.b.pcm
+cflags =
+cflags_cc =
+label = //stuff$:b
+root_out_dir = withmodules
+target_out_dir = obj/stuff
+target_output_name = libb
+
+build obj/stuff/libb.b.pcm: cxx_module ../../stuff/b.modulemap
+build obj/stuff/libb.b.o: cxx ../../stuff/b.cc | obj/stuff/libb.b.pcm
+
+build obj/stuff/libb.a: alink obj/stuff/libb.b.o
+  arflags =
+  output_extension = 
+  output_dir = 
+)";
+
+    std::string out_str = out.str();
+    EXPECT_EQ(expected, out_str) << expected << "\n" << out_str;
+  }
+
+  Target depender(&module_settings, Label(SourceDir("//zap/"), "c"));
+  depender.set_output_type(Target::EXECUTABLE);
+  depender.sources().push_back(SourceFile("//zap/x.cc"));
+  depender.sources().push_back(SourceFile("//zap/y.cc"));
+  depender.source_types_used().Set(SourceFile::SOURCE_CPP);
+  depender.private_deps().push_back(LabelTargetPair(&target));
+  depender.private_deps().push_back(LabelTargetPair(&target2));
+  depender.SetToolchain(&module_toolchain);
+  ASSERT_TRUE(depender.OnResolved(&err));
+
+  // Then the executable that depends on it.
+  {
+    std::ostringstream out;
+    NinjaCBinaryTargetWriter writer(&depender, out);
+    writer.Run();
+
+    const char expected[] = R"(defines =
+include_dirs =
+module_deps = -fmodules-embed-all-files -fmodule-file=//blah$:a=obj/blah/liba.a.pcm -fmodule-file=//stuff$:b=obj/stuff/libb.b.pcm
+cflags =
+cflags_cc =
+label = //zap$:c
+root_out_dir = withmodules
+target_out_dir = obj/zap
+target_output_name = c
+
+build obj/zap/c.x.o: cxx ../../zap/x.cc | obj/blah/liba.a.pcm obj/stuff/libb.b.pcm
+build obj/zap/c.y.o: cxx ../../zap/y.cc | obj/blah/liba.a.pcm obj/stuff/libb.b.pcm
+
+build withmodules/c: link obj/zap/c.x.o obj/zap/c.y.o obj/blah/liba.a obj/stuff/libb.a
+  ldflags =
+  libs =
+  frameworks =
+  swiftmodules =
+  output_extension = 
+  output_dir = 
+)";
+
+    std::string out_str = out.str();
+    EXPECT_EQ(expected, out_str) << expected << "\n" << out_str;
+  }
+}
diff --git a/src/gn/ninja_target_writer.cc b/src/gn/ninja_target_writer.cc
index e284f24..b6bbb9f 100644
--- a/src/gn/ninja_target_writer.cc
+++ b/src/gn/ninja_target_writer.cc
@@ -139,12 +139,18 @@
     written_anything = true;
   }
 
-  // Target label name
+  // Target label name.
   if (bits.used.count(&SubstitutionLabelName)) {
     WriteEscapedSubstitution(&SubstitutionLabelName);
     written_anything = true;
   }
 
+  // Target label name without toolchain.
+  if (bits.used.count(&SubstitutionLabelNoToolchain)) {
+    WriteEscapedSubstitution(&SubstitutionLabelNoToolchain);
+    written_anything = true;
+  }
+
   // Root gen dir.
   if (bits.used.count(&SubstitutionRootGenDir)) {
     WriteEscapedSubstitution(&SubstitutionRootGenDir);
diff --git a/src/gn/substitution_type.cc b/src/gn/substitution_type.cc
index a4d1384..06c21b3 100644
--- a/src/gn/substitution_type.cc
+++ b/src/gn/substitution_type.cc
@@ -20,6 +20,7 @@
     &SubstitutionOutput,
     &SubstitutionLabel,
     &SubstitutionLabelName,
+    &SubstitutionLabelNoToolchain,
     &SubstitutionRootGenDir,
     &SubstitutionRootOutDir,
     &SubstitutionOutputDir,
@@ -72,6 +73,8 @@
 // do not vary on a per-file basis.
 const Substitution SubstitutionLabel = {"{{label}}", "label"};
 const Substitution SubstitutionLabelName = {"{{label_name}}", "label_name"};
+const Substitution SubstitutionLabelNoToolchain = {"{{label_no_toolchain}}",
+                                                   "label_no_toolchain"};
 const Substitution SubstitutionRootGenDir = {"{{root_gen_dir}}",
                                              "root_gen_dir"};
 const Substitution SubstitutionRootOutDir = {"{{root_out_dir}}",
@@ -166,6 +169,7 @@
 bool IsValidToolSubstitution(const Substitution* type) {
   return type == &SubstitutionLiteral || type == &SubstitutionOutput ||
          type == &SubstitutionLabel || type == &SubstitutionLabelName ||
+         type == &SubstitutionLabelNoToolchain ||
          type == &SubstitutionRootGenDir || type == &SubstitutionRootOutDir ||
          type == &SubstitutionTargetGenDir ||
          type == &SubstitutionTargetOutDir ||
diff --git a/src/gn/substitution_type.h b/src/gn/substitution_type.h
index 7ec929f..28f6c66 100644
--- a/src/gn/substitution_type.h
+++ b/src/gn/substitution_type.h
@@ -37,6 +37,7 @@
 extern const Substitution SubstitutionOutput;
 extern const Substitution SubstitutionLabel;
 extern const Substitution SubstitutionLabelName;
+extern const Substitution SubstitutionLabelNoToolchain;
 extern const Substitution SubstitutionRootGenDir;
 extern const Substitution SubstitutionRootOutDir;
 extern const Substitution SubstitutionOutputDir;
diff --git a/src/gn/substitution_writer.cc b/src/gn/substitution_writer.cc
index 6fe83ce..c9624d7 100644
--- a/src/gn/substitution_writer.cc
+++ b/src/gn/substitution_writer.cc
@@ -436,6 +436,8 @@
         target->label().GetUserVisibleName(!target->settings()->is_default());
   } else if (type == &SubstitutionLabelName) {
     *result = target->label().name();
+  } else if (type == &SubstitutionLabelNoToolchain) {
+    *result = target->label().GetUserVisibleName(false);
   } else if (type == &SubstitutionRootGenDir) {
     SetDirOrDotWithNoSlash(
         GetBuildDirAsOutputFile(BuildDirContext(target), BuildDirType::GEN)
diff --git a/src/gn/substitution_writer_unittest.cc b/src/gn/substitution_writer_unittest.cc
index 8e5709b..fc3c446 100644
--- a/src/gn/substitution_writer_unittest.cc
+++ b/src/gn/substitution_writer_unittest.cc
@@ -200,6 +200,10 @@
   EXPECT_EQ("baz", result);
 
   EXPECT_TRUE(SubstitutionWriter::GetTargetSubstitution(
+      &target, &SubstitutionLabelNoToolchain, &result));
+  EXPECT_EQ("//foo/bar:baz", result);
+
+  EXPECT_TRUE(SubstitutionWriter::GetTargetSubstitution(
       &target, &SubstitutionRootGenDir, &result));
   EXPECT_EQ("gen", result);