refactor(repo_utils): create a helper for extracting files (#3459)

This just moves common code to the same place, so that we can better
maintain extracting from the whls easier.

Work towards #2945
diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel
index 7d5314d..dd86dcf 100644
--- a/python/private/pypi/BUILD.bazel
+++ b/python/private/pypi/BUILD.bazel
@@ -247,6 +247,7 @@
     deps = [
         ":parse_whl_name_bzl",
         "//python/private:repo_utils_bzl",
+        "@rules_python_internal//:rules_python_config_bzl",
     ],
 )
 
diff --git a/python/private/pypi/patch_whl.bzl b/python/private/pypi/patch_whl.bzl
index e315989..71b46f6 100644
--- a/python/private/pypi/patch_whl.bzl
+++ b/python/private/pypi/patch_whl.bzl
@@ -27,6 +27,8 @@
 within the wheel.
 """
 
+load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config")
+load("//python/private:repo_utils.bzl", "repo_utils")
 load(":parse_whl_name.bzl", "parse_whl_name")
 load(":pypi_repo_utils.bzl", "pypi_repo_utils")
 
@@ -84,16 +86,11 @@
     # does not support patching in another directory.
     whl_input = rctx.path(whl_path)
 
-    # symlink to a zip file to use bazel's extract so that we can use bazel's
-    # repository_ctx patch implementation. The whl file may be in a different
-    # external repository.
-    #
-    # TODO @aignas 2025-11-24: remove this symlinking workaround when we drop support for bazel 7
-    whl_file_zip = whl_input.basename + ".zip"
-    rctx.symlink(whl_input, whl_file_zip)
-    rctx.extract(whl_file_zip)
-    if not rctx.delete(whl_file_zip):
-        fail("Failed to remove the symlink after extracting")
+    repo_utils.extract(
+        rctx,
+        archive = whl_input,
+        supports_whl_extraction = rp_config.supports_whl_extraction,
+    )
 
     if not patches:
         fail("Trying to patch wheel without any patches")
diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl
index 044d18a..fdb3f93 100644
--- a/python/private/pypi/whl_library.bzl
+++ b/python/private/pypi/whl_library.bzl
@@ -377,17 +377,12 @@
     #
     # Remove non-pipstar and config_load check when we release rules_python 2.
     if enable_pipstar:
-        if rp_config.supports_whl_extraction:
-            extract_path = whl_path
-        else:
-            extract_path = rctx.path(whl_path.basename + ".zip")
-            rctx.symlink(whl_path, extract_path)
-        rctx.extract(
-            archive = extract_path,
+        repo_utils.extract(
+            rctx,
+            archive = whl_path,
             output = "site-packages",
+            supports_whl_extraction = rp_config.supports_whl_extraction,
         )
-        if not rp_config.supports_whl_extraction:
-            rctx.delete(extract_path)
 
         metadata = whl_metadata(
             install_dir = whl_path.dirname.get_child("site-packages"),
diff --git a/python/private/repo_utils.bzl b/python/private/repo_utils.bzl
index 77eac55..1abff36 100644
--- a/python/private/repo_utils.bzl
+++ b/python/private/repo_utils.bzl
@@ -429,11 +429,34 @@
         return "riscv64"
     return arch
 
+def _extract(mrctx, *, archive, supports_whl_extraction = False, **kwargs):
+    """Extract an archive
+
+    TODO: remove when the earliest supported bazel version is at least 8.3.
+
+    Note, we are using the parameter here because there is very little ways how we can detect
+    whether we can support just extracting the whl.
+    """
+    archive_original = None
+    if not supports_whl_extraction and archive.basename.endswith(".whl"):
+        archive_original = archive
+        archive = mrctx.path(archive.basename + ".zip")
+        mrctx.symlink(archive_original, archive)
+
+    mrctx.extract(
+        archive = archive,
+        **kwargs
+    )
+    if archive_original:
+        if not mrctx.delete(archive):
+            fail("Failed to remove the symlink after extracting")
+
 repo_utils = struct(
     # keep sorted
     execute_checked = _execute_checked,
     execute_checked_stdout = _execute_checked_stdout,
     execute_unchecked = _execute_unchecked,
+    extract = _extract,
     get_platforms_cpu_name = _get_platforms_cpu_name,
     get_platforms_os_name = _get_platforms_os_name,
     getenv = _getenv,