fix(pip.parse): make the pip extension reproducible if PyPI is not called (#1937)
With this PR we can finally have fewer lock file entries in setups which
do not use the `experimental_index_url` feature yet. This is fully
backwards compatible change as it relies on `bazel` doing the right
thing and regenerating the lock file.
Fixes #1643.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ca62e38..0720a36 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -57,6 +57,12 @@
replaced by whl_modifications.
* (pip) Correctly select wheels when the python tag includes minor versions.
See ([#1930](https://github.com/bazelbuild/rules_python/issues/1930))
+* (pip.parse): The lock file is now reproducible on any host platform if the
+ `experimental_index_url` is not used by any of the modules in the dependency
+ chain. To make the lock file identical on each `os` and `arch`, please use
+ the `experimental_index_url` feature which will fetch metadata from PyPI or a
+ different private index and write the contents to the lock file. Fixes
+ [#1643](https://github.com/bazelbuild/rules_python/issues/1643).
### Added
* (rules) Precompiling Python source at build time is available. but is
diff --git a/MODULE.bazel b/MODULE.bazel
index 04d8fb2..38ee678 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -56,9 +56,8 @@
#####################
# Install twine for our own runfiles wheel publishing and allow bzlmod users to use it.
-pip = use_extension("//python/extensions:pip.bzl", "pip")
+pip = use_extension("//python/private/bzlmod:pip.bzl", "pip_internal")
pip.parse(
- experimental_index_url = "https://pypi.org/simple",
hub_name = "rules_python_publish_deps",
python_version = "3.11",
requirements_by_platform = {
@@ -80,13 +79,11 @@
bazel_dep(name = "gazelle", version = "0.33.0", dev_dependency = True, repo_name = "bazel_gazelle")
dev_pip = use_extension(
- "//python/extensions:pip.bzl",
- "pip",
+ "//python/private/bzlmod:pip.bzl",
+ "pip_internal",
dev_dependency = True,
)
dev_pip.parse(
- envsubst = ["PIP_INDEX_URL"],
- experimental_index_url = "${PIP_INDEX_URL:-https://pypi.org/simple}",
experimental_requirement_cycles = {
"sphinx": [
"sphinx",
@@ -102,8 +99,6 @@
requirements_lock = "//docs/sphinx:requirements.txt",
)
dev_pip.parse(
- envsubst = ["PIP_INDEX_URL"],
- experimental_index_url = "${PIP_INDEX_URL:-https://pypi.org/simple}",
hub_name = "pypiserver",
python_version = "3.11",
requirements_lock = "//examples/wheel:requirements_server.txt",
diff --git a/python/private/bzlmod/pip.bzl b/python/private/bzlmod/pip.bzl
index 9e29877..8702f1f 100644
--- a/python/private/bzlmod/pip.bzl
+++ b/python/private/bzlmod/pip.bzl
@@ -103,6 +103,7 @@
def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, simpleapi_cache):
logger = repo_utils.logger(module_ctx)
python_interpreter_target = pip_attr.python_interpreter_target
+ is_hub_reproducible = True
# if we do not have the python_interpreter set in the attributes
# we programmatically find it.
@@ -274,6 +275,7 @@
logger.debug(lambda: "Selected: {}".format(distribution))
if distribution:
+ is_hub_reproducible = False
whl_library_args["requirement"] = requirement.srcs.requirement
whl_library_args["urls"] = [distribution.url]
whl_library_args["sha256"] = distribution.sha256
@@ -303,6 +305,8 @@
),
)
+ return is_hub_reproducible
+
def _pip_impl(module_ctx):
"""Implementation of a class tag that creates the pip hub and corresponding pip spoke whl repositories.
@@ -412,6 +416,7 @@
hub_group_map = {}
simpleapi_cache = {}
+ is_extension_reproducible = True
for mod in module_ctx.modules:
for pip_attr in mod.tags.parse:
@@ -448,7 +453,8 @@
else:
pip_hub_map[pip_attr.hub_name].python_versions.append(pip_attr.python_version)
- _create_whl_repos(module_ctx, pip_attr, hub_whl_map, whl_overrides, hub_group_map, simpleapi_cache)
+ is_hub_reproducible = _create_whl_repos(module_ctx, pip_attr, hub_whl_map, whl_overrides, hub_group_map, simpleapi_cache)
+ is_extension_reproducible = is_extension_reproducible and is_hub_reproducible
for hub_name, whl_map in hub_whl_map.items():
pip_repository(
@@ -462,7 +468,34 @@
groups = hub_group_map.get(hub_name),
)
-def _pip_parse_ext_attrs():
+ if bazel_features.external_deps.extension_metadata_has_reproducible:
+ # If we are not using the `experimental_index_url feature, the extension is fully
+ # deterministic and we don't need to create a lock entry for it.
+ #
+ # In order to be able to dogfood the `experimental_index_url` feature before it gets
+ # stabilized, we have created the `_pip_non_reproducible` function, that will result
+ # in extra entries in the lock file.
+ return module_ctx.extension_metadata(reproducible = is_extension_reproducible)
+ else:
+ return None
+
+def _pip_non_reproducible(module_ctx):
+ _pip_impl(module_ctx)
+
+ # We default to calling the PyPI index and that will go into the
+ # MODULE.bazel.lock file, hence return nothing here.
+ return None
+
+def _pip_parse_ext_attrs(**kwargs):
+ """Get the attributes for the pip extension.
+
+ Args:
+ **kwargs: A kwarg for setting defaults for the specific attributes. The
+ key is expected to be the same as the attribute key.
+
+ Returns:
+ A dict of attributes.
+ """
attrs = dict({
"experimental_extra_index_urls": attr.string_list(
doc = """\
@@ -477,6 +510,7 @@
default = [],
),
"experimental_index_url": attr.string(
+ default = kwargs.get("experimental_index_url", ""),
doc = """\
The index URL to use for downloading wheels using bazel downloader. This value is going
to be subject to `envsubst` substitutions if necessary.
@@ -661,17 +695,6 @@
other tags in this extension.""",
)
-def _extension_extra_args():
- args = {}
-
- if bazel_features.external_deps.module_extension_has_os_arch_dependent:
- args = args | {
- "arch_dependent": True,
- "os_dependent": True,
- }
-
- return args
-
pip = module_extension(
doc = """\
This extension is used to make dependencies from pip available.
@@ -714,7 +737,56 @@
""",
),
},
- **_extension_extra_args()
+)
+
+pip_internal = module_extension(
+ doc = """\
+This extension is used to make dependencies from pypi available.
+
+For now this is intended to be used internally so that usage of the `pip`
+extension in `rules_python` does not affect the evaluations of the extension
+for the consumers.
+
+pip.parse:
+To use, call `pip.parse()` and specify `hub_name` and your requirements file.
+Dependencies will be downloaded and made available in a repo named after the
+`hub_name` argument.
+
+Each `pip.parse()` call configures a particular Python version. Multiple calls
+can be made to configure different Python versions, and will be grouped by
+the `hub_name` argument. This allows the same logical name, e.g. `@pypi//numpy`
+to automatically resolve to different, Python version-specific, libraries.
+
+pip.whl_mods:
+This tag class is used to help create JSON files to describe modifications to
+the BUILD files for wheels.
+""",
+ implementation = _pip_non_reproducible,
+ tag_classes = {
+ "override": _override_tag,
+ "parse": tag_class(
+ attrs = _pip_parse_ext_attrs(
+ experimental_index_url = "https://pypi.org/simple",
+ ),
+ doc = """\
+This tag class is used to create a pypi hub and all of the spokes that are part of that hub.
+This tag class reuses most of the pypi attributes that are found in
+@rules_python//python/pip_install:pip_repository.bzl.
+The exception is it does not use the arg 'repo_prefix'. We set the repository
+prefix for the user and the alias arg is always True in bzlmod.
+""",
+ ),
+ "whl_mods": tag_class(
+ attrs = _whl_mod_attrs(),
+ doc = """\
+This tag class is used to create JSON file that are used when calling wheel_builder.py. These
+JSON files contain instructions on how to modify a wheel's project. Each of the attributes
+create different modifications based on the type of attribute. Previously to bzlmod these
+JSON files where referred to as annotations, and were renamed to whl_modifications in this
+extension.
+""",
+ ),
+ },
)
def _whl_mods_repo_impl(rctx):