changes to pip_repository source files now re-trigger the repo rule (#601)

* changes to pip_repository source files now re-trigger the repo rule

* update diff test message.

* added note about auto-gen.

* Updated docs

* remove unnecessary snippet

* installer -> updater

Co-authored-by: Jonathon Belotti <jonathon@canva.com>
diff --git a/docs/BUILD b/docs/BUILD
index 6ef0d6e..e08b751 100644
--- a/docs/BUILD
+++ b/docs/BUILD
@@ -61,9 +61,7 @@
 bzl_library(
     name = "pip_install_bzl",
     srcs = [
-        "//python/pip_install:pip_repository.bzl",
-        "//python/pip_install:repositories.bzl",
-        "//python/pip_install:requirements.bzl",
+        "//python/pip_install:bzl",
     ],
     deps = [
         ":defs",
diff --git a/python/BUILD b/python/BUILD
index 96df383..7dbbec3 100644
--- a/python/BUILD
+++ b/python/BUILD
@@ -46,6 +46,7 @@
         "defs.bzl",
         "packaging.bzl",
         "pip.bzl",
+        "//python/pip_install:bzl",
         "//python/private:bzl",
     ],
     visibility = ["//:__pkg__"],
diff --git a/python/pip_install/BUILD b/python/pip_install/BUILD
index 90be44c..41cac99 100644
--- a/python/pip_install/BUILD
+++ b/python/pip_install/BUILD
@@ -7,14 +7,26 @@
         "pip_compile.py",
         "//python/pip_install/extract_wheels:distribution",
         "//python/pip_install/parse_requirements_to_bzl:distribution",
+        "//python/pip_install/private:distribution",
     ],
     visibility = ["//:__pkg__"],
 )
 
 filegroup(
     name = "bzl",
-    srcs = glob(["*.bzl"]),
-    visibility = ["//:__pkg__"],
+    srcs = glob(["*.bzl"]) + [
+        "//python/pip_install/private:bzl_srcs",
+    ],
+    visibility = ["//:__subpackages__"],
+)
+
+filegroup(
+    name = "py_srcs",
+    srcs = [
+        "//python/pip_install/extract_wheels:py_srcs",
+        "//python/pip_install/parse_requirements_to_bzl:py_srcs",
+    ],
+    visibility = ["//visibility:public"],
 )
 
 exports_files(
diff --git a/python/pip_install/extract_wheels/BUILD b/python/pip_install/extract_wheels/BUILD
index 92a0c7a..a92c562 100644
--- a/python/pip_install/extract_wheels/BUILD
+++ b/python/pip_install/extract_wheels/BUILD
@@ -17,3 +17,14 @@
     ],
     visibility = ["//python/pip_install:__subpackages__"],
 )
+
+filegroup(
+    name = "py_srcs",
+    srcs = glob(
+        include = ["**/*.py"],
+        exclude = ["**/*_test.py"],
+    ) + [
+        "//python/pip_install/extract_wheels/lib:py_srcs",
+    ],
+    visibility = ["//python/pip_install:__subpackages__"],
+)
diff --git a/python/pip_install/extract_wheels/lib/BUILD b/python/pip_install/extract_wheels/lib/BUILD
index 2b0a91f..4758f15 100644
--- a/python/pip_install/extract_wheels/lib/BUILD
+++ b/python/pip_install/extract_wheels/lib/BUILD
@@ -110,3 +110,12 @@
     ),
     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/parse_requirements_to_bzl/BUILD b/python/pip_install/parse_requirements_to_bzl/BUILD
index bb60323..8a876ab 100644
--- a/python/pip_install/parse_requirements_to_bzl/BUILD
+++ b/python/pip_install/parse_requirements_to_bzl/BUILD
@@ -41,3 +41,14 @@
     ],
     visibility = ["//python/pip_install:__subpackages__"],
 )
+
+filegroup(
+    name = "py_srcs",
+    srcs = glob(
+        include = ["**/*.py"],
+        exclude = ["**/*_test.py"],
+    ) + [
+        "//python/pip_install/parse_requirements_to_bzl/extract_single_wheel:py_srcs",
+    ],
+    visibility = ["//python/pip_install:__subpackages__"],
+)
diff --git a/python/pip_install/parse_requirements_to_bzl/extract_single_wheel/BUILD b/python/pip_install/parse_requirements_to_bzl/extract_single_wheel/BUILD
index 17bdfe7..bc0f640 100644
--- a/python/pip_install/parse_requirements_to_bzl/extract_single_wheel/BUILD
+++ b/python/pip_install/parse_requirements_to_bzl/extract_single_wheel/BUILD
@@ -6,3 +6,12 @@
     ),
     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/pip_repository.bzl b/python/pip_install/pip_repository.bzl
