Switch source_set stamp targets to phony targets

GN uses stamp files for a variety of output types as a mechanism for
completing a group of dependencies. Ninja's phony targets can be used in
the same manner. Semantically, stamp and phony only differ in one way:
if a phony target has no dependencies, then it is treated as always
dirty and is run every time.

The source_set output type always generates a stamp file in the target
ninja file. This CL replaces the source_set's stamp target with a phony
target. In the case where the source_set's new phony target does not
have any inputs, no targets will depend on it. An example of where that
might happen is a header only source_set. There are no object files to
generate, but targets that depend on the header only source_set will
generate dependencies on those headers through depfiles.

Bug: 187
Change-Id: I159f2f39a988eea189e4df9f5d9207fda6db5c58
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/9820
Reviewed-by: Brett Wilson <brettw@chromium.org>
Commit-Queue: Brett Wilson <brettw@chromium.org>
diff --git a/build/gen.py b/build/gen.py
index 486354b..a16f16f 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -504,6 +504,7 @@
         'src/gn/bundle_data.cc',
         'src/gn/bundle_data_target_generator.cc',
         'src/gn/bundle_file_rule.cc',
+        'src/gn/builtin_tool.cc',
         'src/gn/c_include_iterator.cc',
         'src/gn/c_substitution_type.cc',
         'src/gn/c_tool.cc',
diff --git a/src/gn/builtin_tool.cc b/src/gn/builtin_tool.cc
new file mode 100644
index 0000000..8bc1e52
--- /dev/null
+++ b/src/gn/builtin_tool.cc
@@ -0,0 +1,42 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/logging.h"
+#include "gn/builtin_tool.h"
+#include "gn/target.h"
+
+const char* BuiltinTool::kBuiltinToolPhony = "phony";
+
+BuiltinTool::BuiltinTool(const char* n) : Tool(n) {
+  CHECK(ValidateName(n));
+}
+
+BuiltinTool::~BuiltinTool() = default;
+
+BuiltinTool* BuiltinTool::AsBuiltin() {
+  return this;
+}
+const BuiltinTool* BuiltinTool::AsBuiltin() const {
+  return this;
+}
+
+bool BuiltinTool::ValidateName(const char* name) const {
+  return name == kBuiltinToolPhony;
+}
+
+void BuiltinTool::SetComplete() {
+  SetToolComplete();
+}
+
+bool BuiltinTool::InitTool(Scope* scope, Toolchain* toolchain, Err* err) {
+  // Initialize default vars.
+  return Tool::InitTool(scope, toolchain, err);
+}
+
+bool BuiltinTool::ValidateSubstitution(const Substitution* sub_type) const {
+  if (name_ == kBuiltinToolPhony)
+    return IsValidToolSubstitution(sub_type);
+  NOTREACHED();
+  return false;
+}
diff --git a/src/gn/builtin_tool.h b/src/gn/builtin_tool.h
new file mode 100644
index 0000000..00cc809
--- /dev/null
+++ b/src/gn/builtin_tool.h
@@ -0,0 +1,39 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef TOOLS_GN_BUILTIN_TOOL_H_
+#define TOOLS_GN_BUILTIN_TOOL_H_
+
+#include <string>
+
+#include "base/macros.h"
+#include "gn/substitution_list.h"
+#include "gn/substitution_pattern.h"
+#include "gn/tool.h"
+
+// A built-in tool that is always available regardless of toolchain. So far, the
+// only example of this is the phony rule that ninja provides.
+class BuiltinTool : public Tool {
+ public:
+  // Builtin tools
+  static const char* kBuiltinToolPhony;
+
+  explicit BuiltinTool(const char* n);
+  ~BuiltinTool();
+
+  // Manual RTTI and required functions ---------------------------------------
+
+  bool InitTool(Scope* block_scope, Toolchain* toolchain, Err* err);
+  bool ValidateName(const char* name) const override;
+  void SetComplete() override;
+  bool ValidateSubstitution(const Substitution* sub_type) const override;
+
+  BuiltinTool* AsBuiltin() override;
+  const BuiltinTool* AsBuiltin() const override;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(BuiltinTool);
+};
+
+#endif  // TOOLS_GN_BUILTIN_TOOL_H_
diff --git a/src/gn/commands.cc b/src/gn/commands.cc
index 97d2629..1d2b899 100644
--- a/src/gn/commands.cc
+++ b/src/gn/commands.cc
@@ -328,8 +328,13 @@
     // Use the link output file if there is one, otherwise fall back to the
     // dependency output file (for actions, for example).
     OutputFile output_file = target->link_output_file();
+    if (output_file.value().empty() && target->dependency_output_file_or_phony())
+      output_file = *target->dependency_output_file_or_phony();
+
+    // This output might be an omitted phony target, but that would mean we
+    // don't have an output file to list.
     if (output_file.value().empty())
-      output_file = target->dependency_output_file();
+      continue;
 
     SourceFile output_as_source = output_file.AsSourceFile(build_settings);
     std::string result =
diff --git a/src/gn/filesystem_utils.cc b/src/gn/filesystem_utils.cc
index ae6d4d9..ab06545 100644
--- a/src/gn/filesystem_utils.cc
+++ b/src/gn/filesystem_utils.cc
@@ -1028,6 +1028,8 @@
     result.value().append("gen/");
   else if (type == BuildDirType::OBJ)
     result.value().append("obj/");
+  else if (type == BuildDirType::PHONY)
+    result.value().append("phony/");
   return result;
 }
 
diff --git a/src/gn/filesystem_utils.h b/src/gn/filesystem_utils.h
index 830478a..9ba3b4e 100644
--- a/src/gn/filesystem_utils.h
+++ b/src/gn/filesystem_utils.h
@@ -232,6 +232,12 @@
 
   // Output file directory.
   OBJ,
+
+  // Phony file directory. As the name implies, this is not a real file
+  // directory, but a path that is used for the declaration of phony targets.
+  // This is done to avoid duplicate target names between real files and phony
+  // aliases that point to them.
+  PHONY,
 };
 
 // In different contexts, different information is known about the toolchain in
diff --git a/src/gn/function_get_target_outputs.cc b/src/gn/function_get_target_outputs.cc
index fa851f3..ca37d9a 100644
--- a/src/gn/function_get_target_outputs.cc
+++ b/src/gn/function_get_target_outputs.cc
@@ -46,8 +46,8 @@
   process_file_template").
 
   source sets and groups: this will return a list containing the path of the
-  "stamp" file that Ninja will produce once all outputs are generated. This
-  probably isn't very useful.
+  phony target or the "stamp" file that Ninja will produce once all outputs are
+  generated. This probably isn't very useful.
 
 Example
 
diff --git a/src/gn/ninja_action_target_writer.cc b/src/gn/ninja_action_target_writer.cc
index 28f28c7..0b49066 100644
--- a/src/gn/ninja_action_target_writer.cc
+++ b/src/gn/ninja_action_target_writer.cc
@@ -89,8 +89,10 @@
   // TODO(thakis): If the action has just a single output, make things depend
   // on that output directly without writing a stamp file.
   std::vector<OutputFile> data_outs;
-  for (const auto& dep : target_->data_deps())
-    data_outs.push_back(dep.ptr->dependency_output_file());
+  for (const auto& dep : target_->data_deps()) {
+    if (dep.ptr->dependency_output_file_or_phony())
+      data_outs.push_back(*dep.ptr->dependency_output_file_or_phony());
+  }
   WriteStampForTarget(output_files, data_outs);
 }
 
diff --git a/src/gn/ninja_binary_target_writer.cc b/src/gn/ninja_binary_target_writer.cc
index 08e8c93..e06700a 100644
--- a/src/gn/ninja_binary_target_writer.cc
+++ b/src/gn/ninja_binary_target_writer.cc
@@ -102,9 +102,9 @@
   return {stamp_file};
 }
 
