[gn] Prevent build.ninja deletion when regeneration is interrupted

Prior to this change, the top-level build.ninja file generated by GN
was configured in such a way that interrupting ninja file regeneration
(e.g. with Ctrl-C) would cause ninja to delete build.ninja. This was
done by ninja's Builder::Cleanup() function because the "build
build.ninja" statement that GN produces uses a depfile argument. GN uses
this depfile to capture implicit dependencies and automatically trigger
ninja regeneration if, for example, any BUILD.gn source file is
modified.

If build.ninja is deleted for this reason, the user must manually run
`gn gen` to recreate it; a poor user experience. Worse, command-line
arguments that the user previously passed to `gn gen` are also lost,
meaning that the user must be sure to pass the same arguments again.
Because of this bug, developer tools layered on top of GN (like
Pigweed's `pw watch` file watcher) have added handling to automatically
run `gn gen` for the user in an attempt to provide a smoother user
experience, but these attempts are still frustrated by the command-line
argument loss.

This change modifies the flow of ninja file generation slightly such
that build.ninja should never be deleted, even if regeneration is
interrupted, and thus ninja should always be able to restart it. The
build rule for build.ninja has been split in two so that ninja will
only delete the .tmp file if interrupted:

  build build.ninja.stamp: gn
    depfile = build.ninja.d

  build build.ninja: phony build.ninja.stamp
    generator = 1

`gn gen` now does the following:

  1. If the '--regeneration' flag is present (i.e. if the caller is
     ninja rather than a user), the 'build.ninja' file is truncated to
     only the commands needed to initiate regeneration again. (This is
     the same operation `gn clean` applies to 'build.ninja'.)

  2. Ninja file generation proceeds as before, writing 'build.ninja' and
     'build.ninja.d', plus a new 'build.ninja.stamp' file.

Essentially, this works by misleading ninja about when 'build.ninja' is
actually written. Importantly, because of this gn takes on the
responsibility for ensuring that 'build.ninja' is always valid, which it
achieves through atomic writes.

Fixed: https://bugs.chromium.org/p/gn/issues/detail?id=25
Change-Id: I4aef0317466b5e3430a9ef44eaa63c9515afefab
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/14200
Reviewed-by: Brett Wilson <brettw@chromium.org>
diff --git a/build/gen.py b/build/gen.py
index f877cc2..ce9d768 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -731,6 +731,7 @@
         'src/gn/xcode_object.cc',
         'src/gn/xcode_writer.cc',
         'src/gn/xml_element_writer.cc',
+        'src/util/atomic_write.cc',
         'src/util/exe_path.cc',
         'src/util/msg_loop.cc',
         'src/util/semaphore.cc',
diff --git a/src/gn/command_clean.cc b/src/gn/command_clean.cc
index 5290dd6..8261845 100644
--- a/src/gn/command_clean.cc
+++ b/src/gn/command_clean.cc
@@ -5,45 +5,16 @@
 #include "base/files/file_enumerator.h"
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
-#include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
 #include "gn/commands.h"
 #include "gn/err.h"
 #include "gn/filesystem_utils.h"
+#include "gn/ninja_build_writer.h"
 #include "gn/setup.h"
 
 namespace {
 
-// Extracts from a build.ninja the commands to run GN.
-//
-// The commands to run GN are the gn rule and build.ninja build step at the top
-// of the build.ninja file. We want to keep these when deleting GN builds since
-// we want to preserve the command-line flags to GN.
-//
-// On error, returns the empty string.
-std::string ExtractGNBuildCommands(const base::FilePath& build_ninja_file) {
-  std::string file_contents;
-  if (!base::ReadFileToString(build_ninja_file, &file_contents))
-    return std::string();
-
-  std::vector<std::string_view> lines = base::SplitStringPiece(
-      file_contents, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
-
-  std::string result;
-  int num_blank_lines = 0;
-  for (const auto& line : lines) {
-    result.append(line);
-    result.push_back('\n');
-    if (line.empty())
-      ++num_blank_lines;
-    if (num_blank_lines == 3)
-      break;
-  }
-
-  return result;
-}
-
 bool CleanOneDir(const std::string& dir) {
   // Deliberately leaked to avoid expensive process teardown.
   Setup* setup = new Setup;
@@ -65,47 +36,28 @@
     return false;
   }
 
-  // Erase everything but the args file, and write a dummy build.ninja file that
-  // will automatically rerun GN the next time Ninja is run.
-  base::FilePath build_ninja_file = build_dir.AppendASCII("build.ninja");
-  std::string build_commands = ExtractGNBuildCommands(build_ninja_file);
-  if (build_commands.empty()) {
-    // Couldn't parse the build.ninja file.
-    Err(Location(), "Couldn't read build.ninja in this directory.",
-        "Try running \"gn gen\" on it and then re-running \"gn clean\".")
-        .PrintToStdout();
+  // Replace existing build.ninja with just enough for ninja to call GN and
+  // regenerate ninja files.
+  if (!commands::PrepareForRegeneration(&setup->build_settings())) {
     return false;
   }
 
+  // Erase everything but (user-created) args.gn and the build.ninja we just
+  // wrote.
+  const base::FilePath::CharType* remaining[]{FILE_PATH_LITERAL("args.gn"),
+                                              FILE_PATH_LITERAL("build.ninja")};
   base::FileEnumerator traversal(
       build_dir, false,
       base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES);
   for (base::FilePath current = traversal.Next(); !current.empty();
        current = traversal.Next()) {
-    if (base::ToLowerASCII(current.BaseName().value()) !=
-        FILE_PATH_LITERAL("args.gn")) {
+    std::string basename = base::ToLowerASCII(current.BaseName().value());
+    if (std::none_of(std::begin(remaining), std::end(remaining),
+                     [&](auto rem) { return basename == rem; })) {
       base::DeleteFile(current, true);
     }
   }
 
-  // Write the build.ninja file sufficiently to regenerate itself.
-  if (base::WriteFile(build_ninja_file, build_commands.data(),
-                      static_cast<int>(build_commands.size())) == -1) {
-    Err(Location(), std::string("Failed to write build.ninja."))
-        .PrintToStdout();
-    return false;
-  }
-
-  // Write a .d file for the build which references a nonexistent file.
-  // This will make Ninja always mark the build as dirty.
-  std::string dummy_content("build.ninja: nonexistant_file.gn\n");
-  if (base::WriteFile(build_ninja_d_file, dummy_content.data(),
-                      static_cast<int>(dummy_content.size())) == -1) {
-    Err(Location(), std::string("Failed to write build.ninja.d."))
-        .PrintToStdout();
-    return false;
-  }
-
   return true;
 }
 
diff --git a/src/gn/command_gen.cc b/src/gn/command_gen.cc
index 5c6cc14..e212fe9 100644
--- a/src/gn/command_gen.cc
+++ b/src/gn/command_gen.cc
@@ -408,7 +408,7 @@
       return false;
     }
 
-    if(!InvokeNinjaRecompactTool(ninja_executable, build_dir, err)) {
+    if (!InvokeNinjaRecompactTool(ninja_executable, build_dir, err)) {
       return false;
     }
   }
@@ -654,6 +654,16 @@
       setup->set_check_system_includes(true);
   }
 
+  // If this is a regeneration, replace existing build.ninja and build.ninja.d
+  // with just enough for ninja to call GN and regenerate ninja files. This
+  // removes any potential soon-to-be-dangling references and ensures that
+  // regeneration can be restarted if interrupted.
+  if (command_line->HasSwitch(switches::kRegeneration)) {
+    if (!commands::PrepareForRegeneration(&setup->build_settings())) {
+      return false;
+    }
+  }
+
   // Cause the load to also generate the ninja files for each target.
   TargetWriteInfo write_info;
   setup->builder().set_resolved_and_generated_callback(
diff --git a/src/gn/commands.cc b/src/gn/commands.cc
index cb44795..5ff7ca8 100644
--- a/src/gn/commands.cc
+++ b/src/gn/commands.cc
@@ -8,6 +8,7 @@
 
 #include "base/command_line.h"
 #include "base/environment.h"
+#include "base/files/file_util.h"
 #include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
 #include "base/values.h"
@@ -17,10 +18,12 @@
 #include "gn/item.h"
 #include "gn/label.h"
 #include "gn/label_pattern.h"
+#include "gn/ninja_build_writer.h"
 #include "gn/setup.h"
 #include "gn/standard_out.h"
 #include "gn/switches.h"
 #include "gn/target.h"
+#include "util/atomic_write.h"
 #include "util/build_config.h"
 
 namespace commands {
@@ -474,6 +477,50 @@
   return true;
 }
 
+bool PrepareForRegeneration(const BuildSettings* settings) {
+  // Write a .d file for the build which references a nonexistent file.
+  // This will make Ninja always mark the build as dirty.
+  base::FilePath build_ninja_d_file(settings->GetFullPath(
+      SourceFile(settings->build_dir().value() + "build.ninja.d")));
+  std::string dummy_depfile("build.ninja.stamp: nonexistent_file.gn\n");
+  if (util::WriteFileAtomically(build_ninja_d_file, dummy_depfile.data(),
+                                static_cast<int>(dummy_depfile.size())) == -1) {
+    Err(Location(), std::string("Failed to write build.ninja.d."))
+        .PrintToStdout();
+    return false;
+  }
+
+  // Write a stripped down build.ninja file with just the commands needed
+  // for ninja to call GN and regenerate ninja files.
+  base::FilePath build_ninja_file(settings->GetFullPath(
+      SourceFile(settings->build_dir().value() + "build.ninja")));
+  std::string build_ninja_contents;
+  if (!base::ReadFileToString(build_ninja_file, &build_ninja_contents)) {
+    // Couldn't parse the build.ninja file.
+    Err(Location(), "Couldn't read build.ninja in this directory.",
+        "Try running \"gn gen\" on it and then re-running \"gn clean\".")
+        .PrintToStdout();
+    return false;
+  }
+  std::string build_commands =
+      NinjaBuildWriter::ExtractRegenerationCommands(build_ninja_contents);
+  if (build_commands.empty()) {
+    Err(Location(), "Unexpected build.ninja contents in this directory.",
+        "Try running \"gn gen\" on it and then re-running \"gn clean\".")
+        .PrintToStdout();
+    return false;
+  }
+  if (util::WriteFileAtomically(build_ninja_file, build_commands.data(),
+                                static_cast<int>(build_commands.size())) ==
+      -1) {
+    Err(Location(), std::string("Failed to write build.ninja."))
+        .PrintToStdout();
+    return false;
+  }
+
+  return true;
+}
+
 const Target* ResolveTargetFromCommandLineString(
     Setup* setup,
     const std::string& label_string) {
diff --git a/src/gn/commands.h b/src/gn/commands.h
index acff044..702bf0c 100644
--- a/src/gn/commands.h
+++ b/src/gn/commands.h
@@ -239,6 +239,17 @@
 
 // Helper functions for some commands ------------------------------------------
 
+// Modifies the existing build.ninja to only contain the commands necessary to
+// run GN and regenerate and build.ninja.d such that build.ninja will be
+// treated as dirty and regenerated.
+//
+// This is used by commands like gen and clean before they modify or delete
+// other ninja files, and ensures that ninja can still call GN if the commands
+// are interrupted before completion.
+//
+// On error, returns false.
+bool PrepareForRegeneration(const BuildSettings* settings);
+
 // Given a setup that has already been run and some command-line input,
 // resolves that input as a target label and returns the corresponding target.
 // On failure, returns null and prints the error to the standard output.
diff --git a/src/gn/ninja_build_writer.cc b/src/gn/ninja_build_writer.cc
index 908c5df..90a296b 100644
--- a/src/gn/ninja_build_writer.cc
+++ b/src/gn/ninja_build_writer.cc
@@ -13,6 +13,7 @@
 
 #include "base/command_line.h"
 #include "base/files/file_util.h"
+#include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
 #include "gn/build_settings.h"
@@ -29,6 +30,7 @@
 #include "gn/switches.h"
 #include "gn/target.h"
 #include "gn/trace.h"
+#include "util/atomic_write.h"
 #include "util/build_config.h"
 #include "util/exe_path.h"
 
@@ -261,16 +263,16 @@
   if (!gen.Run(err))
     return false;
 
-  // Unconditionally write the build.ninja. Ninja's build-out-of-date checking
-  // will re-run GN when any build input is newer than build.ninja, so any time
-  // the build is updated, build.ninja's timestamp needs to updated also, even
-  // if the contents haven't been changed.
+  // Unconditionally write the build.ninja. Ninja's build-out-of-date
+  // checking will re-run GN when any build input is newer than build.ninja, so
+  // any time the build is updated, build.ninja's timestamp needs to updated
+  // also, even if the contents haven't been changed.
   base::FilePath ninja_file_name(build_settings->GetFullPath(
       SourceFile(build_settings->build_dir().value() + "build.ninja")));
   base::CreateDirectory(ninja_file_name.DirName());
   std::string ninja_contents = file.str();
-  if (base::WriteFile(ninja_file_name, ninja_contents.data(),
-                      static_cast<int>(ninja_contents.size())) !=
+  if (util::WriteFileAtomically(ninja_file_name, ninja_contents.data(),
+                                static_cast<int>(ninja_contents.size())) !=
       static_cast<int>(ninja_contents.size()))
     return false;
 
@@ -278,14 +280,46 @@
   base::FilePath dep_file_name(build_settings->GetFullPath(
       SourceFile(build_settings->build_dir().value() + "build.ninja.d")));
   std::string dep_contents = depfile.str();
-  if (base::WriteFile(dep_file_name, dep_contents.data(),
-                      static_cast<int>(dep_contents.size())) !=
+  if (util::WriteFileAtomically(dep_file_name, dep_contents.data(),
+                                static_cast<int>(dep_contents.size())) !=
       static_cast<int>(dep_contents.size()))
     return false;
 
+  // Finally, write the empty build.ninja.stamp file. This is the output
+  // expected by the first of the two ninja rules used to accomplish
+  // regeneration.
+
+  base::FilePath stamp_file_name(build_settings->GetFullPath(
+      SourceFile(build_settings->build_dir().value() + "build.ninja.stamp")));
+  std::string stamp_contents;
+  if (util::WriteFileAtomically(stamp_file_name, stamp_contents.data(),
+                                static_cast<int>(stamp_contents.size())) !=
+      static_cast<int>(stamp_contents.size()))
+    return false;
+
   return true;
 }
 
+// static
+std::string NinjaBuildWriter::ExtractRegenerationCommands(
+    const std::string& build_ninja_in) {
+  std::vector<std::string_view> lines = base::SplitStringPiece(
+      build_ninja_in, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
+
+  std::ostringstream out;
+  int num_blank_lines = 0;
+  for (const auto& line : lines) {
+    out << line << '\n';
+    if (line.empty())
+      ++num_blank_lines;
+    // Warning: Significant magic number. Represents the number of blank lines
+    // in the ninja rules written by NinjaBuildWriter::WriteNinjaRules below.
+    if (num_blank_lines == 4)
+      return out.str();
+  }
+  return std::string{};
+}
+
 void NinjaBuildWriter::WriteNinjaRules() {
   out_ << "ninja_required_version = "
        << build_settings_->ninja_required_version().Describe() << "\n\n";
@@ -295,16 +329,25 @@
   out_ << "  pool = console\n";
   out_ << "  description = Regenerating ninja files\n\n";
 
-  // This rule will regenerate the ninja files when any input file has changed.
-  out_ << "build build.ninja: gn\n"
+  // A comment is left in the build.ninja explaining the two statement setup to
+  // avoid confusion, since build.ninja is written earlier than the ninja rules
+  // might make someone think.
+  out_ << "# The 'gn' rule also writes build.ninja, unbeknownst to ninja. The\n"
+       << "# build.ninja edge is separate to prevent ninja from deleting it\n"
+       << "# (due to depfile usage) if interrupted. gn uses atomic writes to\n"
+       << "# ensure that build.ninja is always valid even if interrupted.\n"
+       << "build build.ninja.stamp: gn\n"
        << "  generator = 1\n"
-       << "  depfile = build.ninja.d\n";
+       << "  depfile = build.ninja.d\n"
+       << "\n"
+       << "build build.ninja: phony build.ninja.stamp\n"
+       << "  generator = 1\n";
 
-  // Input build files. These go in the ".d" file. If we write them as
-  // dependencies in the .ninja file itself, ninja will expect the files to
-  // exist and will error if they don't. When files are listed in a depfile,
-  // missing files are ignored.
-  dep_out_ << "build.ninja:";
+  // Input build files. These go in the ".d" file. If we write them
+  // as dependencies in the .ninja file itself, ninja will expect
+  // the files to exist and will error if they don't. When files are
+  // listed in a depfile, missing files are ignored.
+  dep_out_ << "build.ninja.stamp:";
 
   // Other files read by the build.
   std::vector<base::FilePath> other_files = g_scheduler->GetGenDependencies();
diff --git a/src/gn/ninja_build_writer.h b/src/gn/ninja_build_writer.h
index e77f740..54a80aa 100644
--- a/src/gn/ninja_build_writer.h
+++ b/src/gn/ninja_build_writer.h
@@ -48,9 +48,37 @@
                               const Builder& builder,
                               Err* err);
 
+  // Extracts from an existing build.ninja file's contents the commands
+  // necessary to run GN and regenerate build.ninja.
+  //
+  // The regeneration rules live at the top of the build.ninja file and their
+  // specific contents are an internal detail of NinjaBuildWriter. Used by
+  // commands::PrepareForRegeneration.
+  //
+  // On error, returns an empty string.
+  static std::string ExtractRegenerationCommands(
+      const std::string& build_ninja_in);
+
   bool Run(Err* err);
 
  private:
+  // WriteNinjaRules writes the rules that ninja uses to regenerate its own
+  // build files, used whenever a build input file has changed.
+  //
+  // Ninja file regeneration is accomplished by two separate build statements.
+  // This is necessary to work around ninja's behavior of deleting all output
+  // files of a build edge if the edge uses a depfile and is interrupted before
+  // it can complete. Previously, interrupting regeneration would cause ninja to
+  // delete build.ninja, losing any flags/build settings passed to gen
+  // previously and requiring the user to manually 'gen' again.
+  //
+  // The workaround involves misleading ninja about when the build.ninja file is
+  // actually written. The first build statement runs the actual 'gen
+  // --regeneration' command, writing "build.ninja" (and .d and .stamp) and
+  // lists the "build.ninja.d" depfile to automatically trigger regeneration as
+  // needed, but does not list "build.ninja" as an output. The second
+  // statement's stated output is "build.ninja", but it simply uses the phony
+  // rule to refer to the first statement.
   void WriteNinjaRules();
   void WriteAllPools();
   bool WriteSubninjas(Err* err);
diff --git a/src/gn/ninja_build_writer_unittest.cc b/src/gn/ninja_build_writer_unittest.cc
index fab082f..453ff03 100644
--- a/src/gn/ninja_build_writer_unittest.cc
+++ b/src/gn/ninja_build_writer_unittest.cc
@@ -143,10 +143,13 @@
   ASSERT_TRUE(writer.Run(&err));
 
   const char expected_rule_gn[] = "rule gn\n";
-  const char expected_build_ninja[] =
-      "build build.ninja: gn\n"
+  const char expected_build_ninja_stamp[] =
+      "build build.ninja.stamp: gn\n"
       "  generator = 1\n"
       "  depfile = build.ninja.d\n";
+  const char expected_build_ninja[] =
+      "build build.ninja: phony build.ninja.stamp\n"
+      "  generator = 1\n";
   const char expected_other_pool[] =
       "pool other_toolchain_another_depth_pool\n"
       "  depth = 7\n"
@@ -172,6 +175,7 @@
       << "Expected to find: " << expected << "\n"      \
       << "Within: " << out_str
   EXPECT_SNIPPET(expected_rule_gn);
+  EXPECT_SNIPPET(expected_build_ninja_stamp);
   EXPECT_SNIPPET(expected_build_ninja);
   EXPECT_SNIPPET(expected_other_pool);
   EXPECT_SNIPPET(expected_toolchain);
@@ -184,6 +188,72 @@
   EXPECT_EQ(std::string::npos, out_str.find("pool console"));
 }
 
+TEST_F(NinjaBuildWriterTest, ExtractRegenerationCommands) {
+  TestWithScope setup;
+  Err err;
+
+  Target target_foo(setup.settings(), Label(SourceDir("//foo/"), "bar"));
+  target_foo.set_output_type(Target::ACTION);
+  target_foo.action_values().set_script(SourceFile("//foo/script.py"));
+  target_foo.action_values().outputs() = SubstitutionList::MakeForTest(
+      "//out/Debug/out1.out", "//out/Debug/out2.out");
+  target_foo.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(target_foo.OnResolved(&err));
+
+  // The console pool must be in the default toolchain.
+  Pool console_pool(setup.settings(), Label(SourceDir("//"), "console",
+                                            setup.toolchain()->label().dir(),
+                                            setup.toolchain()->label().name()));
+  console_pool.set_depth(1);
+
+  std::unordered_map<const Settings*, const Toolchain*> used_toolchains;
+  used_toolchains[setup.settings()] = setup.toolchain();
+
+  std::vector<const Target*> targets = {&target_foo};
+
+  std::ostringstream ninja_out;
+  std::ostringstream depfile_out;
+
+  NinjaBuildWriter writer(setup.build_settings(), used_toolchains, targets,
+                          setup.toolchain(), targets, ninja_out, depfile_out);
+  ASSERT_TRUE(writer.Run(&err));
+
+  const char expected_rule_gn[] = "rule gn\n";
+  const char expected_build_ninja_stamp[] = "build build.ninja.stamp: gn\n";
+  const char expected_build_ninja[] =
+      "build build.ninja: phony build.ninja.stamp\n";
+  const char expected_target[] = "build bar:";
+  const char expected_root_target[] = "build all: phony $\n";
+  const char expected_default[] = "default all\n";
+  std::string ninja_out_str = ninja_out.str();
+#define EXPECT_SNIPPET(str, expected)              \
+  EXPECT_NE(std::string::npos, str.find(expected)) \
+      << "Expected to find: " << expected << "\n"  \
+      << "Within: " << str
+#define EXPECT_NO_SNIPPET(str, unexpected)           \
+  EXPECT_EQ(std::string::npos, str.find(unexpected)) \
+      << "Found unexpected: " << unexpected << "\n"  \
+      << "Within: " << str
+  EXPECT_SNIPPET(ninja_out_str, expected_rule_gn);
+  EXPECT_SNIPPET(ninja_out_str, expected_build_ninja_stamp);
+  EXPECT_SNIPPET(ninja_out_str, expected_build_ninja);
+  EXPECT_SNIPPET(ninja_out_str, expected_target);
+  EXPECT_SNIPPET(ninja_out_str, expected_root_target);
+  EXPECT_SNIPPET(ninja_out_str, expected_default);
+
+  std::string commands =
+      NinjaBuildWriter::ExtractRegenerationCommands(ninja_out_str);
+  EXPECT_SNIPPET(commands, expected_rule_gn);
+  EXPECT_SNIPPET(commands, expected_build_ninja_stamp);
+  EXPECT_SNIPPET(commands, expected_build_ninja);
+  EXPECT_NO_SNIPPET(commands, expected_target);
+  EXPECT_NO_SNIPPET(commands, expected_root_target);
+  EXPECT_NO_SNIPPET(commands, expected_default);
+
+#undef EXPECT_SNIPPET
+#undef EXPECT_NO_SNIPPET
+}
+
 TEST_F(NinjaBuildWriterTest, SpaceInDepfile) {
   TestWithScope setup;
   Err err;
@@ -208,7 +278,7 @@
   ASSERT_TRUE(writer.Run(&err));
 
   EXPECT_EQ(depfile_out.str(),
-            "build.ninja: ../../path\\ with\\ space/BUILD.gn");
+            "build.ninja.stamp: ../../path\\ with\\ space/BUILD.gn");
 }
 
 TEST_F(NinjaBuildWriterTest, DuplicateOutputs) {
diff --git a/src/util/atomic_write.cc b/src/util/atomic_write.cc
new file mode 100644
index 0000000..51130d3
--- /dev/null
+++ b/src/util/atomic_write.cc
@@ -0,0 +1,34 @@
+// Copyright (c) 2022 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 "util/atomic_write.h"
+
+#include "base/files/file_util.h"
+
+namespace util {
+
+int WriteFileAtomically(const base::FilePath& filename,
+                        const char* data,
+                        int size) {
+  base::FilePath dir = filename.DirName();
+  base::FilePath temp_file_path;
+
+  {
+    base::File temp_file =
+        base::CreateAndOpenTemporaryFileInDir(dir, &temp_file_path);
+    if (!temp_file.IsValid()) {
+      return -1;
+    }
+    if (temp_file.WriteAtCurrentPos(data, size) != size) {
+      return -1;
+    }
+  }
+
+  if (!base::ReplaceFile(temp_file_path, filename, NULL)) {
+    return -1;
+  }
+  return size;
+}
+
+}  // namespace util
diff --git a/src/util/atomic_write.h b/src/util/atomic_write.h
new file mode 100644
index 0000000..232917a
--- /dev/null
+++ b/src/util/atomic_write.h
@@ -0,0 +1,22 @@
+// Copyright (c) 2022 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_ATOMIC_WRITE_H_
+#define TOOLS_GN_ATOMIC_WRITE_H_
+
+#include "base/files/file_path.h"
+
+namespace util {
+
+// Writes the given buffer into the file, overwriting any data that was
+// previously there. The write is performed atomically by first writing the
+// contents to a temporary file and then moving it into place. Returns the
+// number of bytes written, or -1 on error.
+int WriteFileAtomically(const base::FilePath& filename,
+                        const char* data,
+                        int size);
+
+}  // namespace util
+
+#endif  // TOOLS_GN_ATOMIC_WRITE_H_