[gn] use action() instead of copy() for libcxx headers (#195948)

copy() doesn't handle file deletions. Use an action() that syncs the
output directory with the input list via a response file, removing files
that are no longer in the list.

This works because if files are added or removed, ninja's command line
tracking re-runs the script, and if contents of existing files change,
ninja's input mtime checking reruns it.

This also makes the remove_float_h workaround unnecessary.

Motivated by all the recent header removals in libc++.
diff --git a/llvm/utils/gn/build/sync_source_dir.py b/llvm/utils/gn/build/sync_source_dir.py
new file mode 100644
index 0000000..7f6f9a2
--- /dev/null
+++ b/llvm/utils/gn/build/sync_source_dir.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+
+"""Syncs files from a source dir to an output dir.
+
+Reads a list of files from a response file, copies them from the source dir
+to the output dir, and removes files in the output dir that are not in the
+list (except for files passed via --except)."""
+
+import argparse
+import os
+import shlex
+import sys
+
+
+def read(filename):
+    with open(filename) as f:
+        return f.read()
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description=__doc__,
+        formatter_class=argparse.RawDescriptionHelpFormatter)
+    parser.add_argument("--stamp", required=True,
+                        help="name of a file whose mtime is updated on run")
+    parser.add_argument("--source-dir", required=True)
+    parser.add_argument("--output-dir", required=True)
+    parser.add_argument("--except", dest="exceptions", action="append",
+                        default=[])
+    parser.add_argument("rspfile")
+    args = parser.parse_args()
+
+    files = shlex.split(read(args.rspfile))
+
+    # Copy files from source dir to output dir.
+    for f in files:
+        src = os.path.join(args.source_dir, f)
+        dst = os.path.join(args.output_dir, f)
+        os.makedirs(os.path.dirname(dst), exist_ok=True)
+        data = read(src)
+        if not os.path.exists(dst) or read(dst) != data:
+            with open(dst, "w") as dst_file:
+                dst_file.write(data)
+
+    # Remove files in output dir that are not in the list.
+    want = set(files)
+    exceptions = set(args.exceptions)
+    for root, dirs, filenames in os.walk(args.output_dir):
+        for filename in filenames:
+            filepath = os.path.join(root, filename)
+            relpath = os.path.relpath(filepath, args.output_dir)
+            if relpath not in want and relpath not in exceptions:
+                os.remove(filepath)
+
+    open(args.stamp, "w")  # Update mtime on stamp file.
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/llvm/utils/gn/secondary/libcxx/include/BUILD.gn b/llvm/utils/gn/secondary/libcxx/include/BUILD.gn
index be2a650..797cdcc 100644
--- a/llvm/utils/gn/secondary/libcxx/include/BUILD.gn
+++ b/llvm/utils/gn/secondary/libcxx/include/BUILD.gn
@@ -75,20 +75,9 @@
         [ "LIBCXX_CONFIG_SITE_MODULE_ENTRY=textual header \"__config_site\"" ]
   }
 
-  # FIXME: Remove this after a while.
-  action("remove_float_h") {
-    stamp = "$target_gen_dir/remove_float_h.stamp"
-    outputs = [ stamp ]
-    script = "//llvm/utils/gn/build/remove_if_exists.py"
-    args = [
-      "--stamp",
-      rebase_path(stamp, root_build_dir),
-      rebase_path("$root_build_dir/include/c++/v1/float.h", root_build_dir),
-    ]
-  }
-
-  copy("copy_headers") {
-    sources = [
+  action("copy_headers") {
+    script = "//llvm/utils/gn/build/sync_source_dir.py"
+    inputs = [
       "__algorithm/adjacent_find.h",
       "__algorithm/all_of.h",
       "__algorithm/any_of.h",
@@ -1771,8 +1760,24 @@
       "wchar.h",
       "wctype.h",
     ]
+    response_file_contents = inputs
+    outputs = [ "$target_gen_dir/copy_headers.stamp" ]
+    args = [
+      "--stamp",
+      rebase_path(outputs[0], root_build_dir),
+      "--source-dir",
+      rebase_path("//libcxx/include", root_build_dir),
+      "--output-dir",
+      rebase_path(libcxx_generated_include_dir, root_build_dir),
+      "--except",
+      "__assertion_handler",
+      "--except",
+      "__config_site",
+      "--except",
+      "module.modulemap",
+      "{{response_file_name}}",
+    ]
     deps = [
-      ":remove_float_h",
       ":write_assertion_handler",
       ":write_config_site",
       ":write_modulemap",
@@ -1788,7 +1793,6 @@
       # don't get copied on macOS due to that.
       deps += [ "//libcxxabi/include" ]
     }
-    outputs = [ "$root_build_dir/include/c++/v1/{{source_target_relative}}" ]
   }
 }