-void NinjaBinaryTargetWriter::WriteSourceSetStamp(
+void NinjaBinaryTargetWriter::WriteSourceSetPhony(
     const std::vector<OutputFile>& object_files) {
-  // The stamp rule for source sets is generally not used, since targets that
+  // The phony rule for source sets is generally not used, since targets that
   // depend on this will reference the object files directly. However, writing
   // this rule allows the user to type the name of the target and get a build
   // which can be convenient for development.
@@ -116,10 +116,12 @@
   DCHECK(classified_deps.extra_object_files.empty());
 
   std::vector<OutputFile> order_only_deps;
-  for (auto* dep : classified_deps.non_linkable_deps)
-    order_only_deps.push_back(dep->dependency_output_file());
+  for (auto* dep : classified_deps.non_linkable_deps) {
+    if (dep->dependency_output_file_or_phony())
+      order_only_deps.push_back(*dep->dependency_output_file_or_phony());
+  }
 
-  WriteStampForTarget(object_files, order_only_deps);
+  WritePhonyForTarget(object_files, order_only_deps);
 }
 
 NinjaBinaryTargetWriter::ClassifiedDeps
@@ -177,7 +179,7 @@
       AddSourceSetFiles(dep, &classified_deps->extra_object_files);
 
     // Add the source set itself as a non-linkable dependency on the current
-    // target. This will make sure that anything the source set's stamp file
+    // target. This will make sure that anything the source set's phony target
     // depends on (like data deps) are also built before the current target
     // can be complete. Otherwise, these will be skipped since this target
     // will depend only on the source set's object files.
diff --git a/src/gn/ninja_binary_target_writer.h b/src/gn/ninja_binary_target_writer.h
index 76a8a4e..eb75c89 100644
--- a/src/gn/ninja_binary_target_writer.h
+++ b/src/gn/ninja_binary_target_writer.h
@@ -43,8 +43,8 @@
   std::vector<OutputFile> WriteInputsStampAndGetDep(
       size_t num_stamp_uses) const;
 
-  // Writes the stamp line for a source set. These are not linked.
-  void WriteSourceSetStamp(const std::vector<OutputFile>& object_files);
+  // Writes the phony line for a source set. These are not linked.
+  void WriteSourceSetPhony(const std::vector<OutputFile>& object_files);
 
   // Gets all target dependencies and classifies them, as well as accumulates
   // object files from source sets we need to link.
diff --git a/src/gn/ninja_binary_target_writer_unittest.cc b/src/gn/ninja_binary_target_writer_unittest.cc
index 970aa82..9e4037c 100644
--- a/src/gn/ninja_binary_target_writer_unittest.cc
+++ b/src/gn/ninja_binary_target_writer_unittest.cc
@@ -44,7 +44,7 @@
       "build obj/foo/bar.input1.o: cxx ../../foo/input1.cc\n"
       "build obj/foo/bar.input2.o: cxx ../../foo/input2.cc\n"
       "\n"
-      "build obj/foo/bar.stamp: stamp obj/foo/bar.input1.o "
+      "build phony/foo/bar: phony obj/foo/bar.input1.o "
       "obj/foo/bar.input2.o ../../foo/input3.o ../../foo/input4.obj\n";
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
@@ -71,8 +71,7 @@
       "target_out_dir = obj/foo\n"
       "target_output_name = bar\n"
       "\n"
-      "\n"
-      "build obj/foo/bar.stamp: stamp\n";
+      "\n";
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
 }
@@ -138,7 +137,7 @@
         "build obj/foo/bar.source1.o: cxx ../../foo/source1.cc | "
         "../../foo/input1 ../../foo/input2\n"
         "\n"
-        "build obj/foo/bar.stamp: stamp obj/foo/bar.source1.o\n";
+        "build phony/foo/bar: phony obj/foo/bar.source1.o\n";
     std::string out_str = out.str();
     EXPECT_EQ(expected, out_str) << expected << "\n" << out_str;
   }
@@ -175,7 +174,7 @@
         "build obj/foo/bar.source2.o: cxx ../../foo/source2.cc | "
         "obj/foo/bar.inputs.stamp\n"
         "\n"
-        "build obj/foo/bar.stamp: stamp obj/foo/bar.source1.o "
+        "build phony/foo/bar: phony obj/foo/bar.source1.o "
         "obj/foo/bar.source2.o\n";
     std::string out_str = out.str();
     EXPECT_EQ(expected, out_str) << expected << "\n" << out_str;
diff --git a/src/gn/ninja_build_writer.cc b/src/gn/ninja_build_writer.cc
index f801fb6..50f1155 100644
--- a/src/gn/ninja_build_writer.cc
+++ b/src/gn/ninja_build_writer.cc
@@ -595,8 +595,11 @@
     EscapeOptions ninja_escape;
     ninja_escape.mode = ESCAPE_NINJA;
     for (const Target* target : default_toolchain_targets_) {
-      out_ << " $\n    ";
-      path_output_.WriteFile(out_, target->dependency_output_file());
+      if (target->dependency_output_file_or_phony()) {
+        out_ << " $\n    ";
+        path_output_.WriteFile(out_,
+                               *target->dependency_output_file_or_phony());
+      }
     }
   }
   out_ << std::endl;
@@ -605,9 +608,13 @@
     // Use the short name when available
     if (written_rules.find("default") != written_rules.end()) {
       out_ << "\ndefault default" << std::endl;
-    } else {
+    } else if (default_target->dependency_output_file_or_phony()) {
+      // If the default target does not have a dependency output file or phony,
+      // then the target specified as default is a no-op. We omit the default
+      // statement entirely to avoid ninja runtime failure.
       out_ << "\ndefault ";
-      path_output_.WriteFile(out_, default_target->dependency_output_file());
+      path_output_.WriteFile(
+          out_, *default_target->dependency_output_file_or_phony());
       out_ << std::endl;
     }
   } else if (!default_toolchain_targets_.empty()) {
@@ -625,7 +632,12 @@
   // Escape for special chars Ninja will handle.
   std::string escaped = EscapeString(phony_name, ninja_escape, nullptr);
 
+  // If the target doesn't have a dependency_output_file_or_phony, we should
+  // still emit the phony rule, but with no dependencies. This allows users to
+  // continue to use the phony rule, but it will effectively be a no-op.
   out_ << "build " << escaped << ": phony ";
-  path_output_.WriteFile(out_, target->dependency_output_file());
+  if (target->dependency_output_file_or_phony()) {
+    path_output_.WriteFile(out_, *target->dependency_output_file_or_phony());
+  }
   out_ << std::endl;
 }
diff --git a/src/gn/ninja_bundle_data_target_writer.cc b/src/gn/ninja_bundle_data_target_writer.cc
index 0e3bcb0..239c0c2 100644
--- a/src/gn/ninja_bundle_data_target_writer.cc
+++ b/src/gn/ninja_bundle_data_target_writer.cc
@@ -26,8 +26,10 @@
   output_files.insert(output_files.end(), input_deps.begin(), input_deps.end());
 
   std::vector<OutputFile> order_only_deps;
-  for (const auto& pair : target_->data_deps())
-    order_only_deps.push_back(pair.ptr->dependency_output_file());
+  for (const auto& pair : target_->data_deps()) {
+    if (pair.ptr->dependency_output_file_or_phony())
+      order_only_deps.push_back(*pair.ptr->dependency_output_file_or_phony());
+  }
 
   WriteStampForTarget(output_files, order_only_deps);
 }
