Skip to content

Commit 411ac31

Browse files
committed
Make the pip extension reproducible if PyPI is not called
1 parent 47a78d7 commit 411ac31

File tree

3 files changed

+89
-20
lines changed

3 files changed

+89
-20
lines changed

CHANGELOG.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ A brief description of the categories of changes:
3535
Implemented the use of `pypi/stdlib-list` for standard library module verification.
3636
* (pip.parse): Do not ignore yanked packages when using `experimental_index_url`.
3737
This is to mimic what `uv` is doing. We will print a warning instead.
38-
* (pip.parse): Add references to all supported wheels when using `experimental_index_url`.
38+
* (pip.parse): Add references to all supported wheels when using `experimental_index_url`
39+
to allowing to correctly fetch the wheels for the right platform. See the
40+
updated docs on how to use the feature. This is work towards addressing
41+
[#735](https://github.com/bazelbuild/rules_python/issues/735) and
42+
[#260](https://github.com/bazelbuild/rules_python/issues/260).
3943

4044
### Fixed
4145
* (gazelle) Remove `visibility` from `NonEmptyAttr`.
@@ -53,6 +57,12 @@ A brief description of the categories of changes:
5357
replaced by whl_modifications.
5458
* (pip) Correctly select wheels when the python tag includes minor versions.
5559
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).
5666

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

MODULE.bazel

+5-4
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ 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",
61+
envsubst = ["PIP_INDEX_URL"],
62+
experimental_index_url = "${PIP_INDEX_URL:-https://pypi.org/simple}",
6263
hub_name = "rules_python_publish_deps",
6364
python_version = "3.11",
6465
requirements_by_platform = {
@@ -80,8 +81,8 @@ bazel_dep(name = "rules_go", version = "0.41.0", dev_dependency = True, repo_nam
8081
bazel_dep(name = "gazelle", version = "0.33.0", dev_dependency = True, repo_name = "bazel_gazelle")
8182

8283
dev_pip = use_extension(
83-
"//python/extensions:pip.bzl",
84-
"pip",
84+
"//python/private/bzlmod:pip.bzl",
85+
"pip_internal",
8586
dev_dependency = True,
8687
)
8788
dev_pip.parse(

python/private/bzlmod/pip.bzl

+73-15
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.
@@ -240,7 +241,7 @@ def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, s
240241
)
241242
whl_library_args.update({k: v for k, (v, default) in maybe_args_with_default.items() if v == default})
242243

243-
if pip_attr.experimental_index_url:
244+
if get_index_urls:
244245
# TODO @aignas 2024-05-26: move to a separate function
245246
found_something = False
246247
for requirement in requirements:
@@ -250,6 +251,8 @@ def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, s
250251
continue
251252

252253
found_something = True
254+
is_hub_reproducible = False
255+
253256
if pip_attr.netrc:
254257
whl_library_args["netrc"] = pip_attr.netrc
255258
if pip_attr.auth_patterns:
@@ -300,7 +303,7 @@ def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, s
300303
# packages they want to download, in that case there will be always
301304
# a requirement here, so we will not be in this code branch.
302305
continue
303-
elif pip_attr.experimental_index_url:
306+
elif get_index_urls:
304307
logger.warn(lambda: "falling back to pip for installing the right file for {}".format(requirement.requirement_line))
305308

306309
whl_library_args["requirement"] = requirement.requirement_line
@@ -318,6 +321,8 @@ def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, s
318321
),
319322
)
320323

324+
return is_hub_reproducible
325+
321326
def _pip_impl(module_ctx):
322327
"""Implementation of a class tag that creates the pip hub and corresponding pip spoke whl repositories.
323328
@@ -427,6 +432,7 @@ def _pip_impl(module_ctx):
427432
hub_group_map = {}
428433

429434
simpleapi_cache = {}
435+
is_extension_reproducible = True
430436

431437
for mod in module_ctx.modules:
432438
for pip_attr in mod.tags.parse:
@@ -463,7 +469,8 @@ def _pip_impl(module_ctx):
463469
else:
464470
pip_hub_map[pip_attr.hub_name].python_versions.append(pip_attr.python_version)
465471

466-
_create_whl_repos(module_ctx, pip_attr, hub_whl_map, whl_overrides, hub_group_map, simpleapi_cache)
472+
is_hub_reproducible = _create_whl_repos(module_ctx, pip_attr, hub_whl_map, whl_overrides, hub_group_map, simpleapi_cache)
473+
is_extension_reproducible = is_extension_reproducible and is_hub_reproducible
467474

468475
for hub_name, whl_map in hub_whl_map.items():
469476
pip_repository(
@@ -477,6 +484,20 @@ def _pip_impl(module_ctx):
477484
groups = hub_group_map.get(hub_name),
478485
)
479486

487+
if bazel_features.external_deps.extension_metadata_has_reproducible:
488+
return module_ctx.extension_metadata(reproducible = is_extension_reproducible)
489+
else:
490+
return None
491+
492+
def _pip_non_reproducible(module_ctx):
493+
_pip_impl(module_ctx)
494+
495+
if bazel_features.external_deps.extension_metadata_has_reproducible:
496+
# We allow for calling the PyPI index and that will go into the MODULE.bazel.lock file
497+
return module_ctx.extension_metadata(reproducible = False)
498+
else:
499+
return None
500+
480501
def _pip_parse_ext_attrs():
481502
attrs = dict({
482503
"experimental_extra_index_urls": attr.string_list(
@@ -492,6 +513,7 @@ This is equivalent to `--extra-index-urls` `pip` option.
492513
default = [],
493514
),
494515
"experimental_index_url": attr.string(
516+
default = "https://pypi.org/simple",
495517
doc = """\
496518
The index URL to use for downloading wheels using bazel downloader. This value is going
497519
to be subject to `envsubst` substitutions if necessary.
@@ -676,17 +698,6 @@ Apply any overrides (e.g. patches) to a given Python distribution defined by
676698
other tags in this extension.""",
677699
)
678700

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

735793
def _whl_mods_repo_impl(rctx):

0 commit comments

Comments
 (0)