[build] Add copy_tree.gni and migrate all users

Fixed: 74196

Change-Id: I2dcb8825e5fd402507d6ef8bf8a196202d559dcb
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/514622
Commit-Queue: Jay Zhuang <jayzhuang@google.com>
Reviewed-by: Adam Perry <adamperry@google.com>
Reviewed-by: Shai Barack <shayba@google.com>
diff --git a/build/copy_tree.gni b/build/copy_tree.gni
new file mode 100644
index 0000000..e220e8e
--- /dev/null
+++ b/build/copy_tree.gni
@@ -0,0 +1,113 @@
+# Copyright 2021 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Copies a directory preserving its structure.
+#
+# NOTE: Don't use this tempalte if GN's built-in `copy` can be used instead, see
+# example below, also `gn help copy`. Usually the reason to use this template is
+# the content of the directory cannot be determined in BUILD.gn. Because of
+# this, it is impossible for this template to guarantee incremental correctness.
+# For example, changes to files in a directory may not cause its mtime to
+# change, so the build system won't rerun this action when building
+# incrementally. The `inputs` parameter is required to mitigate this problem,
+# but it provides no guarantees. See more details in https://fxbug.dev/73250.
+#
+# Example:
+#
+# ```
+# copy_tree("my_copy") {
+#   src_dir = "path/to/src/dir"
+#   dest_dir = "path/to/dest/dir"
+#   inputs = [
+#     "version/of/src/dir",
+#   ]
+#   ignore_patterns = [
+#     "*not_useful*",
+#     "*.to_ignore",
+#   ]
+# }
+# ```
+#
+# Use `copy` if content of dir can be determined in BUILD.gn:
+#
+# ```
+# copy("my_copy") {
+#   sources = [
+#     "path/to/src/dir/file1",
+#     "path/to/src/dir/file2",
+#     "path/to/src/dir/file3",
+#   ]
+#   outputs = [ "path/to/dest/dir/{{source_file_part}}" ]
+# }
+# ```
+#
+# Parameters
+#
+#   src_dir (required)
+#     Path to the directory to copy.
+#     Type: path
+#
+#   dest_dir (required)
+#     Path to copy the directory to.
+#     Type: path
+#
+#   inputs (optional)
+#     A list of files that changes when the content of the directory changes, so
+#     in incremental builds a rerun of this action can be correctly triggered.
+#     Type: list(path)
+#     Default: empty
+#
+#   ignore_patterns (optional)
+#     Glob-style patterns to ignore when copying.
+#     Type: list(string)
+#     Default: empty
+#
+#   deps
+#   testonly
+#   visibility
+template("copy_tree") {
+  action(target_name) {
+    # Not all inputs and outputs are listed by this action, so it is not
+    # hermetic. This is usually the very reason a user would want this template
+    # instead of, GN's built-in copy tool.
+    hermetic_deps = false
+
+    forward_variables_from(invoker,
+                           [
+                             "deps",
+                             "dest_dir",
+                             "ignore_patterns",
+                             "inputs",
+                             "src_dir",
+                             "testonly",
+                             "visibility",
+                           ])
+
+    assert(defined(src_dir), "src_dir must be defined for ${target_name}")
+    assert(defined(dest_dir), "dest_dir must be defined for ${target_name}")
+
+    if (defined(inputs)) {
+      inputs += [ src_dir ]
+    } else {
+      inputs = [ src_dir ]
+    }
+
+    script = "//build/copy_tree.py"
+
+    stamp_file = "${dest_dir}.stamp"
+    args = [
+      rebase_path(src_dir, root_build_dir),
+      rebase_path(dest_dir, root_build_dir),
+      rebase_path(stamp_file, root_build_dir),
+    ]
+    if (defined(ignore_patterns)) {
+      args += [ "--ignore_patterns" ] + ignore_patterns
+    }
+
+    outputs = [
+      dest_dir,
+      stamp_file,
+    ]
+  }
+}
diff --git a/build/copy_tree.py b/build/copy_tree.py
index a2c92ec..549d0f0 100755
--- a/build/copy_tree.py
+++ b/build/copy_tree.py
@@ -17,7 +17,7 @@
     params.add_argument("source", type=Path)
     params.add_argument("target", type=Path)
     params.add_argument("stamp", type=Path)
-    params.add_argument("--ignore_pattern", action="append")
+    params.add_argument("--ignore_patterns", nargs="+")
     args = params.parse_args()
 
     if args.target.is_file():
@@ -26,8 +26,8 @@
         shutil.rmtree(args.target, ignore_errors=True)
 
     ignore = None
-    if args.ignore_pattern:
-        ignore = shutil.ignore_patterns(*args.ignore_pattern)
+    if args.ignore_patterns:
+        ignore = shutil.ignore_patterns(*args.ignore_patterns)
 
     shutil.copytree(args.source, args.target, symlinks=True, ignore=ignore)
 