diff --git a/src/gn/ninja_c_binary_target_writer.cc b/src/gn/ninja_c_binary_target_writer.cc
index 17f9c08..2e7e613 100644
--- a/src/gn/ninja_c_binary_target_writer.cc
+++ b/src/gn/ninja_c_binary_target_writer.cc
@@ -207,7 +207,7 @@
     return;
 
   if (target_->output_type() == Target::SOURCE_SET) {
-    WriteSourceSetStamp(obj_files);
+    WriteSourceSetPhony(obj_files);
 #ifndef NDEBUG
     // Verify that the function that separately computes a source set's object
     // files match the object files just computed.
@@ -667,8 +667,11 @@
     swift_order_only_deps.Append(order_only_deps.begin(),
                                  order_only_deps.end());
 
-    for (const Target* swiftmodule : target_->swift_values().modules())
-      swift_order_only_deps.push_back(swiftmodule->dependency_output_file());
+    for (const Target* swiftmodule : target_->swift_values().modules()) {
+      CHECK(swiftmodule->dependency_output_file_or_phony());
+      swift_order_only_deps.push_back(
+          *swiftmodule->dependency_output_file_or_phony());
+    }
 
     WriteCompilerBuildLine(target_->sources(), input_deps,
                            swift_order_only_deps.vector(), tool->name(),
@@ -719,11 +722,12 @@
         cur->output_type() == Target::RUST_PROC_MACRO)
       continue;
 
-    if (cur->dependency_output_file().value() !=
-        cur->link_output_file().value()) {
+    if (cur->dependency_output_file_or_phony() &&
+        (cur->dependency_output_file_or_phony()->value() !=
+         cur->link_output_file().value())) {
       // This is a shared library with separate link and deps files. Save for
       // later.
-      implicit_deps.push_back(cur->dependency_output_file());
+      implicit_deps.push_back(*cur->dependency_output_file_or_phony());
       solibs.push_back(cur->link_output_file());
     } else {
       // Normal case, just link to this target.
@@ -754,12 +758,13 @@
   }
 
   // If any target creates a framework bundle, then treat it as an implicit
-  // dependency via the .stamp file. This is a pessimisation as it is not
+  // dependency via the phony target. This is a pessimisation as it is not
   // always necessary to relink the current target if one of the framework
   // is regenerated, but it ensure that if one of the framework API changes,
   // any dependent target will relink it (see crbug.com/1037607).
   for (const Target* dep : classified_deps.framework_deps) {
-    implicit_deps.push_back(dep->dependency_output_file());
+    if (dep->dependency_output_file_or_phony())
+      implicit_deps.push_back(*dep->dependency_output_file_or_phony());
   }
 
   // The input dependency is only needed if there are no object files, as the
@@ -774,8 +779,9 @@
     for (const auto* dep :
          target_->rust_values().transitive_libs().GetOrdered()) {
       if (dep->output_type() == Target::RUST_LIBRARY) {
-        transitive_rustlibs.push_back(dep->dependency_output_file());
-        implicit_deps.push_back(dep->dependency_output_file());
+        CHECK(dep->dependency_output_file());
+        transitive_rustlibs.push_back(*dep->dependency_output_file());
+        implicit_deps.push_back(*dep->dependency_output_file());
       }
     }
   }
@@ -873,8 +879,11 @@
 
     // Non-linkable targets.
     for (auto* non_linkable_dep : non_linkable_deps) {
-      out_ << " ";
-      path_output_.WriteFile(out_, non_linkable_dep->dependency_output_file());
+      if (non_linkable_dep->dependency_output_file_or_phony()) {
+        out_ << " ";
+        path_output_.WriteFile(
+            out_, *non_linkable_dep->dependency_output_file_or_phony());
+      }
     }
   }
 }
diff --git a/src/gn/ninja_c_binary_target_writer_unittest.cc b/src/gn/ninja_c_binary_target_writer_unittest.cc
index 6290301..df6e8b5 100644
--- a/src/gn/ninja_c_binary_target_writer_unittest.cc
+++ b/src/gn/ninja_c_binary_target_writer_unittest.cc
@@ -55,7 +55,7 @@
         "build obj/foo/bar.input1.o: cxx ../../foo/input1.cc\n"
         "build obj/foo/bar.input2.o: cxx ../../foo/input2.cc\n"
         "\n"
-        "build obj/foo/bar.stamp: stamp obj/foo/bar.input1.o "
+        "build phony/foo/bar: phony obj/foo/bar.input1.o "
         "obj/foo/bar.input2.o ../../foo/input3.o ../../foo/input4.obj\n";
     std::string out_str = out.str();
     EXPECT_EQ(expected, out_str) << expected << "\n" << out_str;
@@ -86,7 +86,7 @@
         // order.
         "build ./libshlib.so: solink obj/foo/bar.input1.o "
         "obj/foo/bar.input2.o ../../foo/input3.o ../../foo/input4.obj "
-        "|| obj/foo/bar.stamp\n"
+        "|| phony/foo/bar\n"
         "  ldflags =\n"
         "  libs =\n"
         "  frameworks =\n"
@@ -119,7 +119,7 @@
         "\n"
         // There are no sources so there are no params to alink. (In practice
         // this will probably fail in the archive tool.)
-        "build obj/foo/libstlib.a: alink || obj/foo/bar.stamp\n"
+        "build obj/foo/libstlib.a: alink || phony/foo/bar\n"
         "  arflags =\n"
         "  output_extension = \n"
         "  output_dir = \n";
@@ -147,7 +147,7 @@
         // order.
         "build obj/foo/libstlib.a: alink obj/foo/bar.input1.o "
         "obj/foo/bar.input2.o ../../foo/input3.o ../../foo/input4.obj "
-        "|| obj/foo/bar.stamp\n"
+        "|| phony/foo/bar\n"
         "  arflags =\n"
         "  output_extension = \n"
         "  output_dir = \n";
@@ -394,7 +394,7 @@
       "build obj/out/Debug/gen_obj.generated.o: cxx generated.cc"
       " || obj/foo/generate.stamp\n"
       "\n"
-      "build obj/foo/gen_obj.stamp: stamp obj/out/Debug/gen_obj.generated.o"
+      "build phony/foo/gen_obj: phony obj/out/Debug/gen_obj.generated.o"
       // The order-only dependency here is strictly unnecessary since the
       // sources list this as an order-only dep.
       " || obj/foo/generate.stamp\n";
@@ -429,8 +429,8 @@
       "build ./libgen_lib.so: solink obj/out/Debug/gen_obj.generated.o"
       // The order-only dependency here is strictly unnecessary since
       // obj/out/Debug/gen_obj.generated.o has dependency to
-      // obj/foo/gen_obj.stamp
-      " || obj/foo/gen_obj.stamp\n"
+      // phony/foo/gen_obj
+      " || phony/foo/gen_obj\n"
       "  ldflags =\n"
       "  libs =\n"
       "  frameworks =\n"
@@ -649,7 +649,7 @@
   NinjaCBinaryTargetWriter inter_writer(&inter, inter_out);
   inter_writer.Run();
 
-  // The intermediate source set will be a stamp file that depends on the
+  // The intermediate source set will be a phony target that depends on the
   // object files, and will have an order-only dependency on its data dep and
   // data file.
   const char inter_expected[] =
@@ -663,7 +663,7 @@
       "\n"
       "build obj/foo/inter.inter.o: cxx ../../foo/inter.cc\n"
       "\n"
-      "build obj/foo/inter.stamp: stamp obj/foo/inter.inter.o || "
+      "build phony/foo/inter: phony obj/foo/inter.inter.o || "
       "./data_target\n";
   EXPECT_EQ(inter_expected, inter_out.str());
 
@@ -682,7 +682,7 @@
 
   // The final output depends on both object files (one from the final target,
   // one from the source set) and has an order-only dependency on the source
-  // set's stamp file and the final target's data file. The source set stamp
+  // set's phony target and the final target's data file. The source set phony
   // dependency will create an implicit order-only dependency on the data
   // target.
   const char final_expected[] =
@@ -697,7 +697,7 @@
       "build obj/foo/exe.final.o: cxx ../../foo/final.cc\n"
       "\n"
       "build ./exe: link obj/foo/exe.final.o obj/foo/inter.inter.o || "
-      "obj/foo/inter.stamp\n"
+      "phony/foo/inter\n"
       "  ldflags =\n"
       "  libs =\n"
       "  frameworks =\n"
@@ -886,8 +886,8 @@
         "build withpch/obj/foo/no_pch_target.input2.o: "
         "withpch_cc ../../foo/input2.c\n"
         "\n"
-        "build withpch/obj/foo/no_pch_target.stamp: "
-        "withpch_stamp withpch/obj/foo/no_pch_target.input1.o "
+        "build withpch/phony/foo/no_pch_target: "
+        "phony withpch/obj/foo/no_pch_target.input1.o "
         "withpch/obj/foo/no_pch_target.input2.o\n";
     EXPECT_EQ(no_pch_expected, out.str());
   }
@@ -940,7 +940,7 @@
         // Explicit dependency on the PCH build step.
         "withpch/obj/build/pch_target.precompile.c.o\n"
         "\n"
-        "build withpch/obj/foo/pch_target.stamp: withpch_stamp "
+        "build withpch/phony/foo/pch_target: phony "
         "withpch/obj/foo/pch_target.input1.o "
         "withpch/obj/foo/pch_target.input2.o "
         // The precompiled object files were added to the outputs.
@@ -1020,8 +1020,8 @@
         "build withpch/obj/foo/no_pch_target.input2.o: "
         "withpch_cc ../../foo/input2.c\n"
         "\n"
-        "build withpch/obj/foo/no_pch_target.stamp: "
-        "withpch_stamp withpch/obj/foo/no_pch_target.input1.o "
+        "build withpch/phony/foo/no_pch_target: "
+        "phony withpch/obj/foo/no_pch_target.input1.o "
         "withpch/obj/foo/no_pch_target.input2.o\n";
     EXPECT_EQ(no_pch_expected, out.str());
   }
