Implement and enable 'no_stamp_files'

This is the last CL to enable the new 'no_stamp_files'
build setting flag, which enables the generation of Ninja
phony targets instead of stamp files.

- Modify the Ninja target writers to understand phony
  aliases.

- Allow no_stamp_files to be set in the build settings
  file (e.g. `.gn`) without asserting. Note that when
  this is not set, this CL should not change the output
  of GN at all.

For testing the output of `gn gen` has been compared before
and after this CL was applied with the Fuchsia source tree
to verify that the output is the same.

On the other hand, if `no_stamp_files = true` is added
to the `.gn` file, the generated Ninja file go from
829 MiB to 797 MiB.

On Chromium, size of Ninja files go from 401 MiB to 384 MiB.

Test expecations are changed like
https://gn-review.googlesource.com/c/gn/+/17620
if 'no_stamp_files' is enabled by default.

Bug: 172
Change-Id: If26e64c759ed34467ce13b5c5c1005d56f435947
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/12866
Reviewed-by: Philipp Wollermann <philwo@google.com>
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: Takuto Ikuta <tikuta@google.com>
diff --git a/docs/reference.md b/docs/reference.md
index 82cfd80..f2e6c42 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -2957,7 +2957,7 @@
   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
+  phony target that Ninja completes once all outputs are generated. This
   probably isn't very useful.
 ```
 
diff --git a/src/gn/commands.cc b/src/gn/commands.cc
index ba7cff0..dbf2b48 100644
--- a/src/gn/commands.cc
+++ b/src/gn/commands.cc
@@ -268,8 +268,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->has_dependency_output())
+      output_file = target->dependency_output();
+
+    // 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/function_get_target_outputs.cc b/src/gn/function_get_target_outputs.cc
index c9f10c0..2686348 100644
--- a/src/gn/function_get_target_outputs.cc
+++ b/src/gn/function_get_target_outputs.cc
@@ -46,7 +46,7 @@
   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
+  phony target that Ninja completes 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 06da244..0fe0c1b 100644
--- a/src/gn/ninja_action_target_writer.cc
+++ b/src/gn/ninja_action_target_writer.cc
@@ -40,7 +40,9 @@
 
   for (const Target* dep : target_deps.linked_deps()) {
     if (dep->IsDataOnly()) {
-      order_only_deps.push_back(dep->dependency_output_file());
+      if (dep->has_dependency_output()) {
+        order_only_deps.push_back(dep->dependency_output());
+      }
     } else {
       additional_hard_deps.push_back(dep);
     }
@@ -60,16 +62,19 @@
   //    and on an incremental build, if the now-implicit dependencies are
   //    'dirty', this action will be considered 'dirty' as well.
   //
-  for (const Target* data_dep : target_deps.data_deps())
-    order_only_deps.push_back(data_dep->dependency_output_file());
+  for (const Target* data_dep : target_deps.data_deps()) {
+    if (data_dep->has_dependency_output()) {
+      order_only_deps.push_back(data_dep->dependency_output());
+    }
+  }
 
   // For ACTIONs, the input deps appear only once in the generated ninja
-  // file, so WriteInputDepsStampAndGetDep() won't create a stamp file
+  // file, so WriteInputDepsStampOrPhonyAndGetDep() won't create a phony rule
   // and the action will just depend on all the input deps directly.
-  size_t num_stamp_uses =
+  size_t num_output_uses =
       target_->output_type() == Target::ACTION ? 1u : target_->sources().size();
-  std::vector<OutputFile> input_deps =
-      WriteInputDepsStampAndGetDep(additional_hard_deps, num_stamp_uses);
+  std::vector<OutputFile> input_deps = WriteInputDepsStampOrPhonyAndGetDep(
+      additional_hard_deps, num_output_uses);
   out_ << std::endl;
 
   // Collects all output files for writing below.
@@ -119,13 +124,12 @@
   }
   out_ << std::endl;
 
-  // Write the stamp, which doesn't need to depend on the data deps because they
+  // Write the phony, which doesn't need to depend on the data deps because they
   // have been added as order-only deps of the action output itself.
   //
   // 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> stamp_file_order_only_deps;
-  WriteStampForTarget(output_files, stamp_file_order_only_deps);
+  WriteStampOrPhonyForTarget(output_files, stamp_file_order_only_deps);
 }
 
 std::string NinjaActionTargetWriter::WriteRuleDefinition() {
diff --git a/src/gn/ninja_binary_target_writer.cc b/src/gn/ninja_binary_target_writer.cc
index 7dc6ea8..c244c7c 100644
--- a/src/gn/ninja_binary_target_writer.cc
+++ b/src/gn/ninja_binary_target_writer.cc
@@ -7,6 +7,7 @@
 #include <sstream>
 
 #include "base/strings/string_util.h"
+#include "gn/builtin_tool.h"
 #include "gn/config_values_extractors.h"
 #include "gn/deps_iterator.h"
 #include "gn/filesystem_utils.h"
@@ -55,8 +56,9 @@
   writer.Run();
 }
 
-std::vector<OutputFile> NinjaBinaryTargetWriter::WriteInputsStampAndGetDep(
-    size_t num_stamp_uses) const {
+std::vector<OutputFile>
+NinjaBinaryTargetWriter::WriteInputsStampOrPhonyAndGetDep(
+    size_t num_output_uses) const {
   CHECK(target_->toolchain()) << "Toolchain not set on target "
                               << target_->label().GetUserVisibleName(true);
 
@@ -70,8 +72,8 @@
   if (inputs.size() == 0)
     return std::vector<OutputFile>();  // No inputs
 
-  // If we only have one input, return it directly instead of writing a stamp
-  // file for it.
+  // If we only have one input, return it directly instead of writing a phony
+  // target for it.
   if (inputs.size() == 1) {
     return std::vector<OutputFile>{
         OutputFile(settings_->build_settings(), *inputs[0])};
@@ -81,22 +83,35 @@
   for (const SourceFile* source : inputs)
     outs.push_back(OutputFile(settings_->build_settings(), *source));
 
-  // If there are multiple inputs, but the stamp file would be referenced only
+  // If there are multiple inputs, but the phony target would be referenced only
   // once, don't write it but depend on the inputs directly.
-  if (num_stamp_uses == 1u)
+  if (num_output_uses == 1u)
     return outs;
 
-  // Make a stamp file.
-  OutputFile stamp_file =
-      GetBuildDirForTargetAsOutputFile(target_, BuildDirType::OBJ);
-  stamp_file.value().append(target_->label().name());
-  stamp_file.value().append(".inputs.stamp");
+  OutputFile stamp_or_phony;
+  std::string tool;
+  if (settings_->build_settings()->no_stamp_files()) {
+    // Make a phony target. We don't need to worry about an empty phony target,
+    // as those would have been peeled off already.
+    CHECK(!inputs.empty());
+    stamp_or_phony =
+        GetBuildDirForTargetAsOutputFile(target_, BuildDirType::PHONY);
+    stamp_or_phony.value().append(target_->label().name());
+    stamp_or_phony.value().append(".inputs");
+    tool = BuiltinTool::kBuiltinToolPhony;
+  } else {
+    // Make a stamp target.
+    stamp_or_phony =
+        GetBuildDirForTargetAsOutputFile(target_, BuildDirType::OBJ);
+    stamp_or_phony.value().append(target_->label().name());
+    stamp_or_phony.value().append(".inputs.stamp");
+    tool = GetNinjaRulePrefixForToolchain(settings_) +
+           GeneralTool::kGeneralToolStamp;
+  }
 
   out_ << "build ";
-  WriteOutput(stamp_file);
-
-  out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
-       << GeneralTool::kGeneralToolStamp;
+  WriteOutput(stamp_or_phony);
+  out_ << ": " << tool;
 
   // File inputs.
   for (const auto* input : inputs) {
@@ -105,7 +120,7 @@
   }
 
   out_ << std::endl;
-  return {stamp_file};
+  return {stamp_or_phony};
 }
 
 NinjaBinaryTargetWriter::ClassifiedDeps
@@ -173,7 +188,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 c013cf8..29105b4 100644
--- a/src/gn/ninja_binary_target_writer.h
+++ b/src/gn/ninja_binary_target_writer.h
@@ -32,15 +32,15 @@
     UniqueVector<const Target*> swiftmodule_deps;
   };
 
-  // Writes to the output stream a stamp rule for inputs, and
-  // returns the file to be appended to source rules that encodes the
+  // Writes to the output stream a phony rule for inputs, and
+  // returns the target to be appended to source rules that encodes the
   // implicit dependencies for the current target.
-  // If num_stamp_uses is small, this might return all input dependencies
-  // directly, without writing a stamp file.
+  // If num_output_uses is small, this might return all input dependencies
+  // directly, without writing a phony rule.
   // If there are no implicit dependencies and no extra target dependencies
   // are passed in, this returns an empty vector.
-  std::vector<OutputFile> WriteInputsStampAndGetDep(
-      size_t num_stamp_uses) const;
+  std::vector<OutputFile> WriteInputsStampOrPhonyAndGetDep(
+      size_t num_phony_uses) const;
 
   // 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_build_writer.cc b/src/gn/ninja_build_writer.cc
index 91e6860..f959bfa 100644
--- a/src/gn/ninja_build_writer.cc
+++ b/src/gn/ninja_build_writer.cc
@@ -663,8 +663,10 @@
     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->has_dependency_output()) {
+        out_ << " $\n    ";
+        path_output_.WriteFile(out_, target->dependency_output());
+      }
     }
   }
   out_ << std::endl;
@@ -673,9 +675,12 @@
     // Use the short name when available
     if (written_rules.find(StringAtom("default")) != written_rules.end()) {
       out_ << "\ndefault default" << std::endl;
-    } else {
+    } else if (default_target->has_dependency_output()) {
+      // 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());
       out_ << std::endl;
     }
   } else if (!default_toolchain_targets_.empty()) {
@@ -693,7 +698,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(), 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->has_dependency_output()) {
+    path_output_.WriteFile(out_, target->dependency_output());
+  }
   out_ << std::endl;
 }
diff --git a/src/gn/ninja_bundle_data_target_writer.cc b/src/gn/ninja_bundle_data_target_writer.cc
index da3bb4e..4ba1733 100644
--- a/src/gn/ninja_bundle_data_target_writer.cc
+++ b/src/gn/ninja_bundle_data_target_writer.cc
@@ -21,13 +21,15 @@
         OutputFile(settings_->build_settings(), source_file));
   }
 
-  std::vector<OutputFile> input_deps = WriteInputDepsStampAndGetDep(
-      std::vector<const Target*>(), /*num_stamp_uses=*/1);
+  std::vector<OutputFile> input_deps = WriteInputDepsStampOrPhonyAndGetDep(
+      std::vector<const Target*>(), /*num_output_uses=*/1);
   output_files.insert(output_files.end(), input_deps.begin(), input_deps.end());
 
   std::vector<OutputFile> order_only_deps;
-  for (const Target* data_dep : resolved().GetDataDeps(target_))
-    order_only_deps.push_back(data_dep->dependency_output_file());
+  for (const Target* data_dep : resolved().GetDataDeps(target_)) {
+    if (data_dep->has_dependency_output())
+      order_only_deps.push_back(data_dep->dependency_output());
+  }
 
-  WriteStampForTarget(output_files, order_only_deps);
+  WriteStampOrPhonyForTarget(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 730840c..96907f7 100644
--- a/src/gn/ninja_c_binary_target_writer.cc
+++ b/src/gn/ninja_c_binary_target_writer.cc
@@ -134,10 +134,10 @@
 
   WriteCompilerVars(module_dep_info);
 
-  size_t num_stamp_uses = target_->sources().size();
+  size_t num_output_uses = target_->sources().size();
 
   std::vector<OutputFile> input_deps =
-      WriteInputsStampAndGetDep(num_stamp_uses);
+      WriteInputsStampOrPhonyAndGetDep(num_output_uses);
 
   // The input dependencies will be an order-only dependency. This will cause
   // Ninja to make sure the inputs are up to date before compiling this source,
@@ -166,11 +166,11 @@
   // The order only deps are referenced by each source file compile,
   // but also by PCH compiles.  The latter are annoying to count, so omit
   // them here.  This means that binary targets with a single source file
-  // that also use PCH files won't have a stamp file even though having
+  // that also use PCH files won't have a phony target even though having
   // one would make output ninja file size a bit lower. That's ok, binary
   // targets with a single source are rare.
-  std::vector<OutputFile> order_only_deps = WriteInputDepsStampAndGetDep(
-      std::vector<const Target*>(), num_stamp_uses);
+  std::vector<OutputFile> order_only_deps = WriteInputDepsStampOrPhonyAndGetDep(
+      std::vector<const Target*>(), num_output_uses);
 
   // For GCC builds, the .gch files are not object files, but still need to be
   // added as explicit dependencies below. The .gch output files are placed in
@@ -525,7 +525,9 @@
 
   for (const Target* swiftmodule :
        resolved().GetSwiftModuleDependencies(target_)) {
-    swift_order_only_deps.push_back(swiftmodule->dependency_output_file());
+      CHECK(swiftmodule->has_dependency_output()); {
+    swift_order_only_deps.push_back(swiftmodule->dependency_output());
+    }
   }
 
   const Tool* tool = target_->swift_values().GetTool(target_);
@@ -552,10 +554,13 @@
   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->has_dependency_output()) {
+      order_only_deps.push_back(dep->dependency_output());
+    }
+  }
 
-  WriteStampForTarget(object_files, order_only_deps);
+  WriteStampOrPhonyForTarget(object_files, order_only_deps);
 }
 
 void NinjaCBinaryTargetWriter::WriteLinkerStuff(
@@ -569,8 +574,7 @@
   out_ << "build";
   WriteOutputs(output_files);
 
-  out_ << ": " << rule_prefix_
-       << Tool::GetToolTypeForTargetFinalOutput(target_);
+  out_ << ": " << rule_prefix_ << tool_->name();
 
   ClassifiedDeps classified_deps = GetClassifiedDeps();
 
@@ -591,11 +595,11 @@
         cur->output_type() == Target::RUST_PROC_MACRO)
       continue;
 
-    if (cur->dependency_output_file().value() !=
-        cur->link_output_file().value()) {
+    if (cur->has_dependency_output() &&
+        cur->dependency_output().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());
       solibs.push_back(cur->link_output_file());
     } else {
       // Normal case, just link to this target.
@@ -625,12 +629,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->has_dependency_output())
+      implicit_deps.push_back(dep->dependency_output());
   }
 
   // The input dependency is only needed if there are no object files, as the
@@ -646,6 +651,7 @@
     for (const auto& inherited : resolved().GetInheritedLibraries(target_)) {
       const Target* dep = inherited.target();
       if (dep->output_type() == Target::RUST_LIBRARY) {
+        CHECK(dep->has_dependency_output_file());
         transitive_rustlibs.push_back(dep->dependency_output_file());
         implicit_deps.push_back(dep->dependency_output_file());
       }
@@ -679,11 +685,11 @@
   // this target.
   //
   // The action dependencies are not strictly necessary in this case. They
-  // should also have been collected via the input deps stamp that each source
-  // file has for an order-only dependency, and since this target depends on
-  // the sources, there is already an implicit order-only dependency. However,
-  // it's extra work to separate these out and there's no disadvantage to
-  // listing them again.
+  // should also have been collected via the input deps phony alias that each
+  // source file has for an order-only dependency, and since this target depends
+  // on the sources, there is already an implicit order-only dependency.
+  // However, it's extra work to separate these out and there's no disadvantage
+  // to listing them again.
   WriteOrderOnlyDependencies(classified_deps.non_linkable_deps);
 
   // End of the link "build" line.
@@ -758,8 +764,10 @@
 
     // 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->has_dependency_output()) {
+        out_ << " ";
+        path_output_.WriteFile(out_, non_linkable_dep->dependency_output());
+      }
     }
   }
 }
diff --git a/src/gn/ninja_c_binary_target_writer.h b/src/gn/ninja_c_binary_target_writer.h
index a50d8c9..f60790f 100644
--- a/src/gn/ninja_c_binary_target_writer.h
+++ b/src/gn/ninja_c_binary_target_writer.h
@@ -39,7 +39,7 @@
   // non-object files (for instance, .gch files from a GCC toolchain, are
   // appended to |other_files|).
   //
-  // input_deps is the stamp file collecting the dependencies required before
+  // input_deps is the phony target collecting the dependencies required before
   // compiling this target. It will be empty if there are no input deps.
   void WritePCHCommands(const std::vector<OutputFile>& input_deps,
                         const std::vector<OutputFile>& order_only_deps,
diff --git a/src/gn/ninja_copy_target_writer.cc b/src/gn/ninja_copy_target_writer.cc
index 0ad7d67..47657c0 100644
--- a/src/gn/ninja_copy_target_writer.cc
+++ b/src/gn/ninja_copy_target_writer.cc
@@ -34,29 +34,29 @@
     return;
   }
 
-  const Tool* stamp_tool =
-      target_->toolchain()->GetTool(GeneralTool::kGeneralToolStamp);
-  if (!stamp_tool) {
-    g_scheduler->FailWithError(Err(
-        nullptr, "Stamp tool not defined",
-        "The toolchain " +
-            target_->toolchain()->label().GetUserVisibleName(false) +
-            "\n used by target " + target_->label().GetUserVisibleName(false) +
-            "\n doesn't define a \"stamp\" tool."));
-    return;
+  SubstitutionBits required_bits = copy_tool->substitution_bits();
+  if (!settings_->build_settings()->no_stamp_files()) {
+    const Tool* stamp_tool =
+        target_->toolchain()->GetTool(GeneralTool::kGeneralToolStamp);
+    if (!stamp_tool) {
+      g_scheduler->FailWithError(
+          Err(nullptr, "Stamp tool not defined",
+              "The toolchain " +
+                  target_->toolchain()->label().GetUserVisibleName(false) +
+                  "\n used by target " +
+                  target_->label().GetUserVisibleName(false) +
+                  "\n doesn't define a \"stamp\" tool."));
+    }
+    required_bits.MergeFrom(stamp_tool->substitution_bits());
   }
 
-  // Figure out the substitutions used by the copy and stamp tools.
-  SubstitutionBits required_bits = copy_tool->substitution_bits();
-  required_bits.MergeFrom(stamp_tool->substitution_bits());
-
-  // General target-related substitutions needed by both tools.
+  // General target-related substitutions needed by the copy/stamp tool.
   WriteSharedVars(required_bits);
 
   std::vector<OutputFile> output_files;
   WriteCopyRules(&output_files);
   out_ << std::endl;
-  WriteStampForTarget(output_files, std::vector<OutputFile>());
+  WriteStampOrPhonyForTarget(output_files, std::vector<OutputFile>());
 }
 
 void NinjaCopyTargetWriter::WriteCopyRules(
@@ -71,13 +71,15 @@
   std::string tool_name =
       GetNinjaRulePrefixForToolchain(settings_) + GeneralTool::kGeneralToolCopy;
 
-  size_t num_stamp_uses = target_->sources().size();
-  std::vector<OutputFile> input_deps = WriteInputDepsStampAndGetDep(
-      std::vector<const Target*>(), num_stamp_uses);
+  size_t num_output_uses = target_->sources().size();
+  std::vector<OutputFile> input_deps = WriteInputDepsStampOrPhonyAndGetDep(
+      std::vector<const Target*>(), num_output_uses);
 
   std::vector<OutputFile> data_outs;
-  for (const Target* data_dep : resolved().GetDataDeps(target_))
-    data_outs.push_back(data_dep->dependency_output_file());
+  for (const Target* data_dep : resolved().GetDataDeps(target_)) {
+    if (data_dep->has_dependency_output())
+      data_outs.push_back(data_dep->dependency_output());
+  }
 
   // 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 a453fce..87f0ab7 100644
--- a/src/gn/ninja_create_bundle_target_writer.cc
+++ b/src/gn/ninja_create_bundle_target_writer.cc
@@ -7,6 +7,7 @@
 #include <iterator>
 
 #include "base/strings/string_util.h"
+#include "gn/builtin_tool.h"
 #include "gn/filesystem_utils.h"
 #include "gn/general_tool.h"
 #include "gn/ninja_utils.h"
@@ -79,7 +80,7 @@
   // Stamp users are CopyBundleData, CompileAssetsCatalog, PostProcessing and
   // StampForTarget.
   size_t num_stamp_uses = 4;
-  std::vector<OutputFile> order_only_deps = WriteInputDepsStampAndGetDep(
+  std::vector<OutputFile> order_only_deps = WriteInputDepsStampOrPhonyAndGetDep(
       std::vector<const Target*>(), num_stamp_uses);
 
   std::string post_processing_rule_name = WritePostProcessingRuleDefinition();
@@ -90,9 +91,17 @@
   WritePostProcessingStep(post_processing_rule_name, order_only_deps,
                           &output_files);
 
-  for (const Target* data_dep : resolved().GetDataDeps(target_))
-    order_only_deps.push_back(data_dep->dependency_output_file());
-  WriteStampForTarget(output_files, order_only_deps);
+  for (const Target* data_dep : resolved().GetDataDeps(target_)) {
+    if (data_dep->has_dependency_output())
+      order_only_deps.push_back(data_dep->dependency_output());
+  }
+
+  // If the target does not have a phony target to write, then we have nothing
+  // left to do.
+  if (!target_->has_dependency_output())
+    return;
+
+  WriteStampOrPhonyForTarget(output_files, order_only_deps);
 
   // Write a phony target for the outer bundle directory. This allows other
   // targets to treat the entire bundle as a single unit, even though it is
@@ -102,8 +111,8 @@
   WriteOutput(
       OutputFile(settings_->build_settings(),
                  target_->bundle_data().GetBundleRootDirOutput(settings_)));
-
-  out_ << ": phony " << target_->dependency_output_file().value();
+  out_ << ": " << BuiltinTool::kBuiltinToolPhony << " ";
+  out_ << target_->dependency_output().value();
   out_ << std::endl;
 }
 
@@ -219,7 +228,7 @@
     return;
   }
 
-  OutputFile input_dep = WriteCompileAssetsCatalogInputDepsStamp(
+  OutputFile input_dep = WriteCompileAssetsCatalogInputDepsStampOrPhony(
       target_->bundle_data().assets_catalog_deps());
   DCHECK(!input_dep.value().empty());
 
@@ -279,28 +288,45 @@
 }
 
 OutputFile
-NinjaCreateBundleTargetWriter::WriteCompileAssetsCatalogInputDepsStamp(
+NinjaCreateBundleTargetWriter::WriteCompileAssetsCatalogInputDepsStampOrPhony(
     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]->has_dependency_output()
+               ? dependencies[0]->dependency_output()
+               : OutputFile{};
+  }
 
-  OutputFile xcassets_input_stamp_file =
-      GetBuildDirForTargetAsOutputFile(target_, BuildDirType::OBJ);
-  xcassets_input_stamp_file.value().append(target_->label().name());
-  xcassets_input_stamp_file.value().append(".xcassets.inputdeps.stamp");
+  OutputFile xcassets_input_stamp_or_phony;
+  std::string tool;
+  if (settings_->build_settings()->no_stamp_files()) {
+    xcassets_input_stamp_or_phony =
+        GetBuildDirForTargetAsOutputFile(target_, BuildDirType::PHONY);
+
+    xcassets_input_stamp_or_phony.value().append(target_->label().name());
+    xcassets_input_stamp_or_phony.value().append(".xcassets.inputdeps");
+    tool = BuiltinTool::kBuiltinToolPhony;
+  } else {
+    xcassets_input_stamp_or_phony =
+        GetBuildDirForTargetAsOutputFile(target_, BuildDirType::OBJ);
+    xcassets_input_stamp_or_phony.value().append(target_->label().name());
+    xcassets_input_stamp_or_phony.value().append(".xcassets.inputdeps.stamp");
+    tool = GetNinjaRulePrefixForToolchain(settings_) +
+           GeneralTool::kGeneralToolStamp;
+  }
 
   out_ << "build ";
-  WriteOutput(xcassets_input_stamp_file);
-  out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
-       << GeneralTool::kGeneralToolStamp;
+  WriteOutput(xcassets_input_stamp_or_phony);
+  out_ << ": " << tool;
 
   for (const Target* target : dependencies) {
-    out_ << " ";
-    path_output_.WriteFile(out_, target->dependency_output_file());
+    if (target->has_dependency_output()) {
+      out_ << " ";
+      path_output_.WriteFile(out_, target->dependency_output());
+    }
   }
   out_ << std::endl;
-  return xcassets_input_stamp_file;
+  return xcassets_input_stamp_or_phony;
 }
 
 void NinjaCreateBundleTargetWriter::WritePostProcessingStep(
@@ -311,7 +337,7 @@
     return;
 
   OutputFile post_processing_input_stamp_file =
-      WritePostProcessingInputDepsStamp(order_only_deps, output_files);
+      WritePostProcessingInputDepsStampOrPhony(order_only_deps, output_files);
   DCHECK(!post_processing_input_stamp_file.value().empty());
 
   out_ << "build";
@@ -332,7 +358,8 @@
   out_ << std::endl;
 }
 
-OutputFile NinjaCreateBundleTargetWriter::WritePostProcessingInputDepsStamp(
+OutputFile
+NinjaCreateBundleTargetWriter::WritePostProcessingInputDepsStampOrPhony(
     const std::vector<OutputFile>& order_only_deps,
     std::vector<OutputFile>* output_files) {
   std::vector<SourceFile> post_processing_input_files;
@@ -352,16 +379,28 @@
     return OutputFile(settings_->build_settings(),
                       post_processing_input_files[0]);
 
-  OutputFile post_processing_input_stamp_file =
-      GetBuildDirForTargetAsOutputFile(target_, BuildDirType::OBJ);
-  post_processing_input_stamp_file.value().append(target_->label().name());
-  post_processing_input_stamp_file.value().append(
-      ".postprocessing.inputdeps.stamp");
+  OutputFile stamp_or_phony;
+  std::string tool;
+  if (settings_->build_settings()->no_stamp_files()) {
+    // Make a phony target. We don't need to worry about an empty phony target,
+    // as those would have been peeled off already.
+    stamp_or_phony =
+        GetBuildDirForTargetAsOutputFile(target_, BuildDirType::PHONY);
+    stamp_or_phony.value().append(target_->label().name());
+    stamp_or_phony.value().append(".postprocessing.inputdeps");
+    tool = BuiltinTool::kBuiltinToolPhony;
+  } else {
+    // Make a stamp target.
+    stamp_or_phony =
+        GetBuildDirForTargetAsOutputFile(target_, BuildDirType::OBJ);
+    stamp_or_phony.value().append(target_->label().name());
+    stamp_or_phony.value().append(".postprocessing.inputdeps.stamp");
+    tool = GeneralTool::kGeneralToolStamp;
+  }
 
   out_ << "build ";
-  WriteOutput(post_processing_input_stamp_file);
-  out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
-       << GeneralTool::kGeneralToolStamp;
+  WriteOutput(stamp_or_phony);
+  out_ << ": " << GetNinjaRulePrefixForToolchain(settings_) << tool;
 
   for (const SourceFile& source : post_processing_input_files) {
     out_ << " ";
@@ -372,5 +411,5 @@
     path_output_.WriteFiles(out_, order_only_deps);
   }
   out_ << std::endl;
-  return post_processing_input_stamp_file;
+  return stamp_or_phony;
 }
diff --git a/src/gn/ninja_create_bundle_target_writer.h b/src/gn/ninja_create_bundle_target_writer.h
index 609bc6b..ee27557 100644
--- a/src/gn/ninja_create_bundle_target_writer.h
+++ b/src/gn/ninja_create_bundle_target_writer.h
@@ -45,9 +45,9 @@
       const std::vector<OutputFile>& order_only_deps,
       std::vector<OutputFile>* output_files);
 
-  // Writes the stamp file for the assets catalog compilation input
+  // Writes the phony target for the assets catalog compilation input
   // dependencies.
-  OutputFile WriteCompileAssetsCatalogInputDepsStamp(
+  OutputFile WriteCompileAssetsCatalogInputDepsStampOrPhony(
       const std::vector<const Target*>& dependencies);
 
   // Writes the post-processing step (if a script is defined).
@@ -59,8 +59,9 @@
                                const std::vector<OutputFile>& order_only_deps,
                                std::vector<OutputFile>* output_files);
 
-  // Writes the stamp file for the post-processing input dependencies.
-  OutputFile WritePostProcessingInputDepsStamp(
+  // Writes the stamp file or phony target for the post-processing input
+  // dependencies.
+  OutputFile WritePostProcessingInputDepsStampOrPhony(
       const std::vector<OutputFile>& order_only_deps,
       std::vector<OutputFile>* output_files);
 
diff --git a/src/gn/ninja_generated_file_target_writer.cc b/src/gn/ninja_generated_file_target_writer.cc
index 307c12b..6de8207 100644
--- a/src/gn/ninja_generated_file_target_writer.cc
+++ b/src/gn/ninja_generated_file_target_writer.cc
@@ -24,7 +24,7 @@
   // Write the file.
   GenerateFile();
 
-  // A generated_file target should generate a stamp file with dependencies
+  // A generated_file target should generate a phony target with dependencies
   // on each of the deps and data_deps in the target. The actual collection is
   // done at gen time, but to have correct input deps in ninja, we add output
   // from generated_file targets as deps for the stamp.
@@ -32,17 +32,22 @@
   std::vector<OutputFile> data_output_files;
   const auto& target_deps = resolved().GetTargetDeps(target_);
   for (const Target* dep : target_deps.linked_deps()) {
+    if (!dep->has_dependency_output()) {
+      continue;
+    }
     if (dep->IsDataOnly()) {
-      data_output_files.push_back(dep->dependency_output_file());
+      data_output_files.push_back(dep->dependency_output());
     } else {
-      output_files.push_back(dep->dependency_output_file());
+      output_files.push_back(dep->dependency_output());
     }
   }
 
-  for (const Target* data_dep : target_deps.data_deps())
-    data_output_files.push_back(data_dep->dependency_output_file());
+  for (const Target* data_dep : target_deps.data_deps()) {
+    if (data_dep->has_dependency_output())
+      data_output_files.push_back(data_dep->dependency_output());
+  }
 
-  WriteStampForTarget(output_files, data_output_files);
+  WriteStampOrPhonyForTarget(output_files, data_output_files);
 }
 
 void NinjaGeneratedFileTargetWriter::GenerateFile() {
diff --git a/src/gn/ninja_group_target_writer.cc b/src/gn/ninja_group_target_writer.cc
index 58d6ce7..7db1a3a 100644
--- a/src/gn/ninja_group_target_writer.cc
+++ b/src/gn/ninja_group_target_writer.cc
@@ -17,22 +17,26 @@
 NinjaGroupTargetWriter::~NinjaGroupTargetWriter() = default;
 
 void NinjaGroupTargetWriter::Run() {
-  // A group rule just generates a stamp file with dependencies on each of
+  // A group rule just generates a phony target with dependencies on each of
   // the deps and data_deps in the group.
   std::vector<OutputFile> output_files;
   std::vector<OutputFile> data_output_files;
   const auto& target_deps = resolved().GetTargetDeps(target_);
 
   for (const Target* dep : target_deps.linked_deps()) {
+    if (!dep->has_dependency_output())
+      continue;
     if (dep->IsDataOnly()) {
-      data_output_files.push_back(dep->dependency_output_file());
+      data_output_files.push_back(dep->dependency_output());
     } else {
-      output_files.push_back(dep->dependency_output_file());
+      output_files.push_back(dep->dependency_output());
     }
   }
 
-  for (const Target* data_dep : target_deps.data_deps())
-    data_output_files.push_back(data_dep->dependency_output_file());
+  for (const Target* data_dep : target_deps.data_deps()) {
+    if (data_dep->has_dependency_output())
+      data_output_files.push_back(data_dep->dependency_output());
+  }
 
-  WriteStampForTarget(output_files, data_output_files);
+  WriteStampOrPhonyForTarget(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 a566dec..ba6b416 100644
--- a/src/gn/ninja_rust_binary_target_writer.cc
+++ b/src/gn/ninja_rust_binary_target_writer.cc
@@ -111,10 +111,10 @@
 void NinjaRustBinaryTargetWriter::Run() {
   DCHECK(target_->output_type() != Target::SOURCE_SET);
 
-  size_t num_stamp_uses = target_->sources().size();
+  size_t num_output_uses = target_->sources().size();
 
   std::vector<OutputFile> input_deps =
-      WriteInputsStampAndGetDep(num_stamp_uses);
+      WriteInputsStampOrPhonyAndGetDep(num_output_uses);
 
   WriteCompilerVars();
 
@@ -125,8 +125,8 @@
   // Ninja to make sure the inputs are up to date before compiling this source,
   // but changes in the inputs deps won't cause the file to be recompiled. See
   // the comment on NinjaCBinaryTargetWriter::Run for more detailed explanation.
-  std::vector<OutputFile> order_only_deps = WriteInputDepsStampAndGetDep(
-      std::vector<const Target*>(), num_stamp_uses);
+  std::vector<OutputFile> order_only_deps = WriteInputDepsStampOrPhonyAndGetDep(
+      std::vector<const Target*>(), num_output_uses);
   std::copy(input_deps.begin(), input_deps.end(),
             std::back_inserter(order_only_deps));
 
@@ -146,7 +146,9 @@
                      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());
+    if (framework_dep->has_dependency_output_file()) {
+      order_only_deps.push_back(framework_dep->dependency_output_file());
+    }
   }
   if (target_->IsFinal()) {
     for (const Target* dep : classified_deps.swiftmodule_deps) {
@@ -155,11 +157,13 @@
     }
   }
   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->has_dependency_output()) {
+      if (non_linkable_dep->source_types_used().RustSourceUsed() &&
+          non_linkable_dep->output_type() != Target::SOURCE_SET) {
+        rustdeps.push_back(non_linkable_dep->dependency_output());
+      }
+      order_only_deps.push_back(non_linkable_dep->dependency_output());
     }
-    order_only_deps.push_back(non_linkable_dep->dependency_output_file());
   }
   for (const auto* linkable_dep : classified_deps.linkable_deps) {
     // Rust cdylibs are treated as non-Rust dependencies for linking purposes.
@@ -169,6 +173,7 @@
     } else {
       nonrustdeps.push_back(linkable_dep->link_output_file());
     }
+    CHECK(linkable_dep->has_dependency_output_file());
     implicit_deps.push_back(linkable_dep->dependency_output_file());
   }
 
diff --git a/src/gn/ninja_target_writer.cc b/src/gn/ninja_target_writer.cc
index b8b156f..01a1407 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/c_substitution_type.h"
 #include "gn/config_values_extractors.h"
 #include "gn/err.h"
@@ -432,16 +433,16 @@
   }
 }
 
-std::vector<OutputFile> NinjaTargetWriter::WriteInputDepsStampAndGetDep(
+std::vector<OutputFile> NinjaTargetWriter::WriteInputDepsStampOrPhonyAndGetDep(
     const std::vector<const Target*>& additional_hard_deps,
-    size_t num_stamp_uses) const {
+    size_t num_output_uses) const {
   CHECK(target_->toolchain()) << "Toolchain not set on target "
                               << target_->label().GetUserVisibleName(true);
 
   // ----------
   // Collect all input files that are input deps of this target. Knowing the
   // number before writing allows us to either skip writing the input deps
-  // stamp or optimize it. Use pointers to avoid copies here.
+  // phony or optimize it. Use pointers to avoid copies here.
   std::vector<const SourceFile*> input_deps_sources;
   input_deps_sources.reserve(32);
 
@@ -496,7 +497,7 @@
   // Toolchain dependencies. These must be resolved before doing anything.
   // This just writes all toolchain deps for simplicity. If we find that
   // toolchains often have more than one dependency, we could consider writing
-  // a toolchain-specific stamp file and only include the stamp here.
+  // a toolchain-specific phony target and only include the phony here.
   // Note that these are usually empty/small.
   const LabelTargetVector& toolchain_deps = target_->toolchain()->deps();
   for (const auto& toolchain_dep : toolchain_deps) {
@@ -513,14 +514,15 @@
     return std::vector<OutputFile>();  // No input dependencies.
 
   // If we're only generating one input dependency, return it directly instead
-  // of writing a stamp file for it.
+  // of writing a phony target for it.
   if (input_deps_sources.size() == 1 && input_deps_targets.size() == 0)
     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 auto& dep = *input_deps_targets[0];
+    if (!dep.has_dependency_output())
+      return std::vector<OutputFile>();
+    return std::vector<OutputFile>{dep.dependency_output()};
   }
 
   std::vector<OutputFile> outs;
@@ -534,48 +536,85 @@
       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->has_dependency_output())
+      outs.push_back(dep->dependency_output());
   }
 
-  // If there are multiple inputs, but the stamp file would be referenced only
+  // If there are multiple inputs, but the phony target would be referenced only
   // once, don't write it but depend on the inputs directly.
-  if (num_stamp_uses == 1u)
+  if (num_output_uses == 1u)
     return outs;
 
-  // Make a stamp file.
-  OutputFile input_stamp_file =
-      GetBuildDirForTargetAsOutputFile(target_, BuildDirType::OBJ);
-  input_stamp_file.value().append(target_->label().name());
-  input_stamp_file.value().append(".inputdeps.stamp");
+  OutputFile input_stamp_or_phony;
+  std::string tool;
+  if (settings_->build_settings()->no_stamp_files()) {
+    // Make a phony target. We don't need to worry about an empty phony target,
+    // as we would return early if there were no inputs.
+    CHECK(!outs.empty());
+    input_stamp_or_phony =
+        GetBuildDirForTargetAsOutputFile(target_, BuildDirType::PHONY);
+    input_stamp_or_phony.value().append(target_->label().name());
+    input_stamp_or_phony.value().append(".inputdeps");
+    tool = BuiltinTool::kBuiltinToolPhony;
+  } else {
+    // Make a stamp file.
+    input_stamp_or_phony =
+        GetBuildDirForTargetAsOutputFile(target_, BuildDirType::OBJ);
+    input_stamp_or_phony.value().append(target_->label().name());
+    input_stamp_or_phony.value().append(".inputdeps.stamp");
+
+    tool = GetNinjaRulePrefixForToolchain(settings_) +
+           GeneralTool::kGeneralToolStamp;
+  }
 
   out_ << "build ";
-  WriteOutput(input_stamp_file);
-
-  out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
-       << GeneralTool::kGeneralToolStamp;
-  path_output_.WriteFiles(out_, outs);
-
+  WriteOutput(input_stamp_or_phony);
+  out_ << ": " << tool;
+  WriteOutputs(outs);
   out_ << "\n";
-  return std::vector<OutputFile>{input_stamp_file};
+  return std::vector<OutputFile>{input_stamp_or_phony};
 }
 
-void NinjaTargetWriter::WriteStampForTarget(
+void NinjaTargetWriter::WriteStampOrPhonyForTarget(
     const std::vector<OutputFile>& files,
     const std::vector<OutputFile>& order_only_deps) {
-  const OutputFile& stamp_file = target_->dependency_output_file();
+  // We should have already discerned whether this target is a stamp or a phony.
+  // If there's a dependency_output_file, it should be a stamp. Else is a phony
+  // or omitted phony (in which case, we don't write it).
+  if (target_->has_dependency_output_file()) {
+    // Make a stamp target.
+    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!
-  CHECK(base::EndsWithCaseInsensitiveASCII(stamp_file.value(), ".stamp"))
-      << "Output should end in \".stamp\" for stamp file output. Instead got: "
-      << "\"" << stamp_file.value() << "\"";
+    // First validate that the target's dependency is a stamp file. Otherwise,
+    // we shouldn't have gotten here!
+    CHECK(base::EndsWithCaseInsensitiveASCII(stamp_file.value(), ".stamp"))
+        << "Output should end in \".stamp\" for stamp file output. Instead "
+           "got: "
+        << "\"" << stamp_file.value() << "\"";
 
-  out_ << "build ";
-  WriteOutput(std::move(stamp_file));
+    out_ << "build ";
+    WriteOutput(stamp_file);
 
-  out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
-       << GeneralTool::kGeneralToolStamp;
+    out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
+         << GeneralTool::kGeneralToolStamp;
+  } else if (target_->has_dependency_output_alias()) {
+    // Make a phony target.
+    const OutputFile& phony_target = target_->dependency_output_alias();
+    CHECK(!phony_target.value().empty());
+
+    out_ << "build ";
+    WriteOutput(phony_target);
+
+    out_ << ": " << BuiltinTool::kBuiltinToolPhony;
+
+  } else {
+    // This is the omitted phony case. We should not get here if there were any
+    // dependencies, so ensure that none got added.
+    CHECK(files.empty());
+    CHECK(order_only_deps.empty());
+    return;
+  }
+
   path_output_.WriteFiles(out_, files);
 
   if (!order_only_deps.empty()) {
diff --git a/src/gn/ninja_target_writer.h b/src/gn/ninja_target_writer.h
index 9655c72..68bc0a3 100644
--- a/src/gn/ninja_target_writer.h
+++ b/src/gn/ninja_target_writer.h
@@ -82,22 +82,22 @@
                              bool indent,
                              bool always_write);
 
-  // Writes to the output stream a stamp rule for input dependencies, and
+  // Writes to the output stream a phony rule for input dependencies, and
   // returns the file to be appended to source rules that encodes the
   // order-only dependencies for the current target.
-  // If num_stamp_uses is small, this might return all input dependencies
-  // directly, without writing a stamp file.
+  // If num_output_uses is small, this might return all input dependencies
+  // directly, without writing a phony rule.
   // If there are no implicit dependencies and no additional target dependencies
   // are passed in, this returns an empty vector.
-  std::vector<OutputFile> WriteInputDepsStampAndGetDep(
+  std::vector<OutputFile> WriteInputDepsStampOrPhonyAndGetDep(
       const std::vector<const Target*>& additional_hard_deps,
-      size_t num_stamp_uses) const;
+      size_t num_output_uses) const;
 
-  // Writes to the output file a final stamp rule for the target that stamps
-  // the given list of files. This function assumes the stamp is for the target
-  // as a whole so the stamp file is set as the target's dependency output.
-  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 WriteStampOrPhonyForTarget(
+      const std::vector<OutputFile>& deps,
+      const std::vector<OutputFile>& order_only_deps);
 
   const Settings* settings_;  // Non-owning.
   const Target* target_;      // Non-owning.
diff --git a/src/gn/ninja_target_writer_unittest.cc b/src/gn/ninja_target_writer_unittest.cc
index 682de45..6b88674 100644
--- a/src/gn/ninja_target_writer_unittest.cc
+++ b/src/gn/ninja_target_writer_unittest.cc
@@ -22,11 +22,11 @@
   void Run() override {}
 
   // Make this public so the test can call it.
-  std::vector<OutputFile> WriteInputDepsStampAndGetDep(
+  std::vector<OutputFile> WriteInputDepsStampOrPhonyAndGetDep(
       const std::vector<const Target*>& additional_hard_deps,
       size_t num_stamp_uses) {
-    return NinjaTargetWriter::WriteInputDepsStampAndGetDep(additional_hard_deps,
-                                                           num_stamp_uses);
+    return NinjaTargetWriter::WriteInputDepsStampOrPhonyAndGetDep(
+        additional_hard_deps, num_stamp_uses);
   }
 };
 
@@ -74,7 +74,7 @@
   EXPECT_EQ(&resolved, &writer.resolved());
 }
 
-TEST(NinjaTargetWriter, WriteInputDepsStampAndGetDep) {
+TEST(NinjaTargetWriter, WriteInputDepsStampOrPhonyAndGetDep) {
   TestWithScope setup;
   Err err;
 
@@ -113,8 +113,8 @@
   {
     std::ostringstream stream;
     TestingNinjaTargetWriter writer(&base_target, setup.toolchain(), stream);
-    std::vector<OutputFile> dep =
-        writer.WriteInputDepsStampAndGetDep(std::vector<const Target*>(), 10u);
+    std::vector<OutputFile> dep = writer.WriteInputDepsStampOrPhonyAndGetDep(
+        std::vector<const Target*>(), 10u);
 
     // Since there is only one dependency, it should just be returned and
     // nothing written to the stream.
@@ -127,8 +127,8 @@
   {
     std::ostringstream stream;
     TestingNinjaTargetWriter writer(&target, setup.toolchain(), stream);
-    std::vector<OutputFile> dep =
-        writer.WriteInputDepsStampAndGetDep(std::vector<const Target*>(), 10u);
+    std::vector<OutputFile> dep = writer.WriteInputDepsStampOrPhonyAndGetDep(
+        std::vector<const Target*>(), 10u);
 
     // Since there is only one dependency, a stamp file will be returned
     // directly without writing any additional rules.
@@ -158,8 +158,8 @@
   {
     std::ostringstream stream;
     TestingNinjaTargetWriter writer(&action, setup.toolchain(), stream);
-    std::vector<OutputFile> dep =
-        writer.WriteInputDepsStampAndGetDep(std::vector<const Target*>(), 10u);
+    std::vector<OutputFile> dep = writer.WriteInputDepsStampOrPhonyAndGetDep(
+        std::vector<const Target*>(), 10u);
 
     ASSERT_EQ(1u, dep.size());
     EXPECT_EQ("obj/foo/action.inputdeps.stamp", dep[0].value());
@@ -170,8 +170,8 @@
   }
 }
 
-// Tests WriteInputDepsStampAndGetDep when toolchain deps are present.
-TEST(NinjaTargetWriter, WriteInputDepsStampAndGetDepWithToolchainDeps) {
+// Tests WriteInputDepsStampOrPhonyAndGetDep when toolchain deps are present.
+TEST(NinjaTargetWriter, WriteInputDepsStampOrPhonyAndGetDepWithToolchainDeps) {
   TestWithScope setup;
   Err err;
 
@@ -194,8 +194,8 @@
 
   std::ostringstream stream;
   TestingNinjaTargetWriter writer(&target, setup.toolchain(), stream);
-  std::vector<OutputFile> dep =
-      writer.WriteInputDepsStampAndGetDep(std::vector<const Target*>(), 10u);
+  std::vector<OutputFile> dep = writer.WriteInputDepsStampOrPhonyAndGetDep(
+      std::vector<const Target*>(), 10u);
 
   // Since there is more than one dependency, a stamp file will be returned
   // and the rule for the stamp file will be written to the stream.
diff --git a/src/gn/runtime_deps.cc b/src/gn/runtime_deps.cc
index 9ca016d..546d63e 100644
--- a/src/gn/runtime_deps.cc
+++ b/src/gn/runtime_deps.cc
@@ -177,7 +177,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) {
@@ -186,11 +186,20 @@
       CHECK(!target->computed_outputs().empty());
       output_file =
           OutputFile(target->computed_outputs()[0].value() + extension);
-    } else {
+    } else if (target->has_dependency_output_file()) {
       output_file =
           OutputFile(target->dependency_output_file().value() + extension);
+    } else {
+      // If there is no dependency_output_file, this target's dependency output
+      // is either a phony alias or was elided entirely (due to lack of real
+      // inputs). In either case, there is no file to add an additional
+      // extension to, so we should compute our own name in the OBJ BuildDir.
+      output_file = GetBuildDirForTargetAsOutputFile(target, BuildDirType::OBJ);
+      output_file->value().append(target->GetComputedOutputName());
+      output_file->value().append(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/setup.cc b/src/gn/setup.cc
index b83e904..c9e6b8b 100644
--- a/src/gn/setup.cc
+++ b/src/gn/setup.cc
@@ -203,6 +203,10 @@
       required version is 1.7.2. Specifying a higher version might enable the
       use of some of newer features that can make the build more efficient.
 
+  no_stamp_files [optional]
+      A boolean flag that can be set to generate Ninja files that use phony
+      rules instead of stamp files whenever possible. This results in smaller
+      Ninja build plans, but requires at least Ninja 1.11.
 
 Example .gn file contents
 
@@ -1135,8 +1139,6 @@
       return false;
     }
     build_settings_.set_no_stamp_files(no_stamp_files_value->boolean_value());
-    CHECK(!build_settings_.no_stamp_files())
-        << "no_stamp_files does not work yet!";
   }
 
   // Export compile commands.
diff --git a/src/gn/switches.cc b/src/gn/switches.cc
index 95b0b14..67c18a1 100644
--- a/src/gn/switches.cc
+++ b/src/gn/switches.cc
@@ -233,9 +233,12 @@
   an output file "bar.so", GN will create a file "bar.so.runtime_deps" in the
   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.
+  For targets that don't generate an output file (such as source set, action,
+  copy or group), the runtime deps file will be in the output directory where an
+  output file would have been located. For example, the source_set target
+  "//foo:bar" would result in a runtime dependency file being written to
+  "<output_dir>/obj/foo/bar.runtime_deps". 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 0dd5689..3697843 100644
--- a/src/gn/target.cc
+++ b/src/gn/target.cc
@@ -784,10 +784,12 @@
   // 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.
 
-  // Actions always have at least one input file: the script used to execute
-  // the action. As such, they will never have an input-less phony target. We
-  // check this first to elide the common checks.
-  if (output_type() == ACTION || output_type() == ACTION_FOREACH) {
+  // Actions and generated_file always have at least one input file: the script
+  // used to execute the action or generated file itself. As such, they will
+  // never have an input-less phony target. We check this first to elide the
+  // common checks.
+  if (output_type() == ACTION || output_type() == ACTION_FOREACH ||
+      output_type() == GENERATED_FILE) {
     return true;
   }
 
diff --git a/src/gn/target.h b/src/gn/target.h
index f794035..b6947f9 100644
--- a/src/gn/target.h
+++ b/src/gn/target.h
@@ -379,28 +379,28 @@
   bool has_dependency_output() const {
     return has_dependency_output_file() || has_dependency_output_alias();
   }
-  // Returns the output dependency file path or phony alias if one is defined,
+  // Return the output dependency file path or phony alias if one is defined,
   // or an empty string otherwise.
   const OutputFile& dependency_output() const {
     return has_dependency_output_file() ? dependency_output_file_
                                         : dependency_output_alias_;
   }
 
-  // Returns true if there is a dependency file path defined for this target.
+  // Return true if there is a dependency file path defined for this target.
   bool has_dependency_output_file() const {
     return !dependency_output_file_.value().empty();
   }
-  // Returns the dependency output file path for this target if defined, or
+  // Return the dependency output file path for this target if defined, or
   // an empty string otherwise.
   const OutputFile& dependency_output_file() const {
     return dependency_output_file_;
   }
 
-  // Returns true if there is a dependency output alias defined for this target.
+  // Return true if there is a dependency output alias defined for this target.
   bool has_dependency_output_alias() const {
     return !dependency_output_alias_.value().empty();
   }
-  // Returns the dependency output alias if any, or an empty string otherwise.
+  // Return the dependency output alias if any, or an empty string otherwise.
   const OutputFile& dependency_output_alias() const {
     return dependency_output_alias_;
   }
diff --git a/src/gn/target_unittest.cc b/src/gn/target_unittest.cc
index c510ba1..cac3670 100644
--- a/src/gn/target_unittest.cc
+++ b/src/gn/target_unittest.cc
@@ -547,6 +547,7 @@
   ASSERT_TRUE(target.OnResolved(&err));
 
   EXPECT_EQ("./liba.so", target.link_output_file().value());
+  ASSERT_TRUE(target.has_dependency_output_file());
   EXPECT_EQ("./liba.so.TOC", target.dependency_output_file().value());
 
   ASSERT_EQ(1u, target.runtime_outputs().size());
@@ -635,6 +636,7 @@
   ASSERT_TRUE(target.OnResolved(&err));
 
   EXPECT_EQ("./a.dll.lib", target.link_output_file().value());
+  ASSERT_TRUE(target.has_dependency_output_file());
   EXPECT_EQ("./a.dll.lib", target.dependency_output_file().value());
 
   ASSERT_EQ(2u, target.runtime_outputs().size());
@@ -718,6 +720,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));
diff --git a/src/gn/toolchain.h b/src/gn/toolchain.h
index 270fada..cc18098 100644
--- a/src/gn/toolchain.h
+++ b/src/gn/toolchain.h
@@ -101,8 +101,8 @@
 
   // 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
-  // will return the stamp tool instead since the final output of a copy
-  // target is to stamp the set of copies done so there is one output.
+  // will return the phony tool instead since the final output of a copy
+  // target is a phony alias to the set of copies done so there is one output.
   const Tool* GetToolForTargetFinalOutput(const Target* target) const;
   const CTool* GetToolForTargetFinalOutputAsC(const Target* target) const;
   const GeneralTool* GetToolForTargetFinalOutputAsGeneral(
diff --git a/src/gn/visual_studio_writer.cc b/src/gn/visual_studio_writer.cc
index 037450e..243fedd 100644
--- a/src/gn/visual_studio_writer.cc
+++ b/src/gn/visual_studio_writer.cc
@@ -542,7 +542,7 @@
 
   project.SubElement("PropertyGroup", XmlAttributes("Label", "UserMacros"));
 
-  std::string ninja_target = GetNinjaTarget(target);
+  auto [ninja_target, ninja_target_is_phony] = GetNinjaTarget(target);
   std::string ninja_exe = GetNinjaExecutable(ninja_executable);
 
   {
@@ -550,7 +550,7 @@
         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);
     }
   }
@@ -927,13 +927,21 @@
   }
 }
 
-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->has_dependency_output_file()) {
+    output_file = target->dependency_output_file();
+  } else if (target->has_dependency_output_alias()) {
+    output_file = target->dependency_output_alias();
+    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 bcb9e22..7161481 100644
--- a/src/gn/visual_studio_writer.h
+++ b/src/gn/visual_studio_writer.h
@@ -131,7 +131,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_;