refactor(whl_library): move bazel file generation to Starlark (#1336)

Before this PR, the `wheel_installer` was doing three things:
1. Downloading the right wheel.
2. Extracting it into the output directory.
3. Generating BUILD.bazel files based on the extracted contents.

This PR is moving the third part into the `whl_library` repository rule
and it has the following benefits:
* We can reduce code duplication and label sanitization functions in
  rules_python.
* There are many things that the `wheel_installer` does not care anymore
  and we don't need to change less code when extending `whl_library` as
  we can now do many things in starlark directly.
* It becomes easier to change the API of how we expose the generated
  BUILD.bazel patching because we only need to change the Starlark
  functions.

Work towards #1330.
diff --git a/python/pip_install/BUILD.bazel b/python/pip_install/BUILD.bazel
index e8e8633..179fd62 100644
--- a/python/pip_install/BUILD.bazel
+++ b/python/pip_install/BUILD.bazel
@@ -4,7 +4,6 @@
         "BUILD.bazel",
         "//python/pip_install/private:distribution",
         "//python/pip_install/tools/dependency_resolver:distribution",
-        "//python/pip_install/tools/lib:distribution",
         "//python/pip_install/tools/wheel_installer:distribution",
     ],
     visibility = ["//:__pkg__"],
@@ -22,7 +21,6 @@
     name = "py_srcs",
     srcs = [
         "//python/pip_install/tools/dependency_resolver:py_srcs",
-        "//python/pip_install/tools/lib:py_srcs",
         "//python/pip_install/tools/wheel_installer:py_srcs",
     ],
     visibility = ["//python/pip_install/private:__pkg__"],
diff --git a/python/pip_install/pip_repository.bzl b/python/pip_install/pip_repository.bzl
index 99d1fb0..1f392ee 100644
--- a/python/pip_install/pip_repository.bzl
+++ b/python/pip_install/pip_repository.bzl
@@ -18,6 +18,7 @@
 load("//python:versions.bzl", "WINDOWS_NAME")
 load("//python/pip_install:repositories.bzl", "all_requirements")
 load("//python/pip_install:requirements_parser.bzl", parse_requirements = "parse")
+load("//python/pip_install/private:generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel")
 load("//python/pip_install/private:srcs.bzl", "PIP_INSTALL_PY_SRCS")
 load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
 load("//python/private:normalize_name.bzl", "normalize_name")
@@ -27,6 +28,8 @@
 
 COMMAND_LINE_TOOLS_PATH_SLUG = "commandlinetools"
 