@@ -1072,8 +1072,8 @@
         // Explicit dependency on the PCH build step.
         "withpch/obj/build/pch_target.precompile.h-c.gch\n"
         "\n"
-        "build withpch/obj/foo/pch_target.stamp: "
-        "withpch_stamp withpch/obj/foo/pch_target.input1.o "
+        "build withpch/phony/foo/pch_target: "
+        "phony withpch/obj/foo/pch_target.input1.o "
         "withpch/obj/foo/pch_target.input2.o\n";
     EXPECT_EQ(pch_gcc_expected, out.str());
   }
@@ -1138,7 +1138,7 @@
         "build obj/foo/bar.input2.o: cxx ../../foo/input2.cc"
         " | ../../foo/input.data\n"
         "\n"
-        "build obj/foo/bar.stamp: stamp obj/foo/bar.input1.o "
+        "build phony/foo/bar: phony obj/foo/bar.input1.o "
         "obj/foo/bar.input2.o\n";
 
     EXPECT_EQ(expected, out.str());
@@ -1209,7 +1209,7 @@
         "build obj/foo/bar.input2.o: cxx ../../foo/input2.cc"
         " | obj/foo/bar.inputs.stamp\n"
         "\n"
-        "build obj/foo/bar.stamp: stamp obj/foo/bar.input1.o "
+        "build phony/foo/bar: phony obj/foo/bar.input1.o "
         "obj/foo/bar.input2.o\n";
 
     EXPECT_EQ(expected, out.str());
@@ -1259,7 +1259,7 @@
         "build obj/foo/bar.input2.o: cxx ../../foo/input2.cc"
         " | obj/foo/bar.inputs.stamp\n"
         "\n"
-        "build obj/foo/bar.stamp: stamp obj/foo/bar.input1.o "
+        "build phony/foo/bar: phony obj/foo/bar.input1.o "
         "obj/foo/bar.input2.o\n";
 
     EXPECT_EQ(expected, out.str());
@@ -1571,7 +1571,7 @@
         "\n"
         "build obj/foo/file1.o obj/foo/file2.o: stamp obj/foo/Foo.swiftmodule\n"
         "\n"
-        "build obj/foo/foo.stamp: stamp"
+        "build phony/foo/foo: phony"
         " obj/foo/file1.o obj/foo/file2.o\n";
 
     const std::string out_str = out.str();
@@ -1604,13 +1604,13 @@
         "target_output_name = bar\n"
         "\n"
         "build obj/bar/Bar.swiftmodule: swift ../../bar/bar.swift"
-        " || obj/foo/foo.stamp\n"
+        " || phony/foo/foo\n"
         "\n"
         "build obj/bar/bar.o: stamp obj/bar/Bar.swiftmodule"
-        " || obj/foo/foo.stamp\n"
+        " || phony/foo/foo\n"
         "\n"
-        "build obj/bar/bar.stamp: stamp obj/bar/bar.o "
-        "|| obj/foo/foo.stamp\n";
+        "build phony/bar/bar: phony obj/bar/bar.o "
+        "|| phony/foo/foo\n";
 
     const std::string out_str = out.str();
     EXPECT_EQ(expected, out_str) << expected << "\n" << out_str;
@@ -1650,13 +1650,13 @@
         "target_output_name = bar\n"
         "\n"
         "build obj/bar/Bar.swiftmodule: swift ../../bar/bar.swift"
-        " || obj/foo/foo.stamp\n"
+        " || phony/foo/foo\n"
         "\n"
         "build obj/bar/bar.o: stamp obj/bar/Bar.swiftmodule"
-        " || obj/foo/foo.stamp\n"
+        " || phony/foo/foo\n"
         "\n"
-        "build obj/bar/bar.stamp: stamp obj/bar/bar.o "
-        "|| obj/bar/group.stamp obj/foo/foo.stamp\n";
+        "build phony/bar/bar: phony obj/bar/bar.o "
+        "|| obj/bar/group.stamp phony/foo/foo\n";
 
     const std::string out_str = out.str();
     EXPECT_EQ(expected, out_str) << expected << "\n" << out_str;
@@ -1685,7 +1685,7 @@
         "\n"
         "build ./bar: link obj/foo/file1.o obj/foo/file2.o "
         "| obj/foo/Foo.swiftmodule "
-        "|| obj/foo/foo.stamp\n"
+        "|| phony/foo/foo\n"
         "  ldflags =\n"
         "  libs =\n"
         "  frameworks =\n"
diff --git a/src/gn/ninja_copy_target_writer.cc b/src/gn/ninja_copy_target_writer.cc
index 9299223..c1a94ee 100644
--- a/src/gn/ninja_copy_target_writer.cc
+++ b/src/gn/ninja_copy_target_writer.cc
@@ -74,8 +74,10 @@
       std::vector<const Target*>(), num_stamp_uses);
 
   std::vector<OutputFile> data_outs;
-  for (const auto& dep : target_->data_deps())
-    data_outs.push_back(dep.ptr->dependency_output_file());
+  for (const auto& dep : target_->data_deps()) {
+    if (dep.ptr->dependency_output_file_or_phony())
+      data_outs.push_back(*dep.ptr->dependency_output_file_or_phony());
+  }
 
   // Note that we don't write implicit deps for copy steps. "copy" only
   // depends on the output files themselves, rather than having includes
diff --git a/src/gn/ninja_create_bundle_target_writer.cc b/src/gn/ninja_create_bundle_target_writer.cc
index 9f000f3..9623e32 100644
--- a/src/gn/ninja_create_bundle_target_writer.cc
+++ b/src/gn/ninja_create_bundle_target_writer.cc
@@ -90,8 +90,10 @@
   WriteCompileAssetsCatalogStep(order_only_deps, &output_files);
   WriteCodeSigningStep(code_signing_rule_name, order_only_deps, &output_files);
 
-  for (const auto& pair : target_->data_deps())
-    order_only_deps.push_back(pair.ptr->dependency_output_file());
+  for (const auto& pair : target_->data_deps()) {
+    if (pair.ptr->dependency_output_file_or_phony())
+      order_only_deps.push_back(*pair.ptr->dependency_output_file_or_phony());
+  }
   WriteStampForTarget(output_files, order_only_deps);
 
   // Write a phony target for the outer bundle directory. This allows other
@@ -102,7 +104,9 @@
       out_,
       OutputFile(settings_->build_settings(),
                  target_->bundle_data().GetBundleRootDirOutput(settings_)));
-  out_ << ": phony " << target_->dependency_output_file().value();
+  CHECK(target_->dependency_output_file_or_phony());
+  out_ << ": " << BuiltinTool::kBuiltinToolPhony << " ";
+  out_ << target_->dependency_output_file_or_phony()->value();
   out_ << std::endl;
 }
 
@@ -281,8 +285,11 @@
 NinjaCreateBundleTargetWriter::WriteCompileAssetsCatalogInputDepsStamp(
     const std::vector<const Target*>& dependencies) {
   DCHECK(!dependencies.empty());
-  if (dependencies.size() == 1)
-    return dependencies[0]->dependency_output_file();
+  if (dependencies.size() == 1) {
+    return dependencies[0]->dependency_output_file_or_phony()
+               ? *dependencies[0]->dependency_output_file_or_phony()
+               : OutputFile{};
+  }
 
   OutputFile xcassets_input_stamp_file =
       GetBuildDirForTargetAsOutputFile(target_, BuildDirType::OBJ);
@@ -295,8 +302,10 @@
        << GeneralTool::kGeneralToolStamp;
 
   for (const Target* target : dependencies) {
-    out_ << " ";
-    path_output_.WriteFile(out_, target->dependency_output_file());
+    if (target->dependency_output_file_or_phony()) {
+      out_ << " ";
+      path_output_.WriteFile(out_, *target->dependency_output_file_or_phony());
+    }
   }
   out_ << std::endl;
   return xcassets_input_stamp_file;
diff --git a/src/gn/ninja_generated_file_target_writer.cc b/src/gn/ninja_generated_file_target_writer.cc
index 6b0db1b..16fcf96 100644
--- a/src/gn/ninja_generated_file_target_writer.cc
+++ b/src/gn/ninja_generated_file_target_writer.cc
@@ -30,13 +30,17 @@
   // on each of the deps and data_deps in the target. The actual collection is
   // done at gen time, and so ninja doesn't need to know about it.
   std::vector<OutputFile> output_files;
-  for (const auto& pair : target_->GetDeps(Target::DEPS_LINKED))
-    output_files.push_back(pair.ptr->dependency_output_file());
+  for (const auto& pair : target_->GetDeps(Target::DEPS_LINKED)) {
+    if (pair.ptr->dependency_output_file_or_phony())
+      output_files.push_back(*pair.ptr->dependency_output_file_or_phony());
+  }
 
   std::vector<OutputFile> data_output_files;
   const LabelTargetVector& data_deps = target_->data_deps();
-  for (const auto& pair : data_deps)
-    data_output_files.push_back(pair.ptr->dependency_output_file());
+  for (const auto& pair : data_deps) {
+    if (pair.ptr->dependency_output_file_or_phony())
+      data_output_files.push_back(*pair.ptr->dependency_output_file_or_phony());
+  }
 
   WriteStampForTarget(output_files, data_output_files);
 }
