Add modulemap generation to GN.

This is the first step in adding a layering check to GN.
It generates modulemap files, and adds -fmodule-map-file to the
command-line.
This is guarded behind a flag, so there should be no change to the
change in behaviour of GN

Bug: b:491925153
Change-Id: Iaadf759240de829349796c43f4deda9a6a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/21521
Commit-Queue: Matt Stark <msta@google.com>
Reviewed-by: Takuto Ikuta <tikuta@google.com>
diff --git a/build/gen.py b/build/gen.py
index 6ccd5fe..e40627c 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -828,6 +828,7 @@
         'src/gn/args_unittest.cc',
         'src/gn/builder_record_map_unittest.cc',
         'src/gn/builder_unittest.cc',
+        'src/gn/binary_target_generator_unittest.cc',
         'src/gn/bundle_data_unittest.cc',
         'src/gn/c_include_iterator_unittest.cc',
         'src/gn/command_format_unittest.cc',
diff --git a/docs/reference.md b/docs/reference.md
index 92ffbf9..0532960 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -78,6 +78,7 @@
     *   [current_os: [string] The operating system of the current toolchain.](#var_current_os)
     *   [current_toolchain: [string] Label of the current toolchain.](#var_current_toolchain)
     *   [default_toolchain: [string] Label of the default toolchain.](#var_default_toolchain)
+    *   [generate_modulemap: [string] Mode for generating modulemaps.](#var_generate_modulemap)
     *   [gn_version: [number] The version of gn.](#var_gn_version)
     *   [host_cpu: [string] The processor architecture that GN is running on.](#var_host_cpu)
     *   [host_os: [string] The operating system that GN is running on.](#var_host_os)
@@ -4615,6 +4616,14 @@
   A fully-qualified label representing the default toolchain, which may not
   necessarily be the current one (see "current_toolchain").
 ```
+### <a name="var_generate_modulemap"></a>**generate_modulemap**: [string] Mode for generating modulemaps.&nbsp;[Back to Top](#gn-reference)
+
+#### **Possible values**:
+```
+  "none" (default): Don't generate a modulemap file for the target.
+  "textual": Generate a modulemap file for the target.
+    All public headers will be marked as textual.
+```
 ### <a name="var_gn_version"></a>**gn_version**: [number] The version of gn.&nbsp;[Back to Top](#gn-reference)
 
 ```
diff --git a/src/gn/binary_target_generator.cc b/src/gn/binary_target_generator.cc
index 1517734..39eb47c 100644
--- a/src/gn/binary_target_generator.cc
+++ b/src/gn/binary_target_generator.cc
@@ -14,7 +14,9 @@
 #include "gn/rust_variables.h"
 #include "gn/scope.h"
 #include "gn/settings.h"
+#include "gn/source_file.h"
 #include "gn/swift_values_generator.h"
+#include "gn/target.h"
 #include "gn/value_extractors.h"
 #include "gn/variables.h"
 
@@ -73,6 +75,9 @@
   if (!FillModuleName())
     return;
 
+  if (!FillModuleType())
+    return;
+
   if (target_->source_types_used().RustSourceUsed()) {
     RustValuesGenerator rustgen(target_, scope_, function_call_, err_);
     rustgen.Run();
@@ -284,3 +289,40 @@
   target_->set_module_name(value->string_value());
   return true;
 }
+
+bool BinaryTargetGenerator::FillModuleType() {
+  // Put this first so it gets marked as used even if it's unnecessary.
+  const Value* generate_modulemap_val =
+      scope_->GetValue(variables::kGenerateModulemap, true);
+
+  if (target_->source_types_used().Get(SourceFile::SOURCE_MODULEMAP)) {
+    target_->set_module_type(Target::EXPLICIT_MODULEMAP);
+    return true;
+  }
+
+  if (target_->all_headers_public()
+          ? !target_->source_types_used().Get(SourceFile::SOURCE_H)
+          : target_->public_headers().empty()) {
+    target_->set_module_type(Target::UNNECESSARY_MODULEMAP);
+    return true;
+  }
+
+  if (!generate_modulemap_val) {
+    return true;
+  }
+
+  generate_modulemap_val->VerifyTypeIs(Value::STRING, err_);
+  if (err_->has_error()) {
+    return false;
+  }
+  auto value = generate_modulemap_val->string_value();
+  if (value == "textual") {
+    target_->set_module_type(Target::GENERATED_TEXTUAL_MODULEMAP);
+  } else if (value != "none") {
+    *err_ = Err(*generate_modulemap_val,
+                "Invalid value for generate_modulemap. Expected \"textual\" or "
+                "\"none\"");
+    return false;
+  }
+  return true;
+}
diff --git a/src/gn/binary_target_generator.h b/src/gn/binary_target_generator.h
index b6d4c2e..51530d0 100644
--- a/src/gn/binary_target_generator.h
+++ b/src/gn/binary_target_generator.h
@@ -33,6 +33,7 @@
   bool FillPool();
   bool ValidateSources();
   bool FillModuleName();
+  bool FillModuleType();
 
   Target::OutputType output_type_;
 
diff --git a/src/gn/binary_target_generator_unittest.cc b/src/gn/binary_target_generator_unittest.cc
new file mode 100644
index 0000000..c1e4215
--- /dev/null
+++ b/src/gn/binary_target_generator_unittest.cc
@@ -0,0 +1,86 @@
+// Copyright 2026 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "gn/binary_target_generator.h"
+#include "gn/err.h"
+#include "gn/scheduler.h"
+#include "gn/target.h"
+#include "gn/test_with_scheduler.h"
+#include "gn/test_with_scope.h"
+#include "util/test/test.h"
+
+using BinaryTargetGeneratorTest = TestWithScheduler;
+
+TEST_F(BinaryTargetGeneratorTest, UnnecessaryModuleMapAllPublic) {
+  TestWithScope setup;
+  Scope::ItemVector items_;
+  setup.scope()->set_item_collector(&items_);
+  setup.scope()->set_source_dir(SourceDir("//test/"));
+
+  TestParseInput input(
+      R"(static_library("foo") {
+           generate_modulemap = "textual"
+           sources = [ "//foo.cc" ]
+         })");
+  ASSERT_FALSE(input.has_error());
+
+  Err err;
+  input.parsed()->Execute(setup.scope(), &err);
+  ASSERT_FALSE(err.has_error()) << err.message();
+
+  ASSERT_EQ(1u, items_.size());
+  Target* target = items_[0]->AsTarget();
+  ASSERT_TRUE(target);
+
+  EXPECT_EQ(Target::UNNECESSARY_MODULEMAP, target->module_type());
+}
+
+TEST_F(BinaryTargetGeneratorTest, GeneratedModuleMapAllPublic) {
+  TestWithScope setup;
+  Scope::ItemVector items_;
+  setup.scope()->set_item_collector(&items_);
+  setup.scope()->set_source_dir(SourceDir("//test/"));
+
+  TestParseInput input(
+      R"(static_library("foo") {
+           generate_modulemap = "textual"
+           sources = [ "//foo.cc", "//foo.h" ]
+         })");
+  ASSERT_FALSE(input.has_error());
+
+  Err err;
+  input.parsed()->Execute(setup.scope(), &err);
+  ASSERT_FALSE(err.has_error()) << err.message();
+
+  ASSERT_EQ(1u, items_.size());
+  Target* target = items_[0]->AsTarget();
+  ASSERT_TRUE(target);
+
+  EXPECT_EQ(Target::GENERATED_TEXTUAL_MODULEMAP, target->module_type());
+}
+
+TEST_F(BinaryTargetGeneratorTest, GeneratedModuleMap) {
+  TestWithScope setup;
+  Scope::ItemVector items_;
+  setup.scope()->set_item_collector(&items_);
+  setup.scope()->set_source_dir(SourceDir("//test/"));
+
+  TestParseInput input(
+      R"(static_library("foo") {
+           generate_modulemap = "textual"
+           sources = [ "//foo.cc" ]
+           public = ["//foo.h"]
+         })");
+  ASSERT_FALSE(input.has_error());
+
+  Err err;
+  input.parsed()->Execute(setup.scope(), &err);
+  ASSERT_FALSE(err.has_error()) << err.message();
+
+  ASSERT_EQ(1u, items_.size());
+  Target* target = items_[0]->AsTarget();
+  ASSERT_TRUE(target);
+
+  EXPECT_EQ(Target::GENERATED_TEXTUAL_MODULEMAP, target->module_type());
+}
diff --git a/src/gn/compile_commands_writer.cc b/src/gn/compile_commands_writer.cc
index bef0287..2ec76a8 100644
--- a/src/gn/compile_commands_writer.cc
+++ b/src/gn/compile_commands_writer.cc
@@ -101,13 +101,12 @@
                                           target, &ConfigValues::include_dirs,
                                           IncludeWriter(path_output));
 
-  std::vector<ClangModuleDep> module_dep_info =
+  std::set<ClangModuleDep> module_dep_info =
       GetModuleDepsInformation(target, resolved);
   if (!module_dep_info.empty()) {
     std::ostringstream module_deps_out;
     for (const auto& module_dep : module_dep_info) {
-      module_deps_out << " -fmodule-file=" << module_dep.module_name << "=";
-      path_output.WriteFile(module_deps_out, module_dep.pcm);
+      module_dep.Write(module_deps_out, path_output);
     }
     base::EscapeJSONString(module_deps_out.str(), false,
                            &flags.clang_module_deps);
@@ -115,13 +114,7 @@
     std::ostringstream module_deps_no_self_out;
     for (const auto& module_dep : module_dep_info) {
       if (!module_dep.is_self) {
-        if (module_dep.modulemap) {
-          module_deps_no_self_out << " -fmodule-map-file=";
-          path_output.WriteFile(module_deps_no_self_out, *module_dep.modulemap);
-        }
-        module_deps_no_self_out << " -fmodule-file=" << module_dep.module_name
-                                << "=";
-        path_output.WriteFile(module_deps_no_self_out, module_dep.pcm);
+        module_dep.Write(module_deps_no_self_out, path_output);
       }
     }
     base::EscapeJSONString(module_deps_no_self_out.str(), false,
diff --git a/src/gn/compile_commands_writer_unittest.cc b/src/gn/compile_commands_writer_unittest.cc
index d7fcad0..c645ac6 100644
--- a/src/gn/compile_commands_writer_unittest.cc
+++ b/src/gn/compile_commands_writer_unittest.cc
@@ -640,6 +640,7 @@
   module_target.visibility().SetPublic();
   module_target.sources().push_back(SourceFile("//foo/foo.modulemap"));
   module_target.source_types_used().Set(SourceFile::SOURCE_MODULEMAP);
+  module_target.set_module_type(Target::EXPLICIT_MODULEMAP);
   module_target.SetToolchain(&module_toolchain);
   ASSERT_TRUE(module_target.OnResolved(&err));
 
@@ -673,6 +674,7 @@
       "    \"file\": \"../../foo/dep.cc\",\r\n"
       "    \"directory\": \"out/Debug\",\r\n"
       "    \"command\": \"c++ ../../foo/dep.cc    "
+      "-fmodule-map-file=../../foo/foo.modulemap "
       "-fmodule-file=module=withmodules/obj/foo/module.foo.pcm   -o  "
       "withmodules/obj/foo/dep.dep.o\"\r\n"
       "  }\r\n"
@@ -691,6 +693,7 @@
       "    \"file\": \"../../foo/dep.cc\",\n"
       "    \"directory\": \"out/Debug\",\n"
       "    \"command\": \"c++ ../../foo/dep.cc    "
+      "-fmodule-map-file=../../foo/foo.modulemap "
       "-fmodule-file=module=withmodules/obj/foo/module.foo.pcm   -o  "
       "withmodules/obj/foo/dep.dep.o\"\n"
       "  }\n"
diff --git a/src/gn/ninja_binary_target_writer.cc b/src/gn/ninja_binary_target_writer.cc
index 412a706..13bc73d 100644
--- a/src/gn/ninja_binary_target_writer.cc
+++ b/src/gn/ninja_binary_target_writer.cc
@@ -18,6 +18,7 @@
 #include "gn/ninja_utils.h"
 #include "gn/pool.h"
 #include "gn/settings.h"
+#include "gn/string_output_buffer.h"
 #include "gn/string_utils.h"
 #include "gn/substitution_writer.h"
 #include "gn/target.h"
@@ -56,6 +57,28 @@
   writer.Run();
 }
 
+void NinjaBinaryTargetWriter::WriteModuleMap(std::ostream& out,
+                                             const SourceDir& out_dir) {
+  out << "module \"" << target_->module_name() << "\" {\n";
+  if (target_->all_headers_public()) {
+    for (const auto& header : target_->sources()) {
+      if (header.GetType() == SourceFile::SOURCE_H) {
+        out << "  textual header \"";
+        out << RebasePath(header.value(), out_dir,
+                          settings_->build_settings()->root_path_utf8());
+        out << "\"\n";
+      }
+    }
+  }
+  for (const auto& header : target_->public_headers()) {
+    out << "  textual header \"";
+    out << RebasePath(header.value(), out_dir,
+                      settings_->build_settings()->root_path_utf8());
+    out << "\"\n";
+  }
+  out << "  export *\n}\n";
+}
+
 std::vector<OutputFile>
 NinjaBinaryTargetWriter::WriteInputsStampOrPhonyAndGetDep(
     size_t num_output_uses) const {
diff --git a/src/gn/ninja_binary_target_writer.h b/src/gn/ninja_binary_target_writer.h
index b64f884..75448c0 100644
--- a/src/gn/ninja_binary_target_writer.h
+++ b/src/gn/ninja_binary_target_writer.h
@@ -21,6 +21,7 @@
   ~NinjaBinaryTargetWriter() override;
 
   void Run() override;
+  void WriteModuleMap(std::ostream& out, const SourceDir& out_dir);
 
  protected:
   // Structure used to return the classified deps from |GetDeps| method.
diff --git a/src/gn/ninja_c_binary_target_writer.cc b/src/gn/ninja_c_binary_target_writer.cc
index d9ee9fe..9041341 100644
--- a/src/gn/ninja_c_binary_target_writer.cc
+++ b/src/gn/ninja_c_binary_target_writer.cc
@@ -63,7 +63,7 @@
 NinjaCBinaryTargetWriter::~NinjaCBinaryTargetWriter() = default;
 
 void NinjaCBinaryTargetWriter::Run() {
-  std::vector<ClangModuleDep> module_dep_info =
+  std::set<ClangModuleDep> module_dep_info =
       GetModuleDepsInformation(target_, resolved());
 
   WriteCompilerVars(module_dep_info);
@@ -169,7 +169,7 @@
 }
 
 void NinjaCBinaryTargetWriter::WriteCompilerVars(
-    const std::vector<ClangModuleDep>& module_dep_info) {
+    const std::set<ClangModuleDep>& module_dep_info) {
   const SubstitutionBits& subst = target_->toolchain()->substitution_bits();
 
   WriteCCompilerVars(subst, /*indent=*/false,
@@ -204,7 +204,7 @@
 
 void NinjaCBinaryTargetWriter::WriteModuleDepsSubstitution(
     const Substitution* substitution,
-    const std::vector<ClangModuleDep>& module_dep_info,
+    const std::set<ClangModuleDep>& module_dep_info,
     bool include_self) {
   if (target_->toolchain()->substitution_bits().used.count(substitution)) {
     EscapeOptions options;
@@ -213,15 +213,7 @@
     out_ << substitution->ninja_name << " =";
     for (const auto& module_dep : module_dep_info) {
       if (!module_dep.is_self || include_self) {
-        if (module_dep.modulemap) {
-          out_ << " -fmodule-map-file=";
-          path_output_.WriteFile(out_, *module_dep.modulemap);
-        }
-        out_ << " ";
-        EscapeStringToStream(out_, "-fmodule-file=", options);
-        EscapeStringToStream(out_, module_dep.module_name, options);
-        out_ << "=";
-        path_output_.WriteFile(out_, module_dep.pcm);
+        module_dep.Write(out_, path_output_);
       }
     }
 
@@ -390,7 +382,7 @@
     const std::vector<OutputFile>& pch_deps,
     const std::vector<OutputFile>& input_deps,
     const std::vector<OutputFile>& order_only_deps,
-    const std::vector<ClangModuleDep>& module_dep_info,
+    const std::set<ClangModuleDep>& module_dep_info,
     std::vector<OutputFile>* object_files,
     std::vector<OutputFile>* extra_files,
     std::vector<SourceFile>* other_files) {
@@ -444,8 +436,8 @@
       }
 
       for (const auto& module_dep : module_dep_info) {
-        if (tool_outputs[0] != module_dep.pcm)
-          deps.push_back(module_dep.pcm);
+        if (module_dep.pcm && tool_outputs[0] != *module_dep.pcm)
+          deps.push_back(*module_dep.pcm);
       }
 
       WriteCompilerBuildLine({source}, deps, order_only_deps, tool,
diff --git a/src/gn/ninja_c_binary_target_writer.h b/src/gn/ninja_c_binary_target_writer.h
index 59cf52d..3353a1b 100644
--- a/src/gn/ninja_c_binary_target_writer.h
+++ b/src/gn/ninja_c_binary_target_writer.h
@@ -26,12 +26,12 @@
   using OutputFileSet = std::set<OutputFile>;
 
   // Writes all flags for the compiler: includes, defines, cflags, etc.
-  void WriteCompilerVars(const std::vector<ClangModuleDep>& module_dep_info);
+  void WriteCompilerVars(const std::set<ClangModuleDep>& module_dep_info);
 
   // Write module_deps or module_deps_no_self flags for clang modulemaps.
   void WriteModuleDepsSubstitution(
       const Substitution* substitution,
-      const std::vector<ClangModuleDep>& module_dep_info,
+      const std::set<ClangModuleDep>& module_dep_info,
       bool include_self);
 
   // Writes module_name substitution for clang modulemaps.
@@ -80,7 +80,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<ClangModuleDep>& module_dep_info,
+                    const std::set<ClangModuleDep>& module_dep_info,
                     std::vector<OutputFile>* object_files,
                     std::vector<OutputFile>* extra_files,
                     std::vector<SourceFile>* other_files);
diff --git a/src/gn/ninja_c_binary_target_writer_unittest.cc b/src/gn/ninja_c_binary_target_writer_unittest.cc
index d12888d..bf81b23 100644
--- a/src/gn/ninja_c_binary_target_writer_unittest.cc
+++ b/src/gn/ninja_c_binary_target_writer_unittest.cc
@@ -2271,6 +2271,7 @@
   target.sources().push_back(SourceFile("//foo/bar.modulemap"));
   target.source_types_used().Set(SourceFile::SOURCE_CPP);
   target.source_types_used().Set(SourceFile::SOURCE_MODULEMAP);
+  target.set_module_type(Target::EXPLICIT_MODULEMAP);
   ASSERT_TRUE(target.OnResolved(&err));
 
   std::ostringstream out;
@@ -2312,6 +2313,7 @@
   target.sources().push_back(SourceFile("//foo/bar.modulemap"));
   target.source_types_used().Set(SourceFile::SOURCE_CPP);
   target.source_types_used().Set(SourceFile::SOURCE_MODULEMAP);
+  target.set_module_type(Target::EXPLICIT_MODULEMAP);
   ASSERT_TRUE(target.OnResolved(&err));
 
   std::ostringstream out;
@@ -2572,6 +2574,7 @@
   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.set_module_type(Target::EXPLICIT_MODULEMAP);
   target.SetToolchain(&module_toolchain);
   ASSERT_TRUE(target.OnResolved(&err));
 
@@ -2617,6 +2620,7 @@
   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.set_module_type(Target::EXPLICIT_MODULEMAP);
   target2.public_deps().push_back(LabelTargetPair(&target));
   target2.SetToolchain(&module_toolchain);
   ASSERT_TRUE(target2.OnResolved(&err));
@@ -2632,7 +2636,7 @@
 cflags =
 cflags_cc =
 cc_module_name = b
-module_deps = -fmodule-map-file=../../blah/a.modulemap -fmodule-file=blah_a=obj/blah/liba.a.pcm -fmodule-map-file=../../stuff/b.modulemap -fmodule-file=b=obj/stuff/libb.b.pcm
+module_deps = -fmodule-map-file=../../stuff/b.modulemap -fmodule-file=b=obj/stuff/libb.b.pcm -fmodule-map-file=../../blah/a.modulemap -fmodule-file=blah_a=obj/blah/liba.a.pcm
 module_deps_no_self = -fmodule-map-file=../../blah/a.modulemap -fmodule-file=blah_a=obj/blah/liba.a.pcm
 root_out_dir = withmodules
 target_out_dir = obj/stuff
@@ -2641,7 +2645,7 @@
 build obj/stuff/libb.b.pcm: cxx_module ../../stuff/b.modulemap | obj/blah/liba.a.pcm
   source_file_part = b.modulemap
   source_name_part = b
-build obj/stuff/libb.b.o: cxx ../../stuff/b.cc | obj/blah/liba.a.pcm obj/stuff/libb.b.pcm
+build obj/stuff/libb.b.o: cxx ../../stuff/b.cc | obj/stuff/libb.b.pcm obj/blah/liba.a.pcm
   source_file_part = b.cc
   source_name_part = b
 
@@ -2660,6 +2664,7 @@
   target3.visibility().SetPublic();
   target3.sources().push_back(SourceFile("//stuff/c.modulemap"));
   target3.source_types_used().Set(SourceFile::SOURCE_MODULEMAP);
+  target3.set_module_type(Target::EXPLICIT_MODULEMAP);
   target3.public_deps().push_back(LabelTargetPair(&target2));
   target3.SetToolchain(&module_toolchain);
   ASSERT_TRUE(target3.OnResolved(&err));
@@ -2676,13 +2681,13 @@
 cflags =
 cflags_cc =
 cc_module_name = c
-module_deps = -fmodule-map-file=../../blah/a.modulemap -fmodule-file=blah_a=obj/blah/liba.a.pcm -fmodule-map-file=../../stuff/b.modulemap -fmodule-file=b=obj/stuff/libb.b.pcm -fmodule-map-file=../../stuff/c.modulemap -fmodule-file=c=obj/stuff/libc.c.pcm
-module_deps_no_self = -fmodule-map-file=../../blah/a.modulemap -fmodule-file=blah_a=obj/blah/liba.a.pcm -fmodule-map-file=../../stuff/b.modulemap -fmodule-file=b=obj/stuff/libb.b.pcm
+module_deps = -fmodule-map-file=../../stuff/b.modulemap -fmodule-file=b=obj/stuff/libb.b.pcm -fmodule-map-file=../../blah/a.modulemap -fmodule-file=blah_a=obj/blah/liba.a.pcm -fmodule-map-file=../../stuff/c.modulemap -fmodule-file=c=obj/stuff/libc.c.pcm
+module_deps_no_self = -fmodule-map-file=../../stuff/b.modulemap -fmodule-file=b=obj/stuff/libb.b.pcm -fmodule-map-file=../../blah/a.modulemap -fmodule-file=blah_a=obj/blah/liba.a.pcm
 root_out_dir = withmodules
 target_out_dir = obj/things
 target_output_name = libc
 
-build obj/stuff/libc.c.pcm: cxx_module ../../stuff/c.modulemap | obj/blah/liba.a.pcm obj/stuff/libb.b.pcm
+build obj/stuff/libc.c.pcm: cxx_module ../../stuff/c.modulemap | obj/stuff/libb.b.pcm obj/blah/liba.a.pcm
   source_file_part = c.modulemap
   source_name_part = c
 
@@ -2716,16 +2721,16 @@
 cflags =
 cflags_cc =
 cc_module_name = c
-module_deps = -fmodule-map-file=../../blah/a.modulemap -fmodule-file=blah_a=obj/blah/liba.a.pcm -fmodule-map-file=../../stuff/b.modulemap -fmodule-file=b=obj/stuff/libb.b.pcm
-module_deps_no_self = -fmodule-map-file=../../blah/a.modulemap -fmodule-file=blah_a=obj/blah/liba.a.pcm -fmodule-map-file=../../stuff/b.modulemap -fmodule-file=b=obj/stuff/libb.b.pcm
+module_deps = -fmodule-map-file=../../stuff/b.modulemap -fmodule-file=b=obj/stuff/libb.b.pcm -fmodule-map-file=../../blah/a.modulemap -fmodule-file=blah_a=obj/blah/liba.a.pcm
+module_deps_no_self = -fmodule-map-file=../../stuff/b.modulemap -fmodule-file=b=obj/stuff/libb.b.pcm -fmodule-map-file=../../blah/a.modulemap -fmodule-file=blah_a=obj/blah/liba.a.pcm
 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.x.o: cxx ../../zap/x.cc | obj/stuff/libb.b.pcm obj/blah/liba.a.pcm
   source_file_part = x.cc
   source_name_part = x
-build obj/zap/c.y.o: cxx ../../zap/y.cc | obj/blah/liba.a.pcm obj/stuff/libb.b.pcm
+build obj/zap/c.y.o: cxx ../../zap/y.cc | obj/stuff/libb.b.pcm obj/blah/liba.a.pcm
   source_file_part = y.cc
   source_name_part = y
 
@@ -2911,3 +2916,88 @@
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str) << expected << "\n" << out_str;
 }
+
+TEST_F(NinjaCBinaryTargetWriterTest, ModuleMapGeneration) {
+  Err err;
+  TestWithScope setup;
+
+  // Let's create a target and give it public headers.
+  Target target(setup.settings(), Label(SourceDir("//foo/"), "bar"));
+  target.set_output_type(Target::SOURCE_SET);
+  target.visibility().SetPublic();
+  target.sources().push_back(SourceFile("//foo/source1.cc"));
+  target.public_headers().push_back(SourceFile("//foo/public_header.h"));
+  target.source_types_used().Set(SourceFile::SOURCE_CPP);
+  target.set_module_type(Target::GENERATED_TEXTUAL_MODULEMAP);
+  target.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  std::ostringstream ninja_out;
+  NinjaCBinaryTargetWriter writer(&target, ninja_out);
+
+  std::ostringstream modulemap_out;
+  SourceDir out_dir("//out/Debug/gen/");
+
+  writer.WriteModuleMap(modulemap_out, out_dir);
+
+  const char expected_modulemap[] =
+      "module \"bar\" {\n"
+      "  textual header \"../../../foo/public_header.h\"\n"
+      "  export *\n"
+      "}\n";
+
+  std::string modulemap_str = modulemap_out.str();
+  EXPECT_EQ(expected_modulemap, modulemap_str) << expected_modulemap << "\n"
+                                               << modulemap_str;
+
+  const char expected_ninja[] =
+      "defines =\n"
+      "include_dirs =\n"
+      "cflags =\n"
+      "cflags_cc =\n"
+      "root_out_dir = .\n"
+      "target_gen_dir = gen/foo\n"
+      "target_out_dir = obj/foo\n"
+      "target_output_name = bar\n"
+      "\n"
+      "build obj/foo/bar.source1.o: cxx ../../foo/source1.cc\n"
+      "  source_file_part = source1.cc\n"
+      "  source_name_part = source1\n"
+      "\n"
+      "build phony/foo/bar: phony obj/foo/bar.source1.o\n";
+  writer.Run();
+  std::string ninja_str = ninja_out.str();
+  EXPECT_EQ(expected_ninja, ninja_str) << expected_ninja << "\n" << ninja_str;
+
+  // Test generation without explicit public headers (uses sources instead)
+  Target target_no_public(setup.settings(),
+                          Label(SourceDir("//foo/"), "no_public"));
+  target_no_public.set_output_type(Target::SOURCE_SET);
+  target_no_public.visibility().SetPublic();
+  target_no_public.sources().push_back(SourceFile("//foo/source1.cc"));
+  target_no_public.sources().push_back(SourceFile("//foo/header1.h"));
+  target_no_public.source_types_used().Set(SourceFile::SOURCE_CPP);
+  target_no_public.source_types_used().Set(SourceFile::SOURCE_H);
+  target_no_public.set_module_type(Target::GENERATED_TEXTUAL_MODULEMAP);
+  target_no_public.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(target_no_public.OnResolved(&err));
+
+  std::ostringstream ninja_out_no_public;
+  NinjaCBinaryTargetWriter writer_no_public(&target_no_public,
+                                            ninja_out_no_public);
+
+  std::ostringstream modulemap_out_no_public;
+
+  writer_no_public.WriteModuleMap(modulemap_out_no_public, out_dir);
+
+  const char expected_modulemap_no_public[] =
+      "module \"no_public\" {\n"
+      "  textual header \"../../../foo/header1.h\"\n"
+      "  export *\n"
+      "}\n";
+
+  std::string modulemap_str_no_public = modulemap_out_no_public.str();
+  EXPECT_EQ(expected_modulemap_no_public, modulemap_str_no_public)
+      << expected_modulemap_no_public << "\n"
+      << modulemap_str_no_public;
+}
diff --git a/src/gn/ninja_module_writer_util.cc b/src/gn/ninja_module_writer_util.cc
index 67b40eb..7c8b0ca 100644
--- a/src/gn/ninja_module_writer_util.cc
+++ b/src/gn/ninja_module_writer_util.cc
@@ -6,56 +6,67 @@
 
 #include <algorithm>
 #include <set>
+#include <utility>
 
 #include "gn/resolved_target_data.h"
 #include "gn/substitution_writer.h"
 #include "gn/target.h"
 
-namespace {
-
-// Returns the first source file in the target's sources that is a modulemap
-// file. Returns nullptr if no modulemap file is found.
-const SourceFile* GetModuleMapFromTargetSources(const Target* target) {
-  for (const SourceFile& sf : target->sources()) {
-    if (sf.IsModuleMapType())
-      return &sf;
-  }
-  return nullptr;
-}
-
-}  // namespace
-
 ClangModuleDep::ClangModuleDep(const SourceFile* modulemap,
                                const std::string& module_name,
-                               const OutputFile& pcm,
+                               std::optional<OutputFile> pcm,
                                bool is_self)
     : modulemap(modulemap),
       module_name(module_name),
-      pcm(pcm),
+      pcm(std::move(pcm)),
       is_self(is_self) {}
 
-std::vector<ClangModuleDep> GetModuleDepsInformation(
+std::strong_ordering ClangModuleDep::operator<=>(
+    const ClangModuleDep& other) const {
+  // Sort by (module name, modulemap path, module file path)
+  if (auto cmp = module_name <=> other.module_name; cmp != 0)
+    return cmp;
+  if (modulemap && other.modulemap) {
+    if (auto cmp = *modulemap <=> *other.modulemap; cmp != 0)
+      return cmp;
+  } else {
+    if (auto cmp = modulemap <=> other.modulemap; cmp != 0)
+      return cmp;
+  }
+  // std::optional doesn't support <=> on older versions of mac.
+  if (pcm.has_value() && other.pcm.has_value()) {
+    return *pcm <=> *other.pcm;
+  } else {
+    return pcm.has_value() <=> other.pcm.has_value();
+  }
+}
+
+std::set<ClangModuleDep> GetModuleDepsInformation(
     const Target* target,
     const ResolvedTargetData& resolved) {
-  std::vector<ClangModuleDep> ret;
-  // Use a set to keep track of added PCM files to ensure uniqueness.
-  std::set<OutputFile> added_pcms;
+  std::set<ClangModuleDep> ret;
 
-  auto add_if_new = [&added_pcms, &ret](const Target* t, bool is_self) {
-    const SourceFile* modulemap = GetModuleMapFromTargetSources(t);
-    if (!modulemap)  // Not a module or no .modulemap file.
-      return;
-
-    const char* tool_type;
-    std::vector<OutputFile> modulemap_outputs;
-    CHECK(
-        t->GetOutputFilesForSource(*modulemap, &tool_type, &modulemap_outputs));
-    CHECK(modulemap_outputs.size() == 1u);  // Must be only one .pcm.
-    const OutputFile& pcm_file = modulemap_outputs[0];
-
-    if (added_pcms.insert(pcm_file).second) {
-      // GN sets the module name to the name of the target.
-      ret.emplace_back(modulemap, t->module_name(), pcm_file, is_self);
+  auto add_if_new = [&ret](const Target* t, bool is_self) {
+    std::optional<OutputFile> pcm = std::nullopt;
+    switch (t->module_type()) {
+      case Target::GENERATED_TEXTUAL_MODULEMAP:
+        ret.emplace(t->modulemap_file(), t->module_name(), std::nullopt,
+                    is_self);
+        break;
+      case Target::EXPLICIT_MODULEMAP: {
+        auto modulemap = t->modulemap_file();
+        CHECK(modulemap != nullptr);
+        const char* tool_type;
+        std::vector<OutputFile> modulemap_outputs;
+        CHECK(t->GetOutputFilesForSource(*modulemap, &tool_type,
+                                         &modulemap_outputs));
+        CHECK(modulemap_outputs.size() == 1u);
+        ret.emplace(modulemap, t->module_name(),
+                    std::move(modulemap_outputs[0]), is_self);
+        break;
+      }
+      default:
+        break;
     }
   };
 
@@ -65,11 +76,17 @@
   for (const auto& pair : resolved.GetModuleDepsInformation(target))
     add_if_new(pair.target(), false);
 
-  // Sort by pcm path for deterministic output.
-  std::sort(ret.begin(), ret.end(),
-            [](const ClangModuleDep& a, const ClangModuleDep& b) {
-              return a.pcm < b.pcm;
-            });
-
   return ret;
 }
+
+void ClangModuleDep::Write(std::ostream& out,
+                           const PathOutput& path_output) const {
+  if (modulemap) {
+    out << " -fmodule-map-file=";
+    path_output.WriteFile(out, *modulemap);
+  }
+  if (pcm) {
+    out << " -fmodule-file=" << module_name << "=";
+    path_output.WriteFile(out, *pcm);
+  }
+}
\ No newline at end of file
diff --git a/src/gn/ninja_module_writer_util.h b/src/gn/ninja_module_writer_util.h
index f6d007b..911a98f 100644
--- a/src/gn/ninja_module_writer_util.h
+++ b/src/gn/ninja_module_writer_util.h
@@ -5,10 +5,13 @@
 #ifndef TOOLS_GN_NINJA_MODULE_WRITER_UTIL_H_
 #define TOOLS_GN_NINJA_MODULE_WRITER_UTIL_H_
 
+#include <compare>
+#include <optional>
+#include <set>
 #include <string>
-#include <vector>
 
 #include "gn/output_file.h"
+#include "gn/path_output.h"
 
 class ResolvedTargetData;
 class SourceFile;
@@ -17,9 +20,13 @@
 struct ClangModuleDep {
   ClangModuleDep(const SourceFile* modulemap,
                  const std::string& module_name,
-                 const OutputFile& pcm,
+                 std::optional<OutputFile> pcm,
                  bool is_self);
 
+  std::strong_ordering operator<=>(const ClangModuleDep& other) const;
+  bool operator==(const ClangModuleDep& other) const = default;
+  void Write(std::ostream& out, const PathOutput& path_output) const;
+
   // The input module.modulemap source file.
   const SourceFile* modulemap;
 
@@ -27,14 +34,15 @@
   std::string module_name;
 
   // The compiled version of the module.
-  OutputFile pcm;
+  // Will be nullopt if the modulemap is purely textual.
+  std::optional<OutputFile> pcm;
 
   // Is this the module for the current target.
   bool is_self;
 };
 
 // Gathers information about all module dependencies for a given target.
-std::vector<ClangModuleDep> GetModuleDepsInformation(
+std::set<ClangModuleDep> GetModuleDepsInformation(
     const Target* target,
     const ResolvedTargetData& resolved);
 
diff --git a/src/gn/ninja_target_writer.cc b/src/gn/ninja_target_writer.cc
index d6c69c7..46964ee 100644
--- a/src/gn/ninja_target_writer.cc
+++ b/src/gn/ninja_target_writer.cc
@@ -166,6 +166,17 @@
     NinjaBinaryTargetWriter writer(target, rules);
     writer.SetResolvedTargetData(resolved);
     writer.SetNinjaOutputs(ninja_outputs);
+    if (target->module_type() == Target::GENERATED_TEXTUAL_MODULEMAP) {
+      const SourceFile* modulemap = target->modulemap_file();
+      CHECK(modulemap);
+      StringOutputBuffer modulemap_storage;
+      std::ostream os(&modulemap_storage);
+      writer.WriteModuleMap(os, modulemap->GetDir());
+
+      base::FilePath file_path =
+          settings->build_settings()->GetFullPath(*modulemap);
+      modulemap_storage.WriteToFileIfChanged(file_path, nullptr);
+    }
     writer.Run();
   } else {
     CHECK(0) << "Output type of target not handled.";
diff --git a/src/gn/output_file.h b/src/gn/output_file.h
index d8d69ff..e3e79a8 100644
--- a/src/gn/output_file.h
+++ b/src/gn/output_file.h
@@ -34,15 +34,10 @@
   SourceFile AsSourceFile(const BuildSettings* build_settings) const;
   SourceDir AsSourceDir(const BuildSettings* build_settings) const;
 
-  bool operator==(const OutputFile& other) const {
-    return value_ == other.value_;
-  }
-  bool operator!=(const OutputFile& other) const {
-    return value_ != other.value_;
-  }
-  bool operator<(const OutputFile& other) const {
-    return value_ < other.value_;
-  }
+  bool operator==(const OutputFile& other) const = default;
+  bool operator!=(const OutputFile& other) const = default;
+  bool operator<(const OutputFile& other) const = default;
+  std::strong_ordering operator<=>(const OutputFile& other) const = default;
 
  private:
   std::string value_;
diff --git a/src/gn/source_file.h b/src/gn/source_file.h
index 467c119..f682f0d 100644
--- a/src/gn/source_file.h
+++ b/src/gn/source_file.h
@@ -104,6 +104,13 @@
   bool operator<(const SourceFile& other) const {
     return value_ < other.value_;
   }
+  // Needs to be overridden because == has custom logic.
+  std::strong_ordering operator<=>(const SourceFile& other) const {
+    if (*this == other)
+      return std::strong_ordering::equal;
+    return *this < other ? std::strong_ordering::less
+                         : std::strong_ordering::greater;
+  }
 
   struct PtrCompare {
     bool operator()(const SourceFile& a, const SourceFile& b) const noexcept {
diff --git a/src/gn/target.cc b/src/gn/target.cc
index b66c636..f2130e6 100644
--- a/src/gn/target.cc
+++ b/src/gn/target.cc
@@ -1392,3 +1392,36 @@
                  std::make_move_iterator(current_result.end()));
   return true;
 }
+
+void Target::set_module_type(ModuleType type) {
+  module_type_ = type;
+  switch (type) {
+    case GENERATED_TEXTUAL_MODULEMAP: {
+      auto source_dir =
+          GetBuildDirForTargetAsOutputFile(this, BuildDirType::GEN)
+              .AsSourceDir(settings()->build_settings());
+
+      generated_modulemap_file_ = SourceFile(
+          base::StringPrintf("%s%s.modulemap", source_dir.value().c_str(),
+                             label().name().c_str()));
+      break;
+    }
+    default:
+      break;
+  }
+}
+
+const SourceFile* Target::modulemap_file() const {
+  switch (module_type_) {
+    case GENERATED_TEXTUAL_MODULEMAP:
+      return &generated_modulemap_file_;
+    case EXPLICIT_MODULEMAP:
+      for (const SourceFile& sf : sources_) {
+        if (sf.IsModuleMapType()) {
+          return &sf;
+        }
+      }
+    default:
+      return nullptr;
+  }
+}
diff --git a/src/gn/target.h b/src/gn/target.h
index eb9e0b9..f219402 100644
--- a/src/gn/target.h
+++ b/src/gn/target.h
@@ -59,6 +59,14 @@
     DEPS_LINKED,  // Iterates through all non-data dependencies.
   };
 
+  enum ModuleType {
+    NO_MODULEMAP,
+    EXPLICIT_MODULEMAP,
+    // The target didn't have any public headers, so no modulemap is needed.
+    UNNECESSARY_MODULEMAP,
+    GENERATED_TEXTUAL_MODULEMAP,
+  };
+
   using FileList = std::vector<SourceFile>;
   using StringVector = std::vector<std::string>;
 
@@ -348,6 +356,10 @@
     return assert_no_deps_;
   }
 
+  ModuleType module_type() const { return module_type_; }
+  void set_module_type(ModuleType type);
+  const SourceFile* modulemap_file() const;
+
   // The toolchain is only known once this target is resolved (all if its
   // dependencies are known). They will be null until then. Generally, this can
   // only be used during target writing.
@@ -526,6 +538,9 @@
   bool output_extension_set_ = false;
 
   std::string module_name_override_;
+  ModuleType module_type_ = NO_MODULEMAP;
+  // Only filled if the module type is GENERATED_*
+  SourceFile generated_modulemap_file_;
 
   FileList sources_;
   SourceFileTypeSet source_types_used_;
diff --git a/src/gn/variables.cc b/src/gn/variables.cc
index 5090e3a..ad9657f 100644
--- a/src/gn/variables.cc
+++ b/src/gn/variables.cc
@@ -405,6 +405,18 @@
   }
 )";
 
+const char kGenerateModulemap[] = "generate_modulemap";
+const char kGenerateModulemap_HelpShort[] =
+    "generate_modulemap: [string] Mode for generating modulemaps.";
+const char kGenerateModulemap_Help[] =
+    R"(generate_modulemap: [string] Mode for generating modulemaps.
+
+Possible values:
+  "none" (default): Don't generate a modulemap file for the target.
+  "textual": Generate a modulemap file for the target.
+    All public headers will be marked as textual.
+)";
+
 // Target variables ------------------------------------------------------------
 
 #define COMMON_ORDERING_HELP                                                 \
@@ -2429,6 +2441,7 @@
     INSERT_VARIABLE(CurrentOs)
     INSERT_VARIABLE(CurrentToolchain)
     INSERT_VARIABLE(DefaultToolchain)
+    INSERT_VARIABLE(GenerateModulemap)
     INSERT_VARIABLE(GnVersion)
     INSERT_VARIABLE(HostCpu)
     INSERT_VARIABLE(HostOs)
diff --git a/src/gn/variables.h b/src/gn/variables.h
index 1d953af..6820121 100644
--- a/src/gn/variables.h
+++ b/src/gn/variables.h
@@ -80,6 +80,10 @@
 extern const char kTargetOutDir_HelpShort[];
 extern const char kTargetOutDir_Help[];
 
+extern const char kGenerateModulemap[];
+extern const char kGenerateModulemap_HelpShort[];
+extern const char kGenerateModulemap_Help[];
+
 // Target vars -----------------------------------------------------------------
 
 extern const char kAllDependentConfigs[];