Add cmake rule iree_fetch_artifact (#10516)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5df8be9..416578f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -369,6 +369,7 @@
 include(iree_microbenchmark_suite)
 include(iree_hal_cts_test_suite)
 include(iree_static_linker_test)
+include(iree_fetch_artifact)
 
 set(CMAKE_POSITION_INDEPENDENT_CODE TRUE)
 
diff --git a/benchmarks/generated_benchmark_suites.cmake b/benchmarks/generated_benchmark_suites.cmake
index abf9456..a67e32f 100644
--- a/benchmarks/generated_benchmark_suites.cmake
+++ b/benchmarks/generated_benchmark_suites.cmake
@@ -16,19 +16,14 @@
 # Below is generated by build_tools/benchmarks/suites/cmake_rule_generator.py  #
 ################################################################################
 # Fetch the model from "https://storage.googleapis.com/iree-model-artifacts/mobilenet_v2_1.0_224.tflite"
-add_custom_command(
-  OUTPUT "${_MODEL_ARTIFACTS_DIR}/7d45f8e5-bb5e-48d0-928d-8f125104578f_mobilenet_v2.tflite"
-  COMMAND
-    "${Python3_EXECUTABLE}" "${IREE_ROOT_DIR}/build_tools/scripts/download_file.py"
-    "https://storage.googleapis.com/iree-model-artifacts/mobilenet_v2_1.0_224.tflite" -o "${_MODEL_ARTIFACTS_DIR}/7d45f8e5-bb5e-48d0-928d-8f125104578f_mobilenet_v2.tflite"
-  DEPENDS
-    "${IREE_ROOT_DIR}/build_tools/scripts/download_file.py"
-  COMMENT "Downloading https://storage.googleapis.com/iree-model-artifacts/mobilenet_v2_1.0_224.tflite"
-)
-add_custom_target(
-    "${_PACKAGE_NAME}_model-7d45f8e5-bb5e-48d0-928d-8f125104578f"
-  DEPENDS
+iree_fetch_artifact(
+  NAME
+    "model-7d45f8e5-bb5e-48d0-928d-8f125104578f"
+  SOURCE_URL
+    "https://storage.googleapis.com/iree-model-artifacts/mobilenet_v2_1.0_224.tflite"
+  OUTPUT
     "${_MODEL_ARTIFACTS_DIR}/7d45f8e5-bb5e-48d0-928d-8f125104578f_mobilenet_v2.tflite"
+  UNPACK
 )
 
 # Import the TFLite model "${_MODEL_ARTIFACTS_DIR}/7d45f8e5-bb5e-48d0-928d-8f125104578f_mobilenet_v2.tflite"
diff --git a/build_tools/cmake/iree_benchmark_suite.cmake b/build_tools/cmake/iree_benchmark_suite.cmake
index ee4e013..3795bd7 100644
--- a/build_tools/cmake/iree_benchmark_suite.cmake
+++ b/build_tools/cmake/iree_benchmark_suite.cmake
@@ -223,26 +223,23 @@
       # Update the source file to the downloaded-to place.
       string(REPLACE "/" ";" _SOURCE_URL_SEGMENTS "${_SOURCE_URL}")
       list(POP_BACK _SOURCE_URL_SEGMENTS _LAST_URL_SEGMENT)
-      set(_DOWNLOAD_TARGET "${_PACKAGE_NAME}_iree-download-benchmark-source-${_LAST_URL_SEGMENT}")
+      set(_DOWNLOAD_TARGET_NAME "iree-download-benchmark-source-${_LAST_URL_SEGMENT}")
 
       # Strip off gzip/tar suffix if present (downloader unpacks if necessary)
       string(REGEX REPLACE "(\.gz)|(\.tar\.gz)$" "" _SOURCE_FILE_BASENAME "${_LAST_URL_SEGMENT}")
       set(_MODULE_SOURCE "${_ROOT_ARTIFACTS_DIR}/${_SOURCE_FILE_BASENAME}")
-      if(NOT TARGET "${_DOWNLOAD_TARGET}")
-        add_custom_command(
-          OUTPUT "${_MODULE_SOURCE}"
-          COMMAND
-            "${Python3_EXECUTABLE}" "${IREE_ROOT_DIR}/build_tools/scripts/download_file.py"
-            "${_SOURCE_URL}" -o "${_MODULE_SOURCE}"
-          DEPENDS
-            "${IREE_ROOT_DIR}/build_tools/scripts/download_file.py"
-          COMMENT "Downloading ${_SOURCE_URL}"
-        )
-        add_custom_target("${_DOWNLOAD_TARGET}"
-          DEPENDS "${_MODULE_SOURCE}"
+      if(NOT TARGET "${_PACKAGE_NAME}_${_DOWNLOAD_TARGET_NAME}")
+        iree_fetch_artifact(
+          NAME
+            "${_DOWNLOAD_TARGET_NAME}"
+          SOURCE_URL
+            "${_SOURCE_URL}"
+          OUTPUT
+            "${_MODULE_SOURCE}"
+          UNPACK
         )
       endif()
-      set(_MODULE_SOURCE_TARGET "${_DOWNLOAD_TARGET}")
+      set(_MODULE_SOURCE_TARGET "${_PACKAGE_NAME}_${_DOWNLOAD_TARGET_NAME}")
     endif()
 
     # If the source is a TFLite file, import it.
diff --git a/build_tools/cmake/iree_fetch_artifact.cmake b/build_tools/cmake/iree_fetch_artifact.cmake
new file mode 100644
index 0000000..b282b7d
--- /dev/null
+++ b/build_tools/cmake/iree_fetch_artifact.cmake
@@ -0,0 +1,56 @@
+# Copyright 2022 The IREE Authors
+#
+# Licensed under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+# iree_fetch_artifact()
+#
+# Download file from URL. NEVER Use this rule to download from untrusted
+# sources, it doesn't unpack the file safely.
+#
+# Parameters:
+# NAME: Name of target (see Note).
+# SOURCE_URL: Source URL to donwload the file.
+# OUTPUT: Path to the output file or directory to unpack.
+# UNPACK: When added, it will try to unpack the archive if supported.
+#
+# Note:
+# By default, it will create a target named ${_PACKAGE_NAME}_${_RULE_NAME}.
+function(iree_fetch_artifact)
+  cmake_parse_arguments(
+    _RULE
+    "UNPACK"
+    "NAME;SOURCE_URL;OUTPUT"
+    ""
+    ${ARGN}
+  )
+
+  set(_ARGS "${IREE_ROOT_DIR}/build_tools/scripts/download_file.py")
+  list(APPEND _ARGS "${_RULE_SOURCE_URL}")
+  list(APPEND _ARGS "-o")
+  list(APPEND _ARGS "${_RULE_OUTPUT}")
+
+  if(_RULE_UNPACK)
+    list(APPEND _ARGS "--unpack")
+  endif()
+
+  # TODO: CMake built-in file command can replace the python script. But python
+  # script also provides streaming unpack (doesn't use double space when
+  # unpacking). Need to evaluate if we want to replace.
+  add_custom_command(
+    OUTPUT "${_RULE_OUTPUT}"
+    COMMAND
+      "${Python3_EXECUTABLE}"
+      ${_ARGS}
+    DEPENDS
+      "${IREE_ROOT_DIR}/build_tools/scripts/download_file.py"
+    COMMENT "Downloading ${_RULE_SOURCE_URL}"
+  )
+
+  iree_package_name(_PACKAGE_NAME)
+  add_custom_target("${_PACKAGE_NAME}_${_RULE_NAME}"
+    DEPENDS
+      "${_RULE_OUTPUT}"
+  )
+endfunction()
diff --git a/build_tools/python/e2e_test_framework/iree_download_artifact_template.cmake b/build_tools/python/e2e_test_framework/iree_download_artifact_template.cmake
index 5dc8577..3c74089 100644
--- a/build_tools/python/e2e_test_framework/iree_download_artifact_template.cmake
+++ b/build_tools/python/e2e_test_framework/iree_download_artifact_template.cmake
@@ -1,15 +1,10 @@
 # Fetch the model from "$__SOURCE_URL"
-add_custom_command(
-  OUTPUT "$__OUTPUT_PATH"
-  COMMAND
-    "$${Python3_EXECUTABLE}" "$${IREE_ROOT_DIR}/build_tools/scripts/download_file.py"
-    "$__SOURCE_URL" -o "$__OUTPUT_PATH"
-  DEPENDS
-    "$${IREE_ROOT_DIR}/build_tools/scripts/download_file.py"
-  COMMENT "Downloading $__SOURCE_URL"
-)
-add_custom_target(
-    "$${_PACKAGE_NAME}_$__TARGET_NAME"
-  DEPENDS
+iree_fetch_artifact(
+  NAME
+    "$__TARGET_NAME"
+  SOURCE_URL
+    "$__SOURCE_URL"
+  OUTPUT
     "$__OUTPUT_PATH"
+  UNPACK
 )
diff --git a/build_tools/scripts/download_file.py b/build_tools/scripts/download_file.py
index 492ad1c..7e7a689 100755
--- a/build_tools/scripts/download_file.py
+++ b/build_tools/scripts/download_file.py
@@ -34,6 +34,10 @@
                       required=True,
                       metavar="<output-file>",
                       help="Output file path")
+  parser.add_argument("--unpack",
+                      action='store_true',
+                      default=False,
+                      help="Unpack the downloaded file if it's an archive.")
   return parser.parse_args()
 
 
@@ -50,24 +54,26 @@
           f"Failed to download file with status {response.status} {response.msg}"
       )
 
-    if args.source_url.endswith(".tar.gz"):
-      # Open tar.gz in the streaming mode.
-      with tarfile.open(fileobj=response, mode="r|*") as tar_file:
-        if os.path.exists(args.output):
-          shutil.rmtree(args.output)
-        os.makedirs(args.output)
-        tar_file.extractall(args.output)
+    if args.unpack:
+      if args.source_url.endswith(".tar.gz"):
+        # Open tar.gz in the streaming mode.
+        with tarfile.open(fileobj=response, mode="r|*") as tar_file:
+          if os.path.exists(args.output):
+            shutil.rmtree(args.output)
+          os.makedirs(args.output)
+          tar_file.extractall(args.output)
+        return
+      elif args.source_url.endswith(".gz"):
+        # Open gzip from a file-like object, which will be in the streaming mode.
+        with gzip.open(filename=response, mode="rb") as input_file:
+          with open(args.output, "wb") as output_file:
+            shutil.copyfileobj(input_file, output_file)
+        return
 
-    elif args.source_url.endswith(".gz"):
-      # Open gzip from a file-like object, which will be in the streaming mode.
-      with gzip.open(filename=response, mode="rb") as input_file:
-        with open(args.output, "wb") as output_file:
-          shutil.copyfileobj(input_file, output_file)
-
-    else:
-      with open(args.output, "wb") as output_file:
-        # Streaming copy.
-        shutil.copyfileobj(response, output_file)
+    # Fallback to download the file only.
+    with open(args.output, "wb") as output_file:
+      # Streaming copy.
+      shutil.copyfileobj(response, output_file)
 
 
 if __name__ == "__main__":