diff --git a/src/gn/ninja_group_target_writer.cc b/src/gn/ninja_group_target_writer.cc
index b518977..1ba6d1c 100644
--- a/src/gn/ninja_group_target_writer.cc
+++ b/src/gn/ninja_group_target_writer.cc
@@ -20,13 +20,17 @@
   // A group rule just generates a stamp file with dependencies on each of
   // the deps and data_deps in the group.
   std::vector<OutputFile> output_files;
-  for (const auto& pair : target_->GetDeps(Target::DEPS_LINKED))
-    output_files.push_back(pair.ptr->dependency_output_file());
+  for (const auto& pair : target_->GetDeps(Target::DEPS_LINKED)) {
+    if (pair.ptr->dependency_output_file_or_phony())
+      output_files.push_back(*pair.ptr->dependency_output_file_or_phony());
+  }
 
   std::vector<OutputFile> data_output_files;
   const LabelTargetVector& data_deps = target_->data_deps();
-  for (const auto& pair : data_deps)
-    data_output_files.push_back(pair.ptr->dependency_output_file());
+  for (const auto& pair : data_deps) {
+    if (pair.ptr->dependency_output_file_or_phony())
+      data_output_files.push_back(*pair.ptr->dependency_output_file_or_phony());
+  }
 
   WriteStampForTarget(output_files, data_output_files);
 }
diff --git a/src/gn/ninja_rust_binary_target_writer.cc b/src/gn/ninja_rust_binary_target_writer.cc
index 59960a1..a8aecaf 100644
--- a/src/gn/ninja_rust_binary_target_writer.cc
+++ b/src/gn/ninja_rust_binary_target_writer.cc
@@ -144,14 +144,19 @@
                      classified_deps.extra_object_files.begin(),
                      classified_deps.extra_object_files.end());
   for (const auto* framework_dep : classified_deps.framework_deps) {
-    order_only_deps.push_back(framework_dep->dependency_output_file());
+    CHECK(framework_dep->dependency_output_file());
+    order_only_deps.push_back(*framework_dep->dependency_output_file());
   }
   for (const auto* non_linkable_dep : classified_deps.non_linkable_deps) {
-    if (non_linkable_dep->source_types_used().RustSourceUsed() &&
-        non_linkable_dep->output_type() != Target::SOURCE_SET) {
-      rustdeps.push_back(non_linkable_dep->dependency_output_file());
+    if (non_linkable_dep->dependency_output_file_or_phony()) {
+      if (non_linkable_dep->source_types_used().RustSourceUsed() &&
+          non_linkable_dep->output_type() != Target::SOURCE_SET) {
+        rustdeps.push_back(
+            *non_linkable_dep->dependency_output_file_or_phony());
+      }
+      order_only_deps.push_back(
+          *non_linkable_dep->dependency_output_file_or_phony());
     }
-    order_only_deps.push_back(non_linkable_dep->dependency_output_file());
   }
   for (const auto* linkable_dep : classified_deps.linkable_deps) {
     if (linkable_dep->source_types_used().RustSourceUsed()) {
@@ -159,7 +164,8 @@
     } else {
       nonrustdeps.push_back(linkable_dep->link_output_file());
     }
-    implicit_deps.push_back(linkable_dep->dependency_output_file());
+    CHECK(linkable_dep->dependency_output_file());
+    implicit_deps.push_back(*linkable_dep->dependency_output_file());
   }
 
   // Rust libraries specified by paths.
@@ -178,7 +184,8 @@
   for (const auto* dep :
        target_->rust_values().transitive_libs().GetOrdered()) {
     if (dep->source_types_used().RustSourceUsed()) {
-      transitive_rustlibs.push_back(dep->dependency_output_file());
+      CHECK(dep->dependency_output_file());
+      transitive_rustlibs.push_back(*dep->dependency_output_file());
     }
   }
 
@@ -249,6 +256,7 @@
   for (const Target* target : deps) {
     if (target->output_type() == Target::RUST_LIBRARY ||
         target->output_type() == Target::RUST_PROC_MACRO) {
+      CHECK(target->dependency_output_file());
       out_ << " --extern ";
       const auto& renamed_dep =
           target_->rust_values().aliased_deps().find(target->label());
@@ -257,7 +265,7 @@
       } else {
         out_ << std::string(target->rust_values().crate_name()) << "=";
       }
-      path_output_.WriteFile(out_, target->dependency_output_file());
+      path_output_.WriteFile(out_, *target->dependency_output_file());
     }
   }
 
diff --git a/src/gn/ninja_rust_binary_target_writer_unittest.cc b/src/gn/ninja_rust_binary_target_writer_unittest.cc
index 34d87f0..a440a6f 100644
--- a/src/gn/ninja_rust_binary_target_writer_unittest.cc
+++ b/src/gn/ninja_rust_binary_target_writer_unittest.cc
@@ -436,7 +436,7 @@
         "../../foo/main.rs obj/baz/sourceset.csourceset.o "
         "obj/bar/libmylib.rlib "
         "obj/foo/libstatic.a ./libshared.so ./libshared_with_toc.so.TOC "
-        "|| obj/baz/sourceset.stamp\n"
+        "|| phony/baz/sourceset\n"
         "  externs = --extern mylib=obj/bar/libmylib.rlib\n"
         "  rustdeps = -Ldependency=obj/bar -Lnative=obj/baz -Lnative=obj/foo "
         "-Lnative=. -Clink-arg=obj/baz/sourceset.csourceset.o -lstatic "
diff --git a/src/gn/ninja_target_writer.cc b/src/gn/ninja_target_writer.cc
index b6bbb9f..5b443f1 100644
--- a/src/gn/ninja_target_writer.cc
+++ b/src/gn/ninja_target_writer.cc
@@ -8,6 +8,7 @@
 
 #include "base/files/file_util.h"
 #include "base/strings/string_util.h"
+#include "gn/builtin_tool.h"
 #include "gn/config_values_extractors.h"
 #include "gn/err.h"
 #include "gn/escape.h"
@@ -267,9 +268,11 @@
     return std::vector<OutputFile>{
         OutputFile(settings_->build_settings(), *input_deps_sources[0])};
   if (input_deps_sources.size() == 0 && input_deps_targets.size() == 1) {
-    const OutputFile& dep = input_deps_targets[0]->dependency_output_file();
-    DCHECK(!dep.value().empty());
-    return std::vector<OutputFile>{dep};
+    const std::optional<OutputFile>& dep =
+        input_deps_targets[0]->dependency_output_file_or_phony();
+    if (!dep)
+      return std::vector<OutputFile>();
+    return std::vector<OutputFile>{*dep};
   }
 
   std::vector<OutputFile> outs;
@@ -283,8 +286,8 @@
       input_deps_targets.begin(), input_deps_targets.end(),
       [](const Target* a, const Target* b) { return a->label() < b->label(); });
   for (auto* dep : input_deps_targets) {
-    DCHECK(!dep->dependency_output_file().value().empty());
-    outs.push_back(dep->dependency_output_file());
+    if (dep->dependency_output_file_or_phony())
+      outs.push_back(*dep->dependency_output_file_or_phony());
   }
 
   // If there are multiple inputs, but the stamp file would be referenced only
@@ -311,7 +314,8 @@
 void NinjaTargetWriter::WriteStampForTarget(
     const std::vector<OutputFile>& files,
     const std::vector<OutputFile>& order_only_deps) {
-  const OutputFile& stamp_file = target_->dependency_output_file();
+  CHECK(target_->dependency_output_file());
+  const OutputFile& stamp_file = *target_->dependency_output_file();
 
   // First validate that the target's dependency is a stamp file. Otherwise,
   // we shouldn't have gotten here!
@@ -333,3 +337,29 @@
   }
   out_ << std::endl;
 }