index 1dc49c7..3d0710a 100644
--- a/python/pip_install/pip_repository.bzl
+++ b/python/pip_install/pip_repository.bzl
@@ -1,6 +1,7 @@
 ""
 
 load("//python/pip_install:repositories.bzl", "all_requirements")
+load("//python/pip_install/private:srcs.bzl", "PIP_INSTALL_PY_SRCS")
 
 def _construct_pypath(rctx):
     """Helper function to construct a PYTHONPATH.
@@ -250,6 +251,11 @@
         default = 600,
         doc = "Timeout (in seconds) on the rule's execution duration.",
     ),
+    "_py_srcs": attr.label_list(
+        doc = "Python sources used in the repository rule",
+        allow_files = True,
+        default = PIP_INSTALL_PY_SRCS,
+    ),
 }
 
 pip_repository_attrs = {
diff --git a/python/pip_install/private/BUILD b/python/pip_install/private/BUILD
new file mode 100644
index 0000000..86b4b3d
--- /dev/null
+++ b/python/pip_install/private/BUILD
@@ -0,0 +1,24 @@
+load(":pip_install_utils.bzl", "srcs_module")
+
+package(default_visibility = ["//:__subpackages__"])
+
+exports_files([
+    "srcs.bzl",
+])
+
+filegroup(
+    name = "distribution",
+    srcs = glob(["*"]),
+    visibility = ["//python/pip_install:__subpackages__"],
+)
+
+filegroup(
+    name = "bzl_srcs",
+    srcs = glob(["*.bzl"]),
+)
+
+srcs_module(
+    name = "srcs_module",
+    srcs = "//python/pip_install:py_srcs",
+    dest = ":srcs.bzl",
+)
diff --git a/python/pip_install/private/pip_install_utils.bzl b/python/pip_install/private/pip_install_utils.bzl
new file mode 100644
index 0000000..038ee0e
--- /dev/null
+++ b/python/pip_install/private/pip_install_utils.bzl
@@ -0,0 +1,118 @@
+"""Utilities for `rules_python` pip rules"""
+
+_SRCS_TEMPLATE = """\
+\"\"\"A generate file containing all source files used for `@rules_python//python/pip_install:pip_repository.bzl` rules
+
+This file is auto-generated from the `@rules_python//python/pip_install/private:srcs_module.install` target. Please
+`bazel run` this target to apply any updates. Note that doing so will discard any local modifications.
+"\"\"
+
+# Each source file is tracked as a target so `pip_repository` rules will know to automatically rebuild if any of the
+# sources changed.
+PIP_INSTALL_PY_SRCS = [
+    {srcs}
+]
+"""
+
+def _src_label(file):
+    dir_path, file_name = file.short_path.rsplit("/", 1)
+
+    return "@rules_python//{}:{}".format(
+        dir_path,
+        file_name,
+    )
+
+def _srcs_module_impl(ctx):
+    srcs = [_src_label(src) for src in ctx.files.srcs]
+    if not srcs:
+        fail("`srcs` cannot be empty")
+    output = ctx.actions.declare_file(ctx.label.name)
+
+    ctx.actions.write(
+        output = output,
+        content = _SRCS_TEMPLATE.format(
+            srcs = "\n    ".join(["\"{}\",".format(src) for src in srcs]),
+        ),
+    )
+
+    return DefaultInfo(
+        files = depset([output]),
+    )
+
+_srcs_module = rule(
+    doc = "A rule for writing a list of sources to a templated file",
+    implementation = _srcs_module_impl,
+    attrs = {
+        "srcs": attr.label(
+            doc = "A filegroup of source files",
+            allow_files = True,
+        ),
+    },
+)
+
+_INSTALLER_TEMPLATE = """\
+#!/bin/bash
+set -euo pipefail
+cp -f "{path}" "${{BUILD_WORKSPACE_DIRECTORY}}/{dest}"
+"""
+
+def _srcs_updater_impl(ctx):
+    output = ctx.actions.declare_file(ctx.label.name + ".sh")
+    target_file = ctx.file.input
+    dest = ctx.file.dest.short_path
+
+    ctx.actions.write(
+        output = output,
+        content = _INSTALLER_TEMPLATE.format(
+            path = target_file.short_path,
+            dest = dest,
+        ),
+        is_executable = True,
+    )
+
+    return DefaultInfo(
+        files = depset([output]),
+        runfiles = ctx.runfiles(files = [target_file]),
+        executable = output,
+    )
+
+_srcs_updater = rule(
+    doc = "A rule for writing a `srcs.bzl` file back to the repository",
+    implementation = _srcs_updater_impl,
+    attrs = {
+        "dest": attr.label(
+            doc = "The target file to write the new `input` to.",
+            allow_single_file = ["srcs.bzl"],
+            mandatory = True,
+        ),
+        "input": attr.label(
+            doc = "The file to write back to the repository",
+            allow_single_file = True,
+            mandatory = True,
+        ),
+    },
+    executable = True,
+)
+
+def srcs_module(name, dest, **kwargs):
+    """A helper rule to ensure `pip_repository` rules are always up to date
+
+    Args:
+        name (str): The name of the sources module
+        dest (str): The filename the module should be written as in the current package.
+        **kwargs (dict): Additional keyword arguments
+    """
+    tags = kwargs.pop("tags", [])
+
+    _srcs_module(
+        name = name,
+        tags = tags,
+        **kwargs
+    )
+
+    _srcs_updater(
+        name = name + ".update",
+        input = name,
+        dest = dest,
+        tags = tags,
+    )
diff --git a/python/pip_install/private/srcs.bzl b/python/pip_install/private/srcs.bzl
new file mode 100644
index 0000000..3784fa8
--- /dev/null
+++ b/python/pip_install/private/srcs.bzl
@@ -0,0 +1,23 @@
+"""A generate file containing all source files used for `@rules_python//python/pip_install:pip_repository.bzl` rules
+
+This file is auto-generated from the `@rules_python//python/pip_install/private:srcs_module.install` target. Please
+`bazel run` this target to apply any updates. Note that doing so will discard any local modifications.
+"""
+
+# Each source file is tracked as a target so `pip_repository` rules will know to automatically rebuild if any of the
+# sources changed.
+PIP_INSTALL_PY_SRCS = [
+    "@rules_python//python/pip_install/extract_wheels:__init__.py",
+    "@rules_python//python/pip_install/extract_wheels:__main__.py",
+    "@rules_python//python/pip_install/extract_wheels/lib:__init__.py",
+    "@rules_python//python/pip_install/extract_wheels/lib:arguments.py",
+    "@rules_python//python/pip_install/extract_wheels/lib:bazel.py",
+    "@rules_python//python/pip_install/extract_wheels/lib:namespace_pkgs.py",
+    "@rules_python//python/pip_install/extract_wheels/lib:purelib.py",
+    "@rules_python//python/pip_install/extract_wheels/lib:requirements.py",
+    "@rules_python//python/pip_install/extract_wheels/lib:wheel.py",
+    "@rules_python//python/pip_install/parse_requirements_to_bzl:__init__.py",
+    "@rules_python//python/pip_install/parse_requirements_to_bzl:__main__.py",
+    "@rules_python//python/pip_install/parse_requirements_to_bzl/extract_single_wheel:__init__.py",
+    "@rules_python//python/pip_install/parse_requirements_to_bzl/extract_single_wheel:__main__.py",
+]
diff --git a/python/pip_install/private/test/BUILD b/python/pip_install/private/test/BUILD
new file mode 100644
index 0000000..90d1846
--- /dev/null
+++ b/python/pip_install/private/test/BUILD
@@ -0,0 +1,17 @@
+load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
+
+diff_test(
+    name = "srcs_diff_test",
+    failure_message = (
+        "Please run `bazel run //python/pip_install/private:srcs_module.update` " +
+        "to update the `srcs.bzl` module found in the same package."
+    ),
+    file1 = "//python/pip_install/private:srcs_module",
+    file2 = "//python/pip_install/private:srcs.bzl",
+    # TODO: The diff_test here fails on Windows. As does the
+    # install script. This should be fixed.
+    target_compatible_with = select({
+        "@platforms//os:windows": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
+)