Add support for remote_files for http_archive

This issue adds support necessary to tackle
https://github.com/bazelbuild/bazel-central-registry/issues/1566

Add two new attributes to http_archive: remote_file_urls and remote_file_integrity.

The purpose of these two attributes is to allow files to effectively be overlaid ontop of an http_archive. The goal of such functionality would be useful for BCR since the BUILD & WORKSPACE files need no longer be stored as patch files.

This means we could probably deprecate `build_file` since that could be referenced as a file:// url in the remote_file_urls attribute.

Co-authored-by: Mark Williams <markrwilliams@google.com>

CC @fmeum

Closes #22155.

PiperOrigin-RevId: 632594203
Change-Id: I6310093482c5c58537ed6dbe4ff90bafdbd696ff
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index 2b8ffa5..095a3f2 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -2917,7 +2917,7 @@
         "bzlTransitiveDigest": "tunTSmgwd2uvTzkCLtdbuCp0AI+WR+ftiPNqZ0rmcZk=",
         "recordedFileInputs": {
           "@@//MODULE.bazel": "939a48e4dbb71472db80f16a6a639f65cb4ddb07d8e1fd01db14cf1d0323917d",
-          "@@//src/test/tools/bzlmod/MODULE.bazel.lock": "1fffc063b7830519a4f8ea2b8a1192fef8c4065e4f2e5a1aeedd4f563a5bc683"
+          "@@//src/test/tools/bzlmod/MODULE.bazel.lock": "43259b270347fc1a76734df4b73b2d8c4e2c14293f3c0fb7dca5c48fd4b22758"
         },
         "recordedDirentsInputs": {},
         "envVariables": {},