+
+void NinjaTargetWriter::WritePhonyForTarget(
+    const std::vector<OutputFile>& files,
+    const std::vector<OutputFile>& order_only_deps) {
+  // If there's no phony, then we should not have any inputs and it is okay to
+  // omit the build rule.
+  if (!target_->dependency_output_phony()) {
+    CHECK(files.empty());
+    CHECK(order_only_deps.empty());
+    return;
+  }
+  const OutputFile& phony_target = *target_->dependency_output_phony();
+  CHECK(!phony_target.value().empty());
+
+  out_ << "build ";
+  path_output_.WriteFile(out_, phony_target);
+
+  out_ << ": " << BuiltinTool::kBuiltinToolPhony;
+  path_output_.WriteFiles(out_, files);
+
+  if (!order_only_deps.empty()) {
+    out_ << " ||";
+    path_output_.WriteFiles(out_, order_only_deps);
+  }
+  out_ << std::endl;
+}
diff --git a/src/gn/ninja_target_writer.h b/src/gn/ninja_target_writer.h
index f4c9eae..f6c4e04 100644
--- a/src/gn/ninja_target_writer.h
+++ b/src/gn/ninja_target_writer.h
@@ -57,6 +57,11 @@
   void WriteStampForTarget(const std::vector<OutputFile>& deps,
                            const std::vector<OutputFile>& order_only_deps);
 
+  // Writes to the output file a final phony rule for the target that aliases
+  // the given list of files.
+  void WritePhonyForTarget(const std::vector<OutputFile>& deps,
+                           const std::vector<OutputFile>& order_only_deps);
+
   const Settings* settings_;  // Non-owning.
   const Target* target_;      // Non-owning.
   std::ostream& out_;
diff --git a/src/gn/runtime_deps.cc b/src/gn/runtime_deps.cc
index 3b6d683..d002458 100644
--- a/src/gn/runtime_deps.cc
+++ b/src/gn/runtime_deps.cc
@@ -176,7 +176,7 @@
       return false;
     }
 
-    OutputFile output_file;
+    std::optional<OutputFile> output_file;
     const char extension[] = ".runtime_deps";
     if (target->output_type() == Target::SHARED_LIBRARY ||
         target->output_type() == Target::LOADABLE_MODULE) {
@@ -185,11 +185,12 @@
       CHECK(!target->computed_outputs().empty());
       output_file =
           OutputFile(target->computed_outputs()[0].value() + extension);
-    } else {
-      output_file =
-          OutputFile(target->dependency_output_file().value() + extension);
+    } else if (target->dependency_output_file_or_phony()) {
+      output_file = OutputFile(
+          target->dependency_output_file_or_phony()->value() + extension);
     }
-    files_to_write->push_back(std::make_pair(output_file, target));
+    if (output_file)
+      files_to_write->push_back(std::make_pair(*output_file, target));
   }
   return true;
 }
diff --git a/src/gn/switches.cc b/src/gn/switches.cc
index e7b3a81..cbeaa26 100644
--- a/src/gn/switches.cc
+++ b/src/gn/switches.cc
@@ -199,8 +199,9 @@
   build directory.
 
   If a source set, action, copy, or group is listed, the runtime deps file will
-  correspond to the .stamp file corresponding to that target. This is probably
-  not useful; the use-case for this feature is generally executable targets.
+  correspond to the .stamp file or phony rule corresponding to that target. This
+  is probably not useful; the use-case for this feature is generally executable
+  targets.
 
   The runtime dependency file will list one file per line, with no escaping.
   The files will be relative to the root_build_dir. The first line of the file
diff --git a/src/gn/target.cc b/src/gn/target.cc
index b8bf2f9..a5493e8 100644
--- a/src/gn/target.cc
+++ b/src/gn/target.cc
@@ -5,6 +5,7 @@
 #include "gn/target.h"
 
 #include <stddef.h>
+#include <algorithm>
 
 #include "base/stl_util.h"
 #include "base/strings/string_util.h"
@@ -522,8 +523,7 @@
     if (!bundle_data().GetOutputsAsSourceFiles(settings(), this, outputs, err))
       return false;
   } else if (IsBinary() && output_type() != Target::SOURCE_SET) {
-    // Binary target with normal outputs (source sets have stamp outputs like
-    // groups).
+    // Binary target with normal outputs (source sets have phony targets).
     DCHECK(IsBinary()) << static_cast<int>(output_type());
     if (!build_complete) {
       // Can't access the toolchain for a target before the build is complete.
@@ -543,15 +543,20 @@
           output_file.AsSourceFile(settings()->build_settings()));
     }
   } else {
-    // Everything else (like a group or something) has a stamp output. The
-    // dependency output file should have computed what this is. This won't be
-    // valid unless the build is complete.
+    // Everything else (like a group or something) has a stamp or phony output.
+    // The dependency output file should have computed what this is. This won't
+    // be valid unless the build is complete.
     if (!build_complete) {
       *err = Err(loc_for_error, kBuildIncompleteMsg);
       return false;
     }
-    outputs->push_back(
-        dependency_output_file().AsSourceFile(settings()->build_settings()));
+
+    // The dependency output might be empty if there is no output file or a
+    // phony alias for a set of inputs.
+    if (dependency_output_file_or_phony()) {
+      outputs->push_back(dependency_output_file_or_phony()->AsSourceFile(
+          settings()->build_settings()));
+    }
   }
   return true;
 }
@@ -782,14 +787,48 @@
   bundle_data_.OnTargetResolved(this);
 }
 
