Fork gsutil module from fuchsia recipes.

Bug:https://github.com/flutter/flutter/issues/120248
Change-Id: Ib1c08b7a51b00b503a8eb3aa05033a2bf91c0976
Reviewed-on: https://flutter-review.googlesource.com/c/recipes/+/38802
Reviewed-by: Nehal Patel <nehalvpatel@google.com>
Reviewed-by: Godofredo Contreras <godofredoc@google.com>
Commit-Queue: Yusuf Mohsinally <mohsinally@google.com>
diff --git a/recipe_modules/gsutil/__init__.py b/recipe_modules/gsutil/__init__.py
new file mode 100644
index 0000000..c224c00
--- /dev/null
+++ b/recipe_modules/gsutil/__init__.py
@@ -0,0 +1,14 @@
+# Copyright 2020 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.
+
+DEPS = [
+    "fuchsia/buildbucket_util",
+    "fuchsia/ensure_tool",
+    "fuchsia/python3",
+    "fuchsia/utils",
+    "recipe_engine/platform",
+    "recipe_engine/step",
+    "recipe_engine/time",
+    "recipe_engine/url",
+]
diff --git a/recipe_modules/gsutil/api.py b/recipe_modules/gsutil/api.py
new file mode 100644
index 0000000..d5ce30d
--- /dev/null
+++ b/recipe_modules/gsutil/api.py
@@ -0,0 +1,289 @@
+# Copyright 2016 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.
+
+from recipe_engine import recipe_api
+
+
+class GSUtilApi(recipe_api.RecipeApi):
+    """GSUtilApi provides support for GSUtil."""
+
+    @recipe_api.non_step
+    def join(self, *parts):
+        """Constructs a GS path from composite parts."""
+        return "/".join(p.strip("/") for p in parts)
+
+    def upload_namespaced_file(
+        self,
+        source,
+        bucket,
+        subpath,
+        namespace=None,
+        metadata=None,
+        no_clobber=True,
+        unauthenticated_url=False,
+        **kwargs,
+    ):
+        """Uploads a file to GCS under a subpath specific to the given build.
+
+        Will upload the file to:
+        gs://<bucket>/<build id>/<subpath or basename of file>
+
+        Args:
+            source (Path): A path to the file to upload.
+            bucket (str): The name of the GCS bucket to upload to.
+            subpath (str): The end of the destination path within the
+                build-specific subdirectory.
+            namespace (str or None): A unique ID for this build. Defaults to the
+                current build ID or led run ID.
+            metadata (dict): A dictionary of metadata values to upload along
+                with the file.
+            no_clobber (bool): Skip upload if destination path already exists in
+                GCS.
+            unauthenticated_url (bool): Whether to present a URL that requires
+                no authentication in the GCP web UI.
+        """
+        kwargs.setdefault("link_name", subpath)
+        return self.upload(
+            bucket=bucket,
+            src=source,
+            dst=self.namespaced_gcs_path(subpath, namespace),
+            metadata=metadata,
+            no_clobber=no_clobber,
+            unauthenticated_url=unauthenticated_url,
+            name=f"upload {subpath} to {bucket}",
+            **kwargs,
+        )
+
+    def upload_namespaced_directory(
+        self, source, bucket, subpath, namespace=None, rsync=True, **kwargs
+    ):
+        """Uploads a directory to GCS under a subpath specific to the given build.
+
+        Will upload the directory to:
+        gs://<bucket>/<build id>/<subpath>
+
+        Args:
+            source (Path): A path to the file to upload.
+            bucket (str): The name of the GCS bucket to upload to.
+            subpath (str): The end of the destination path within the
+                build-specific subdirectory.
+            namespace (str or None): A unique ID for this build. Defaults to the
+                current build ID or led run ID.
+            rsync (bool): Whether to use rsync, which is idempotent but
+                sometimes less reliable.
+        """
+        kwargs.setdefault("link_name", subpath)
+        func = self.upload
+        if rsync:
+            func = self.rsync
+        return func(
+            bucket=bucket,
+            src=source,
+            dst=self.namespaced_gcs_path(subpath, namespace),
+            recursive=True,
+            multithreaded=True,
+            no_clobber=True,
+            name=f"upload {subpath} to {bucket}",
+            **kwargs,
+        )
+
+    def namespaced_gcs_path(self, relative_path, namespace=None):
+        if not namespace:
+            namespace = self.m.buildbucket_util.id
+        return f"builds/{namespace}/{relative_path}"
+
+    def http_url(self, bucket, dest, unauthenticated_url=False):
+        base = (
+            "https://storage.googleapis.com"
+            if unauthenticated_url
+            else "https://storage.cloud.google.com"
+        )
+        return f"{base}/{bucket}/{self.m.url.quote(dest)}"
+
+    def _directory_listing_url(self, bucket, dest):
+        """Returns the URL for a GCS bucket subdirectory listing in the GCP console."""
+        return (
+            f"https://console.cloud.google.com/storage/browser/{bucket}/"
+            f"{self.m.url.quote(dest)}"
+        )
+
+    def namespaced_directory_url(self, bucket, subpath="", namespace=None):
+        return self._directory_listing_url(
+            bucket,
+            self.namespaced_gcs_path(subpath, namespace),
+        )
+
+    @staticmethod
+    def _get_metadata_field(name, provider_prefix=None):
+        """Returns: (str) the metadata field to use with Google Storage
+
+        The Google Storage specification for metadata can be found at:
+        https://developers.google.com/storage/docs/gsutil/addlhelp/WorkingWithObjectMetadata
+        """
+        # Already contains custom provider prefix
+        if name.lower().startswith("x-"):
+            return name
+
+        # See if it's innately supported by Google Storage
+        if name in (
+            "Cache-Control",
+            "Content-Disposition",
+            "Content-Encoding",
+            "Content-Language",
+            "Content-MD5",
+            "Content-Type",
+            "Custom-Time",
+        ):
+            return name
+
+        # Add provider prefix
+        if not provider_prefix:
+            provider_prefix = "x-goog-meta"
+        return f"{provider_prefix}-{name}"
+
+    @staticmethod
+    def unauthenticated_url(url):
+        """Transform an authenticated URL to an unauthenticated URL."""
+        return url.replace(
+            "https://storage.cloud.google.com/", "https://storage.googleapis.com/"
+        )
+
+    def _add_custom_time(self, metadata):
+        if not metadata:
+            metadata = {}
+        metadata["Custom-Time"] = self.m.time.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+        return metadata
+
+    def upload(
+        self,
+        bucket,
+        src,
+        dst,
+        link_name="gsutil.upload",
+        unauthenticated_url=False,
+        recursive=False,
+        no_clobber=False,
+        gzip_exts=(),
+        **kwargs,
+    ):
+        kwargs["metadata"] = self._add_custom_time(kwargs.pop("metadata", {}))
+        args = ["cp"]
+        if recursive:
+            args.append("-r")
+        if no_clobber:
+            args.append("-n")
+        if gzip_exts:
+            args.extend(["-j"] + gzip_exts)
+        args.extend([src, f"gs://{bucket}/{dst}"])
+        if not recursive or no_clobber:
+            # gsutil supports resumable uploads if we run the same command
+            # again, but it's only safe to resume uploading if we're only
+            # uploading a single file, or if we're operating in no_clobber mode.
+            step = self.m.utils.retry(
+                lambda: self._run(*args, **kwargs),
+                max_attempts=3,
+            )
+        else:
+            step = self._run(*args, **kwargs)
+        if link_name:
+            link_url = self.http_url(
+                bucket, dst, unauthenticated_url=unauthenticated_url
+            )
+            step.presentation.links[link_name] = link_url
+        return step
+
+    def rsync(
+        self,
+        bucket,
+        src,
+        dst,
+        link_name="gsutil.rsync",
+        recursive=True,
+        no_clobber=False,
+        gzip_exts=(),
+        **kwargs,
+    ):
+        kwargs["metadata"] = self._add_custom_time(kwargs.pop("metadata", {}))
+        args = ["rsync"]
+        if recursive:
+            args.append("-r")
+        if no_clobber:
+            # This will skip files already existing in dst with a later
+            # timestamp.
+            args.append("-u")
+        if gzip_exts:
+            args.extend(["-j"] + gzip_exts)
+        args.extend([src, f"gs://{bucket}/{dst}"])
+        step = self.m.utils.retry(lambda: self._run(*args, **kwargs), max_attempts=3)
+        if link_name:
+            link_url = self._directory_listing_url(bucket, dst)
+            step.presentation.links[link_name] = link_url
+        return step
+
+    def copy(
+        self,
+        src_bucket,
+        src,
+        dst_bucket,
+        dst,
+        link_name="gsutil.copy",
+        unauthenticated_url=False,
+        recursive=False,
+        **kwargs,
+    ):
+        args = ["cp"]
+        if recursive:
+            args.append("-r")
+        args.extend([f"gs://{src_bucket}/{src}", f"gs://{dst_bucket}/{dst}"])
+        step = self._run(*args, **kwargs)
+        if link_name:
+            step.presentation.links[link_name] = self.http_url(
+                dst_bucket, dst, unauthenticated_url=unauthenticated_url
+            )
+        return step
+
+    def download(self, src_bucket, src, dest, recursive=False, **kwargs):
+        """Downloads gcs bucket file to local disk.
+
+        Args:
+            src_bucket (str): gcs bucket name.
+            src (str): gcs file or path name.
+            recursive (bool): bool to indicate to copy recursively.
+            dest (str): local file path root to copy to.
+        """
+        args = ["cp"]
+        if recursive:
+            args.append("-r")
+        args.extend([f"gs://{src_bucket}/{src}", dest])
+        return self._run(*args, **kwargs)
+
+    @property
+    def _gsutil_tool(self):
+        return self.m.ensure_tool("gsutil", self.resource("tool_manifest.json"))
+
+    def _run(self, *args, **kwargs):
+        """Return a step to run arbitrary gsutil command."""
+        assert self._gsutil_tool
+        name = kwargs.pop("name", "gsutil " + args[0])
+        infra_step = kwargs.pop("infra_step", True)
+        cmd_prefix = [self._gsutil_tool]
+        # Note that metadata arguments have to be passed before the command.
+        metadata = kwargs.pop("metadata", [])
+        if metadata:
+            for k, v in sorted(metadata.items()):
+                field = self._get_metadata_field(k)
+                param = (field) if v is None else (f"{field}:{v}")
+                cmd_prefix.extend(["-h", param])
+        options = kwargs.pop("options", {})
+        options["software_update_check_period"] = 0
+        if options:
+            for k, v in sorted(options.items()):
+                cmd_prefix.extend(["-o", f"GSUtil:{k}={v}"])
+        if kwargs.pop("multithreaded", False):
+            cmd_prefix.extend(["-m"])
+
+        # The `gsutil` executable is a Python script with a shebang, and Windows
+        # doesn't support shebangs so we have to run it via Python.
+        step_func = self.m.python3 if self.m.platform.is_win else self.m.step
+        return step_func(name, cmd_prefix + list(args), infra_step=infra_step, **kwargs)
diff --git a/recipe_modules/gsutil/resources/tool_manifest.json b/recipe_modules/gsutil/resources/tool_manifest.json
new file mode 100644
index 0000000..6faf032
--- /dev/null
+++ b/recipe_modules/gsutil/resources/tool_manifest.json
@@ -0,0 +1,5 @@
+{
+	"path": "infra/3pp/tools/gsutil",
+	"version": "version:2@5.19",
+	"do_not_autoroll": true
+}
diff --git a/recipe_modules/gsutil/tests/full.expected/basic.json b/recipe_modules/gsutil/tests/full.expected/basic.json
new file mode 100644
index 0000000..cdfcb1a
--- /dev/null
+++ b/recipe_modules/gsutil/tests/full.expected/basic.json
@@ -0,0 +1,313 @@
+[
+  {
+    "cmd": [],
+    "name": "ensure gsutil"
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "RECIPE_MODULE[flutter::gsutil]/resources/tool_manifest.json",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "ensure gsutil.read manifest",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@tool_manifest.json@{@@@",
+      "@@@STEP_LOG_LINE@tool_manifest.json@  \"path\": \"path/to/gsutil\",@@@",
+      "@@@STEP_LOG_LINE@tool_manifest.json@  \"version\": \"version:pinned-version\"@@@",
+      "@@@STEP_LOG_LINE@tool_manifest.json@}@@@",
+      "@@@STEP_LOG_END@tool_manifest.json@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "ensure gsutil.install path/to/gsutil",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/cipd_tool/path/to/gsutil/version%3Apinned-version"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "ensure gsutil.install path/to/gsutil.ensure package directory",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[START_DIR]/cipd_tool/path/to/gsutil/version%3Apinned-version",
+      "-ensure-file",
+      "path/to/gsutil version:pinned-version",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "ensure gsutil.install path/to/gsutil.ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-version:pinned-v\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"path/to/gsutil\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cipd_tool/path/to/gsutil/version%3Apinned-version/gsutil",
+      "-h",
+      "Cache-Control:no-cache",
+      "-h",
+      "Custom-Time:2012-05-14T12:53:21.500000Z",
+      "-h",
+      "x-goog-meta-Remove-Me",
+      "-h",
+      "x-goog-meta-Test-Field:value",
+      "-h",
+      "x-custom-field:custom-value",
+      "-o",
+      "GSUtil:parallel_composite_upload_threshold=50M",
+      "-o",
+      "GSUtil:software_update_check_period=0",
+      "cp",
+      "-n",
+      "example",
+      "gs://[CLEANUP]/file/builds/8945511751514863184/path/to/file"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "upload path/to/file to [CLEANUP]/file",
+    "~followup_annotations": [
+      "@@@STEP_LINK@path/to/file@https://storage.googleapis.com/[CLEANUP]/file/builds/8945511751514863184/path/to/file@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cipd_tool/path/to/gsutil/version%3Apinned-version/gsutil",
+      "-h",
+      "Custom-Time:2012-05-14T12:53:23.000000Z",
+      "-o",
+      "GSUtil:software_update_check_period=0",
+      "-m",
+      "rsync",
+      "-r",
+      "-u",
+      "-j",
+      "html",
+      "[CLEANUP]/dir",
+      "gs://example/builds/8945511751514863184/rsync_subpath"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "upload rsync_subpath to example",
+    "~followup_annotations": [
+      "@@@STEP_LINK@rsync_subpath@https://console.cloud.google.com/storage/browser/example/builds/8945511751514863184/rsync_subpath@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cipd_tool/path/to/gsutil/version%3Apinned-version/gsutil",
+      "-h",
+      "Custom-Time:2012-05-14T12:53:24.500000Z",
+      "-o",
+      "GSUtil:software_update_check_period=0",
+      "-m",
+      "cp",
+      "-r",
+      "-n",
+      "-j",
+      "html",
+      "[CLEANUP]/dir",
+      "gs://example/builds/8945511751514863184/cp_subpath"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "upload cp_subpath to example",
+    "~followup_annotations": [
+      "@@@STEP_LINK@cp_subpath@https://storage.cloud.google.com/example/builds/8945511751514863184/cp_subpath@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cipd_tool/path/to/gsutil/version%3Apinned-version/gsutil",
+      "-h",
+      "Custom-Time:2012-05-14T12:53:26.000000Z",
+      "-o",
+      "GSUtil:software_update_check_period=0",
+      "cp",
+      "-r",
+      "[CLEANUP]/dir",
+      "gs://example/dir"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "gsutil cp",
+    "~followup_annotations": [
+      "@@@STEP_LINK@gsutil.upload@https://storage.cloud.google.com/example/dir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cipd_tool/path/to/gsutil/version%3Apinned-version/gsutil",
+      "-o",
+      "GSUtil:software_update_check_period=0",
+      "cp",
+      "-r",
+      "gs://example/foo",
+      "gs://example/bar"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "gsutil cp (2)",
+    "~followup_annotations": [
+      "@@@STEP_LINK@gsutil.copy@https://storage.cloud.google.com/example/bar@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cipd_tool/path/to/gsutil/version%3Apinned-version/gsutil",
+      "-o",
+      "GSUtil:software_update_check_period=0",
+      "cp",
+      "-r",
+      "gs://example/foo",
+      "tmp/"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "gsutil cp (3)"
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/gsutil/tests/full.expected/retry_on_failure.json b/recipe_modules/gsutil/tests/full.expected/retry_on_failure.json
new file mode 100644
index 0000000..2b7aefc
--- /dev/null
+++ b/recipe_modules/gsutil/tests/full.expected/retry_on_failure.json
@@ -0,0 +1,471 @@
+[
+  {
+    "cmd": [],
+    "name": "ensure gsutil"
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]\\resources\\fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "RECIPE_MODULE[flutter::gsutil]\\resources\\tool_manifest.json",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "ensure gsutil.read manifest",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@tool_manifest.json@{@@@",
+      "@@@STEP_LOG_LINE@tool_manifest.json@  \"path\": \"path/to/gsutil\",@@@",
+      "@@@STEP_LOG_LINE@tool_manifest.json@  \"version\": \"version:pinned-version\"@@@",
+      "@@@STEP_LOG_LINE@tool_manifest.json@}@@@",
+      "@@@STEP_LOG_END@tool_manifest.json@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "ensure gsutil.install path/to/gsutil",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]\\resources\\fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]\\cipd_tool\\path\\to\\gsutil\\version%3Apinned-version"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "ensure gsutil.install path/to/gsutil.ensure package directory",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd.bat",
+      "ensure",
+      "-root",
+      "[START_DIR]\\cipd_tool\\path\\to\\gsutil\\version%3Apinned-version",
+      "-ensure-file",
+      "path/to/gsutil version:pinned-version",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "ensure gsutil.install path/to/gsutil.ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-version:pinned-v\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"path/to/gsutil\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "ensure cpython3"
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]\\resources\\fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "RECIPE_MODULE[fuchsia::python3]\\resources\\tool_manifest.json",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "ensure cpython3.read manifest",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@tool_manifest.json@{@@@",
+      "@@@STEP_LOG_LINE@tool_manifest.json@  \"path\": \"path/to/cpython3\",@@@",
+      "@@@STEP_LOG_LINE@tool_manifest.json@  \"version\": \"version:pinned-version\"@@@",
+      "@@@STEP_LOG_LINE@tool_manifest.json@}@@@",
+      "@@@STEP_LOG_END@tool_manifest.json@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "ensure cpython3.install path/to/cpython3",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]\\resources\\fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]\\cipd_tool\\path\\to\\cpython3\\version%3Apinned-version"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "ensure cpython3.install path/to/cpython3.ensure package directory",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd.bat",
+      "ensure",
+      "-root",
+      "[START_DIR]\\cipd_tool\\path\\to\\cpython3\\version%3Apinned-version",
+      "-ensure-file",
+      "path/to/cpython3 version:pinned-version",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "ensure cpython3.install path/to/cpython3.ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-version:pinned-v\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"path/to/cpython3\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]\\cipd_tool\\path\\to\\cpython3\\version%3Apinned-version\\bin\\python3.exe",
+      "[START_DIR]\\cipd_tool\\path\\to\\gsutil\\version%3Apinned-version\\gsutil",
+      "-h",
+      "Cache-Control:no-cache",
+      "-h",
+      "Custom-Time:2012-05-14T12:53:21.500000Z",
+      "-h",
+      "x-goog-meta-Remove-Me",
+      "-h",
+      "x-goog-meta-Test-Field:value",
+      "-h",
+      "x-custom-field:custom-value",
+      "-o",
+      "GSUtil:parallel_composite_upload_threshold=50M",
+      "-o",
+      "GSUtil:software_update_check_period=0",
+      "cp",
+      "-n",
+      "example",
+      "gs://[CLEANUP]\\file/builds/8945511751514863184/path/to/file"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "upload path/to/file to [CLEANUP]\\file",
+    "~followup_annotations": [
+      "@@@STEP_LINK@path/to/file@https://storage.googleapis.com/[CLEANUP]\\file/builds/8945511751514863184/path/to/file@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]\\cipd_tool\\path\\to\\cpython3\\version%3Apinned-version\\bin\\python3.exe",
+      "[START_DIR]\\cipd_tool\\path\\to\\gsutil\\version%3Apinned-version\\gsutil",
+      "-h",
+      "Custom-Time:2012-05-14T12:53:23.000000Z",
+      "-o",
+      "GSUtil:software_update_check_period=0",
+      "-m",
+      "rsync",
+      "-r",
+      "-u",
+      "-j",
+      "html",
+      "[CLEANUP]\\dir",
+      "gs://example/builds/8945511751514863184/rsync_subpath"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "upload rsync_subpath to example",
+    "~followup_annotations": [
+      "@@@STEP_LINK@rsync_subpath@https://console.cloud.google.com/storage/browser/example/builds/8945511751514863184/rsync_subpath@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]\\cipd_tool\\path\\to\\cpython3\\version%3Apinned-version\\bin\\python3.exe",
+      "[START_DIR]\\cipd_tool\\path\\to\\gsutil\\version%3Apinned-version\\gsutil",
+      "-h",
+      "Custom-Time:2012-05-14T12:53:24.500000Z",
+      "-o",
+      "GSUtil:software_update_check_period=0",
+      "-m",
+      "cp",
+      "-r",
+      "-n",
+      "-j",
+      "html",
+      "[CLEANUP]\\dir",
+      "gs://example/builds/8945511751514863184/cp_subpath"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "upload cp_subpath to example",
+    "~followup_annotations": [
+      "@@@STEP_EXCEPTION@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]\\cipd_tool\\path\\to\\cpython3\\version%3Apinned-version\\bin\\python3.exe",
+      "[START_DIR]\\cipd_tool\\path\\to\\gsutil\\version%3Apinned-version\\gsutil",
+      "-h",
+      "Custom-Time:2012-05-14T12:53:24.500000Z",
+      "-o",
+      "GSUtil:software_update_check_period=0",
+      "-m",
+      "cp",
+      "-r",
+      "-n",
+      "-j",
+      "html",
+      "[CLEANUP]\\dir",
+      "gs://example/builds/8945511751514863184/cp_subpath"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "upload cp_subpath to example (2)",
+    "~followup_annotations": [
+      "@@@STEP_LINK@cp_subpath@https://storage.cloud.google.com/example/builds/8945511751514863184/cp_subpath@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]\\cipd_tool\\path\\to\\cpython3\\version%3Apinned-version\\bin\\python3.exe",
+      "[START_DIR]\\cipd_tool\\path\\to\\gsutil\\version%3Apinned-version\\gsutil",
+      "-h",
+      "Custom-Time:2012-05-14T12:53:26.000000Z",
+      "-o",
+      "GSUtil:software_update_check_period=0",
+      "cp",
+      "-r",
+      "[CLEANUP]\\dir",
+      "gs://example/dir"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "gsutil cp",
+    "~followup_annotations": [
+      "@@@STEP_LINK@gsutil.upload@https://storage.cloud.google.com/example/dir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]\\cipd_tool\\path\\to\\cpython3\\version%3Apinned-version\\bin\\python3.exe",
+      "[START_DIR]\\cipd_tool\\path\\to\\gsutil\\version%3Apinned-version\\gsutil",
+      "-o",
+      "GSUtil:software_update_check_period=0",
+      "cp",
+      "-r",
+      "gs://example/foo",
+      "gs://example/bar"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "gsutil cp (2)",
+    "~followup_annotations": [
+      "@@@STEP_LINK@gsutil.copy@https://storage.cloud.google.com/example/bar@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]\\cipd_tool\\path\\to\\cpython3\\version%3Apinned-version\\bin\\python3.exe",
+      "[START_DIR]\\cipd_tool\\path\\to\\gsutil\\version%3Apinned-version\\gsutil",
+      "-o",
+      "GSUtil:software_update_check_period=0",
+      "cp",
+      "-r",
+      "gs://example/foo",
+      "tmp/"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "gsutil cp (3)"
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/gsutil/tests/full.py b/recipe_modules/gsutil/tests/full.py
new file mode 100644
index 0000000..2b9e291
--- /dev/null
+++ b/recipe_modules/gsutil/tests/full.py
@@ -0,0 +1,61 @@
+# Copyright 2013 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.
+
+DEPS = [
+    "fuchsia/buildbucket_util",
+    "flutter/gsutil",
+    "recipe_engine/path",
+    "recipe_engine/platform",
+]
+
+BUCKET = "example"
+
+
+def RunSteps(api):
+    api.gsutil.upload_namespaced_file(
+        BUCKET,
+        api.path["cleanup"].join("file"),
+        api.gsutil.join("path", "to", "file"),
+        metadata={
+            "Test-Field": "value",
+            "Remove-Me": None,
+            "x-custom-field": "custom-value",
+            "Cache-Control": "no-cache",
+        },
+        unauthenticated_url=True,
+        options={"parallel_composite_upload_threshold": "50M"},
+    )
+
+    api.gsutil.upload_namespaced_directory(
+        api.path["cleanup"].join("dir"),
+        BUCKET,
+        "rsync_subpath",
+        gzip_exts=["html"],
+    )
+    api.gsutil.upload_namespaced_directory(
+        api.path["cleanup"].join("dir"),
+        BUCKET,
+        "cp_subpath",
+        rsync=False,
+        gzip_exts=["html"],
+    )
+    api.gsutil.upload(BUCKET, api.path["cleanup"].join("dir"), "dir", recursive=True)
+
+    api.gsutil.copy(BUCKET, "foo", BUCKET, "bar", recursive=True)
+    api.gsutil.download(BUCKET, "foo", "tmp/", recursive=True)
+
+    api.gsutil.unauthenticated_url("https://storage.cloud.google.com/foo/bar")
+
+    dir_url = api.gsutil.namespaced_directory_url("bucket", "foo")
+    assert dir_url.endswith("builds/8945511751514863184/foo"), dir_url
+
+
+def GenTests(api):
+    yield api.buildbucket_util.test("basic")
+    yield (
+        api.buildbucket_util.test("retry_on_failure")
+        # Cover the windows-specific codepath.
+        + api.platform.name("win")
+        + api.step_data(f"upload cp_subpath to {BUCKET}", retcode=1)
+    )
diff --git a/recipe_modules/sdk/__init__.py b/recipe_modules/sdk/__init__.py
index 9e90d46..97e6bd5 100644
--- a/recipe_modules/sdk/__init__.py
+++ b/recipe_modules/sdk/__init__.py
@@ -3,7 +3,7 @@
 # found in the LICENSE file.
 
 DEPS = [
-    'fuchsia/gsutil',
+    'flutter/gsutil',
     'flutter/tar',
     'recipe_engine/buildbucket',
     'recipe_engine/context',
diff --git a/recipe_modules/sdk/examples/full.expected/ensure_arm_sdk.json b/recipe_modules/sdk/examples/full.expected/ensure_arm_sdk.json
index 0ab686b..1731296 100644
--- a/recipe_modules/sdk/examples/full.expected/ensure_arm_sdk.json
+++ b/recipe_modules/sdk/examples/full.expected/ensure_arm_sdk.json
@@ -53,7 +53,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "RECIPE_MODULE[fuchsia::gsutil]/resources/tool_manifest.json",
+      "RECIPE_MODULE[flutter::gsutil]/resources/tool_manifest.json",
       "/path/to/tmp/json"
     ],
     "infra_step": true,
diff --git a/recipe_modules/sdk/examples/full.expected/ensure_intel_sdk.json b/recipe_modules/sdk/examples/full.expected/ensure_intel_sdk.json
index 698e91a..eb4739a 100644
--- a/recipe_modules/sdk/examples/full.expected/ensure_intel_sdk.json
+++ b/recipe_modules/sdk/examples/full.expected/ensure_intel_sdk.json
@@ -53,7 +53,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "RECIPE_MODULE[fuchsia::gsutil]/resources/tool_manifest.json",
+      "RECIPE_MODULE[flutter::gsutil]/resources/tool_manifest.json",
       "/path/to/tmp/json"
     ],
     "infra_step": true,
diff --git a/recipe_modules/sdk/examples/full.expected/has_cache_sdk.json b/recipe_modules/sdk/examples/full.expected/has_cache_sdk.json
index ac23543..d2da765 100644
--- a/recipe_modules/sdk/examples/full.expected/has_cache_sdk.json
+++ b/recipe_modules/sdk/examples/full.expected/has_cache_sdk.json
@@ -119,7 +119,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "RECIPE_MODULE[fuchsia::gsutil]/resources/tool_manifest.json",
+      "RECIPE_MODULE[flutter::gsutil]/resources/tool_manifest.json",
       "/path/to/tmp/json"
     ],
     "infra_step": true,
diff --git a/recipe_modules/sdk/examples/full.expected/missing_package_file.json b/recipe_modules/sdk/examples/full.expected/missing_package_file.json
index 357dcfb..8e9ab50 100644
--- a/recipe_modules/sdk/examples/full.expected/missing_package_file.json
+++ b/recipe_modules/sdk/examples/full.expected/missing_package_file.json
@@ -119,7 +119,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "RECIPE_MODULE[fuchsia::gsutil]/resources/tool_manifest.json",
+      "RECIPE_MODULE[flutter::gsutil]/resources/tool_manifest.json",
       "/path/to/tmp/json"
     ],
     "infra_step": true,
diff --git a/recipe_modules/vdl/examples/full.expected/ensure_vdl.json b/recipe_modules/vdl/examples/full.expected/ensure_vdl.json
index e96c7e2..3c75b5f 100644
--- a/recipe_modules/vdl/examples/full.expected/ensure_vdl.json
+++ b/recipe_modules/vdl/examples/full.expected/ensure_vdl.json
@@ -226,7 +226,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "RECIPE_MODULE[fuchsia::gsutil]/resources/tool_manifest.json",
+      "RECIPE_MODULE[flutter::gsutil]/resources/tool_manifest.json",
       "/path/to/tmp/json"
     ],
     "infra_step": true,
diff --git a/recipes/engine/femu_test.expected/arm64_emulator_arch.json b/recipes/engine/femu_test.expected/arm64_emulator_arch.json
index 14739a4..8d70459 100644
--- a/recipes/engine/femu_test.expected/arm64_emulator_arch.json
+++ b/recipes/engine/femu_test.expected/arm64_emulator_arch.json
@@ -1151,7 +1151,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "RECIPE_MODULE[fuchsia::gsutil]/resources/tool_manifest.json",
+      "RECIPE_MODULE[flutter::gsutil]/resources/tool_manifest.json",
       "/path/to/tmp/json"
     ],
     "cwd": "[CACHE]/builder",
diff --git a/recipes/engine/femu_test.expected/dangerous_test_commands.json b/recipes/engine/femu_test.expected/dangerous_test_commands.json
index 51885ea..b9f8653 100644
--- a/recipes/engine/femu_test.expected/dangerous_test_commands.json
+++ b/recipes/engine/femu_test.expected/dangerous_test_commands.json
@@ -1151,7 +1151,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "RECIPE_MODULE[fuchsia::gsutil]/resources/tool_manifest.json",
+      "RECIPE_MODULE[flutter::gsutil]/resources/tool_manifest.json",
       "/path/to/tmp/json"
     ],
     "cwd": "[CACHE]/builder",
diff --git a/recipes/engine/femu_test.expected/femu_with_package_list.json b/recipes/engine/femu_test.expected/femu_with_package_list.json
index e846bff..d19a0b9 100644
--- a/recipes/engine/femu_test.expected/femu_with_package_list.json
+++ b/recipes/engine/femu_test.expected/femu_with_package_list.json
@@ -1151,7 +1151,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "RECIPE_MODULE[fuchsia::gsutil]/resources/tool_manifest.json",
+      "RECIPE_MODULE[flutter::gsutil]/resources/tool_manifest.json",
       "/path/to/tmp/json"
     ],
     "cwd": "[CACHE]/builder",
diff --git a/recipes/engine/femu_test.expected/invalid_emulator_arch.json b/recipes/engine/femu_test.expected/invalid_emulator_arch.json
index 7e0d2d1..ebb06c5 100644
--- a/recipes/engine/femu_test.expected/invalid_emulator_arch.json
+++ b/recipes/engine/femu_test.expected/invalid_emulator_arch.json
@@ -1151,7 +1151,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "RECIPE_MODULE[fuchsia::gsutil]/resources/tool_manifest.json",
+      "RECIPE_MODULE[flutter::gsutil]/resources/tool_manifest.json",
       "/path/to/tmp/json"
     ],
     "cwd": "[CACHE]/builder",
diff --git a/recipes/engine/femu_test.expected/multiple_non_root_fars.json b/recipes/engine/femu_test.expected/multiple_non_root_fars.json
index 2e8db87..e86ce1e 100644
--- a/recipes/engine/femu_test.expected/multiple_non_root_fars.json
+++ b/recipes/engine/femu_test.expected/multiple_non_root_fars.json
@@ -1151,7 +1151,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "RECIPE_MODULE[fuchsia::gsutil]/resources/tool_manifest.json",
+      "RECIPE_MODULE[flutter::gsutil]/resources/tool_manifest.json",
       "/path/to/tmp/json"
     ],
     "cwd": "[CACHE]/builder",
diff --git a/recipes/engine/femu_test.expected/no_zircon_file.json b/recipes/engine/femu_test.expected/no_zircon_file.json
index 367cb58..ee3a0fe 100644
--- a/recipes/engine/femu_test.expected/no_zircon_file.json
+++ b/recipes/engine/femu_test.expected/no_zircon_file.json
@@ -1151,7 +1151,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "RECIPE_MODULE[fuchsia::gsutil]/resources/tool_manifest.json",
+      "RECIPE_MODULE[flutter::gsutil]/resources/tool_manifest.json",
       "/path/to/tmp/json"
     ],
     "cwd": "[CACHE]/builder",
diff --git a/recipes/engine/femu_test.expected/run_on_test_specified_arch.json b/recipes/engine/femu_test.expected/run_on_test_specified_arch.json
index bdc3667..16d8455 100644
--- a/recipes/engine/femu_test.expected/run_on_test_specified_arch.json
+++ b/recipes/engine/femu_test.expected/run_on_test_specified_arch.json
@@ -1151,7 +1151,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "RECIPE_MODULE[fuchsia::gsutil]/resources/tool_manifest.json",
+      "RECIPE_MODULE[flutter::gsutil]/resources/tool_manifest.json",
       "/path/to/tmp/json"
     ],
     "cwd": "[CACHE]/builder",
diff --git a/recipes/engine/femu_test.expected/run_with_dart_aot_behavior.json b/recipes/engine/femu_test.expected/run_with_dart_aot_behavior.json
index dc6b2af..66df4b5 100644
--- a/recipes/engine/femu_test.expected/run_with_dart_aot_behavior.json
+++ b/recipes/engine/femu_test.expected/run_with_dart_aot_behavior.json
@@ -1151,7 +1151,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "RECIPE_MODULE[fuchsia::gsutil]/resources/tool_manifest.json",
+      "RECIPE_MODULE[flutter::gsutil]/resources/tool_manifest.json",
       "/path/to/tmp/json"
     ],
     "cwd": "[CACHE]/builder",
diff --git a/recipes/engine/femu_test.expected/start_femu.json b/recipes/engine/femu_test.expected/start_femu.json
index bdd01e2..e4cd803 100644
--- a/recipes/engine/femu_test.expected/start_femu.json
+++ b/recipes/engine/femu_test.expected/start_femu.json
@@ -1151,7 +1151,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "RECIPE_MODULE[fuchsia::gsutil]/resources/tool_manifest.json",
+      "RECIPE_MODULE[flutter::gsutil]/resources/tool_manifest.json",
       "/path/to/tmp/json"
     ],
     "cwd": "[CACHE]/builder",