diff --git a/build/python/BUILD.gn b/build/python/BUILD.gn
index e3409a86..a9004a9 100644
--- a/build/python/BUILD.gn
+++ b/build/python/BUILD.gn
@@ -2,40 +2,27 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import("//build/copy_tree.gni")
 import("//build/python/python.gni")
 import("//build/testing/host_test_data.gni")
 
 # Make the host python prebuilt available in the out dir so it
 # can be used in python_host_test without making multiple copies.
 
-# A regular copy() doesn't work properly with directories.
-action("copy_lib") {
-  # TODO(https://fxbug.dev/73140): fix hermeticity of copy_tree.py
-  hermetic_deps = false
-  script = "//build/copy_tree.py"
-  args = [
-    rebase_path("//prebuilt/third_party/python3/${host_platform}/lib",
-                root_build_dir),
-    rebase_path("${python_out_dir}/lib", root_build_dir),
-    rebase_path("${python_out_dir}/lib.stamp", root_build_dir),
-
+copy_tree("copy_lib") {
+  src_dir = "//prebuilt/third_party/python3/${host_platform}/lib"
+  dest_dir = "${python_out_dir}/lib"
+  ignore_patterns = [
     # The .pyc files may be produced while this action is running,
     # so we don't want to try to copy them while the're being written.
-    "--ignore_pattern",
     "__pycache__",
-    "--ignore_pattern",
     "*.pyc.*",
-    "--ignore_pattern",
     "*.pyc",
   ]
   inputs = [
     # This file should change when the package version changes.
     "//prebuilt/third_party/python3/${host_platform}/include/python${python_version}/pyconfig.h",
   ]
-  outputs = [
-    "${python_out_dir}/lib",
-    "${python_out_dir}/lib.stamp",
-  ]
   visibility = [ ":*" ]
 }
 
diff --git a/tools/auto_owners/BUILD.gn b/tools/auto_owners/BUILD.gn
index b4bc131..77b98ce 100644
--- a/tools/auto_owners/BUILD.gn
+++ b/tools/auto_owners/BUILD.gn
@@ -2,9 +2,11 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import("//build/copy_tree.gni")
 import("//build/host.gni")
 import("//build/rust/rustc_binary.gni")
 import("//build/rust/rustc_test.gni")
+import("//build/testing/host_test_data.gni")
 
 if (is_host) {
   crate_deps = [
@@ -58,38 +60,19 @@
     outputs = [ "$test_output_dir/{{source_target_relative}}" ]
   }
 
-  # we need to copy the GN binary from the builder into the test output directory so we can
-  # run it on a different bot than does the builds
-  # TODO(fxbug.dev/74196) extract this, cargo-gnaw's, potentially others to a file in //build
-  template("_copy_tool") {
-    action(target_name) {
-      hermetic_deps = false
-      source = "//prebuilt/third_party/${invoker.tool}/${host_platform}"
-      target = "${invoker.out_dir}/${invoker.tool}"
-      inputs = [ "$source/.versions/${invoker.tool}.cipd_version" ]
-      script = "//build/copy_tree.py"
-      stamp = "${target}.stamp"
-      args = [
-        rebase_path(source, root_build_dir),
-        rebase_path(target, root_build_dir),
-        rebase_path(stamp, root_build_dir),
-      ]
-
-      # In theory we could use the version file as our output, but that causes
-      # a problem: ninja will attempt to mkdir $target, and if $target exists
-      # but is not a directory, the build will fail. So we use a stamp file in
-      # the parent directory instead.
-      outputs = [ stamp ]
-
-      metadata = {
-        test_runtime_deps = [ target ]
-      }
-    }
+  # we need to copy the GN binary from the builder into the test output
+  # directory so we can run it on a different bot than does the builds.
+  auto_owners_gn_out_dir = "${test_output_dir}/runfiles/gn"
+  copy_tree("auto_owners_gn_copy") {
+    src_dir = "//prebuilt/third_party/gn/${host_platform}"
+    dest_dir = auto_owners_gn_out_dir
+    inputs = [ "${src_dir}/.versions/gn.cipd_version" ]
   }
 
-  _copy_tool("auto_owners_gn") {
-    tool = "gn"
-    out_dir = "$test_output_dir/runfiles"
+  # Make the copied directory available at test runtime.
+  host_test_data("auto_owners_gn") {
+    sources = [ auto_owners_gn_out_dir ]
+    deps = [ ":auto_owners_gn_copy" ]
   }
 }
 
diff --git a/tools/cargo-gnaw/tests/BUILD.gn b/tools/cargo-gnaw/tests/BUILD.gn
index 7665ab6..6a1d761 100644
--- a/tools/cargo-gnaw/tests/BUILD.gn
+++ b/tools/cargo-gnaw/tests/BUILD.gn
@@ -2,6 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import("//build/copy_tree.gni")
 import("//build/rust/rustc_binary.gni")
 import("//build/rust/rustc_library.gni")
 import("//build/rust/rustc_test.gni")
@@ -127,39 +128,31 @@
     outputs = [ "$root_out_dir/test_data/tools/cargo-gnaw/tests/{{source_target_relative}}" ]
   }
 
-  template("_copy_tool") {
-    action(target_name) {
-      hermetic_deps = false
-      source = "//prebuilt/third_party/${invoker.tool}/${host_platform}"
-      target = "${invoker.out_dir}/${invoker.tool}"
-      inputs = [ "$source/.versions/${invoker.tool}.cipd_version" ]
-      script = "//build/copy_tree.py"
-      stamp = "${target}.stamp"
-      args = [
-        rebase_path(source, root_build_dir),
-        rebase_path(target, root_build_dir),
-        rebase_path(stamp, root_build_dir),
-      ]
-
-      # In theory we could use the version file as our output, but that causes
-      # a problem: ninja will attempt to mkdir $target, and if $target exists
-      # but is not a directory, the build will fail. So we use a stamp file in
-      # the parent directory instead.
-      outputs = [ stamp ]
-
-      metadata = {
-        test_runtime_deps = [ target ]
-      }
-    }
+  cargo_gnaw_rust_out_dir =
+      "${root_out_dir}/test_data/tools/cargo-gnaw/runfiles/rust"
+  copy_tree("cargo_gnaw_rust_copy") {
+    src_dir = "//prebuilt/third_party/rust/${host_platform}"
+    dest_dir = cargo_gnaw_rust_out_dir
+    inputs = [ "${src_dir}/.versions/rust.cipd_version" ]
   }
 
-  _copy_tool("cargo_gnaw_rust") {
-    tool = "rust"
-    out_dir = "$root_out_dir/test_data/tools/cargo-gnaw/runfiles"
+  # Make the copied directory available at test runtime.
+  host_test_data("cargo_gnaw_rust") {
+    sources = [ cargo_gnaw_rust_out_dir ]
+    deps = [ ":cargo_gnaw_rust_copy" ]
   }
 
-  _copy_tool("cargo_gnaw_gn") {
-    tool = "gn"
-    out_dir = "$root_out_dir/test_data/tools/cargo-gnaw/runfiles"
+  cargo_gnaw_gn_out_dir =
+      "${root_out_dir}/test_data/tools/cargo-gnaw/runfiles/gn"
+  copy_tree("cargo_gnaw_gn_copy") {
+    src_dir = "//prebuilt/third_party/gn/${host_platform}"
+    dest_dir = cargo_gnaw_gn_out_dir
+    inputs = [ "${src_dir}/.versions/gn.cipd_version" ]
+  }
+
+  # Make the copied directory available at test runtime.
+  host_test_data("cargo_gnaw_gn") {
+    sources = [ cargo_gnaw_gn_out_dir ]
+    deps = [ ":cargo_gnaw_gn_copy" ]
   }
 }
diff --git a/tools/symbolizer/BUILD.gn b/tools/symbolizer/BUILD.gn
index 2c4e695..0458ade3 100644
--- a/tools/symbolizer/BUILD.gn
+++ b/tools/symbolizer/BUILD.gn
@@ -2,6 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import("//build/copy_tree.gni")
 import("//build/host.gni")
 import("//build/sdk/sdk_host_tool.gni")
 import("//build/test.gni")
@@ -78,29 +79,27 @@
         [ "$root_out_dir/test_data/symbolizer/test_cases/{{source_file_part}}" ]
   }
 
-  # Manually copy the symbol directory since host_test_data does not support copying directory
-  # any more (fxbug.dev/73250). It's not a big issue for us since the symbol directory seldom
-  # changes, and when it changes, we usually only add new files to it which will change the
-  # directory's mtime.
-  action("copy_e2e_test_symbols") {
-    hermetic_deps = false
-    script = "//build/copy_tree.py"
-    inputs = [ "//prebuilt/test_data/symbolizer/symbols" ]
-    outputs = [
-      "$root_out_dir/test_data/symbolizer/symbols",
-      "$root_out_dir/test_data/symbolizer/symbols.stamp",
-    ]
-    args = rebase_path(inputs + outputs)
-    metadata = {
-      test_runtime_deps = outputs
-    }
+  # Manually copy the symbol directory since host_test_data does not support
+  # copying directory any more (fxbug.dev/73250). It's not a big issue for us
+  # since the symbol directory seldom changes, and when it changes, we usually
+  # only add new files to it which will change the directory's mtime.
+  e2e_test_symbols_out_dir = "$root_out_dir/test_data/symbolizer/symbols"
+  copy_tree("e2e_test_symbols_copy") {
+    src_dir = "//prebuilt/test_data/symbolizer/symbols"
+    dest_dir = e2e_test_symbols_out_dir
+  }
+
+  # Make the copied directory available at test runtime.
+  host_test_data("e2e_test_symbols") {
+    sources = [ e2e_test_symbols_out_dir ]
+    deps = [ ":e2e_test_symbols_copy" ]
   }
 
   test("symbolizer_e2e_tests") {
     sources = [ "e2e_test.cc" ]
     deps = [
-      ":copy_e2e_test_symbols",
       ":e2e_test_cases",
+      ":e2e_test_symbols",
       ":src",
       "//third_party/googletest:gtest",
     ]