+_WHEEL_ENTRY_POINT_PREFIX = "rules_python_wheel_entry_point"
+
 def _construct_pypath(rctx):
     """Helper function to construct a PYTHONPATH.
 
@@ -663,16 +666,7 @@
         "python.pip_install.tools.wheel_installer.wheel_installer",
         "--requirement",
         rctx.attr.requirement,
-        "--repo",
-        rctx.attr.repo,
-        "--repo-prefix",
-        rctx.attr.repo_prefix,
     ]
-    if rctx.attr.annotation:
-        args.extend([
-            "--annotation",
-            rctx.path(rctx.attr.annotation),
-        ])
 
     args = _parse_optional_attrs(rctx, args)
 
@@ -687,8 +681,72 @@
     if result.return_code:
         fail("whl_library %s failed: %s (%s) error code: '%s'" % (rctx.attr.name, result.stdout, result.stderr, result.return_code))
 
+    metadata = json.decode(rctx.read("metadata.json"))
+    rctx.delete("metadata.json")
+
+    entry_points = {}
+    for item in metadata["entry_points"]:
+        name = item["name"]
+        module = item["module"]
+        attribute = item["attribute"]
+
+        # There is an extreme edge-case with entry_points that end with `.py`
+        # See: https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/test/java/com/google/devtools/build/lib/rules/python/PyBinaryConfiguredTargetTest.java#L174
+        entry_point_without_py = name[:-3] + "_py" if name.endswith(".py") else name
+        entry_point_target_name = (
+            _WHEEL_ENTRY_POINT_PREFIX + "_" + entry_point_without_py
+        )
+        entry_point_script_name = entry_point_target_name + ".py"
+
+        rctx.file(
+            entry_point_script_name,
+            _generate_entry_point_contents(module, attribute),
+        )
+        entry_points[entry_point_without_py] = entry_point_script_name
+
+    build_file_contents = generate_whl_library_build_bazel(
+        repo_prefix = rctx.attr.repo_prefix,
+        dependencies = metadata["deps"],
+        data_exclude = rctx.attr.pip_data_exclude,
+        tags = [
+            "pypi_name=" + metadata["name"],
+            "pypi_version=" + metadata["version"],
+        ],
+        entry_points = entry_points,
+        annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))),
+    )
+    rctx.file("BUILD.bazel", build_file_contents)
+
     return
 
+def _generate_entry_point_contents(
+        module,
+        attribute,
+        shebang = "#!/usr/bin/env python3"):
+    """Generate the contents of an entry point script.
+
+    Args:
+        module (str): The name of the module to use.
+        attribute (str): The name of the attribute to call.
+        shebang (str, optional): The shebang to use for the entry point python
+            file.
+
+    Returns:
+        str: A string of python code.
+    """
+    contents = """\
+{shebang}
+import sys
+from {module} import {attribute}
+if __name__ == "__main__":
+    sys.exit({attribute}())
+""".format(
+        shebang = shebang,
+        module = module,
+        attribute = attribute,
+    )
+    return contents
+
 whl_library_attrs = {
     "annotation": attr.label(
         doc = (
diff --git a/python/pip_install/private/generate_whl_library_build_bazel.bzl b/python/pip_install/private/generate_whl_library_build_bazel.bzl
new file mode 100644
index 0000000..229a917
--- /dev/null
+++ b/python/pip_install/private/generate_whl_library_build_bazel.bzl
@@ -0,0 +1,224 @@
+# Copyright 2023 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.
+
+"""Generate the BUILD.bazel contents for a repo defined by a whl_library."""
+
+load("//python/private:normalize_name.bzl", "normalize_name")
+
+_WHEEL_FILE_LABEL = "whl"
+_PY_LIBRARY_LABEL = "pkg"
+_DATA_LABEL = "data"
+_DIST_INFO_LABEL = "dist_info"
+_WHEEL_ENTRY_POINT_PREFIX = "rules_python_wheel_entry_point"
+
+_COPY_FILE_TEMPLATE = """\
+copy_file(
+    name = "{dest}.copy",
+    src = "{src}",
+    out = "{dest}",
+    is_executable = {is_executable},
+)
+"""
+
+_ENTRY_POINT_RULE_TEMPLATE = """\
+py_binary(
+    name = "{name}",
+    srcs = ["{src}"],
+    # This makes this directory a top-level in the python import
+    # search path for anything that depends on this.
+    imports = ["."],
+    deps = ["{pkg}"],
+)
+"""
+
+_BUILD_TEMPLATE = """\
+load("@rules_python//python:defs.bzl", "py_library", "py_binary")
+load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
+
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+    name = "{dist_info_label}",
+    srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
+)
+
+filegroup(
+    name = "{data_label}",
+    srcs = glob(["data/**"], allow_empty = True),
+)
+
+filegroup(
+    name = "{whl_file_label}",
+    srcs = glob(["*.whl"], allow_empty = True),
+    data = {whl_file_deps},
+)
+
+py_library(
+    name = "{name}",
+    srcs = glob(
+        ["site-packages/**/*.py"],
+        exclude={srcs_exclude},
+        # Empty sources are allowed to support wheels that don't have any
+        # pure-Python code, e.g. pymssql, which is written in Cython.
+        allow_empty = True,
+    ),
+    data = {data} + glob(
+        ["site-packages/**/*"],
+        exclude={data_exclude},
+    ),
+    # This makes this directory a top-level in the python import
+    # search path for anything that depends on this.
+    imports = ["site-packages"],
+    deps = {dependencies},
+    tags = {tags},
+)
+"""
+
+def generate_whl_library_build_bazel(
+        repo_prefix,
+        dependencies,
+        data_exclude,
+        tags,
+        entry_points,
+        annotation = None):
+    """Generate a BUILD file for an unzipped Wheel
+
+    Args:
+        repo_prefix: the repo prefix that should be used for dependency lists.
+        dependencies: a list of PyPI packages that are dependencies to the py_library.
+        data_exclude: more patterns to exclude from the data attribute of generated py_library rules.
+        tags: list of tags to apply to generated py_library rules.
+        entry_points: A dict of entry points to add py_binary rules for.
+        annotation: The annotation for the build file.
+
+    Returns:
+        A complete BUILD file as a string
+    """
+
+    additional_content = []
+    data = []
+    srcs_exclude = []
+    data_exclude = [] + data_exclude
+    dependencies = sorted(dependencies)
+    tags = sorted(tags)
+
+    for entry_point, entry_point_script_name in entry_points.items():
+        additional_content.append(
+            _generate_entry_point_rule(
+                name = "{}_{}".format(_WHEEL_ENTRY_POINT_PREFIX, entry_point),
+                script = entry_point_script_name,
+                pkg = ":" + _PY_LIBRARY_LABEL,
+            ),
+        )
+
+    if annotation:
+        for src, dest in annotation.copy_files.items():
+            data.append(dest)
+            additional_content.append(_generate_copy_commands(src, dest))
+        for src, dest in annotation.copy_executables.items():
+            data.append(dest)
+            additional_content.append(
+                _generate_copy_commands(src, dest, is_executable = True),
+            )
+        data.extend(annotation.data)
+        data_exclude.extend(annotation.data_exclude_glob)
+        srcs_exclude.extend(annotation.srcs_exclude_glob)
+        if annotation.additive_build_content:
+            additional_content.append(annotation.additive_build_content)
+
+    _data_exclude = [
+        "**/* *",
+        "**/*.py",
+        "**/*.pyc",
+        "**/*.pyc.*",  # During pyc creation, temp files named *.pyc.NNNN are created
+        # RECORD is known to contain sha256 checksums of files which might include the checksums
+        # of generated files produced when wheels are installed. The file is ignored to avoid
+        # Bazel caching issues.
+        "**/*.dist-info/RECORD",
+    ]
+    for item in data_exclude:
+        if item not in _data_exclude:
+            _data_exclude.append(item)
+
+    lib_dependencies = [
+        "@" + repo_prefix + normalize_name(d) + "//:" + _PY_LIBRARY_LABEL
+        for d in dependencies
+    ]
+    whl_file_deps = [
+        "@" + repo_prefix + normalize_name(d) + "//:" + _WHEEL_FILE_LABEL
+        for d in dependencies
+    ]
+
+    contents = "\n".join(
+        [
+            _BUILD_TEMPLATE.format(
+                name = _PY_LIBRARY_LABEL,
+                dependencies = repr(lib_dependencies),
+                data_exclude = repr(_data_exclude),
+                whl_file_label = _WHEEL_FILE_LABEL,
+                whl_file_deps = repr(whl_file_deps),
+                tags = repr(tags),
+                data_label = _DATA_LABEL,
+                dist_info_label = _DIST_INFO_LABEL,
+                entry_point_prefix = _WHEEL_ENTRY_POINT_PREFIX,
+                srcs_exclude = repr(srcs_exclude),
+                data = repr(data),
+            ),
+        ] + additional_content,
+    )
+
+    # NOTE: Ensure that we terminate with a new line
+    return contents.rstrip() + "\n"
+
+def _generate_copy_commands(src, dest, is_executable = False):
+    """Generate a [@bazel_skylib//rules:copy_file.bzl%copy_file][cf] target
+
+    [cf]: https://github.com/bazelbuild/bazel-skylib/blob/1.1.1/docs/copy_file_doc.md
+
+    Args:
+        src (str): The label for the `src` attribute of [copy_file][cf]
+        dest (str): The label for the `out` attribute of [copy_file][cf]
+        is_executable (bool, optional): Whether or not the file being copied is executable.
+            sets `is_executable` for [copy_file][cf]
+
+    Returns:
+        str: A `copy_file` instantiation.
+    """
+    return _COPY_FILE_TEMPLATE.format(
+        src = src,
+        dest = dest,
+        is_executable = is_executable,
+    )
+
+def _generate_entry_point_rule(*, name, script, pkg):
+    """Generate a Bazel `py_binary` rule for an entry point script.
+
+    Note that the script is used to determine the name of the target. The name of
+    entry point targets should be uniuqe to avoid conflicts with existing sources or
+    directories within a wheel.
+
+    Args:
+        name (str): The name of the generated py_binary.
+        script (str): The path to the entry point's python file.
+        pkg (str): The package owning the entry point. This is expected to
+            match up with the `py_library` defined for each repository.
+
+    Returns:
+        str: A `py_binary` instantiation.
+    """
+    return _ENTRY_POINT_RULE_TEMPLATE.format(
+        name = name,
+        src = script.replace("\\", "/"),
+        pkg = pkg,
+    )
diff --git a/python/pip_install/private/srcs.bzl b/python/pip_install/private/srcs.bzl
index f3064a3..e342d90 100644
--- a/python/pip_install/private/srcs.bzl
+++ b/python/pip_install/private/srcs.bzl
@@ -9,10 +9,7 @@
 PIP_INSTALL_PY_SRCS = [
     "@rules_python//python/pip_install/tools/dependency_resolver:__init__.py",
     "@rules_python//python/pip_install/tools/dependency_resolver:dependency_resolver.py",
-    "@rules_python//python/pip_install/tools/lib:__init__.py",
-    "@rules_python//python/pip_install/tools/lib:annotation.py",
-    "@rules_python//python/pip_install/tools/lib:arguments.py",
-    "@rules_python//python/pip_install/tools/lib:bazel.py",
+    "@rules_python//python/pip_install/tools/wheel_installer:arguments.py",
     "@rules_python//python/pip_install/tools/wheel_installer:namespace_pkgs.py",
     "@rules_python//python/pip_install/tools/wheel_installer:wheel.py",
     "@rules_python//python/pip_install/tools/wheel_installer:wheel_installer.py",
diff --git a/python/pip_install/tools/lib/BUILD.bazel b/python/pip_install/tools/lib/BUILD.bazel
deleted file mode 100644
index 37a8b09..0000000
--- a/python/pip_install/tools/lib/BUILD.bazel
+++ /dev/null
@@ -1,82 +0,0 @@
-load("//python:defs.bzl", "py_library", "py_test")
-load(":annotations_test_helpers.bzl", "package_annotation", "package_annotations_file")
-
-py_library(
-    name = "lib",
-    srcs = [
-        "annotation.py",
-        "arguments.py",
-        "bazel.py",
-    ],
-    visibility = ["//python/pip_install:__subpackages__"],
-)
-
-package_annotations_file(
-    name = "mock_annotations",
-    annotations = {
-        "pkg_a": package_annotation(),
-        "pkg_b": package_annotation(
-            data_exclude_glob = [
-                "*.foo",
-                "*.bar",
-            ],
-        ),
-        "pkg_c": package_annotation(
-            # The `join` and `strip` here accounts for potential differences
-            # in new lines between unix and windows hosts.
-            additive_build_content = "\n".join([line.strip() for line in """\
-cc_library(
-    name = "my_target",
-    hdrs = glob(["**/*.h"]),
-    srcs = glob(["**/*.cc"]),
-)
-""".splitlines()]),
-            data = [":my_target"],
-        ),
-        "pkg_d": package_annotation(
-            srcs_exclude_glob = ["pkg_d/tests/**"],
-        ),
-    },
-    tags = ["manual"],
-)
-
-py_test(
-    name = "annotations_test",
-    size = "small",
-    srcs = ["annotations_test.py"],
-    data = [":mock_annotations"],
-    env = {"MOCK_ANNOTATIONS": "$(rootpath :mock_annotations)"},
-    deps = [
-        ":lib",
-        "//python/runfiles",
-    ],
-)
-
-py_test(
-    name = "arguments_test",
-    size = "small",
-    srcs = [
-        "arguments_test.py",
-    ],
-    deps = [
-        ":lib",
-    ],
-)
-
-filegroup(
-    name = "distribution",
-    srcs = glob(
-        ["*"],
-        exclude = ["*_test.py"],
-    ),
-    visibility = ["//python/pip_install:__subpackages__"],
-)
-
-filegroup(
-    name = "py_srcs",
-    srcs = glob(
-        include = ["**/*.py"],
-        exclude = ["**/*_test.py"],
-    ),
-    visibility = ["//python/pip_install:__subpackages__"],
-)
diff --git a/python/pip_install/tools/lib/__init__.py b/python/pip_install/tools/lib/__init__.py
deleted file mode 100644
index bbdfb4c..0000000
--- a/python/pip_install/tools/lib/__init__.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright 2023 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.
-
diff --git a/python/pip_install/tools/lib/annotation.py b/python/pip_install/tools/lib/annotation.py
deleted file mode 100644
index c980080..0000000
--- a/python/pip_install/tools/lib/annotation.py
+++ /dev/null
@@ -1,129 +0,0 @@
-# Copyright 2023 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.
-
-import json
-import logging
-from collections import OrderedDict
-from pathlib import Path
-from typing import Any, Dict, List
-
-
-class Annotation(OrderedDict):
-    """A python representation of `@rules_python//python:pip.bzl%package_annotation`"""
-
-    def __init__(self, content: Dict[str, Any]) -> None:
-
-        missing = []
-        ordered_content = OrderedDict()
-        for field in (
-            "additive_build_content",
-            "copy_executables",
-            "copy_files",
-            "data",
-            "data_exclude_glob",
-            "srcs_exclude_glob",
-        ):
-            if field not in content:
-                missing.append(field)
-                continue
-            ordered_content.update({field: content.pop(field)})
-
-        if missing:
-            raise ValueError("Data missing from initial annotation: {}".format(missing))
-
-        if content:
-            raise ValueError(
-                "Unexpected data passed to annotations: {}".format(
-                    sorted(list(content.keys()))
-                )
-            )
-
-        return OrderedDict.__init__(self, ordered_content)
-
-    @property
-    def additive_build_content(self) -> str:
-        return self["additive_build_content"]
-
-    @property
-    def copy_executables(self) -> Dict[str, str]:
-        return self["copy_executables"]
-
-    @property
-    def copy_files(self) -> Dict[str, str]:
-        return self["copy_files"]
-
-    @property
-    def data(self) -> List[str]:
-        return self["data"]
-
-    @property
-    def data_exclude_glob(self) -> List[str]:
-        return self["data_exclude_glob"]
-
-    @property
-    def srcs_exclude_glob(self) -> List[str]:
-        return self["srcs_exclude_glob"]
-
-
-class AnnotationsMap:
-    """A mapping of python package names to [Annotation]"""
-
-    def __init__(self, json_file: Path):
-        content = json.loads(json_file.read_text())
-
-        self._annotations = {pkg: Annotation(data) for (pkg, data) in content.items()}
-
-    @property
-    def annotations(self) -> Dict[str, Annotation]:
-        return self._annotations
-
-    def collect(self, requirements: List[str]) -> Dict[str, Annotation]:
-        unused = self.annotations
-        collection = {}
-        for pkg in requirements:
-            if pkg in unused:
-                collection.update({pkg: unused.pop(pkg)})
-
-        if unused:
-            logging.warning(
-                "Unused annotations: {}".format(sorted(list(unused.keys())))
-            )
-
-        return collection
-
-
-def annotation_from_str_path(path: str) -> Annotation:
-    """Load an annotation from a json encoded file
-
-    Args:
-        path (str): The path to a json encoded file
-
-    Returns:
-        Annotation: The deserialized annotations
-    """
-    json_file = Path(path)
-    content = json.loads(json_file.read_text())
-    return Annotation(content)
-
-
-def annotations_map_from_str_path(path: str) -> AnnotationsMap:
-    """Load an annotations map from a json encoded file
-
-    Args:
-        path (str): The path to a json encoded file
-
-    Returns:
-        AnnotationsMap: The deserialized annotations map
-    """
-    return AnnotationsMap(Path(path))
diff --git a/python/pip_install/tools/lib/annotations_test.py b/python/pip_install/tools/lib/annotations_test.py
deleted file mode 100644
index f7c360f..0000000
--- a/python/pip_install/tools/lib/annotations_test.py
+++ /dev/null
@@ -1,121 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2023 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.
-
-
-import os
-import textwrap
-import unittest
-from pathlib import Path
-
-from python.pip_install.tools.lib.annotation import Annotation, AnnotationsMap
-from python.runfiles import runfiles
-
-
-class AnnotationsTestCase(unittest.TestCase):
-
-    maxDiff = None
-
-    def test_annotations_constructor(self) -> None:
-        annotations_env = os.environ.get("MOCK_ANNOTATIONS")
-        self.assertIsNotNone(annotations_env)
-
-        r = runfiles.Create()
-
-        annotations_path = Path(r.Rlocation("rules_python/{}".format(annotations_env)))
-        self.assertTrue(annotations_path.exists())
-
-        annotations_map = AnnotationsMap(annotations_path)
-        self.assertListEqual(
-            list(annotations_map.annotations.keys()),
-            ["pkg_a", "pkg_b", "pkg_c", "pkg_d"],
-        )
-
-        collection = annotations_map.collect(["pkg_a", "pkg_b", "pkg_c", "pkg_d"])
-
-        self.assertEqual(
-            collection["pkg_a"],
-            Annotation(
-                {
-                    "additive_build_content": None,
-                    "copy_executables": {},
-                    "copy_files": {},
-                    "data": [],
-                    "data_exclude_glob": [],
-                    "srcs_exclude_glob": [],
-                }
-            ),
-        )
-
-        self.assertEqual(
-            collection["pkg_b"],
-            Annotation(
-                {
-                    "additive_build_content": None,
-                    "copy_executables": {},
-                    "copy_files": {},
-                    "data": [],
-                    "data_exclude_glob": ["*.foo", "*.bar"],
-                    "srcs_exclude_glob": [],
-                }
-            ),
-        )
-
-        self.assertEqual(
-            collection["pkg_c"],
-            Annotation(
-                {
-                    # The `join` and `strip` here accounts for potential
-                    # differences in new lines between unix and windows
-                    # hosts.
-                    "additive_build_content": "\n".join(
-                        [
-                            line.strip()
-                            for line in textwrap.dedent(
-                                """\
-                cc_library(
-                    name = "my_target",
-                    hdrs = glob(["**/*.h"]),
-                    srcs = glob(["**/*.cc"]),
-                )
-                """
-                            ).splitlines()
-                        ]
-                    ),
-                    "copy_executables": {},
-                    "copy_files": {},
-                    "data": [":my_target"],
-                    "data_exclude_glob": [],
-                    "srcs_exclude_glob": [],
-                }
-            ),
-        )
-
-        self.assertEqual(
-            collection["pkg_d"],
-            Annotation(
-                {
-                    "additive_build_content": None,
-                    "copy_executables": {},
-                    "copy_files": {},
-                    "data": [],
-                    "data_exclude_glob": [],
-                    "srcs_exclude_glob": ["pkg_d/tests/**"],
-                }
-            ),
-        )
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/python/pip_install/tools/lib/annotations_test_helpers.bzl b/python/pip_install/tools/lib/annotations_test_helpers.bzl
deleted file mode 100644
index 4f56bb7..0000000
--- a/python/pip_install/tools/lib/annotations_test_helpers.bzl
+++ /dev/null
@@ -1,47 +0,0 @@
-# Copyright 2023 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.
-
-"""Helper macros and rules for testing the `annotations` module of `tools`"""
-
-load("//python:pip.bzl", _package_annotation = "package_annotation")
-
-package_annotation = _package_annotation
-
-def _package_annotations_file_impl(ctx):
-    output = ctx.actions.declare_file(ctx.label.name + ".annotations.json")
-
-    annotations = {package: json.decode(data) for (package, data) in ctx.attr.annotations.items()}
-    ctx.actions.write(
-        output = output,
-        content = json.encode_indent(annotations, indent = " " * 4),
-    )
-
-    return DefaultInfo(
-        files = depset([output]),
-        runfiles = ctx.runfiles(files = [output]),
-    )
-
-package_annotations_file = rule(
-    implementation = _package_annotations_file_impl,
-    doc = (
-        "Consumes `package_annotation` definitions in the same way " +
-        "`pip_repository` rules do to produce an annotations file."
-    ),
-    attrs = {
-        "annotations": attr.string_dict(
-            doc = "See `@rules_python//python:pip.bzl%package_annotation",
-            mandatory = True,
-        ),
-    },
-)
diff --git a/python/pip_install/tools/lib/bazel.py b/python/pip_install/tools/lib/bazel.py
deleted file mode 100644
index 81119e9..0000000
--- a/python/pip_install/tools/lib/bazel.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright 2023 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.
-
-import re
-
-WHEEL_FILE_LABEL = "whl"
-PY_LIBRARY_LABEL = "pkg"
-DATA_LABEL = "data"
-DIST_INFO_LABEL = "dist_info"
-WHEEL_ENTRY_POINT_PREFIX = "rules_python_wheel_entry_point"
-
-
-def sanitise_name(name: str, prefix: str) -> str:
-    """Sanitises the name to be compatible with Bazel labels.
-
-    See the doc in ../../../private/normalize_name.bzl.
-    """
-    return prefix + re.sub(r"[-_.]+", "_", name).lower()
-
-
-def _whl_name_to_repo_root(whl_name: str, repo_prefix: str) -> str:
-    return "@{}//".format(sanitise_name(whl_name, prefix=repo_prefix))
-
-
-def sanitised_repo_library_label(whl_name: str, repo_prefix: str) -> str:
-    return '"{}:{}"'.format(
-        _whl_name_to_repo_root(whl_name, repo_prefix), PY_LIBRARY_LABEL
-    )
-
-
-def sanitised_repo_file_label(whl_name: str, repo_prefix: str) -> str:
-    return '"{}:{}"'.format(
-        _whl_name_to_repo_root(whl_name, repo_prefix), WHEEL_FILE_LABEL
-    )
diff --git a/python/pip_install/tools/wheel_installer/BUILD.bazel b/python/pip_install/tools/wheel_installer/BUILD.bazel
index 54bbc46..6360ca5 100644
--- a/python/pip_install/tools/wheel_installer/BUILD.bazel
+++ b/python/pip_install/tools/wheel_installer/BUILD.bazel
@@ -4,12 +4,12 @@
 py_library(
     name = "lib",
     srcs = [
+        "arguments.py",
         "namespace_pkgs.py",
         "wheel.py",
         "wheel_installer.py",
     ],
     deps = [
-        "//python/pip_install/tools/lib",
         requirement("installer"),
         requirement("pip"),
         requirement("setuptools"),
@@ -25,6 +25,17 @@
 )
 
 py_test(
+    name = "arguments_test",
+    size = "small",
+    srcs = [
+        "arguments_test.py",
+    ],
+    deps = [
+        ":lib",
+    ],
+)
+
+py_test(
     name = "namespace_pkgs_test",
     size = "small",
     srcs = [
diff --git a/python/pip_install/tools/lib/arguments.py b/python/pip_install/tools/wheel_installer/arguments.py
similarity index 87%
rename from python/pip_install/tools/lib/arguments.py
rename to python/pip_install/tools/wheel_installer/arguments.py
index 974f03c..aac3c01 100644
--- a/python/pip_install/tools/lib/arguments.py
+++ b/python/pip_install/tools/wheel_installer/arguments.py
@@ -12,16 +12,21 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import argparse
 import json
-from argparse import ArgumentParser
+from typing import Any
 
 
-def parse_common_args(parser: ArgumentParser) -> ArgumentParser:
+def parser(**kwargs: Any) -> argparse.ArgumentParser:
+    """Create a parser for the wheel_installer tool."""
+    parser = argparse.ArgumentParser(
+        **kwargs,
+    )
     parser.add_argument(
-        "--repo",
+        "--requirement",
         action="store",
         required=True,
-        help="The external repo name to install dependencies. In the format '@{REPO_NAME}'",
+        help="A single PEP508 requirement specifier string.",
     )
     parser.add_argument(
         "--isolated",
@@ -49,11 +54,6 @@
         help="Extra environment variables to set on the pip environment.",
     )
     parser.add_argument(
-        "--repo-prefix",
-        required=True,
-        help="Prefix to prepend to packages",
-    )
-    parser.add_argument(
         "--download_only",
         action="store_true",
         help="Use 'pip download' instead of 'pip wheel'. Disables building wheels from source, but allows use of "
diff --git a/python/pip_install/tools/lib/arguments_test.py b/python/pip_install/tools/wheel_installer/arguments_test.py
similarity index 81%
rename from python/pip_install/tools/lib/arguments_test.py
rename to python/pip_install/tools/wheel_installer/arguments_test.py
index dfa96a8..7193f4a 100644
--- a/python/pip_install/tools/lib/arguments_test.py
+++ b/python/pip_install/tools/wheel_installer/arguments_test.py
@@ -16,35 +16,30 @@
 import json
 import unittest
 
-from python.pip_install.tools.lib import arguments
+from python.pip_install.tools.wheel_installer import arguments
 
 
 class ArgumentsTestCase(unittest.TestCase):
     def test_arguments(self) -> None:
-        parser = argparse.ArgumentParser()
-        parser = arguments.parse_common_args(parser)
+        parser = arguments.parser()
         repo_name = "foo"
         repo_prefix = "pypi_"
         index_url = "--index_url=pypi.org/simple"
         extra_pip_args = [index_url]
+        requirement = "foo==1.0.0 --hash=sha256:deadbeef"
         args_dict = vars(
             parser.parse_args(
                 args=[
-                    "--repo",
-                    repo_name,
+                    f'--requirement="{requirement}"',
                     f"--extra_pip_args={json.dumps({'arg': extra_pip_args})}",
-                    "--repo-prefix",
-                    repo_prefix,
                 ]
             )
         )
         args_dict = arguments.deserialize_structured_args(args_dict)
-        self.assertIn("repo", args_dict)
+        self.assertIn("requirement", args_dict)
         self.assertIn("extra_pip_args", args_dict)
         self.assertEqual(args_dict["pip_data_exclude"], [])
         self.assertEqual(args_dict["enable_implicit_namespace_pkgs"], False)
-        self.assertEqual(args_dict["repo"], repo_name)
-        self.assertEqual(args_dict["repo_prefix"], repo_prefix)
         self.assertEqual(args_dict["extra_pip_args"], extra_pip_args)
 
     def test_deserialize_structured_args(self) -> None:
diff --git a/python/pip_install/tools/wheel_installer/wheel_installer.py b/python/pip_install/tools/wheel_installer/wheel_installer.py
index 9b363c3..c6c2961 100644
--- a/python/pip_install/tools/wheel_installer/wheel_installer.py
+++ b/python/pip_install/tools/wheel_installer/wheel_installer.py
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+"""Build and/or fetch a single wheel based on the requirement passed in"""
+
 import argparse
 import errno
 import glob
@@ -28,8 +30,7 @@
 
 from pip._vendor.packaging.utils import canonicalize_name
 
-from python.pip_install.tools.lib import annotation, arguments, bazel
-from python.pip_install.tools.wheel_installer import namespace_pkgs, wheel
+from python.pip_install.tools.wheel_installer import arguments, namespace_pkgs, wheel
 
 
 def _configure_reproducible_wheels() -> None:
@@ -103,201 +104,11 @@
         namespace_pkgs.add_pkgutil_style_namespace_pkg_init(ns_pkg_dir)
 
 
-def _generate_entry_point_contents(
-    module: str, attribute: str, shebang: str = "#!/usr/bin/env python3"
-) -> str:
-    """Generate the contents of an entry point script.
-
-    Args:
-        module (str): The name of the module to use.
-        attribute (str): The name of the attribute to call.
-        shebang (str, optional): The shebang to use for the entry point python
-            file.
-
-    Returns:
-        str: A string of python code.
-    """
-    return textwrap.dedent(
-        """\
-        {shebang}
-        import sys
-        from {module} import {attribute}
-        if __name__ == "__main__":
-            sys.exit({attribute}())
-        """.format(
-            shebang=shebang, module=module, attribute=attribute
-        )
-    )
-
-
-def _generate_entry_point_rule(name: str, script: str, pkg: str) -> str:
-    """Generate a Bazel `py_binary` rule for an entry point script.
-
-    Note that the script is used to determine the name of the target. The name of
-    entry point targets should be uniuqe to avoid conflicts with existing sources or
-    directories within a wheel.
-
-    Args:
-        name (str): The name of the generated py_binary.
-        script (str): The path to the entry point's python file.
-        pkg (str): The package owning the entry point. This is expected to
-            match up with the `py_library` defined for each repository.
-
-
-    Returns:
-        str: A `py_binary` instantiation.
-    """
-    return textwrap.dedent(
-        """\
-        py_binary(
-            name = "{name}",
-            srcs = ["{src}"],
-            # This makes this directory a top-level in the python import
-            # search path for anything that depends on this.
-            imports = ["."],
-            deps = ["{pkg}"],
-        )
-        """.format(
-            name=name, src=str(script).replace("\\", "/"), pkg=pkg
-        )
-    )
-
-
-def _generate_copy_commands(src, dest, is_executable=False) -> str:
-    """Generate a [@bazel_skylib//rules:copy_file.bzl%copy_file][cf] target
-
-    [cf]: https://github.com/bazelbuild/bazel-skylib/blob/1.1.1/docs/copy_file_doc.md
-
-    Args:
-        src (str): The label for the `src` attribute of [copy_file][cf]
-        dest (str): The label for the `out` attribute of [copy_file][cf]
-        is_executable (bool, optional): Whether or not the file being copied is executable.
-            sets `is_executable` for [copy_file][cf]
-
-    Returns:
-        str: A `copy_file` instantiation.
-    """
-    return textwrap.dedent(
-        """\
-        copy_file(
-            name = "{dest}.copy",
-            src = "{src}",
-            out = "{dest}",
-            is_executable = {is_executable},
-        )
-    """.format(
-            src=src,
-            dest=dest,
-            is_executable=is_executable,
-        )
-    )
-
-
-def _generate_build_file_contents(
-    name: str,
-    dependencies: List[str],
-    whl_file_deps: List[str],
-    data_exclude: List[str],
-    tags: List[str],
-    srcs_exclude: List[str] = [],
-    data: List[str] = [],
-    additional_content: List[str] = [],
-) -> str:
-    """Generate a BUILD file for an unzipped Wheel
-
-    Args:
-        name: the target name of the py_library
-        dependencies: a list of Bazel labels pointing to dependencies of the library
-        whl_file_deps: a list of Bazel labels pointing to wheel file dependencies of this wheel.
-        data_exclude: more patterns to exclude from the data attribute of generated py_library rules.
-        tags: list of tags to apply to generated py_library rules.
-        additional_content: A list of additional content to append to the BUILD file.
-
-    Returns:
-        A complete BUILD file as a string
-
-    We allow for empty Python sources as for Wheels containing only compiled C code
-    there may be no Python sources whatsoever (e.g. packages written in Cython: like `pymssql`).
-    """
-
-    data_exclude = list(
-        set(
-            [
-                "**/* *",
-                "**/*.py",
-                "**/*.pyc",
-                "**/*.pyc.*", # During pyc creation, temp files named *.pyc.NNNN are created
-                # RECORD is known to contain sha256 checksums of files which might include the checksums
-                # of generated files produced when wheels are installed. The file is ignored to avoid
-                # Bazel caching issues.
-                "**/*.dist-info/RECORD",
-            ]
-            + data_exclude
-        )
-    )
-
-    return "\n".join(
-        [
-            textwrap.dedent(
-                """\
-        load("@rules_python//python:defs.bzl", "py_library", "py_binary")
-        load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
-
-        package(default_visibility = ["//visibility:public"])
-
-        filegroup(
-            name = "{dist_info_label}",
-            srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
-        )
-
-        filegroup(
-            name = "{data_label}",
-            srcs = glob(["data/**"], allow_empty = True),
-        )
-
-        filegroup(
-            name = "{whl_file_label}",
-            srcs = glob(["*.whl"], allow_empty = True),
-            data = [{whl_file_deps}],
-        )
-
-        py_library(
-            name = "{name}",
-            srcs = glob(["site-packages/**/*.py"], exclude={srcs_exclude}, allow_empty = True),
-            data = {data} + glob(["site-packages/**/*"], exclude={data_exclude}),
-            # This makes this directory a top-level in the python import
-            # search path for anything that depends on this.
-            imports = ["site-packages"],
-            deps = [{dependencies}],
-            tags = [{tags}],
-        )
-        """.format(
-                    name=name,
-                    dependencies=",".join(sorted(dependencies)),
-                    data_exclude=json.dumps(sorted(data_exclude)),
-                    whl_file_label=bazel.WHEEL_FILE_LABEL,
-                    whl_file_deps=",".join(sorted(whl_file_deps)),
-                    tags=",".join(sorted(['"%s"' % t for t in tags])),
-                    data_label=bazel.DATA_LABEL,
-                    dist_info_label=bazel.DIST_INFO_LABEL,
-                    entry_point_prefix=bazel.WHEEL_ENTRY_POINT_PREFIX,
-                    srcs_exclude=json.dumps(sorted(srcs_exclude)),
-                    data=json.dumps(sorted(data)),
-                )
-            )
-        ]
-        + additional_content
-    )
-
-
 def _extract_wheel(
     wheel_file: str,
     extras: Dict[str, Set[str]],
-    pip_data_exclude: List[str],
     enable_implicit_namespace_pkgs: bool,
-    repo_prefix: str,
     installation_dir: Path = Path("."),
-    annotation: Optional[annotation.Annotation] = None,
 ) -> None:
     """Extracts wheel into given directory and creates py_library and filegroup targets.
 
@@ -305,9 +116,7 @@
         wheel_file: the filepath of the .whl
         installation_dir: the destination directory for installation of the wheel.
         extras: a list of extras to add as dependencies for the installed wheel
-        pip_data_exclude: list of file patterns to exclude from the generated data section of the py_library
         enable_implicit_namespace_pkgs: if true, disables conversion of implicit namespace packages and will unzip as-is
-        annotation: An optional set of annotations to apply to the BUILD contents of the wheel.
     """
 
     whl = wheel.Wheel(wheel_file)
@@ -322,83 +131,25 @@
     self_edge_dep = set([whl.name])
     whl_deps = sorted(whl.dependencies(extras_requested) - self_edge_dep)
 
-    sanitised_dependencies = [
-        bazel.sanitised_repo_library_label(d, repo_prefix=repo_prefix) for d in whl_deps
-    ]
-    sanitised_wheel_file_dependencies = [
-        bazel.sanitised_repo_file_label(d, repo_prefix=repo_prefix) for d in whl_deps
-    ]
-
-    entry_points = []
-    for name, (module, attribute) in sorted(whl.entry_points().items()):
-        # There is an extreme edge-case with entry_points that end with `.py`
-        # See: https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/test/java/com/google/devtools/build/lib/rules/python/PyBinaryConfiguredTargetTest.java#L174
-        entry_point_without_py = f"{name[:-3]}_py" if name.endswith(".py") else name
-        entry_point_target_name = (
-            f"{bazel.WHEEL_ENTRY_POINT_PREFIX}_{entry_point_without_py}"
-        )
-        entry_point_script_name = f"{entry_point_target_name}.py"
-        (installation_dir / entry_point_script_name).write_text(
-            _generate_entry_point_contents(module, attribute)
-        )
-        entry_points.append(
-            _generate_entry_point_rule(
-                entry_point_target_name,
-                entry_point_script_name,
-                bazel.PY_LIBRARY_LABEL,
-            )
-        )
-
-    with open(os.path.join(installation_dir, "BUILD.bazel"), "w") as build_file:
-        additional_content = entry_points
-        data = []
-        data_exclude = pip_data_exclude
-        srcs_exclude = []
-        if annotation:
-            for src, dest in annotation.copy_files.items():
-                data.append(dest)
-                additional_content.append(_generate_copy_commands(src, dest))
-            for src, dest in annotation.copy_executables.items():
-                data.append(dest)
-                additional_content.append(
-                    _generate_copy_commands(src, dest, is_executable=True)
-                )
-            data.extend(annotation.data)
-            data_exclude.extend(annotation.data_exclude_glob)
-            srcs_exclude.extend(annotation.srcs_exclude_glob)
-            if annotation.additive_build_content:
-                additional_content.append(annotation.additive_build_content)
-
-        contents = _generate_build_file_contents(
-            name=bazel.PY_LIBRARY_LABEL,
-            dependencies=sanitised_dependencies,
-            whl_file_deps=sanitised_wheel_file_dependencies,
-            data_exclude=data_exclude,
-            data=data,
-            srcs_exclude=srcs_exclude,
-            tags=["pypi_name=" + whl.name, "pypi_version=" + whl.version],
-            additional_content=additional_content,
-        )
-        build_file.write(contents)
+    with open(os.path.join(installation_dir, "metadata.json"), "w") as f:
+        metadata = {
+            "name": whl.name,
+            "version": whl.version,
+            "deps": whl_deps,
+            "entry_points": [
+                {
+                    "name": name,
+                    "module": module,
+                    "attribute": attribute,
+                }
+                for name, (module, attribute) in sorted(whl.entry_points().items())
+            ],
+        }
+        json.dump(metadata, f)
 
 
 def main() -> None:
-    parser = argparse.ArgumentParser(
-        description="Build and/or fetch a single wheel based on the requirement passed in"
-    )
-    parser.add_argument(
-        "--requirement",
-        action="store",
-        required=True,
-        help="A single PEP508 requirement specifier string.",
-    )
-    parser.add_argument(
-        "--annotation",
-        type=annotation.annotation_from_str_path,
-        help="A json encoded file containing annotations for rendered packages.",
-    )
-    arguments.parse_common_args(parser)
-    args = parser.parse_args()
+    args = arguments.parser(description=__doc__).parse_args()
     deserialized_args = dict(vars(args))
     arguments.deserialize_structured_args(deserialized_args)
 
@@ -441,10 +192,7 @@
     _extract_wheel(
         wheel_file=whl,
         extras=extras,
-        pip_data_exclude=deserialized_args["pip_data_exclude"],
         enable_implicit_namespace_pkgs=args.enable_implicit_namespace_pkgs,
-        repo_prefix=args.repo_prefix,
-        annotation=args.annotation,
     )
 
 
diff --git a/python/pip_install/tools/wheel_installer/wheel_installer_test.py b/python/pip_install/tools/wheel_installer/wheel_installer_test.py
index 8758b67..b24e500 100644
--- a/python/pip_install/tools/wheel_installer/wheel_installer_test.py
+++ b/python/pip_install/tools/wheel_installer/wheel_installer_test.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import json
 import os
 import shutil
 import tempfile
@@ -54,28 +55,29 @@
                 )
 
 
-class BazelTestCase(unittest.TestCase):
-    def test_generate_entry_point_contents(self):
-        got = wheel_installer._generate_entry_point_contents("sphinx.cmd.build", "main")
-        want = """#!/usr/bin/env python3
-import sys
-from sphinx.cmd.build import main
-if __name__ == "__main__":
-    sys.exit(main())
-"""
-        self.assertEqual(got, want)
-
-    def test_generate_entry_point_contents_with_shebang(self):
-        got = wheel_installer._generate_entry_point_contents(
-            "sphinx.cmd.build", "main", shebang="#!/usr/bin/python"
-        )
-        want = """#!/usr/bin/python
-import sys
-from sphinx.cmd.build import main
-if __name__ == "__main__":
-    sys.exit(main())
-"""
-        self.assertEqual(got, want)
+# TODO @aignas 2023-07-21: migrate to starlark
+# class BazelTestCase(unittest.TestCase):
+#     def test_generate_entry_point_contents(self):
+#         got = wheel_installer._generate_entry_point_contents("sphinx.cmd.build", "main")
+#         want = """#!/usr/bin/env python3
+# import sys
+# from sphinx.cmd.build import main
+# if __name__ == "__main__":
+#     sys.exit(main())
+# """
+#         self.assertEqual(got, want)
+#
+#     def test_generate_entry_point_contents_with_shebang(self):
+#         got = wheel_installer._generate_entry_point_contents(
+#             "sphinx.cmd.build", "main", shebang="#!/usr/bin/python"
+#         )
+#         want = """#!/usr/bin/python
+# import sys
+# from sphinx.cmd.build import main
+# if __name__ == "__main__":
+#     sys.exit(main())
+# """
+#         self.assertEqual(got, want)
 
 
 class TestWhlFilegroup(unittest.TestCase):
@@ -93,15 +95,33 @@
             self.wheel_path,
             installation_dir=Path(self.wheel_dir),
             extras={},
-            pip_data_exclude=[],
             enable_implicit_namespace_pkgs=False,
-            repo_prefix="prefix_",
         )
 
-        self.assertIn(self.wheel_name, os.listdir(self.wheel_dir))
-        with open("{}/BUILD.bazel".format(self.wheel_dir)) as build_file:
-            build_file_content = build_file.read()
-            self.assertIn("filegroup", build_file_content)
+        want_files = [
+            "metadata.json",
+            "site-packages",
+            self.wheel_name,
+        ]
+        self.assertEqual(
+            sorted(want_files),
+            sorted(
+                [
+                    str(p.relative_to(self.wheel_dir))
+                    for p in Path(self.wheel_dir).glob("*")
+                ]
+            ),
+        )
+        with open("{}/metadata.json".format(self.wheel_dir)) as metadata_file:
+            metadata_file_content = json.load(metadata_file)
+
+        want = dict(
+            version="0.0.1",
+            name="example-minimal-package",
+            deps=[],
+            entry_points=[],
+        )
+        self.assertEqual(want, metadata_file_content)
 
 
 if __name__ == "__main__":
diff --git a/tests/pip_install/BUILD.bazel b/tests/pip_install/BUILD.bazel
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/pip_install/BUILD.bazel
diff --git a/tests/pip_install/whl_library/BUILD.bazel b/tests/pip_install/whl_library/BUILD.bazel
new file mode 100644
index 0000000..5a27e11
--- /dev/null
+++ b/tests/pip_install/whl_library/BUILD.bazel
@@ -0,0 +1,3 @@
+load(":generate_build_bazel_tests.bzl", "generate_build_bazel_test_suite")
+
+generate_build_bazel_test_suite(name = "generate_build_bazel_tests")
diff --git a/tests/pip_install/whl_library/generate_build_bazel_tests.bzl b/tests/pip_install/whl_library/generate_build_bazel_tests.bzl
new file mode 100644
index 0000000..365233d
--- /dev/null
+++ b/tests/pip_install/whl_library/generate_build_bazel_tests.bzl
@@ -0,0 +1,225 @@
+# Copyright 2023 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.
+
+""
+
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("//python/pip_install/private:generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel")  # buildifier: disable=bzl-visibility
+
+_tests = []
+
+def _test_simple(env):
+    want = """\
+load("@rules_python//python:defs.bzl", "py_library", "py_binary")
+load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
+
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+    name = "dist_info",
+    srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
+)
+
+filegroup(
+    name = "data",
+    srcs = glob(["data/**"], allow_empty = True),
+)
+
+filegroup(
+    name = "whl",
+    srcs = glob(["*.whl"], allow_empty = True),
+    data = ["@pypi_bar_baz//:whl", "@pypi_foo//:whl"],
+)
+
+py_library(
+    name = "pkg",
+    srcs = glob(
+        ["site-packages/**/*.py"],
+        exclude=[],
+        # Empty sources are allowed to support wheels that don't have any
+        # pure-Python code, e.g. pymssql, which is written in Cython.
+        allow_empty = True,
+    ),
+    data = [] + glob(
+        ["site-packages/**/*"],
+        exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"],
+    ),
+    # This makes this directory a top-level in the python import
+    # search path for anything that depends on this.
+    imports = ["site-packages"],
+    deps = ["@pypi_bar_baz//:pkg", "@pypi_foo//:pkg"],
+    tags = ["tag1", "tag2"],
+)
+"""
+    actual = generate_whl_library_build_bazel(
+        repo_prefix = "pypi_",
+        dependencies = ["foo", "bar-baz"],
+        data_exclude = [],
+        tags = ["tag1", "tag2"],
+        entry_points = {},
+        annotation = None,
+    )
+    env.expect.that_str(actual).equals(want)
+
+_tests.append(_test_simple)
+
+def _test_with_annotation(env):
+    want = """\
+load("@rules_python//python:defs.bzl", "py_library", "py_binary")
+load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
+
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+    name = "dist_info",
+    srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
+)
+
+filegroup(
+    name = "data",
+    srcs = glob(["data/**"], allow_empty = True),
+)
+
+filegroup(
+    name = "whl",
+    srcs = glob(["*.whl"], allow_empty = True),
+    data = ["@pypi_bar_baz//:whl", "@pypi_foo//:whl"],
+)
+
+py_library(
+    name = "pkg",
+    srcs = glob(
+        ["site-packages/**/*.py"],
+        exclude=["srcs_exclude_all"],
+        # Empty sources are allowed to support wheels that don't have any
+        # pure-Python code, e.g. pymssql, which is written in Cython.
+        allow_empty = True,
+    ),
+    data = ["file_dest", "exec_dest"] + glob(
+        ["site-packages/**/*"],
+        exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD", "data_exclude_all"],
+    ),
+    # This makes this directory a top-level in the python import
+    # search path for anything that depends on this.
+    imports = ["site-packages"],
+    deps = ["@pypi_bar_baz//:pkg", "@pypi_foo//:pkg"],
+    tags = ["tag1", "tag2"],
+)
+
+copy_file(
+    name = "file_dest.copy",
+    src = "file_src",
+    out = "file_dest",
+    is_executable = False,
+)
+
+copy_file(
+    name = "exec_dest.copy",
+    src = "exec_src",
+    out = "exec_dest",
+    is_executable = True,
+)
+
+# SOMETHING SPECIAL AT THE END
+"""
+    actual = generate_whl_library_build_bazel(
+        repo_prefix = "pypi_",
+        dependencies = ["foo", "bar-baz"],
+        data_exclude = [],
+        tags = ["tag1", "tag2"],
+        entry_points = {},
+        annotation = struct(
+            copy_files = {"file_src": "file_dest"},
+            copy_executables = {"exec_src": "exec_dest"},
+            data = [],
+            data_exclude_glob = ["data_exclude_all"],
+            srcs_exclude_glob = ["srcs_exclude_all"],
+            additive_build_content = """# SOMETHING SPECIAL AT THE END""",
+        ),
+    )
+    env.expect.that_str(actual).equals(want)
+
+_tests.append(_test_with_annotation)
+
+def _test_with_entry_points(env):
+    want = """\
+load("@rules_python//python:defs.bzl", "py_library", "py_binary")
+load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
+
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+    name = "dist_info",
+    srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
+)
+
+filegroup(
+    name = "data",
+    srcs = glob(["data/**"], allow_empty = True),
+)
+
+filegroup(
+    name = "whl",
+    srcs = glob(["*.whl"], allow_empty = True),
+    data = ["@pypi_bar_baz//:whl", "@pypi_foo//:whl"],
+)
+
+py_library(
+    name = "pkg",
+    srcs = glob(
+        ["site-packages/**/*.py"],
+        exclude=[],
+        # Empty sources are allowed to support wheels that don't have any
+        # pure-Python code, e.g. pymssql, which is written in Cython.
+        allow_empty = True,
+    ),
+    data = [] + glob(
+        ["site-packages/**/*"],
+        exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"],
+    ),
+    # This makes this directory a top-level in the python import
+    # search path for anything that depends on this.
+    imports = ["site-packages"],
+    deps = ["@pypi_bar_baz//:pkg", "@pypi_foo//:pkg"],
+    tags = ["tag1", "tag2"],
+)
+
+py_binary(
+    name = "rules_python_wheel_entry_point_fizz",
+    srcs = ["buzz.py"],
+    # This makes this directory a top-level in the python import
+    # search path for anything that depends on this.
+    imports = ["."],
+    deps = [":pkg"],
+)
+"""
+    actual = generate_whl_library_build_bazel(
+        repo_prefix = "pypi_",
+        dependencies = ["foo", "bar-baz"],
+        data_exclude = [],
+        tags = ["tag1", "tag2"],
+        entry_points = {"fizz": "buzz.py"},
+        annotation = None,
+    )
+    env.expect.that_str(actual).equals(want)
+
+_tests.append(_test_with_entry_points)
+
+def generate_build_bazel_test_suite(name):
+    """Create the test suite.
+
+    Args:
+        name: the name of the test suite
+    """
+    test_suite(name = name, basic_tests = _tests)