Skip to content

Commit e42f8f4

Browse files
authored
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.
1 parent d0e25cf commit e42f8f4

File tree

3 files changed

+95
-22
lines changed

3 files changed

+95
-22
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ A brief description of the categories of changes:
5757
replaced by whl_modifications.
5858
* (pip) Correctly select wheels when the python tag includes minor versions.
5959
See ([#1930](https://github.com/bazelbuild/rules_python/issues/1930))
60+
* (pip.parse): The lock file is now reproducible on any host platform if the
61+
`experimental_index_url` is not used by any of the modules in the dependency
62+
chain. To make the lock file identical on each `os` and `arch`, please use
63+
the `experimental_index_url` feature which will fetch metadata from PyPI or a
64+
different private index and write the contents to the lock file. Fixes
65+
[#1643](https://github.com/bazelbuild/rules_python/issues/1643).
6066

6167
### Added
6268
* (rules) Precompiling Python source at build time is available. but is

MODULE.bazel

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,8 @@ register_toolchains("@pythons_hub//:all")
5656
#####################
5757
# Install twine for our own runfiles wheel publishing and allow bzlmod users to use it.
5858

59-
pip = use_extension("//python/extensions:pip.bzl", "pip")
59+
pip = use_extension("//python/private/bzlmod:pip.bzl", "pip_internal")
6060
pip.parse(
61-
experimental_index_url = "https://pypi.org/simple",
6261
hub_name = "rules_python_publish_deps",
6362
python_version = "3.11",
6463
requirements_by_platform = {
@@ -80,13 +79,11 @@ bazel_dep(name = "rules_go", version = "0.41.0", dev_dependency = True, repo_nam
8079
bazel_dep(name = "gazelle", version = "0.33.0", dev_dependency = True, repo_name = "bazel_gazelle")
8180

8281
dev_pip = use_extension(
83-
"//python/extensions:pip.bzl",
84-
"pip",
82+
"//python/private/bzlmod:pip.bzl",
83+
"pip_internal",
8584
dev_dependency = True,
8685
)
8786
dev_pip.parse(
88-
envsubst = ["PIP_INDEX_URL"],
89-
experimental_index_url = "${PIP_INDEX_URL:-https://pypi.org/simple}",
9087
experimental_requirement_cycles = {
9188
"sphinx": [
9289
"sphinx",
@@ -102,8 +99,6 @@ dev_pip.parse(
10299
requirements_lock = "//docs/sphinx:requirements.txt",
103100
)
104101
dev_pip.parse(
105-
envsubst = ["PIP_INDEX_URL"],
106-
experimental_index_url = "${PIP_INDEX_URL:-https://pypi.org/simple}",
107102
hub_name = "pypiserver",
108103
python_version = "3.11",
109104
requirements_lock = "//examples/wheel:requirements_server.txt",

python/private/bzlmod/pip.bzl

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ You cannot use both the additive_build_content and additive_build_content_file a
103103
def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, simpleapi_cache):
104104
logger = repo_utils.logger(module_ctx)
105105
python_interpreter_target = pip_attr.python_interpreter_target
106+
is_hub_reproducible = True
106107

107108
# if we do not have the python_interpreter set in the attributes
108109
# we programmatically find it.
@@ -274,6 +275,7 @@ def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, s
274275
logger.debug(lambda: "Selected: {}".format(distribution))
275276

276277
if distribution:
278+
is_hub_reproducible = False
277279
whl_library_args["requirement"] = requirement.srcs.requirement
278280
whl_library_args["urls"] = [distribution.url]
279281
whl_library_args["sha256"] = distribution.sha256
@@ -303,6 +305,8 @@ def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, s
303305
),
304306
)
305307

308+
return is_hub_reproducible
309+
306310
def _pip_impl(module_ctx):
307311
"""Implementation of a class tag that creates the pip hub and corresponding pip spoke whl repositories.
308312
@@ -412,6 +416,7 @@ def _pip_impl(module_ctx):
412416
hub_group_map = {}
413417

414418
simpleapi_cache = {}
419+
is_extension_reproducible = True
415420

416421
for mod in module_ctx.modules:
417422
for pip_attr in mod.tags.parse:
@@ -448,7 +453,8 @@ def _pip_impl(module_ctx):
448453
else:
449454
pip_hub_map[pip_attr.hub_name].python_versions.append(pip_attr.python_version)
450455

451-
_create_whl_repos(module_ctx, pip_attr, hub_whl_map, whl_overrides, hub_group_map, simpleapi_cache)
456+
is_hub_reproducible = _create_whl_repos(module_ctx, pip_attr, hub_whl_map, whl_overrides, hub_group_map, simpleapi_cache)
457+
is_extension_reproducible = is_extension_reproducible and is_hub_reproducible
452458

453459
for hub_name, whl_map in hub_whl_map.items():
454460
pip_repository(
@@ -462,7 +468,34 @@ def _pip_impl(module_ctx):
462468
groups = hub_group_map.get(hub_name),
463469
)
464470

465-
def _pip_parse_ext_attrs():
471+
if bazel_features.external_deps.extension_metadata_has_reproducible:
472+
# If we are not using the `experimental_index_url feature, the extension is fully
473+
# deterministic and we don't need to create a lock entry for it.
474+
#
475+
# In order to be able to dogfood the `experimental_index_url` feature before it gets
476+
# stabilized, we have created the `_pip_non_reproducible` function, that will result
477+
# in extra entries in the lock file.
478+
return module_ctx.extension_metadata(reproducible = is_extension_reproducible)
479+
else:
480+
return None
481+
482+
def _pip_non_reproducible(module_ctx):
483+
_pip_impl(module_ctx)
484+
485+
# We default to calling the PyPI index and that will go into the
486+
# MODULE.bazel.lock file, hence return nothing here.
487+
return None
488+
489+
def _pip_parse_ext_attrs(**kwargs):
490+
"""Get the attributes for the pip extension.
491+
492+
Args:
493+
**kwargs: A kwarg for setting defaults for the specific attributes. The
494+
key is expected to be the same as the attribute key.
495+
496+
Returns:
497+
A dict of attributes.
498+
"""
466499
attrs = dict({
467500
"experimental_extra_index_urls": attr.string_list(
468501
doc = """\
@@ -477,6 +510,7 @@ This is equivalent to `--extra-index-urls` `pip` option.
477510
default = [],
478511
),
479512
"experimental_index_url": attr.string(
513+
default = kwargs.get("experimental_index_url", ""),
480514
doc = """\
481515
The index URL to use for downloading wheels using bazel downloader. This value is going
482516
to be subject to `envsubst` substitutions if necessary.
@@ -661,17 +695,6 @@ Apply any overrides (e.g. patches) to a given Python distribution defined by
661695
other tags in this extension.""",
662696
)
663697

664-
def _extension_extra_args():
665-
args = {}
666-
667-
if bazel_features.external_deps.module_extension_has_os_arch_dependent:
668-
args = args | {
669-
"arch_dependent": True,
670-
"os_dependent": True,
671-
}
672-
673-
return args
674-
675698
pip = module_extension(
676699
doc = """\
677700
This extension is used to make dependencies from pip available.
@@ -714,7 +737,56 @@ extension.
714737
""",
715738
),
716739
},
717-
**_extension_extra_args()
740+
)
741+
742+
pip_internal = module_extension(
743+
doc = """\
744+
This extension is used to make dependencies from pypi available.
745+
746+
For now this is intended to be used internally so that usage of the `pip`
747+
extension in `rules_python` does not affect the evaluations of the extension
748+
for the consumers.
749+
750+
pip.parse:
751+
To use, call `pip.parse()` and specify `hub_name` and your requirements file.
752+
Dependencies will be downloaded and made available in a repo named after the
753+
`hub_name` argument.
754+
755+
Each `pip.parse()` call configures a particular Python version. Multiple calls
756+
can be made to configure different Python versions, and will be grouped by
757+
the `hub_name` argument. This allows the same logical name, e.g. `@pypi//numpy`
758+
to automatically resolve to different, Python version-specific, libraries.
759+
760+
pip.whl_mods:
761+
This tag class is used to help create JSON files to describe modifications to
762+
the BUILD files for wheels.
763+
""",
764+
implementation = _pip_non_reproducible,
765+
tag_classes = {
766+
"override": _override_tag,
767+
"parse": tag_class(
768+
attrs = _pip_parse_ext_attrs(
769+
experimental_index_url = "https://pypi.org/simple",
770+
),
771+
doc = """\
772+
This tag class is used to create a pypi hub and all of the spokes that are part of that hub.
773+
This tag class reuses most of the pypi attributes that are found in
774+
@rules_python//python/pip_install:pip_repository.bzl.
775+
The exception is it does not use the arg 'repo_prefix'. We set the repository
776+
prefix for the user and the alias arg is always True in bzlmod.
777+
""",
778+
),
779+
"whl_mods": tag_class(
780+
attrs = _whl_mod_attrs(),
781+
doc = """\
782+
This tag class is used to create JSON file that are used when calling wheel_builder.py. These
783+
JSON files contain instructions on how to modify a wheel's project. Each of the attributes
784+
create different modifications based on the type of attribute. Previously to bzlmod these
785+
JSON files where referred to as annotations, and were renamed to whl_modifications in this
786+
extension.
787+
""",
788+
),
789+
},
718790
)
719791

720792
def _whl_mods_repo_impl(rctx):

0 commit comments

Comments
 (0)