+bool Target::HasRealInputs() const {
+  // This check is only necessary if this target will result in a phony target.
+  // Phony targets with no real inputs are treated as always dirty.
+
+  // TODO(bug 194): This method is currently just checking the relevant inputs
+  // for the current list of output types that result in phony targets. As the
+  // list of phony targets expands, this method should be updated to properly
+  // account for which inputs matter for the given output type.
+
+  // If any of this target's dependencies is non-phony target or a phony target
+  // with real inputs, then this target should be considered to have inputs.
+  for (const auto& pair : GetDeps(DEPS_ALL)) {
+    if (pair.ptr->dependency_output_file_or_phony()) {
+      return true;
+    }
+  }
+
+  // If any of this target's sources will result in output files, then this
+  // target should be considered to have real inputs.
+  std::vector<OutputFile> tool_outputs;
+  return std::any_of(
+      sources().begin(), sources().end(), [&, this](const auto& source) {
+        const char* tool_name = Tool::kToolNone;
+        return GetOutputFilesForSource(source, &tool_name, &tool_outputs);
+      });
+}
+
 bool Target::FillOutputFiles(Err* err) {
   const Tool* tool = toolchain_->GetToolForTargetFinalOutput(this);
   bool check_tool_outputs = false;
   switch (output_type_) {
+    case SOURCE_SET: {
+      if (HasRealInputs()) {
+        dependency_output_phony_ =
+            GetBuildDirForTargetAsOutputFile(this, BuildDirType::PHONY);
+        dependency_output_phony_->value().append(GetComputedOutputName());
+      }
+      break;
+    }
     case GROUP:
     case BUNDLE_DATA:
     case CREATE_BUNDLE:
-    case SOURCE_SET:
     case COPY_FILES:
     case ACTION:
     case ACTION_FOREACH:
@@ -799,8 +838,8 @@
       // "<target_out_dir>/<targetname>.stamp".
       dependency_output_file_ =
           GetBuildDirForTargetAsOutputFile(this, BuildDirType::OBJ);
-      dependency_output_file_.value().append(GetComputedOutputName());
-      dependency_output_file_.value().append(".stamp");
+      dependency_output_file_->value().append(GetComputedOutputName());
+      dependency_output_file_->value().append(".stamp");
       break;
     }
     case EXECUTABLE:
@@ -815,7 +854,7 @@
 
       if (tool->runtime_outputs().list().empty()) {
         // Default to the first output for the runtime output.
-        runtime_outputs_.push_back(dependency_output_file_);
+        runtime_outputs_.push_back(*dependency_output_file_);
       } else {
         SubstitutionWriter::ApplyListToLinkerAsOutputFile(
             this, tool, tool->runtime_outputs(), &runtime_outputs_);
@@ -827,9 +866,10 @@
       // first output.
       CHECK(tool->outputs().list().size() >= 1);
       check_tool_outputs = true;
-      link_output_file_ = dependency_output_file_ =
+      dependency_output_file_ =
           SubstitutionWriter::ApplyPatternToLinkerAsOutputFile(
               this, tool, tool->outputs().list()[0]);
+      link_output_file_ = *dependency_output_file_;
       break;
     case RUST_PROC_MACRO:
     case SHARED_LIBRARY:
@@ -838,9 +878,10 @@
       if (const CTool* ctool = tool->AsC()) {
         if (ctool->link_output().empty() && ctool->depend_output().empty()) {
           // Default behavior, use the first output file for both.
-          link_output_file_ = dependency_output_file_ =
+          dependency_output_file_ =
               SubstitutionWriter::ApplyPatternToLinkerAsOutputFile(
                   this, tool, tool->outputs().list()[0]);
+          link_output_file_ = *dependency_output_file_;
         } else {
           // Use the tool-specified ones.
           if (!ctool->link_output().empty()) {
@@ -863,9 +904,10 @@
         }
       } else if (const RustTool* rstool = tool->AsRust()) {
         // Default behavior, use the first output file for both.
-        link_output_file_ = dependency_output_file_ =
+        dependency_output_file_ =
             SubstitutionWriter::ApplyPatternToLinkerAsOutputFile(
                 this, tool, tool->outputs().list()[0]);
+        link_output_file_ = *dependency_output_file_;
       }
       break;
     case UNKNOWN:
diff --git a/src/gn/target.h b/src/gn/target.h
index dbcfa69..dcbaf2c 100644
--- a/src/gn/target.h
+++ b/src/gn/target.h
@@ -320,7 +320,7 @@
   // action or a copy step, and the output library or executable file(s) from
   // binary targets.
   //
-  // It will NOT include stamp files and object files.
+  // It will NOT include stamp files, phony targets or object files.
   const std::vector<OutputFile>& computed_outputs() const {
     return computed_outputs_;
   }
@@ -335,13 +335,30 @@
   // could be the same or different than the link output file, depending on the
   // system. For actions this will be the stamp file.
   //
+  // The dependency output phony is only set when the target does not have an
+  // output file and is using a phony alias to represent it. The exception to
+  // this is for phony targets without any real inputs. Ninja treats empty phony
+  // targets as always dirty, so no other targets should depend on that target.
+  // In that scenario, both dependency_output_phony or dependency_output_file
+  // will be std::nullopt.
+  //
+  // Callers that do not care whether the dependency is represented by a file or
+  // a phony should use dependency_output_file_or_phony().
+  //
   // These are only known once the target is resolved and will be empty before
   // that. This is a cache of the files to prevent every target that depends on
   // a given library from recomputing the same pattern.
   const OutputFile& link_output_file() const { return link_output_file_; }
-  const OutputFile& dependency_output_file() const {
+  const std::optional<OutputFile>& dependency_output_file() const {
     return dependency_output_file_;
   }
+  const std::optional<OutputFile>& dependency_output_phony() const {
+    return dependency_output_phony_;
+  }
+  const std::optional<OutputFile>& dependency_output_file_or_phony() const {
+    return dependency_output_file_ ? dependency_output_file_
+                                   : dependency_output_phony_;
+  }
 
   // The subset of computed_outputs that are considered runtime outputs.
   const std::vector<OutputFile>& runtime_outputs() const {
@@ -366,6 +383,11 @@
   // The |loc_for_error| is used to blame a location for any errors produced. It
   // can be empty if there is no range (like this is being called based on the
   // command-line.
+  //
+  // It is possible for |outputs| to be returned empty without an error being
+  // reported. This can occur when the output type will result in a phony alias
+  // target (like a source_set) that is omitted from build files when they have
+  // no real inputs.
   bool GetOutputsAsSourceFiles(const LocationRange& loc_for_error,
                                bool build_complete,
                                std::vector<SourceFile>* outputs,
@@ -402,6 +424,11 @@
   void PullRecursiveHardDeps();
   void PullRecursiveBundleData();
 
+  // Checks to see whether this target or any of its dependencies have real
+  // inputs. If not, this target should be omitted as a dependency. This check
+  // only applies to targets that will result in a phony rule.
+  bool HasRealInputs() const;
+
   // Fills the link and dependency output files when a target is resolved.
   bool FillOutputFiles(Err* err);
 
@@ -489,7 +516,8 @@
   // Output files. Empty until the target is resolved.
   std::vector<OutputFile> computed_outputs_;
   OutputFile link_output_file_;
-  OutputFile dependency_output_file_;
+  std::optional<OutputFile> dependency_output_file_;
+  std::optional<OutputFile> dependency_output_phony_;
   std::vector<OutputFile> runtime_outputs_;
 
   Metadata metadata_;
diff --git a/src/gn/target_unittest.cc b/src/gn/target_unittest.cc
index 792c530..2496696 100644
--- a/src/gn/target_unittest.cc
+++ b/src/gn/target_unittest.cc
@@ -726,7 +726,8 @@
   ASSERT_TRUE(target.OnResolved(&err));
 
   EXPECT_EQ("./liba.so", target.link_output_file().value());
-  EXPECT_EQ("./liba.so.TOC", target.dependency_output_file().value());
+  ASSERT_TRUE(target.dependency_output_file());
+  EXPECT_EQ("./liba.so.TOC", target.dependency_output_file()->value());
 
   ASSERT_EQ(1u, target.runtime_outputs().size());
   EXPECT_EQ("./liba.so", target.runtime_outputs()[0].value());
@@ -772,7 +773,8 @@
   ASSERT_TRUE(target.OnResolved(&err));
 
   EXPECT_EQ("./a.dll.lib", target.link_output_file().value());
-  EXPECT_EQ("./a.dll.lib", target.dependency_output_file().value());
+  ASSERT_TRUE(target.dependency_output_file());
+  EXPECT_EQ("./a.dll.lib", target.dependency_output_file()->value());
 
   ASSERT_EQ(2u, target.runtime_outputs().size());
   EXPECT_EQ("./a.dll", target.runtime_outputs()[0].value());
@@ -802,6 +804,7 @@
 
   Target target(setup.settings(), Label(SourceDir("//a/"), "a"));
   target.set_output_type(Target::SOURCE_SET);
+  target.sources().push_back(SourceFile("//a/source_file1.cc"));
   target.SetToolchain(&toolchain);
   Err err;
   ASSERT_TRUE(target.OnResolved(&err));
@@ -818,12 +821,12 @@
   EXPECT_EQ("input.cc.o", output[0].value()) << output[0].value();
 
   // Test GetOutputsAsSourceFiles(). Since this is a source set it should give a
-  // stamp file.
+  // phony target.
   std::vector<SourceFile> computed_outputs;
   EXPECT_TRUE(target.GetOutputsAsSourceFiles(LocationRange(), true,
                                              &computed_outputs, &err));
   ASSERT_EQ(1u, computed_outputs.size());
-  EXPECT_EQ("//out/Debug/obj/a/a.stamp", computed_outputs[0].value());
+  EXPECT_EQ("//out/Debug/phony/a/a", computed_outputs[0].value());
 }
 
 // Tests Target::GetOutputFilesForSource for action_foreach targets (these, like
diff --git a/src/gn/tool.cc b/src/gn/tool.cc
index 5e4186d..9708e44 100644
--- a/src/gn/tool.cc
+++ b/src/gn/tool.cc
@@ -4,6 +4,7 @@
 
 #include "gn/tool.h"
 
+#include "gn/builtin_tool.h"
 #include "gn/c_tool.h"
 #include "gn/general_tool.h"
 #include "gn/rust_tool.h"
@@ -52,6 +53,13 @@
   return nullptr;
 }
 
+BuiltinTool* Tool::AsBuiltin() {
+  return nullptr;
+}
+const BuiltinTool* Tool::AsBuiltin() const {
+  return nullptr;
+}
+
 bool Tool::IsPatternInOutputList(const SubstitutionList& output_list,
                                  const SubstitutionPattern& pattern) const {
   for (const auto& cur : output_list.list()) {
@@ -392,7 +400,7 @@
     case Target::STATIC_LIBRARY:
       return CTool::kCToolAlink;
     case Target::SOURCE_SET:
-      return GeneralTool::kGeneralToolStamp;
+      return BuiltinTool::kBuiltinToolPhony;
     case Target::ACTION:
     case Target::ACTION_FOREACH:
     case Target::BUNDLE_DATA:
diff --git a/src/gn/tool.h b/src/gn/tool.h
index e64935f..5e824c9 100644
--- a/src/gn/tool.h
+++ b/src/gn/tool.h
@@ -24,6 +24,7 @@
 class CTool;
 class GeneralTool;
 class RustTool;
+class BuiltinTool;
 
 // To add a new Tool category, create a subclass implementing SetComplete()
 // Add a new category to ToolCategories
@@ -63,6 +64,8 @@
   virtual const GeneralTool* AsGeneral() const;
   virtual RustTool* AsRust();
   virtual const RustTool* AsRust() const;
+  virtual BuiltinTool* AsBuiltin();
+  virtual const BuiltinTool* AsBuiltin() const;
 
   // Basic information ---------------------------------------------------------
 
diff --git a/src/gn/toolchain.cc b/src/gn/toolchain.cc
index bfad81d..a00aec4 100644
--- a/src/gn/toolchain.cc
+++ b/src/gn/toolchain.cc
@@ -9,13 +9,15 @@
 #include <utility>
 
 #include "base/logging.h"
+#include "gn/builtin_tool.h"
 #include "gn/target.h"
 #include "gn/value.h"
 
 Toolchain::Toolchain(const Settings* settings,
                      const Label& label,
                      const SourceFileSet& build_dependency_files)
-    : Item(settings, label, build_dependency_files) {}
+    : Item(settings, label, build_dependency_files),
+      phony_tool_(BuiltinTool::kBuiltinToolPhony) {}
 
 Toolchain::~Toolchain() = default;
 
@@ -29,6 +31,9 @@
 
 Tool* Toolchain::GetTool(const char* name) {
   DCHECK(name != Tool::kToolNone);
+  if (name == BuiltinTool::kBuiltinToolPhony) {
+    return &phony_tool_;
+  }
   auto pair = tools_.find(name);
   if (pair != tools_.end()) {
     return pair->second.get();
@@ -38,6 +43,9 @@
 
 const Tool* Toolchain::GetTool(const char* name) const {
   DCHECK(name != Tool::kToolNone);
+  if (name == BuiltinTool::kBuiltinToolPhony) {
+    return &phony_tool_;
+  }
   auto pair = tools_.find(name);
   if (pair != tools_.end()) {
     return pair->second.get();
@@ -87,6 +95,20 @@
   return nullptr;
 }
 
+BuiltinTool* Toolchain::GetToolAsBuiltin(const char* name) {
+  if (Tool* tool = GetTool(name)) {
+    return tool->AsBuiltin();
+  }
+  return nullptr;
+}
+
+const BuiltinTool* Toolchain::GetToolAsBuiltin(const char* name) const {
+  if (const Tool* tool = GetTool(name)) {
+    return tool->AsBuiltin();
+  }
+  return nullptr;
+}
+
 void Toolchain::SetTool(std::unique_ptr<Tool> t) {
   DCHECK(t->name() != Tool::kToolNone);
   DCHECK(tools_.find(t->name()) == tools_.end());
@@ -120,6 +142,11 @@
   return GetToolAsRust(Tool::GetToolTypeForSourceType(type));
 }
 
+const BuiltinTool* Toolchain::GetToolForSourceTypeAsBuiltin(
+    SourceFile::Type type) const {
+  return GetToolAsBuiltin(Tool::GetToolTypeForSourceType(type));
+}
+
 const Tool* Toolchain::GetToolForTargetFinalOutput(const Target* target) const {
   return GetTool(Tool::GetToolTypeForTargetFinalOutput(target));
 }
@@ -138,3 +165,8 @@
     const Target* target) const {
   return GetToolAsRust(Tool::GetToolTypeForTargetFinalOutput(target));
 }
+
+const BuiltinTool* Toolchain::GetToolForTargetFinalOutputAsBuiltin(
+    const Target* target) const {
+  return GetToolAsBuiltin(Tool::GetToolTypeForTargetFinalOutput(target));
+}
diff --git a/src/gn/toolchain.h b/src/gn/toolchain.h
index eb5a60c..3f2b83a 100644
--- a/src/gn/toolchain.h
+++ b/src/gn/toolchain.h
@@ -9,6 +9,7 @@
 #include <string_view>
 
 #include "base/logging.h"
+#include "gn/builtin_tool.h"
 #include "gn/item.h"
 #include "gn/label_ptr.h"
 #include "gn/scope.h"
@@ -62,6 +63,8 @@
   const CTool* GetToolAsC(const char* name) const;
   RustTool* GetToolAsRust(const char* name);
   const RustTool* GetToolAsRust(const char* name) const;
+  BuiltinTool* GetToolAsBuiltin(const char* name);
+  const BuiltinTool* GetToolAsBuiltin(const char* name) const;
 
   // Set a tool. When all tools are configured, you should call
   // ToolchainSetupComplete().
@@ -93,6 +96,7 @@
   const CTool* GetToolForSourceTypeAsC(SourceFile::Type type) const;
   const GeneralTool* GetToolForSourceTypeAsGeneral(SourceFile::Type type) const;
   const RustTool* GetToolForSourceTypeAsRust(SourceFile::Type type) const;
+  const BuiltinTool* GetToolForSourceTypeAsBuiltin(SourceFile::Type type) const;
 
   // Returns the tool that produces the final output for the given target type.
   // This isn't necessarily the tool you would expect. For copy target, this
@@ -103,6 +107,8 @@
   const GeneralTool* GetToolForTargetFinalOutputAsGeneral(
       const Target* target) const;
   const RustTool* GetToolForTargetFinalOutputAsRust(const Target* target) const;
+  const BuiltinTool* GetToolForTargetFinalOutputAsBuiltin(
+      const Target* target) const;
 
   const SubstitutionBits& substitution_bits() const {
     DCHECK(setup_complete_);
@@ -114,6 +120,7 @@
   }
 
  private:
+  BuiltinTool phony_tool_;
   std::map<const char*, std::unique_ptr<Tool>> tools_;
 
   bool setup_complete_ = false;
diff --git a/src/gn/visual_studio_writer.cc b/src/gn/visual_studio_writer.cc
index fd16536..3717345 100644
--- a/src/gn/visual_studio_writer.cc
+++ b/src/gn/visual_studio_writer.cc
@@ -521,14 +521,14 @@
 
   project.SubElement("PropertyGroup", XmlAttributes("Label", "UserMacros"));
 
-  std::string ninja_target = GetNinjaTarget(target);
+  auto [ninja_target, ninja_target_is_phony] = GetNinjaTarget(target);
 
   {
     std::unique_ptr<XmlElementWriter> properties =
         project.SubElement("PropertyGroup");
     properties->SubElement("OutDir")->Text("$(SolutionDir)");
     properties->SubElement("TargetName")->Text("$(ProjectName)");
-    if (target->output_type() != Target::GROUP) {
+    if (target->output_type() != Target::GROUP && !ninja_target_is_phony) {
       properties->SubElement("TargetPath")->Text("$(OutDir)\\" + ninja_target);
     }
   }
@@ -904,13 +904,20 @@
   }
 }
 
-std::string VisualStudioWriter::GetNinjaTarget(const Target* target) {
+std::pair<std::string, bool> VisualStudioWriter::GetNinjaTarget(const Target* target) {
   std::ostringstream ninja_target_out;
-  DCHECK(!target->dependency_output_file().value().empty());
-  ninja_path_output_.WriteFile(ninja_target_out,
-                               target->dependency_output_file());
+  bool is_phony = false;
+  OutputFile output_file;
+  if (target->dependency_output_file()) {
+    output_file = *target->dependency_output_file();
+  } else if (target->dependency_output_phony()) {
+    output_file = *target->dependency_output_phony();
+    is_phony = true;
+  }
+
+  ninja_path_output_.WriteFile(ninja_target_out, output_file);
   std::string s = ninja_target_out.str();
   if (s.compare(0, 2, "./") == 0)
     s = s.substr(2);
-  return s;
+  return std::make_pair(s, is_phony);
 }
diff --git a/src/gn/visual_studio_writer.h b/src/gn/visual_studio_writer.h
index e4957a1..55ae705 100644
--- a/src/gn/visual_studio_writer.h
+++ b/src/gn/visual_studio_writer.h
@@ -127,7 +127,8 @@
   // and updates |root_folder_dir_|. Also sets |parent_folder| for |projects_|.
   void ResolveSolutionFolders();
 
-  std::string GetNinjaTarget(const Target* target);
+  // Returns the ninja target string and whether the target is phony.
+  std::pair<std::string, bool> GetNinjaTarget(const Target* target);
 
   const BuildSettings* build_settings_;