@@ -3469,7 +3469,7 @@
     },
     "//tools/android:android_extensions.bzl%android_sdk_proxy_extensions": {
       "general": {
-        "bzlTransitiveDigest": "JQoCqdz3xQGJ+/ILJLx/pnLPCjAYRvq3Tmj3wkpmflI=",
+        "bzlTransitiveDigest": "XLVk7GLje5iUTApmiCu5fapV2+/7HqWPF7cQEu7SPOE=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
         "envVariables": {},
@@ -3485,7 +3485,7 @@
     },
     "//tools/android:android_extensions.bzl%remote_android_tools_extensions": {
       "general": {
-        "bzlTransitiveDigest": "JQoCqdz3xQGJ+/ILJLx/pnLPCjAYRvq3Tmj3wkpmflI=",
+        "bzlTransitiveDigest": "XLVk7GLje5iUTApmiCu5fapV2+/7HqWPF7cQEu7SPOE=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
         "envVariables": {},
@@ -3512,7 +3512,7 @@
     },
     "//tools/test:extensions.bzl%remote_coverage_tools_extension": {
       "general": {
-        "bzlTransitiveDigest": "kcwMubtgxBru6ttbkwo5IjUmLeSz0AODaQoox5L4zVU=",
+        "bzlTransitiveDigest": "ia4DMKKuTJ4QAHKSrRs4v/QBfK48p52gn2SwHL6BXd8=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
         "envVariables": {},
diff --git a/src/test/shell/bazel/BUILD b/src/test/shell/bazel/BUILD
index eb05d50..a7103af 100644
--- a/src/test/shell/bazel/BUILD
+++ b/src/test/shell/bazel/BUILD
@@ -804,6 +804,17 @@
 )
 
 sh_test(
+    name = "external_remote_file_test",
+    size = "small",
+    srcs = ["external_remote_file_test.sh"],
+    data = [
+        ":test-deps",
+        "@bazel_tools//tools/bash/runfiles",
+    ],
+    shard_count = 3,
+)
+
+sh_test(
     name = "external_path_test",
     size = "medium",
     srcs = ["external_path_test.sh"],
diff --git a/src/test/shell/bazel/external_remote_file_test.sh b/src/test/shell/bazel/external_remote_file_test.sh
new file mode 100755
index 0000000..7fc3a29
--- /dev/null
+++ b/src/test/shell/bazel/external_remote_file_test.sh
@@ -0,0 +1,323 @@
+#!/bin/bash
+#
+# Copyright 2017 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Tests the patching functionality of external repositories.
+
+set -euo pipefail
+# --- begin runfiles.bash initialization ---
+if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
+  if [[ -f "$0.runfiles_manifest" ]]; then
+    export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"
+  elif [[ -f "$0.runfiles/MANIFEST" ]]; then
+    export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"
+  elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+    export RUNFILES_DIR="$0.runfiles"
+  fi
+fi
+if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+  source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"
+elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
+  source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \
+            "$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)"
+else
+  echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash"
+  exit 1
+fi
+# --- end runfiles.bash initialization ---
+
+source "$(rlocation "io_bazel/src/test/shell/integration_test_setup.sh")" \
+  || { echo "integration_test_setup.sh not found!" >&2; exit 1; }
+
+# `uname` returns the current platform, e.g "MSYS_NT-10.0" or "Linux".
+# `tr` converts all upper case letters to lower case.
+# `case` matches the result if the `uname | tr` expression to string prefixes
+# that use the same wildcards as names do in Bash, i.e. "msys*" matches strings
+# starting with "msys", and "*" matches everything (it's the default case).
+case "$(uname -s | tr [:upper:] [:lower:])" in
+msys*)
+  # As of 2019-01-15, Bazel on Windows only supports MSYS Bash.
+  declare -r is_windows=true
+  ;;
+*)
+  declare -r is_windows=false
+  ;;
+esac
+
+if "$is_windows"; then
+  # Disable MSYS path conversion that converts path-looking command arguments to
+  # Windows paths (even if they arguments are not in fact paths).
+  export MSYS_NO_PATHCONV=1
+  export MSYS2_ARG_CONV_EXCL="*"
+fi
+
+
+if $is_windows; then
+  export MSYS_NO_PATHCONV=1
+  export MSYS2_ARG_CONV_EXCL="*"
+fi
+
+function set_up() {
+  WRKDIR=$(mktemp -d "${TEST_TMPDIR}/testXXXXXX")
+  cd "${WRKDIR}"
+  write_default_lockfile "MODULE.bazel.lock"
+  # create an archive file with files interesting for patching
+  mkdir hello_world-0.1.2
+  cat > hello_world-0.1.2/hello_world.c <<'EOF'
+#include <stdio.h>
+int main() {
+  printf("Hello, world!\n");
+  return 0;
+}
+EOF
+  zip hello_world.zip hello_world-0.1.2/*
+  rm -rf hello_world-0.1.2
+}
+
+function get_extrepourl() {
+  if $is_windows; then
+    echo "file:///$(cygpath -m $1)"
+  else
+    echo "file://$1"
+  fi
+}
+
+
+test_overlay_remote_file_multiple_segments() {
+  EXTREPODIR=`pwd`
+  EXTREPOURL="$(get_extrepourl ${EXTREPODIR})"
+
+  # Generate the remote files to overlay
+  mkdir -p child
+    cat > child/foo_bar.c <<'EOF'
+#include <stdio.h>
+int main() {
+  printf("Foo, Bar!\n");
+  return 0;
+}
+EOF
+  cat > child/BUILD.bazel <<'EOF'
+load("@rules_cc//cc:defs.bzl", "cc_binary")
+
+cc_binary(
+    name = "foo_bar",
+    srcs = ["foo_bar.c"],
+)
+EOF
+  touch BUILD.bazel
+  touch WORKSPACE
+
+  mkdir main
+  cd main
+  cat > WORKSPACE <<EOF
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+http_archive(
+  name="hello_world",
+  strip_prefix="hello_world-0.1.2",
+  urls=["${EXTREPOURL}/hello_world.zip"],
+  remote_file_urls={
+    "WORKSPACE": ["${EXTREPOURL}/WORKSPACE"],
+    "BUILD.bazel": ["${EXTREPOURL}/BUILD.bazel"],
+    "child/foo_bar.c": ["${EXTREPOURL}/child/foo_bar.c"],
+    "child/BUILD.bazel": ["${EXTREPOURL}/child/BUILD.bazel"],
+  },
+)
+EOF
+  write_default_lockfile "MODULE.bazel.lock"
+
+  bazel build @hello_world//child:foo_bar
+}
+
+test_overlay_remote_file_with_integrity() {
+  EXTREPODIR=`pwd`
+  EXTREPOURL="$(get_extrepourl ${EXTREPODIR})"
+
+  # Generate the remote files to overlay
+  cat > BUILD.bazel <<'EOF'
+load("@rules_cc//cc:defs.bzl", "cc_binary")
+
+cc_binary(
+    name = "hello_world",
+    srcs = ["hello_world.c"],
+)
+EOF
+  touch WORKSPACE
+
+  mkdir main
+  cd main
+  cat > WORKSPACE <<EOF
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+http_archive(
+  name="hello_world",
+  strip_prefix="hello_world-0.1.2",
+  urls=["${EXTREPOURL}/hello_world.zip"],
+  remote_file_urls={
+    "WORKSPACE": ["${EXTREPOURL}/WORKSPACE"],
+    "BUILD.bazel": ["${EXTREPOURL}/BUILD.bazel"],
+  },
+  remote_file_integrity={
+    "WORKSPACE": "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
+    "BUILD.bazel": "sha256-0bs+dwSOzHTbNAgDS02I3giLAZu2/NLn7BJWwQGN/Pk=",
+  },
+)
+EOF
+  write_default_lockfile "MODULE.bazel.lock"
+
+  bazel build @hello_world//:hello_world
+}
+
+test_overlay_remote_file_fail_with_integrity() {
+  EXTREPODIR=`pwd`
+  EXTREPOURL="$(get_extrepourl ${EXTREPODIR})"
+
+  # Generate the remote files to overlay
+  cat > BUILD.bazel <<'EOF'
+load("@rules_cc//cc:defs.bzl", "cc_binary")
+
+cc_binary(
+    name = "hello_world",
+    srcs = ["hello_world.c"],
+)
+EOF
+  touch WORKSPACE
+
+  mkdir main
+  cd main
+  cat > WORKSPACE <<EOF
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+http_archive(
+  name="hello_world",
+  strip_prefix="hello_world-0.1.2",
+  urls=["${EXTREPOURL}/hello_world.zip"],
+  remote_file_urls={
+    "WORKSPACE": ["${EXTREPOURL}/WORKSPACE"],
+    "BUILD.bazel": ["${EXTREPOURL}/BUILD.bazel"],
+  },
+  remote_file_integrity={
+    "WORKSPACE": "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFZ=",
+    "BUILD.bazel": "sha256-0bs+dwSOzHTbNAgDS02I3giLAZu2/NLn7BJWwQGN/Pk=",
+  },
+)
+EOF
+  write_default_lockfile "MODULE.bazel.lock"
+
+  bazel build @hello_world//:hello_world &> $TEST_log 2>&1 && fail "Expected to fail"
+  expect_log "but wanted sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFY="
+}
+
+test_overlay_remote_file_without_integrity() {
+  EXTREPODIR=`pwd`
+  EXTREPOURL="$(get_extrepourl ${EXTREPODIR})"
+
+  # Generate the remote files to overlay
+  cat > BUILD.bazel <<'EOF'
+load("@rules_cc//cc:defs.bzl", "cc_binary")
+
+cc_binary(
+    name = "hello_world",
+    srcs = ["hello_world.c"],
+)
+EOF
+  touch WORKSPACE
+
+  mkdir main
+  cd main
+  cat > WORKSPACE <<EOF
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+http_archive(
+  name="hello_world",
+  strip_prefix="hello_world-0.1.2",
+  urls=["${EXTREPOURL}/hello_world.zip"],
+  remote_file_urls={
+    "WORKSPACE": ["${EXTREPOURL}/WORKSPACE"],
+    "BUILD.bazel": ["${EXTREPOURL}/BUILD.bazel"],
+  },
+)
+EOF
+  write_default_lockfile "MODULE.bazel.lock"
+
+  bazel build @hello_world//:hello_world
+}
+
+test_overlay_remote_file_disallow_relative_outside_repo() {
+  EXTREPODIR=`pwd`
+  EXTREPOURL="$(get_extrepourl ${EXTREPODIR})"
+
+  # Generate the remote files to overlay
+  cat > BUILD.bazel <<'EOF'
+load("@rules_cc//cc:defs.bzl", "cc_binary")
+
+cc_binary(
+    name = "hello_world",
+    srcs = ["hello_world.c"],
+)
+EOF
+  touch WORKSPACE
+
+  mkdir main
+  cd main
+  cat > WORKSPACE <<EOF
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+http_archive(
+  name="hello_world",
+  strip_prefix="hello_world-0.1.2",
+  urls=["${EXTREPOURL}/hello_world.zip"],
+  remote_file_urls={
+    "../../../WORKSPACE": ["${EXTREPOURL}/WORKSPACE"],
+    "BUILD.bazel": ["${EXTREPOURL}/BUILD.bazel"],
+  },
+)
+EOF
+  write_default_lockfile "MODULE.bazel.lock"
+
+  bazel build @hello_world//:hello_world &> $TEST_log 2>&1 && fail "Expected to fail"
+  expect_log "Error in download: Cannot write outside of the repository directory"
+}
+
+test_overlay_remote_file_disallow_absolute_path() {
+  EXTREPODIR=`pwd`
+  EXTREPOURL="$(get_extrepourl ${EXTREPODIR})"
+
+  cat > BUILD.bazel <<'EOF'
+load("@rules_cc//cc:defs.bzl", "cc_binary")
+
+cc_binary(
+    name = "hello_world",
+    srcs = ["hello_world.c"],
+)
+EOF
+  touch WORKSPACE
+
+  mkdir main
+  cd main
+  cat > WORKSPACE <<EOF
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+http_archive(
+  name="hello_world",
+  strip_prefix="hello_world-0.1.2",
+  urls=["${EXTREPOURL}/hello_world.zip"],
+  remote_file_urls={
+    "/tmp/WORKSPACE": ["${EXTREPOURL}/WORKSPACE"],
+    "BUILD.bazel": ["${EXTREPOURL}/BUILD.bazel"],
+  },
+)
+EOF
+  write_default_lockfile "MODULE.bazel.lock"
+
+  bazel build @hello_world//:hello_world &> $TEST_log 2>&1 && fail "Expected to fail"
+  expect_log "Error in download: Cannot write outside of the repository directory"
+}
+
+run_suite "external remote file tests"
diff --git a/src/test/tools/bzlmod/MODULE.bazel.lock b/src/test/tools/bzlmod/MODULE.bazel.lock
index 625916c..4672dfd 100644
--- a/src/test/tools/bzlmod/MODULE.bazel.lock
+++ b/src/test/tools/bzlmod/MODULE.bazel.lock
@@ -1216,7 +1216,7 @@
     },
     "@@bazel_tools//tools/android:android_extensions.bzl%android_sdk_proxy_extensions": {
       "general": {
-        "bzlTransitiveDigest": "JQoCqdz3xQGJ+/ILJLx/pnLPCjAYRvq3Tmj3wkpmflI=",
+        "bzlTransitiveDigest": "XLVk7GLje5iUTApmiCu5fapV2+/7HqWPF7cQEu7SPOE=",
         "usagesDigest": "MxHwjf0M2Z/ciNsN0U0t4T1LF5A1ZXwM1kigPKI+3SA=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
@@ -1233,7 +1233,7 @@
     },
     "@@bazel_tools//tools/android:android_extensions.bzl%remote_android_tools_extensions": {
       "general": {
-        "bzlTransitiveDigest": "JQoCqdz3xQGJ+/ILJLx/pnLPCjAYRvq3Tmj3wkpmflI=",
+        "bzlTransitiveDigest": "XLVk7GLje5iUTApmiCu5fapV2+/7HqWPF7cQEu7SPOE=",
         "usagesDigest": "pfoI3DAAesYHdp38xIvu9Ry0AM3EIPGwvav/Cyv23Kw=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
@@ -1326,7 +1326,7 @@
     },
     "@@bazel_tools//tools/test:extensions.bzl%remote_coverage_tools_extension": {
       "general": {
-        "bzlTransitiveDigest": "kcwMubtgxBru6ttbkwo5IjUmLeSz0AODaQoox5L4zVU=",
+        "bzlTransitiveDigest": "ia4DMKKuTJ4QAHKSrRs4v/QBfK48p52gn2SwHL6BXd8=",
         "usagesDigest": "5AzD10fWCl4vQ0i0Zl96Ial7tU8raC84LTK6DeHefuk=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
@@ -1391,7 +1391,7 @@
     },
     "@@rules_java~//java:extensions.bzl%toolchains": {
       "general": {
-        "bzlTransitiveDigest": "vVD15tu/rctjxFcmEY/kGV8BU29Wa5EgvggNQpJVYrE=",
+        "bzlTransitiveDigest": "3/zOmP1mlgajX04YvWpwPL1sUWIIbCnLT3W3i7GTSAI=",
         "usagesDigest": "km/MmwGP92TBuE8sy/Jk2BmLrfOADnezet92ElEDuSA=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
@@ -1957,7 +1957,7 @@
     },
     "@@rules_jvm_external~//:extensions.bzl%maven": {
       "general": {
-        "bzlTransitiveDigest": "B3lrNG2za+3c/y7Bldx38hxzAkTa6uYaesdEicYzZes=",
+        "bzlTransitiveDigest": "226YAg74jgPpN2590v8XjmUVZU1nLLfY26Q0RHpkZYo=",
         "usagesDigest": "UPebZtX4g40+QepdK3oMHged0o0tq6ojKbW84wE6XRA=",
         "recordedFileInputs": {
           "@@rules_jvm_external~//rules_jvm_external_deps_install.json": "10442a5ae27d9ff4c2003e5ab71643bf0d8b48dcf968b4173fa274c3232a8c06"
@@ -2981,7 +2981,7 @@
     },
     "@@rules_jvm_external~//:non-module-deps.bzl%non_module_deps": {
       "general": {
-        "bzlTransitiveDigest": "4OsuF/ozYz/n2U+6GGxM/3IzzX+qvQtjlY1fXeg2jE0=",
+        "bzlTransitiveDigest": "5AogXHZkwASsdwr6DZ7FypQvxHHp3r0l7HGPUx4wbgY=",
         "usagesDigest": "bTG4ItERqhG1LeSs62hQ01DiMarFsflWgpZaghM5qik=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
@@ -3009,7 +3009,7 @@
     },
     "@@rules_python~//python/extensions:python.bzl%python": {
       "general": {
-        "bzlTransitiveDigest": "f/rffFOqp0Y5r6QNmElWpkVrIk/9b0pfhd/j+JtG0YM=",
+        "bzlTransitiveDigest": "vmavs6wkry+PckMdBQFrxN22WzruPHKNVE+U7VyukC0=",
         "usagesDigest": "7vjNHuEgQORYN9+9/77Q4zw1kawobM2oCQb9p0uhL68=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
@@ -3039,7 +3039,7 @@
     },
     "@@rules_python~//python/extensions/private:internal_deps.bzl%internal_deps": {
       "general": {
-        "bzlTransitiveDigest": "2QkuIItPQyACdxi2VVWUF2DrUHZ4M8To5Q5TMr6R+CU=",
+        "bzlTransitiveDigest": "6AMTpVCtd1n3YvJkISRnSZyaiRuIDQejEOplE09R2sM=",
         "usagesDigest": "b+nMDqtqPCBxiMBewNNde3aNjzKqZyvJuN5/49xB62s=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
diff --git a/tools/build_defs/repo/http.bzl b/tools/build_defs/repo/http.bzl
index 9735cc8..642d19d 100644
--- a/tools/build_defs/repo/http.bzl
+++ b/tools/build_defs/repo/http.bzl
@@ -51,6 +51,7 @@
 )
 load(
     ":utils.bzl",
+    "download_remote_files",
     "get_auth",
     "patch",
     "update_attrs",
@@ -139,6 +140,8 @@
     auth = get_auth(ctx, all_urls)
 
     download_info = ctx.download_and_extract(
+        # TODO(fzakaria): all_urls here has the remote_patch URL which is incorrect
+        # I believe this to be a file
         all_urls,
         ctx.attr.add_prefix,
         ctx.attr.sha256,
@@ -149,6 +152,8 @@
         integrity = ctx.attr.integrity,
     )
     workspace_and_buildfile(ctx)
+
+    download_remote_files(ctx, auth = auth)
     patch(ctx, auth = auth)
 
     return _update_integrity_attr(ctx, _http_archive_attrs, download_info)
@@ -304,6 +309,21 @@
             "patch command line tool if `patch_tool` attribute is specified or there are " +
             "arguments other than `-p` in `patch_args` attribute.",
     ),
+    "remote_file_urls": attr.string_list_dict(
+        default = {},
+        doc =
+            "A map of relative paths (key) to a list of URLs (value) that are to be downloaded " +
+            "and made available as overlaid files on the repo. This is useful when you want " +
+            "to add WORKSPACE or BUILD.bazel files atop an existing repository. The files " +
+            "are downloaded before applying the patches in the `patches` attribute and the list of URLs " +
+            "should all be possible mirrors of the same file. The URLs are tried in order until one succeeds. ",
+    ),
+    "remote_file_integrity": attr.string_dict(
+        default = {},
+        doc =
+            "A map of file relative paths (key) to its integrity value (value). These relative paths should map " +
+            "to the files (key) in the `remote_file_urls` attribute.",
+    ),
     "remote_patches": attr.string_dict(
         default = {},
         doc =
diff --git a/tools/build_defs/repo/utils.bzl b/tools/build_defs/repo/utils.bzl
index 89bcf0f..9620853 100644
--- a/tools/build_defs/repo/utils.bzl
+++ b/tools/build_defs/repo/utils.bzl
@@ -91,6 +91,34 @@
     )
     return patch_path
 
+def download_remote_files(ctx, auth = None):
+    """Utility function for downloading remote files.
+
+    This rule is intended to be used in the implementation function of
+    a repository rule. It assumes the parameters `remote_file_urls` and
+    `remote_file_integrity` to be present in `ctx.attr`.
+
+    Args:
+      ctx: The repository context of the repository rule calling this utility
+        function.
+      auth: An optional dict specifying authentication information for some of the URLs.
+    """
+    pending = [
+        ctx.download(
+            remote_file_urls,
+            path,
+            canonical_id = ctx.attr.canonical_id,
+            auth = auth,
+            integrity = ctx.attr.remote_file_integrity.get(path, ""),
+            block = False,
+        )
+        for path, remote_file_urls in ctx.attr.remote_file_urls.items()
+    ]
+
+    # Wait until the requests are done
+    for p in pending:
+        p.wait()
+
 def patch(ctx, patches = None, patch_cmds = None, patch_cmds_win = None, patch_tool = None, patch_args = None, auth = None):
     """Implementation of patching an already extracted repository.