From 3d98aeea9c70b2a7336d9ea8f7397b5c6d07d405 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sat, 5 Apr 2025 22:17:28 +0900 Subject: [PATCH 001/156] fix(toolchains): correctly order the toolchains (#2735) Since toolchain matching is done by matching the first target that matches target settings, the `minor_mapping` config setting is special, because e.g. all `3.11.X` toolchains match the `python_version = "3.11"` setting. This just reshuffles the list so that we have toolchains that are in the `minor_mapping` before the rest. At the same time remove the workaround from the `lock.bzl` where the bug was initially discovered. Fixes #2685 --- CHANGELOG.md | 3 + python/private/python.bzl | 17 +- python/uv/private/BUILD.bazel | 2 - python/uv/private/lock.bzl | 31 +-- .../transition/multi_version_tests.bzl | 3 +- tests/python/python_tests.bzl | 52 +++++ tests/toolchains/transitions/BUILD.bazel | 5 + .../transitions/transitions_tests.bzl | 182 ++++++++++++++++++ 8 files changed, 269 insertions(+), 26 deletions(-) create mode 100644 tests/toolchains/transitions/BUILD.bazel create mode 100644 tests/toolchains/transitions/transitions_tests.bzl diff --git a/CHANGELOG.md b/CHANGELOG.md index bbcf2561c8..b11270cb25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,9 @@ Unreleased changes template. * (toolchains) Do not try to run `chmod` when downloading non-windows hermetic toolchain repositories on Windows. Fixes [#2660](https://github.com/bazel-contrib/rules_python/issues/2660). +* (toolchains) The toolchain matching is has been fixed when writing + transitions transitioning on the `python_version` flag. + Fixes [#2685](https://github.com/bazel-contrib/rules_python/issues/2685). {#v0-0-0-added} ### Added diff --git a/python/private/python.bzl b/python/private/python.bzl index 44eb09f766..296fb0ab7d 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -243,10 +243,25 @@ def parse_modules(*, module_ctx, _fail = fail): if len(toolchains) > _MAX_NUM_TOOLCHAINS: fail("more than {} python versions are not supported".format(_MAX_NUM_TOOLCHAINS)) + # sort the toolchains so that the toolchain versions that are in the + # `minor_mapping` are coming first. This ensures that `python_version = + # "3.X"` transitions work as expected. + minor_version_toolchains = [] + other_toolchains = [] + minor_mapping = list(config.minor_mapping.values()) + for t in toolchains: + # FIXME @aignas 2025-04-04: How can we unit test that this ordering is + # consistent with what would actually work? + if config.minor_mapping.get(t.python_version, t.python_version) in minor_mapping: + minor_version_toolchains.append(t) + else: + other_toolchains.append(t) + toolchains = minor_version_toolchains + other_toolchains + return struct( config = config, debug_info = debug_info, - default_python_version = toolchains[-1].python_version, + default_python_version = default_toolchain.python_version, toolchains = [ struct( python_version = t.python_version, diff --git a/python/uv/private/BUILD.bazel b/python/uv/private/BUILD.bazel index d17ca39490..587ad9a0f9 100644 --- a/python/uv/private/BUILD.bazel +++ b/python/uv/private/BUILD.bazel @@ -43,10 +43,8 @@ bzl_library( ":toolchain_types_bzl", "//python:py_binary_bzl", "//python/private:bzlmod_enabled_bzl", - "//python/private:full_version_bzl", "//python/private:toolchain_types_bzl", "@bazel_skylib//lib:shell", - "@pythons_hub//:versions_bzl", ], ) diff --git a/python/uv/private/lock.bzl b/python/uv/private/lock.bzl index 69d277d653..45a3819ee6 100644 --- a/python/uv/private/lock.bzl +++ b/python/uv/private/lock.bzl @@ -16,10 +16,8 @@ """ load("@bazel_skylib//lib:shell.bzl", "shell") -load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING") load("//python:py_binary.bzl", "py_binary") load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility -load("//python/private:full_version.bzl", "full_version") load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE") # buildifier: disable=bzl-visibility load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE") @@ -75,15 +73,15 @@ def _args(ctx): def _lock_impl(ctx): srcs = ctx.files.srcs - python_version = full_version( - version = ctx.attr.python_version or DEFAULT_PYTHON_VERSION, - minor_mapping = MINOR_MAPPING, - ) - output = ctx.actions.declare_file("{}.{}.out".format( - ctx.label.name, - python_version.replace(".", "_"), - )) + fname = "{}.out".format(ctx.label.name) + python_version = ctx.attr.python_version + if python_version: + fname = "{}.{}.out".format( + ctx.label.name, + python_version.replace(".", "_"), + ) + output = ctx.actions.declare_file(fname) toolchain_info = ctx.toolchains[UV_TOOLCHAIN_TYPE] uv = toolchain_info.uv_toolchain_info.uv[DefaultInfo].files_to_run.executable @@ -166,15 +164,7 @@ def _transition_impl(input_settings, attr): _PYTHON_VERSION_FLAG: input_settings[_PYTHON_VERSION_FLAG], } if attr.python_version: - # FIXME @aignas 2025-03-20: using `full_version` is a workaround for a bug in - # how we order toolchains in bazel. If I set the `python_version` flag - # to `3.12`, I would expect the latest version to be selected, i.e. the - # one that is in MINOR_MAPPING, but it seems that 3.12.0 is selected, - # because of how the targets are ordered. - settings[_PYTHON_VERSION_FLAG] = full_version( - version = attr.python_version, - minor_mapping = MINOR_MAPPING, - ) + settings[_PYTHON_VERSION_FLAG] = attr.python_version return settings _python_version_transition = transition( @@ -436,9 +426,6 @@ def lock( if not BZLMOD_ENABLED: kwargs["target_compatible_with"] = ["@platforms//:incompatible"] - # FIXME @aignas 2025-03-17: should we have one more target that transitions - # the python_version to ensure that if somebody calls `bazel build - # :requirements` that it is locked with the right `python_version`? _lock( name = name, args = args, diff --git a/tests/config_settings/transition/multi_version_tests.bzl b/tests/config_settings/transition/multi_version_tests.bzl index aca341a295..93f6efd728 100644 --- a/tests/config_settings/transition/multi_version_tests.bzl +++ b/tests/config_settings/transition/multi_version_tests.bzl @@ -13,6 +13,7 @@ # limitations under the License. """Tests for py_test.""" +load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION") load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") load("@rules_testing//lib:util.bzl", "TestingAspectInfo", rt_util = "util") @@ -29,7 +30,7 @@ load("//tests/support:support.bzl", "CC_TOOLCHAIN") # If the toolchain is not resolved then you will have a weird message telling # you that your transition target does not have a PyRuntime provider, which is # caused by there not being a toolchain detected for the target. -_PYTHON_VERSION = "3.11" +_PYTHON_VERSION = DEFAULT_PYTHON_VERSION _tests = [] diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl index 1679794e15..97c47b57db 100644 --- a/tests/python/python_tests.bzl +++ b/tests/python/python_tests.bzl @@ -284,6 +284,58 @@ def _test_default_non_rules_python_ignore_root_user_error_non_root_module(env): _tests.append(_test_default_non_rules_python_ignore_root_user_error_non_root_module) +def _test_toolchain_ordering(env): + py = parse_modules( + module_ctx = _mock_mctx( + _mod( + name = "my_module", + toolchain = [ + _toolchain("3.10"), + _toolchain("3.10.15"), + _toolchain("3.10.16"), + _toolchain("3.10.11"), + _toolchain("3.11.1"), + _toolchain("3.11.10"), + _toolchain("3.11.11", is_default = True), + ], + ), + _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), + ), + ) + got_versions = [ + t.python_version + for t in py.toolchains + ] + + env.expect.that_str(py.default_python_version).equals("3.11.11") + env.expect.that_dict(py.config.minor_mapping).contains_exactly({ + "3.10": "3.10.16", + "3.11": "3.11.11", + "3.12": "3.12.9", + "3.13": "3.13.2", + "3.8": "3.8.20", + "3.9": "3.9.21", + }) + env.expect.that_collection(got_versions).contains_exactly([ + # First the full-version toolchains that are in minor_mapping + # so that they get matched first if only the `python_version` is in MINOR_MAPPING + # + # The default version is always set in the `python_version` flag, so know, that + # the default match will be somewhere in the first bunch. + "3.10", + "3.10.16", + "3.11", + "3.11.11", + # Next, the rest, where we will match things based on the `python_version` being + # the same + "3.10.15", + "3.10.11", + "3.11.1", + "3.11.10", + ]).in_order() + +_tests.append(_test_toolchain_ordering) + def _test_default_from_defaults(env): py = parse_modules( module_ctx = _mock_mctx( diff --git a/tests/toolchains/transitions/BUILD.bazel b/tests/toolchains/transitions/BUILD.bazel new file mode 100644 index 0000000000..a7bef8c0e5 --- /dev/null +++ b/tests/toolchains/transitions/BUILD.bazel @@ -0,0 +1,5 @@ +load(":transitions_tests.bzl", "transitions_test_suite") + +transitions_test_suite( + name = "transitions_tests", +) diff --git a/tests/toolchains/transitions/transitions_tests.bzl b/tests/toolchains/transitions/transitions_tests.bzl new file mode 100644 index 0000000000..bddd1745f0 --- /dev/null +++ b/tests/toolchains/transitions/transitions_tests.bzl @@ -0,0 +1,182 @@ +# Copyright 2022 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("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING") +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:util.bzl", rt_util = "util") +load("//python:versions.bzl", "TOOL_VERSIONS") +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility +load("//python/private:full_version.bzl", "full_version") # buildifier: disable=bzl-visibility +load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE") # buildifier: disable=bzl-visibility +load("//tests/support:support.bzl", "PYTHON_VERSION") + +_analysis_tests = [] + +def _transition_impl(input_settings, attr): + """Transition based on python_version flag. + + This is a simple transition impl that a user of rules_python may implement + for their own rule. + """ + settings = { + PYTHON_VERSION: input_settings[PYTHON_VERSION], + } + if attr.python_version: + settings[PYTHON_VERSION] = attr.python_version + return settings + +_python_version_transition = transition( + implementation = _transition_impl, + inputs = [PYTHON_VERSION], + outputs = [PYTHON_VERSION], +) + +TestInfo = provider( + doc = "A simple test provider to forward the values for the assertion.", + fields = {"got": "", "want": ""}, +) + +def _impl(ctx): + if ctx.attr.skip: + return [TestInfo(got = "", want = "")] + + exec_tools = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE].exec_tools + got_version = exec_tools.exec_interpreter[platform_common.ToolchainInfo].py3_runtime.interpreter_version_info + + return [ + TestInfo( + got = "{}.{}.{}".format( + got_version.major, + got_version.minor, + got_version.micro, + ), + want = ctx.attr.want_version, + ), + ] + +_simple_transition = rule( + implementation = _impl, + attrs = { + "python_version": attr.string( + doc = "The input python version which we transition on.", + ), + "skip": attr.bool( + doc = "Whether to skip the test", + ), + "want_version": attr.string( + doc = "The python version that we actually expect to receive.", + ), + "_allowlist_function_transition": attr.label( + default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + ), + }, + toolchains = [ + config_common.toolchain_type( + EXEC_TOOLS_TOOLCHAIN_TYPE, + mandatory = False, + ), + ], + cfg = _python_version_transition, +) + +def _test_transitions(*, name, tests, skip = False): + """A reusable rule so that we can split the tests.""" + targets = {} + for test_name, (input_version, want_version) in tests.items(): + target_name = "{}_{}".format(name, test_name) + targets["python_" + test_name] = target_name + rt_util.helper_target( + _simple_transition, + name = target_name, + python_version = input_version, + want_version = want_version, + skip = skip, + ) + + analysis_test( + name = name, + impl = _test_transition_impl, + targets = targets, + ) + +def _test_transition_impl(env, targets): + # Check that the forwarded version from the PyRuntimeInfo is correct + for target in dir(targets): + if not target.startswith("python"): + # Skip other attributes that might be not the ones we set (e.g. to_json, to_proto). + continue + + test_info = env.expect.that_target(getattr(targets, target)).provider( + TestInfo, + factory = lambda v, meta: v, + ) + env.expect.that_str(test_info.got).equals(test_info.want) + +def _test_full_version(name): + """Check that python_version transitions work. + + Expectation is to get the same full version that we input. + """ + _test_transitions( + name = name, + tests = { + v.replace(".", "_"): (v, v) + for v in TOOL_VERSIONS + }, + ) + +_analysis_tests.append(_test_full_version) + +def _test_minor_versions(name): + """Ensure that MINOR_MAPPING versions are correctly selected.""" + _test_transitions( + name = name, + skip = not BZLMOD_ENABLED, + tests = { + minor.replace(".", "_"): (minor, full) + for minor, full in MINOR_MAPPING.items() + }, + ) + +_analysis_tests.append(_test_minor_versions) + +def _test_default(name): + """Check the default version. + + Lastly, if we don't provide any version to the transition, we should + get the default version + """ + default_version = full_version( + version = DEFAULT_PYTHON_VERSION, + minor_mapping = MINOR_MAPPING, + ) if DEFAULT_PYTHON_VERSION else "" + + _test_transitions( + name = name, + skip = not BZLMOD_ENABLED, + tests = { + "default": (None, default_version), + }, + ) + +_analysis_tests.append(_test_default) + +def transitions_test_suite(name): + test_suite( + name = name, + tests = _analysis_tests, + ) From f685fe9a192dcdc8b65376821d9f25b990aa54fa Mon Sep 17 00:00:00 2001 From: Matt Mackay Date: Sat, 5 Apr 2025 09:43:16 -0400 Subject: [PATCH 002/156] fix: allow warn logging to be disabled via RULES_PYTHON_REPO_DEBUG_VERBOSITY (#2737) Allows the logging level to be set to `FAIL`, removing `WARN` logging. --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- CHANGELOG.md | 1 + docs/environment-variables.md | 1 + python/private/repo_utils.bzl | 1 + 3 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b11270cb25..33acd38706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,7 @@ Unreleased changes template. * (toolchains) Do not try to run `chmod` when downloading non-windows hermetic toolchain repositories on Windows. Fixes [#2660](https://github.com/bazel-contrib/rules_python/issues/2660). +* (logging) Allow repo rule logging level to be set to `FAIL` via the `RULES_PYTHON_REPO_DEBUG_VERBOSITY` environment variable. * (toolchains) The toolchain matching is has been fixed when writing transitions transitioning on the `python_version` flag. Fixes [#2685](https://github.com/bazel-contrib/rules_python/issues/2685). diff --git a/docs/environment-variables.md b/docs/environment-variables.md index d8735cb2d5..9500fa8295 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -101,6 +101,7 @@ doing. This is mostly useful for development to debug errors. Determines the verbosity of logging output for repo rules. Valid values: * `DEBUG` +* `FAIL` * `INFO` * `TRACE` ::: diff --git a/python/private/repo_utils.bzl b/python/private/repo_utils.bzl index d9ad2449f1..73883a9244 100644 --- a/python/private/repo_utils.bzl +++ b/python/private/repo_utils.bzl @@ -56,6 +56,7 @@ def _logger(mrctx, name = None): verbosity = { "DEBUG": 2, + "FAIL": -1, "INFO": 1, "TRACE": 3, }.get(verbosity_level, 0) From f65b2ac7b20354cf18400cb6512548405a88639c Mon Sep 17 00:00:00 2001 From: Matt Mackay Date: Sat, 5 Apr 2025 11:51:41 -0400 Subject: [PATCH 003/156] fix: run check on interpreter in isolated mode (#2738) Runs the check on the interpreter in the toolchain repo in isolated mode via `-I`. This ensures it's not influenced by userland environment variables, such as `PYTHONPATH` which will cause issues if it allows this invocation to use into another interpreter versions site-packages. --- CHANGELOG.md | 1 + python/private/toolchains_repo.bzl | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33acd38706..ac41e81f6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,7 @@ Unreleased changes template. * (toolchains) The toolchain matching is has been fixed when writing transitions transitioning on the `python_version` flag. Fixes [#2685](https://github.com/bazel-contrib/rules_python/issues/2685). +* (toolchains) Run the check on the Python interpreter in isolated mode, to ensure it's not affected by userland environment variables, such as `PYTHONPATH`. {#v0-0-0-added} ### Added diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl index 4e4a5de501..23c4643c0a 100644 --- a/python/private/toolchains_repo.bzl +++ b/python/private/toolchains_repo.bzl @@ -275,7 +275,14 @@ assert want_python == got_python, \ repo_utils.execute_checked( rctx, op = "CheckHostInterpreter", - arguments = [rctx.path(python_binary), python_tester], + arguments = [ + rctx.path(python_binary), + # Run the interpreter in isolated mode, this options implies -E, -P and -s. + # This ensures that environment variables are ignored that are set in userspace, such as PYTHONPATH, + # which may interfere with this invocation. + "-I", + python_tester, + ], ) if not rctx.delete(python_tester): fail("Failed to delete the python tester") From 537fc4b9e461639144083a1542e10f7589c5251f Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 6 Apr 2025 00:51:57 +0900 Subject: [PATCH 004/156] fix(pypi): correctly fallback to pip for git direct URLs (#2732) Whilst integrating #2695 I introduced a regression and here I add a test for that and fix it. The code that was getting the filename from the URL was too eager and would break if there was a git ref as noted in the test. Before this commit and #2695 the code was not handling all of the cases that are tested now either, so I think now we are in a good place. I am not sure how we should handle the `git_repository` URLs. Maybe having `http_archive` and `git_repository` usage would be nice, but I am not sure how we can introduce it at the moment. Work towards #2363 --- python/private/pypi/parse_requirements.bzl | 6 +++ tests/pypi/extension/extension_tests.bzl | 50 +++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index 3280ce8df1..d2014a7eb9 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -297,6 +297,12 @@ def _add_dists(*, requirement, index_urls, logger = None): if requirement.srcs.url: url = requirement.srcs.url _, _, filename = url.rpartition("/") + if "." not in filename: + # detected filename has no extension, it might be an sdist ref + # TODO @aignas 2025-04-03: should be handled if the following is fixed: + # https://github.com/bazel-contrib/rules_python/issues/2363 + return [], None + direct_url_dist = struct( url = url, filename = filename, diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index 3a91c7b108..ab7a1358ad 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -662,6 +662,8 @@ some_pkg==0.0.1 @ example-direct.org/some_pkg-0.0.1-py3-none-any.whl \ direct_without_sha==0.0.1 @ example-direct.org/direct_without_sha-0.0.1-py3-none-any.whl some_other_pkg==0.0.1 pip_fallback==0.0.1 +direct_sdist_without_sha @ some-archive/any-name.tar.gz +git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef """, }[x], ), @@ -672,10 +674,28 @@ pip_fallback==0.0.1 ) pypi.is_reproducible().equals(False) - pypi.exposed_packages().contains_exactly({"pypi": ["direct_without_sha", "pip_fallback", "simple", "some_other_pkg", "some_pkg"]}) + pypi.exposed_packages().contains_exactly({"pypi": [ + "direct_sdist_without_sha", + "direct_without_sha", + "git_dep", + "pip_fallback", + "simple", + "some_other_pkg", + "some_pkg", + ]}) pypi.hub_group_map().contains_exactly({"pypi": {}}) pypi.hub_whl_map().contains_exactly({ "pypi": { + "direct_sdist_without_sha": { + "pypi_315_any_name": [ + struct( + config_setting = None, + filename = "any-name.tar.gz", + target_platforms = None, + version = "3.15", + ), + ], + }, "direct_without_sha": { "pypi_315_direct_without_sha_0_0_1_py3_none_any": [ struct( @@ -686,6 +706,16 @@ pip_fallback==0.0.1 ), ], }, + "git_dep": { + "pypi_315_git_dep": [ + struct( + config_setting = None, + filename = None, + target_platforms = None, + version = "3.15", + ), + ], + }, "pip_fallback": { "pypi_315_pip_fallback": [ struct( @@ -737,6 +767,17 @@ pip_fallback==0.0.1 }, }) pypi.whl_libraries().contains_exactly({ + "pypi_315_any_name": { + "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"], + "extra_pip_args": ["--extra-args-for-sdist-building"], + "filename": "any-name.tar.gz", + "python_interpreter_target": "unit_test_interpreter_target", + "repo": "pypi_315", + "requirement": "direct_sdist_without_sha @ some-archive/any-name.tar.gz", + "sha256": "", + "urls": ["some-archive/any-name.tar.gz"], + }, "pypi_315_direct_without_sha_0_0_1_py3_none_any": { "dep_template": "@pypi//{name}:{target}", "experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"], @@ -747,6 +788,13 @@ pip_fallback==0.0.1 "sha256": "", "urls": ["example-direct.org/direct_without_sha-0.0.1-py3-none-any.whl"], }, + "pypi_315_git_dep": { + "dep_template": "@pypi//{name}:{target}", + "extra_pip_args": ["--extra-args-for-sdist-building"], + "python_interpreter_target": "unit_test_interpreter_target", + "repo": "pypi_315", + "requirement": "git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef", + }, "pypi_315_pip_fallback": { "dep_template": "@pypi//{name}:{target}", "extra_pip_args": ["--extra-args-for-sdist-building"], From 69a99200fa38096675bd37ba2856eb3077cd3b86 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Sat, 5 Apr 2025 09:02:59 -0700 Subject: [PATCH 005/156] fix: support gazelle generation_mode:update_only (#2708) This just fixes a crash when `generation_mode: update_only` causes `GenerateRules` to not be invoked for 100% of directories. Fix #2707 --- gazelle/pythonconfig/pythonconfig.go | 25 +++++++++++------- gazelle/pythonconfig/pythonconfig_test.go | 32 +++++++++++++++++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/gazelle/pythonconfig/pythonconfig.go b/gazelle/pythonconfig/pythonconfig.go index 2183ec60a3..23c0cfd572 100644 --- a/gazelle/pythonconfig/pythonconfig.go +++ b/gazelle/pythonconfig/pythonconfig.go @@ -22,8 +22,8 @@ import ( "github.com/emirpasic/gods/lists/singlylinkedlist" - "github.com/bazelbuild/bazel-gazelle/label" "github.com/bazel-contrib/rules_python/gazelle/manifest" + "github.com/bazelbuild/bazel-gazelle/label" ) // Directives @@ -125,21 +125,28 @@ const ( // defaultIgnoreFiles is the list of default values used in the // python_ignore_files option. -var defaultIgnoreFiles = map[string]struct{}{ -} +var defaultIgnoreFiles = map[string]struct{}{} // Configs is an extension of map[string]*Config. It provides finding methods // on top of the mapping. type Configs map[string]*Config // ParentForPackage returns the parent Config for the given Bazel package. -func (c *Configs) ParentForPackage(pkg string) *Config { - dir := path.Dir(pkg) - if dir == "." { - dir = "" +func (c Configs) ParentForPackage(pkg string) *Config { + for { + dir := path.Dir(pkg) + if dir == "." { + dir = "" + } + parent := (map[string]*Config)(c)[dir] + if parent != nil { + return parent + } + if dir == "" { + return nil + } + pkg = dir } - parent := (map[string]*Config)(*c)[dir] - return parent } // Config represents a config extension for a specific Bazel package. diff --git a/gazelle/pythonconfig/pythonconfig_test.go b/gazelle/pythonconfig/pythonconfig_test.go index 7cdb9af1d1..fe21ce236e 100644 --- a/gazelle/pythonconfig/pythonconfig_test.go +++ b/gazelle/pythonconfig/pythonconfig_test.go @@ -248,3 +248,35 @@ func TestFormatThirdPartyDependency(t *testing.T) { }) } } + +func TestConfigsMap(t *testing.T) { + t.Run("only root", func(t *testing.T) { + configs := Configs{"": New("root/dir", "")} + + if configs.ParentForPackage("") == nil { + t.Fatal("expected non-nil for root config") + } + + if configs.ParentForPackage("a/b/c") != configs[""] { + t.Fatal("expected root for subpackage") + } + }) + + t.Run("sparse child configs", func(t *testing.T) { + configs := Configs{"": New("root/dir", "")} + configs["a"] = configs[""].NewChild() + configs["a/b/c"] = configs["a"].NewChild() + + if configs.ParentForPackage("a/b/c/d") != configs["a/b/c"] { + t.Fatal("child should match direct parent") + } + + if configs.ParentForPackage("a/b/c/d/e") != configs["a/b/c"] { + t.Fatal("grandchild should match first parant") + } + + if configs.ParentForPackage("other/root/path") != configs[""] { + t.Fatal("non-configured subpackage should match root") + } + }) +} From 2bc357787e8d6e76fd2f58e401cf3062bcf4f415 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 6 Apr 2025 01:27:12 +0900 Subject: [PATCH 006/156] fix(pypi): mark the extension reproducible (#2730) This will remove the merge conflicts and improve the usability when the `MODULE.bazel.lock` is used together with `rules_python`. This means that the lock file will not be used to read the `URL` and `sha256` values for the Python sources when the `experimental_index_url` is used, but the idea is that that information will be kept in repo cache. Fixes #2434 Created #2731 to leverage the bazel feature to write immutable facts to the lock file once it becomes available. --- CHANGELOG.md | 3 +++ python/private/pypi/extension.bzl | 6 +----- tests/pypi/extension/extension_tests.bzl | 7 ------- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac41e81f6b..69e9330f64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,9 @@ Unreleased changes template. * (toolchains) Remove all but `3.8.20` versions of the Python `3.8` interpreter who has reached EOL. If users still need other versions of the `3.8` interpreter, please supply the URLs manually {bzl:ob}`python.toolchain` or {bzl:obj}`python_register_toolchains` calls. +* (pypi) The PyPI extension will no longer write the lock file entries as the + extension has been marked reproducible. + Fixes [#2434](https://github.com/bazel-contrib/rules_python/issues/2434). [20250317]: https://github.com/astral-sh/python-build-standalone/releases/tag/20250317 diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index f782e69a45..8fce47656b 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -419,8 +419,6 @@ You cannot use both the additive_build_content and additive_build_content_file a extra_aliases = {} whl_libraries = {} - is_reproducible = True - for mod in module_ctx.modules: for pip_attr in mod.tags.parse: hub_name = pip_attr.hub_name @@ -458,7 +456,6 @@ You cannot use both the additive_build_content and additive_build_content_file a get_index_urls = None if pip_attr.experimental_index_url: - is_reproducible = False skip_sources = [ normalize_name(s) for s in pip_attr.simpleapi_skip @@ -543,7 +540,6 @@ You cannot use both the additive_build_content and additive_build_content_file a k: dict(sorted(args.items())) for k, args in sorted(whl_libraries.items()) }, - is_reproducible = is_reproducible, ) def _pip_impl(module_ctx): @@ -640,7 +636,7 @@ def _pip_impl(module_ctx): # 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 = mods.is_reproducible) + return module_ctx.extension_metadata(reproducible = True) else: return None diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index ab7a1358ad..1652e76156 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -64,7 +64,6 @@ def _parse_modules(env, **kwargs): return env.expect.that_struct( parse_modules(**kwargs), attrs = dict( - is_reproducible = subjects.bool, exposed_packages = subjects.dict, hub_group_map = subjects.dict, hub_whl_map = subjects.dict, @@ -160,7 +159,6 @@ def _test_simple(env): }, ) - pypi.is_reproducible().equals(True) pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) pypi.hub_group_map().contains_exactly({"pypi": {}}) pypi.hub_whl_map().contains_exactly({"pypi": { @@ -209,7 +207,6 @@ def _test_simple_multiple_requirements(env): }, ) - pypi.is_reproducible().equals(True) pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) pypi.hub_group_map().contains_exactly({"pypi": {}}) pypi.hub_whl_map().contains_exactly({"pypi": { @@ -278,7 +275,6 @@ torch==2.4.1 ; platform_machine != 'x86_64' \ }, ) - pypi.is_reproducible().equals(True) pypi.exposed_packages().contains_exactly({"pypi": ["torch"]}) pypi.hub_group_map().contains_exactly({"pypi": {}}) pypi.hub_whl_map().contains_exactly({"pypi": { @@ -404,7 +400,6 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ simpleapi_download = mocksimpleapi_download, ) - pypi.is_reproducible().equals(False) pypi.exposed_packages().contains_exactly({"pypi": ["torch"]}) pypi.hub_group_map().contains_exactly({"pypi": {}}) pypi.hub_whl_map().contains_exactly({"pypi": { @@ -535,7 +530,6 @@ simple==0.0.3 \ }, ) - pypi.is_reproducible().equals(True) pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) pypi.hub_group_map().contains_exactly({"pypi": {}}) pypi.hub_whl_map().contains_exactly({"pypi": { @@ -673,7 +667,6 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef simpleapi_download = mocksimpleapi_download, ) - pypi.is_reproducible().equals(False) pypi.exposed_packages().contains_exactly({"pypi": [ "direct_sdist_without_sha", "direct_without_sha", From 01968255660aa99041c0c8989a0d68c01aa2978e Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 5 Apr 2025 09:37:21 -0700 Subject: [PATCH 007/156] feat: allow populating binary's venv site-packages with symlinks (#2617) This implements functionality to allow libraries to populate the site-packages directory of downstream binaries. The basic implementation is: * Libraries provide tuples of `(runfile path, site packages path)` in the `PyInfo.site_packages_symlinks` field. * Binaries create symlinks (using declare_symlink) in their site-packages directory pointing to the runfiles paths libraries provide. The design was chosen because of the following properties: * The site-packages directory is relocatable * Populating site packages is cheap ( `O(number 3p dependencies)` ) * Dependencies are only created once in the runfiles, no matter how many how many binaries there that use them. This minimizes disk usage, file counts, inodes, etc. The `site_packages_symlinks` field is a depset with topological ordering. Using topological ordering allows dependencies closer to the binary to have precedence, which gives some basic control over what entries are used. Additionally, the runfiles path to link to can be None/empty, in which case, the directory in site-packages won't be created. This allows binaries to prevent creation of directories that might e.g. conflict. For now, this functionality is disabled by default. The flag `--venvs_site_packages=yes` can be set to allow using it, which is automatically enable it for pypi generated targets. When enabled, it does basic detection of implicit namespace directories, which allows multiple distributions to "install" into the the same site-packages directory. Though this functionality is primarily useful for dependencies from pypi (e.g. via pip.parse), it is not yet activated for those targets, for two main reasons: 1. The wheel extraction code creates pkgutil-style `__init__.py` shims during the repo-phase. The build phase can't distinguish these artifical rules_python generated shims from actual `__init__.py` files, which breaks the implicit namespace detection logic. 2. A flag guard is needed before changing the behavior. Even though how 3p libraries are added to sys.path is an implementation detail, the behavior has been there for many years, so an escape hatch should be added. Work towards https://github.com/bazelbuild/rules_python/issues/2156 --- .bazelrc | 4 +- CHANGELOG.md | 4 + MODULE.bazel | 6 + docs/BUILD.bazel | 1 + docs/_includes/experimental_api.md | 5 + .../python/config_settings/index.md | 17 ++ internal_dev_deps.bzl | 6 + python/BUILD.bazel | 3 + python/config_settings/BUILD.bazel | 8 + python/features.bzl | 43 ++++- python/private/attributes.bzl | 11 ++ python/private/builders.bzl | 13 +- python/private/common.bzl | 17 +- python/private/enum.bzl | 20 +++ python/private/flags.bzl | 38 ++--- python/private/py_executable.bzl | 76 ++++++++- python/private/py_info.bzl | 35 +++- python/private/py_library.bzl | 161 +++++++++++++++++- python/private/pypi/whl_library_targets.bzl | 1 + tests/modules/other/BUILD.bazel | 0 tests/modules/other/MODULE.bazel | 3 + tests/modules/other/nspkg_delta/BUILD.bazel | 10 ++ .../nspkg/subnspkg/delta/__init__.py | 1 + tests/modules/other/nspkg_gamma/BUILD.bazel | 10 ++ .../nspkg/subnspkg/gamma/__init__.py | 1 + .../whl_library_targets_tests.bzl | 2 + tests/support/sh_py_run_test.bzl | 4 + tests/venv_site_packages_libs/BUILD.bazel | 17 ++ tests/venv_site_packages_libs/bin.py | 32 ++++ .../nspkg_alpha/BUILD.bazel | 10 ++ .../nspkg/subnspkg/alpha/__init__.py | 1 + .../nspkg_beta/BUILD.bazel | 10 ++ .../nspkg/subnspkg/beta/__init__.py | 1 + .../venv_site_packages_pypi_test.py | 36 ++++ 34 files changed, 574 insertions(+), 33 deletions(-) create mode 100644 docs/_includes/experimental_api.md create mode 100644 tests/modules/other/BUILD.bazel create mode 100644 tests/modules/other/MODULE.bazel create mode 100644 tests/modules/other/nspkg_delta/BUILD.bazel create mode 100644 tests/modules/other/nspkg_delta/site-packages/nspkg/subnspkg/delta/__init__.py create mode 100644 tests/modules/other/nspkg_gamma/BUILD.bazel create mode 100644 tests/modules/other/nspkg_gamma/site-packages/nspkg/subnspkg/gamma/__init__.py create mode 100644 tests/venv_site_packages_libs/BUILD.bazel create mode 100644 tests/venv_site_packages_libs/bin.py create mode 100644 tests/venv_site_packages_libs/nspkg_alpha/BUILD.bazel create mode 100644 tests/venv_site_packages_libs/nspkg_alpha/site-packages/nspkg/subnspkg/alpha/__init__.py create mode 100644 tests/venv_site_packages_libs/nspkg_beta/BUILD.bazel create mode 100644 tests/venv_site_packages_libs/nspkg_beta/site-packages/nspkg/subnspkg/beta/__init__.py create mode 100644 tests/venv_site_packages_libs/venv_site_packages_pypi_test.py diff --git a/.bazelrc b/.bazelrc index ada5c5a0a7..4e6f2fa187 100644 --- a/.bazelrc +++ b/.bazelrc @@ -4,8 +4,8 @@ # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it) # To update these lines, execute # `bazel run @rules_bazel_integration_test//tools:update_deleted_packages` -build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered -query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered +build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma +query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma test --test_output=errors diff --git a/CHANGELOG.md b/CHANGELOG.md index 69e9330f64..818773e589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,6 +107,10 @@ Unreleased changes template. please check the {obj}`uv.configure` tag class. * Add support for riscv64 linux platform. * (toolchains) Add python 3.13.2 and 3.12.9 toolchains +* (providers) (experimental) {obj}`PyInfo.site_packages_symlinks` field added to + allow specifying links to create within the venv site packages (only + applicable with {obj}`--bootstrap_impl=script`) + ([#2156](https://github.com/bazelbuild/rules_python/issues/2156)). {#v0-0-0-removed} ### Removed diff --git a/MODULE.bazel b/MODULE.bazel index e4e45af7f0..c649896344 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -85,6 +85,7 @@ bazel_dep(name = "rules_shell", version = "0.3.0", dev_dependency = True) bazel_dep(name = "rules_multirun", version = "0.9.0", dev_dependency = True) bazel_dep(name = "bazel_ci_rules", version = "1.0.0", dev_dependency = True) bazel_dep(name = "rules_pkg", version = "1.0.1", dev_dependency = True) +bazel_dep(name = "other", version = "0", dev_dependency = True) # Extra gazelle plugin deps so that WORKSPACE.bzlmod can continue including it for e2e tests. # We use `WORKSPACE.bzlmod` because it is impossible to have dev-only local overrides. @@ -106,6 +107,11 @@ local_path_override( path = "gazelle", ) +local_path_override( + module_name = "other", + path = "tests/modules/other", +) + dev_python = use_extension( "//python/extensions:python.bzl", "python", diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index bebecd18b2..29eac6e714 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -87,6 +87,7 @@ sphinx_stardocs( name = "bzl_api_docs", srcs = [ "//python:defs_bzl", + "//python:features_bzl", "//python:packaging_bzl", "//python:pip_bzl", "//python:py_binary_bzl", diff --git a/docs/_includes/experimental_api.md b/docs/_includes/experimental_api.md new file mode 100644 index 0000000000..45473a7cbf --- /dev/null +++ b/docs/_includes/experimental_api.md @@ -0,0 +1,5 @@ +:::{warning} + +**Experimental API.** This API is still under development and may change or be +removed without notice. +::: diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md index 79c7d0c109..340335d9b1 100644 --- a/docs/api/rules_python/python/config_settings/index.md +++ b/docs/api/rules_python/python/config_settings/index.md @@ -213,6 +213,23 @@ Values: :::: +:::: + +:::{flag} venvs_site_packages + +Determines if libraries use a site-packages layout for their files. + +Note this flag only affects PyPI dependencies of `--bootstrap_impl=script` binaries + +:::{include} /_includes/experimental_api.md +::: + + +Values: +* `no` (default): Make libraries importable by adding to `sys.path` +* `yes`: Make libraries importable by creating paths in a binary's site-packages directory. +:::: + ::::{bzl:flag} bootstrap_impl Determine how programs implement their startup process. diff --git a/internal_dev_deps.bzl b/internal_dev_deps.bzl index cd33475f43..87690be1ad 100644 --- a/internal_dev_deps.bzl +++ b/internal_dev_deps.bzl @@ -15,6 +15,7 @@ """Dependencies that are needed for development and testing of rules_python itself.""" load("@bazel_tools//tools/build_defs/repo:http.bzl", _http_archive = "http_archive", _http_file = "http_file") +load("@bazel_tools//tools/build_defs/repo:local.bzl", "local_repository") load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") load("//python/private:internal_config_repo.bzl", "internal_config_repo") # buildifier: disable=bzl-visibility @@ -42,6 +43,11 @@ def rules_python_internal_deps(): """ internal_config_repo(name = "rules_python_internal") + local_repository( + name = "other", + path = "tests/modules/other", + ) + http_archive( name = "bazel_skylib", sha256 = "bc283cdfcd526a52c3201279cda4bc298652efa898b10b4db0837dc51652756f", diff --git a/python/BUILD.bazel b/python/BUILD.bazel index c52e772666..a699c81cc4 100644 --- a/python/BUILD.bazel +++ b/python/BUILD.bazel @@ -79,6 +79,9 @@ bzl_library( bzl_library( name = "features_bzl", srcs = ["features.bzl"], + deps = [ + "@rules_python_internal//:rules_python_config_bzl", + ], ) bzl_library( diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel index 796cf0c9c4..45354e24d9 100644 --- a/python/config_settings/BUILD.bazel +++ b/python/config_settings/BUILD.bazel @@ -9,6 +9,7 @@ load( "LibcFlag", "PrecompileFlag", "PrecompileSourceRetentionFlag", + "VenvsSitePackages", "VenvsUseDeclareSymlinkFlag", ) load( @@ -195,6 +196,13 @@ string_flag( visibility = ["//visibility:public"], ) +string_flag( + name = "venvs_site_packages", + build_setting_default = VenvsSitePackages.NO, + # NOTE: Only public because it is used in pip hub repos. + visibility = ["//visibility:public"], +) + define_pypi_internal_flags( name = "define_pypi_internal_flags", ) diff --git a/python/features.bzl b/python/features.bzl index a7098f4710..8edfb698fc 100644 --- a/python/features.bzl +++ b/python/features.bzl @@ -19,8 +19,49 @@ load("@rules_python_internal//:rules_python_config.bzl", "config") # See https://git-scm.com/docs/git-archive/2.29.0#Documentation/git-archive.txt-export-subst _VERSION_PRIVATE = "$Format:%(describe:tags=true)$" +def _features_typedef(): + """Information about features rules_python has implemented. + + ::::{field} precompile + :type: bool + + True if the precompile attributes are available. + + :::{versionadded} 0.33.0 + ::: + :::: + + ::::{field} py_info_site_packages_symlinks + + True if the `PyInfo.site_packages_symlinks` field is available. + + :::{versionadded} VERSION_NEXT_FEATURE + ::: + :::: + + ::::{field} uses_builtin_rules + :type: bool + + True if the rules are using the Bazel-builtin implementation. + + :::{versionadded} 1.1.0 + ::: + :::: + + ::::{field} version + :type: str + + The rules_python version. This is a semver format, e.g. `X.Y.Z` with + optional trailing `-rcN`. For unreleased versions, it is an empty string. + :::{versionadded} 0.38.0 + :::: + """ + features = struct( - version = _VERSION_PRIVATE if "$Format" not in _VERSION_PRIVATE else "", + TYPEDEF = _features_typedef, + # keep sorted precompile = True, + py_info_site_packages_symlinks = True, uses_builtin_rules = not config.enable_pystar, + version = _VERSION_PRIVATE if "$Format" not in _VERSION_PRIVATE else "", ) diff --git a/python/private/attributes.bzl b/python/private/attributes.bzl index b042b3db6a..8543caba7b 100644 --- a/python/private/attributes.bzl +++ b/python/private/attributes.bzl @@ -254,6 +254,17 @@ These are typically `py_library` rules. Targets that only provide data files used at runtime belong in the `data` attribute. + +:::{note} +The order of this list can matter because it affects the order that information +from dependencies is merged in, which can be relevant depending on the ordering +mode of depsets that are merged. + +* {obj}`PyInfo.site_packages_symlinks` uses topological ordering. + +See {obj}`PyInfo` for more information about the ordering of its depsets and +how its fields are merged. +::: """, ), "precompile": lambda: attrb.String( diff --git a/python/private/builders.bzl b/python/private/builders.bzl index 50aa3ed91a..54d46c2af2 100644 --- a/python/private/builders.bzl +++ b/python/private/builders.bzl @@ -15,12 +15,19 @@ load("@bazel_skylib//lib:types.bzl", "types") -def _DepsetBuilder(): - """Create a builder for a depset.""" +def _DepsetBuilder(order = None): + """Create a builder for a depset. + + Args: + order: {type}`str | None` The order to initialize the depset to, if any. + + Returns: + {type}`DepsetBuilder` + """ # buildifier: disable=uninitialized self = struct( - _order = [None], + _order = [order], add = lambda *a, **k: _DepsetBuilder_add(self, *a, **k), build = lambda *a, **k: _DepsetBuilder_build(self, *a, **k), direct = [], diff --git a/python/private/common.bzl b/python/private/common.bzl index 48e2653ebb..072a1bb296 100644 --- a/python/private/common.bzl +++ b/python/private/common.bzl @@ -30,6 +30,16 @@ PackageSpecificationInfo = getattr(py_internal, "PackageSpecificationInfo", None # Extensions without the dot _PYTHON_SOURCE_EXTENSIONS = ["py"] +# Extensions that mean a file is relevant to Python +PYTHON_FILE_EXTENSIONS = [ + "dll", # Python C modules, Windows specific + "dylib", # Python C modules, Mac specific + "py", + "pyc", + "pyi", + "so", # Python C modules, usually Linux +] + def create_binary_semantics_struct( *, create_executable, @@ -367,7 +377,8 @@ def create_py_info( required_pyc_files, implicit_pyc_files, implicit_pyc_source_files, - imports): + imports, + site_packages_symlinks = []): """Create PyInfo provider. Args: @@ -385,6 +396,9 @@ def create_py_info( implicit_pyc_files: {type}`depset[File]` Implicitly generated pyc files that a binary can choose to include. imports: depset of strings; the import path values to propagate. + site_packages_symlinks: {type}`list[tuple[str, str]]` tuples of + `(runfiles_path, site_packages_path)` for symlinks to create + in the consuming binary's venv site packages. Returns: A tuple of the PyInfo instance and a depset of the @@ -392,6 +406,7 @@ def create_py_info( necessary for deprecated extra actions support). """ py_info = PyInfoBuilder() + py_info.site_packages_symlinks.add(site_packages_symlinks) py_info.direct_original_sources.add(original_sources) py_info.direct_pyc_files.add(required_pyc_files) py_info.direct_pyi_files.add(ctx.files.pyi_srcs) diff --git a/python/private/enum.bzl b/python/private/enum.bzl index d71442e3b5..4d0fb10699 100644 --- a/python/private/enum.bzl +++ b/python/private/enum.bzl @@ -43,3 +43,23 @@ def enum(methods = {}, **kwargs): self = struct(__members__ = members, **kwargs) return self + +def _FlagEnum_flag_values(self): + return sorted(self.__members__.values()) + +def FlagEnum(**kwargs): + """Define an enum specialized for flags. + + Args: + **kwargs: members of the enum. + + Returns: + {type}`FlagEnum` struct. This is an enum with the following extras: + * `flag_values`: A function that returns a sorted list of the + flag values (enum `__members__`). Useful for passing to the + `values` attribute for string flags. + """ + return enum( + methods = dict(flag_values = _FlagEnum_flag_values), + **kwargs + ) diff --git a/python/private/flags.bzl b/python/private/flags.bzl index 1019faa8d6..c53e4610ff 100644 --- a/python/private/flags.bzl +++ b/python/private/flags.bzl @@ -19,27 +19,7 @@ unnecessary files when all that are needed are flag definitions. """ load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") -load(":enum.bzl", "enum") - -def _FlagEnum_flag_values(self): - return sorted(self.__members__.values()) - -def FlagEnum(**kwargs): - """Define an enum specialized for flags. - - Args: - **kwargs: members of the enum. - - Returns: - {type}`FlagEnum` struct. This is an enum with the following extras: - * `flag_values`: A function that returns a sorted list of the - flag values (enum `__members__`). Useful for passing to the - `values` attribute for string flags. - """ - return enum( - methods = dict(flag_values = _FlagEnum_flag_values), - **kwargs - ) +load(":enum.bzl", "FlagEnum", "enum") def _AddSrcsToRunfilesFlag_is_enabled(ctx): value = ctx.attr._add_srcs_to_runfiles_flag[BuildSettingInfo].value @@ -138,6 +118,22 @@ VenvsUseDeclareSymlinkFlag = FlagEnum( get_value = _venvs_use_declare_symlink_flag_get_value, ) +def _venvs_site_packages_is_enabled(ctx): + if not ctx.attr.experimental_venvs_site_packages: + return False + flag_value = ctx.attr.experimental_venvs_site_packages[BuildSettingInfo].value + return flag_value == VenvsSitePackages.YES + +# Decides if libraries try to use a site-packages layout using site_packages_symlinks +# buildifier: disable=name-conventions +VenvsSitePackages = FlagEnum( + # Use site_packages_symlinks + YES = "yes", + # Don't use site_packages_symlinks + NO = "no", + is_enabled = _venvs_site_packages_is_enabled, +) + # Used for matching freethreaded toolchains and would have to be used in wheels # as well. # buildifier: disable=name-conventions diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index fed46ab223..f33c2b6ca1 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -612,15 +612,89 @@ def _create_venv(ctx, output_prefix, imports, runtime_details): }, computed_substitutions = computed_subs, ) + site_packages_symlinks = _create_site_packages_symlinks(ctx, site_packages) return struct( interpreter = interpreter, recreate_venv_at_runtime = not venvs_use_declare_symlink_enabled, # Runfiles root relative path or absolute path interpreter_actual_path = interpreter_actual_path, - files_without_interpreter = [pyvenv_cfg, pth, site_init], + files_without_interpreter = [pyvenv_cfg, pth, site_init] + site_packages_symlinks, ) +def _create_site_packages_symlinks(ctx, site_packages): + """Creates symlinks within site-packages. + + Args: + ctx: current rule ctx + site_packages: runfiles-root-relative path to the site-packages directory + + Returns: + {type}`list[File]` list of the File symlink objects created. + """ + + # maps site-package symlink to the runfiles path it should point to + entries = depset( + # NOTE: Topological ordering is used so that dependencies closer to the + # binary have precedence in creating their symlinks. This allows the + # binary a modicum of control over the result. + order = "topological", + transitive = [ + dep[PyInfo].site_packages_symlinks + for dep in ctx.attr.deps + if PyInfo in dep + ], + ).to_list() + link_map = _build_link_map(entries) + + sp_files = [] + for sp_dir_path, link_to in link_map.items(): + sp_link = ctx.actions.declare_symlink(paths.join(site_packages, sp_dir_path)) + sp_link_rf_path = runfiles_root_path(ctx, sp_link.short_path) + rel_path = relative_path( + # dirname is necessary because a relative symlink is relative to + # the directory the symlink resides within. + from_ = paths.dirname(sp_link_rf_path), + to = link_to, + ) + ctx.actions.symlink(output = sp_link, target_path = rel_path) + sp_files.append(sp_link) + return sp_files + +def _build_link_map(entries): + link_map = {} + for link_to_runfiles_path, site_packages_path in entries: + if site_packages_path in link_map: + # We ignore duplicates by design. The dependency closer to the + # binary gets precedence due to the topological ordering. + continue + else: + link_map[site_packages_path] = link_to_runfiles_path + + # An empty link_to value means to not create the site package symlink. + # Because of the topological ordering, this allows binaries to remove + # entries by having an earlier dependency produce empty link_to values. + for sp_dir_path, link_to in link_map.items(): + if not link_to: + link_map.pop(sp_dir_path) + + # Remove entries that would be a child path of a created symlink. + # Earlier entries have precedence to match how exact matches are handled. + keep_link_map = {} + for _ in range(len(link_map)): + if not link_map: + break + dirname, value = link_map.popitem() + keep_link_map[dirname] = value + + prefix = dirname + "/" # Add slash to prevent /X matching /XY + for maybe_suffix in link_map.keys(): + maybe_suffix += "/" # Add slash to prevent /X matching /XY + if maybe_suffix.startswith(prefix) or prefix.startswith(maybe_suffix): + link_map.pop(maybe_suffix) + + return keep_link_map + def _map_each_identity(v): return v diff --git a/python/private/py_info.bzl b/python/private/py_info.bzl index ef654c303e..4ecd02a438 100644 --- a/python/private/py_info.bzl +++ b/python/private/py_info.bzl @@ -42,7 +42,8 @@ def _PyInfo_init( direct_original_sources = depset(), transitive_original_sources = depset(), direct_pyi_files = depset(), - transitive_pyi_files = depset()): + transitive_pyi_files = depset(), + site_packages_symlinks = depset()): _check_arg_type("transitive_sources", "depset", transitive_sources) # Verify it's postorder compatible, but retain is original ordering. @@ -70,6 +71,7 @@ def _PyInfo_init( "has_py2_only_sources": has_py2_only_sources, "has_py3_only_sources": has_py2_only_sources, "imports": imports, + "site_packages_symlinks": site_packages_symlinks, "transitive_implicit_pyc_files": transitive_implicit_pyc_files, "transitive_implicit_pyc_source_files": transitive_implicit_pyc_source_files, "transitive_original_sources": transitive_original_sources, @@ -140,6 +142,34 @@ A depset of import path strings to be added to the `PYTHONPATH` of executable Python targets. These are accumulated from the transitive `deps`. The order of the depset is not guaranteed and may be changed in the future. It is recommended to use `default` order (the default). +""", + "site_packages_symlinks": """ +:type: depset[tuple[str | None, str]] + +A depset with `topological` ordering. + +Tuples of `(runfiles_path, site_packages_path)`. Where +* `runfiles_path` is a runfiles-root relative path. It is the path that + has the code to make importable. If `None` or empty string, then it means + to not create a site packages directory with the `site_packages_path` + name. +* `site_packages_path` is a path relative to the site-packages directory of + the venv for whatever creates the venv (typically py_binary). It makes + the code in `runfiles_path` available for import. Note that this + is created as a "raw" symlink (via `declare_symlink`). + +:::{include} /_includes/experimental_api.md +::: + +:::{tip} +The topological ordering means dependencies earlier and closer to the consumer +have precedence. This allows e.g. a binary to add dependencies that override +values from further way dependencies, such as forcing symlinks to point to +specific paths or preventing symlinks from being created. +::: + +:::{versionadded} VERSION_NEXT_FEATURE +::: """, "transitive_implicit_pyc_files": """ :type: depset[File] @@ -266,6 +296,7 @@ def PyInfoBuilder(): transitive_pyc_files = builders.DepsetBuilder(), transitive_pyi_files = builders.DepsetBuilder(), transitive_sources = builders.DepsetBuilder(), + site_packages_symlinks = builders.DepsetBuilder(order = "topological"), ) return self @@ -351,6 +382,7 @@ def _PyInfoBuilder_merge_all(self, transitive, *, direct = []): self.transitive_original_sources.add(info.transitive_original_sources) self.transitive_pyc_files.add(info.transitive_pyc_files) self.transitive_pyi_files.add(info.transitive_pyi_files) + self.site_packages_symlinks.add(info.site_packages_symlinks) return self @@ -400,6 +432,7 @@ def _PyInfoBuilder_build(self): transitive_original_sources = self.transitive_original_sources.build(), transitive_pyc_files = self.transitive_pyc_files.build(), transitive_pyi_files = self.transitive_pyi_files.build(), + site_packages_symlinks = self.site_packages_symlinks.build(), ) else: kwargs = {} diff --git a/python/private/py_library.bzl b/python/private/py_library.bzl index f6c7b12578..edd0db579f 100644 --- a/python/private/py_library.bzl +++ b/python/private/py_library.bzl @@ -14,6 +14,7 @@ """Common code for implementing py_library rules.""" load("@bazel_skylib//lib:dicts.bzl", "dicts") +load("@bazel_skylib//lib:paths.bzl", "paths") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load(":attr_builders.bzl", "attrb") load( @@ -25,8 +26,21 @@ load( "REQUIRED_EXEC_GROUP_BUILDERS", ) load(":builders.bzl", "builders") -load(":common.bzl", "collect_cc_info", "collect_imports", "collect_runfiles", "create_instrumented_files_info", "create_library_semantics_struct", "create_output_group_info", "create_py_info", "filter_to_py_srcs", "get_imports") -load(":flags.bzl", "AddSrcsToRunfilesFlag", "PrecompileFlag") +load( + ":common.bzl", + "PYTHON_FILE_EXTENSIONS", + "collect_cc_info", + "collect_imports", + "collect_runfiles", + "create_instrumented_files_info", + "create_library_semantics_struct", + "create_output_group_info", + "create_py_info", + "filter_to_py_srcs", + "get_imports", + "runfiles_root_path", +) +load(":flags.bzl", "AddSrcsToRunfilesFlag", "PrecompileFlag", "VenvsSitePackages") load(":precompile.bzl", "maybe_precompile") load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo") load(":py_internal.bzl", "py_internal") @@ -44,6 +58,46 @@ LIBRARY_ATTRS = dicts.add( PY_SRCS_ATTRS, IMPORTS_ATTRS, { + "experimental_venvs_site_packages": lambda: attrb.Label( + doc = """ +**INTERNAL ATTRIBUTE. SHOULD ONLY BE SET BY rules_python-INTERNAL CODE.** + +:::{include} /_includes/experimental_api.md +::: + +A flag that decides whether the library should treat its sources as a +site-packages layout. + +When the flag is `yes`, then the `srcs` files are treated as a site-packages +layout that is relative to the `imports` attribute. The `imports` attribute +can have only a single element. It is a repo-relative runfiles path. + +For example, in the `my/pkg/BUILD.bazel` file, given +`srcs=["site-packages/foo/bar.py"]`, specifying +`imports=["my/pkg/site-packages"]` means `foo/bar.py` is the file path +under the binary's venv site-packages directory that should be made available (i.e. +`import foo.bar` will work). + +`__init__.py` files are treated specially to provide basic support for [implicit +namespace packages]( +https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages). +However, the *content* of the files cannot be taken into account, merely their +presence or absense. Stated another way: [pkgutil-style namespace packages]( +https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages) +won't be understood as namespace packages; they'll be seen as regular packages. This will +likely lead to conflicts with other targets that contribute to the namespace. + +:::{tip} +This attributes populates {obj}`PyInfo.site_packages_symlinks`, which is +a topologically ordered depset. This means dependencies closer and earlier +to a consumer have precedence. See {obj}`PyInfo.site_packages_symlinks` for +more information. +::: + +:::{versionadded} VERSION_NEXT_FEATURE +::: +""", + ), "_add_srcs_to_runfiles_flag": lambda: attrb.Label( default = "//python/config_settings:add_srcs_to_runfiles", ), @@ -98,6 +152,11 @@ def py_library_impl(ctx, *, semantics): runfiles.add(collect_runfiles(ctx)) runfiles = runfiles.build(ctx) + imports = [] + site_packages_symlinks = [] + + imports, site_packages_symlinks = _get_imports_and_site_packages_symlinks(ctx, semantics) + cc_info = semantics.get_cc_info_for_library(ctx) py_info, deps_transitive_sources, builtins_py_info = create_py_info( ctx, @@ -106,7 +165,8 @@ def py_library_impl(ctx, *, semantics): required_pyc_files = required_pyc_files, implicit_pyc_files = implicit_pyc_files, implicit_pyc_source_files = implicit_pyc_source_files, - imports = collect_imports(ctx, semantics), + imports = imports, + site_packages_symlinks = site_packages_symlinks, ) # TODO(b/253059598): Remove support for extra actions; https://github.com/bazelbuild/bazel/issues/16455 @@ -144,6 +204,101 @@ Source files are no longer added to the runfiles directly. ::: """ +def _get_imports_and_site_packages_symlinks(ctx, semantics): + imports = depset() + site_packages_symlinks = depset() + if VenvsSitePackages.is_enabled(ctx): + site_packages_symlinks = _get_site_packages_symlinks(ctx) + else: + imports = collect_imports(ctx, semantics) + return imports, site_packages_symlinks + +def _get_site_packages_symlinks(ctx): + imports = ctx.attr.imports + if len(imports) == 0: + fail("When venvs_site_packages is enabled, exactly one `imports` " + + "value must be specified, got 0") + elif len(imports) > 1: + fail("When venvs_site_packages is enabled, exactly one `imports` " + + "value must be specified, got {}".format(imports)) + else: + site_packages_root = imports[0] + + if site_packages_root.endswith("/"): + fail("The site packages root value from `imports` cannot end in " + + "slash, got {}".format(site_packages_root)) + if site_packages_root.startswith("/"): + fail("The site packages root value from `imports` cannot start with " + + "slash, got {}".format(site_packages_root)) + + # Append slash to prevent incorrectly prefix-string matches + site_packages_root += "/" + + # We have to build a list of (runfiles path, site-packages path) pairs of + # the files to create in the consuming binary's venv site-packages directory. + # To minimize the number of files to create, we just return the paths + # to the directories containing the code of interest. + # + # However, namespace packages complicate matters: multiple + # distributions install in the same directory in site-packages. This + # works out because they don't overlap in their files. Typically, they + # install to different directories within the namespace package + # directory. Namespace package directories are simply directories + # within site-packages that *don't* have an `__init__.py` file, which + # can be arbitrarily deep. Thus, we simply have to look for the + # directories that _do_ have an `__init__.py` file and treat those as + # the path to symlink to. + + repo_runfiles_dirname = None + dirs_with_init = {} # dirname -> runfile path + for src in ctx.files.srcs: + if src.extension not in PYTHON_FILE_EXTENSIONS: + continue + path = _repo_relative_short_path(src.short_path) + if not path.startswith(site_packages_root): + continue + path = path.removeprefix(site_packages_root) + dir_name, _, filename = path.rpartition("/") + if not dir_name: + # This would be e.g. `site-packages/__init__.py`, which isn't valid + # because it's not within a directory for an importable Python package. + # However, the pypi integration over-eagerly adds a pkgutil-style + # __init__.py file during the repo phase. Just ignore them for now. + continue + + if filename.startswith("__init__."): + dirs_with_init[dir_name] = None + repo_runfiles_dirname = runfiles_root_path(ctx, src.short_path).partition("/")[0] + + # Sort so that we encounter `foo` before `foo/bar`. This ensures we + # see the top-most explicit package first. + dirnames = sorted(dirs_with_init.keys()) + first_level_explicit_packages = [] + for d in dirnames: + is_sub_package = False + for existing in first_level_explicit_packages: + # Suffix with / to prevent foo matching foobar + if d.startswith(existing + "/"): + is_sub_package = True + break + if not is_sub_package: + first_level_explicit_packages.append(d) + + site_packages_symlinks = [] + for dirname in first_level_explicit_packages: + site_packages_symlinks.append(( + paths.join(repo_runfiles_dirname, site_packages_root, dirname), + dirname, + )) + return site_packages_symlinks + +def _repo_relative_short_path(short_path): + # Convert `../+pypi+foo/some/file.py` to `some/file.py` + if short_path.startswith("../"): + return short_path[3:].partition("/")[2] + else: + return short_path + # NOTE: Exported publicaly def create_py_library_rule_builder(): """Create a rule builder for a py_library. diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl index c390da2613..95031e6181 100644 --- a/python/private/pypi/whl_library_targets.bzl +++ b/python/private/pypi/whl_library_targets.bzl @@ -266,6 +266,7 @@ def whl_library_targets( ), tags = tags, visibility = impl_vis, + experimental_venvs_site_packages = Label("@rules_python//python/config_settings:venvs_site_packages"), ) def _config_settings(dependencies_by_platform, native = native, **kwargs): diff --git a/tests/modules/other/BUILD.bazel b/tests/modules/other/BUILD.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modules/other/MODULE.bazel b/tests/modules/other/MODULE.bazel new file mode 100644 index 0000000000..7cd3118b81 --- /dev/null +++ b/tests/modules/other/MODULE.bazel @@ -0,0 +1,3 @@ +module(name = "other") + +bazel_dep(name = "rules_python", version = "0") diff --git a/tests/modules/other/nspkg_delta/BUILD.bazel b/tests/modules/other/nspkg_delta/BUILD.bazel new file mode 100644 index 0000000000..457033aacf --- /dev/null +++ b/tests/modules/other/nspkg_delta/BUILD.bazel @@ -0,0 +1,10 @@ +load("@rules_python//python:py_library.bzl", "py_library") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "nspkg_delta", + srcs = glob(["site-packages/**/*.py"]), + experimental_venvs_site_packages = "@rules_python//python/config_settings:venvs_site_packages", + imports = [package_name() + "/site-packages"], +) diff --git a/tests/modules/other/nspkg_delta/site-packages/nspkg/subnspkg/delta/__init__.py b/tests/modules/other/nspkg_delta/site-packages/nspkg/subnspkg/delta/__init__.py new file mode 100644 index 0000000000..bb7b160deb --- /dev/null +++ b/tests/modules/other/nspkg_delta/site-packages/nspkg/subnspkg/delta/__init__.py @@ -0,0 +1 @@ +# Intentionally empty diff --git a/tests/modules/other/nspkg_gamma/BUILD.bazel b/tests/modules/other/nspkg_gamma/BUILD.bazel new file mode 100644 index 0000000000..89038e80d2 --- /dev/null +++ b/tests/modules/other/nspkg_gamma/BUILD.bazel @@ -0,0 +1,10 @@ +load("@rules_python//python:py_library.bzl", "py_library") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "nspkg_gamma", + srcs = glob(["site-packages/**/*.py"]), + experimental_venvs_site_packages = "@rules_python//python/config_settings:venvs_site_packages", + imports = [package_name() + "/site-packages"], +) diff --git a/tests/modules/other/nspkg_gamma/site-packages/nspkg/subnspkg/gamma/__init__.py b/tests/modules/other/nspkg_gamma/site-packages/nspkg/subnspkg/gamma/__init__.py new file mode 100644 index 0000000000..bb7b160deb --- /dev/null +++ b/tests/modules/other/nspkg_gamma/site-packages/nspkg/subnspkg/gamma/__init__.py @@ -0,0 +1 @@ +# Intentionally empty diff --git a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl index a042ed0346..f738e03b5d 100644 --- a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl +++ b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl @@ -273,6 +273,7 @@ def _test_whl_and_library_deps(env): ), "tags": ["tag1", "tag2"], "visibility": ["//visibility:public"], + "experimental_venvs_site_packages": Label("//python/config_settings:venvs_site_packages"), }, ]) # buildifier: @unsorted-dict-items @@ -335,6 +336,7 @@ def _test_group(env): }), "tags": [], "visibility": ["@pypi__groups//:__pkg__"], + "experimental_venvs_site_packages": Label("//python/config_settings:venvs_site_packages"), }, ]) # buildifier: @unsorted-dict-items diff --git a/tests/support/sh_py_run_test.bzl b/tests/support/sh_py_run_test.bzl index 7b3b617da1..9c8134ff40 100644 --- a/tests/support/sh_py_run_test.bzl +++ b/tests/support/sh_py_run_test.bzl @@ -40,6 +40,8 @@ def _perform_transition_impl(input_settings, attr, base_impl): settings["//python/bin:python_src"] = attr.python_src if attr.venvs_use_declare_symlink: settings["//python/config_settings:venvs_use_declare_symlink"] = attr.venvs_use_declare_symlink + if attr.venvs_site_packages: + settings["//python/config_settings:venvs_site_packages"] = attr.venvs_site_packages return settings _RECONFIG_INPUTS = [ @@ -47,6 +49,7 @@ _RECONFIG_INPUTS = [ "//python/bin:python_src", "//command_line_option:extra_toolchains", "//python/config_settings:venvs_use_declare_symlink", + "//python/config_settings:venvs_site_packages", ] _RECONFIG_OUTPUTS = _RECONFIG_INPUTS + [ "//command_line_option:build_python_zip", @@ -67,6 +70,7 @@ toolchain. """, ), "python_src": attrb.Label(), + "venvs_site_packages": attrb.String(), "venvs_use_declare_symlink": attrb.String(), } diff --git a/tests/venv_site_packages_libs/BUILD.bazel b/tests/venv_site_packages_libs/BUILD.bazel new file mode 100644 index 0000000000..5d02708800 --- /dev/null +++ b/tests/venv_site_packages_libs/BUILD.bazel @@ -0,0 +1,17 @@ +load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") +load("//tests/support:support.bzl", "SUPPORTS_BOOTSTRAP_SCRIPT") + +py_reconfig_test( + name = "venvs_site_packages_libs_test", + srcs = ["bin.py"], + bootstrap_impl = "script", + main = "bin.py", + target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT, + venvs_site_packages = "yes", + deps = [ + "//tests/venv_site_packages_libs/nspkg_alpha", + "//tests/venv_site_packages_libs/nspkg_beta", + "@other//nspkg_delta", + "@other//nspkg_gamma", + ], +) diff --git a/tests/venv_site_packages_libs/bin.py b/tests/venv_site_packages_libs/bin.py new file mode 100644 index 0000000000..b944be69e3 --- /dev/null +++ b/tests/venv_site_packages_libs/bin.py @@ -0,0 +1,32 @@ +import importlib +import os +import sys +import unittest + + +class VenvSitePackagesLibraryTest(unittest.TestCase): + def setUp(self): + super().setUp() + if sys.prefix == sys.base_prefix: + raise AssertionError("Not running under a venv") + self.venv = sys.prefix + + def assert_imported_from_venv(self, module_name): + module = importlib.import_module(module_name) + self.assertEqual(module.__name__, module_name) + self.assertTrue( + module.__file__.startswith(self.venv), + f"\n{module_name} was imported, but not from the venv.\n" + + f"venv : {self.venv}\n" + + f"actual: {module.__file__}", + ) + + def test_imported_from_venv(self): + self.assert_imported_from_venv("nspkg.subnspkg.alpha") + self.assert_imported_from_venv("nspkg.subnspkg.beta") + self.assert_imported_from_venv("nspkg.subnspkg.gamma") + self.assert_imported_from_venv("nspkg.subnspkg.delta") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/venv_site_packages_libs/nspkg_alpha/BUILD.bazel b/tests/venv_site_packages_libs/nspkg_alpha/BUILD.bazel new file mode 100644 index 0000000000..c40c3b4080 --- /dev/null +++ b/tests/venv_site_packages_libs/nspkg_alpha/BUILD.bazel @@ -0,0 +1,10 @@ +load("@rules_python//python:py_library.bzl", "py_library") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "nspkg_alpha", + srcs = glob(["site-packages/**/*.py"]), + experimental_venvs_site_packages = "//python/config_settings:venvs_site_packages", + imports = [package_name() + "/site-packages"], +) diff --git a/tests/venv_site_packages_libs/nspkg_alpha/site-packages/nspkg/subnspkg/alpha/__init__.py b/tests/venv_site_packages_libs/nspkg_alpha/site-packages/nspkg/subnspkg/alpha/__init__.py new file mode 100644 index 0000000000..b5ee093672 --- /dev/null +++ b/tests/venv_site_packages_libs/nspkg_alpha/site-packages/nspkg/subnspkg/alpha/__init__.py @@ -0,0 +1 @@ +whoami = "alpha" diff --git a/tests/venv_site_packages_libs/nspkg_beta/BUILD.bazel b/tests/venv_site_packages_libs/nspkg_beta/BUILD.bazel new file mode 100644 index 0000000000..5d402183bd --- /dev/null +++ b/tests/venv_site_packages_libs/nspkg_beta/BUILD.bazel @@ -0,0 +1,10 @@ +load("@rules_python//python:py_library.bzl", "py_library") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "nspkg_beta", + srcs = glob(["site-packages/**/*.py"]), + experimental_venvs_site_packages = "//python/config_settings:venvs_site_packages", + imports = [package_name() + "/site-packages"], +) diff --git a/tests/venv_site_packages_libs/nspkg_beta/site-packages/nspkg/subnspkg/beta/__init__.py b/tests/venv_site_packages_libs/nspkg_beta/site-packages/nspkg/subnspkg/beta/__init__.py new file mode 100644 index 0000000000..a2a65910c7 --- /dev/null +++ b/tests/venv_site_packages_libs/nspkg_beta/site-packages/nspkg/subnspkg/beta/__init__.py @@ -0,0 +1 @@ +whoami = "beta" diff --git a/tests/venv_site_packages_libs/venv_site_packages_pypi_test.py b/tests/venv_site_packages_libs/venv_site_packages_pypi_test.py new file mode 100644 index 0000000000..519b258044 --- /dev/null +++ b/tests/venv_site_packages_libs/venv_site_packages_pypi_test.py @@ -0,0 +1,36 @@ +import os +import sys +import unittest + + +class VenvSitePackagesLibraryTest(unittest.TestCase): + def test_imported_from_venv(self): + self.assertNotEqual(sys.prefix, sys.base_prefix, "Not running under a venv") + venv = sys.prefix + + from nspkg.subnspkg import alpha + + self.assertEqual(alpha.whoami, "alpha") + self.assertEqual(alpha.__name__, "nspkg.subnspkg.alpha") + + self.assertTrue( + alpha.__file__.startswith(sys.prefix), + f"\nalpha was imported, not from within the venv.\n" + + f"venv : {venv}\n" + + f"actual: {alpha.__file__}", + ) + + from nspkg.subnspkg import beta + + self.assertEqual(beta.whoami, "beta") + self.assertEqual(beta.__name__, "nspkg.subnspkg.beta") + self.assertTrue( + beta.__file__.startswith(sys.prefix), + f"\nbeta was imported, not from within the venv.\n" + + f"venv : {venv}\n" + + f"actual: {beta.__file__}", + ) + + +if __name__ == "__main__": + unittest.main() From e5fa023b27cf3583eb9e45efcbcb887e660ce65f Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 5 Apr 2025 10:05:08 -0700 Subject: [PATCH 008/156] docs: fix a few xrefs (#2740) Fixes a few xrefs in the docs that had typos or missing external bazel links. --- CHANGELOG.md | 2 +- docs/api/rules_python/python/config_settings/index.md | 2 +- docs/toolchains.md | 4 ++-- python/private/py_executable.bzl | 2 +- sphinxdocs/inventories/bazel_inventory.txt | 8 ++++++++ 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 818773e589..5172e742c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,7 +68,7 @@ Unreleased changes template. using `experimental_index_url`. * (toolchains) Remove all but `3.8.20` versions of the Python `3.8` interpreter who has reached EOL. If users still need other versions of the `3.8` interpreter, please supply - the URLs manually {bzl:ob}`python.toolchain` or {bzl:obj}`python_register_toolchains` calls. + the URLs manually {bzl:obj}`python.toolchain` or {bzl:obj}`python_register_toolchains` calls. * (pypi) The PyPI extension will no longer write the lock file entries as the extension has been marked reproducible. Fixes [#2434](https://github.com/bazel-contrib/rules_python/issues/2434). diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md index 340335d9b1..ed6444298e 100644 --- a/docs/api/rules_python/python/config_settings/index.md +++ b/docs/api/rules_python/python/config_settings/index.md @@ -46,7 +46,7 @@ of builtin, known versions. If you need to match a version that isn't present, then you have two options: 1. Manually define a `config_setting` and have it match {obj}`--python_version` - or {ob}`python_version_major_minor`. This works best when you don't control the + or {obj}`python_version_major_minor`. This works best when you don't control the root module, or don't want to rely on the MODULE.bazel configuration. Such a config settings would look like: ``` diff --git a/docs/toolchains.md b/docs/toolchains.md index 0e4f5c2321..73a8a48121 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -265,7 +265,7 @@ use_repo(python, "python_3_10", "python_3_10_host") ``` Note, the user has to import the `*_host` repository to use the python interpreter in the -{bzl:obj}`pip_parse` and {bzl:obj}`whl_library` repository rules and once that is done +{bzl:obj}`pip_parse` and `whl_library` repository rules and once that is done users should be able to ensure the setting of the default toolchain even during the transition period when some of the code is still defined in `WORKSPACE`. @@ -364,7 +364,7 @@ toolchains a "toolchain suite". One of the underlying design goals of the toolchains is to support complex and bespoke environments. Such environments may use an arbitrary combination of -{obj}`RBE`, cross-platform building, multiple Python versions, +{bzl:obj}`RBE`, cross-platform building, multiple Python versions, building Python from source, embeding Python (as opposed to building separate interpreters), using prebuilt binaries, or using binaries built from source. To that end, many of the attributes they accept, and fields they provide, are diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index f33c2b6ca1..e6f4700b20 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -92,7 +92,7 @@ Only supported for {obj}`--bootstrap_impl=script`. Ignored otherwise. ::: :::{seealso} -The {obj}`RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` environment variable +The {any}`RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` environment variable ::: :::{versionadded} 1.3.0 diff --git a/sphinxdocs/inventories/bazel_inventory.txt b/sphinxdocs/inventories/bazel_inventory.txt index dc11f02b5b..458126a849 100644 --- a/sphinxdocs/inventories/bazel_inventory.txt +++ b/sphinxdocs/inventories/bazel_inventory.txt @@ -28,6 +28,14 @@ attr.string_list bzl:type 1 rules/lib/toplevel/attr#string_list - attr.string_list_dict bzl:type 1 rules/lib/toplevel/attr#string_list_dict - bool bzl:type 1 rules/lib/bool - callable bzl:type 1 rules/lib/core/function - +config bzl:obj 1 rules/lib/toplevel/config - +config.bool bzl:function 1 rules/lib/toplevel/config#bool - +config.exec bzl:function 1 rules/lib/toplevel/config#exec - +config.int bzl:function 1 rules/lib/toplevel/config#int - +config.none bzl:function 1 rules/lib/toplevel/config#none - +config.string bzl:function 1 rules/lib/toplevel/config#string - +config.string_list bzl:function 1 rules/lib/toplevel/config#string_list - +config.target bzl:function 1 rules/lib/toplevel/config#target - config_common.FeatureFlagInfo bzl:type 1 rules/lib/toplevel/config_common#FeatureFlagInfo - config_common.toolchain_type bzl:function 1 rules/lib/toplevel/config_common#toolchain_type - ctx.actions bzl:obj 1 rules/lib/builtins/ctx#actions - From 6854dc3880b1ff81659ad4a36fb2e6551f41d0e2 Mon Sep 17 00:00:00 2001 From: Matt Mackay Date: Sat, 5 Apr 2025 14:42:03 -0400 Subject: [PATCH 009/156] fix: treat ignore_root_user_error either ignored or warning (#2739) Previously [#2636](https://github.com/bazel-contrib/rules_python/pull/2636) changed the semantics of `ignore_root_user_error` from "ignore" to "warning". This is now flipped back to ignoring the issue, and will only emit a warning when the attribute is set `False`. This does also change the semantics of what #2636 did by flipping the attribute, as now there is no warning, and the user would have to explicitly set it to `False` (they don't want to ignore the error) to see the warning. Co-authored-by: Richard Levasseur --- CHANGELOG.md | 4 +++ python/private/python.bzl | 4 +-- python/private/python_repository.bzl | 40 +++++++++++++++------------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5172e742c9..dbb0c03e59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,10 @@ Unreleased changes template. * (toolchains) Remove all but `3.8.20` versions of the Python `3.8` interpreter who has reached EOL. If users still need other versions of the `3.8` interpreter, please supply the URLs manually {bzl:obj}`python.toolchain` or {bzl:obj}`python_register_toolchains` calls. +* (toolchains) Previously [#2636](https://github.com/bazel-contrib/rules_python/pull/2636) + changed the semantics of `ignore_root_user_error` from "ignore" to "warning". This is now + flipped back to ignoring the issue, and will only emit a warning when the attribute is set + `False`. * (pypi) The PyPI extension will no longer write the lock file entries as the extension has been marked reproducible. Fixes [#2434](https://github.com/bazel-contrib/rules_python/issues/2434). diff --git a/python/private/python.bzl b/python/private/python.bzl index 296fb0ab7d..efc429420e 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -803,8 +803,8 @@ to spurious cache misses or build failures). However, if the user is running Bazel as root, this read-onlyness is not respected. Bazel will print a warning message when it detects that the runtime installation is writable despite being made read only (i.e. it's running with -root access). If this attribute is set to `False`, Bazel will make it a hard -error to run with root access instead. +root access) while this attribute is set `False`, however this messaging can be ignored by setting +this to `False`. """, mandatory = False, ), diff --git a/python/private/python_repository.bzl b/python/private/python_repository.bzl index f3ec13d67d..cfc06452a9 100644 --- a/python/private/python_repository.bzl +++ b/python/private/python_repository.bzl @@ -137,28 +137,30 @@ def _python_repository_impl(rctx): logger = logger, ) - fail_or_warn = logger.warn if rctx.attr.ignore_root_user_error else logger.fail - exec_result = repo_utils.execute_unchecked( - rctx, - op = "python_repository.TestReadOnly", - arguments = [repo_utils.which_checked(rctx, "touch"), "lib/.test"], - logger = logger, - ) - - # The issue with running as root is the installation is no longer - # read-only, so the problems due to pyc can resurface. - if exec_result.return_code == 0: - stdout = repo_utils.execute_checked_stdout( + # If the user is not ignoring the warnings, then proceed to run a check, + # otherwise these steps can be skipped, as they both result in some warning. + if not rctx.attr.ignore_root_user_error: + exec_result = repo_utils.execute_unchecked( rctx, - op = "python_repository.GetUserId", - arguments = [repo_utils.which_checked(rctx, "id"), "-u"], + op = "python_repository.TestReadOnly", + arguments = [repo_utils.which_checked(rctx, "touch"), "lib/.test"], logger = logger, ) - uid = int(stdout.strip()) - if uid == 0: - fail_or_warn("The current user is root, which can cause spurious cache misses or build failures with the hermetic Python interpreter. See https://github.com/bazel-contrib/rules_python/pull/713.") - else: - fail_or_warn("The current user has CAP_DAC_OVERRIDE set, which can cause spurious cache misses or build failures with the hermetic Python interpreter. See https://github.com/bazel-contrib/rules_python/pull/713.") + + # The issue with running as root is the installation is no longer + # read-only, so the problems due to pyc can resurface. + if exec_result.return_code == 0: + stdout = repo_utils.execute_checked_stdout( + rctx, + op = "python_repository.GetUserId", + arguments = [repo_utils.which_checked(rctx, "id"), "-u"], + logger = logger, + ) + uid = int(stdout.strip()) + if uid == 0: + logger.warn("The current user is root, which can cause spurious cache misses or build failures with the hermetic Python interpreter. See https://github.com/bazel-contrib/rules_python/pull/713.") + else: + logger.warn("The current user has CAP_DAC_OVERRIDE set, which can cause spurious cache misses or build failures with the hermetic Python interpreter. See https://github.com/bazel-contrib/rules_python/pull/713.") python_bin = "python.exe" if ("windows" in platform) else "bin/python3" From 7f5a1b5a0e6fbe29c5c33d8e164b4cda6ded99b7 Mon Sep 17 00:00:00 2001 From: Matt Mackay Date: Sat, 5 Apr 2025 18:48:14 -0400 Subject: [PATCH 010/156] fix: Ensure temporary .pyc & .pyo files are excluded from the interpreters repository files (#2743) We've seen cases the temporary versions for the `.pyc` and `.pyo` files are unstable on certain interpreter toolchains. The temp files take for form of `.pyc.NNN`, so the amended glob patten will still match both the `.pyc` and `.pyc.NNN` versions of the file names. --------- Co-authored-by: Richard Levasseur --- CHANGELOG.md | 1 + python/private/python_repository.bzl | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbb0c03e59..abe718c389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ Unreleased changes template. transitions transitioning on the `python_version` flag. Fixes [#2685](https://github.com/bazel-contrib/rules_python/issues/2685). * (toolchains) Run the check on the Python interpreter in isolated mode, to ensure it's not affected by userland environment variables, such as `PYTHONPATH`. +* (toolchains) Ensure temporary `.pyc` and `.pyo` files are also excluded from the interpreters repository files. {#v0-0-0-added} ### Added diff --git a/python/private/python_repository.bzl b/python/private/python_repository.bzl index cfc06452a9..fd86b415cc 100644 --- a/python/private/python_repository.bzl +++ b/python/private/python_repository.bzl @@ -193,8 +193,9 @@ def _python_repository_impl(rctx): # Exclude them from the glob because otherwise between the first time and second time a python toolchain is used," # the definition of this filegroup will change, and depending rules will get invalidated." # See https://github.com/bazel-contrib/rules_python/issues/1008 for unconditionally adding these to toolchains so we can stop ignoring them." - "**/__pycache__/*.pyc", - "**/__pycache__/*.pyo", + # pyc* is ignored because pyc creation creates temporary .pyc.NNNN files + "**/__pycache__/*.pyc*", + "**/__pycache__/*.pyo*", ] if "windows" in platform: From da0e52f59047ab47bcb561787d42a8f93537dc41 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 5 Apr 2025 16:47:44 -0700 Subject: [PATCH 011/156] chore: remove unnecessary DEFAULT_BOOTSTRAP_TEMPLATE global (#2744) I think the DEFAULT_BOOTSTRAP_TEMPLATE global was used by something in the original Bazel impl, but now it's just used in one place. Remove the shared global and just inline the single usage. --- python/private/py_runtime_info.bzl | 2 -- python/private/py_runtime_rule.bzl | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/python/private/py_runtime_info.bzl b/python/private/py_runtime_info.bzl index 19857c9ede..4297391068 100644 --- a/python/private/py_runtime_info.bzl +++ b/python/private/py_runtime_info.bzl @@ -17,8 +17,6 @@ load(":util.bzl", "define_bazel_6_provider") DEFAULT_STUB_SHEBANG = "#!/usr/bin/env python3" -DEFAULT_BOOTSTRAP_TEMPLATE = Label("//python/private:bootstrap_template") - _PYTHON_VERSION_VALUES = ["PY2", "PY3"] def _optional_int(value): diff --git a/python/private/py_runtime_rule.bzl b/python/private/py_runtime_rule.bzl index 3dc00baa12..a85f5b25f2 100644 --- a/python/private/py_runtime_rule.bzl +++ b/python/private/py_runtime_rule.bzl @@ -19,7 +19,7 @@ load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load(":attributes.bzl", "NATIVE_RULES_ALLOWLIST_ATTRS") load(":flags.bzl", "FreeThreadedFlag") load(":py_internal.bzl", "py_internal") -load(":py_runtime_info.bzl", "DEFAULT_BOOTSTRAP_TEMPLATE", "DEFAULT_STUB_SHEBANG", "PyRuntimeInfo") +load(":py_runtime_info.bzl", "DEFAULT_STUB_SHEBANG", "PyRuntimeInfo") load(":reexports.bzl", "BuiltinPyRuntimeInfo") load(":util.bzl", "IS_BAZEL_7_OR_HIGHER") @@ -201,7 +201,7 @@ If not set, then it will be set based on flags. ), "bootstrap_template": attr.label( allow_single_file = True, - default = DEFAULT_BOOTSTRAP_TEMPLATE, + default = Label("//python/private:bootstrap_template"), doc = """ The bootstrap script template file to use. Should have %python_binary%, %workspace_name%, %main%, and %imports%. From 996ae2658bffe7163a5abc384eff57ff28d4f409 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 23:16:24 +0000 Subject: [PATCH 012/156] build(deps): bump jinja2 from 3.1.4 to 3.1.6 in /docs (#2750) Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.4 to 3.1.6.
Release notes

Sourced from jinja2's releases.

3.1.6

This is the Jinja 3.1.6 security release, which fixes security issues but does not otherwise change behavior and should not result in breaking changes compared to the latest feature release.

PyPI: https://pypi.org/project/Jinja2/3.1.6/ Changes: https://jinja.palletsprojects.com/en/stable/changes/#version-3-1-6

3.1.5

This is the Jinja 3.1.5 security fix release, which fixes security issues and bugs but does not otherwise change behavior and should not result in breaking changes compared to the latest feature release.

PyPI: https://pypi.org/project/Jinja2/3.1.5/ Changes: https://jinja.palletsprojects.com/changes/#version-3-1-5 Milestone: https://github.com/pallets/jinja/milestone/16?closed=1

  • The sandboxed environment handles indirect calls to str.format, such as by passing a stored reference to a filter that calls its argument. GHSA-q2x7-8rv6-6q7h
  • Escape template name before formatting it into error messages, to avoid issues with names that contain f-string syntax. #1792, GHSA-gmj6-6f8f-6699
  • Sandbox does not allow clear and pop on known mutable sequence types. #2032
  • Calling sync render for an async template uses asyncio.run. #1952
  • Avoid unclosed auto_aiter warnings. #1960
  • Return an aclose-able AsyncGenerator from Template.generate_async. #1960
  • Avoid leaving root_render_func() unclosed in Template.generate_async. #1960
  • Avoid leaving async generators unclosed in blocks, includes and extends. #1960
  • The runtime uses the correct concat function for the current environment when calling block references. #1701
  • Make |unique async-aware, allowing it to be used after another async-aware filter. #1781
  • |int filter handles OverflowError from scientific notation. #1921
  • Make compiling deterministic for tuple unpacking in a {% set ... %} call. #2021
  • Fix dunder protocol (copy/pickle/etc) interaction with Undefined objects. #2025
  • Fix copy/pickle support for the internal missing object. #2027
  • Environment.overlay(enable_async) is applied correctly. #2061
  • The error message from FileSystemLoader includes the paths that were searched. #1661
  • PackageLoader shows a clearer error message when the package does not contain the templates directory. #1705
  • Improve annotations for methods returning copies. #1880
  • urlize does not add mailto: to values like @a@b. #1870
  • Tests decorated with @pass_context can be used with the |select filter. #1624
  • Using set for multiple assignment (a, b = 1, 2) does not fail when the target is a namespace attribute. #1413
  • Using set in all branches of {% if %}{% elif %}{% else %} blocks does not cause the variable to be considered initially undefined. #1253
Changelog

Sourced from jinja2's changelog.

Version 3.1.6

Released 2025-03-05

  • The |attr filter does not bypass the environment's attribute lookup, allowing the sandbox to apply its checks. :ghsa:cpwx-vrp4-4pq7

Version 3.1.5

Released 2024-12-21

  • The sandboxed environment handles indirect calls to str.format, such as by passing a stored reference to a filter that calls its argument. :ghsa:q2x7-8rv6-6q7h
  • Escape template name before formatting it into error messages, to avoid issues with names that contain f-string syntax. :issue:1792, :ghsa:gmj6-6f8f-6699
  • Sandbox does not allow clear and pop on known mutable sequence types. :issue:2032
  • Calling sync render for an async template uses asyncio.run. :pr:1952
  • Avoid unclosed auto_aiter warnings. :pr:1960
  • Return an aclose-able AsyncGenerator from Template.generate_async. :pr:1960
  • Avoid leaving root_render_func() unclosed in Template.generate_async. :pr:1960
  • Avoid leaving async generators unclosed in blocks, includes and extends. :pr:1960
  • The runtime uses the correct concat function for the current environment when calling block references. :issue:1701
  • Make |unique async-aware, allowing it to be used after another async-aware filter. :issue:1781
  • |int filter handles OverflowError from scientific notation. :issue:1921
  • Make compiling deterministic for tuple unpacking in a {% set ... %} call. :issue:2021
  • Fix dunder protocol (copy/pickle/etc) interaction with Undefined objects. :issue:2025
  • Fix copy/pickle support for the internal missing object. :issue:2027
  • Environment.overlay(enable_async) is applied correctly. :pr:2061
  • The error message from FileSystemLoader includes the paths that were searched. :issue:1661
  • PackageLoader shows a clearer error message when the package does not contain the templates directory. :issue:1705
  • Improve annotations for methods returning copies. :pr:1880
  • urlize does not add mailto: to values like @a@b. :pr:1870

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=jinja2&package-manager=pip&previous-version=3.1.4&new-version=3.1.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index e838daca8f..0b4909535a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -148,9 +148,9 @@ imagesize==1.4.1 \ --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a # via sphinx -jinja2==3.1.4 \ - --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ - --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via # myst-parser # readthedocs-sphinx-ext From 8bda670add1c490477a3ac9914405c802a087847 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 23:18:33 +0000 Subject: [PATCH 013/156] build(deps): bump absl-py from 2.1.0 to 2.2.2 in /docs (#2751) Bumps [absl-py](https://github.com/abseil/abseil-py) from 2.1.0 to 2.2.2.
Release notes

Sourced from absl-py's releases.

v2.2.2

Added

  • (testing) Added a new method absltest.TestCase.assertMappingEqual that tests equality of Mapping objects not requiring them to be dicts. Similar to assertSequenceEqual but for mappings.
  • (testing) Added a new method absltest.assertDictContainsSubset that checks that a dictionary contains a subset of keys and values. Similar to a removed method unittest.assertDictContainsSubset (existed until Python 3.11).
  • Added type annotations that are compliant with MyPy.

Changed

  • Removed support for Python 3.7.

Fixed

  • (testing) Fixed an issue where the test reporter crashes with exceptions with no string representation, starting with Python 3.11.

(The change log also includes changes in 2.2.0 and 2.2.1.)

Changelog

Sourced from absl-py's changelog.

Python Absl Changelog

All notable changes to Python Absl are recorded here.

The format is based on Keep a Changelog.

Unreleased

Nothing notable unreleased.

  • (testing) Added a new method absltest.TestCase.assertMappingEqual that tests equality of Mapping objects not requiring them to be dicts. Similar to assertSequenceEqual but for mappings.

  • (testing) Added a new method absltest.assertDictContainsSubset that checks that a dictionary contains a subset of keys and values. Similar to a removed method unittest.assertDictContainsSubset (existed until Python 3.11).

Fixed

  • (testing) Fixed an issue where the test reporter crashes with exceptions with no string representation, starting with Python 3.11.
Commits
  • 4de3812 Fixing a typo in hex regex in logging_functional_test.py
  • e889843 Exclude files and bump version to 2.2.2
  • d45bb4b Bump absl-py version to 2.2.1 to prepare for a release
  • 014aa0a Fixing the behavior of assertDictAlmostEqual
  • 57ea862 Bump absl-py version to 2.2 to prepare for a release
  • 214f0ff Changing assertMappingEqual to support arbitrary equality function. Also addi...
  • c98852f Avoid double negation in the error message for required flags.
  • f1cd92d Updating string substitution with modern f-string style in assertMappingEqual...
  • f63fe8d pytype fails to build the target in Python 3.12. suppress a misleading type w...
  • 6609299 Minor improvements of assertDictContainsSubset method.
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=absl-py&package-manager=pip&previous-version=2.1.0&new-version=2.2.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 0b4909535a..66d41a963f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,9 +2,9 @@ # bazel run //docs:requirements.update --index-url https://pypi.org/simple -absl-py==2.1.0 \ - --hash=sha256:526a04eadab8b4ee719ce68f204172ead1027549089702d99b9059f129ff1308 \ - --hash=sha256:7820790efbb316739cde8b4e19357243fc3608a152024288513dd968d7d959ff +absl-py==2.2.2 \ + --hash=sha256:bf25b2c2eed013ca456918c453d687eab4e8309fba81ee2f4c1a6aa2494175eb \ + --hash=sha256:e5797bc6abe45f64fd95dc06394ca3f2bedf3b5d895e9da691c9ee3397d70092 # via rules-python-docs (docs/pyproject.toml) alabaster==1.0.0 \ --hash=sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e \ From 23157f96117cc82adb540030e9da737b8811608d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:46:41 +0900 Subject: [PATCH 014/156] build(deps): bump charset-normalizer from 3.4.0 to 3.4.1 in /tools/publish (#2753) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [charset-normalizer](https://github.com/jawah/charset_normalizer) from 3.4.0 to 3.4.1.
Release notes

Sourced from charset-normalizer's releases.

Version 3.4.1

🚀 We're still raising awareness around HTTP/2, and HTTP/3!

Did you know that Internet Explorer 11 shipped with an optional HTTP/2 support back in 2013? also libcurl did ship it in 2014[...] Using Requests today is the rough equivalent of using EOL Windows 8! We promptly invite Python developers to look at the first drop-in replacement for Requests, namely Niquests. Ship with native WebSocket, SSE, Happy Eyeballs, DNS over HTTPS, and so on[...] All of this while remaining compatible with all Requests prior plug-ins / add-ons.

It leverages charset-normalizer in a better way! Check it out, you will gain up to being 3X faster and get a real/respectable support with it.

3.4.1 (2024-12-24)

Changed

  • Project metadata are now stored using pyproject.toml instead of setup.cfg using setuptools as the build backend.
  • Enforce annotation delayed loading for a simpler and consistent types in the project.
  • Optional mypyc compilation upgraded to version 1.14 for Python >= 3.8

Added

  • pre-commit configuration.
  • noxfile.

Removed

  • build-requirements.txt as per using pyproject.toml native build configuration.
  • bin/integration.py and bin/serve.py in favor of downstream integration test (see noxfile).
  • setup.cfg in favor of pyproject.toml metadata configuration.
  • Unused utils.range_scan function.

Fixed

  • Converting content to Unicode bytes may insert utf_8 instead of preferred utf-8. (#572)
  • Deprecation warning "'count' is passed as positional argument" when converting to Unicode bytes on Python 3.13+
Changelog

Sourced from charset-normalizer's changelog.

3.4.1 (2024-12-24)

Changed

  • Project metadata are now stored using pyproject.toml instead of setup.cfg using setuptools as the build backend.
  • Enforce annotation delayed loading for a simpler and consistent types in the project.
  • Optional mypyc compilation upgraded to version 1.14 for Python >= 3.8

Added

  • pre-commit configuration.
  • noxfile.

Removed

  • build-requirements.txt as per using pyproject.toml native build configuration.
  • bin/integration.py and bin/serve.py in favor of downstream integration test (see noxfile).
  • setup.cfg in favor of pyproject.toml metadata configuration.
  • Unused utils.range_scan function.

Fixed

  • Converting content to Unicode bytes may insert utf_8 instead of preferred utf-8. (#572)
  • Deprecation warning "'count' is passed as positional argument" when converting to Unicode bytes on Python 3.13+
Commits
  • ffdf7f5 :wrench: fix long description content-type inferred as rst instead of md
  • c7197b7 :pencil: fix changelog entries (#582)
  • c390e1f Merge pull request #581 from jawah/refresh-part-2
  • f9d6b8c :lock: add CODEOWNERS
  • 7ce1ef1 :wrench: use ubuntu-22.04 for cibuildwheel in continuous deployment workflow
  • deed205 :wrench: update LICENSE copyright
  • f11f571 :wrench: include noxfile in sdist
  • 1ec7c06 :wrench: update changelog
  • 14b4649 :bug: output(...) replace declarative mark using non iana compliant encoding ...
  • 1b06bc0 Merge branch 'refresh-part-2' of github.com:jawah/charset_normalizer into ref...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=charset-normalizer&package-manager=pip&previous-version=3.4.0&new-version=3.4.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tools/publish/requirements_darwin.txt | 199 +++++++++++------------ tools/publish/requirements_linux.txt | 199 +++++++++++------------ tools/publish/requirements_universal.txt | 199 +++++++++++------------ tools/publish/requirements_windows.txt | 199 +++++++++++------------ 4 files changed, 372 insertions(+), 424 deletions(-) diff --git a/tools/publish/requirements_darwin.txt b/tools/publish/requirements_darwin.txt index e8ee1e9b89..5f8a33c3f5 100644 --- a/tools/publish/requirements_darwin.txt +++ b/tools/publish/requirements_darwin.txt @@ -10,112 +10,99 @@ certifi==2025.1.31 \ --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe # via requests -charset-normalizer==3.4.0 \ - --hash=sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621 \ - --hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \ - --hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \ - --hash=sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912 \ - --hash=sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c \ - --hash=sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b \ - --hash=sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d \ - --hash=sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d \ - --hash=sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95 \ - --hash=sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e \ - --hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \ - --hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \ - --hash=sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab \ - --hash=sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be \ - --hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \ - --hash=sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907 \ - --hash=sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0 \ - --hash=sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2 \ - --hash=sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62 \ - --hash=sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62 \ - --hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \ - --hash=sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc \ - --hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \ - --hash=sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca \ - --hash=sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455 \ - --hash=sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858 \ - --hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \ - --hash=sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594 \ - --hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \ - --hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \ - --hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \ - --hash=sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea \ - --hash=sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6 \ - --hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \ - --hash=sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749 \ - --hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \ - --hash=sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd \ - --hash=sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99 \ - --hash=sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242 \ - --hash=sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee \ - --hash=sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129 \ - --hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \ - --hash=sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51 \ - --hash=sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee \ - --hash=sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8 \ - --hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \ - --hash=sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613 \ - --hash=sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742 \ - --hash=sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe \ - --hash=sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3 \ - --hash=sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5 \ - --hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \ - --hash=sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7 \ - --hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \ - --hash=sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c \ - --hash=sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea \ - --hash=sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417 \ - --hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \ - --hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \ - --hash=sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca \ - --hash=sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa \ - --hash=sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99 \ - --hash=sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149 \ - --hash=sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41 \ - --hash=sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574 \ - --hash=sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0 \ - --hash=sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f \ - --hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \ - --hash=sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654 \ - --hash=sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3 \ - --hash=sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19 \ - --hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \ - --hash=sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578 \ - --hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \ - --hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \ - --hash=sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51 \ - --hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \ - --hash=sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236 \ - --hash=sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a \ - --hash=sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c \ - --hash=sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade \ - --hash=sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944 \ - --hash=sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc \ - --hash=sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6 \ - --hash=sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6 \ - --hash=sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27 \ - --hash=sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6 \ - --hash=sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2 \ - --hash=sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12 \ - --hash=sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf \ - --hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \ - --hash=sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7 \ - --hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \ - --hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \ - --hash=sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b \ - --hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \ - --hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \ - --hash=sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4 \ - --hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \ - --hash=sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365 \ - --hash=sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a \ - --hash=sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748 \ - --hash=sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b \ - --hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079 \ - --hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482 +charset-normalizer==3.4.1 \ + --hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \ + --hash=sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa \ + --hash=sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a \ + --hash=sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294 \ + --hash=sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b \ + --hash=sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd \ + --hash=sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601 \ + --hash=sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd \ + --hash=sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4 \ + --hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \ + --hash=sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2 \ + --hash=sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313 \ + --hash=sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd \ + --hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \ + --hash=sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8 \ + --hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1 \ + --hash=sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2 \ + --hash=sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496 \ + --hash=sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d \ + --hash=sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b \ + --hash=sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e \ + --hash=sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a \ + --hash=sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4 \ + --hash=sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca \ + --hash=sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78 \ + --hash=sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408 \ + --hash=sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5 \ + --hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \ + --hash=sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f \ + --hash=sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a \ + --hash=sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765 \ + --hash=sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6 \ + --hash=sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146 \ + --hash=sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6 \ + --hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \ + --hash=sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd \ + --hash=sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c \ + --hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \ + --hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \ + --hash=sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176 \ + --hash=sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770 \ + --hash=sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824 \ + --hash=sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f \ + --hash=sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf \ + --hash=sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487 \ + --hash=sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d \ + --hash=sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd \ + --hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \ + --hash=sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534 \ + --hash=sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f \ + --hash=sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b \ + --hash=sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9 \ + --hash=sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd \ + --hash=sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125 \ + --hash=sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9 \ + --hash=sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de \ + --hash=sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 \ + --hash=sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d \ + --hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \ + --hash=sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f \ + --hash=sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda \ + --hash=sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7 \ + --hash=sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a \ + --hash=sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971 \ + --hash=sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8 \ + --hash=sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41 \ + --hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \ + --hash=sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f \ + --hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \ + --hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \ + --hash=sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886 \ + --hash=sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77 \ + --hash=sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76 \ + --hash=sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247 \ + --hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \ + --hash=sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb \ + --hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \ + --hash=sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e \ + --hash=sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6 \ + --hash=sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037 \ + --hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \ + --hash=sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e \ + --hash=sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807 \ + --hash=sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407 \ + --hash=sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c \ + --hash=sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12 \ + --hash=sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3 \ + --hash=sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089 \ + --hash=sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd \ + --hash=sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e \ + --hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \ + --hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616 # via requests docutils==0.21.2 \ --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ diff --git a/tools/publish/requirements_linux.txt b/tools/publish/requirements_linux.txt index 892b8b26b3..90b07d4c97 100644 --- a/tools/publish/requirements_linux.txt +++ b/tools/publish/requirements_linux.txt @@ -79,112 +79,99 @@ cffi==1.17.1 \ --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b # via cryptography -charset-normalizer==3.4.0 \ - --hash=sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621 \ - --hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \ - --hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \ - --hash=sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912 \ - --hash=sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c \ - --hash=sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b \ - --hash=sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d \ - --hash=sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d \ - --hash=sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95 \ - --hash=sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e \ - --hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \ - --hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \ - --hash=sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab \ - --hash=sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be \ - --hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \ - --hash=sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907 \ - --hash=sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0 \ - --hash=sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2 \ - --hash=sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62 \ - --hash=sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62 \ - --hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \ - --hash=sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc \ - --hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \ - --hash=sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca \ - --hash=sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455 \ - --hash=sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858 \ - --hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \ - --hash=sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594 \ - --hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \ - --hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \ - --hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \ - --hash=sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea \ - --hash=sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6 \ - --hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \ - --hash=sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749 \ - --hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \ - --hash=sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd \ - --hash=sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99 \ - --hash=sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242 \ - --hash=sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee \ - --hash=sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129 \ - --hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \ - --hash=sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51 \ - --hash=sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee \ - --hash=sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8 \ - --hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \ - --hash=sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613 \ - --hash=sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742 \ - --hash=sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe \ - --hash=sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3 \ - --hash=sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5 \ - --hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \ - --hash=sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7 \ - --hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \ - --hash=sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c \ - --hash=sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea \ - --hash=sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417 \ - --hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \ - --hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \ - --hash=sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca \ - --hash=sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa \ - --hash=sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99 \ - --hash=sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149 \ - --hash=sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41 \ - --hash=sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574 \ - --hash=sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0 \ - --hash=sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f \ - --hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \ - --hash=sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654 \ - --hash=sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3 \ - --hash=sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19 \ - --hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \ - --hash=sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578 \ - --hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \ - --hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \ - --hash=sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51 \ - --hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \ - --hash=sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236 \ - --hash=sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a \ - --hash=sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c \ - --hash=sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade \ - --hash=sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944 \ - --hash=sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc \ - --hash=sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6 \ - --hash=sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6 \ - --hash=sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27 \ - --hash=sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6 \ - --hash=sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2 \ - --hash=sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12 \ - --hash=sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf \ - --hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \ - --hash=sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7 \ - --hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \ - --hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \ - --hash=sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b \ - --hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \ - --hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \ - --hash=sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4 \ - --hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \ - --hash=sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365 \ - --hash=sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a \ - --hash=sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748 \ - --hash=sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b \ - --hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079 \ - --hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482 +charset-normalizer==3.4.1 \ + --hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \ + --hash=sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa \ + --hash=sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a \ + --hash=sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294 \ + --hash=sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b \ + --hash=sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd \ + --hash=sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601 \ + --hash=sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd \ + --hash=sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4 \ + --hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \ + --hash=sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2 \ + --hash=sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313 \ + --hash=sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd \ + --hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \ + --hash=sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8 \ + --hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1 \ + --hash=sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2 \ + --hash=sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496 \ + --hash=sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d \ + --hash=sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b \ + --hash=sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e \ + --hash=sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a \ + --hash=sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4 \ + --hash=sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca \ + --hash=sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78 \ + --hash=sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408 \ + --hash=sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5 \ + --hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \ + --hash=sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f \ + --hash=sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a \ + --hash=sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765 \ + --hash=sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6 \ + --hash=sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146 \ + --hash=sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6 \ + --hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \ + --hash=sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd \ + --hash=sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c \ + --hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \ + --hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \ + --hash=sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176 \ + --hash=sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770 \ + --hash=sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824 \ + --hash=sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f \ + --hash=sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf \ + --hash=sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487 \ + --hash=sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d \ + --hash=sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd \ + --hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \ + --hash=sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534 \ + --hash=sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f \ + --hash=sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b \ + --hash=sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9 \ + --hash=sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd \ + --hash=sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125 \ + --hash=sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9 \ + --hash=sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de \ + --hash=sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 \ + --hash=sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d \ + --hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \ + --hash=sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f \ + --hash=sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda \ + --hash=sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7 \ + --hash=sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a \ + --hash=sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971 \ + --hash=sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8 \ + --hash=sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41 \ + --hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \ + --hash=sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f \ + --hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \ + --hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \ + --hash=sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886 \ + --hash=sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77 \ + --hash=sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76 \ + --hash=sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247 \ + --hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \ + --hash=sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb \ + --hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \ + --hash=sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e \ + --hash=sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6 \ + --hash=sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037 \ + --hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \ + --hash=sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e \ + --hash=sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807 \ + --hash=sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407 \ + --hash=sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c \ + --hash=sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12 \ + --hash=sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3 \ + --hash=sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089 \ + --hash=sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd \ + --hash=sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e \ + --hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \ + --hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616 # via requests cryptography==43.0.3 \ --hash=sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362 \ diff --git a/tools/publish/requirements_universal.txt b/tools/publish/requirements_universal.txt index 337073ac25..9b145fce49 100644 --- a/tools/publish/requirements_universal.txt +++ b/tools/publish/requirements_universal.txt @@ -79,112 +79,99 @@ cffi==1.17.1 ; platform_python_implementation != 'PyPy' and sys_platform == 'lin --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b # via cryptography -charset-normalizer==3.4.0 \ - --hash=sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621 \ - --hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \ - --hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \ - --hash=sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912 \ - --hash=sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c \ - --hash=sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b \ - --hash=sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d \ - --hash=sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d \ - --hash=sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95 \ - --hash=sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e \ - --hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \ - --hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \ - --hash=sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab \ - --hash=sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be \ - --hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \ - --hash=sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907 \ - --hash=sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0 \ - --hash=sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2 \ - --hash=sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62 \ - --hash=sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62 \ - --hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \ - --hash=sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc \ - --hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \ - --hash=sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca \ - --hash=sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455 \ - --hash=sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858 \ - --hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \ - --hash=sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594 \ - --hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \ - --hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \ - --hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \ - --hash=sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea \ - --hash=sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6 \ - --hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \ - --hash=sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749 \ - --hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \ - --hash=sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd \ - --hash=sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99 \ - --hash=sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242 \ - --hash=sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee \ - --hash=sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129 \ - --hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \ - --hash=sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51 \ - --hash=sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee \ - --hash=sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8 \ - --hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \ - --hash=sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613 \ - --hash=sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742 \ - --hash=sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe \ - --hash=sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3 \ - --hash=sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5 \ - --hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \ - --hash=sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7 \ - --hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \ - --hash=sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c \ - --hash=sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea \ - --hash=sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417 \ - --hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \ - --hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \ - --hash=sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca \ - --hash=sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa \ - --hash=sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99 \ - --hash=sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149 \ - --hash=sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41 \ - --hash=sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574 \ - --hash=sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0 \ - --hash=sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f \ - --hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \ - --hash=sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654 \ - --hash=sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3 \ - --hash=sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19 \ - --hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \ - --hash=sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578 \ - --hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \ - --hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \ - --hash=sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51 \ - --hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \ - --hash=sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236 \ - --hash=sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a \ - --hash=sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c \ - --hash=sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade \ - --hash=sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944 \ - --hash=sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc \ - --hash=sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6 \ - --hash=sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6 \ - --hash=sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27 \ - --hash=sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6 \ - --hash=sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2 \ - --hash=sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12 \ - --hash=sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf \ - --hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \ - --hash=sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7 \ - --hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \ - --hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \ - --hash=sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b \ - --hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \ - --hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \ - --hash=sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4 \ - --hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \ - --hash=sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365 \ - --hash=sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a \ - --hash=sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748 \ - --hash=sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b \ - --hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079 \ - --hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482 +charset-normalizer==3.4.1 \ + --hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \ + --hash=sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa \ + --hash=sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a \ + --hash=sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294 \ + --hash=sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b \ + --hash=sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd \ + --hash=sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601 \ + --hash=sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd \ + --hash=sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4 \ + --hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \ + --hash=sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2 \ + --hash=sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313 \ + --hash=sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd \ + --hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \ + --hash=sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8 \ + --hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1 \ + --hash=sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2 \ + --hash=sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496 \ + --hash=sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d \ + --hash=sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b \ + --hash=sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e \ + --hash=sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a \ + --hash=sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4 \ + --hash=sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca \ + --hash=sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78 \ + --hash=sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408 \ + --hash=sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5 \ + --hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \ + --hash=sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f \ + --hash=sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a \ + --hash=sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765 \ + --hash=sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6 \ + --hash=sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146 \ + --hash=sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6 \ + --hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \ + --hash=sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd \ + --hash=sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c \ + --hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \ + --hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \ + --hash=sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176 \ + --hash=sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770 \ + --hash=sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824 \ + --hash=sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f \ + --hash=sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf \ + --hash=sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487 \ + --hash=sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d \ + --hash=sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd \ + --hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \ + --hash=sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534 \ + --hash=sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f \ + --hash=sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b \ + --hash=sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9 \ + --hash=sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd \ + --hash=sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125 \ + --hash=sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9 \ + --hash=sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de \ + --hash=sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 \ + --hash=sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d \ + --hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \ + --hash=sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f \ + --hash=sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda \ + --hash=sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7 \ + --hash=sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a \ + --hash=sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971 \ + --hash=sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8 \ + --hash=sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41 \ + --hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \ + --hash=sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f \ + --hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \ + --hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \ + --hash=sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886 \ + --hash=sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77 \ + --hash=sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76 \ + --hash=sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247 \ + --hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \ + --hash=sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb \ + --hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \ + --hash=sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e \ + --hash=sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6 \ + --hash=sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037 \ + --hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \ + --hash=sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e \ + --hash=sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807 \ + --hash=sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407 \ + --hash=sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c \ + --hash=sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12 \ + --hash=sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3 \ + --hash=sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089 \ + --hash=sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd \ + --hash=sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e \ + --hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \ + --hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616 # via requests cryptography==43.0.3 ; sys_platform == 'linux' \ --hash=sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362 \ diff --git a/tools/publish/requirements_windows.txt b/tools/publish/requirements_windows.txt index 1c6b9808fb..1980812d15 100644 --- a/tools/publish/requirements_windows.txt +++ b/tools/publish/requirements_windows.txt @@ -10,112 +10,99 @@ certifi==2025.1.31 \ --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe # via requests -charset-normalizer==3.4.0 \ - --hash=sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621 \ - --hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \ - --hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \ - --hash=sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912 \ - --hash=sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c \ - --hash=sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b \ - --hash=sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d \ - --hash=sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d \ - --hash=sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95 \ - --hash=sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e \ - --hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \ - --hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \ - --hash=sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab \ - --hash=sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be \ - --hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \ - --hash=sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907 \ - --hash=sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0 \ - --hash=sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2 \ - --hash=sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62 \ - --hash=sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62 \ - --hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \ - --hash=sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc \ - --hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \ - --hash=sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca \ - --hash=sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455 \ - --hash=sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858 \ - --hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \ - --hash=sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594 \ - --hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \ - --hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \ - --hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \ - --hash=sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea \ - --hash=sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6 \ - --hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \ - --hash=sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749 \ - --hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \ - --hash=sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd \ - --hash=sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99 \ - --hash=sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242 \ - --hash=sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee \ - --hash=sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129 \ - --hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \ - --hash=sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51 \ - --hash=sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee \ - --hash=sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8 \ - --hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \ - --hash=sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613 \ - --hash=sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742 \ - --hash=sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe \ - --hash=sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3 \ - --hash=sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5 \ - --hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \ - --hash=sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7 \ - --hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \ - --hash=sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c \ - --hash=sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea \ - --hash=sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417 \ - --hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \ - --hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \ - --hash=sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca \ - --hash=sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa \ - --hash=sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99 \ - --hash=sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149 \ - --hash=sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41 \ - --hash=sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574 \ - --hash=sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0 \ - --hash=sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f \ - --hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \ - --hash=sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654 \ - --hash=sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3 \ - --hash=sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19 \ - --hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \ - --hash=sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578 \ - --hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \ - --hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \ - --hash=sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51 \ - --hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \ - --hash=sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236 \ - --hash=sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a \ - --hash=sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c \ - --hash=sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade \ - --hash=sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944 \ - --hash=sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc \ - --hash=sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6 \ - --hash=sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6 \ - --hash=sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27 \ - --hash=sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6 \ - --hash=sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2 \ - --hash=sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12 \ - --hash=sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf \ - --hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \ - --hash=sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7 \ - --hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \ - --hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \ - --hash=sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b \ - --hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \ - --hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \ - --hash=sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4 \ - --hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \ - --hash=sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365 \ - --hash=sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a \ - --hash=sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748 \ - --hash=sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b \ - --hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079 \ - --hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482 +charset-normalizer==3.4.1 \ + --hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \ + --hash=sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa \ + --hash=sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a \ + --hash=sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294 \ + --hash=sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b \ + --hash=sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd \ + --hash=sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601 \ + --hash=sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd \ + --hash=sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4 \ + --hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \ + --hash=sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2 \ + --hash=sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313 \ + --hash=sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd \ + --hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \ + --hash=sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8 \ + --hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1 \ + --hash=sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2 \ + --hash=sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496 \ + --hash=sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d \ + --hash=sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b \ + --hash=sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e \ + --hash=sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a \ + --hash=sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4 \ + --hash=sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca \ + --hash=sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78 \ + --hash=sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408 \ + --hash=sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5 \ + --hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \ + --hash=sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f \ + --hash=sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a \ + --hash=sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765 \ + --hash=sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6 \ + --hash=sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146 \ + --hash=sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6 \ + --hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \ + --hash=sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd \ + --hash=sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c \ + --hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \ + --hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \ + --hash=sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176 \ + --hash=sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770 \ + --hash=sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824 \ + --hash=sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f \ + --hash=sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf \ + --hash=sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487 \ + --hash=sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d \ + --hash=sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd \ + --hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \ + --hash=sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534 \ + --hash=sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f \ + --hash=sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b \ + --hash=sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9 \ + --hash=sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd \ + --hash=sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125 \ + --hash=sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9 \ + --hash=sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de \ + --hash=sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 \ + --hash=sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d \ + --hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \ + --hash=sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f \ + --hash=sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda \ + --hash=sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7 \ + --hash=sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a \ + --hash=sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971 \ + --hash=sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8 \ + --hash=sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41 \ + --hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \ + --hash=sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f \ + --hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \ + --hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \ + --hash=sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886 \ + --hash=sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77 \ + --hash=sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76 \ + --hash=sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247 \ + --hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \ + --hash=sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb \ + --hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \ + --hash=sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e \ + --hash=sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6 \ + --hash=sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037 \ + --hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \ + --hash=sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e \ + --hash=sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807 \ + --hash=sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407 \ + --hash=sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c \ + --hash=sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12 \ + --hash=sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3 \ + --hash=sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089 \ + --hash=sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd \ + --hash=sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e \ + --hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \ + --hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616 # via requests docutils==0.21.2 \ --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ From 97637d2451647561205b10494b410f3b6edc3f83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:46:55 +0900 Subject: [PATCH 015/156] build(deps): bump charset-normalizer from 3.4.0 to 3.4.1 in /docs (#2752) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [charset-normalizer](https://github.com/jawah/charset_normalizer) from 3.4.0 to 3.4.1.
Release notes

Sourced from charset-normalizer's releases.

Version 3.4.1

🚀 We're still raising awareness around HTTP/2, and HTTP/3!

Did you know that Internet Explorer 11 shipped with an optional HTTP/2 support back in 2013? also libcurl did ship it in 2014[...] Using Requests today is the rough equivalent of using EOL Windows 8! We promptly invite Python developers to look at the first drop-in replacement for Requests, namely Niquests. Ship with native WebSocket, SSE, Happy Eyeballs, DNS over HTTPS, and so on[...] All of this while remaining compatible with all Requests prior plug-ins / add-ons.

It leverages charset-normalizer in a better way! Check it out, you will gain up to being 3X faster and get a real/respectable support with it.

3.4.1 (2024-12-24)

Changed

  • Project metadata are now stored using pyproject.toml instead of setup.cfg using setuptools as the build backend.
  • Enforce annotation delayed loading for a simpler and consistent types in the project.
  • Optional mypyc compilation upgraded to version 1.14 for Python >= 3.8

Added

  • pre-commit configuration.
  • noxfile.

Removed

  • build-requirements.txt as per using pyproject.toml native build configuration.
  • bin/integration.py and bin/serve.py in favor of downstream integration test (see noxfile).
  • setup.cfg in favor of pyproject.toml metadata configuration.
  • Unused utils.range_scan function.

Fixed

  • Converting content to Unicode bytes may insert utf_8 instead of preferred utf-8. (#572)
  • Deprecation warning "'count' is passed as positional argument" when converting to Unicode bytes on Python 3.13+
Changelog

Sourced from charset-normalizer's changelog.

3.4.1 (2024-12-24)

Changed

  • Project metadata are now stored using pyproject.toml instead of setup.cfg using setuptools as the build backend.
  • Enforce annotation delayed loading for a simpler and consistent types in the project.
  • Optional mypyc compilation upgraded to version 1.14 for Python >= 3.8

Added

  • pre-commit configuration.
  • noxfile.

Removed

  • build-requirements.txt as per using pyproject.toml native build configuration.
  • bin/integration.py and bin/serve.py in favor of downstream integration test (see noxfile).
  • setup.cfg in favor of pyproject.toml metadata configuration.
  • Unused utils.range_scan function.

Fixed

  • Converting content to Unicode bytes may insert utf_8 instead of preferred utf-8. (#572)
  • Deprecation warning "'count' is passed as positional argument" when converting to Unicode bytes on Python 3.13+
Commits
  • ffdf7f5 :wrench: fix long description content-type inferred as rst instead of md
  • c7197b7 :pencil: fix changelog entries (#582)
  • c390e1f Merge pull request #581 from jawah/refresh-part-2
  • f9d6b8c :lock: add CODEOWNERS
  • 7ce1ef1 :wrench: use ubuntu-22.04 for cibuildwheel in continuous deployment workflow
  • deed205 :wrench: update LICENSE copyright
  • f11f571 :wrench: include noxfile in sdist
  • 1ec7c06 :wrench: update changelog
  • 14b4649 :bug: output(...) replace declarative mark using non iana compliant encoding ...
  • 1b06bc0 Merge branch 'refresh-part-2' of github.com:jawah/charset_normalizer into ref...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=charset-normalizer&package-manager=pip&previous-version=3.4.0&new-version=3.4.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 199 ++++++++++++++++++++---------------------- 1 file changed, 93 insertions(+), 106 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 66d41a963f..8d1cbabffc 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -22,112 +22,99 @@ certifi==2025.1.31 \ --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe # via requests -charset-normalizer==3.4.0 \ - --hash=sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621 \ - --hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \ - --hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \ - --hash=sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912 \ - --hash=sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c \ - --hash=sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b \ - --hash=sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d \ - --hash=sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d \ - --hash=sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95 \ - --hash=sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e \ - --hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \ - --hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \ - --hash=sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab \ - --hash=sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be \ - --hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \ - --hash=sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907 \ - --hash=sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0 \ - --hash=sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2 \ - --hash=sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62 \ - --hash=sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62 \ - --hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \ - --hash=sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc \ - --hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \ - --hash=sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca \ - --hash=sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455 \ - --hash=sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858 \ - --hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \ - --hash=sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594 \ - --hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \ - --hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \ - --hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \ - --hash=sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea \ - --hash=sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6 \ - --hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \ - --hash=sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749 \ - --hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \ - --hash=sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd \ - --hash=sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99 \ - --hash=sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242 \ - --hash=sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee \ - --hash=sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129 \ - --hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \ - --hash=sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51 \ - --hash=sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee \ - --hash=sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8 \ - --hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \ - --hash=sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613 \ - --hash=sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742 \ - --hash=sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe \ - --hash=sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3 \ - --hash=sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5 \ - --hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \ - --hash=sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7 \ - --hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \ - --hash=sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c \ - --hash=sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea \ - --hash=sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417 \ - --hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \ - --hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \ - --hash=sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca \ - --hash=sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa \ - --hash=sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99 \ - --hash=sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149 \ - --hash=sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41 \ - --hash=sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574 \ - --hash=sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0 \ - --hash=sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f \ - --hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \ - --hash=sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654 \ - --hash=sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3 \ - --hash=sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19 \ - --hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \ - --hash=sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578 \ - --hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \ - --hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \ - --hash=sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51 \ - --hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \ - --hash=sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236 \ - --hash=sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a \ - --hash=sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c \ - --hash=sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade \ - --hash=sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944 \ - --hash=sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc \ - --hash=sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6 \ - --hash=sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6 \ - --hash=sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27 \ - --hash=sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6 \ - --hash=sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2 \ - --hash=sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12 \ - --hash=sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf \ - --hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \ - --hash=sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7 \ - --hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \ - --hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \ - --hash=sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b \ - --hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \ - --hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \ - --hash=sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4 \ - --hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \ - --hash=sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365 \ - --hash=sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a \ - --hash=sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748 \ - --hash=sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b \ - --hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079 \ - --hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482 +charset-normalizer==3.4.1 \ + --hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \ + --hash=sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa \ + --hash=sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a \ + --hash=sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294 \ + --hash=sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b \ + --hash=sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd \ + --hash=sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601 \ + --hash=sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd \ + --hash=sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4 \ + --hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \ + --hash=sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2 \ + --hash=sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313 \ + --hash=sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd \ + --hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \ + --hash=sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8 \ + --hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1 \ + --hash=sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2 \ + --hash=sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496 \ + --hash=sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d \ + --hash=sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b \ + --hash=sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e \ + --hash=sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a \ + --hash=sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4 \ + --hash=sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca \ + --hash=sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78 \ + --hash=sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408 \ + --hash=sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5 \ + --hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \ + --hash=sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f \ + --hash=sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a \ + --hash=sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765 \ + --hash=sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6 \ + --hash=sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146 \ + --hash=sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6 \ + --hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \ + --hash=sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd \ + --hash=sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c \ + --hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \ + --hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \ + --hash=sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176 \ + --hash=sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770 \ + --hash=sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824 \ + --hash=sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f \ + --hash=sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf \ + --hash=sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487 \ + --hash=sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d \ + --hash=sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd \ + --hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \ + --hash=sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534 \ + --hash=sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f \ + --hash=sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b \ + --hash=sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9 \ + --hash=sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd \ + --hash=sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125 \ + --hash=sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9 \ + --hash=sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de \ + --hash=sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 \ + --hash=sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d \ + --hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \ + --hash=sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f \ + --hash=sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda \ + --hash=sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7 \ + --hash=sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a \ + --hash=sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971 \ + --hash=sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8 \ + --hash=sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41 \ + --hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \ + --hash=sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f \ + --hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \ + --hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \ + --hash=sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886 \ + --hash=sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77 \ + --hash=sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76 \ + --hash=sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247 \ + --hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \ + --hash=sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb \ + --hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \ + --hash=sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e \ + --hash=sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6 \ + --hash=sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037 \ + --hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \ + --hash=sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e \ + --hash=sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807 \ + --hash=sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407 \ + --hash=sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c \ + --hash=sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12 \ + --hash=sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3 \ + --hash=sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089 \ + --hash=sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd \ + --hash=sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e \ + --hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \ + --hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616 # via requests colorama==0.4.6 ; sys_platform == 'win32' \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ From 2a710f07c2eafd5c6d32d4721ee4403a34769361 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:47:08 +0900 Subject: [PATCH 016/156] build(deps): bump jinja2 from 3.1.4 to 3.1.6 in /examples/pip_parse (#2754) Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.4 to 3.1.6.
Release notes

Sourced from jinja2's releases.

3.1.6

This is the Jinja 3.1.6 security release, which fixes security issues but does not otherwise change behavior and should not result in breaking changes compared to the latest feature release.

PyPI: https://pypi.org/project/Jinja2/3.1.6/ Changes: https://jinja.palletsprojects.com/en/stable/changes/#version-3-1-6

3.1.5

This is the Jinja 3.1.5 security fix release, which fixes security issues and bugs but does not otherwise change behavior and should not result in breaking changes compared to the latest feature release.

PyPI: https://pypi.org/project/Jinja2/3.1.5/ Changes: https://jinja.palletsprojects.com/changes/#version-3-1-5 Milestone: https://github.com/pallets/jinja/milestone/16?closed=1

  • The sandboxed environment handles indirect calls to str.format, such as by passing a stored reference to a filter that calls its argument. GHSA-q2x7-8rv6-6q7h
  • Escape template name before formatting it into error messages, to avoid issues with names that contain f-string syntax. #1792, GHSA-gmj6-6f8f-6699
  • Sandbox does not allow clear and pop on known mutable sequence types. #2032
  • Calling sync render for an async template uses asyncio.run. #1952
  • Avoid unclosed auto_aiter warnings. #1960
  • Return an aclose-able AsyncGenerator from Template.generate_async. #1960
  • Avoid leaving root_render_func() unclosed in Template.generate_async. #1960
  • Avoid leaving async generators unclosed in blocks, includes and extends. #1960
  • The runtime uses the correct concat function for the current environment when calling block references. #1701
  • Make |unique async-aware, allowing it to be used after another async-aware filter. #1781
  • |int filter handles OverflowError from scientific notation. #1921
  • Make compiling deterministic for tuple unpacking in a {% set ... %} call. #2021
  • Fix dunder protocol (copy/pickle/etc) interaction with Undefined objects. #2025
  • Fix copy/pickle support for the internal missing object. #2027
  • Environment.overlay(enable_async) is applied correctly. #2061
  • The error message from FileSystemLoader includes the paths that were searched. #1661
  • PackageLoader shows a clearer error message when the package does not contain the templates directory. #1705
  • Improve annotations for methods returning copies. #1880
  • urlize does not add mailto: to values like @a@b. #1870
  • Tests decorated with @pass_context can be used with the |select filter. #1624
  • Using set for multiple assignment (a, b = 1, 2) does not fail when the target is a namespace attribute. #1413
  • Using set in all branches of {% if %}{% elif %}{% else %} blocks does not cause the variable to be considered initially undefined. #1253
Changelog

Sourced from jinja2's changelog.

Version 3.1.6

Released 2025-03-05

  • The |attr filter does not bypass the environment's attribute lookup, allowing the sandbox to apply its checks. :ghsa:cpwx-vrp4-4pq7

Version 3.1.5

Released 2024-12-21

  • The sandboxed environment handles indirect calls to str.format, such as by passing a stored reference to a filter that calls its argument. :ghsa:q2x7-8rv6-6q7h
  • Escape template name before formatting it into error messages, to avoid issues with names that contain f-string syntax. :issue:1792, :ghsa:gmj6-6f8f-6699
  • Sandbox does not allow clear and pop on known mutable sequence types. :issue:2032
  • Calling sync render for an async template uses asyncio.run. :pr:1952
  • Avoid unclosed auto_aiter warnings. :pr:1960
  • Return an aclose-able AsyncGenerator from Template.generate_async. :pr:1960
  • Avoid leaving root_render_func() unclosed in Template.generate_async. :pr:1960
  • Avoid leaving async generators unclosed in blocks, includes and extends. :pr:1960
  • The runtime uses the correct concat function for the current environment when calling block references. :issue:1701
  • Make |unique async-aware, allowing it to be used after another async-aware filter. :issue:1781
  • |int filter handles OverflowError from scientific notation. :issue:1921
  • Make compiling deterministic for tuple unpacking in a {% set ... %} call. :issue:2021
  • Fix dunder protocol (copy/pickle/etc) interaction with Undefined objects. :issue:2025
  • Fix copy/pickle support for the internal missing object. :issue:2027
  • Environment.overlay(enable_async) is applied correctly. :pr:2061
  • The error message from FileSystemLoader includes the paths that were searched. :issue:1661
  • PackageLoader shows a clearer error message when the package does not contain the templates directory. :issue:1705
  • Improve annotations for methods returning copies. :pr:1880
  • urlize does not add mailto: to values like @a@b. :pr:1870

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=jinja2&package-manager=pip&previous-version=3.1.4&new-version=3.1.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/bazel-contrib/rules_python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/pip_parse/requirements_lock.txt | 6 +++--- examples/pip_parse/requirements_windows.txt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/pip_parse/requirements_lock.txt b/examples/pip_parse/requirements_lock.txt index 5e7a198c38..aeac61eff9 100644 --- a/examples/pip_parse/requirements_lock.txt +++ b/examples/pip_parse/requirements_lock.txt @@ -36,9 +36,9 @@ importlib-metadata==6.8.0 \ --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743 # via sphinx -jinja2==3.1.4 \ - --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ - --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via sphinx markupsafe==2.1.3 \ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ diff --git a/examples/pip_parse/requirements_windows.txt b/examples/pip_parse/requirements_windows.txt index 4b1969255a..61a6682047 100644 --- a/examples/pip_parse/requirements_windows.txt +++ b/examples/pip_parse/requirements_windows.txt @@ -40,9 +40,9 @@ importlib-metadata==6.8.0 \ --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743 # via sphinx -jinja2==3.1.4 \ - --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ - --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via sphinx markupsafe==2.1.3 \ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ From 6821709d7c79e9a1156287d06522de674e5c376d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 02:21:43 +0000 Subject: [PATCH 017/156] build(deps): bump cryptography from 43.0.3 to 44.0.1 in /tools/publish (#2756) Bumps [cryptography](https://github.com/pyca/cryptography) from 43.0.3 to 44.0.1.
Changelog

Sourced from cryptography's changelog.

44.0.1 - 2025-02-11


* Updated Windows, macOS, and Linux wheels to be compiled with OpenSSL
3.4.1.
* We now build ``armv7l`` ``manylinux`` wheels and publish them to PyPI.
* We now build ``manylinux_2_34`` wheels and publish them to PyPI.

.. _v44-0-0:

44.0.0 - 2024-11-27

  • BACKWARDS INCOMPATIBLE: Dropped support for LibreSSL < 3.9.
  • Deprecated Python 3.7 support. Python 3.7 is no longer supported by the Python core team. Support for Python 3.7 will be removed in a future cryptography release.
  • Updated Windows, macOS, and Linux wheels to be compiled with OpenSSL 3.4.0.
  • macOS wheels are now built against the macOS 10.13 SDK. Users on older versions of macOS should upgrade, or they will need to build cryptography themselves.
  • Enforce the :rfc:5280 requirement that extended key usage extensions must not be empty.
  • Added support for timestamp extraction to the :class:~cryptography.fernet.MultiFernet class.
  • Relax the Authority Key Identifier requirements on root CA certificates during X.509 verification to allow fields permitted by :rfc:5280 but forbidden by the CA/Browser BRs.
  • Added support for :class:~cryptography.hazmat.primitives.kdf.argon2.Argon2id when using OpenSSL 3.2.0+.
  • Added support for the :class:~cryptography.x509.Admissions certificate extension.
  • Added basic support for PKCS7 decryption (including S/MIME 3.2) via :func:~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_decrypt_der, :func:~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_decrypt_pem, and :func:~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_decrypt_smime.

.. _v43-0-3:

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cryptography&package-manager=pip&previous-version=43.0.3&new-version=44.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/bazel-contrib/rules_python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tools/publish/requirements_linux.txt | 60 +++++++++++++----------- tools/publish/requirements_universal.txt | 60 +++++++++++++----------- 2 files changed, 64 insertions(+), 56 deletions(-) diff --git a/tools/publish/requirements_linux.txt b/tools/publish/requirements_linux.txt index 90b07d4c97..40d987b16d 100644 --- a/tools/publish/requirements_linux.txt +++ b/tools/publish/requirements_linux.txt @@ -173,34 +173,38 @@ charset-normalizer==3.4.1 \ --hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \ --hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616 # via requests -cryptography==43.0.3 \ - --hash=sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362 \ - --hash=sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4 \ - --hash=sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa \ - --hash=sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83 \ - --hash=sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff \ - --hash=sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805 \ - --hash=sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6 \ - --hash=sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664 \ - --hash=sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08 \ - --hash=sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e \ - --hash=sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18 \ - --hash=sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f \ - --hash=sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73 \ - --hash=sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5 \ - --hash=sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984 \ - --hash=sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd \ - --hash=sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3 \ - --hash=sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e \ - --hash=sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405 \ - --hash=sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2 \ - --hash=sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c \ - --hash=sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995 \ - --hash=sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73 \ - --hash=sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16 \ - --hash=sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7 \ - --hash=sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd \ - --hash=sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7 +cryptography==44.0.1 \ + --hash=sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7 \ + --hash=sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3 \ + --hash=sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183 \ + --hash=sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69 \ + --hash=sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a \ + --hash=sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62 \ + --hash=sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911 \ + --hash=sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7 \ + --hash=sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a \ + --hash=sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41 \ + --hash=sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83 \ + --hash=sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12 \ + --hash=sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864 \ + --hash=sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf \ + --hash=sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c \ + --hash=sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2 \ + --hash=sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b \ + --hash=sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0 \ + --hash=sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4 \ + --hash=sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9 \ + --hash=sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008 \ + --hash=sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862 \ + --hash=sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009 \ + --hash=sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7 \ + --hash=sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f \ + --hash=sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026 \ + --hash=sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f \ + --hash=sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd \ + --hash=sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420 \ + --hash=sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14 \ + --hash=sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00 # via secretstorage docutils==0.21.2 \ --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ diff --git a/tools/publish/requirements_universal.txt b/tools/publish/requirements_universal.txt index 9b145fce49..c8bc0bb258 100644 --- a/tools/publish/requirements_universal.txt +++ b/tools/publish/requirements_universal.txt @@ -173,34 +173,38 @@ charset-normalizer==3.4.1 \ --hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \ --hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616 # via requests -cryptography==43.0.3 ; sys_platform == 'linux' \ - --hash=sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362 \ - --hash=sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4 \ - --hash=sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa \ - --hash=sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83 \ - --hash=sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff \ - --hash=sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805 \ - --hash=sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6 \ - --hash=sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664 \ - --hash=sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08 \ - --hash=sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e \ - --hash=sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18 \ - --hash=sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f \ - --hash=sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73 \ - --hash=sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5 \ - --hash=sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984 \ - --hash=sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd \ - --hash=sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3 \ - --hash=sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e \ - --hash=sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405 \ - --hash=sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2 \ - --hash=sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c \ - --hash=sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995 \ - --hash=sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73 \ - --hash=sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16 \ - --hash=sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7 \ - --hash=sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd \ - --hash=sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7 +cryptography==44.0.1 ; sys_platform == 'linux' \ + --hash=sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7 \ + --hash=sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3 \ + --hash=sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183 \ + --hash=sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69 \ + --hash=sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a \ + --hash=sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62 \ + --hash=sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911 \ + --hash=sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7 \ + --hash=sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a \ + --hash=sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41 \ + --hash=sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83 \ + --hash=sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12 \ + --hash=sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864 \ + --hash=sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf \ + --hash=sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c \ + --hash=sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2 \ + --hash=sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b \ + --hash=sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0 \ + --hash=sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4 \ + --hash=sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9 \ + --hash=sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008 \ + --hash=sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862 \ + --hash=sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009 \ + --hash=sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7 \ + --hash=sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f \ + --hash=sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026 \ + --hash=sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f \ + --hash=sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd \ + --hash=sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420 \ + --hash=sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14 \ + --hash=sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00 # via secretstorage docutils==0.21.2 \ --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ From 34e433b75373aa9ad5645f370a0e0a4025e328da Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 8 Apr 2025 22:43:06 -0700 Subject: [PATCH 018/156] feat(toolchains): create toolchains from locally installed python (#2742) This adds docs and public APIs for using a locally installed python for a toolchain. Work towards https://github.com/bazel-contrib/rules_python/issues/2070 --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- CHANGELOG.md | 4 + docs/BUILD.bazel | 1 + docs/toolchains.md | 97 ++++++++++++++++++- python/BUILD.bazel | 1 + python/local_toolchains/BUILD.bazel | 18 ++++ python/local_toolchains/repos.bzl | 18 ++++ python/private/BUILD.bazel | 18 ++++ .../integration/local_toolchains/MODULE.bazel | 4 +- 8 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 python/local_toolchains/BUILD.bazel create mode 100644 python/local_toolchains/repos.bzl diff --git a/CHANGELOG.md b/CHANGELOG.md index abe718c389..7aeb135788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,10 @@ Unreleased changes template. allow specifying links to create within the venv site packages (only applicable with {obj}`--bootstrap_impl=script`) ([#2156](https://github.com/bazelbuild/rules_python/issues/2156)). +* (toolchains) Local Python installs can be used to create a toolchain + equivalent to the standard toolchains. See [Local toolchains] docs for how to + configure them. + {#v0-0-0-removed} ### Removed diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index 29eac6e714..25da682012 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -108,6 +108,7 @@ sphinx_stardocs( "//python/cc:py_cc_toolchain_bzl", "//python/cc:py_cc_toolchain_info_bzl", "//python/entry_points:py_console_script_binary_bzl", + "//python/local_toolchains:repos_bzl", "//python/private:attr_builders_bzl", "//python/private:builders_util_bzl", "//python/private:py_binary_rule_bzl", diff --git a/docs/toolchains.md b/docs/toolchains.md index 73a8a48121..5cd9eb268e 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -199,10 +199,10 @@ Remember to call `use_repo()` to make repos visible to your module: :::{deprecated} 1.1.0 -The toolchain specific `py_binary` and `py_test` symbols are aliases to the regular rules. +The toolchain specific `py_binary` and `py_test` symbols are aliases to the regular rules. i.e. Deprecated `load("@python_versions//3.11:defs.bzl", "py_binary")` & `load("@python_versions//3.11:defs.bzl", "py_test")` -Usages of them should be changed to load the regular rules directly; +Usages of them should be changed to load the regular rules directly; i.e. Use `load("@rules_python//python:py_binary.bzl", "py_binary")` & `load("@rules_python//python:py_test.bzl", "py_test")` and then specify the `python_version` when using the rules corresponding to the python version you defined in your toolchain. {ref}`Library modules with version constraints` ::: @@ -327,7 +327,97 @@ After registration, your Python targets will use the toolchain's interpreter dur is still used to 'bootstrap' Python targets (see https://github.com/bazel-contrib/rules_python/issues/691). You may also find some quirks while using this toolchain. Please refer to [python-build-standalone documentation's _Quirks_ section](https://gregoryszorc.com/docs/python-build-standalone/main/quirks.html). -## Autodetecting toolchain +## Local toolchain + +It's possible to use a locally installed Python runtime instead of the regular +prebuilt, remotely downloaded ones. A local toolchain contains the Python +runtime metadata (Python version, headers, ABI flags, etc) that the regular +remotely downloaded runtimes contain, which makes it possible to build e.g. C +extensions (unlike the autodetecting and runtime environment toolchains). + +For simple cases, some rules are provided that will introspect +a Python installation and create an appropriate Bazel definition from +it. To do this, three pieces need to be wired together: + +1. Specify a path or command to a Python interpreter (multiple can be defined). +2. Create toolchains for the runtimes in (1) +3. Register the toolchains created by (2) + +The below is an example that will use `python3` from PATH to find the +interpreter, then introspect its installation to generate a full toolchain. + +```starlark +# File: MODULE.bazel + +local_runtime_repo = use_repo_rule( + "@rules_python//python/local_toolchains:repos.bzl", + "local_runtime_repo", + dev_dependency = True, +) + +local_runtime_toolchains_repo = use_repo_rule( + "@rules_python//python/local_toolchains:repos.bzl" + "local_runtime_toolchains_repo" + dev_dependency = True, +) + +# Step 1: Define the Python runtime +local_runtime_repo( + name = "local_python3", + interpreter_path = "python3", + on_failure = "fail", +) + +# Step 2: Create toolchains for the runtimes +local_runtime_toolchains_repo( + name = "local_toolchains", + runtimes = ["local_python3"], +) + +# Step 3: Register the toolchains +register_toolchains("@local_toolchains//:all", dev_dependency = True) +``` + +Note that `register_toolchains` will insert the local toolchain earlier in the +toolchain ordering, so it will take precedence over other registered toolchains. + +:::{important} +Be sure to set `dev_dependency = True`. Using a local toolchain only makes sense +for the root module. + +If an intermediate module does it, then the `register_toolchains()` call will +take precedence over the default rules_python toolchains and cause problems for +downstream modules. +::: + +Multiple runtimes and/or toolchains can be defined, which allows for multiple +Python versions and/or platforms to be configured in a single `MODULE.bazel`. + +## Runtime environment toolchain + +The runtime environment toolchain is a minimal toolchain that doesn't provide +information about Python at build time. In particular, this means it is not able +to build C extensions -- doing so requires knowing, at build time, what Python +headers to use. + +In effect, all it does is generate a small wrapper script that simply calls e.g. +`/usr/bin/env python3` to run a program. This makes it easy to change what +Python is used to run a program, but also makes it easy to use a Python version +that isn't compatible with build-time assumptions. + +``` +register_toolchains("@rules_python//python/runtime_env_toolchains:all") +``` + +Note that this toolchain has no constraints, i.e. it will match any platform, +Python version, etc. + +:::{seealso} +[Local toolchain], which creates a more full featured toolchain from a +locally installed Python. +::: + +### Autodetecting toolchain The autodetecting toolchain is a deprecated toolchain that is built into Bazel. It's name is a bit misleading: it doesn't autodetect anything. All it does is @@ -345,7 +435,6 @@ To aid migration off the Bazel-builtin toolchain, rules_python provides {bzl:obj}`@rules_python//python/runtime_env_toolchains:all`. This is an equivalent toolchain, but is implemented using rules_python's objects. - ## Custom toolchains While rules_python provides toolchains by default, it is not required to use diff --git a/python/BUILD.bazel b/python/BUILD.bazel index a699c81cc4..3389a0dacc 100644 --- a/python/BUILD.bazel +++ b/python/BUILD.bazel @@ -41,6 +41,7 @@ filegroup( "//python/constraints:distribution", "//python/entry_points:distribution", "//python/extensions:distribution", + "//python/local_toolchains:distribution", "//python/pip_install:distribution", "//python/private:distribution", "//python/runfiles:distribution", diff --git a/python/local_toolchains/BUILD.bazel b/python/local_toolchains/BUILD.bazel new file mode 100644 index 0000000000..211f3e21a7 --- /dev/null +++ b/python/local_toolchains/BUILD.bazel @@ -0,0 +1,18 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +package(default_visibility = ["//:__subpackages__"]) + +bzl_library( + name = "repos_bzl", + srcs = ["repos.bzl"], + visibility = ["//visibility:public"], + deps = [ + "//python/private:local_runtime_repo_bzl", + "//python/private:local_runtime_toolchains_repo_bzl", + ], +) + +filegroup( + name = "distribution", + srcs = glob(["**"]), +) diff --git a/python/local_toolchains/repos.bzl b/python/local_toolchains/repos.bzl new file mode 100644 index 0000000000..d1b45cfd7f --- /dev/null +++ b/python/local_toolchains/repos.bzl @@ -0,0 +1,18 @@ +"""Rules/macros for repository phase for local toolchains. + +:::{versionadded} VERSION_NEXT_FEATURE +::: +""" + +load( + "@rules_python//python/private:local_runtime_repo.bzl", + _local_runtime_repo = "local_runtime_repo", +) +load( + "@rules_python//python/private:local_runtime_toolchains_repo.bzl", + _local_runtime_toolchains_repo = "local_runtime_toolchains_repo", +) + +local_runtime_repo = _local_runtime_repo + +local_runtime_toolchains_repo = _local_runtime_toolchains_repo diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index ef4580e1ce..b63f446be3 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -205,6 +205,24 @@ bzl_library( ], ) +bzl_library( + name = "local_runtime_repo_bzl", + srcs = ["local_runtime_repo.bzl"], + deps = [ + ":enum_bzl", + ":repo_utils.bzl", + ], +) + +bzl_library( + name = "local_runtime_toolchains_repo_bzl", + srcs = ["local_runtime_toolchains_repo.bzl"], + deps = [ + ":repo_utils.bzl", + ":text_util_bzl", + ], +) + bzl_library( name = "normalize_name_bzl", srcs = ["normalize_name.bzl"], diff --git a/tests/integration/local_toolchains/MODULE.bazel b/tests/integration/local_toolchains/MODULE.bazel index d4ef12e952..98f1ed9ac4 100644 --- a/tests/integration/local_toolchains/MODULE.bazel +++ b/tests/integration/local_toolchains/MODULE.bazel @@ -19,9 +19,9 @@ local_path_override( path = "../../..", ) -local_runtime_repo = use_repo_rule("@rules_python//python/private:local_runtime_repo.bzl", "local_runtime_repo") +local_runtime_repo = use_repo_rule("@rules_python//python/local_toolchains:repos.bzl", "local_runtime_repo") -local_runtime_toolchains_repo = use_repo_rule("@rules_python//python/private:local_runtime_toolchains_repo.bzl", "local_runtime_toolchains_repo") +local_runtime_toolchains_repo = use_repo_rule("@rules_python//python/local_toolchains:repos.bzl", "local_runtime_toolchains_repo") local_runtime_repo( name = "local_python3", From 9fb13ec1af33ecc9da8beb7dcea7bb25b4dbc241 Mon Sep 17 00:00:00 2001 From: Matt Mackay Date: Wed, 9 Apr 2025 08:37:57 -0400 Subject: [PATCH 019/156] fix: run python version call in isolated mode (#2761) Similar to https://github.com/bazel-contrib/rules_python/pull/2738, runs the call to get the Python interpreter version in isolated mode via `-I`, ensuring userland Python variables do not affect this call. --- CHANGELOG.md | 1 + python/private/pypi/whl_library.bzl | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aeb135788..f38732f7d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,7 @@ Unreleased changes template. Fixes [#2685](https://github.com/bazel-contrib/rules_python/issues/2685). * (toolchains) Run the check on the Python interpreter in isolated mode, to ensure it's not affected by userland environment variables, such as `PYTHONPATH`. * (toolchains) Ensure temporary `.pyc` and `.pyo` files are also excluded from the interpreters repository files. +* (pypi) Run interpreter version call in isolated mode, to ensure it's not affected by userland environment variables, such as `PYTHONPATH`. {#v0-0-0-added} ### Added diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 2904f85f1b..493f11353e 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -109,6 +109,10 @@ def _get_toolchain_unix_cflags(rctx, python_interpreter, logger = None): op = "GetPythonVersionForUnixCflags", python = python_interpreter, arguments = [ + # Run the interpreter in isolated mode, this options implies -E, -P and -s. + # Ensures environment variables are ignored that are set in userspace, such as PYTHONPATH, + # which may interfere with this invocation. + "-I", "-c", "import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}', end='')", ], From 55d68369e37da847ee8ac2be0358ef4969f1b194 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 11 Apr 2025 03:43:17 +0900 Subject: [PATCH 020/156] fix(pypi): fixes to the marker evaluation and utils (#2767) These are just bugfixes to already merged code: * Fix nested bracket parsing in PEP508 marker parser. * Fix the sys_platform constants, which I noticed in #2629 but they got also pointed out in #2766. * Port some of python tests for requirement parsing and improve the implementation. Those tests will be removed in #2629. * Move the platform related code to a separate file. * Rename `pep508_req.bzl` to `pep508_requirement.bzl` to follow the convention. All of the bug fixes have added tests. Work towards #2423. --- python/private/pypi/BUILD.bazel | 15 ++++- python/private/pypi/evaluate_markers.bzl | 9 +-- python/private/pypi/pep508_env.bzl | 63 ++++++++++--------- python/private/pypi/pep508_platform.bzl | 57 +++++++++++++++++ ...{pep508_req.bzl => pep508_requirement.bzl} | 9 ++- tests/pypi/pep508/BUILD.bazel | 5 ++ tests/pypi/pep508/requirement_tests.bzl | 47 ++++++++++++++ 7 files changed, 165 insertions(+), 40 deletions(-) create mode 100644 python/private/pypi/pep508_platform.bzl rename python/private/pypi/{pep508_req.bzl => pep508_requirement.bzl} (82%) create mode 100644 tests/pypi/pep508/requirement_tests.bzl diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 21e05f2895..e0a2f20c14 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -77,7 +77,8 @@ bzl_library( deps = [ ":pep508_env_bzl", ":pep508_evaluate_bzl", - ":pep508_req_bzl", + ":pep508_platform_bzl", + ":pep508_requirement_bzl", ], ) @@ -223,6 +224,9 @@ bzl_library( bzl_library( name = "pep508_env_bzl", srcs = ["pep508_env.bzl"], + deps = [ + ":pep508_platform_bzl", + ], ) bzl_library( @@ -235,8 +239,13 @@ bzl_library( ) bzl_library( - name = "pep508_req_bzl", - srcs = ["pep508_req.bzl"], + name = "pep508_platform_bzl", + srcs = ["pep508_platform.bzl"], +) + +bzl_library( + name = "pep508_requirement_bzl", + srcs = ["pep508_requirement.bzl"], deps = [ "//python/private:normalize_name_bzl", ], diff --git a/python/private/pypi/evaluate_markers.bzl b/python/private/pypi/evaluate_markers.bzl index 1d4c30753f..a0223abdc8 100644 --- a/python/private/pypi/evaluate_markers.bzl +++ b/python/private/pypi/evaluate_markers.bzl @@ -14,9 +14,10 @@ """A simple function that evaluates markers using a python interpreter.""" -load(":pep508_env.bzl", "env", _platform_from_str = "platform_from_str") +load(":pep508_env.bzl", "env") load(":pep508_evaluate.bzl", "evaluate") -load(":pep508_req.bzl", _req = "requirement") +load(":pep508_platform.bzl", "platform_from_str") +load(":pep508_requirement.bzl", "requirement") def evaluate_markers(requirements): """Return the list of supported platforms per requirements line. @@ -29,9 +30,9 @@ def evaluate_markers(requirements): """ ret = {} for req_string, platforms in requirements.items(): - req = _req(req_string) + req = requirement(req_string) for platform in platforms: - if evaluate(req.marker, env = env(_platform_from_str(platform, None))): + if evaluate(req.marker, env = env(platform_from_str(platform, None))): ret.setdefault(req_string, []).append(platform) return ret diff --git a/python/private/pypi/pep508_env.bzl b/python/private/pypi/pep508_env.bzl index 17d41871d1..265a8e9b99 100644 --- a/python/private/pypi/pep508_env.bzl +++ b/python/private/pypi/pep508_env.bzl @@ -15,7 +15,9 @@ """This module is for implementing PEP508 environment definition. """ -# See https://stackoverflow.com/questions/45125516/possible-values-for-uname-m +load(":pep508_platform.bzl", "platform_from_str") + +# See https://stackoverflow.com/a/45125525 _platform_machine_aliases = { # These pairs mean the same hardware, but different values may be used # on different host platforms. @@ -24,13 +26,41 @@ _platform_machine_aliases = { "i386": "x86_32", "i686": "x86_32", } + +# Platform system returns results from the `uname` call. _platform_system_values = { "linux": "Linux", "osx": "Darwin", "windows": "Windows", } + +# The copy of SO [answer](https://stackoverflow.com/a/13874620) containing +# all of the platforms: +# ┍━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━┑ +# │ System │ Value │ +# ┝━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━┥ +# │ Linux │ linux or linux2 (*) │ +# │ Windows │ win32 │ +# │ Windows/Cygwin │ cygwin │ +# │ Windows/MSYS2 │ msys │ +# │ Mac OS X │ darwin │ +# │ OS/2 │ os2 │ +# │ OS/2 EMX │ os2emx │ +# │ RiscOS │ riscos │ +# │ AtheOS │ atheos │ +# │ FreeBSD 7 │ freebsd7 │ +# │ FreeBSD 8 │ freebsd8 │ +# │ FreeBSD N │ freebsdN │ +# │ OpenBSD 6 │ openbsd6 │ +# │ AIX │ aix (**) │ +# ┕━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━┙ +# +# (*) Prior to Python 3.3, the value for any Linux version is always linux2; after, it is linux. +# (**) Prior Python 3.8 could also be aix5 or aix7; use sys.platform.startswith() +# +# We are using only the subset that we actually support. _sys_platform_values = { - "linux": "posix", + "linux": "linux", "osx": "darwin", "windows": "win32", } @@ -61,6 +91,7 @@ def env(target_platform, *, extra = None): "platform_release": "", "platform_version": "", } + if type(target_platform) == type(""): target_platform = platform_from_str(target_platform, python_version = "") @@ -87,31 +118,3 @@ def env(target_platform, *, extra = None): "platform_machine": _platform_machine_aliases, }, } - -def _platform(*, abi = None, os = None, arch = None): - return struct( - abi = abi, - os = os, - arch = arch, - ) - -def platform_from_str(p, python_version): - """Return a platform from a string. - - Args: - p: {type}`str` the actual string. - python_version: {type}`str` the python version to add to platform if needed. - - Returns: - A struct that is returned by the `_platform` function. - """ - if p.startswith("cp"): - abi, _, p = p.partition("_") - elif python_version: - major, _, tail = python_version.partition(".") - abi = "cp{}{}".format(major, tail) - else: - abi = None - - os, _, arch = p.partition("_") - return _platform(abi = abi, os = os or None, arch = arch or None) diff --git a/python/private/pypi/pep508_platform.bzl b/python/private/pypi/pep508_platform.bzl new file mode 100644 index 0000000000..381a8d7a08 --- /dev/null +++ b/python/private/pypi/pep508_platform.bzl @@ -0,0 +1,57 @@ +# Copyright 2025 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. + +"""The platform abstraction +""" + +def platform(*, abi = None, os = None, arch = None): + """platform returns a struct for the platform. + + Args: + abi: {type}`str | None` the target ABI, e.g. `"cp39"`. + os: {type}`str | None` the target os, e.g. `"linux"`. + arch: {type}`str | None` the target CPU, e.g. `"aarch64"`. + + Returns: + A struct. + """ + + # Note, this is used a lot as a key in dictionaries, so it cannot contain + # methods. + return struct( + abi = abi, + os = os, + arch = arch, + ) + +def platform_from_str(p, python_version): + """Return a platform from a string. + + Args: + p: {type}`str` the actual string. + python_version: {type}`str` the python version to add to platform if needed. + + Returns: + A struct that is returned by the `_platform` function. + """ + if p.startswith("cp"): + abi, _, p = p.partition("_") + elif python_version: + major, _, tail = python_version.partition(".") + abi = "cp{}{}".format(major, tail) + else: + abi = None + + os, _, arch = p.partition("_") + return platform(abi = abi, os = os or None, arch = arch or None) diff --git a/python/private/pypi/pep508_req.bzl b/python/private/pypi/pep508_requirement.bzl similarity index 82% rename from python/private/pypi/pep508_req.bzl rename to python/private/pypi/pep508_requirement.bzl index 618ffaf17a..11f2b3e8fa 100644 --- a/python/private/pypi/pep508_req.bzl +++ b/python/private/pypi/pep508_requirement.bzl @@ -17,7 +17,7 @@ load("//python/private:normalize_name.bzl", "normalize_name") -_STRIP = ["(", " ", ">", "=", "<", "~", "!"] +_STRIP = ["(", " ", ">", "=", "<", "~", "!", "@"] def requirement(spec): """Parse a PEP508 requirement line @@ -28,15 +28,18 @@ def requirement(spec): Returns: A struct with the information. """ + spec = spec.strip() requires, _, maybe_hashes = spec.partition(";") marker, _, _ = maybe_hashes.partition("--hash") requires, _, extras_unparsed = requires.partition("[") + extras_unparsed, _, _ = extras_unparsed.partition("]") for char in _STRIP: requires, _, _ = requires.partition(char) - extras = extras_unparsed.strip("]").split(",") + extras = extras_unparsed.replace(" ", "").split(",") + name = requires.strip(" ") return struct( - name = normalize_name(requires.strip(" ")), + name = normalize_name(name).replace("_", "-"), marker = marker.strip(" "), extras = extras, ) diff --git a/tests/pypi/pep508/BUILD.bazel b/tests/pypi/pep508/BUILD.bazel index b795db0591..575f28ada6 100644 --- a/tests/pypi/pep508/BUILD.bazel +++ b/tests/pypi/pep508/BUILD.bazel @@ -1,5 +1,10 @@ load(":evaluate_tests.bzl", "evaluate_test_suite") +load(":requirement_tests.bzl", "requirement_test_suite") evaluate_test_suite( name = "evaluate_tests", ) + +requirement_test_suite( + name = "requirement_tests", +) diff --git a/tests/pypi/pep508/requirement_tests.bzl b/tests/pypi/pep508/requirement_tests.bzl new file mode 100644 index 0000000000..7c81ea50fc --- /dev/null +++ b/tests/pypi/pep508/requirement_tests.bzl @@ -0,0 +1,47 @@ +# Copyright 2025 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. +"""Tests for parsing the requirement specifier.""" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private/pypi:pep508_requirement.bzl", "requirement") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_requirement_line_parsing(env): + want = { + " name1[ foo ] ": ("name1", ["foo"]), + "Name[foo]": ("name", ["foo"]), + "name [fred,bar] @ http://foo.com ; python_version=='2.7'": ("name", ["fred", "bar"]), + "name; (os_name=='a' or os_name=='b') and os_name=='c'": ("name", [""]), + "name@http://foo.com": ("name", [""]), + "name[ Foo123 ]": ("name", ["Foo123"]), + "name[extra]@http://foo.com": ("name", ["extra"]), + "name[foo]": ("name", ["foo"]), + "name[quux, strange];python_version<'2.7' and platform_version=='2'": ("name", ["quux", "strange"]), + "name_foo[bar]": ("name-foo", ["bar"]), + } + + got = { + i: (parsed.name, parsed.extras) + for i, parsed in {case: requirement(case) for case in want}.items() + } + env.expect.that_dict(got).contains_exactly(want) + +_tests.append(_test_requirement_line_parsing) + +def requirement_test_suite(name): # buildifier: disable=function-docstring + test_suite( + name = name, + basic_tests = _tests, + ) From 6e2d493f3e8e12c7cf208a4e9a398c5eabb65f24 Mon Sep 17 00:00:00 2001 From: asa <96153+asa@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:44:56 -0700 Subject: [PATCH 021/156] fix: Prevent absolute path creation in uv lock template (#2769) This change fixes a bug in the `lock` rule where, when the package is at the root level, the path to `requirements.txt` is constructed incorrectly with a leading double slash (`//requirements.txt`), causing it to be interpreted as an absolute path. This change detects if the package is empty before constructing the output path. Work towards #1975 --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- python/uv/private/lock.bzl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/python/uv/private/lock.bzl b/python/uv/private/lock.bzl index 45a3819ee6..2731d6b009 100644 --- a/python/uv/private/lock.bzl +++ b/python/uv/private/lock.bzl @@ -327,10 +327,15 @@ def _maybe_file(path): def _expand_template_impl(ctx): pkg = ctx.label.package update_src = ctx.actions.declare_file(ctx.attr.update_target + ".py") + + # Fix the path construction to avoid absolute paths + # If package is empty (root), don't add a leading slash + dst = "{}/{}".format(pkg, ctx.attr.output) if pkg else ctx.attr.output + ctx.actions.expand_template( template = ctx.files._template[0], substitutions = { - "{{dst}}": "{}/{}".format(pkg, ctx.attr.output), + "{{dst}}": dst, "{{src}}": "{}".format(ctx.files.src[0].short_path), "{{update_target}}": "//{}:{}".format(pkg, ctx.attr.update_target), }, From 84351d4ec14e474bc196c0b8cd70e04fcc9a25ca Mon Sep 17 00:00:00 2001 From: "Elvis M. Wianda" <7077790+ewianda@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:18:46 -0600 Subject: [PATCH 022/156] fix: Resolve incorrect platform specific dependency (#2766) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change addresses a bug where `pip.parse` selects the wrong requirement entry when multiple extras are listed with platform-specific markers. #### 🔍 Problem: In a `requirements.txt` generated by tools like `uv` or `poetry`, it's valid to have multiple entries for the same package, each with different extras and `sys_platform` markers, for example: ```ini optimum[onnxruntime]==1.17.1 ; sys_platform == 'darwin' optimum[onnxruntime-gpu]==1.17.1 ; sys_platform == 'linux' ``` The current implementation in [`[parse_requirements.bzl](https://github.com/bazel-contrib/rules_python/blob/032f6aa738a673b13b605dabf55465c6fc1a56eb/python/private/pypi/parse_requirements.bzl#L114-L126)`](https://github.com/bazel-contrib/rules_python/blob/032f6aa738a673b13b605dabf55465c6fc1a56eb/python/private/pypi/parse_requirements.bzl#L114-L126) uses a sort-by-length heuristic to select the “best” requirement when there are multiple entries with the same base name. This works well in legacy `requirements.txt` files where: ``` my_dep my_dep[foo] my_dep[foo,bar] ``` ...would indicate an intent to select the **most specific subset of extras** (i.e. the longest name). However, this heuristic **breaks** in the presence of **platform markers**, where extras are **not subsets**, but distinct variants. In the example above, Bazel mistakenly selects `optimum[onnxruntime-gpu]` on macOS because it's a longer match, even though it is guarded by a Linux-only marker. #### ✅ Fix: This PR modifies the behavior to: 1. **Add the requirement marker** as part of the sorting key. 2. **Then apply the longest-match logic** to drop duplicate requirements with different extras but the same markers. This ensures that only applicable requirements are considered during resolution, preserving correctness in multi-platform environments. #### 🧪 Before: On macOS, the following entry is incorrectly selected: ``` optimum[onnxruntime-gpu]==1.17.1 ; sys_platform == 'linux' ``` #### ✅ After: Correct entry is selected: ``` optimum[onnxruntime]==1.17.1 ; sys_platform == 'darwin' ``` close https://github.com/bazel-contrib/rules_python/issues/2690 --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- CHANGELOG.md | 2 + python/private/pypi/parse_requirements.bzl | 44 +++++------- python/private/pypi/pep508_requirement.bzl | 11 +++ tests/pypi/extension/extension_tests.bzl | 78 ++++++++++++++++++++++ tests/pypi/pep508/requirement_tests.bzl | 23 ++++--- 5 files changed, 119 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f38732f7d8..7d9b648bea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,8 @@ Unreleased changes template. {#v0-0-0-fixed} ### Fixed +* (pypi) Platform specific extras are now correctly handled when using + universal lock files with environment markers. Fixes [#2690](https://github.com/bazel-contrib/rules_python/pull/2690). * (runfiles) ({obj}`--bootstrap_impl=script`) Follow symlinks when searching for runfiles. * (toolchains) Do not try to run `chmod` when downloading non-windows hermetic toolchain repositories on Windows. Fixes diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index d2014a7eb9..1cbf094f5c 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -30,22 +30,9 @@ load("//python/private:normalize_name.bzl", "normalize_name") load("//python/private:repo_utils.bzl", "repo_utils") load(":index_sources.bzl", "index_sources") load(":parse_requirements_txt.bzl", "parse_requirements_txt") +load(":pep508_requirement.bzl", "requirement") load(":whl_target_platforms.bzl", "select_whls") -def _extract_version(entry): - """Extract the version part from the requirement string. - - - Args: - entry: {type}`str` The requirement string. - """ - version_start = entry.find("==") - if version_start != -1: - # Extract everything after '==' until the next space or end of the string - version, _, _ = entry[version_start + 2:].partition(" ") - return version - return None - def parse_requirements( ctx, *, @@ -111,19 +98,20 @@ def parse_requirements( # The requirement lines might have duplicate names because lines for extras # are returned as just the base package name. e.g., `foo[bar]` results # in an entry like `("foo", "foo[bar] == 1.0 ...")`. - requirements_dict = { - (normalize_name(entry[0]), _extract_version(entry[1])): entry - for entry in sorted( - parse_result.requirements, - # Get the longest match and fallback to original WORKSPACE sorting, - # which should get us the entry with most extras. - # - # FIXME @aignas 2024-05-13: The correct behaviour might be to get an - # entry with all aggregated extras, but it is unclear if we - # should do this now. - key = lambda x: (len(x[1].partition("==")[0]), x), - ) - }.values() + # Lines with different markers are not condidered duplicates. + requirements_dict = {} + for entry in sorted( + parse_result.requirements, + # Get the longest match and fallback to original WORKSPACE sorting, + # which should get us the entry with most extras. + # + # FIXME @aignas 2024-05-13: The correct behaviour might be to get an + # entry with all aggregated extras, but it is unclear if we + # should do this now. + key = lambda x: (len(x[1].partition("==")[0]), x), + ): + req = requirement(entry[1]) + requirements_dict[(req.name, req.version, req.marker)] = entry tokenized_options = [] for opt in parse_result.options: @@ -132,7 +120,7 @@ def parse_requirements( pip_args = tokenized_options + extra_pip_args for plat in plats: - requirements[plat] = requirements_dict + requirements[plat] = requirements_dict.values() options[plat] = pip_args requirements_by_platform = {} diff --git a/python/private/pypi/pep508_requirement.bzl b/python/private/pypi/pep508_requirement.bzl index 11f2b3e8fa..ee7b5dfc35 100644 --- a/python/private/pypi/pep508_requirement.bzl +++ b/python/private/pypi/pep508_requirement.bzl @@ -30,6 +30,16 @@ def requirement(spec): """ spec = spec.strip() requires, _, maybe_hashes = spec.partition(";") + + version_start = requires.find("==") + version = None + if version_start != -1: + # Extract everything after '==' until the next space or end of the string + version, _, _ = requires[version_start + 2:].partition(" ") + + # Remove any trailing characters from the version string + version = version.strip(" ") + marker, _, _ = maybe_hashes.partition("--hash") requires, _, extras_unparsed = requires.partition("[") extras_unparsed, _, _ = extras_unparsed.partition("]") @@ -42,4 +52,5 @@ def requirement(spec): name = normalize_name(name).replace("_", "-"), marker = marker.strip(" "), extras = extras, + version = version, ) diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index 1652e76156..66c9e0549e 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -856,6 +856,84 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef _tests.append(_test_simple_get_index) +def _test_optimum_sys_platform_extra(env): + pypi = _parse_modules( + env, + module_ctx = _mock_mctx( + _mod( + name = "rules_python", + parse = [ + _parse( + hub_name = "pypi", + python_version = "3.15", + requirements_lock = "universal.txt", + ), + ], + ), + read = lambda x: { + "universal.txt": """\ +optimum[onnxruntime]==1.17.1 ; sys_platform == 'darwin' +optimum[onnxruntime-gpu]==1.17.1 ; sys_platform == 'linux' +""", + }[x], + ), + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + ) + + pypi.exposed_packages().contains_exactly({"pypi": []}) + pypi.hub_group_map().contains_exactly({"pypi": {}}) + pypi.hub_whl_map().contains_exactly({ + "pypi": { + "optimum": { + "pypi_315_optimum_linux_aarch64_linux_arm_linux_ppc_linux_s390x_linux_x86_64": [ + whl_config_setting( + version = "3.15", + target_platforms = [ + "cp315_linux_aarch64", + "cp315_linux_arm", + "cp315_linux_ppc", + "cp315_linux_s390x", + "cp315_linux_x86_64", + ], + config_setting = None, + filename = None, + ), + ], + "pypi_315_optimum_osx_aarch64_osx_x86_64": [ + whl_config_setting( + version = "3.15", + target_platforms = [ + "cp315_osx_aarch64", + "cp315_osx_x86_64", + ], + config_setting = None, + filename = None, + ), + ], + }, + }, + }) + + pypi.whl_libraries().contains_exactly({ + "pypi_315_optimum_linux_aarch64_linux_arm_linux_ppc_linux_s390x_linux_x86_64": { + "dep_template": "@pypi//{name}:{target}", + "python_interpreter_target": "unit_test_interpreter_target", + "repo": "pypi_315", + "requirement": "optimum[onnxruntime-gpu]==1.17.1", + }, + "pypi_315_optimum_osx_aarch64_osx_x86_64": { + "dep_template": "@pypi//{name}:{target}", + "python_interpreter_target": "unit_test_interpreter_target", + "repo": "pypi_315", + "requirement": "optimum[onnxruntime]==1.17.1", + }, + }) + pypi.whl_mods().contains_exactly({}) + +_tests.append(_test_optimum_sys_platform_extra) + def extension_test_suite(name): """Create the test suite. diff --git a/tests/pypi/pep508/requirement_tests.bzl b/tests/pypi/pep508/requirement_tests.bzl index 7c81ea50fc..9afb43a437 100644 --- a/tests/pypi/pep508/requirement_tests.bzl +++ b/tests/pypi/pep508/requirement_tests.bzl @@ -20,20 +20,21 @@ _tests = [] def _test_requirement_line_parsing(env): want = { - " name1[ foo ] ": ("name1", ["foo"]), - "Name[foo]": ("name", ["foo"]), - "name [fred,bar] @ http://foo.com ; python_version=='2.7'": ("name", ["fred", "bar"]), - "name; (os_name=='a' or os_name=='b') and os_name=='c'": ("name", [""]), - "name@http://foo.com": ("name", [""]), - "name[ Foo123 ]": ("name", ["Foo123"]), - "name[extra]@http://foo.com": ("name", ["extra"]), - "name[foo]": ("name", ["foo"]), - "name[quux, strange];python_version<'2.7' and platform_version=='2'": ("name", ["quux", "strange"]), - "name_foo[bar]": ("name-foo", ["bar"]), + " name1[ foo ] ": ("name1", ["foo"], None, ""), + "Name[foo]": ("name", ["foo"], None, ""), + "name [fred,bar] @ http://foo.com ; python_version=='2.7'": ("name", ["fred", "bar"], None, "python_version=='2.7'"), + "name; (os_name=='a' or os_name=='b') and os_name=='c'": ("name", [""], None, "(os_name=='a' or os_name=='b') and os_name=='c'"), + "name@http://foo.com": ("name", [""], None, ""), + "name[ Foo123 ]": ("name", ["Foo123"], None, ""), + "name[extra]@http://foo.com": ("name", ["extra"], None, ""), + "name[foo]": ("name", ["foo"], None, ""), + "name[quux, strange];python_version<'2.7' and platform_version=='2'": ("name", ["quux", "strange"], None, "python_version<'2.7' and platform_version=='2'"), + "name_foo[bar]": ("name-foo", ["bar"], None, ""), + "name_foo[bar]==0.25": ("name-foo", ["bar"], "0.25", ""), } got = { - i: (parsed.name, parsed.extras) + i: (parsed.name, parsed.extras, parsed.version, parsed.marker) for i, parsed in {case: requirement(case) for case in want}.items() } env.expect.that_dict(got).contains_exactly(want) From aa0d16c1463e4e26f6ed633ae83d9785a2ea9dfa Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 14 Apr 2025 07:10:51 +0900 Subject: [PATCH 023/156] fix(rules): make the srcs trully optional (#2768) With this PR we mark the srcs attribute as optional as we can leverage the `main_module` to just run things from the deps. This also removes a long-standing `TODO` note. Fixes #2765 --------- Co-authored-by: Richard Levasseur --- CHANGELOG.md | 2 + python/private/py_executable.bzl | 3 +- tests/base_rules/py_executable_base_tests.bzl | 72 ++++++++++++------- tests/support/support.bzl | 1 + 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d9b648bea..33d99dfaa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,8 @@ Unreleased changes template. * (pypi) The PyPI extension will no longer write the lock file entries as the extension has been marked reproducible. Fixes [#2434](https://github.com/bazel-contrib/rules_python/issues/2434). +* (rules) {attr}`py_binary.srcs` and {attr}`py_test.srcs` is no longer mandatory when + `main_module` is specified (for `--bootstrap_impl=script`) [20250317]: https://github.com/astral-sh/python-build-standalone/releases/tag/20250317 diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index e6f4700b20..dd3ad869fa 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -786,6 +786,8 @@ def _create_stage1_bootstrap( ) template = runtime.bootstrap_template subs["%shebang%"] = runtime.stub_shebang + elif not ctx.files.srcs: + fail("mandatory 'srcs' files have not been provided") else: if (ctx.configuration.coverage_enabled and runtime and @@ -1888,7 +1890,6 @@ def create_executable_rule_builder(implementation, **kwargs): ), **kwargs ) - builder.attrs.get("srcs").set_mandatory(True) return builder def cc_configure_features( diff --git a/tests/base_rules/py_executable_base_tests.bzl b/tests/base_rules/py_executable_base_tests.bzl index 3cc6dfb702..37707831fc 100644 --- a/tests/base_rules/py_executable_base_tests.bzl +++ b/tests/base_rules/py_executable_base_tests.bzl @@ -24,7 +24,7 @@ load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable load("//tests/base_rules:base_tests.bzl", "create_base_tests") load("//tests/base_rules:util.bzl", "WINDOWS_ATTR", pt_util = "util") load("//tests/support:py_executable_info_subject.bzl", "PyExecutableInfoSubject") -load("//tests/support:support.bzl", "CC_TOOLCHAIN", "CROSSTOOL_TOP", "LINUX_X86_64", "WINDOWS_X86_64") +load("//tests/support:support.bzl", "BOOTSTRAP_IMPL", "CC_TOOLCHAIN", "CROSSTOOL_TOP", "LINUX_X86_64", "WINDOWS_X86_64") _tests = [] @@ -342,6 +342,53 @@ def _test_name_cannot_end_in_py_impl(env, target): matching.str_matches("name must not end in*.py"), ) +def _test_main_module_bootstrap_system_python(name, config): + rt_util.helper_target( + config.rule, + name = name + "_subject", + main_module = "dummy", + ) + analysis_test( + name = name, + impl = _test_main_module_bootstrap_system_python_impl, + target = name + "_subject", + config_settings = { + BOOTSTRAP_IMPL: "system_python", + "//command_line_option:platforms": [LINUX_X86_64], + }, + expect_failure = True, + ) + +def _test_main_module_bootstrap_system_python_impl(env, target): + env.expect.that_target(target).failures().contains_predicate( + matching.str_matches("mandatory*srcs"), + ) + +_tests.append(_test_main_module_bootstrap_system_python) + +def _test_main_module_bootstrap_script(name, config): + rt_util.helper_target( + config.rule, + name = name + "_subject", + main_module = "dummy", + ) + analysis_test( + name = name, + impl = _test_main_module_bootstrap_script_impl, + target = name + "_subject", + config_settings = { + BOOTSTRAP_IMPL: "script", + "//command_line_option:platforms": [LINUX_X86_64], + }, + ) + +def _test_main_module_bootstrap_script_impl(env, target): + env.expect.that_target(target).default_outputs().contains( + "{package}/{test_name}_subject", + ) + +_tests.append(_test_main_module_bootstrap_script) + def _test_py_runtime_info_provided(name, config): rt_util.helper_target( config.rule, @@ -365,29 +412,6 @@ def _test_py_runtime_info_provided_impl(env, target): _tests.append(_test_py_runtime_info_provided) -# Can't test this -- mandatory validation happens before analysis test -# can intercept it -# TODO(#1069): Once re-implemented in Starlark, modify rule logic to make this -# testable. -# def _test_srcs_is_mandatory(name, config): -# rt_util.helper_target( -# config.rule, -# name = name + "_subject", -# ) -# analysis_test( -# name = name, -# impl = _test_srcs_is_mandatory, -# target = name + "_subject", -# expect_failure = True, -# ) -# -# _tests.append(_test_srcs_is_mandatory) -# -# def _test_srcs_is_mandatory_impl(env, target): -# env.expect.that_target(target).failures().contains_predicate( -# matching.str_matches("mandatory*srcs"), -# ) - # ===== # You were gonna add a test at the end, weren't you? # Nope. Please keep them sorted; put it in its alphabetical location. diff --git a/tests/support/support.bzl b/tests/support/support.bzl index 2b6703843b..6330155d8c 100644 --- a/tests/support/support.bzl +++ b/tests/support/support.bzl @@ -35,6 +35,7 @@ CROSSTOOL_TOP = Label("//tests/support/cc_toolchains:cc_toolchain_suite") # str() around Label() is necessary because rules_testing's config_settings # doesn't accept yet Label objects. ADD_SRCS_TO_RUNFILES = str(Label("//python/config_settings:add_srcs_to_runfiles")) +BOOTSTRAP_IMPL = str(Label("//python/config_settings:bootstrap_impl")) EXEC_TOOLS_TOOLCHAIN = str(Label("//python/config_settings:exec_tools_toolchain")) PRECOMPILE = str(Label("//python/config_settings:precompile")) PRECOMPILE_SOURCE_RETENTION = str(Label("//python/config_settings:precompile_source_retention")) From 2cb920c1e52a85239d6bcc38919fbf143b514dac Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 14 Apr 2025 08:32:10 +0900 Subject: [PATCH 024/156] refactor(pypi): translate wheel METADATA parsing to starlark (#2629) This PR starts using the newly introduced (#2692) PEP508 compliant requirement marker parser in starlark and moves the dependency generation from the Python language (`whl_installer`) to the Starlark in the `whl_library` repository rule. This PR is (almost) a pure refactor where no bugs are fixed, but this is foundational work that also adds notes on how things will be moved to macros (i.e. analysis phase) so that we can fix a few long standing bugs and prepare for stabilizing the `experimental_index_url` (#260). Refactor: * I have migrated all of the unit tests from Python to starlark for deps generation from METADATA `Requires-Dist` fields. * Read the `METADATA` file itself in Starlark. Work towards #260, #2319, #2241 Fixes #2423 --- python/private/pypi/BUILD.bazel | 19 + python/private/pypi/pep508_deps.bzl | 351 ++++++++++++++++ python/private/pypi/pep508_evaluate.bzl | 13 +- python/private/pypi/whl_installer/BUILD.bazel | 1 - .../private/pypi/whl_installer/arguments.py | 8 - python/private/pypi/whl_installer/platform.py | 304 -------------- python/private/pypi/whl_installer/wheel.py | 281 ------------- .../pypi/whl_installer/wheel_installer.py | 37 +- python/private/pypi/whl_library.bzl | 57 ++- python/private/pypi/whl_library_targets.bzl | 2 - python/private/pypi/whl_metadata.bzl | 108 +++++ tests/pypi/pep508/BUILD.bazel | 5 + tests/pypi/pep508/deps_tests.bzl | 385 ++++++++++++++++++ tests/pypi/pep508/evaluate_tests.bzl | 2 + tests/pypi/whl_installer/BUILD.bazel | 24 -- tests/pypi/whl_installer/arguments_test.py | 14 +- tests/pypi/whl_installer/platform_test.py | 154 ------- .../whl_installer/wheel_installer_test.py | 42 +- tests/pypi/whl_installer/wheel_test.py | 371 ----------------- tests/pypi/whl_metadata/BUILD.bazel | 5 + .../pypi/whl_metadata/whl_metadata_tests.bzl | 147 +++++++ 21 files changed, 1099 insertions(+), 1231 deletions(-) create mode 100644 python/private/pypi/pep508_deps.bzl delete mode 100644 python/private/pypi/whl_installer/platform.py create mode 100644 python/private/pypi/whl_metadata.bzl create mode 100644 tests/pypi/pep508/deps_tests.bzl delete mode 100644 tests/pypi/whl_installer/platform_test.py delete mode 100644 tests/pypi/whl_installer/wheel_test.py create mode 100644 tests/pypi/whl_metadata/BUILD.bazel create mode 100644 tests/pypi/whl_metadata/whl_metadata_tests.bzl diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index e0a2f20c14..7297238cb4 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -221,6 +221,18 @@ bzl_library( ], ) +bzl_library( + name = "pep508_deps_bzl", + srcs = ["pep508_deps.bzl"], + deps = [ + ":pep508_env_bzl", + ":pep508_evaluate_bzl", + ":pep508_platform_bzl", + ":pep508_requirement_bzl", + "//python/private:normalize_name_bzl", + ], +) + bzl_library( name = "pep508_env_bzl", srcs = ["pep508_env.bzl"], @@ -368,7 +380,9 @@ bzl_library( ":generate_whl_library_build_bazel_bzl", ":parse_whl_name_bzl", ":patch_whl_bzl", + ":pep508_deps_bzl", ":pypi_repo_utils_bzl", + ":whl_metadata_bzl", ":whl_target_platforms_bzl", "//python/private:auth_bzl", "//python/private:envsubst_bzl", @@ -377,6 +391,11 @@ bzl_library( ], ) +bzl_library( + name = "whl_metadata_bzl", + srcs = ["whl_metadata.bzl"], +) + bzl_library( name = "whl_repo_name_bzl", srcs = ["whl_repo_name.bzl"], diff --git a/python/private/pypi/pep508_deps.bzl b/python/private/pypi/pep508_deps.bzl new file mode 100644 index 0000000000..af0a75362b --- /dev/null +++ b/python/private/pypi/pep508_deps.bzl @@ -0,0 +1,351 @@ +# Copyright 2025 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. + +"""This module is for implementing PEP508 compliant METADATA deps parsing. +""" + +load("//python/private:normalize_name.bzl", "normalize_name") +load(":pep508_env.bzl", "env") +load(":pep508_evaluate.bzl", "evaluate") +load(":pep508_platform.bzl", "platform", "platform_from_str") +load(":pep508_requirement.bzl", "requirement") + +_ALL_OS_VALUES = [ + "windows", + "osx", + "linux", +] +_ALL_ARCH_VALUES = [ + "aarch64", + "ppc64", + "ppc64le", + "s390x", + "x86_32", + "x86_64", +] + +def deps(name, *, requires_dist, platforms = [], extras = [], host_python_version = None): + """Parse the RequiresDist from wheel METADATA + + Args: + name: {type}`str` the name of the wheel. + requires_dist: {type}`list[str]` the list of RequiresDist lines from the + METADATA file. + extras: {type}`list[str]` the requested extras to generate targets for. + platforms: {type}`list[str]` the list of target platform strings. + host_python_version: {type}`str` the host python version. + + Returns: + A struct with attributes: + * deps: {type}`list[str]` dependencies to include unconditionally. + * deps_select: {type}`dict[str, list[str]]` dependencies to include on particular + subset of target platforms. + """ + reqs = sorted( + [requirement(r) for r in requires_dist], + key = lambda x: "{}:{}:".format(x.name, sorted(x.extras), x.marker), + ) + deps = {} + deps_select = {} + name = normalize_name(name) + want_extras = _resolve_extras(name, reqs, extras) + + # drop self edges + reqs = [r for r in reqs if r.name != name] + + platforms = [ + platform_from_str(p, python_version = host_python_version) + for p in platforms + ] or [ + platform_from_str("", python_version = host_python_version), + ] + + abis = sorted({p.abi: True for p in platforms if p.abi}) + if host_python_version and len(abis) > 1: + _, _, minor_version = host_python_version.partition(".") + minor_version, _, _ = minor_version.partition(".") + default_abi = "cp3" + minor_version + elif len(abis) > 1: + fail( + "all python versions need to be specified explicitly, got: {}".format(platforms), + ) + else: + default_abi = None + + for req in reqs: + _add_req( + deps, + deps_select, + req, + extras = want_extras, + platforms = platforms, + default_abi = default_abi, + ) + + return struct( + deps = sorted(deps), + deps_select = { + _platform_str(p): sorted(deps) + for p, deps in deps_select.items() + }, + ) + +def _platform_str(self): + if self.abi == None: + if not self.os and not self.arch: + return "//conditions:default" + elif not self.arch: + return "@platforms//os:{}".format(self.os) + else: + return "{}_{}".format(self.os, self.arch) + + minor_version = self.abi[3:] + if self.arch == None and self.os == None: + return str(Label("//python/config_settings:is_python_3.{}".format(minor_version))) + + return "cp3{}_{}_{}".format( + minor_version, + self.os or "anyos", + self.arch or "anyarch", + ) + +def _platform_specializations(self, cpu_values = _ALL_ARCH_VALUES, os_values = _ALL_OS_VALUES): + """Return the platform itself and all its unambiguous specializations. + + For more info about specializations see + https://bazel.build/docs/configurable-attributes + """ + specializations = [] + specializations.append(self) + if self.arch == None: + specializations.extend([ + platform(os = self.os, arch = arch, abi = self.abi) + for arch in cpu_values + ]) + if self.os == None: + specializations.extend([ + platform(os = os, arch = self.arch, abi = self.abi) + for os in os_values + ]) + if self.os == None and self.arch == None: + specializations.extend([ + platform(os = os, arch = arch, abi = self.abi) + for os in os_values + for arch in cpu_values + ]) + return specializations + +def _add(deps, deps_select, dep, platform): + dep = normalize_name(dep) + + if platform == None: + deps[dep] = True + + # If the dep is in the platform-specific list, remove it from the select. + pop_keys = [] + for p, _deps in deps_select.items(): + if dep not in _deps: + continue + + _deps.pop(dep) + if not _deps: + pop_keys.append(p) + + for p in pop_keys: + deps_select.pop(p) + return + + if dep in deps: + # If the dep is already in the main dependency list, no need to add it in the + # platform-specific dependency list. + return + + # Add the platform-specific branch + deps_select.setdefault(platform, {}) + + # Add the dep to specializations of the given platform if they + # exist in the select statement. + for p in _platform_specializations(platform): + if p not in deps_select: + continue + + deps_select[p][dep] = True + + if len(deps_select[platform]) == 1: + # We are adding a new item to the select and we need to ensure that + # existing dependencies from less specialized platforms are propagated + # to the newly added dependency set. + for p, _deps in deps_select.items(): + # Check if the existing platform overlaps with the given platform + if p == platform or platform not in _platform_specializations(p): + continue + + deps_select[platform].update(_deps) + +def _maybe_add_common_dep(deps, deps_select, platforms, dep): + abis = sorted({p.abi: True for p in platforms if p.abi}) + if len(abis) < 2: + return + + platforms = [platform()] + [ + platform(abi = abi) + for abi in abis + ] + + # If the dep is targeting all target python versions, lets add it to + # the common dependency list to simplify the select statements. + for p in platforms: + if p not in deps_select: + return + + if dep not in deps_select[p]: + return + + # All of the python version-specific branches have the dep, so lets add + # it to the common deps. + deps[dep] = True + for p in platforms: + deps_select[p].pop(dep) + if not deps_select[p]: + deps_select.pop(p) + +def _resolve_extras(self_name, reqs, extras): + """Resolve extras which are due to depending on self[some_other_extra]. + + Some packages may have cyclic dependencies resulting from extras being used, one example is + `etils`, where we have one set of extras as aliases for other extras + and we have an extra called 'all' that includes all other extras. + + Example: github.com/google/etils/blob/a0b71032095db14acf6b33516bca6d885fe09e35/pyproject.toml#L32. + + When the `requirements.txt` is generated by `pip-tools`, then it is likely that + this step is not needed, but for other `requirements.txt` files this may be useful. + + NOTE @aignas 2023-12-08: the extra resolution is not platform dependent, + but in order for it to become platform dependent we would have to have + separate targets for each extra in extras. + """ + + # Resolve any extra extras due to self-edges, empty string means no + # extras The empty string in the set is just a way to make the handling + # of no extras and a single extra easier and having a set of {"", "foo"} + # is equivalent to having {"foo"}. + extras = extras or [""] + + self_reqs = [] + for req in reqs: + if req.name != self_name: + continue + + if req.marker == None: + # I am pretty sure we cannot reach this code as it does not + # make sense to specify packages in this way, but since it is + # easy to handle, lets do it. + # + # TODO @aignas 2023-12-08: add a test + extras = extras + req.extras + else: + # process these in a separate loop + self_reqs.append(req) + + # A double loop is not strictly optimal, but always correct without recursion + for req in self_reqs: + if [True for extra in extras if evaluate(req.marker, env = {"extra": extra})]: + extras = extras + req.extras + else: + continue + + # Iterate through all packages to ensure that we include all of the extras from previously + # visited packages. + for req_ in self_reqs: + if [True for extra in extras if evaluate(req.marker, env = {"extra": extra})]: + extras = extras + req_.extras + + # Poor mans set + return sorted({x: None for x in extras}) + +def _add_req(deps, deps_select, req, *, extras, platforms, default_abi = None): + if not req.marker: + _add(deps, deps_select, req.name, None) + return + + # NOTE @aignas 2023-12-08: in order to have reasonable select statements + # we do have to have some parsing of the markers, so it begs the question + # if packaging should be reimplemented in Starlark to have the best solution + # for now we will implement it in Python and see what the best parsing result + # can be before making this decision. + match_os = len([ + tag + for tag in [ + "os_name", + "sys_platform", + "platform_system", + ] + if tag in req.marker + ]) > 0 + match_arch = "platform_machine" in req.marker + match_version = "version" in req.marker + + if not (match_os or match_arch or match_version): + if [ + True + for extra in extras + for p in platforms + if evaluate( + req.marker, + env = env( + target_platform = p, + extra = extra, + ), + ) + ]: + _add(deps, deps_select, req.name, None) + return + + for plat in platforms: + if not [ + True + for extra in extras + if evaluate( + req.marker, + env = env( + target_platform = plat, + extra = extra, + ), + ) + ]: + continue + + if match_arch and default_abi: + _add(deps, deps_select, req.name, plat) + if plat.abi == default_abi: + _add(deps, deps_select, req.name, platform(os = plat.os, arch = plat.arch)) + elif match_arch: + _add(deps, deps_select, req.name, platform(os = plat.os, arch = plat.arch)) + elif match_os and default_abi: + _add(deps, deps_select, req.name, platform(os = plat.os, abi = plat.abi)) + if plat.abi == default_abi: + _add(deps, deps_select, req.name, platform(os = plat.os)) + elif match_os: + _add(deps, deps_select, req.name, platform(os = plat.os)) + elif match_version and default_abi: + _add(deps, deps_select, req.name, platform(abi = plat.abi)) + if plat.abi == default_abi: + _add(deps, deps_select, req.name, platform()) + elif match_version: + _add(deps, deps_select, req.name, None) + else: + fail("BUG: {} support is not implemented".format(req.marker)) + + _maybe_add_common_dep(deps, deps_select, platforms, req.name) diff --git a/python/private/pypi/pep508_evaluate.bzl b/python/private/pypi/pep508_evaluate.bzl index f45eb75cdb..f8ef553034 100644 --- a/python/private/pypi/pep508_evaluate.bzl +++ b/python/private/pypi/pep508_evaluate.bzl @@ -138,7 +138,7 @@ def evaluate(marker, *, env, strict = True, **kwargs): """ tokens = tokenize(marker) - ast = _new_expr(**kwargs) + ast = _new_expr(marker = marker, **kwargs) for _ in range(len(tokens) * 2): if not tokens: break @@ -219,17 +219,20 @@ def _not_fn(x): return not x def _new_expr( + *, + marker, and_fn = _and_fn, or_fn = _or_fn, not_fn = _not_fn): # buildifier: disable=uninitialized self = struct( + marker = marker, tree = [], parse = lambda **kwargs: _parse(self, **kwargs), value = lambda: _value(self), # This is a way for us to have a handle to the currently constructed # expression tree branch. - current = lambda: self._current[0] if self._current else None, + current = lambda: self._current[-1] if self._current else None, _current = [], _and = and_fn, _or = or_fn, @@ -313,6 +316,7 @@ def marker_expr(left, op, right, *, env, strict = True): # # The following normalizes the values left = env.get(_ENV_ALIASES, {}).get(var_name, {}).get(left, left) + else: var_name = left left = env[left] @@ -392,12 +396,15 @@ def _append(self, value): current.tree.append(value) elif hasattr(current.tree[-1], "append"): current.tree[-1].append(value) - else: + elif hasattr(current.tree, "_append"): current.tree._append(value) + else: + fail("Cannot evaluate '{}' in '{}', current: {}".format(value, self.marker, current)) def _open_parenthesis(self): """Add an extra node into the tree to perform evaluate inside parenthesis.""" self._current.append(_new_expr( + marker = self.marker, and_fn = self._and, or_fn = self._or, not_fn = self._not, diff --git a/python/private/pypi/whl_installer/BUILD.bazel b/python/private/pypi/whl_installer/BUILD.bazel index 5fb617004d..49f1a119c1 100644 --- a/python/private/pypi/whl_installer/BUILD.bazel +++ b/python/private/pypi/whl_installer/BUILD.bazel @@ -6,7 +6,6 @@ py_library( srcs = [ "arguments.py", "namespace_pkgs.py", - "platform.py", "wheel.py", "wheel_installer.py", ], diff --git a/python/private/pypi/whl_installer/arguments.py b/python/private/pypi/whl_installer/arguments.py index 29bea8026e..bb841ea9ab 100644 --- a/python/private/pypi/whl_installer/arguments.py +++ b/python/private/pypi/whl_installer/arguments.py @@ -17,8 +17,6 @@ import pathlib from typing import Any, Dict, Set -from python.private.pypi.whl_installer.platform import Platform - def parser(**kwargs: Any) -> argparse.ArgumentParser: """Create a parser for the wheel_installer tool.""" @@ -41,12 +39,6 @@ def parser(**kwargs: Any) -> argparse.ArgumentParser: action="store", help="Extra arguments to pass down to pip.", ) - parser.add_argument( - "--platform", - action="extend", - type=Platform.from_string, - help="Platforms to target dependencies. Can be used multiple times.", - ) parser.add_argument( "--pip_data_exclude", action="store", diff --git a/python/private/pypi/whl_installer/platform.py b/python/private/pypi/whl_installer/platform.py deleted file mode 100644 index 11dd6e37ab..0000000000 --- a/python/private/pypi/whl_installer/platform.py +++ /dev/null @@ -1,304 +0,0 @@ -# Copyright 2024 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. - -"""Utility class to inspect an extracted wheel directory""" - -import platform -import sys -from dataclasses import dataclass -from enum import Enum -from typing import Any, Dict, Iterator, List, Optional, Union - - -class OS(Enum): - linux = 1 - osx = 2 - windows = 3 - darwin = osx - win32 = windows - - @classmethod - def interpreter(cls) -> "OS": - "Return the interpreter operating system." - return cls[sys.platform.lower()] - - def __str__(self) -> str: - return self.name.lower() - - -class Arch(Enum): - x86_64 = 1 - x86_32 = 2 - aarch64 = 3 - ppc = 4 - ppc64le = 5 - s390x = 6 - arm = 7 - amd64 = x86_64 - arm64 = aarch64 - i386 = x86_32 - i686 = x86_32 - x86 = x86_32 - - @classmethod - def interpreter(cls) -> "Arch": - "Return the currently running interpreter architecture." - # FIXME @aignas 2023-12-13: Hermetic toolchain on Windows 3.11.6 - # is returning an empty string here, so lets default to x86_64 - return cls[platform.machine().lower() or "x86_64"] - - def __str__(self) -> str: - return self.name.lower() - - -def _as_int(value: Optional[Union[OS, Arch]]) -> int: - """Convert one of the enums above to an int for easier sorting algorithms. - - Args: - value: The value of an enum or None. - - Returns: - -1 if we get None, otherwise, the numeric value of the given enum. - """ - if value is None: - return -1 - - return int(value.value) - - -def host_interpreter_minor_version() -> int: - return sys.version_info.minor - - -@dataclass(frozen=True) -class Platform: - os: Optional[OS] = None - arch: Optional[Arch] = None - minor_version: Optional[int] = None - - @classmethod - def all( - cls, - want_os: Optional[OS] = None, - minor_version: Optional[int] = None, - ) -> List["Platform"]: - return sorted( - [ - cls(os=os, arch=arch, minor_version=minor_version) - for os in OS - for arch in Arch - if not want_os or want_os == os - ] - ) - - @classmethod - def host(cls) -> List["Platform"]: - """Use the Python interpreter to detect the platform. - - We extract `os` from sys.platform and `arch` from platform.machine - - Returns: - A list of parsed values which makes the signature the same as - `Platform.all` and `Platform.from_string`. - """ - return [ - Platform( - os=OS.interpreter(), - arch=Arch.interpreter(), - minor_version=host_interpreter_minor_version(), - ) - ] - - def all_specializations(self) -> Iterator["Platform"]: - """Return the platform itself and all its unambiguous specializations. - - For more info about specializations see - https://bazel.build/docs/configurable-attributes - """ - yield self - if self.arch is None: - for arch in Arch: - yield Platform(os=self.os, arch=arch, minor_version=self.minor_version) - if self.os is None: - for os in OS: - yield Platform(os=os, arch=self.arch, minor_version=self.minor_version) - if self.arch is None and self.os is None: - for os in OS: - for arch in Arch: - yield Platform(os=os, arch=arch, minor_version=self.minor_version) - - def __lt__(self, other: Any) -> bool: - """Add a comparison method, so that `sorted` returns the most specialized platforms first.""" - if not isinstance(other, Platform) or other is None: - raise ValueError(f"cannot compare {other} with Platform") - - self_arch, self_os = _as_int(self.arch), _as_int(self.os) - other_arch, other_os = _as_int(other.arch), _as_int(other.os) - - if self_os == other_os: - return self_arch < other_arch - else: - return self_os < other_os - - def __str__(self) -> str: - if self.minor_version is None: - if self.os is None and self.arch is None: - return "//conditions:default" - - if self.arch is None: - return f"@platforms//os:{self.os}" - else: - return f"{self.os}_{self.arch}" - - if self.arch is None and self.os is None: - return f"@//python/config_settings:is_python_3.{self.minor_version}" - - if self.arch is None: - return f"cp3{self.minor_version}_{self.os}_anyarch" - - if self.os is None: - return f"cp3{self.minor_version}_anyos_{self.arch}" - - return f"cp3{self.minor_version}_{self.os}_{self.arch}" - - @classmethod - def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]: - """Parse a string and return a list of platforms""" - platform = [platform] if isinstance(platform, str) else list(platform) - ret = set() - for p in platform: - if p == "host": - ret.update(cls.host()) - continue - - abi, _, tail = p.partition("_") - if not abi.startswith("cp"): - # The first item is not an abi - tail = p - abi = "" - os, _, arch = tail.partition("_") - arch = arch or "*" - - minor_version = int(abi[len("cp3") :]) if abi else None - - if arch != "*": - ret.add( - cls( - os=OS[os] if os != "*" else None, - arch=Arch[arch], - minor_version=minor_version, - ) - ) - - else: - ret.update( - cls.all( - want_os=OS[os] if os != "*" else None, - minor_version=minor_version, - ) - ) - - return sorted(ret) - - # NOTE @aignas 2023-12-05: below is the minimum number of accessors that are defined in - # https://peps.python.org/pep-0496/ to make rules_python generate dependencies. - # - # WARNING: It may not work in cases where the python implementation is different between - # different platforms. - - # derived from OS - @property - def os_name(self) -> str: - if self.os == OS.linux or self.os == OS.osx: - return "posix" - elif self.os == OS.windows: - return "nt" - else: - return "" - - @property - def sys_platform(self) -> str: - if self.os == OS.linux: - return "linux" - elif self.os == OS.osx: - return "darwin" - elif self.os == OS.windows: - return "win32" - else: - return "" - - @property - def platform_system(self) -> str: - if self.os == OS.linux: - return "Linux" - elif self.os == OS.osx: - return "Darwin" - elif self.os == OS.windows: - return "Windows" - else: - return "" - - # derived from OS and Arch - @property - def platform_machine(self) -> str: - """Guess the target 'platform_machine' marker. - - NOTE @aignas 2023-12-05: this may not work on really new systems, like - Windows if they define the platform markers in a different way. - """ - if self.arch == Arch.x86_64: - return "x86_64" - elif self.arch == Arch.x86_32 and self.os != OS.osx: - return "i386" - elif self.arch == Arch.x86_32: - return "" - elif self.arch == Arch.aarch64 and self.os == OS.linux: - return "aarch64" - elif self.arch == Arch.aarch64: - # Assuming that OSX and Windows use this one since the precedent is set here: - # https://github.com/cgohlke/win_arm64-wheels - return "arm64" - elif self.os != OS.linux: - return "" - elif self.arch == Arch.ppc: - return "ppc" - elif self.arch == Arch.ppc64le: - return "ppc64le" - elif self.arch == Arch.s390x: - return "s390x" - else: - return "" - - def env_markers(self, extra: str) -> Dict[str, str]: - # If it is None, use the host version - minor_version = self.minor_version or host_interpreter_minor_version() - - return { - "extra": extra, - "os_name": self.os_name, - "sys_platform": self.sys_platform, - "platform_machine": self.platform_machine, - "platform_system": self.platform_system, - "platform_release": "", # unset - "platform_version": "", # unset - "python_version": f"3.{minor_version}", - # FIXME @aignas 2024-01-14: is putting zero last a good idea? Maybe we should - # use `20` or something else to avoid having weird issues where the full version is used for - # matching and the author decides to only support 3.y.5 upwards. - "implementation_version": f"3.{minor_version}.0", - "python_full_version": f"3.{minor_version}.0", - # we assume that the following are the same as the interpreter used to setup the deps: - # "implementation_name": "cpython" - # "platform_python_implementation: "CPython", - } diff --git a/python/private/pypi/whl_installer/wheel.py b/python/private/pypi/whl_installer/wheel.py index d95b33a194..da81b5ea9f 100644 --- a/python/private/pypi/whl_installer/wheel.py +++ b/python/private/pypi/whl_installer/wheel.py @@ -25,275 +25,6 @@ from packaging.requirements import Requirement from pip._vendor.packaging.utils import canonicalize_name -from python.private.pypi.whl_installer.platform import ( - Platform, - host_interpreter_minor_version, -) - - -@dataclass(frozen=True) -class FrozenDeps: - deps: List[str] - deps_select: Dict[str, List[str]] - - -class Deps: - """Deps is a dependency builder that has a build() method to return FrozenDeps.""" - - def __init__( - self, - name: str, - requires_dist: List[str], - *, - extras: Optional[Set[str]] = None, - platforms: Optional[Set[Platform]] = None, - ): - """Create a new instance and parse the requires_dist - - Args: - name (str): The name of the whl distribution - requires_dist (list[Str]): The Requires-Dist from the METADATA of the whl - distribution. - extras (set[str], optional): The list of requested extras, defaults to None. - platforms (set[Platform], optional): The list of target platforms, defaults to - None. If the list of platforms has multiple `minor_version` values, it - will change the code to generate the select statements using - `@rules_python//python/config_settings:is_python_3.y` conditions. - """ - self.name: str = Deps._normalize(name) - self._platforms: Set[Platform] = platforms or set() - self._target_versions = {p.minor_version for p in platforms or {}} - self._default_minor_version = None - if platforms and len(self._target_versions) > 2: - # TODO @aignas 2024-06-23: enable this to be set via a CLI arg - # for being more explicit. - self._default_minor_version = host_interpreter_minor_version() - - if None in self._target_versions and len(self._target_versions) > 2: - raise ValueError( - f"all python versions need to be specified explicitly, got: {platforms}" - ) - - # Sort so that the dictionary order in the FrozenDeps is deterministic - # without the final sort because Python retains insertion order. That way - # the sorting by platform is limited within the Platform class itself and - # the unit-tests for the Deps can be simpler. - reqs = sorted( - (Requirement(wheel_req) for wheel_req in requires_dist), - key=lambda x: f"{x.name}:{sorted(x.extras)}", - ) - - want_extras = self._resolve_extras(reqs, extras) - - # Then add all of the requirements in order - self._deps: Set[str] = set() - self._select: Dict[Platform, Set[str]] = defaultdict(set) - for req in reqs: - self._add_req(req, want_extras) - - def _add(self, dep: str, platform: Optional[Platform]): - dep = Deps._normalize(dep) - - # Self-edges are processed in _resolve_extras - if dep == self.name: - return - - if not platform: - self._deps.add(dep) - - # If the dep is in the platform-specific list, remove it from the select. - pop_keys = [] - for p, deps in self._select.items(): - if dep not in deps: - continue - - deps.remove(dep) - if not deps: - pop_keys.append(p) - - for p in pop_keys: - self._select.pop(p) - return - - if dep in self._deps: - # If the dep is already in the main dependency list, no need to add it in the - # platform-specific dependency list. - return - - # Add the platform-specific dep - self._select[platform].add(dep) - - # Add the dep to specializations of the given platform if they - # exist in the select statement. - for p in platform.all_specializations(): - if p not in self._select: - continue - - self._select[p].add(dep) - - if len(self._select[platform]) == 1: - # We are adding a new item to the select and we need to ensure that - # existing dependencies from less specialized platforms are propagated - # to the newly added dependency set. - for p, deps in self._select.items(): - # Check if the existing platform overlaps with the given platform - if p == platform or platform not in p.all_specializations(): - continue - - self._select[platform].update(self._select[p]) - - def _maybe_add_common_dep(self, dep): - if len(self._target_versions) < 2: - return - - platforms = [Platform()] + [ - Platform(minor_version=v) for v in self._target_versions - ] - - # If the dep is targeting all target python versions, lets add it to - # the common dependency list to simplify the select statements. - for p in platforms: - if p not in self._select: - return - - if dep not in self._select[p]: - return - - # All of the python version-specific branches have the dep, so lets add - # it to the common deps. - self._deps.add(dep) - for p in platforms: - self._select[p].remove(dep) - if not self._select[p]: - self._select.pop(p) - - @staticmethod - def _normalize(name: str) -> str: - return re.sub(r"[-_.]+", "_", name).lower() - - def _resolve_extras( - self, reqs: List[Requirement], extras: Optional[Set[str]] - ) -> Set[str]: - """Resolve extras which are due to depending on self[some_other_extra]. - - Some packages may have cyclic dependencies resulting from extras being used, one example is - `etils`, where we have one set of extras as aliases for other extras - and we have an extra called 'all' that includes all other extras. - - Example: github.com/google/etils/blob/a0b71032095db14acf6b33516bca6d885fe09e35/pyproject.toml#L32. - - When the `requirements.txt` is generated by `pip-tools`, then it is likely that - this step is not needed, but for other `requirements.txt` files this may be useful. - - NOTE @aignas 2023-12-08: the extra resolution is not platform dependent, - but in order for it to become platform dependent we would have to have - separate targets for each extra in extras. - """ - - # Resolve any extra extras due to self-edges, empty string means no - # extras The empty string in the set is just a way to make the handling - # of no extras and a single extra easier and having a set of {"", "foo"} - # is equivalent to having {"foo"}. - extras = extras or {""} - - self_reqs = [] - for req in reqs: - if Deps._normalize(req.name) != self.name: - continue - - if req.marker is None: - # I am pretty sure we cannot reach this code as it does not - # make sense to specify packages in this way, but since it is - # easy to handle, lets do it. - # - # TODO @aignas 2023-12-08: add a test - extras = extras | req.extras - else: - # process these in a separate loop - self_reqs.append(req) - - # A double loop is not strictly optimal, but always correct without recursion - for req in self_reqs: - if any(req.marker.evaluate({"extra": extra}) for extra in extras): - extras = extras | req.extras - else: - continue - - # Iterate through all packages to ensure that we include all of the extras from previously - # visited packages. - for req_ in self_reqs: - if any(req_.marker.evaluate({"extra": extra}) for extra in extras): - extras = extras | req_.extras - - return extras - - def _add_req(self, req: Requirement, extras: Set[str]) -> None: - if req.marker is None: - self._add(req.name, None) - return - - marker_str = str(req.marker) - - if not self._platforms: - if any(req.marker.evaluate({"extra": extra}) for extra in extras): - self._add(req.name, None) - return - - # NOTE @aignas 2023-12-08: in order to have reasonable select statements - # we do have to have some parsing of the markers, so it begs the question - # if packaging should be reimplemented in Starlark to have the best solution - # for now we will implement it in Python and see what the best parsing result - # can be before making this decision. - match_os = any( - tag in marker_str - for tag in [ - "os_name", - "sys_platform", - "platform_system", - ] - ) - match_arch = "platform_machine" in marker_str - match_version = "version" in marker_str - - if not (match_os or match_arch or match_version): - if any(req.marker.evaluate({"extra": extra}) for extra in extras): - self._add(req.name, None) - return - - for plat in self._platforms: - if not any( - req.marker.evaluate(plat.env_markers(extra)) for extra in extras - ): - continue - - if match_arch and self._default_minor_version: - self._add(req.name, plat) - if plat.minor_version == self._default_minor_version: - self._add(req.name, Platform(plat.os, plat.arch)) - elif match_arch: - self._add(req.name, Platform(plat.os, plat.arch)) - elif match_os and self._default_minor_version: - self._add(req.name, Platform(plat.os, minor_version=plat.minor_version)) - if plat.minor_version == self._default_minor_version: - self._add(req.name, Platform(plat.os)) - elif match_os: - self._add(req.name, Platform(plat.os)) - elif match_version and self._default_minor_version: - self._add(req.name, Platform(minor_version=plat.minor_version)) - if plat.minor_version == self._default_minor_version: - self._add(req.name, Platform()) - elif match_version: - self._add(req.name, None) - - # Merge to common if possible after processing all platforms - self._maybe_add_common_dep(req.name) - - def build(self) -> FrozenDeps: - return FrozenDeps( - deps=sorted(self._deps), - deps_select={str(p): sorted(deps) for p, deps in self._select.items()}, - ) - class Wheel: """Representation of the compressed .whl file""" @@ -344,18 +75,6 @@ def entry_points(self) -> Dict[str, Tuple[str, str]]: return entry_points_mapping - def dependencies( - self, - extras_requested: Set[str] = None, - platforms: Optional[Set[Platform]] = None, - ) -> FrozenDeps: - return Deps( - self.name, - extras=extras_requested, - platforms=platforms, - requires_dist=self.metadata.get_all("Requires-Dist", []), - ).build() - def unzip(self, directory: str) -> None: installation_schemes = { "purelib": "/site-packages", diff --git a/python/private/pypi/whl_installer/wheel_installer.py b/python/private/pypi/whl_installer/wheel_installer.py index ef8181c30d..c7695d92e8 100644 --- a/python/private/pypi/whl_installer/wheel_installer.py +++ b/python/private/pypi/whl_installer/wheel_installer.py @@ -23,7 +23,7 @@ import sys from pathlib import Path from tempfile import NamedTemporaryFile -from typing import Dict, List, Optional, Set, Tuple +from typing import Dict, Optional, Set, Tuple from pip._vendor.packaging.utils import canonicalize_name @@ -103,9 +103,7 @@ def _setup_namespace_pkg_compatibility(wheel_dir: str) -> None: def _extract_wheel( wheel_file: str, - extras: Dict[str, Set[str]], enable_implicit_namespace_pkgs: bool, - platforms: List[wheel.Platform], installation_dir: Path = Path("."), ) -> None: """Extracts wheel into given directory and creates py_library and filegroup targets. @@ -113,7 +111,6 @@ def _extract_wheel( Args: 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 enable_implicit_namespace_pkgs: if true, disables conversion of implicit namespace packages and will unzip as-is """ @@ -123,25 +120,19 @@ def _extract_wheel( if not enable_implicit_namespace_pkgs: _setup_namespace_pkg_compatibility(installation_dir) - extras_requested = extras[whl.name] if whl.name in extras else set() - - dependencies = whl.dependencies(extras_requested, platforms) + metadata = { + "python_version": sys.version.partition(" ")[0], + "entry_points": [ + { + "name": name, + "module": module, + "attribute": attribute, + } + for name, (module, attribute) in sorted(whl.entry_points().items()) + ], + } with open(os.path.join(installation_dir, "metadata.json"), "w") as f: - metadata = { - "name": whl.name, - "version": whl.version, - "deps": dependencies.deps, - "deps_by_platform": dependencies.deps_select, - "entry_points": [ - { - "name": name, - "module": module, - "attribute": attribute, - } - for name, (module, attribute) in sorted(whl.entry_points().items()) - ], - } json.dump(metadata, f) @@ -155,13 +146,9 @@ def main() -> None: if args.whl_file: whl = Path(args.whl_file) - name, extras_for_pkg = _parse_requirement_for_extra(args.requirement) - extras = {name: extras_for_pkg} if extras_for_pkg and name else dict() _extract_wheel( wheel_file=whl, - extras=extras, enable_implicit_namespace_pkgs=args.enable_implicit_namespace_pkgs, - platforms=arguments.get_platforms(args), ) return diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 493f11353e..54f9ff3909 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -21,9 +21,13 @@ load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") load(":attrs.bzl", "ATTRS", "use_isolated") load(":deps.bzl", "all_repo_names", "record_files") load(":generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel") +load(":parse_requirements.bzl", "host_platform") load(":parse_whl_name.bzl", "parse_whl_name") load(":patch_whl.bzl", "patch_whl") +load(":pep508_deps.bzl", "deps") +load(":pep508_requirement.bzl", "requirement") load(":pypi_repo_utils.bzl", "pypi_repo_utils") +load(":whl_metadata.bzl", "whl_metadata") load(":whl_target_platforms.bzl", "whl_target_platforms") _CPPFLAGS = "CPPFLAGS" @@ -361,7 +365,7 @@ def _whl_library_impl(rctx): arguments = args + [ "--whl-file", whl_path, - ] + ["--platform={}".format(p) for p in target_platforms], + ], srcs = rctx.attr._python_srcs, environment = environment, quiet = rctx.attr.quiet, @@ -396,17 +400,60 @@ def _whl_library_impl(rctx): ) entry_points[entry_point_without_py] = entry_point_script_name + # TODO @aignas 2025-04-04: move this to whl_library_targets.bzl to have + # this in the analysis phase. + # + # This means that whl_library_targets will have to accept the following args: + # * name - the name of the package in the METADATA. + # * requires_dist - the list of METADATA Requires-Dist. + # * platforms - the list of target platforms. The target_platforms + # should come from the hub repo via a 'load' statement so that they don't + # need to be passed as an argument to `whl_library`. + # * extras - the list of required extras. This comes from the + # `rctx.attr.requirement` for now. In the future the required extras could + # stay in the hub repo, where we calculate the extra aliases that we need + # to create automatically and this way expose the targets for the specific + # extras. The first step will be to generate a target per extra for the + # `py_library` and `filegroup`. Maybe we need to have a special provider + # or an output group so that we can return the `whl` file from the + # `py_library` target? filegroup can use output groups to expose files. + # * host_python_version/versons - the list of python versions to support + # should come from the hub, similar to how the target platforms are specified. + # + # Extra things that we should move at the same time: + # * group_name, group_deps - this info can stay in the hub repository so that + # it is piped at the analysis time and changing the requirement groups does + # cause to re-fetch the deps. + python_version = metadata["python_version"] + metadata = whl_metadata( + install_dir = rctx.path("site-packages"), + read_fn = rctx.read, + logger = logger, + ) + + # TODO @aignas 2025-04-09: this will later be removed when loaded through the hub + major_minor, _, _ = python_version.rpartition(".") + package_deps = deps( + name = metadata.name, + requires_dist = metadata.requires_dist, + platforms = target_platforms or [ + "cp{}_{}".format(major_minor.replace(".", ""), host_platform(rctx)), + ], + extras = requirement(rctx.attr.requirement).extras, + host_python_version = python_version, + ) + build_file_contents = generate_whl_library_build_bazel( name = whl_path.basename, dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix), - dependencies = metadata["deps"], - dependencies_by_platform = metadata["deps_by_platform"], + dependencies = package_deps.deps, + dependencies_by_platform = package_deps.deps_select, group_name = rctx.attr.group_name, group_deps = rctx.attr.group_deps, data_exclude = rctx.attr.pip_data_exclude, tags = [ - "pypi_name=" + metadata["name"], - "pypi_version=" + metadata["version"], + "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))), diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl index 95031e6181..d32746b604 100644 --- a/python/private/pypi/whl_library_targets.bzl +++ b/python/private/pypi/whl_library_targets.bzl @@ -90,8 +90,6 @@ def whl_library_targets( native: {type}`native` The native struct for overriding in tests. rules: {type}`struct` A struct with references to rules for creating targets. """ - _ = name # buildifier: @unused - dependencies = sorted([normalize_name(d) for d in dependencies]) dependencies_by_platform = { platform: sorted([normalize_name(d) for d in deps]) diff --git a/python/private/pypi/whl_metadata.bzl b/python/private/pypi/whl_metadata.bzl new file mode 100644 index 0000000000..8a86ffbff1 --- /dev/null +++ b/python/private/pypi/whl_metadata.bzl @@ -0,0 +1,108 @@ +"""A simple function to find the METADATA file and parse it""" + +_NAME = "Name: " +_PROVIDES_EXTRA = "Provides-Extra: " +_REQUIRES_DIST = "Requires-Dist: " +_VERSION = "Version: " + +def whl_metadata(*, install_dir, read_fn, logger): + """Find and parse the METADATA file in the extracted whl contents dir. + + Args: + install_dir: {type}`path` location where the wheel has been extracted. + read_fn: the function used to read files. + logger: the function used to log failures. + + Returns: + A struct with parsed values: + * `name`: {type}`str` the name of the wheel. + * `version`: {type}`str` the version of the wheel. + * `requires_dist`: {type}`list[str]` the list of requirements. + * `provides_extra`: {type}`list[str]` the list of extras that this package + provides. + """ + metadata_file = find_whl_metadata(install_dir = install_dir, logger = logger) + contents = read_fn(metadata_file) + result = parse_whl_metadata(contents) + + if not (result.name and result.version): + logger.fail("Failed to parsed the wheel METADATA file:\n{}".format(contents)) + return None + + return result + +def parse_whl_metadata(contents): + """Parse .whl METADATA file + + Args: + contents: {type}`str` the contents of the file. + + Returns: + A struct with parsed values: + * `name`: {type}`str` the name of the wheel. + * `version`: {type}`str` the version of the wheel. + * `requires_dist`: {type}`list[str]` the list of requirements. + * `provides_extra`: {type}`list[str]` the list of extras that this package + provides. + """ + parsed = { + "name": "", + "provides_extra": [], + "requires_dist": [], + "version": "", + } + for line in contents.strip().split("\n"): + if not line.strip(): + # Stop parsing on first empty line, which marks the end of the + # headers containing the metadata. + break + + if line.startswith(_NAME): + _, _, value = line.partition(_NAME) + parsed["name"] = value.strip() + elif line.startswith(_VERSION): + _, _, value = line.partition(_VERSION) + parsed["version"] = value.strip() + elif line.startswith(_REQUIRES_DIST): + _, _, value = line.partition(_REQUIRES_DIST) + parsed["requires_dist"].append(value.strip(" ")) + elif line.startswith(_PROVIDES_EXTRA): + _, _, value = line.partition(_PROVIDES_EXTRA) + parsed["provides_extra"].append(value.strip(" ")) + + return struct( + name = parsed["name"], + provides_extra = parsed["provides_extra"], + requires_dist = parsed["requires_dist"], + version = parsed["version"], + ) + +def find_whl_metadata(*, install_dir, logger): + """Find the whl METADATA file in the install_dir. + + Args: + install_dir: {type}`path` location where the wheel has been extracted. + logger: the function used to log failures. + + Returns: + {type}`path` The path to the METADATA file. + """ + dist_info = None + for maybe_dist_info in install_dir.readdir(): + # first find the ".dist-info" folder + if not (maybe_dist_info.is_dir and maybe_dist_info.basename.endswith(".dist-info")): + continue + + dist_info = maybe_dist_info + metadata_file = dist_info.get_child("METADATA") + + if metadata_file.exists: + return metadata_file + + break + + if dist_info: + logger.fail("The METADATA file for the wheel could not be found in '{}/{}'".format(install_dir.basename, dist_info.basename)) + else: + logger.fail("The '*.dist-info' directory could not be found in '{}'".format(install_dir.basename)) + return None diff --git a/tests/pypi/pep508/BUILD.bazel b/tests/pypi/pep508/BUILD.bazel index 575f28ada6..7eab2e096a 100644 --- a/tests/pypi/pep508/BUILD.bazel +++ b/tests/pypi/pep508/BUILD.bazel @@ -1,6 +1,11 @@ +load(":deps_tests.bzl", "deps_test_suite") load(":evaluate_tests.bzl", "evaluate_test_suite") load(":requirement_tests.bzl", "requirement_test_suite") +deps_test_suite( + name = "deps_tests", +) + evaluate_test_suite( name = "evaluate_tests", ) diff --git a/tests/pypi/pep508/deps_tests.bzl b/tests/pypi/pep508/deps_tests.bzl new file mode 100644 index 0000000000..44031ab6a5 --- /dev/null +++ b/tests/pypi/pep508/deps_tests.bzl @@ -0,0 +1,385 @@ +# Copyright 2025 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. +"""Tests for construction of Python version matching config settings.""" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private/pypi:pep508_deps.bzl", "deps") # buildifier: disable=bzl-visibility + +_tests = [] + +def test_simple_deps(env): + got = deps( + "foo", + requires_dist = ["bar-Bar"], + ) + env.expect.that_collection(got.deps).contains_exactly(["bar_bar"]) + env.expect.that_dict(got.deps_select).contains_exactly({}) + +_tests.append(test_simple_deps) + +def test_can_add_os_specific_deps(env): + got = deps( + "foo", + requires_dist = [ + "bar", + "an_osx_dep; sys_platform=='darwin'", + "posix_dep; os_name=='posix'", + "win_dep; os_name=='nt'", + ], + platforms = [ + "linux_x86_64", + "osx_x86_64", + "osx_aarch64", + "windows_x86_64", + ], + host_python_version = "3.3.1", + ) + + env.expect.that_collection(got.deps).contains_exactly(["bar"]) + env.expect.that_dict(got.deps_select).contains_exactly({ + "@platforms//os:linux": ["posix_dep"], + "@platforms//os:osx": ["an_osx_dep", "posix_dep"], + "@platforms//os:windows": ["win_dep"], + }) + +_tests.append(test_can_add_os_specific_deps) + +def test_can_add_os_specific_deps_with_python_version(env): + got = deps( + "foo", + requires_dist = [ + "bar", + "an_osx_dep; sys_platform=='darwin'", + "posix_dep; os_name=='posix'", + "win_dep; os_name=='nt'", + ], + platforms = [ + "cp33_linux_x86_64", + "cp33_osx_x86_64", + "cp33_osx_aarch64", + "cp33_windows_x86_64", + ], + ) + + env.expect.that_collection(got.deps).contains_exactly(["bar"]) + env.expect.that_dict(got.deps_select).contains_exactly({ + "@platforms//os:linux": ["posix_dep"], + "@platforms//os:osx": ["an_osx_dep", "posix_dep"], + "@platforms//os:windows": ["win_dep"], + }) + +_tests.append(test_can_add_os_specific_deps_with_python_version) + +def test_deps_are_added_to_more_specialized_platforms(env): + got = deps( + "foo", + requires_dist = [ + "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", + "mac_dep; sys_platform=='darwin'", + ], + platforms = [ + "osx_x86_64", + "osx_aarch64", + ], + host_python_version = "3.8.4", + ) + + env.expect.that_collection(got.deps).contains_exactly([]) + env.expect.that_dict(got.deps_select).contains_exactly({ + "@platforms//os:osx": ["mac_dep"], + "osx_aarch64": ["m1_dep", "mac_dep"], + }) + +_tests.append(test_deps_are_added_to_more_specialized_platforms) + +def test_deps_from_more_specialized_platforms_are_propagated(env): + got = deps( + "foo", + requires_dist = [ + "a_mac_dep; sys_platform=='darwin'", + "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", + ], + platforms = [ + "osx_x86_64", + "osx_aarch64", + ], + host_python_version = "3.8.4", + ) + + env.expect.that_collection(got.deps).contains_exactly([]) + env.expect.that_dict(got.deps_select).contains_exactly( + { + "@platforms//os:osx": ["a_mac_dep"], + "osx_aarch64": ["a_mac_dep", "m1_dep"], + }, + ) + +_tests.append(test_deps_from_more_specialized_platforms_are_propagated) + +def test_non_platform_markers_are_added_to_common_deps(env): + got = deps( + "foo", + requires_dist = [ + "bar", + "baz; implementation_name=='cpython'", + "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", + ], + platforms = [ + "linux_x86_64", + "osx_x86_64", + "osx_aarch64", + "windows_x86_64", + ], + host_python_version = "3.8.4", + ) + + env.expect.that_collection(got.deps).contains_exactly(["bar", "baz"]) + env.expect.that_dict(got.deps_select).contains_exactly({ + "osx_aarch64": ["m1_dep"], + }) + +_tests.append(test_non_platform_markers_are_added_to_common_deps) + +def test_self_is_ignored(env): + got = deps( + "foo", + requires_dist = [ + "bar", + "req_dep; extra == 'requests'", + "foo[requests]; extra == 'ssl'", + "ssl_lib; extra == 'ssl'", + ], + extras = ["ssl"], + ) + + env.expect.that_collection(got.deps).contains_exactly(["bar", "req_dep", "ssl_lib"]) + env.expect.that_dict(got.deps_select).contains_exactly({}) + +_tests.append(test_self_is_ignored) + +def test_self_dependencies_can_come_in_any_order(env): + got = deps( + "foo", + requires_dist = [ + "bar", + "baz; extra == 'feat'", + "foo[feat2]; extra == 'all'", + "foo[feat]; extra == 'feat2'", + "zdep; extra == 'all'", + ], + extras = ["all"], + ) + + env.expect.that_collection(got.deps).contains_exactly(["bar", "baz", "zdep"]) + env.expect.that_dict(got.deps_select).contains_exactly({}) + +_tests.append(test_self_dependencies_can_come_in_any_order) + +def _test_can_get_deps_based_on_specific_python_version(env): + requires_dist = [ + "bar", + "baz; python_version < '3.8'", + "posix_dep; os_name=='posix' and python_version >= '3.8'", + ] + + py38 = deps( + "foo", + requires_dist = requires_dist, + platforms = ["cp38_linux_x86_64"], + ) + py37 = deps( + "foo", + requires_dist = requires_dist, + platforms = ["cp37_linux_x86_64"], + ) + + env.expect.that_collection(py37.deps).contains_exactly(["bar", "baz"]) + env.expect.that_dict(py37.deps_select).contains_exactly({}) + env.expect.that_collection(py38.deps).contains_exactly(["bar"]) + env.expect.that_dict(py38.deps_select).contains_exactly({"@platforms//os:linux": ["posix_dep"]}) + +_tests.append(_test_can_get_deps_based_on_specific_python_version) + +def _test_no_version_select_when_single_version(env): + requires_dist = [ + "bar", + "baz; python_version >= '3.8'", + "posix_dep; os_name=='posix'", + "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", + "arch_dep; platform_machine=='x86_64' and python_version >= '3.8'", + ] + host_python_version = "3.7.5" + + got = deps( + "foo", + requires_dist = requires_dist, + platforms = [ + "cp38_linux_x86_64", + "cp38_windows_x86_64", + ], + host_python_version = host_python_version, + ) + + env.expect.that_collection(got.deps).contains_exactly(["bar", "baz"]) + env.expect.that_dict(got.deps_select).contains_exactly({ + "@platforms//os:linux": ["posix_dep", "posix_dep_with_version"], + "linux_x86_64": ["arch_dep", "posix_dep", "posix_dep_with_version"], + "windows_x86_64": ["arch_dep"], + }) + +_tests.append(_test_no_version_select_when_single_version) + +def _test_can_get_version_select(env): + requires_dist = [ + "bar", + "baz; python_version < '3.8'", + "baz_new; python_version >= '3.8'", + "posix_dep; os_name=='posix'", + "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", + "arch_dep; platform_machine=='x86_64' and python_version < '3.8'", + ] + host_python_version = "3.7.4" + + got = deps( + "foo", + requires_dist = requires_dist, + platforms = [ + "cp3{}_{}_x86_64".format(minor, os) + for minor in [7, 8, 9] + for os in ["linux", "windows"] + ], + host_python_version = host_python_version, + ) + + env.expect.that_collection(got.deps).contains_exactly(["bar"]) + env.expect.that_dict(got.deps_select).contains_exactly({ + str(Label("//python/config_settings:is_python_3.7")): ["baz"], + str(Label("//python/config_settings:is_python_3.8")): ["baz_new"], + str(Label("//python/config_settings:is_python_3.9")): ["baz_new"], + "@platforms//os:linux": ["baz", "posix_dep"], + "cp37_linux_anyarch": ["baz", "posix_dep"], + "cp37_linux_x86_64": ["arch_dep", "baz", "posix_dep"], + "cp37_windows_x86_64": ["arch_dep", "baz"], + "cp38_linux_anyarch": [ + "baz_new", + "posix_dep", + "posix_dep_with_version", + ], + "cp39_linux_anyarch": [ + "baz_new", + "posix_dep", + "posix_dep_with_version", + ], + "linux_x86_64": ["arch_dep", "baz", "posix_dep"], + "windows_x86_64": ["arch_dep", "baz"], + "//conditions:default": ["baz"], + }) + +_tests.append(_test_can_get_version_select) + +def _test_deps_spanning_all_target_py_versions_are_added_to_common(env): + requires_dist = [ + "bar", + "baz (<2,>=1.11) ; python_version < '3.8'", + "baz (<2,>=1.14) ; python_version >= '3.8'", + ] + host_python_version = "3.8.4" + + got = deps( + "foo", + requires_dist = requires_dist, + platforms = [ + "cp3{}_linux_x86_64".format(minor) + for minor in [7, 8, 9] + ], + host_python_version = host_python_version, + ) + + env.expect.that_collection(got.deps).contains_exactly(["bar", "baz"]) + env.expect.that_dict(got.deps_select).contains_exactly({}) + +_tests.append(_test_deps_spanning_all_target_py_versions_are_added_to_common) + +def _test_deps_are_not_duplicated(env): + host_python_version = "3.7.4" + + # See an example in + # https://files.pythonhosted.org/packages/76/9e/db1c2d56c04b97981c06663384f45f28950a73d9acf840c4006d60d0a1ff/opencv_python-4.9.0.80-cp37-abi3-win32.whl.metadata + requires_dist = [ + "bar >=0.1.0 ; python_version < '3.7'", + "bar >=0.2.0 ; python_version >= '3.7'", + "bar >=0.4.0 ; python_version >= '3.6' and platform_system == 'Linux' and platform_machine == 'aarch64'", + "bar >=0.4.0 ; python_version >= '3.9'", + "bar >=0.5.0 ; python_version <= '3.9' and platform_system == 'Darwin' and platform_machine == 'arm64'", + "bar >=0.5.0 ; python_version >= '3.10' and platform_system == 'Darwin'", + "bar >=0.5.0 ; python_version >= '3.10'", + "bar >=0.6.0 ; python_version >= '3.11'", + ] + + got = deps( + "foo", + requires_dist = requires_dist, + platforms = [ + "cp3{}_{}_{}".format(minor, os, arch) + for minor in [7, 10] + for os in ["linux", "osx", "windows"] + for arch in ["x86_64", "aarch64"] + ], + host_python_version = host_python_version, + ) + + env.expect.that_collection(got.deps).contains_exactly(["bar"]) + env.expect.that_dict(got.deps_select).contains_exactly({}) + +_tests.append(_test_deps_are_not_duplicated) + +def _test_deps_are_not_duplicated_when_encountering_platform_dep_first(env): + host_python_version = "3.7.1" + + # Note, that we are sorting the incoming `requires_dist` and we need to ensure that we are not getting any + # issues even if the platform-specific line comes first. + requires_dist = [ + "bar >=0.4.0 ; python_version >= '3.6' and platform_system == 'Linux' and platform_machine == 'aarch64'", + "bar >=0.5.0 ; python_version >= '3.9'", + ] + + got = deps( + "foo", + requires_dist = requires_dist, + platforms = [ + "cp37_linux_aarch64", + "cp37_linux_x86_64", + "cp310_linux_aarch64", + "cp310_linux_x86_64", + ], + host_python_version = host_python_version, + ) + + # TODO @aignas 2025-02-24: this test case in the python version is passing but + # I am not sure why. The starlark version behaviour looks more correct. + env.expect.that_collection(got.deps).contains_exactly([]) + env.expect.that_dict(got.deps_select).contains_exactly({ + str(Label("//python/config_settings:is_python_3.10")): ["bar"], + "cp310_linux_aarch64": ["bar"], + "cp37_linux_aarch64": ["bar"], + "linux_aarch64": ["bar"], + }) + +_tests.append(_test_deps_are_not_duplicated_when_encountering_platform_dep_first) + +def deps_test_suite(name): # buildifier: disable=function-docstring + test_suite( + name = name, + basic_tests = _tests, + ) diff --git a/tests/pypi/pep508/evaluate_tests.bzl b/tests/pypi/pep508/evaluate_tests.bzl index 80b70f4dad..14e5e40b43 100644 --- a/tests/pypi/pep508/evaluate_tests.bzl +++ b/tests/pypi/pep508/evaluate_tests.bzl @@ -148,6 +148,8 @@ def _logical_expression_tests(env): # expr "os_name == 'fo'": False, "(os_name == 'fo')": False, + "((os_name == 'fo'))": False, + "((os_name == 'foo'))": True, "not (os_name == 'fo')": True, # and diff --git a/tests/pypi/whl_installer/BUILD.bazel b/tests/pypi/whl_installer/BUILD.bazel index 040e4d765f..fea6a46d01 100644 --- a/tests/pypi/whl_installer/BUILD.bazel +++ b/tests/pypi/whl_installer/BUILD.bazel @@ -27,18 +27,6 @@ py_test( ], ) -py_test( - name = "platform_test", - size = "small", - srcs = [ - "platform_test.py", - ], - data = ["//examples/wheel:minimal_with_py_package"], - deps = [ - ":lib", - ], -) - py_test( name = "wheel_installer_test", size = "small", @@ -50,15 +38,3 @@ py_test( ":lib", ], ) - -py_test( - name = "wheel_test", - size = "small", - srcs = [ - "wheel_test.py", - ], - data = ["//examples/wheel:minimal_with_py_package"], - deps = [ - ":lib", - ], -) diff --git a/tests/pypi/whl_installer/arguments_test.py b/tests/pypi/whl_installer/arguments_test.py index 5538054a59..9f73ae96a9 100644 --- a/tests/pypi/whl_installer/arguments_test.py +++ b/tests/pypi/whl_installer/arguments_test.py @@ -15,7 +15,7 @@ import json import unittest -from python.private.pypi.whl_installer import arguments, wheel +from python.private.pypi.whl_installer import arguments class ArgumentsTestCase(unittest.TestCase): @@ -49,18 +49,6 @@ def test_deserialize_structured_args(self) -> None: self.assertEqual(args["environment"], {"PIP_DO_SOMETHING": "True"}) self.assertEqual(args["extra_pip_args"], []) - def test_platform_aggregation(self) -> None: - parser = arguments.parser() - args = parser.parse_args( - args=[ - "--platform=linux_*", - "--platform=osx_*", - "--platform=windows_*", - "--requirement=foo", - ] - ) - self.assertEqual(set(wheel.Platform.all()), arguments.get_platforms(args)) - if __name__ == "__main__": unittest.main() diff --git a/tests/pypi/whl_installer/platform_test.py b/tests/pypi/whl_installer/platform_test.py deleted file mode 100644 index 2aeb4caa69..0000000000 --- a/tests/pypi/whl_installer/platform_test.py +++ /dev/null @@ -1,154 +0,0 @@ -import unittest -from random import shuffle - -from python.private.pypi.whl_installer.platform import ( - OS, - Arch, - Platform, - host_interpreter_minor_version, -) - - -class MinorVersionTest(unittest.TestCase): - def test_host(self): - host = host_interpreter_minor_version() - self.assertIsNotNone(host) - - -class PlatformTest(unittest.TestCase): - def test_can_get_host(self): - host = Platform.host() - self.assertIsNotNone(host) - self.assertEqual(1, len(Platform.from_string("host"))) - self.assertEqual(host, Platform.from_string("host")) - - def test_can_get_linux_x86_64_without_py_version(self): - got = Platform.from_string("linux_x86_64") - want = Platform(os=OS.linux, arch=Arch.x86_64) - self.assertEqual(want, got[0]) - - def test_can_get_specific_from_string(self): - got = Platform.from_string("cp33_linux_x86_64") - want = Platform(os=OS.linux, arch=Arch.x86_64, minor_version=3) - self.assertEqual(want, got[0]) - - def test_can_get_all_for_py_version(self): - cp39 = Platform.all(minor_version=9) - self.assertEqual(21, len(cp39), f"Got {cp39}") - self.assertEqual(cp39, Platform.from_string("cp39_*")) - - def test_can_get_all_for_os(self): - linuxes = Platform.all(OS.linux, minor_version=9) - self.assertEqual(7, len(linuxes)) - self.assertEqual(linuxes, Platform.from_string("cp39_linux_*")) - - def test_can_get_all_for_os_for_host_python(self): - linuxes = Platform.all(OS.linux) - self.assertEqual(7, len(linuxes)) - self.assertEqual(linuxes, Platform.from_string("linux_*")) - - def test_specific_version_specializations(self): - any_py33 = Platform(minor_version=3) - - # When - all_specializations = list(any_py33.all_specializations()) - - want = ( - [any_py33] - + [ - Platform(arch=arch, minor_version=any_py33.minor_version) - for arch in Arch - ] - + [Platform(os=os, minor_version=any_py33.minor_version) for os in OS] - + Platform.all(minor_version=any_py33.minor_version) - ) - self.assertEqual(want, all_specializations) - - def test_aarch64_specializations(self): - any_aarch64 = Platform(arch=Arch.aarch64) - all_specializations = list(any_aarch64.all_specializations()) - want = [ - Platform(os=None, arch=Arch.aarch64), - Platform(os=OS.linux, arch=Arch.aarch64), - Platform(os=OS.osx, arch=Arch.aarch64), - Platform(os=OS.windows, arch=Arch.aarch64), - ] - self.assertEqual(want, all_specializations) - - def test_linux_specializations(self): - any_linux = Platform(os=OS.linux) - all_specializations = list(any_linux.all_specializations()) - want = [ - Platform(os=OS.linux, arch=None), - Platform(os=OS.linux, arch=Arch.x86_64), - Platform(os=OS.linux, arch=Arch.x86_32), - Platform(os=OS.linux, arch=Arch.aarch64), - Platform(os=OS.linux, arch=Arch.ppc), - Platform(os=OS.linux, arch=Arch.ppc64le), - Platform(os=OS.linux, arch=Arch.s390x), - Platform(os=OS.linux, arch=Arch.arm), - ] - self.assertEqual(want, all_specializations) - - def test_osx_specializations(self): - any_osx = Platform(os=OS.osx) - all_specializations = list(any_osx.all_specializations()) - # NOTE @aignas 2024-01-14: even though in practice we would only have - # Python on osx aarch64 and osx x86_64, we return all arch posibilities - # to make the code simpler. - want = [ - Platform(os=OS.osx, arch=None), - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.x86_32), - Platform(os=OS.osx, arch=Arch.aarch64), - Platform(os=OS.osx, arch=Arch.ppc), - Platform(os=OS.osx, arch=Arch.ppc64le), - Platform(os=OS.osx, arch=Arch.s390x), - Platform(os=OS.osx, arch=Arch.arm), - ] - self.assertEqual(want, all_specializations) - - def test_platform_sort(self): - platforms = [ - Platform(os=OS.linux, arch=None), - Platform(os=OS.linux, arch=Arch.x86_64), - Platform(os=OS.osx, arch=None), - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.aarch64), - ] - shuffle(platforms) - platforms.sort() - want = [ - Platform(os=OS.linux, arch=None), - Platform(os=OS.linux, arch=Arch.x86_64), - Platform(os=OS.osx, arch=None), - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.aarch64), - ] - - self.assertEqual(want, platforms) - - def test_wheel_os_alias(self): - self.assertEqual("osx", str(OS.osx)) - self.assertEqual(str(OS.darwin), str(OS.osx)) - - def test_wheel_arch_alias(self): - self.assertEqual("x86_64", str(Arch.x86_64)) - self.assertEqual(str(Arch.amd64), str(Arch.x86_64)) - - def test_wheel_platform_alias(self): - give = Platform( - os=OS.darwin, - arch=Arch.amd64, - ) - alias = Platform( - os=OS.osx, - arch=Arch.x86_64, - ) - - self.assertEqual("osx_x86_64", str(give)) - self.assertEqual(str(alias), str(give)) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/pypi/whl_installer/wheel_installer_test.py b/tests/pypi/whl_installer/wheel_installer_test.py index 7139779c3e..3c118af3c4 100644 --- a/tests/pypi/whl_installer/wheel_installer_test.py +++ b/tests/pypi/whl_installer/wheel_installer_test.py @@ -22,39 +22,6 @@ from python.private.pypi.whl_installer import wheel_installer -class TestRequirementExtrasParsing(unittest.TestCase): - def test_parses_requirement_for_extra(self) -> None: - cases = [ - ("name[foo]", ("name", frozenset(["foo"]))), - ("name[ Foo123 ]", ("name", frozenset(["Foo123"]))), - (" name1[ foo ] ", ("name1", frozenset(["foo"]))), - ("Name[foo]", ("name", frozenset(["foo"]))), - ("name_foo[bar]", ("name-foo", frozenset(["bar"]))), - ( - "name [fred,bar] @ http://foo.com ; python_version=='2.7'", - ("name", frozenset(["fred", "bar"])), - ), - ( - "name[quux, strange];python_version<'2.7' and platform_version=='2'", - ("name", frozenset(["quux", "strange"])), - ), - ( - "name; (os_name=='a' or os_name=='b') and os_name=='c'", - (None, None), - ), - ( - "name@http://foo.com", - (None, None), - ), - ] - - for case, expected in cases: - with self.subTest(): - self.assertTupleEqual( - wheel_installer._parse_requirement_for_extra(case), expected - ) - - class TestWhlFilegroup(unittest.TestCase): def setUp(self) -> None: self.wheel_name = "example_minimal_package-0.0.1-py3-none-any.whl" @@ -68,10 +35,8 @@ def tearDown(self): def test_wheel_exists(self) -> None: wheel_installer._extract_wheel( Path(self.wheel_path), - installation_dir=Path(self.wheel_dir), - extras={}, enable_implicit_namespace_pkgs=False, - platforms=[], + installation_dir=Path(self.wheel_dir), ) want_files = [ @@ -92,11 +57,8 @@ def test_wheel_exists(self) -> None: metadata_file_content = json.load(metadata_file) want = dict( - version="0.0.1", - name="example-minimal-package", - deps=[], - deps_by_platform={}, entry_points=[], + python_version="3.11.11", ) self.assertEqual(want, metadata_file_content) diff --git a/tests/pypi/whl_installer/wheel_test.py b/tests/pypi/whl_installer/wheel_test.py deleted file mode 100644 index 404218e12b..0000000000 --- a/tests/pypi/whl_installer/wheel_test.py +++ /dev/null @@ -1,371 +0,0 @@ -import unittest -from unittest import mock - -from python.private.pypi.whl_installer import wheel -from python.private.pypi.whl_installer.platform import OS, Arch, Platform - -_HOST_INTERPRETER_FN = ( - "python.private.pypi.whl_installer.wheel.host_interpreter_minor_version" -) - - -class DepsTest(unittest.TestCase): - def test_simple(self): - deps = wheel.Deps("foo", requires_dist=["bar"]) - - got = deps.build() - - self.assertIsInstance(got, wheel.FrozenDeps) - self.assertEqual(["bar"], got.deps) - self.assertEqual({}, got.deps_select) - - def test_can_add_os_specific_deps(self): - deps = wheel.Deps( - "foo", - requires_dist=[ - "bar", - "an_osx_dep; sys_platform=='darwin'", - "posix_dep; os_name=='posix'", - "win_dep; os_name=='nt'", - ], - platforms={ - Platform(os=OS.linux, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.aarch64), - Platform(os=OS.windows, arch=Arch.x86_64), - }, - ) - - got = deps.build() - - self.assertEqual(["bar"], got.deps) - self.assertEqual( - { - "@platforms//os:linux": ["posix_dep"], - "@platforms//os:osx": ["an_osx_dep", "posix_dep"], - "@platforms//os:windows": ["win_dep"], - }, - got.deps_select, - ) - - def test_can_add_os_specific_deps_with_specific_python_version(self): - deps = wheel.Deps( - "foo", - requires_dist=[ - "bar", - "an_osx_dep; sys_platform=='darwin'", - "posix_dep; os_name=='posix'", - "win_dep; os_name=='nt'", - ], - platforms={ - Platform(os=OS.linux, arch=Arch.x86_64, minor_version=8), - Platform(os=OS.osx, arch=Arch.x86_64, minor_version=8), - Platform(os=OS.osx, arch=Arch.aarch64, minor_version=8), - Platform(os=OS.windows, arch=Arch.x86_64, minor_version=8), - }, - ) - - got = deps.build() - - self.assertEqual(["bar"], got.deps) - self.assertEqual( - { - "@platforms//os:linux": ["posix_dep"], - "@platforms//os:osx": ["an_osx_dep", "posix_dep"], - "@platforms//os:windows": ["win_dep"], - }, - got.deps_select, - ) - - def test_deps_are_added_to_more_specialized_platforms(self): - got = wheel.Deps( - "foo", - requires_dist=[ - "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", - "mac_dep; sys_platform=='darwin'", - ], - platforms={ - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.aarch64), - }, - ).build() - - self.assertEqual( - wheel.FrozenDeps( - deps=[], - deps_select={ - "osx_aarch64": ["m1_dep", "mac_dep"], - "@platforms//os:osx": ["mac_dep"], - }, - ), - got, - ) - - def test_deps_from_more_specialized_platforms_are_propagated(self): - got = wheel.Deps( - "foo", - requires_dist=[ - "a_mac_dep; sys_platform=='darwin'", - "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", - ], - platforms={ - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.aarch64), - }, - ).build() - - self.assertEqual([], got.deps) - self.assertEqual( - { - "osx_aarch64": ["a_mac_dep", "m1_dep"], - "@platforms//os:osx": ["a_mac_dep"], - }, - got.deps_select, - ) - - def test_non_platform_markers_are_added_to_common_deps(self): - got = wheel.Deps( - "foo", - requires_dist=[ - "bar", - "baz; implementation_name=='cpython'", - "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", - ], - platforms={ - Platform(os=OS.linux, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.aarch64), - Platform(os=OS.windows, arch=Arch.x86_64), - }, - ).build() - - self.assertEqual(["bar", "baz"], got.deps) - self.assertEqual( - { - "osx_aarch64": ["m1_dep"], - }, - got.deps_select, - ) - - def test_self_is_ignored(self): - deps = wheel.Deps( - "foo", - requires_dist=[ - "bar", - "req_dep; extra == 'requests'", - "foo[requests]; extra == 'ssl'", - "ssl_lib; extra == 'ssl'", - ], - extras={"ssl"}, - ) - - got = deps.build() - - self.assertEqual(["bar", "req_dep", "ssl_lib"], got.deps) - self.assertEqual({}, got.deps_select) - - def test_self_dependencies_can_come_in_any_order(self): - deps = wheel.Deps( - "foo", - requires_dist=[ - "bar", - "baz; extra == 'feat'", - "foo[feat2]; extra == 'all'", - "foo[feat]; extra == 'feat2'", - "zdep; extra == 'all'", - ], - extras={"all"}, - ) - - got = deps.build() - - self.assertEqual(["bar", "baz", "zdep"], got.deps) - self.assertEqual({}, got.deps_select) - - def test_can_get_deps_based_on_specific_python_version(self): - requires_dist = [ - "bar", - "baz; python_version < '3.8'", - "posix_dep; os_name=='posix' and python_version >= '3.8'", - ] - - py38_deps = wheel.Deps( - "foo", - requires_dist=requires_dist, - platforms=[ - Platform(os=OS.linux, arch=Arch.x86_64, minor_version=8), - ], - ).build() - py37_deps = wheel.Deps( - "foo", - requires_dist=requires_dist, - platforms=[ - Platform(os=OS.linux, arch=Arch.x86_64, minor_version=7), - ], - ).build() - - self.assertEqual(["bar", "baz"], py37_deps.deps) - self.assertEqual({}, py37_deps.deps_select) - self.assertEqual(["bar"], py38_deps.deps) - self.assertEqual({"@platforms//os:linux": ["posix_dep"]}, py38_deps.deps_select) - - @mock.patch(_HOST_INTERPRETER_FN) - def test_no_version_select_when_single_version(self, mock_host_interpreter_version): - requires_dist = [ - "bar", - "baz; python_version >= '3.8'", - "posix_dep; os_name=='posix'", - "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", - "arch_dep; platform_machine=='x86_64' and python_version >= '3.8'", - ] - mock_host_interpreter_version.return_value = 7 - - self.maxDiff = None - - deps = wheel.Deps( - "foo", - requires_dist=requires_dist, - platforms=[ - Platform(os=os, arch=Arch.x86_64, minor_version=minor) - for minor in [8] - for os in [OS.linux, OS.windows] - ], - ) - got = deps.build() - - self.assertEqual(["bar", "baz"], got.deps) - self.assertEqual( - { - "@platforms//os:linux": ["posix_dep", "posix_dep_with_version"], - "linux_x86_64": ["arch_dep", "posix_dep", "posix_dep_with_version"], - "windows_x86_64": ["arch_dep"], - }, - got.deps_select, - ) - - @mock.patch(_HOST_INTERPRETER_FN) - def test_can_get_version_select(self, mock_host_interpreter_version): - requires_dist = [ - "bar", - "baz; python_version < '3.8'", - "baz_new; python_version >= '3.8'", - "posix_dep; os_name=='posix'", - "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", - "arch_dep; platform_machine=='x86_64' and python_version < '3.8'", - ] - mock_host_interpreter_version.return_value = 7 - - self.maxDiff = None - - deps = wheel.Deps( - "foo", - requires_dist=requires_dist, - platforms=[ - Platform(os=os, arch=Arch.x86_64, minor_version=minor) - for minor in [7, 8, 9] - for os in [OS.linux, OS.windows] - ], - ) - got = deps.build() - - self.assertEqual(["bar"], got.deps) - self.assertEqual( - { - "//conditions:default": ["baz"], - "@//python/config_settings:is_python_3.7": ["baz"], - "@//python/config_settings:is_python_3.8": ["baz_new"], - "@//python/config_settings:is_python_3.9": ["baz_new"], - "@platforms//os:linux": ["baz", "posix_dep"], - "cp37_linux_x86_64": ["arch_dep", "baz", "posix_dep"], - "cp37_windows_x86_64": ["arch_dep", "baz"], - "cp37_linux_anyarch": ["baz", "posix_dep"], - "cp38_linux_anyarch": [ - "baz_new", - "posix_dep", - "posix_dep_with_version", - ], - "cp39_linux_anyarch": [ - "baz_new", - "posix_dep", - "posix_dep_with_version", - ], - "linux_x86_64": ["arch_dep", "baz", "posix_dep"], - "windows_x86_64": ["arch_dep", "baz"], - }, - got.deps_select, - ) - - @mock.patch(_HOST_INTERPRETER_FN) - def test_deps_spanning_all_target_py_versions_are_added_to_common( - self, mock_host_version - ): - requires_dist = [ - "bar", - "baz (<2,>=1.11) ; python_version < '3.8'", - "baz (<2,>=1.14) ; python_version >= '3.8'", - ] - mock_host_version.return_value = 8 - - deps = wheel.Deps( - "foo", - requires_dist=requires_dist, - platforms=Platform.from_string(["cp37_*", "cp38_*", "cp39_*"]), - ) - got = deps.build() - - self.assertEqual(["bar", "baz"], got.deps) - self.assertEqual({}, got.deps_select) - - @mock.patch(_HOST_INTERPRETER_FN) - def test_deps_are_not_duplicated(self, mock_host_version): - mock_host_version.return_value = 7 - - # See an example in - # https://files.pythonhosted.org/packages/76/9e/db1c2d56c04b97981c06663384f45f28950a73d9acf840c4006d60d0a1ff/opencv_python-4.9.0.80-cp37-abi3-win32.whl.metadata - requires_dist = [ - "bar >=0.1.0 ; python_version < '3.7'", - "bar >=0.2.0 ; python_version >= '3.7'", - "bar >=0.4.0 ; python_version >= '3.6' and platform_system == 'Linux' and platform_machine == 'aarch64'", - "bar >=0.4.0 ; python_version >= '3.9'", - "bar >=0.5.0 ; python_version <= '3.9' and platform_system == 'Darwin' and platform_machine == 'arm64'", - "bar >=0.5.0 ; python_version >= '3.10' and platform_system == 'Darwin'", - "bar >=0.5.0 ; python_version >= '3.10'", - "bar >=0.6.0 ; python_version >= '3.11'", - ] - - deps = wheel.Deps( - "foo", - requires_dist=requires_dist, - platforms=Platform.from_string(["cp37_*", "cp310_*"]), - ) - got = deps.build() - - self.assertEqual(["bar"], got.deps) - self.assertEqual({}, got.deps_select) - - @mock.patch(_HOST_INTERPRETER_FN) - def test_deps_are_not_duplicated_when_encountering_platform_dep_first( - self, mock_host_version - ): - mock_host_version.return_value = 7 - - # Note, that we are sorting the incoming `requires_dist` and we need to ensure that we are not getting any - # issues even if the platform-specific line comes first. - requires_dist = [ - "bar >=0.4.0 ; python_version >= '3.6' and platform_system == 'Linux' and platform_machine == 'aarch64'", - "bar >=0.5.0 ; python_version >= '3.9'", - ] - - deps = wheel.Deps( - "foo", - requires_dist=requires_dist, - platforms=Platform.from_string(["cp37_*", "cp310_*"]), - ) - got = deps.build() - - self.assertEqual(["bar"], got.deps) - self.assertEqual({}, got.deps_select) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/pypi/whl_metadata/BUILD.bazel b/tests/pypi/whl_metadata/BUILD.bazel new file mode 100644 index 0000000000..3f1d665dd2 --- /dev/null +++ b/tests/pypi/whl_metadata/BUILD.bazel @@ -0,0 +1,5 @@ +load(":whl_metadata_tests.bzl", "whl_metadata_test_suite") + +whl_metadata_test_suite( + name = "whl_metadata_tests", +) diff --git a/tests/pypi/whl_metadata/whl_metadata_tests.bzl b/tests/pypi/whl_metadata/whl_metadata_tests.bzl new file mode 100644 index 0000000000..4acbc9213d --- /dev/null +++ b/tests/pypi/whl_metadata/whl_metadata_tests.bzl @@ -0,0 +1,147 @@ +"" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:truth.bzl", "subjects") +load( + "//python/private/pypi:whl_metadata.bzl", + "find_whl_metadata", + "parse_whl_metadata", +) # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_empty(env): + fake_path = struct( + basename = "site-packages", + readdir = lambda watch = None: [], + ) + fail_messages = [] + find_whl_metadata(install_dir = fake_path, logger = struct( + fail = fail_messages.append, + )) + env.expect.that_collection(fail_messages).contains_exactly([ + "The '*.dist-info' directory could not be found in 'site-packages'", + ]) + +_tests.append(_test_empty) + +def _test_contains_dist_info_but_no_metadata(env): + fake_path = struct( + basename = "site-packages", + readdir = lambda watch = None: [ + struct( + basename = "something.dist-info", + is_dir = True, + get_child = lambda basename: struct( + basename = basename, + exists = False, + ), + ), + ], + ) + fail_messages = [] + find_whl_metadata(install_dir = fake_path, logger = struct( + fail = fail_messages.append, + )) + env.expect.that_collection(fail_messages).contains_exactly([ + "The METADATA file for the wheel could not be found in 'site-packages/something.dist-info'", + ]) + +_tests.append(_test_contains_dist_info_but_no_metadata) + +def _test_contains_metadata(env): + fake_path = struct( + basename = "site-packages", + readdir = lambda watch = None: [ + struct( + basename = "something.dist-info", + is_dir = True, + get_child = lambda basename: struct( + basename = basename, + exists = True, + ), + ), + ], + ) + fail_messages = [] + got = find_whl_metadata(install_dir = fake_path, logger = struct( + fail = fail_messages.append, + )) + env.expect.that_collection(fail_messages).contains_exactly([]) + env.expect.that_str(got.basename).equals("METADATA") + +_tests.append(_test_contains_metadata) + +def _parse_whl_metadata(env, **kwargs): + result = parse_whl_metadata(**kwargs) + + return env.expect.that_struct( + struct( + name = result.name, + version = result.version, + requires_dist = result.requires_dist, + provides_extra = result.provides_extra, + ), + attrs = dict( + name = subjects.str, + version = subjects.str, + requires_dist = subjects.collection, + provides_extra = subjects.collection, + ), + ) + +def _test_parse_metadata_invalid(env): + got = _parse_whl_metadata( + env, + contents = "", + ) + got.name().equals("") + got.version().equals("") + got.requires_dist().contains_exactly([]) + got.provides_extra().contains_exactly([]) + +_tests.append(_test_parse_metadata_invalid) + +def _test_parse_metadata_basic(env): + got = _parse_whl_metadata( + env, + contents = """\ +Name: foo +Version: 0.0.1 +""", + ) + got.name().equals("foo") + got.version().equals("0.0.1") + got.requires_dist().contains_exactly([]) + got.provides_extra().contains_exactly([]) + +_tests.append(_test_parse_metadata_basic) + +def _test_parse_metadata_all(env): + got = _parse_whl_metadata( + env, + contents = """\ +Name: foo +Version: 0.0.1 +Requires-Dist: bar; extra == "all" +Provides-Extra: all + +Requires-Dist: this will be ignored +""", + ) + got.name().equals("foo") + got.version().equals("0.0.1") + got.requires_dist().contains_exactly([ + "bar; extra == \"all\"", + ]) + got.provides_extra().contains_exactly([ + "all", + ]) + +_tests.append(_test_parse_metadata_all) + +def whl_metadata_test_suite(name): # buildifier: disable=function-docstring + test_suite( + name = name, + basic_tests = _tests, + ) From 79abef898ece1a6ae2af8cb855418ac342dd27d8 Mon Sep 17 00:00:00 2001 From: Ivo List Date: Tue, 15 Apr 2025 04:21:33 +0200 Subject: [PATCH 025/156] fix: replace string with modern providers in tests (#2773) Strings used to refer to legacy struct providers, which were removed from Bazel. Legacy struct providers have been deprecated by Bazel. Replacing them with modern providers, will make it possible to simplify and remove legacy handling from Blaze. The change is a no-op. More information: https://github.com/bazelbuild/bazel/issues/25836 --- tests/builders/attr_builders_tests.bzl | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/builders/attr_builders_tests.bzl b/tests/builders/attr_builders_tests.bzl index 58557cd633..e92ba2ae0a 100644 --- a/tests/builders/attr_builders_tests.bzl +++ b/tests/builders/attr_builders_tests.bzl @@ -28,6 +28,7 @@ def _expect_cfg_defaults(expect, cfg): expect.where(expr = "cfg.which_cfg").that_str(cfg.which_cfg()).equals("target") _some_aspect = aspect(implementation = lambda target, ctx: None) +_SomeInfo = provider("MyInfo", fields = []) _tests = [] @@ -186,7 +187,7 @@ def _test_label(name): subject.set_executable(True) subject.add_allow_files(".txt") subject.cfg.set_target() - subject.providers().append("provider") + subject.providers().append(_SomeInfo) subject.aspects().append(_some_aspect) subject.cfg.outputs().append(Label("//some:output")) subject.cfg.inputs().append(Label("//some:input")) @@ -199,7 +200,7 @@ def _test_label(name): expect.that_bool(subject.executable()).equals(True) expect.that_collection(subject.allow_files()).contains_exactly([".txt"]) expect.that_bool(subject.allow_single_file()).equals(None) - expect.that_collection(subject.providers()).contains_exactly(["provider"]) + expect.that_collection(subject.providers()).contains_exactly([_SomeInfo]) expect.that_collection(subject.aspects()).contains_exactly([_some_aspect]) expect.that_collection(subject.cfg.outputs()).contains_exactly([Label("//some:output")]) expect.that_collection(subject.cfg.inputs()).contains_exactly([Label("//some:input")]) @@ -229,7 +230,7 @@ def _test_label_keyed_string_dict(name): subject.set_mandatory(True) subject.set_allow_files(True) subject.cfg.set_target() - subject.providers().append("provider") + subject.providers().append(_SomeInfo) subject.aspects().append(_some_aspect) subject.cfg.outputs().append("//some:output") subject.cfg.inputs().append("//some:input") @@ -240,7 +241,7 @@ def _test_label_keyed_string_dict(name): expect.that_str(subject.doc()).equals("doc") expect.that_bool(subject.mandatory()).equals(True) expect.that_bool(subject.allow_files()).equals(True) - expect.that_collection(subject.providers()).contains_exactly(["provider"]) + expect.that_collection(subject.providers()).contains_exactly([_SomeInfo]) expect.that_collection(subject.aspects()).contains_exactly([_some_aspect]) expect.that_collection(subject.cfg.outputs()).contains_exactly(["//some:output"]) expect.that_collection(subject.cfg.inputs()).contains_exactly(["//some:input"]) @@ -274,14 +275,14 @@ def _test_label_list(name): subject.set_doc("doc") subject.set_mandatory(True) subject.set_allow_files([".txt"]) - subject.providers().append("provider") + subject.providers().append(_SomeInfo) subject.aspects().append(_some_aspect) expect.that_collection(subject.default()).contains_exactly(["//some:label"]) expect.that_str(subject.doc()).equals("doc") expect.that_bool(subject.mandatory()).equals(True) expect.that_collection(subject.allow_files()).contains_exactly([".txt"]) - expect.that_collection(subject.providers()).contains_exactly(["provider"]) + expect.that_collection(subject.providers()).contains_exactly([_SomeInfo]) expect.that_collection(subject.aspects()).contains_exactly([_some_aspect]) _expect_builds(expect, subject, "attr.label_list") @@ -395,14 +396,14 @@ def _test_string_keyed_label_dict(name): subject.set_doc("doc") subject.set_mandatory(True) subject.set_allow_files([".txt"]) - subject.providers().append("provider") + subject.providers().append(_SomeInfo) subject.aspects().append(_some_aspect) expect.that_dict(subject.default()).contains_exactly({"key": "//some:label"}) expect.that_str(subject.doc()).equals("doc") expect.that_bool(subject.mandatory()).equals(True) expect.that_collection(subject.allow_files()).contains_exactly([".txt"]) - expect.that_collection(subject.providers()).contains_exactly(["provider"]) + expect.that_collection(subject.providers()).contains_exactly([_SomeInfo]) expect.that_collection(subject.aspects()).contains_exactly([_some_aspect]) _expect_builds(expect, subject, "attr.string_keyed_label_dict") From a0400e9a832d554de032fe44d8b8375ceaa32db8 Mon Sep 17 00:00:00 2001 From: Frank Portman Date: Tue, 15 Apr 2025 04:37:01 -0400 Subject: [PATCH 026/156] feat(toolchain): Add new make vars for Python interpreter path compliant with `--no_legacy_external_runfiles` (#2772) Using these new make vars in `py_binary` or `py_test` will correctly find the interpreter when setting `--no_legacy_external_runfiles`. Fixes #2728 --- CHANGELOG.md | 2 ++ docs/toolchains.md | 6 +++++- python/current_py_toolchain.bzl | 7 +++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33d99dfaa1..6f86851bdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -124,6 +124,8 @@ Unreleased changes template. * (toolchains) Local Python installs can be used to create a toolchain equivalent to the standard toolchains. See [Local toolchains] docs for how to configure them. +* (toolchains) Expose `$(PYTHON2_ROOTPATH)` and `$(PYTHON3_ROOTPATH)` which are runfiles + locations equivalents of `$(PYTHON2)` and `$(PYTHON3) respectively. {#v0-0-0-removed} diff --git a/docs/toolchains.md b/docs/toolchains.md index 5cd9eb268e..320e16335b 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -215,7 +215,11 @@ attribute. You can obtain the path to the Python interpreter using the `$(PYTHON2)` and `$(PYTHON3)` ["Make" Variables](https://bazel.build/reference/be/make-variables). See the {gh-path}`test_current_py_toolchain ` target -for an example. +for an example. We also make available `$(PYTHON2_ROOTPATH)` and `$(PYTHON3_ROOTPATH)` +which are Make Variable equivalents of `$(PYTHON2)` and `$(PYTHON3)` but for runfiles +locations. These will be helpful if you need to set env vars of binary/test rules +while using [`--nolegacy_external_runfiles`](https://bazel.build/reference/command-line-reference#flag--legacy_external_runfiles). +The original make variables still work in exec contexts such as genrules. ### Overriding toolchain defaults and adding more versions diff --git a/python/current_py_toolchain.bzl b/python/current_py_toolchain.bzl index f3ff2ace07..f5c5638a88 100644 --- a/python/current_py_toolchain.bzl +++ b/python/current_py_toolchain.bzl @@ -27,11 +27,13 @@ def _current_py_toolchain_impl(ctx): direct.append(toolchain.py3_runtime.interpreter) transitive.append(toolchain.py3_runtime.files) vars["PYTHON3"] = toolchain.py3_runtime.interpreter.path + vars["PYTHON3_ROOTPATH"] = toolchain.py3_runtime.interpreter.short_path if toolchain.py2_runtime and toolchain.py2_runtime.interpreter: direct.append(toolchain.py2_runtime.interpreter) transitive.append(toolchain.py2_runtime.files) vars["PYTHON2"] = toolchain.py2_runtime.interpreter.path + vars["PYTHON2_ROOTPATH"] = toolchain.py2_runtime.interpreter.short_path files = depset(direct, transitive = transitive) return [ @@ -49,6 +51,11 @@ current_py_toolchain = rule( other rules, such as genrule. It allows exposing a python toolchain after toolchain resolution has happened, to a rule which expects a concrete implementation of a toolchain, rather than a toolchain_type which could be resolved to that toolchain. + + :::{versionchanged} VERSION_NEXT_FEATURE + From now on, we also expose `$(PYTHON2_ROOTPATH)` and `$(PYTHON3_ROOTPATH)` which are runfiles + locations equivalents of `$(PYTHON2)` and `$(PYTHON3) respectively. + ::: """, implementation = _current_py_toolchain_impl, attrs = { From ccf3141bbe85f1bd7396febe08ff367101826205 Mon Sep 17 00:00:00 2001 From: Frank Portman Date: Tue, 15 Apr 2025 04:38:54 -0400 Subject: [PATCH 027/156] fix(packaging): Format `METADATA` correctly if given empty `requires_file` (#2771) An empty `requires_file` used to be okay, but at some point regressed to leaving an empty line (due to the `metadata.replace(...)`) in the `METADATA` file - rendering the wheel uninstallable. This PR initially attempted to solve that by introducing a new list that processed `METADATA` lines go into, rather than relying on repeated string replacement. But it seems like the repeated string replace actually did more than simply process one line at a time, so I reverted to a single substitution at the end. --- CHANGELOG.md | 1 + examples/wheel/BUILD.bazel | 16 ++++++++++++++++ examples/wheel/wheel_test.py | 24 +++++++++++++++++++++++- python/packaging.bzl | 5 +++++ tools/wheelmaker.py | 7 ++++++- 5 files changed, 51 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f86851bdf..e7f9fe30e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,7 @@ Unreleased changes template. * (toolchains) Run the check on the Python interpreter in isolated mode, to ensure it's not affected by userland environment variables, such as `PYTHONPATH`. * (toolchains) Ensure temporary `.pyc` and `.pyo` files are also excluded from the interpreters repository files. * (pypi) Run interpreter version call in isolated mode, to ensure it's not affected by userland environment variables, such as `PYTHONPATH`. +* (packaging) An empty `requires_file` is treated as if it were omitted, resulting in a valid `METADATA` file. {#v0-0-0-added} ### Added diff --git a/examples/wheel/BUILD.bazel b/examples/wheel/BUILD.bazel index d9ba800125..b434e67405 100644 --- a/examples/wheel/BUILD.bazel +++ b/examples/wheel/BUILD.bazel @@ -294,6 +294,12 @@ starlark # Example comment """.splitlines(), ) +write_file( + name = "empty_requires_file", + out = "empty_requires.txt", + content = [""], +) + write_file( name = "extra_requires_file", out = "extra_requires.txt", @@ -324,6 +330,15 @@ py_wheel( deps = [":example_pkg"], ) +py_wheel( + name = "empty_requires_files", + distribution = "empty_requires_files", + python_tag = "py3", + requires_file = ":empty_requires.txt", + version = "0.0.1", + deps = [":example_pkg"], +) + # Package just a specific py_libraries, without their dependencies py_wheel( name = "minimal_data_files", @@ -367,6 +382,7 @@ py_test( ":custom_package_root_multi_prefix", ":custom_package_root_multi_prefix_reverse_order", ":customized", + ":empty_requires_files", ":extra_requires", ":filename_escaping", ":minimal_data_files", diff --git a/examples/wheel/wheel_test.py b/examples/wheel/wheel_test.py index a3d6034930..9ec150301d 100644 --- a/examples/wheel/wheel_test.py +++ b/examples/wheel/wheel_test.py @@ -483,7 +483,6 @@ def test_requires_file_and_extra_requires_files(self): if line.startswith(b"Requires-Dist:"): requires.append(line.decode("utf-8").strip()) - print(requires) self.assertEqual( [ "Requires-Dist: tomli>=2.0.0", @@ -495,6 +494,29 @@ def test_requires_file_and_extra_requires_files(self): requires, ) + def test_empty_requires_file(self): + filename = self._get_path("empty_requires_files-0.0.1-py3-none-any.whl") + + with zipfile.ZipFile(filename) as zf: + self.assertAllEntriesHasReproducibleMetadata(zf) + metadata_file = None + for f in zf.namelist(): + if os.path.basename(f) == "METADATA": + metadata_file = f + self.assertIsNotNone(metadata_file) + + metadata = zf.read(metadata_file).decode("utf-8") + metadata_lines = metadata.splitlines() + + requires = [] + for i, line in enumerate(metadata_lines): + if line.startswith("Name:"): + self.assertTrue(metadata_lines[i + 1].startswith("Version:")) + if line.startswith("Requires-Dist:"): + requires.append(line.strip()) + + self.assertEqual([], requires) + def test_minimal_data_files(self): filename = self._get_path("minimal_data_files-0.0.1-py3-none-any.whl") diff --git a/python/packaging.bzl b/python/packaging.bzl index 629af2d6a4..b190635cfe 100644 --- a/python/packaging.bzl +++ b/python/packaging.bzl @@ -101,6 +101,11 @@ def py_wheel( Currently only pure-python wheels are supported. + :::{versionchanged} VERSION_NEXT_FEATURE + From now on, an empty `requires_file` is treated as if it were omitted, resulting in a valid + `METADATA` file. + ::: + Examples: ```python diff --git a/tools/wheelmaker.py b/tools/wheelmaker.py index 23b18eca5f..908b3fe956 100644 --- a/tools/wheelmaker.py +++ b/tools/wheelmaker.py @@ -599,7 +599,12 @@ def get_new_requirement_line(reqs_text, extra): reqs.append(get_new_requirement_line(reqs_text, extra)) - metadata = metadata.replace(meta_line, "\n".join(reqs)) + if reqs: + metadata = metadata.replace(meta_line, "\n".join(reqs)) + # File is empty + # So replace the meta_line entirely, including removing newline chars + else: + metadata = re.sub(re.escape(meta_line) + r"(?:\r?\n)?", "", metadata, count=1) maker.add_metadata( metadata=metadata, From ff1388356b0d47b6249dc606ae4ba521df54a06f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:40:02 +0900 Subject: [PATCH 028/156] build(deps): bump typing-extensions from 4.12.2 to 4.13.2 in /docs (#2776) Bumps [typing-extensions](https://github.com/python/typing_extensions) from 4.12.2 to 4.13.2.
Release notes

Sourced from typing-extensions's releases.

4.13.2

  • Fix TypeError when taking the union of typing_extensions.TypeAliasType and a typing.TypeAliasType on Python 3.12 and 3.13. Patch by Joren Hammudoglu.
  • Backport from CPython PR #132160 to avoid having user arguments shadowed in generated __new__ by @typing_extensions.deprecated. Patch by Victorien Plot.

4.13.1

This is a bugfix release fixing two edge cases that appear on old bugfix releases of CPython.

Bugfixes:

  • Fix regression in 4.13.0 on Python 3.10.2 causing a TypeError when using Concatenate. Patch by Daraan.
  • Fix TypeError when using evaluate_forward_ref on Python 3.10.1-2 and 3.9.8-10. Patch by Daraan.

4.13.0

New features:

  • Add typing_extensions.TypeForm from PEP 747. Patch by Jelle Zijlstra.
  • Add typing_extensions.get_annotations, a backport of inspect.get_annotations that adds features specified by PEP 649. Patches by Jelle Zijlstra and Alex Waygood.
  • Backport evaluate_forward_ref from CPython PR #119891 to evaluate ForwardRefs. Patch by Daraan, backporting a CPython PR by Jelle Zijlstra.

Bugfixes and changed features:

  • Update PEP 728 implementation to a newer version of the PEP. Patch by Jelle Zijlstra.
  • Copy the coroutine status of functions and methods wrapped with @typing_extensions.deprecated. Patch by Sebastian Rittau.
  • Fix bug where TypeAliasType instances could be subscripted even where they were not generic. Patch by Daraan.
  • Fix bug where a subscripted TypeAliasType instance did not have all attributes of the original TypeAliasType instance on older Python versions. Patch by Daraan and Alex Waygood.
  • Fix bug where subscripted TypeAliasType instances (and some other subscripted objects) had wrong parameters if they were directly subscripted with an Unpack object. Patch by Daraan.
  • Backport to Python 3.10 the ability to substitute ... in generic Callable aliases that have a Concatenate special form as their argument. Patch by Daraan.
  • Extended the Concatenate backport for Python 3.8-3.10 to now accept Ellipsis as an argument. Patch by Daraan.
  • Fix backport of get_type_hints to reflect Python 3.11+ behavior which does not add

... (truncated)

Changelog

Sourced from typing-extensions's changelog.

Release 4.13.2 (April 10, 2025)

  • Fix TypeError when taking the union of typing_extensions.TypeAliasType and a typing.TypeAliasType on Python 3.12 and 3.13. Patch by Joren Hammudoglu.
  • Backport from CPython PR #132160 to avoid having user arguments shadowed in generated __new__ by @typing_extensions.deprecated. Patch by Victorien Plot.

Release 4.13.1 (April 3, 2025)

Bugfixes:

  • Fix regression in 4.13.0 on Python 3.10.2 causing a TypeError when using Concatenate. Patch by Daraan.
  • Fix TypeError when using evaluate_forward_ref on Python 3.10.1-2 and 3.9.8-10. Patch by Daraan.

Release 4.13.0 (March 25, 2025)

No user-facing changes since 4.13.0rc1.

Release 4.13.0rc1 (March 18, 2025)

New features:

  • Add typing_extensions.TypeForm from PEP 747. Patch by Jelle Zijlstra.
  • Add typing_extensions.get_annotations, a backport of inspect.get_annotations that adds features specified by PEP 649. Patches by Jelle Zijlstra and Alex Waygood.
  • Backport evaluate_forward_ref from CPython PR #119891 to evaluate ForwardRefs. Patch by Daraan, backporting a CPython PR by Jelle Zijlstra.

Bugfixes and changed features:

  • Update PEP 728 implementation to a newer version of the PEP. Patch by Jelle Zijlstra.
  • Copy the coroutine status of functions and methods wrapped with @typing_extensions.deprecated. Patch by Sebastian Rittau.
  • Fix bug where TypeAliasType instances could be subscripted even where they were not generic. Patch by Daraan.
  • Fix bug where a subscripted TypeAliasType instance did not have all attributes of the original TypeAliasType instance on older Python versions. Patch by Daraan and Alex Waygood.
  • Fix bug where subscripted TypeAliasType instances (and some other subscripted objects) had wrong parameters if they were directly subscripted with an Unpack object. Patch by Daraan.
  • Backport to Python 3.10 the ability to substitute ... in generic Callable

... (truncated)

Commits
  • 4525e9d Prepare release 4.13.2 (#583)
  • 88a0c20 Do not shadow user arguments in generated __new__ by @deprecated (#581)
  • 281d7b0 Add 3rd party tests for litestar (#578)
  • 8092c39 fix TypeAliasType union with typing.TypeAliasType (#575)
  • 45a8847 Prepare release 4.13.1 (#573)
  • f264e58 Move CI to "ubuntu-latest" (round 2) (#570)
  • 5ce0e69 Fix TypeError with evaluate_forward_ref on some 3.10 and 3.9 versions (#558)
  • 304f5cb Add SQLAlchemy to third-party daily tests (#561)
  • ebe2b94 Fix duplicated keywords for typing._ConcatenateGenericAlias in 3.10.2 (#557)
  • 9f93d6f Add intersphinx links for 3.13 typing features (#550)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typing-extensions&package-manager=pip&previous-version=4.12.2&new-version=4.13.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 8d1cbabffc..e2fb59565a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -351,9 +351,9 @@ sphinxcontrib-serializinghtml==2.0.0 \ --hash=sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 \ --hash=sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d # via sphinx -typing-extensions==4.12.2 \ - --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ - --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 +typing-extensions==4.13.2 \ + --hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \ + --hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef # via # rules-python-docs (docs/pyproject.toml) # sphinx-autodoc2 From 2cf7ba4bb76f630ff7f2c83cab0b5294db65107b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:40:24 +0900 Subject: [PATCH 029/156] build(deps): bump urllib3 from 2.3.0 to 2.4.0 in /tools/publish (#2775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.3.0 to 2.4.0.
Release notes

Sourced from urllib3's releases.

2.4.0

🚀 urllib3 is fundraising for HTTP/2 support

urllib3 is raising ~$40,000 USD to release HTTP/2 support and ensure long-term sustainable maintenance of the project after a sharp decline in financial support. If your company or organization uses Python and would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and thousands of other projects please consider contributing financially to ensure HTTP/2 support is developed sustainably and maintained for the long-haul.

Thank you for your support.

Features

  • Applied PEP 639 by specifying the license fields in pyproject.toml. (#3522)
  • Updated exceptions to save and restore more properties during the pickle/serialization process. (#3567)
  • Added verify_flags option to create_urllib3_context with a default of VERIFY_X509_PARTIAL_CHAIN and VERIFY_X509_STRICT for Python 3.13+. (#3571)

Bugfixes

  • Fixed a bug with partial reads of streaming data in Emscripten. (#3555)

Misc

  • Switched to uv for installing development dependecies. (#3550)
  • Removed the multiple.intoto.jsonl asset from GitHub releases. Attestation of release files since v2.3.0 can be found on PyPI. (#3566)
Changelog

Sourced from urllib3's changelog.

2.4.0 (2025-04-10)

Features

  • Applied PEP 639 by specifying the license fields in pyproject.toml. ([#3522](https://github.com/urllib3/urllib3/issues/3522) <https://github.com/urllib3/urllib3/issues/3522>__)
  • Updated exceptions to save and restore more properties during the pickle/serialization process. ([#3567](https://github.com/urllib3/urllib3/issues/3567) <https://github.com/urllib3/urllib3/issues/3567>__)
  • Added verify_flags option to create_urllib3_context with a default of VERIFY_X509_PARTIAL_CHAIN and VERIFY_X509_STRICT for Python 3.13+. ([#3571](https://github.com/urllib3/urllib3/issues/3571) <https://github.com/urllib3/urllib3/issues/3571>__)

Bugfixes

  • Fixed a bug with partial reads of streaming data in Emscripten. ([#3555](https://github.com/urllib3/urllib3/issues/3555) <https://github.com/urllib3/urllib3/issues/3555>__)

Misc

  • Switched to uv for installing development dependecies. ([#3550](https://github.com/urllib3/urllib3/issues/3550) <https://github.com/urllib3/urllib3/issues/3550>__)
  • Removed the multiple.intoto.jsonl asset from GitHub releases. Attestation of release files since v2.3.0 can be found on PyPI. ([#3566](https://github.com/urllib3/urllib3/issues/3566) <https://github.com/urllib3/urllib3/issues/3566>__)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=urllib3&package-manager=pip&previous-version=2.3.0&new-version=2.4.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tools/publish/requirements_darwin.txt | 6 +++--- tools/publish/requirements_linux.txt | 6 +++--- tools/publish/requirements_universal.txt | 6 +++--- tools/publish/requirements_windows.txt | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tools/publish/requirements_darwin.txt b/tools/publish/requirements_darwin.txt index 5f8a33c3f5..eaec72c01c 100644 --- a/tools/publish/requirements_darwin.txt +++ b/tools/publish/requirements_darwin.txt @@ -202,9 +202,9 @@ twine==5.1.1 \ --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db # via -r tools/publish/requirements.in -urllib3==2.3.0 \ - --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ - --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d +urllib3==2.4.0 \ + --hash=sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466 \ + --hash=sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813 # via # requests # twine diff --git a/tools/publish/requirements_linux.txt b/tools/publish/requirements_linux.txt index 40d987b16d..5fdc742a88 100644 --- a/tools/publish/requirements_linux.txt +++ b/tools/publish/requirements_linux.txt @@ -318,9 +318,9 @@ twine==5.1.1 \ --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db # via -r tools/publish/requirements.in -urllib3==2.3.0 \ - --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ - --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d +urllib3==2.4.0 \ + --hash=sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466 \ + --hash=sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813 # via # requests # twine diff --git a/tools/publish/requirements_universal.txt b/tools/publish/requirements_universal.txt index c8bc0bb258..97cbef0221 100644 --- a/tools/publish/requirements_universal.txt +++ b/tools/publish/requirements_universal.txt @@ -322,9 +322,9 @@ twine==5.1.1 \ --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db # via -r tools/publish/requirements.in -urllib3==2.3.0 \ - --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ - --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d +urllib3==2.4.0 \ + --hash=sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466 \ + --hash=sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813 # via # requests # twine diff --git a/tools/publish/requirements_windows.txt b/tools/publish/requirements_windows.txt index 1980812d15..458414009e 100644 --- a/tools/publish/requirements_windows.txt +++ b/tools/publish/requirements_windows.txt @@ -206,9 +206,9 @@ twine==5.1.1 \ --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db # via -r tools/publish/requirements.in -urllib3==2.3.0 \ - --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ - --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d +urllib3==2.4.0 \ + --hash=sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466 \ + --hash=sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813 # via # requests # twine From 101962aecbe048525248361d7a8e6341655fa30f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:40:45 +0900 Subject: [PATCH 030/156] build(deps): bump urllib3 from 2.3.0 to 2.4.0 in /docs (#2774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.3.0 to 2.4.0.
Release notes

Sourced from urllib3's releases.

2.4.0

🚀 urllib3 is fundraising for HTTP/2 support

urllib3 is raising ~$40,000 USD to release HTTP/2 support and ensure long-term sustainable maintenance of the project after a sharp decline in financial support. If your company or organization uses Python and would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and thousands of other projects please consider contributing financially to ensure HTTP/2 support is developed sustainably and maintained for the long-haul.

Thank you for your support.

Features

  • Applied PEP 639 by specifying the license fields in pyproject.toml. (#3522)
  • Updated exceptions to save and restore more properties during the pickle/serialization process. (#3567)
  • Added verify_flags option to create_urllib3_context with a default of VERIFY_X509_PARTIAL_CHAIN and VERIFY_X509_STRICT for Python 3.13+. (#3571)

Bugfixes

  • Fixed a bug with partial reads of streaming data in Emscripten. (#3555)

Misc

  • Switched to uv for installing development dependecies. (#3550)
  • Removed the multiple.intoto.jsonl asset from GitHub releases. Attestation of release files since v2.3.0 can be found on PyPI. (#3566)
Changelog

Sourced from urllib3's changelog.

2.4.0 (2025-04-10)

Features

  • Applied PEP 639 by specifying the license fields in pyproject.toml. ([#3522](https://github.com/urllib3/urllib3/issues/3522) <https://github.com/urllib3/urllib3/issues/3522>__)
  • Updated exceptions to save and restore more properties during the pickle/serialization process. ([#3567](https://github.com/urllib3/urllib3/issues/3567) <https://github.com/urllib3/urllib3/issues/3567>__)
  • Added verify_flags option to create_urllib3_context with a default of VERIFY_X509_PARTIAL_CHAIN and VERIFY_X509_STRICT for Python 3.13+. ([#3571](https://github.com/urllib3/urllib3/issues/3571) <https://github.com/urllib3/urllib3/issues/3571>__)

Bugfixes

  • Fixed a bug with partial reads of streaming data in Emscripten. ([#3555](https://github.com/urllib3/urllib3/issues/3555) <https://github.com/urllib3/urllib3/issues/3555>__)

Misc

  • Switched to uv for installing development dependecies. ([#3550](https://github.com/urllib3/urllib3/issues/3550) <https://github.com/urllib3/urllib3/issues/3550>__)
  • Removed the multiple.intoto.jsonl asset from GitHub releases. Attestation of release files since v2.3.0 can be found on PyPI. ([#3566](https://github.com/urllib3/urllib3/issues/3566) <https://github.com/urllib3/urllib3/issues/3566>__)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=urllib3&package-manager=pip&previous-version=2.3.0&new-version=2.4.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index e2fb59565a..5e308b00f4 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -357,7 +357,7 @@ typing-extensions==4.13.2 \ # via # rules-python-docs (docs/pyproject.toml) # sphinx-autodoc2 -urllib3==2.3.0 \ - --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ - --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d +urllib3==2.4.0 \ + --hash=sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466 \ + --hash=sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813 # via requests From 8fc25de7dcec1d1106edd8e076c9fcb58497b40b Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Wed, 16 Apr 2025 14:45:34 +0900 Subject: [PATCH 031/156] refactor(bzlmod): stop using 'repo' attr in whl_library (#2779) A simple non-functional cleanup that just removes legacy code paths from bzlmod PyPI integration. --- python/private/pypi/extension.bzl | 1 - python/private/pypi/whl_library.bzl | 6 ++++-- tests/pypi/extension/extension_tests.bzl | 22 ---------------------- 3 files changed, 4 insertions(+), 25 deletions(-) diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 8fce47656b..d2ae132741 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -181,7 +181,6 @@ def _create_whl_repos( # Construct args separately so that the lock file can be smaller and does not include unused # attrs. whl_library_args = dict( - repo = pip_name, dep_template = "@{}//{{name}}:{{target}}".format(hub_name), ) maybe_args = dict( diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 54f9ff3909..0a580011ab 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -517,8 +517,10 @@ and the target that we need respectively. doc = "Name of the group, if any.", ), "repo": attr.string( - mandatory = True, - doc = "Pointer to parent repo name. Used to make these rules rerun if the parent repo changes.", + doc = """\ +Pointer to parent repo name. Used to make these rules rerun if the parent repo changes. +Only used in WORKSPACE when the {attr}`dep_template` is not set. +""", ), "repo_prefix": attr.string( doc = """ diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index 66c9e0549e..4d86d6a6e0 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -174,7 +174,6 @@ def _test_simple(env): "pypi_315_simple": { "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "simple==0.0.1 --hash=sha256:deadbeef --hash=sha256:deadbaaf", }, }) @@ -234,13 +233,11 @@ def _test_simple_multiple_requirements(env): "pypi_315_simple_osx_aarch64_osx_x86_64": { "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "simple==0.0.2 --hash=sha256:deadb00f", }, "pypi_315_simple_windows_x86_64": { "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "simple==0.0.1 --hash=sha256:deadbeef", }, }) @@ -307,13 +304,11 @@ torch==2.4.1 ; platform_machine != 'x86_64' \ "pypi_315_torch_linux_aarch64_linux_arm_linux_ppc_linux_s390x_osx_aarch64": { "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "torch==2.4.1 --hash=sha256:deadbeef", }, "pypi_315_torch_linux_x86_64_osx_x86_64_windows_x86_64": { "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "torch==2.4.1+cpu", }, }) @@ -444,7 +439,6 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ "experimental_target_platforms": ["cp312_linux_x86_64"], "filename": "torch-2.4.1+cpu-cp312-cp312-linux_x86_64.whl", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_312", "requirement": "torch==2.4.1+cpu", "sha256": "8800deef0026011d502c0c256cc4b67d002347f63c3a38cd8e45f1f445c61364", "urls": ["https://torch.index/whl/cpu/torch-2.4.1%2Bcpu-cp312-cp312-linux_x86_64.whl"], @@ -454,7 +448,6 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ "experimental_target_platforms": ["cp312_linux_aarch64"], "filename": "torch-2.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_312", "requirement": "torch==2.4.1", "sha256": "36109432b10bd7163c9b30ce896f3c2cca1b86b9765f956a1594f0ff43091e2a", "urls": ["https://torch.index/whl/cpu/torch-2.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"], @@ -464,7 +457,6 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ "experimental_target_platforms": ["cp312_windows_x86_64"], "filename": "torch-2.4.1+cpu-cp312-cp312-win_amd64.whl", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_312", "requirement": "torch==2.4.1+cpu", "sha256": "3a570e5c553415cdbddfe679207327b3a3806b21c6adea14fba77684d1619e97", "urls": ["https://torch.index/whl/cpu/torch-2.4.1%2Bcpu-cp312-cp312-win_amd64.whl"], @@ -474,7 +466,6 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ "experimental_target_platforms": ["cp312_osx_aarch64"], "filename": "torch-2.4.1-cp312-none-macosx_11_0_arm64.whl", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_312", "requirement": "torch==2.4.1", "sha256": "72b484d5b6cec1a735bf3fa5a1c4883d01748698c5e9cfdbeb4ffab7c7987e0d", "urls": ["https://torch.index/whl/cpu/torch-2.4.1-cp312-none-macosx_11_0_arm64.whl"], @@ -560,7 +551,6 @@ simple==0.0.3 \ "experimental_target_platforms": ["cp315_linux_x86_64"], "extra_pip_args": ["--platform=manylinux_2_17_x86_64", "--python-version=315", "--implementation=cp", "--abi=cp315"], "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "extra==0.0.1 --hash=sha256:deadb00f", }, "pypi_315_simple_linux_x86_64": { @@ -569,7 +559,6 @@ simple==0.0.3 \ "experimental_target_platforms": ["cp315_linux_x86_64"], "extra_pip_args": ["--platform=manylinux_2_17_x86_64", "--python-version=315", "--implementation=cp", "--abi=cp315"], "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "simple==0.0.1 --hash=sha256:deadbeef", }, "pypi_315_simple_osx_aarch64": { @@ -578,7 +567,6 @@ simple==0.0.3 \ "experimental_target_platforms": ["cp315_osx_aarch64"], "extra_pip_args": ["--platform=macosx_10_9_arm64", "--python-version=315", "--implementation=cp", "--abi=cp315"], "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "simple==0.0.3 --hash=sha256:deadbaaf", }, }) @@ -766,7 +754,6 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "extra_pip_args": ["--extra-args-for-sdist-building"], "filename": "any-name.tar.gz", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "direct_sdist_without_sha @ some-archive/any-name.tar.gz", "sha256": "", "urls": ["some-archive/any-name.tar.gz"], @@ -776,7 +763,6 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"], "filename": "direct_without_sha-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "direct_without_sha==0.0.1 @ example-direct.org/direct_without_sha-0.0.1-py3-none-any.whl", "sha256": "", "urls": ["example-direct.org/direct_without_sha-0.0.1-py3-none-any.whl"], @@ -785,14 +771,12 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "dep_template": "@pypi//{name}:{target}", "extra_pip_args": ["--extra-args-for-sdist-building"], "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef", }, "pypi_315_pip_fallback": { "dep_template": "@pypi//{name}:{target}", "extra_pip_args": ["--extra-args-for-sdist-building"], "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "pip_fallback==0.0.1", }, "pypi_315_simple_py3_none_any_deadb00f": { @@ -800,7 +784,6 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"], "filename": "simple-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "simple==0.0.1", "sha256": "deadb00f", "urls": ["example2.org"], @@ -811,7 +794,6 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "extra_pip_args": ["--extra-args-for-sdist-building"], "filename": "simple-0.0.1.tar.gz", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "simple==0.0.1", "sha256": "deadbeef", "urls": ["example.org"], @@ -821,7 +803,6 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"], "filename": "some_pkg-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "some_pkg==0.0.1 @ example-direct.org/some_pkg-0.0.1-py3-none-any.whl --hash=sha256:deadbaaf", "sha256": "deadbaaf", "urls": ["example-direct.org/some_pkg-0.0.1-py3-none-any.whl"], @@ -831,7 +812,6 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"], "filename": "some-other-pkg-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "some_other_pkg==0.0.1", "sha256": "deadb33f", "urls": ["example2.org/index/some_other_pkg/"], @@ -920,13 +900,11 @@ optimum[onnxruntime-gpu]==1.17.1 ; sys_platform == 'linux' "pypi_315_optimum_linux_aarch64_linux_arm_linux_ppc_linux_s390x_linux_x86_64": { "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "optimum[onnxruntime-gpu]==1.17.1", }, "pypi_315_optimum_osx_aarch64_osx_x86_64": { "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", - "repo": "pypi_315", "requirement": "optimum[onnxruntime]==1.17.1", }, }) From c813d845b959e37d4949e368c86bc1277d153b38 Mon Sep 17 00:00:00 2001 From: Matt Mackay Date: Wed, 16 Apr 2025 23:45:50 -0400 Subject: [PATCH 032/156] perf: lazily load gazelle manifest files (#2746) In large repositories where Python may not be the only language, the gazelle manifest loading is done unnecessarily, and is done during the configuration walk. This means that even for non-python gazelle invocations (eg `bazel run gazelle -- web/`), Python manifest files are being parsed and loaded into memory. This issue compounds if the repository uses multiple dependency closures, ie multiple `gazelle_python.yaml` files. In our repo, we currently have ~250 Python manifests, so loading them when Gazelle is only running over other languages is time consuming. Co-authored-by: Douglas Thor --- CHANGELOG.md | 3 +++ gazelle/python/configure.go | 24 +---------------- gazelle/pythonconfig/pythonconfig.go | 40 +++++++++++++++++++++++++--- 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7f9fe30e2..299a43e1ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,9 @@ Unreleased changes template. * (pypi) The PyPI extension will no longer write the lock file entries as the extension has been marked reproducible. Fixes [#2434](https://github.com/bazel-contrib/rules_python/issues/2434). +* (gazelle) Lazily load and parse manifest files when running Gazelle. This ensures no + manifest files are loaded when Gazelle is run over a set of non-python directories + [PR #2746](https://github.com/bazel-contrib/rules_python/pull/2746). * (rules) {attr}`py_binary.srcs` and {attr}`py_test.srcs` is no longer mandatory when `main_module` is specified (for `--bootstrap_impl=script`) diff --git a/gazelle/python/configure.go b/gazelle/python/configure.go index 7b1f091b34..a00b0ba0ba 100644 --- a/gazelle/python/configure.go +++ b/gazelle/python/configure.go @@ -18,7 +18,6 @@ import ( "flag" "fmt" "log" - "os" "path/filepath" "strconv" "strings" @@ -27,7 +26,6 @@ import ( "github.com/bazelbuild/bazel-gazelle/rule" "github.com/bmatcuk/doublestar/v4" - "github.com/bazel-contrib/rules_python/gazelle/manifest" "github.com/bazel-contrib/rules_python/gazelle/pythonconfig" ) @@ -228,25 +226,5 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) { } gazelleManifestPath := filepath.Join(c.RepoRoot, rel, gazelleManifestFilename) - gazelleManifest, err := py.loadGazelleManifest(gazelleManifestPath) - if err != nil { - log.Fatal(err) - } - if gazelleManifest != nil { - config.SetGazelleManifest(gazelleManifest) - } -} - -func (py *Configurer) loadGazelleManifest(gazelleManifestPath string) (*manifest.Manifest, error) { - if _, err := os.Stat(gazelleManifestPath); err != nil { - if os.IsNotExist(err) { - return nil, nil - } - return nil, fmt.Errorf("failed to load Gazelle manifest at %q: %w", gazelleManifestPath, err) - } - manifestFile := new(manifest.File) - if err := manifestFile.Decode(gazelleManifestPath); err != nil { - return nil, fmt.Errorf("failed to load Gazelle manifest at %q: %w", gazelleManifestPath, err) - } - return manifestFile.Manifest, nil + config.SetGazelleManifestPath(gazelleManifestPath) } diff --git a/gazelle/pythonconfig/pythonconfig.go b/gazelle/pythonconfig/pythonconfig.go index 23c0cfd572..866339d449 100644 --- a/gazelle/pythonconfig/pythonconfig.go +++ b/gazelle/pythonconfig/pythonconfig.go @@ -16,6 +16,8 @@ package pythonconfig import ( "fmt" + "log" + "os" "path" "regexp" "strings" @@ -153,10 +155,11 @@ func (c Configs) ParentForPackage(pkg string) *Config { type Config struct { parent *Config - extensionEnabled bool - repoRoot string - pythonProjectRoot string - gazelleManifest *manifest.Manifest + extensionEnabled bool + repoRoot string + pythonProjectRoot string + gazelleManifestPath string + gazelleManifest *manifest.Manifest excludedPatterns *singlylinkedlist.List ignoreFiles map[string]struct{} @@ -281,11 +284,26 @@ func (c *Config) SetGazelleManifest(gazelleManifest *manifest.Manifest) { c.gazelleManifest = gazelleManifest } +// SetGazelleManifestPath sets the path to the gazelle_python.yaml file +// for the current configuration. +func (c *Config) SetGazelleManifestPath(gazelleManifestPath string) { + c.gazelleManifestPath = gazelleManifestPath +} + // FindThirdPartyDependency scans the gazelle manifests for the current config // and the parent configs up to the root finding if it can resolve the module // name. func (c *Config) FindThirdPartyDependency(modName string) (string, string, bool) { for currentCfg := c; currentCfg != nil; currentCfg = currentCfg.parent { + // Attempt to load the manifest if needed. + if currentCfg.gazelleManifestPath != "" && currentCfg.gazelleManifest == nil { + currentCfgManifest, err := loadGazelleManifest(currentCfg.gazelleManifestPath) + if err != nil { + log.Fatal(err) + } + currentCfg.SetGazelleManifest(currentCfgManifest) + } + if currentCfg.gazelleManifest != nil { gazelleManifest := currentCfg.gazelleManifest if distributionName, ok := gazelleManifest.ModulesMapping[modName]; ok { @@ -526,3 +544,17 @@ func (c *Config) FormatThirdPartyDependency(repositoryName string, distributionN return label.New(repositoryName, normConventionalDistributionName, normConventionalDistributionName) } + +func loadGazelleManifest(gazelleManifestPath string) (*manifest.Manifest, error) { + if _, err := os.Stat(gazelleManifestPath); err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, fmt.Errorf("failed to load Gazelle manifest at %q: %w", gazelleManifestPath, err) + } + manifestFile := new(manifest.File) + if err := manifestFile.Decode(gazelleManifestPath); err != nil { + return nil, fmt.Errorf("failed to load Gazelle manifest at %q: %w", gazelleManifestPath, err) + } + return manifestFile.Manifest, nil +} From d0950c5648789071667b852a6d736cf865e2ff07 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 18 Apr 2025 07:00:05 +0900 Subject: [PATCH 033/156] fix(ci): use ubuntu-latest for mypy action (#2784) --- .github/workflows/mypy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mypy.yaml b/.github/workflows/mypy.yaml index 866c43abd1..e774b9b03b 100644 --- a/.github/workflows/mypy.yaml +++ b/.github/workflows/mypy.yaml @@ -15,7 +15,7 @@ defaults: jobs: ci: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: # Checkout the code - uses: actions/checkout@v4 From 183d2973060c653fc393209241b46e4ec807dd7b Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 18 Apr 2025 08:43:27 +0900 Subject: [PATCH 034/156] doc: better document supported platform tiers (#2783) Fixes #2722. Related #2734, #2276, #1579 --- docs/support.md | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/support.md b/docs/support.md index ea099650bd..5e6de57fcb 100644 --- a/docs/support.md +++ b/docs/support.md @@ -31,11 +31,35 @@ minor/patch versions. See [Bazel's release support matrix](https://bazel.build/release#support-matrix) for what versions are the rolling, active, and prior releases. +## Supported Python versions + +As a general rule we test all released non-EOL Python versions. Different +interpreter versions may work but are not guaranteed. We are interested in +staying compatible with upcoming unreleased versions, so if you see that things +stop working, please create tickets or, more preferably, pull requests. + ## Supported Platforms We only support the platforms that our continuous integration jobs run, which -is Linux, Mac, and Windows. Code to support other platforms is allowed, but -can only be on a best-effort basis. +is Linux, Mac, and Windows. + +In order to better describe different support levels, the below acts as a rough +guideline for different platform tiers: +* Tier 0 - The platforms that our CI runs: `linux_x86_64`, `osx_x86_64`, `RBE linux_x86_64`. +* Tier 1 - The platforms that are similar enough to what the CI runs: `linux_aarch64`, `osx_arm64`. + What is more, `windows_x86_64` is in this list as we run tests in CI but + developing for Windows is more challenging and features may come later to + this platform. +* Tier 2 - The rest of the platforms that may have varying level of support, e.g. + `linux_s390x`, `linux_ppc64le`, `windows_arm64`. + +:::{note} +Code to support Tier 2 platforms is allowed, but regressions will be fixed on a +best-effort basis, so feel free to contribute by creating PRs. + +If you would like to provide/sponsor CI setup for a platform that is not Tier 0, +please create a ticket or contact the maintainers on Slack. +::: ## Compatibility Policy From abdf560f56490beb43c1e4d72338f8553bc4d73f Mon Sep 17 00:00:00 2001 From: David Sanderson <32687193+dws@users.noreply.github.com> Date: Fri, 18 Apr 2025 16:04:22 -0400 Subject: [PATCH 035/156] fix(rules): copy_propagating_kwargs() now also copies target_compatible_with (#2788) This routine already copies `compatible_with`, which is little used, but does not copy `target_compatible_with`, which is broadly used. This seems like an oversight. I noticed this discrepancy when working on a system that assumes that any `tags` or `target_compatible_with` parameters supplied to a macro will propagate to all rules created by that macro. In rules_python, this already works for `tags`, but not for `target_compatible_with`. It would be great to get this accepted upstream, so that I can stop patching rules_python. --------- Co-authored-by: Richard Levasseur --- CHANGELOG.md | 2 ++ python/private/util.bzl | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 299a43e1ff..47ccd2459a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,8 @@ Unreleased changes template. * (toolchains) Ensure temporary `.pyc` and `.pyo` files are also excluded from the interpreters repository files. * (pypi) Run interpreter version call in isolated mode, to ensure it's not affected by userland environment variables, such as `PYTHONPATH`. * (packaging) An empty `requires_file` is treated as if it were omitted, resulting in a valid `METADATA` file. +* (rules) py_wheel and sphinxdocs rules now propagate `target_compatible_with` to all targets they create. + [PR #2788](https://github.com/bazel-contrib/rules_python/pull/2788). {#v0-0-0-added} ### Added diff --git a/python/private/util.bzl b/python/private/util.bzl index 33261befaf..4d2da57760 100644 --- a/python/private/util.bzl +++ b/python/private/util.bzl @@ -42,7 +42,7 @@ def copy_propagating_kwargs(from_kwargs, into_kwargs = None): into_kwargs = {} # Include tags because people generally expect tags to propagate. - for attr in ("testonly", "tags", "compatible_with", "restricted_to"): + for attr in ("testonly", "tags", "compatible_with", "restricted_to", "target_compatible_with"): if attr in from_kwargs and attr not in into_kwargs: into_kwargs[attr] = from_kwargs[attr] return into_kwargs From 844e7ada6738fc0e1f040df3c967e778af2af1c7 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 19 Apr 2025 20:18:40 -0700 Subject: [PATCH 036/156] release: 1.4.0 release prep (#2789) Updates changelog and version markers. Also updates the release docs with some shell-one liners to copy and paste to make it a bit more mechanical. --- CHANGELOG.md | 22 ++++++++++++---------- RELEASING.md | 21 +++++++++++++++++++++ python/current_py_toolchain.bzl | 2 +- python/features.bzl | 2 +- python/local_toolchains/repos.bzl | 2 +- python/packaging.bzl | 2 +- python/private/py_exec_tools_toolchain.bzl | 2 +- python/private/py_info.bzl | 2 +- python/private/py_library.bzl | 2 +- python/private/pypi/extension.bzl | 4 ++-- python/private/python.bzl | 8 ++++---- 11 files changed, 46 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ccd2459a..1378853626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ A brief description of the categories of changes: `(docs)`. -{#v0-0-0} -## Unreleased +{#1-4-0} +## [1.4.0] - 2025-04-19 -[0.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.0.0 +[1.4.0]: https://github.com/bazel-contrib/rules_python/releases/tag/1.4.0 -{#v0-0-0-changed} +{#1-4-0-changed} ### Changed * (toolchain) The `exec` configuration toolchain now has the forwarded `exec_interpreter` now also forwards the `ToolchainInfo` provider. This is @@ -72,7 +74,7 @@ Unreleased changes template. * (toolchains) Previously [#2636](https://github.com/bazel-contrib/rules_python/pull/2636) changed the semantics of `ignore_root_user_error` from "ignore" to "warning". This is now flipped back to ignoring the issue, and will only emit a warning when the attribute is set - `False`. + `False`. * (pypi) The PyPI extension will no longer write the lock file entries as the extension has been marked reproducible. Fixes [#2434](https://github.com/bazel-contrib/rules_python/issues/2434). @@ -84,7 +86,7 @@ Unreleased changes template. [20250317]: https://github.com/astral-sh/python-build-standalone/releases/tag/20250317 -{#v0-0-0-fixed} +{#1-4-0-fixed} ### Fixed * (pypi) Platform specific extras are now correctly handled when using universal lock files with environment markers. Fixes [#2690](https://github.com/bazel-contrib/rules_python/pull/2690). @@ -103,7 +105,7 @@ Unreleased changes template. * (rules) py_wheel and sphinxdocs rules now propagate `target_compatible_with` to all targets they create. [PR #2788](https://github.com/bazel-contrib/rules_python/pull/2788). -{#v0-0-0-added} +{#1-4-0-added} ### Added * (pypi) From now on `sha256` values in the `requirements.txt` is no longer mandatory when enabling {attr}`pip.parse.experimental_index_url` feature. @@ -134,13 +136,13 @@ Unreleased changes template. locations equivalents of `$(PYTHON2)` and `$(PYTHON3) respectively. -{#v0-0-0-removed} +{#1-4-0-removed} ### Removed * Nothing removed. {#v1-3-0} -## Unreleased +## [1.3.0] - 2025-03-27 [1.3.0]: https://github.com/bazel-contrib/rules_python/releases/tag/1.3.0 diff --git a/RELEASING.md b/RELEASING.md index 82510b99c7..c9d46c39f0 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -14,7 +14,14 @@ These are the steps for a regularly scheduled release from HEAD. 1. [Determine the next semantic version number](#determining-semantic-version). 1. Update CHANGELOG.md: replace the `v0-0-0` and `0.0.0` with `X.Y.0`. + ``` + awk -v version=X.Y.0 'BEGIN { hv=version; gsub(/\./, "-", hv) } /END_UNRELEASED_TEMPLATE/ { found_marker = 1 } found_marker { gsub(/v0-0-0/, hv, $0); gsub(/Unreleased/, "[" version "] - " strftime("%Y-%m-%d"), $0); gsub(/0.0.0/, version, $0); } { print } ' CHANGELOG.md > /tmp/changelog && cp /tmp/changelog CHANGELOG.md + ``` 1. Replace `VERSION_NEXT_*` strings with `X.Y.0`. + ``` + grep -l --exclude=CONTRIBUTING.md --exclude=RELEASING.md --exclude-dir=.* VERSION_NEXT_ -r \ + | xargs sed -i -e 's/VERSION_NEXT_FEATURE/X.Y.0/' -e 's/VERSION_NEXT_PATCH/X.Y.0/' + ``` 1. Send these changes for review and get them merged. 1. Create a branch for the new release, named `release/X.Y` ``` @@ -90,6 +97,20 @@ It will be promoted to stable next week, pending feedback. It's traditional to include notable changes from the changelog, but not required. +### Re-releasing a version + +Re-releasing a version (i.e. changing the commit a tag points to) is +*sometimes* possible, but it depends on how far into the release process it got. + +The two points of no return are: + * If the PyPI package has been published: PyPI disallows using the same + filename/version twice. Once published, it cannot be replaced. + * If the BCR package has been published: Once it's been committed to the BCR + registry, it cannot be replaced. + +If release steps fail _prior_ to those steps, then its OK to change the tag. You +may need to manually delete the GitHub release. + ## Secrets ### PyPI user rules-python diff --git a/python/current_py_toolchain.bzl b/python/current_py_toolchain.bzl index f5c5638a88..0ca5c90ccc 100644 --- a/python/current_py_toolchain.bzl +++ b/python/current_py_toolchain.bzl @@ -52,7 +52,7 @@ current_py_toolchain = rule( happened, to a rule which expects a concrete implementation of a toolchain, rather than a toolchain_type which could be resolved to that toolchain. - :::{versionchanged} VERSION_NEXT_FEATURE + :::{versionchanged} 1.4.0 From now on, we also expose `$(PYTHON2_ROOTPATH)` and `$(PYTHON3_ROOTPATH)` which are runfiles locations equivalents of `$(PYTHON2)` and `$(PYTHON3) respectively. ::: diff --git a/python/features.bzl b/python/features.bzl index 8edfb698fc..917bd3800c 100644 --- a/python/features.bzl +++ b/python/features.bzl @@ -35,7 +35,7 @@ def _features_typedef(): True if the `PyInfo.site_packages_symlinks` field is available. - :::{versionadded} VERSION_NEXT_FEATURE + :::{versionadded} 1.4.0 ::: :::: diff --git a/python/local_toolchains/repos.bzl b/python/local_toolchains/repos.bzl index d1b45cfd7f..320e503e1a 100644 --- a/python/local_toolchains/repos.bzl +++ b/python/local_toolchains/repos.bzl @@ -1,6 +1,6 @@ """Rules/macros for repository phase for local toolchains. -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.4.0 ::: """ diff --git a/python/packaging.bzl b/python/packaging.bzl index b190635cfe..223aba142d 100644 --- a/python/packaging.bzl +++ b/python/packaging.bzl @@ -101,7 +101,7 @@ def py_wheel( Currently only pure-python wheels are supported. - :::{versionchanged} VERSION_NEXT_FEATURE + :::{versionchanged} 1.4.0 From now on, an empty `requires_file` is treated as if it were omitted, resulting in a valid `METADATA` file. ::: diff --git a/python/private/py_exec_tools_toolchain.bzl b/python/private/py_exec_tools_toolchain.bzl index ff30431ff4..332570b26b 100644 --- a/python/private/py_exec_tools_toolchain.bzl +++ b/python/private/py_exec_tools_toolchain.bzl @@ -77,7 +77,7 @@ handle all the necessary transitions and runtime setup to invoke a program. See {obj}`PyExecToolsInfo.exec_interpreter` for further docs. -:::{versionchanged} VERSION_NEXT_FEATURE +:::{versionchanged} 1.4.0 From now on the provided target also needs to provide `platform_common.ToolchainInfo` so that the toolchain `py_runtime` field can be correctly forwarded. ::: diff --git a/python/private/py_info.bzl b/python/private/py_info.bzl index 4ecd02a438..dc3cb24c51 100644 --- a/python/private/py_info.bzl +++ b/python/private/py_info.bzl @@ -168,7 +168,7 @@ values from further way dependencies, such as forcing symlinks to point to specific paths or preventing symlinks from being created. ::: -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.4.0 ::: """, "transitive_implicit_pyc_files": """ diff --git a/python/private/py_library.bzl b/python/private/py_library.bzl index edd0db579f..6b5882de5a 100644 --- a/python/private/py_library.bzl +++ b/python/private/py_library.bzl @@ -94,7 +94,7 @@ to a consumer have precedence. See {obj}`PyInfo.site_packages_symlinks` for more information. ::: -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.4.0 ::: """, ), diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index d2ae132741..68776e32d0 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -686,7 +686,7 @@ If {attr}`download_only` is set, then `sdist` archives will be discarded and `pi operate in wheel-only mode. ::: -:::{versionchanged} VERSION_NEXT_FEATURE +:::{versionchanged} 1.4.0 Index metadata will be used to deduct `sha256` values for packages even if the `sha256` values are not present in the requirements.txt lock file. ::: @@ -767,7 +767,7 @@ to `rules_python` and use this attribute until the bug is fixed. EXPERIMENTAL: this may be removed without notice. -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.4.0 ::: """, ), diff --git a/python/private/python.bzl b/python/private/python.bzl index efc429420e..f49fb26d52 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -695,7 +695,7 @@ matches the {attr}`python_version` attribute of a toolchain, this toolchain is the default version. If this attribute is set, the {attr}`is_default` attribute of the toolchain is ignored. -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.4.0 ::: """, ), @@ -707,7 +707,7 @@ If the string matches the {attr}`python_version` attribute of a toolchain, this toolchain is the default version. If this attribute is set, the {attr}`is_default` attribute of the toolchain is ignored. -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.4.0 ::: """, ), @@ -720,7 +720,7 @@ of the file match the {attr}`python_version` attribute of a toolchain, this toolchain is the default version. If this attribute is set, the {attr}`is_default` attribute of the toolchain is ignored. -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.4.0 ::: """, ), @@ -813,7 +813,7 @@ this to `False`. doc = """\ Whether the toolchain is the default version. -:::{versionchanged} VERSION_NEXT_FEATURE +:::{versionchanged} 1.4.0 This setting is ignored if the default version is set using the `defaults` tag class. ::: From cc46fb26d629b9e440371861f031cb2a85fd9c55 Mon Sep 17 00:00:00 2001 From: Guillaume Maudoux Date: Sun, 20 Apr 2025 08:05:13 +0200 Subject: [PATCH 037/156] fix: declare PyInfo as provided by test/binary/library (#2777) Currently, the rules don't advertise the PyInfo provider through the provides argument to the rule function. This means that aspects that want to consume PyInfo can't use `required_providers` to restrict themselves to the Python rules, and instead have to apply to all rules. To fix, add PyInfo to the provides arg of the rules. Fixes https://github.com/bazel-contrib/rules_python/issues/2506 --------- Co-authored-by: Richard Levasseur Co-authored-by: Richard Levasseur --- CHANGELOG.md | 22 ++++++++++++++++++++++ python/private/py_executable.bzl | 4 +++- python/private/py_library.bzl | 5 +++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1378853626..cad074e6a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,28 @@ BEGIN_UNRELEASED_TEMPLATE END_UNRELEASED_TEMPLATE --> +{#v0-0-0} +## Unreleased + +[0.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.0.0 + +{#v0-0-0-changed} +### Changed +* Nothing changed. + +{#v0-0-0-fixed} +### Fixed +* (rules) PyInfo provider is now advertised by py_test, py_binary, and py_library; + this allows aspects using required_providers to function correctly. + ([#2506](https://github.com/bazel-contrib/rules_python/issues/2506)). + +{#v0-0-0-added} +### Added +* Nothing added. + +{#v0-0-0-removed} +### Removed +* Nothing removed. {#1-4-0} ## [1.4.0] - 2025-04-19 diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index dd3ad869fa..b4cda21b1d 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -1854,6 +1854,8 @@ def create_base_executable_rule(): """ return create_executable_rule_builder().build() +_MaybeBuiltinPyInfo = [BuiltinPyInfo] if BuiltinPyInfo != None else [] + # NOTE: Exported publicly def create_executable_rule_builder(implementation, **kwargs): """Create a rule builder for an executable Python program. @@ -1877,7 +1879,7 @@ def create_executable_rule_builder(implementation, **kwargs): attrs = EXECUTABLE_ATTRS, exec_groups = dict(REQUIRED_EXEC_GROUP_BUILDERS), # Mutable copy fragments = ["py", "bazel_py"], - provides = [PyExecutableInfo], + provides = [PyExecutableInfo, PyInfo] + _MaybeBuiltinPyInfo, toolchains = [ ruleb.ToolchainType(TOOLCHAIN_TYPE), ruleb.ToolchainType(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False), diff --git a/python/private/py_library.bzl b/python/private/py_library.bzl index 6b5882de5a..bf0c25439e 100644 --- a/python/private/py_library.bzl +++ b/python/private/py_library.bzl @@ -43,7 +43,9 @@ load( load(":flags.bzl", "AddSrcsToRunfilesFlag", "PrecompileFlag", "VenvsSitePackages") load(":precompile.bzl", "maybe_precompile") load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo") +load(":py_info.bzl", "PyInfo") load(":py_internal.bzl", "py_internal") +load(":reexports.bzl", "BuiltinPyInfo") load(":rule_builders.bzl", "ruleb") load( ":toolchain_types.bzl", @@ -299,6 +301,8 @@ def _repo_relative_short_path(short_path): else: return short_path +_MaybeBuiltinPyInfo = [BuiltinPyInfo] if BuiltinPyInfo != None else [] + # NOTE: Exported publicaly def create_py_library_rule_builder(): """Create a rule builder for a py_library. @@ -319,6 +323,7 @@ def create_py_library_rule_builder(): exec_groups = dict(REQUIRED_EXEC_GROUP_BUILDERS), attrs = LIBRARY_ATTRS, fragments = ["py"], + provides = [PyCcLinkParamsInfo, PyInfo] + _MaybeBuiltinPyInfo, toolchains = [ ruleb.ToolchainType(TOOLCHAIN_TYPE, mandatory = False), ruleb.ToolchainType(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False), From a19e1e41a609dd10ae6cdc49d76eb1f119145d2e Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 20 Apr 2025 19:17:59 +0900 Subject: [PATCH 038/156] fix: load target_platforms through the hub (#2781) This PR moves the parsing of `Requires-Dist` to the loading phase within the `whl_library_targets_from_requires` macro. The original `whl_library_targets` macro has been left unchanged so that I don't have to reinvent the unit tests - it is well covered under tests. Before this PR we had to wire the `target_platforms` via the `experimental_target_platforms` attr in the `whl_library`, which means that whenever this would change (e.g. the minor Python version changes), the wheel would be re-extracted even though the final result may be the same. This refactor uncovered that the dependency graph creation was incorrect if we had multiple target Python versions due to various heuristics that this had. In hindsight I had them to make the generated `BUILD.bazel` files more readable when the unit test coverage was not great. Now this is unnecessary and since everything is happening in Starlark I thought that having a simpler algorithm that does the right thing always is the best way. This also cleans up the code by removing left over TODO notes or code that no longer make sense. Work towards #260, #2319 --- CHANGELOG.md | 7 + config.bzl.tmpl.bzlmod | 0 python/private/pypi/BUILD.bazel | 14 +- python/private/pypi/attrs.bzl | 3 + python/private/pypi/config.bzl.tmpl.bzlmod | 9 + python/private/pypi/extension.bzl | 41 ++-- .../pypi/generate_whl_library_build_bazel.bzl | 27 +- python/private/pypi/hub_repository.bzl | 18 +- python/private/pypi/pep508.bzl | 23 -- python/private/pypi/pep508_deps.bzl | 231 ++++-------------- python/private/pypi/pep508_requirement.bzl | 4 +- python/private/pypi/whl_library.bzl | 97 +++----- python/private/pypi/whl_library_targets.bzl | 83 +++++++ tests/pypi/extension/extension_tests.bzl | 10 - ...generate_whl_library_build_bazel_tests.bzl | 92 +++++-- tests/pypi/pep508/deps_tests.bzl | 191 ++++++--------- .../whl_library_targets_tests.bzl | 67 ++++- 17 files changed, 451 insertions(+), 466 deletions(-) create mode 100644 config.bzl.tmpl.bzlmod create mode 100644 python/private/pypi/config.bzl.tmpl.bzlmod delete mode 100644 python/private/pypi/pep508.bzl diff --git a/CHANGELOG.md b/CHANGELOG.md index cad074e6a6..154b66114b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,6 +105,13 @@ END_UNRELEASED_TEMPLATE [PR #2746](https://github.com/bazel-contrib/rules_python/pull/2746). * (rules) {attr}`py_binary.srcs` and {attr}`py_test.srcs` is no longer mandatory when `main_module` is specified (for `--bootstrap_impl=script`) +* (pypi) From now on the `Requires-Dist` from the wheel metadata is analysed in + the loading phase instead of repository rule phase giving better caching + performance when the target platforms are changed (e.g. target python + versions). This is preparatory work for stabilizing the cross-platform wheel + support. From now on the usage of `experimental_target_platforms` should be + avoided and the `requirements_by_platform` values should be instead used to + specify the target platforms for the given dependencies. [20250317]: https://github.com/astral-sh/python-build-standalone/releases/tag/20250317 diff --git a/config.bzl.tmpl.bzlmod b/config.bzl.tmpl.bzlmod new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 7297238cb4..a758b3f153 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -212,15 +212,6 @@ bzl_library( ], ) -bzl_library( - name = "pep508_bzl", - srcs = ["pep508.bzl"], - deps = [ - ":pep508_env_bzl", - ":pep508_evaluate_bzl", - ], -) - bzl_library( name = "pep508_deps_bzl", srcs = ["pep508_deps.bzl"], @@ -378,13 +369,12 @@ bzl_library( ":attrs_bzl", ":deps_bzl", ":generate_whl_library_build_bazel_bzl", - ":parse_whl_name_bzl", ":patch_whl_bzl", - ":pep508_deps_bzl", + ":pep508_requirement_bzl", ":pypi_repo_utils_bzl", ":whl_metadata_bzl", - ":whl_target_platforms_bzl", "//python/private:auth_bzl", + "//python/private:bzlmod_enabled_bzl", "//python/private:envsubst_bzl", "//python/private:is_standalone_interpreter_bzl", "//python/private:repo_utils_bzl", diff --git a/python/private/pypi/attrs.bzl b/python/private/pypi/attrs.bzl index 9d88c1e32c..fe35d8bf7d 100644 --- a/python/private/pypi/attrs.bzl +++ b/python/private/pypi/attrs.bzl @@ -123,6 +123,9 @@ Warning: "experimental_target_platforms": attr.string_list( default = [], doc = """\ +*NOTE*: This will be removed in the next major version, so please consider migrating +to `bzlmod` and rely on {attr}`pip.parse.requirements_by_platform` for this feature. + A list of platforms that we will generate the conditional dependency graph for cross platform wheels by parsing the wheel metadata. This will generate the correct dependencies for packages like `sphinx` or `pylint`, which include diff --git a/python/private/pypi/config.bzl.tmpl.bzlmod b/python/private/pypi/config.bzl.tmpl.bzlmod new file mode 100644 index 0000000000..deb53631d1 --- /dev/null +++ b/python/private/pypi/config.bzl.tmpl.bzlmod @@ -0,0 +1,9 @@ +"""Extra configuration values that are exposed from the hub repository for spoke repositories to access. + +NOTE: This is internal `rules_python` API and if you would like to depend on it, please raise an issue +with your usecase. This may change in between rules_python versions without any notice. + +@generated by rules_python pip.parse bzlmod extension. +""" + +target_platforms = %%TARGET_PLATFORMS%% diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 68776e32d0..d1895ca211 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -32,7 +32,6 @@ load(":simpleapi_download.bzl", "simpleapi_download") load(":whl_config_setting.bzl", "whl_config_setting") load(":whl_library.bzl", "whl_library") load(":whl_repo_name.bzl", "pypi_repo_name", "whl_repo_name") -load(":whl_target_platforms.bzl", "whl_target_platforms") def _major_minor_version(version): version = semver(version) @@ -68,7 +67,6 @@ def _create_whl_repos( *, pip_attr, whl_overrides, - evaluate_markers = evaluate_markers, available_interpreters = INTERPRETER_LABELS, get_index_urls = None): """create all of the whl repositories @@ -77,7 +75,6 @@ def _create_whl_repos( module_ctx: {type}`module_ctx`. pip_attr: {type}`struct` - the struct that comes from the tag class iteration. whl_overrides: {type}`dict[str, struct]` - per-wheel overrides. - evaluate_markers: the function to use to evaluate markers. get_index_urls: A function used to get the index URLs available_interpreters: {type}`dict[str, Label]` The dictionary of available interpreters that have been registered using the `python` bzlmod extension. @@ -162,14 +159,12 @@ def _create_whl_repos( requirements_osx = pip_attr.requirements_darwin, requirements_windows = pip_attr.requirements_windows, extra_pip_args = pip_attr.extra_pip_args, + # TODO @aignas 2025-04-15: pass the full version into here python_version = major_minor, logger = logger, ), extra_pip_args = pip_attr.extra_pip_args, get_index_urls = get_index_urls, - # NOTE @aignas 2025-02-24: we will use the "cp3xx_os_arch" platform labels - # for converting to the PEP508 environment and will evaluate them in starlark - # without involving the interpreter at all. evaluate_markers = evaluate_markers, logger = logger, ) @@ -191,7 +186,6 @@ def _create_whl_repos( enable_implicit_namespace_pkgs = pip_attr.enable_implicit_namespace_pkgs, environment = pip_attr.environment, envsubst = pip_attr.envsubst, - experimental_target_platforms = pip_attr.experimental_target_platforms, group_deps = group_deps, group_name = group_name, pip_data_exclude = pip_attr.pip_data_exclude, @@ -244,6 +238,12 @@ def _create_whl_repos( }, extra_aliases = extra_aliases, whl_libraries = whl_libraries, + target_platforms = { + plat: None + for reqs in requirements_by_platform.values() + for req in reqs + for plat in req.target_platforms + }, ) def _whl_repos(*, requirement, whl_library_args, download_only, netrc, auth_patterns, multiple_requirements_for_whl = False, python_version): @@ -274,20 +274,11 @@ def _whl_repos(*, requirement, whl_library_args, download_only, netrc, auth_patt args["urls"] = [distribution.url] args["sha256"] = distribution.sha256 args["filename"] = distribution.filename - args["experimental_target_platforms"] = requirement.target_platforms # Pure python wheels or sdists may need to have a platform here target_platforms = None if distribution.filename.endswith(".whl") and not distribution.filename.endswith("-any.whl"): - parsed_whl = parse_whl_name(distribution.filename) - whl_platforms = whl_target_platforms( - platform_tag = parsed_whl.platform_tag, - ) - args["experimental_target_platforms"] = [ - p - for p in requirement.target_platforms - if [None for wp in whl_platforms if p.endswith(wp.target_platform)] - ] + pass elif multiple_requirements_for_whl: target_platforms = requirement.target_platforms @@ -416,6 +407,7 @@ You cannot use both the additive_build_content and additive_build_content_file a hub_group_map = {} exposed_packages = {} extra_aliases = {} + target_platforms = {} whl_libraries = {} for mod in module_ctx.modules: @@ -498,6 +490,7 @@ You cannot use both the additive_build_content and additive_build_content_file a for whl_name, aliases in out.extra_aliases.items(): extra_aliases[hub_name].setdefault(whl_name, {}).update(aliases) exposed_packages.setdefault(hub_name, {}).update(out.exposed_packages) + target_platforms.setdefault(hub_name, {}).update(out.target_platforms) whl_libraries.update(out.whl_libraries) # TODO @aignas 2024-04-05: how do we support different requirement @@ -535,6 +528,10 @@ You cannot use both the additive_build_content and additive_build_content_file a } for hub_name, extra_whl_aliases in extra_aliases.items() }, + target_platforms = { + hub_name: sorted(p) + for hub_name, p in target_platforms.items() + }, whl_libraries = { k: dict(sorted(args.items())) for k, args in sorted(whl_libraries.items()) @@ -626,15 +623,13 @@ def _pip_impl(module_ctx): }, packages = mods.exposed_packages.get(hub_name, []), groups = mods.hub_group_map.get(hub_name), + target_platforms = mods.target_platforms.get(hub_name, []), ) 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. + # NOTE @aignas 2025-04-15: this is set to be reproducible, because the + # results after calling the PyPI index should be reproducible on each + # machine. return module_ctx.extension_metadata(reproducible = True) else: return None diff --git a/python/private/pypi/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl index 8050cd22ad..7988aca1c4 100644 --- a/python/private/pypi/generate_whl_library_build_bazel.bzl +++ b/python/private/pypi/generate_whl_library_build_bazel.bzl @@ -21,23 +21,23 @@ _RENDER = { "copy_files": render.dict, "data": render.list, "data_exclude": render.list, - "dependencies": render.list, - "dependencies_by_platform": lambda x: render.dict(x, value_repr = render.list), "entry_points": render.dict, + "extras": render.list, "group_deps": render.list, + "requires_dist": render.list, "srcs_exclude": render.list, - "tags": render.list, + "target_platforms": lambda x: render.list(x) if x else "target_platforms", } # NOTE @aignas 2024-10-25: We have to keep this so that files in # this repository can be publicly visible without the need for # export_files _TEMPLATE = """\ -load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets") +{loads} package(default_visibility = ["//visibility:public"]) -whl_library_targets( +whl_library_targets_from_requires( {kwargs} ) """ @@ -45,11 +45,13 @@ whl_library_targets( def generate_whl_library_build_bazel( *, annotation = None, + default_python_version = None, **kwargs): """Generate a BUILD file for an unzipped Wheel Args: annotation: The annotation for the build file. + default_python_version: The python version to use to parse the METADATA. **kwargs: Extra args serialized to be passed to the {obj}`whl_library_targets`. @@ -57,6 +59,18 @@ def generate_whl_library_build_bazel( A complete BUILD file as a string """ + loads = [ + """load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires")""", + ] + if not kwargs.setdefault("target_platforms", None): + dep_template = kwargs["dep_template"] + loads.append( + "load(\"{}\", \"{}\")".format( + dep_template.format(name = "", target = "config.bzl"), + "target_platforms", + ), + ) + additional_content = [] if annotation: kwargs["data"] = annotation.data @@ -66,10 +80,13 @@ def generate_whl_library_build_bazel( kwargs["srcs_exclude"] = annotation.srcs_exclude_glob if annotation.additive_build_content: additional_content.append(annotation.additive_build_content) + if default_python_version: + kwargs["default_python_version"] = default_python_version contents = "\n".join( [ _TEMPLATE.format( + loads = "\n".join(loads), kwargs = render.indent("\n".join([ "{} = {},".format(k, _RENDER.get(k, repr)(v)) for k, v in sorted(kwargs.items()) diff --git a/python/private/pypi/hub_repository.bzl b/python/private/pypi/hub_repository.bzl index 48245b4106..d2cbf88c24 100644 --- a/python/private/pypi/hub_repository.bzl +++ b/python/private/pypi/hub_repository.bzl @@ -45,7 +45,14 @@ def _impl(rctx): macro_tmpl = "@@{name}//{{}}:{{}}".format(name = rctx.attr.name) rctx.file("BUILD.bazel", _BUILD_FILE_CONTENTS) - rctx.template("requirements.bzl", rctx.attr._template, substitutions = { + rctx.template( + "config.bzl", + rctx.attr._config_template, + substitutions = { + "%%TARGET_PLATFORMS%%": render.list(rctx.attr.target_platforms), + }, + ) + rctx.template("requirements.bzl", rctx.attr._requirements_bzl_template, substitutions = { "%%ALL_DATA_REQUIREMENTS%%": render.list([ macro_tmpl.format(p, "data") for p in bzl_packages @@ -80,6 +87,10 @@ The list of packages that will be exposed via all_*requirements macros. Defaults mandatory = True, doc = "The apparent name of the repo. This is needed because in bzlmod, the name attribute becomes the canonical name.", ), + "target_platforms": attr.string_list( + mandatory = True, + doc = "All of the target platforms for the hub repo", + ), "whl_map": attr.string_dict( mandatory = True, doc = """\ @@ -87,7 +98,10 @@ The wheel map where values are json.encoded strings of the whl_map constructed in the pip.parse tag class. """, ), - "_template": attr.label( + "_config_template": attr.label( + default = ":config.bzl.tmpl.bzlmod", + ), + "_requirements_bzl_template": attr.label( default = ":requirements.bzl.tmpl.bzlmod", ), }, diff --git a/python/private/pypi/pep508.bzl b/python/private/pypi/pep508.bzl deleted file mode 100644 index e74352def2..0000000000 --- a/python/private/pypi/pep508.bzl +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2025 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. - -"""This module is for implementing PEP508 in starlark as FeatureFlagInfo -""" - -load(":pep508_env.bzl", _env = "env") -load(":pep508_evaluate.bzl", _evaluate = "evaluate", _to_string = "to_string") - -to_string = _to_string -evaluate = _evaluate -env = _env diff --git a/python/private/pypi/pep508_deps.bzl b/python/private/pypi/pep508_deps.bzl index af0a75362b..115bbd78d8 100644 --- a/python/private/pypi/pep508_deps.bzl +++ b/python/private/pypi/pep508_deps.bzl @@ -15,36 +15,24 @@ """This module is for implementing PEP508 compliant METADATA deps parsing. """ +load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION") load("//python/private:normalize_name.bzl", "normalize_name") load(":pep508_env.bzl", "env") load(":pep508_evaluate.bzl", "evaluate") load(":pep508_platform.bzl", "platform", "platform_from_str") load(":pep508_requirement.bzl", "requirement") -_ALL_OS_VALUES = [ - "windows", - "osx", - "linux", -] -_ALL_ARCH_VALUES = [ - "aarch64", - "ppc64", - "ppc64le", - "s390x", - "x86_32", - "x86_64", -] - -def deps(name, *, requires_dist, platforms = [], extras = [], host_python_version = None): +def deps(name, *, requires_dist, platforms = [], extras = [], excludes = [], default_python_version = None): """Parse the RequiresDist from wheel METADATA Args: name: {type}`str` the name of the wheel. requires_dist: {type}`list[str]` the list of RequiresDist lines from the METADATA file. + excludes: {type}`list[str]` what packages should we exclude. extras: {type}`list[str]` the requested extras to generate targets for. platforms: {type}`list[str]` the list of target platform strings. - host_python_version: {type}`str` the host python version. + default_python_version: {type}`str` the host python version. Returns: A struct with attributes: @@ -62,18 +50,17 @@ def deps(name, *, requires_dist, platforms = [], extras = [], host_python_versio want_extras = _resolve_extras(name, reqs, extras) # drop self edges - reqs = [r for r in reqs if r.name != name] + excludes = [name] + [normalize_name(x) for x in excludes] + default_python_version = default_python_version or DEFAULT_PYTHON_VERSION platforms = [ - platform_from_str(p, python_version = host_python_version) + platform_from_str(p, python_version = default_python_version) for p in platforms - ] or [ - platform_from_str("", python_version = host_python_version), ] abis = sorted({p.abi: True for p in platforms if p.abi}) - if host_python_version and len(abis) > 1: - _, _, minor_version = host_python_version.partition(".") + if default_python_version and len(abis) > 1: + _, _, minor_version = default_python_version.partition(".") minor_version, _, _ = minor_version.partition(".") default_abi = "cp3" + minor_version elif len(abis) > 1: @@ -83,11 +70,20 @@ def deps(name, *, requires_dist, platforms = [], extras = [], host_python_versio else: default_abi = None + reqs_by_name = {} + for req in reqs: - _add_req( + if req.name_ in excludes: + continue + + reqs_by_name.setdefault(req.name, []).append(req) + + for name, reqs in reqs_by_name.items(): + _add_reqs( deps, deps_select, - req, + normalize_name(name), + reqs, extras = want_extras, platforms = platforms, default_abi = default_abi, @@ -103,49 +99,14 @@ def deps(name, *, requires_dist, platforms = [], extras = [], host_python_versio def _platform_str(self): if self.abi == None: - if not self.os and not self.arch: - return "//conditions:default" - elif not self.arch: - return "@platforms//os:{}".format(self.os) - else: - return "{}_{}".format(self.os, self.arch) + return "{}_{}".format(self.os, self.arch) - minor_version = self.abi[3:] - if self.arch == None and self.os == None: - return str(Label("//python/config_settings:is_python_3.{}".format(minor_version))) - - return "cp3{}_{}_{}".format( - minor_version, + return "{}_{}_{}".format( + self.abi, self.os or "anyos", self.arch or "anyarch", ) -def _platform_specializations(self, cpu_values = _ALL_ARCH_VALUES, os_values = _ALL_OS_VALUES): - """Return the platform itself and all its unambiguous specializations. - - For more info about specializations see - https://bazel.build/docs/configurable-attributes - """ - specializations = [] - specializations.append(self) - if self.arch == None: - specializations.extend([ - platform(os = self.os, arch = arch, abi = self.abi) - for arch in cpu_values - ]) - if self.os == None: - specializations.extend([ - platform(os = os, arch = self.arch, abi = self.abi) - for os in os_values - ]) - if self.os == None and self.arch == None: - specializations.extend([ - platform(os = os, arch = arch, abi = self.abi) - for os in os_values - for arch in cpu_values - ]) - return specializations - def _add(deps, deps_select, dep, platform): dep = normalize_name(dep) @@ -172,53 +133,7 @@ def _add(deps, deps_select, dep, platform): return # Add the platform-specific branch - deps_select.setdefault(platform, {}) - - # Add the dep to specializations of the given platform if they - # exist in the select statement. - for p in _platform_specializations(platform): - if p not in deps_select: - continue - - deps_select[p][dep] = True - - if len(deps_select[platform]) == 1: - # We are adding a new item to the select and we need to ensure that - # existing dependencies from less specialized platforms are propagated - # to the newly added dependency set. - for p, _deps in deps_select.items(): - # Check if the existing platform overlaps with the given platform - if p == platform or platform not in _platform_specializations(p): - continue - - deps_select[platform].update(_deps) - -def _maybe_add_common_dep(deps, deps_select, platforms, dep): - abis = sorted({p.abi: True for p in platforms if p.abi}) - if len(abis) < 2: - return - - platforms = [platform()] + [ - platform(abi = abi) - for abi in abis - ] - - # If the dep is targeting all target python versions, lets add it to - # the common dependency list to simplify the select statements. - for p in platforms: - if p not in deps_select: - return - - if dep not in deps_select[p]: - return - - # All of the python version-specific branches have the dep, so lets add - # it to the common deps. - deps[dep] = True - for p in platforms: - deps_select[p].pop(dep) - if not deps_select[p]: - deps_select.pop(p) + deps_select.setdefault(platform, {})[dep] = True def _resolve_extras(self_name, reqs, extras): """Resolve extras which are due to depending on self[some_other_extra]. @@ -275,77 +190,37 @@ def _resolve_extras(self_name, reqs, extras): # Poor mans set return sorted({x: None for x in extras}) -def _add_req(deps, deps_select, req, *, extras, platforms, default_abi = None): - if not req.marker: - _add(deps, deps_select, req.name, None) - return - - # NOTE @aignas 2023-12-08: in order to have reasonable select statements - # we do have to have some parsing of the markers, so it begs the question - # if packaging should be reimplemented in Starlark to have the best solution - # for now we will implement it in Python and see what the best parsing result - # can be before making this decision. - match_os = len([ - tag - for tag in [ - "os_name", - "sys_platform", - "platform_system", - ] - if tag in req.marker - ]) > 0 - match_arch = "platform_machine" in req.marker - match_version = "version" in req.marker - - if not (match_os or match_arch or match_version): - if [ - True - for extra in extras - for p in platforms - if evaluate( - req.marker, - env = env( - target_platform = p, - extra = extra, - ), - ) - ]: - _add(deps, deps_select, req.name, None) - return +def _add_reqs(deps, deps_select, dep, reqs, *, extras, platforms, default_abi = None): + for req in reqs: + if not req.marker: + _add(deps, deps_select, dep, None) + return + platforms_to_add = {} for plat in platforms: - if not [ - True - for extra in extras - if evaluate( - req.marker, - env = env( - target_platform = plat, - extra = extra, - ), - ) - ]: + if plat in platforms_to_add: + # marker evaluation is more expensive than this check continue - if match_arch and default_abi: - _add(deps, deps_select, req.name, plat) - if plat.abi == default_abi: - _add(deps, deps_select, req.name, platform(os = plat.os, arch = plat.arch)) - elif match_arch: - _add(deps, deps_select, req.name, platform(os = plat.os, arch = plat.arch)) - elif match_os and default_abi: - _add(deps, deps_select, req.name, platform(os = plat.os, abi = plat.abi)) - if plat.abi == default_abi: - _add(deps, deps_select, req.name, platform(os = plat.os)) - elif match_os: - _add(deps, deps_select, req.name, platform(os = plat.os)) - elif match_version and default_abi: - _add(deps, deps_select, req.name, platform(abi = plat.abi)) - if plat.abi == default_abi: - _add(deps, deps_select, req.name, platform()) - elif match_version: - _add(deps, deps_select, req.name, None) - else: - fail("BUG: {} support is not implemented".format(req.marker)) + added = False + for extra in extras: + if added: + break + + for req in reqs: + if evaluate(req.marker, env = env(target_platform = plat, extra = extra)): + platforms_to_add[plat] = True + added = True + break + + if len(platforms_to_add) == len(platforms): + # the dep is in all target platforms, let's just add it to the regular + # list + _add(deps, deps_select, dep, None) + return - _maybe_add_common_dep(deps, deps_select, platforms, req.name) + for plat in platforms_to_add: + if default_abi: + _add(deps, deps_select, dep, plat) + if plat.abi == default_abi or not default_abi: + _add(deps, deps_select, dep, platform(os = plat.os, arch = plat.arch)) diff --git a/python/private/pypi/pep508_requirement.bzl b/python/private/pypi/pep508_requirement.bzl index ee7b5dfc35..b5be17f890 100644 --- a/python/private/pypi/pep508_requirement.bzl +++ b/python/private/pypi/pep508_requirement.bzl @@ -47,9 +47,11 @@ def requirement(spec): requires, _, _ = requires.partition(char) extras = extras_unparsed.replace(" ", "").split(",") name = requires.strip(" ") + name = normalize_name(name) return struct( - name = normalize_name(name).replace("_", "-"), + name = name.replace("_", "-"), + name_ = name, marker = marker.strip(" "), extras = extras, version = version, diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 0a580011ab..630dc8519f 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -15,6 +15,7 @@ "" load("//python/private:auth.bzl", "AUTH_ATTRS", "get_auth") +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") load("//python/private:envsubst.bzl", "envsubst") load("//python/private:is_standalone_interpreter.bzl", "is_standalone_interpreter") load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") @@ -22,13 +23,10 @@ load(":attrs.bzl", "ATTRS", "use_isolated") load(":deps.bzl", "all_repo_names", "record_files") load(":generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel") load(":parse_requirements.bzl", "host_platform") -load(":parse_whl_name.bzl", "parse_whl_name") load(":patch_whl.bzl", "patch_whl") -load(":pep508_deps.bzl", "deps") load(":pep508_requirement.bzl", "requirement") load(":pypi_repo_utils.bzl", "pypi_repo_utils") load(":whl_metadata.bzl", "whl_metadata") -load(":whl_target_platforms.bzl", "whl_target_platforms") _CPPFLAGS = "CPPFLAGS" _COMMAND_LINE_TOOLS_PATH_SLUG = "commandlinetools" @@ -344,20 +342,6 @@ def _whl_library_impl(rctx): timeout = rctx.attr.timeout, ) - target_platforms = rctx.attr.experimental_target_platforms - if target_platforms: - parsed_whl = parse_whl_name(whl_path.basename) - if parsed_whl.platform_tag != "any": - # NOTE @aignas 2023-12-04: if the wheel is a platform specific - # wheel, we only include deps for that target platform - target_platforms = [ - p.target_platform - for p in whl_target_platforms( - platform_tag = parsed_whl.platform_tag, - abi_tag = parsed_whl.abi_tag.strip("tm"), - ) - ] - pypi_repo_utils.execute_checked( rctx, op = "whl_library.ExtractWheel({}, {})".format(rctx.attr.name, whl_path), @@ -400,63 +384,45 @@ def _whl_library_impl(rctx): ) entry_points[entry_point_without_py] = entry_point_script_name - # TODO @aignas 2025-04-04: move this to whl_library_targets.bzl to have - # this in the analysis phase. - # - # This means that whl_library_targets will have to accept the following args: - # * name - the name of the package in the METADATA. - # * requires_dist - the list of METADATA Requires-Dist. - # * platforms - the list of target platforms. The target_platforms - # should come from the hub repo via a 'load' statement so that they don't - # need to be passed as an argument to `whl_library`. - # * extras - the list of required extras. This comes from the - # `rctx.attr.requirement` for now. In the future the required extras could - # stay in the hub repo, where we calculate the extra aliases that we need - # to create automatically and this way expose the targets for the specific - # extras. The first step will be to generate a target per extra for the - # `py_library` and `filegroup`. Maybe we need to have a special provider - # or an output group so that we can return the `whl` file from the - # `py_library` target? filegroup can use output groups to expose files. - # * host_python_version/versons - the list of python versions to support - # should come from the hub, similar to how the target platforms are specified. - # - # Extra things that we should move at the same time: - # * group_name, group_deps - this info can stay in the hub repository so that - # it is piped at the analysis time and changing the requirement groups does - # cause to re-fetch the deps. - python_version = metadata["python_version"] + if BZLMOD_ENABLED: + # The following attributes are unset on bzlmod and we pass data through + # the hub via load statements. + default_python_version = None + target_platforms = [] + else: + # NOTE @aignas 2025-04-16: if BZLMOD_ENABLED, we should use + # DEFAULT_PYTHON_VERSION since platforms always come with the actual + # python version otherwise we should use the version of the interpreter + # here. In WORKSPACE `multi_pip_parse` is using an interpreter for each + # `pip_parse` invocation, so we will have the host target platform + # only. Even if somebody would change the code to support + # `experimental_target_platforms`, they would be for a single python + # version. Hence, using the `default_python_version` that we get from the + # interpreter is correct. Hence, we unset the argument if we are on bzlmod. + default_python_version = metadata["python_version"] + target_platforms = rctx.attr.experimental_target_platforms or [host_platform(rctx)] + metadata = whl_metadata( install_dir = rctx.path("site-packages"), read_fn = rctx.read, logger = logger, ) - # TODO @aignas 2025-04-09: this will later be removed when loaded through the hub - major_minor, _, _ = python_version.rpartition(".") - package_deps = deps( - name = metadata.name, - requires_dist = metadata.requires_dist, - platforms = target_platforms or [ - "cp{}_{}".format(major_minor.replace(".", ""), host_platform(rctx)), - ], - extras = requirement(rctx.attr.requirement).extras, - host_python_version = python_version, - ) - build_file_contents = generate_whl_library_build_bazel( name = whl_path.basename, + metadata_name = metadata.name, + metadata_version = metadata.version, + requires_dist = metadata.requires_dist, dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix), - dependencies = package_deps.deps, - dependencies_by_platform = package_deps.deps_select, - group_name = rctx.attr.group_name, - group_deps = rctx.attr.group_deps, - data_exclude = rctx.attr.pip_data_exclude, - tags = [ - "pypi_name=" + metadata.name, - "pypi_version=" + metadata.version, - ], entry_points = entry_points, + target_platforms = target_platforms, + default_python_version = default_python_version, + # TODO @aignas 2025-04-14: load through the hub: annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), + data_exclude = rctx.attr.pip_data_exclude, + extras = requirement(rctx.attr.requirement).extras, + group_deps = rctx.attr.group_deps, + group_name = rctx.attr.group_name, ) rctx.file("BUILD.bazel", build_file_contents) @@ -517,10 +483,7 @@ and the target that we need respectively. doc = "Name of the group, if any.", ), "repo": attr.string( - doc = """\ -Pointer to parent repo name. Used to make these rules rerun if the parent repo changes. -Only used in WORKSPACE when the {attr}`dep_template` is not set. -""", + doc = "Pointer to parent repo name. Used to make these rules rerun if the parent repo changes.", ), "repo_prefix": attr.string( doc = """ diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl index d32746b604..cf3df133c4 100644 --- a/python/private/pypi/whl_library_targets.bzl +++ b/python/private/pypi/whl_library_targets.bzl @@ -29,6 +29,89 @@ load( "WHEEL_FILE_IMPL_LABEL", "WHEEL_FILE_PUBLIC_LABEL", ) +load(":parse_whl_name.bzl", "parse_whl_name") +load(":pep508_deps.bzl", "deps") +load(":whl_target_platforms.bzl", "whl_target_platforms") + +def whl_library_targets_from_requires( + *, + name, + metadata_name = "", + metadata_version = "", + requires_dist = [], + extras = [], + target_platforms = [], + default_python_version = None, + group_deps = [], + **kwargs): + """The macro to create whl targets from the METADATA. + + Args: + name: {type}`str` The wheel filename + metadata_name: {type}`str` The package name as written in wheel `METADATA`. + metadata_version: {type}`str` The package version as written in wheel `METADATA`. + group_deps: {type}`list[str]` names of fellow members of the group (if + any). These will be excluded from generated deps lists so as to avoid + direct cycles. These dependencies will be provided at runtime by the + group rules which wrap this library and its fellows together. + requires_dist: {type}`list[str]` The list of `Requires-Dist` values from + the whl `METADATA`. + extras: {type}`list[str]` The list of requested extras. This essentially includes extra transitive dependencies in the final targets depending on the wheel `METADATA`. + target_platforms: {type}`list[str]` The list of target platforms to create + dependency closures for. + default_python_version: {type}`str` The python version to assume when parsing + the `METADATA`. This is only used when the `target_platforms` do not + include the version information. + **kwargs: Extra args passed to the {obj}`whl_library_targets` + """ + package_deps = _parse_requires_dist( + name = name, + default_python_version = default_python_version, + requires_dist = requires_dist, + excludes = group_deps, + extras = extras, + target_platforms = target_platforms, + ) + whl_library_targets( + name = name, + dependencies = package_deps.deps, + dependencies_by_platform = package_deps.deps_select, + tags = [ + "pypi_name={}".format(metadata_name), + "pypi_version={}".format(metadata_version), + ], + **kwargs + ) + +def _parse_requires_dist( + *, + name, + default_python_version, + requires_dist, + excludes, + extras, + target_platforms): + parsed_whl = parse_whl_name(name) + + # NOTE @aignas 2023-12-04: if the wheel is a platform specific wheel, we + # only include deps for that target platform + if parsed_whl.platform_tag != "any": + target_platforms = [ + p.target_platform + for p in whl_target_platforms( + platform_tag = parsed_whl.platform_tag, + abi_tag = parsed_whl.abi_tag.strip("tm"), + ) + ] + + return deps( + name = normalize_name(parsed_whl.distribution), + requires_dist = requires_dist, + platforms = target_platforms, + excludes = excludes, + extras = extras, + default_python_version = default_python_version, + ) def whl_library_targets( *, diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index 4d86d6a6e0..ce5474e35b 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -436,7 +436,6 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ pypi.whl_libraries().contains_exactly({ "pypi_312_torch_cp312_cp312_linux_x86_64_8800deef": { "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": ["cp312_linux_x86_64"], "filename": "torch-2.4.1+cpu-cp312-cp312-linux_x86_64.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "torch==2.4.1+cpu", @@ -445,7 +444,6 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ }, "pypi_312_torch_cp312_cp312_manylinux_2_17_aarch64_36109432": { "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": ["cp312_linux_aarch64"], "filename": "torch-2.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "torch==2.4.1", @@ -454,7 +452,6 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ }, "pypi_312_torch_cp312_cp312_win_amd64_3a570e5c": { "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": ["cp312_windows_x86_64"], "filename": "torch-2.4.1+cpu-cp312-cp312-win_amd64.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "torch==2.4.1+cpu", @@ -463,7 +460,6 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ }, "pypi_312_torch_cp312_none_macosx_11_0_arm64_72b484d5": { "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": ["cp312_osx_aarch64"], "filename": "torch-2.4.1-cp312-none-macosx_11_0_arm64.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "torch==2.4.1", @@ -750,7 +746,6 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef pypi.whl_libraries().contains_exactly({ "pypi_315_any_name": { "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"], "extra_pip_args": ["--extra-args-for-sdist-building"], "filename": "any-name.tar.gz", "python_interpreter_target": "unit_test_interpreter_target", @@ -760,7 +755,6 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }, "pypi_315_direct_without_sha_0_0_1_py3_none_any": { "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"], "filename": "direct_without_sha-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "direct_without_sha==0.0.1 @ example-direct.org/direct_without_sha-0.0.1-py3-none-any.whl", @@ -781,7 +775,6 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }, "pypi_315_simple_py3_none_any_deadb00f": { "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"], "filename": "simple-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "simple==0.0.1", @@ -790,7 +783,6 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }, "pypi_315_simple_sdist_deadbeef": { "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"], "extra_pip_args": ["--extra-args-for-sdist-building"], "filename": "simple-0.0.1.tar.gz", "python_interpreter_target": "unit_test_interpreter_target", @@ -800,7 +792,6 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }, "pypi_315_some_pkg_py3_none_any_deadbaaf": { "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"], "filename": "some_pkg-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "some_pkg==0.0.1 @ example-direct.org/some_pkg-0.0.1-py3-none-any.whl --hash=sha256:deadbaaf", @@ -809,7 +800,6 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }, "pypi_315_some_py3_none_any_deadb33f": { "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"], "filename": "some-other-pkg-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "some_other_pkg==0.0.1", diff --git a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl index b0d8f6d17e..7bd19b65c1 100644 --- a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl +++ b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl @@ -21,11 +21,11 @@ _tests = [] def _test_all(env): want = """\ -load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets") +load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires") package(default_visibility = ["//visibility:public"]) -whl_library_targets( +whl_library_targets_from_requires( copy_executables = { "exec_src": "exec_dest", }, @@ -38,19 +38,71 @@ whl_library_targets( "data_exclude_all", ], dep_template = "@pypi//{name}:{target}", - dependencies = [ + entry_points = { + "foo": "bar.py", + }, + group_deps = [ + "foo", + "fox", + "qux", + ], + group_name = "qux", + name = "foo.whl", + requires_dist = [ "foo", "bar-baz", "qux", ], - dependencies_by_platform = { - "linux_x86_64": [ - "box", - "box-amd64", - ], - "windows_x86_64": ["fox"], - "@platforms//os:linux": ["box"], + srcs_exclude = ["srcs_exclude_all"], + target_platforms = ["foo"], +) + +# SOMETHING SPECIAL AT THE END +""" + actual = generate_whl_library_build_bazel( + dep_template = "@pypi//{name}:{target}", + name = "foo.whl", + requires_dist = ["foo", "bar-baz", "qux"], + entry_points = { + "foo": "bar.py", + }, + data_exclude = ["exclude_via_attr"], + annotation = struct( + copy_files = {"file_src": "file_dest"}, + copy_executables = {"exec_src": "exec_dest"}, + data = ["extra_target"], + data_exclude_glob = ["data_exclude_all"], + srcs_exclude_glob = ["srcs_exclude_all"], + additive_build_content = """# SOMETHING SPECIAL AT THE END""", + ), + group_name = "qux", + target_platforms = ["foo"], + group_deps = ["foo", "fox", "qux"], + ) + env.expect.that_str(actual.replace("@@", "@")).equals(want) + +_tests.append(_test_all) + +def _test_all_with_loads(env): + want = """\ +load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires") +load("@pypi//:config.bzl", "target_platforms") + +package(default_visibility = ["//visibility:public"]) + +whl_library_targets_from_requires( + copy_executables = { + "exec_src": "exec_dest", }, + copy_files = { + "file_src": "file_dest", + }, + data = ["extra_target"], + data_exclude = [ + "exclude_via_attr", + "data_exclude_all", + ], + dep_template = "@pypi//{name}:{target}", entry_points = { "foo": "bar.py", }, @@ -61,11 +113,13 @@ whl_library_targets( ], group_name = "qux", name = "foo.whl", - srcs_exclude = ["srcs_exclude_all"], - tags = [ - "tag2", - "tag1", + requires_dist = [ + "foo", + "bar-baz", + "qux", ], + srcs_exclude = ["srcs_exclude_all"], + target_platforms = target_platforms, ) # SOMETHING SPECIAL AT THE END @@ -73,13 +127,7 @@ whl_library_targets( actual = generate_whl_library_build_bazel( dep_template = "@pypi//{name}:{target}", name = "foo.whl", - dependencies = ["foo", "bar-baz", "qux"], - dependencies_by_platform = { - "linux_x86_64": ["box", "box-amd64"], - "windows_x86_64": ["fox"], - "@platforms//os:linux": ["box"], # buildifier: disable=unsorted-dict-items to check that we sort inside the test - }, - tags = ["tag2", "tag1"], + requires_dist = ["foo", "bar-baz", "qux"], entry_points = { "foo": "bar.py", }, @@ -97,7 +145,7 @@ whl_library_targets( ) env.expect.that_str(actual.replace("@@", "@")).equals(want) -_tests.append(_test_all) +_tests.append(_test_all_with_loads) def generate_whl_library_build_bazel_test_suite(name): """Create the test suite. diff --git a/tests/pypi/pep508/deps_tests.bzl b/tests/pypi/pep508/deps_tests.bzl index 44031ab6a5..d362925080 100644 --- a/tests/pypi/pep508/deps_tests.bzl +++ b/tests/pypi/pep508/deps_tests.bzl @@ -29,58 +29,48 @@ def test_simple_deps(env): _tests.append(test_simple_deps) def test_can_add_os_specific_deps(env): - got = deps( - "foo", - requires_dist = [ - "bar", - "an_osx_dep; sys_platform=='darwin'", - "posix_dep; os_name=='posix'", - "win_dep; os_name=='nt'", - ], - platforms = [ - "linux_x86_64", - "osx_x86_64", - "osx_aarch64", - "windows_x86_64", - ], - host_python_version = "3.3.1", - ) - - env.expect.that_collection(got.deps).contains_exactly(["bar"]) - env.expect.that_dict(got.deps_select).contains_exactly({ - "@platforms//os:linux": ["posix_dep"], - "@platforms//os:osx": ["an_osx_dep", "posix_dep"], - "@platforms//os:windows": ["win_dep"], - }) + for target in [ + struct( + platforms = [ + "linux_x86_64", + "osx_x86_64", + "osx_aarch64", + "windows_x86_64", + ], + python_version = "3.3.1", + ), + struct( + platforms = [ + "cp33_linux_x86_64", + "cp33_osx_x86_64", + "cp33_osx_aarch64", + "cp33_windows_x86_64", + ], + python_version = "", + ), + ]: + got = deps( + "foo", + requires_dist = [ + "bar", + "an_osx_dep; sys_platform=='darwin'", + "posix_dep; os_name=='posix'", + "win_dep; os_name=='nt'", + ], + platforms = target.platforms, + default_python_version = target.python_version, + ) + + env.expect.that_collection(got.deps).contains_exactly(["bar"]) + env.expect.that_dict(got.deps_select).contains_exactly({ + "linux_x86_64": ["posix_dep"], + "osx_aarch64": ["an_osx_dep", "posix_dep"], + "osx_x86_64": ["an_osx_dep", "posix_dep"], + "windows_x86_64": ["win_dep"], + }) _tests.append(test_can_add_os_specific_deps) -def test_can_add_os_specific_deps_with_python_version(env): - got = deps( - "foo", - requires_dist = [ - "bar", - "an_osx_dep; sys_platform=='darwin'", - "posix_dep; os_name=='posix'", - "win_dep; os_name=='nt'", - ], - platforms = [ - "cp33_linux_x86_64", - "cp33_osx_x86_64", - "cp33_osx_aarch64", - "cp33_windows_x86_64", - ], - ) - - env.expect.that_collection(got.deps).contains_exactly(["bar"]) - env.expect.that_dict(got.deps_select).contains_exactly({ - "@platforms//os:linux": ["posix_dep"], - "@platforms//os:osx": ["an_osx_dep", "posix_dep"], - "@platforms//os:windows": ["win_dep"], - }) - -_tests.append(test_can_add_os_specific_deps_with_python_version) - def test_deps_are_added_to_more_specialized_platforms(env): got = deps( "foo", @@ -92,41 +82,16 @@ def test_deps_are_added_to_more_specialized_platforms(env): "osx_x86_64", "osx_aarch64", ], - host_python_version = "3.8.4", + default_python_version = "3.8.4", ) - env.expect.that_collection(got.deps).contains_exactly([]) + env.expect.that_collection(got.deps).contains_exactly(["mac_dep"]) env.expect.that_dict(got.deps_select).contains_exactly({ - "@platforms//os:osx": ["mac_dep"], - "osx_aarch64": ["m1_dep", "mac_dep"], + "osx_aarch64": ["m1_dep"], }) _tests.append(test_deps_are_added_to_more_specialized_platforms) -def test_deps_from_more_specialized_platforms_are_propagated(env): - got = deps( - "foo", - requires_dist = [ - "a_mac_dep; sys_platform=='darwin'", - "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", - ], - platforms = [ - "osx_x86_64", - "osx_aarch64", - ], - host_python_version = "3.8.4", - ) - - env.expect.that_collection(got.deps).contains_exactly([]) - env.expect.that_dict(got.deps_select).contains_exactly( - { - "@platforms//os:osx": ["a_mac_dep"], - "osx_aarch64": ["a_mac_dep", "m1_dep"], - }, - ) - -_tests.append(test_deps_from_more_specialized_platforms_are_propagated) - def test_non_platform_markers_are_added_to_common_deps(env): got = deps( "foo", @@ -141,7 +106,7 @@ def test_non_platform_markers_are_added_to_common_deps(env): "osx_aarch64", "windows_x86_64", ], - host_python_version = "3.8.4", + default_python_version = "3.8.4", ) env.expect.that_collection(got.deps).contains_exactly(["bar", "baz"]) @@ -204,38 +169,34 @@ def _test_can_get_deps_based_on_specific_python_version(env): platforms = ["cp37_linux_x86_64"], ) + # since there is a single target platform, the deps_select will be empty env.expect.that_collection(py37.deps).contains_exactly(["bar", "baz"]) env.expect.that_dict(py37.deps_select).contains_exactly({}) - env.expect.that_collection(py38.deps).contains_exactly(["bar"]) - env.expect.that_dict(py38.deps_select).contains_exactly({"@platforms//os:linux": ["posix_dep"]}) + env.expect.that_collection(py38.deps).contains_exactly(["bar", "posix_dep"]) + env.expect.that_dict(py38.deps_select).contains_exactly({}) _tests.append(_test_can_get_deps_based_on_specific_python_version) def _test_no_version_select_when_single_version(env): - requires_dist = [ - "bar", - "baz; python_version >= '3.8'", - "posix_dep; os_name=='posix'", - "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", - "arch_dep; platform_machine=='x86_64' and python_version >= '3.8'", - ] - host_python_version = "3.7.5" - got = deps( "foo", - requires_dist = requires_dist, + requires_dist = [ + "bar", + "baz; python_version >= '3.8'", + "posix_dep; os_name=='posix'", + "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", + "arch_dep; platform_machine=='x86_64' and python_version >= '3.8'", + ], platforms = [ "cp38_linux_x86_64", "cp38_windows_x86_64", ], - host_python_version = host_python_version, + default_python_version = "", ) - env.expect.that_collection(got.deps).contains_exactly(["bar", "baz"]) + env.expect.that_collection(got.deps).contains_exactly(["bar", "baz", "arch_dep"]) env.expect.that_dict(got.deps_select).contains_exactly({ - "@platforms//os:linux": ["posix_dep", "posix_dep_with_version"], - "linux_x86_64": ["arch_dep", "posix_dep", "posix_dep_with_version"], - "windows_x86_64": ["arch_dep"], + "linux_x86_64": ["posix_dep", "posix_dep_with_version"], }) _tests.append(_test_no_version_select_when_single_version) @@ -249,7 +210,7 @@ def _test_can_get_version_select(env): "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", "arch_dep; platform_machine=='x86_64' and python_version < '3.8'", ] - host_python_version = "3.7.4" + default_python_version = "3.7.4" got = deps( "foo", @@ -259,31 +220,19 @@ def _test_can_get_version_select(env): for minor in [7, 8, 9] for os in ["linux", "windows"] ], - host_python_version = host_python_version, + default_python_version = default_python_version, ) env.expect.that_collection(got.deps).contains_exactly(["bar"]) env.expect.that_dict(got.deps_select).contains_exactly({ - str(Label("//python/config_settings:is_python_3.7")): ["baz"], - str(Label("//python/config_settings:is_python_3.8")): ["baz_new"], - str(Label("//python/config_settings:is_python_3.9")): ["baz_new"], - "@platforms//os:linux": ["baz", "posix_dep"], - "cp37_linux_anyarch": ["baz", "posix_dep"], "cp37_linux_x86_64": ["arch_dep", "baz", "posix_dep"], "cp37_windows_x86_64": ["arch_dep", "baz"], - "cp38_linux_anyarch": [ - "baz_new", - "posix_dep", - "posix_dep_with_version", - ], - "cp39_linux_anyarch": [ - "baz_new", - "posix_dep", - "posix_dep_with_version", - ], + "cp38_linux_x86_64": ["baz_new", "posix_dep", "posix_dep_with_version"], + "cp38_windows_x86_64": ["baz_new"], + "cp39_linux_x86_64": ["baz_new", "posix_dep", "posix_dep_with_version"], + "cp39_windows_x86_64": ["baz_new"], "linux_x86_64": ["arch_dep", "baz", "posix_dep"], "windows_x86_64": ["arch_dep", "baz"], - "//conditions:default": ["baz"], }) _tests.append(_test_can_get_version_select) @@ -294,7 +243,7 @@ def _test_deps_spanning_all_target_py_versions_are_added_to_common(env): "baz (<2,>=1.11) ; python_version < '3.8'", "baz (<2,>=1.14) ; python_version >= '3.8'", ] - host_python_version = "3.8.4" + default_python_version = "3.8.4" got = deps( "foo", @@ -303,7 +252,7 @@ def _test_deps_spanning_all_target_py_versions_are_added_to_common(env): "cp3{}_linux_x86_64".format(minor) for minor in [7, 8, 9] ], - host_python_version = host_python_version, + default_python_version = default_python_version, ) env.expect.that_collection(got.deps).contains_exactly(["bar", "baz"]) @@ -312,7 +261,7 @@ def _test_deps_spanning_all_target_py_versions_are_added_to_common(env): _tests.append(_test_deps_spanning_all_target_py_versions_are_added_to_common) def _test_deps_are_not_duplicated(env): - host_python_version = "3.7.4" + default_python_version = "3.7.4" # See an example in # https://files.pythonhosted.org/packages/76/9e/db1c2d56c04b97981c06663384f45f28950a73d9acf840c4006d60d0a1ff/opencv_python-4.9.0.80-cp37-abi3-win32.whl.metadata @@ -336,7 +285,7 @@ def _test_deps_are_not_duplicated(env): for os in ["linux", "osx", "windows"] for arch in ["x86_64", "aarch64"] ], - host_python_version = host_python_version, + default_python_version = default_python_version, ) env.expect.that_collection(got.deps).contains_exactly(["bar"]) @@ -345,7 +294,7 @@ def _test_deps_are_not_duplicated(env): _tests.append(_test_deps_are_not_duplicated) def _test_deps_are_not_duplicated_when_encountering_platform_dep_first(env): - host_python_version = "3.7.1" + default_python_version = "3.7.1" # Note, that we are sorting the incoming `requires_dist` and we need to ensure that we are not getting any # issues even if the platform-specific line comes first. @@ -363,15 +312,13 @@ def _test_deps_are_not_duplicated_when_encountering_platform_dep_first(env): "cp310_linux_aarch64", "cp310_linux_x86_64", ], - host_python_version = host_python_version, + default_python_version = default_python_version, ) - # TODO @aignas 2025-02-24: this test case in the python version is passing but - # I am not sure why. The starlark version behaviour looks more correct. env.expect.that_collection(got.deps).contains_exactly([]) env.expect.that_dict(got.deps_select).contains_exactly({ - str(Label("//python/config_settings:is_python_3.10")): ["bar"], "cp310_linux_aarch64": ["bar"], + "cp310_linux_x86_64": ["bar"], "cp37_linux_aarch64": ["bar"], "linux_aarch64": ["bar"], }) diff --git a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl index f738e03b5d..61e5441050 100644 --- a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl +++ b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl @@ -16,7 +16,7 @@ load("@rules_testing//lib:test_suite.bzl", "test_suite") load("//python/private:glob_excludes.bzl", "glob_excludes") # buildifier: disable=bzl-visibility -load("//python/private/pypi:whl_library_targets.bzl", "whl_library_targets") # buildifier: disable=bzl-visibility +load("//python/private/pypi:whl_library_targets.bzl", "whl_library_targets", "whl_library_targets_from_requires") # buildifier: disable=bzl-visibility _tests = [] @@ -183,6 +183,71 @@ def _test_entrypoints(env): _tests.append(_test_entrypoints) +def _test_whl_and_library_deps_from_requires(env): + filegroup_calls = [] + py_library_calls = [] + + whl_library_targets_from_requires( + name = "foo-0-py3-none-any.whl", + metadata_name = "Foo", + metadata_version = "0", + dep_template = "@pypi_{name}//:{target}", + requires_dist = [ + "foo", # this self-edge will be ignored + "bar-baz", + ], + target_platforms = ["cp38_linux_x86_64"], + default_python_version = "3.8.1", + data_exclude = [], + # Overrides for testing + filegroups = {}, + native = struct( + filegroup = lambda **kwargs: filegroup_calls.append(kwargs), + config_setting = lambda **_: None, + glob = _glob, + select = _select, + ), + rules = struct( + py_library = lambda **kwargs: py_library_calls.append(kwargs), + ), + ) + + env.expect.that_collection(filegroup_calls).contains_exactly([ + { + "name": "whl", + "srcs": ["foo-0-py3-none-any.whl"], + "data": ["@pypi_bar_baz//:whl"], + "visibility": ["//visibility:public"], + }, + ]) # buildifier: @unsorted-dict-items + env.expect.that_collection(py_library_calls).contains_exactly([ + { + "name": "pkg", + "srcs": _glob( + ["site-packages/**/*.py"], + exclude = [], + allow_empty = True, + ), + "pyi_srcs": _glob(["site-packages/**/*.pyi"], allow_empty = True), + "data": [] + _glob( + ["site-packages/**/*"], + exclude = [ + "**/*.py", + "**/*.pyc", + "**/*.pyc.*", + "**/*.dist-info/RECORD", + ] + glob_excludes.version_dependent_exclusions(), + ), + "imports": ["site-packages"], + "deps": ["@pypi_bar_baz//:pkg"], + "tags": ["pypi_name=Foo", "pypi_version=0"], + "visibility": ["//visibility:public"], + "experimental_venvs_site_packages": Label("//python/config_settings:venvs_site_packages"), + }, + ]) # buildifier: @unsorted-dict-items + +_tests.append(_test_whl_and_library_deps_from_requires) + def _test_whl_and_library_deps(env): filegroup_calls = [] py_library_calls = [] From c981569cc89c76eb57a78f0bbc47f1566211c924 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 21 Apr 2025 15:13:10 +0900 Subject: [PATCH 039/156] chore: remove a stray file (#2795) Remove a stray file --- config.bzl.tmpl.bzlmod | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 config.bzl.tmpl.bzlmod diff --git a/config.bzl.tmpl.bzlmod b/config.bzl.tmpl.bzlmod deleted file mode 100644 index e69de29bb2..0000000000 From e11873323ffc2694489131fd2f861c0619907bc1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 22:19:07 +0000 Subject: [PATCH 040/156] build(deps): bump sphinx-rtd-theme from 3.0.1 to 3.0.2 in /docs (#2802) Bumps [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) from 3.0.1 to 3.0.2.
Changelog

Sourced from sphinx-rtd-theme's changelog.

3.0.2

  • Show current translation when the flyout is attached
  • Fix JavaScript issue that didn't allow users to disable selectors

.. _release-3.0.1:

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=sphinx-rtd-theme&package-manager=pip&previous-version=3.0.1&new-version=3.0.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 5e308b00f4..747ae59e1a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -319,9 +319,9 @@ sphinx-reredirects==0.1.6 \ --hash=sha256:c491cba545f67be9697508727818d8626626366245ae64456fe29f37e9bbea64 \ --hash=sha256:efd50c766fbc5bf40cd5148e10c00f2c00d143027de5c5e48beece93cc40eeea # via rules-python-docs (docs/pyproject.toml) -sphinx-rtd-theme==3.0.1 \ - --hash=sha256:921c0ece75e90633ee876bd7b148cfaad136b481907ad154ac3669b6fc957916 \ - --hash=sha256:a4c5745d1b06dfcb80b7704fe532eb765b44065a8fad9851e4258c8804140703 +sphinx-rtd-theme==3.0.2 \ + --hash=sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13 \ + --hash=sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85 # via rules-python-docs (docs/pyproject.toml) sphinxcontrib-applehelp==2.0.0 \ --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \ From a57c4de9dbb722765685cd2deae71fc73efcde75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 22:19:54 +0000 Subject: [PATCH 041/156] build(deps): bump astroid from 3.3.6 to 3.3.9 in /docs (#2803) Bumps [astroid](https://github.com/pylint-dev/astroid) from 3.3.6 to 3.3.9.
Release notes

Sourced from astroid's releases.

v3.3.9

What's New in astroid 3.3.9?

Release date: 2025-03-09

v3.3.8

What's New in astroid 3.3.8?

Release date: 2024-12-23

  • Fix inability to import collections.abc in python 3.13.1. The reported fixes in astroid 3.3.6 and 3.3.7 did not actually fix this issue.

    Closes pylint-dev/pylint#10112

v3.3.7

What's New in astroid 3.3.7?

Release date: 2024-12-21

  • Fix inability to import collections.abc in python 3.13.1. The reported fix in astroid 3.3.6 did not actually fix this issue.

    Closes pylint-dev/pylint#10112

Changelog

Sourced from astroid's changelog.

What's New in astroid 3.3.9?

Release date: 2025-03-09

What's New in astroid 3.3.8?

Release date: 2024-12-23

  • Fix inability to import collections.abc in python 3.13.1. The reported fixes in astroid 3.3.6 and 3.3.7 did not actually fix this issue.

    Closes pylint-dev/pylint#10112

What's New in astroid 3.3.7?

Release date: 2024-12-20

This release was yanked.

  • Fix inability to import collections.abc in python 3.13.1. The reported fix in astroid 3.3.6 did not actually fix this issue.

    Closes pylint-dev/pylint#10112

Commits
  • a6ccad5 Bump astroid to 3.3.9, update changelog
  • ec2df97 Add setuptools in order to run 3.12/3.13 tests
  • 74c34fb Bump actions/cache from 4.2.0 to 4.2.2 (#2692)
  • 5512bf2 Update release workflow to use Trusted Publishing (#2696)
  • aad8e68 [Backport maintenance/3.3.x] Fix missing dict (#2685) (#2690)
  • 234be58 Fix RuntimeError caused by analyzing live objects with __getattribute__ or ...
  • 6aeafd5 Bump pylint in pre-commit configuration to 3.2.7
  • d52799b Bump astroid to 3.3.8, update changelog
  • 68714df [Backport maintenance/3.3.x] Another attempt at fixing the collections.abc ...
  • 7cfbad1 Skip flaky recursion test on PyPy (#2661) (#2663)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=astroid&package-manager=pip&previous-version=3.3.6&new-version=3.3.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 747ae59e1a..ee242e07d0 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -10,9 +10,9 @@ alabaster==1.0.0 \ --hash=sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e \ --hash=sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b # via sphinx -astroid==3.3.6 \ - --hash=sha256:6aaea045f938c735ead292204afdb977a36e989522b7833ef6fea94de743f442 \ - --hash=sha256:db676dc4f3ae6bfe31cda227dc60e03438378d7a896aec57422c95634e8d722f +astroid==3.3.9 \ + --hash=sha256:622cc8e3048684aa42c820d9d218978021c3c3d174fb03a9f0d615921744f550 \ + --hash=sha256:d05bfd0acba96a7bd43e222828b7d9bc1e138aaeb0649707908d3702a9831248 # via sphinx-autodoc2 babel==2.17.0 \ --hash=sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d \ From aaf8ce8adb43536f24ecfe38038351afafcbfa65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 22:22:05 +0000 Subject: [PATCH 042/156] build(deps): bump packaging from 24.2 to 25.0 in /docs (#2804) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [packaging](https://github.com/pypa/packaging) from 24.2 to 25.0.
Release notes

Sourced from packaging's releases.

25.0

What's Changed

New Contributors

Full Changelog: https://github.com/pypa/packaging/compare/24.2...25.0

Changelog

Sourced from packaging's changelog.

25.0 - 2025-04-19


* PEP 751: Add support for ``extras`` and ``dependency_groups`` markers.
(:issue:`885`)
* PEP 738: Add support for Android platform tags. (:issue:`880`)
Commits
  • f585376 Bump for release
  • 600ecea Add changelog entries
  • 3910129 support 'extras' and 'dependency_groups' markers (#888)
  • 8e49b43 Add support for PEP 738 Android tags (#880)
  • e624d8e Bump the github-actions group with 3 updates (#886)
  • 71f38d8 Bump the github-actions group with 2 updates (#878)
  • 9b4922d Bump the github-actions group with 3 updates (#870)
  • 8510bd9 Upgrade to ruff 0.9.1 (#865)
  • 9375ec2 Re-add tests for Unicode file name parsing (#863)
  • 2256ed4 Bump the github-actions group across 1 directory with 2 updates (#864)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=packaging&package-manager=pip&previous-version=24.2&new-version=25.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index ee242e07d0..e4ec16fa5e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -223,9 +223,9 @@ myst-parser==4.0.0 \ --hash=sha256:851c9dfb44e36e56d15d05e72f02b80da21a9e0d07cba96baf5e2d476bb91531 \ --hash=sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d # via rules-python-docs (docs/pyproject.toml) -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f +packaging==25.0 \ + --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ + --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f # via # readthedocs-sphinx-ext # sphinx From f4780f7b71dc224ea3b51b4ec8048b829e1f3375 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 21 Apr 2025 15:35:13 -0700 Subject: [PATCH 043/156] fix: fixes to prepare for making bootstrap=script the default for Linux (#2760) Various cleanup and prep work to switch bootstrap=script to be the default. * Change `bootstrap_impl` to always be disabled for windows. This allows setting it to true in a bazelrc without worrying about the target platform. This is done by using FeatureFlagInfo to force the value to disabled for windows. This allows any downstream usages of the flag to Just Work and not have to add selects() for windows themselves. * Switch pip_repository_annotations test to `import python.runfiles`. The script bootstrap doesn't add the runfiles root to sys.path, so `import rules_python` stops working. * Switch gazelle workspace to using the runtime-env toolchain. It was previously implicitly using the deprecated one built into bazel, which doesn't provide various necessary provider fields. * Make the local toolchain use `sys._base_executable` instead of `sys.executable` when finding the interpreter. Otherwise, it might find a venv interpreter or not properly handle wrapper scripts like pyenv. * Adds a toolchain attribute/field to indicate if the toolchain supports a build-time created venv. This is due to the runtime_env toolchain. See PR comments for details, but in short: if we don't know the python interpreter path and version at build time, the venv may not properly activate or find site-packages. If it isn't supported, then the stage1 bootstrap creates a temporary venv, similar to how the zip case is handled. Unfortunately, this requires invoking Python itself as part of program startup, but I don't see a way around that -- note this is only triggered by the runtime-env toolchain. * Make the runtime-env toolchain better support virtualenvs. Because it's a wrapper that re-invokes Python, Python can't automatically detect its in a venv. Two tricks are used (`exec -a` and PYTHONEXECUTABLE) to help address this (but they aren't guaranteed to work, hence the "recreate at runtime" logic). * Fix a subtle issue where `sys._base_executable` isn't set correctly due to `home` missing in the pyvenv.cfg file. This mostly only affected the creation of venvs from within the bazel-created venv. * Change the bazel site init to always add the build-time created site-packages (if it exists) as a site directory. This matches the system_python bootstrap behavior a bit better, which just shoved everything onto sys.path using PYTHONPATH. * Skip running runtime_env_toolchains tests on RBE. RBE's system python is 3.6, but the script bootstrap uses 3.9 features. (Running it on RBE is questionable anyways). Along the way... * Ignore gazelle convenience symlinks * Switch pip_repository_annotations test to use non-legacy_external_runfiles based paths. The legacy behavior is disabled in Bazel 8+ by default. * Also document why the script bootstrap doesn't add the runfiles root to sys.path. Work towards https://github.com/bazel-contrib/rules_python/issues/2521 --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- .bazelignore | 1 + CHANGELOG.md | 10 +- examples/pip_repository_annotations/.bazelrc | 1 + .../pip_repository_annotations_test.py | 25 ++--- gazelle/WORKSPACE | 2 + python/config_settings/BUILD.bazel | 16 +++- python/private/BUILD.bazel | 1 + python/private/config_settings.bzl | 30 ++++++ python/private/flags.bzl | 32 ++++++- python/private/get_local_runtime_info.py | 1 + python/private/local_runtime_repo.bzl | 14 +++ python/private/py_executable.bzl | 35 ++++++- python/private/py_runtime_info.bzl | 26 ++++- python/private/py_runtime_rule.bzl | 12 +++ python/private/runtime_env_toolchain.bzl | 12 +++ .../runtime_env_toolchain_interpreter.sh | 26 ++++- python/private/site_init_template.py | 30 ++++++ python/private/stage1_bootstrap_template.sh | 94 ++++++++++++++----- python/private/stage2_bootstrap_template.py | 22 +++++ .../integration/local_toolchains/BUILD.bazel | 2 + tests/integration/local_toolchains/test.py | 53 +++++++++-- tests/runtime_env_toolchain/BUILD.bazel | 4 + 22 files changed, 393 insertions(+), 56 deletions(-) diff --git a/.bazelignore b/.bazelignore index e10af2035d..fb999097f5 100644 --- a/.bazelignore +++ b/.bazelignore @@ -25,6 +25,7 @@ examples/pip_parse/bazel-pip_parse examples/pip_parse_vendored/bazel-pip_parse_vendored examples/pip_repository_annotations/bazel-pip_repository_annotations examples/py_proto_library/bazel-py_proto_library +gazelle/bazel-gazelle tests/integration/compile_pip_requirements/bazel-compile_pip_requirements tests/integration/ignore_root_user_error/bazel-ignore_root_user_error tests/integration/local_toolchains/bazel-local_toolchains diff --git a/CHANGELOG.md b/CHANGELOG.md index 154b66114b..f696cefde2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,13 +54,21 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-changed} ### Changed -* Nothing changed. +* (rules) On Windows, {obj}`--bootstrap_impl=system_python` is forced. This + allows setting `--bootstrap_impl=script` in bazelrc for mixed-platform + environments. {#v0-0-0-fixed} ### Fixed + * (rules) PyInfo provider is now advertised by py_test, py_binary, and py_library; this allows aspects using required_providers to function correctly. ([#2506](https://github.com/bazel-contrib/rules_python/issues/2506)). +* Fixes when using {obj}`--bootstrap_impl=script`: + * `compile_pip_requirements` now works with it + * The `sys._base_executable` value will reflect the underlying interpreter, + not venv interpreter. + * The {obj}`//python/runtime_env_toolchains:all` toolchain now works with it. {#v0-0-0-added} ### Added diff --git a/examples/pip_repository_annotations/.bazelrc b/examples/pip_repository_annotations/.bazelrc index c16c5a24f2..9397bd31b8 100644 --- a/examples/pip_repository_annotations/.bazelrc +++ b/examples/pip_repository_annotations/.bazelrc @@ -5,4 +5,5 @@ try-import %workspace%/user.bazelrc # is in examples/bzlmod as the `whl_mods` feature. common --noenable_bzlmod common --enable_workspace +common --legacy_external_runfiles=false common --incompatible_python_disallow_native_rules diff --git a/examples/pip_repository_annotations/pip_repository_annotations_test.py b/examples/pip_repository_annotations/pip_repository_annotations_test.py index e41dd4f0f6..219be1ba03 100644 --- a/examples/pip_repository_annotations/pip_repository_annotations_test.py +++ b/examples/pip_repository_annotations/pip_repository_annotations_test.py @@ -21,7 +21,7 @@ import unittest from pathlib import Path -from rules_python.python.runfiles import runfiles +from python.runfiles import runfiles class PipRepositoryAnnotationsTest(unittest.TestCase): @@ -34,11 +34,7 @@ def wheel_pkg_dir(self) -> str: def test_build_content_and_data(self): r = runfiles.Create() - rpath = r.Rlocation( - "pip_repository_annotations_example/external/{}/generated_file.txt".format( - self.wheel_pkg_dir() - ) - ) + rpath = r.Rlocation("{}/generated_file.txt".format(self.wheel_pkg_dir())) generated_file = Path(rpath) self.assertTrue(generated_file.exists()) @@ -47,11 +43,7 @@ def test_build_content_and_data(self): def test_copy_files(self): r = runfiles.Create() - rpath = r.Rlocation( - "pip_repository_annotations_example/external/{}/copied_content/file.txt".format( - self.wheel_pkg_dir() - ) - ) + rpath = r.Rlocation("{}/copied_content/file.txt".format(self.wheel_pkg_dir())) copied_file = Path(rpath) self.assertTrue(copied_file.exists()) @@ -61,7 +53,7 @@ def test_copy_files(self): def test_copy_executables(self): r = runfiles.Create() rpath = r.Rlocation( - "pip_repository_annotations_example/external/{}/copied_content/executable{}".format( + "{}/copied_content/executable{}".format( self.wheel_pkg_dir(), ".exe" if platform.system() == "windows" else ".py", ) @@ -82,7 +74,7 @@ def test_data_exclude_glob(self): current_wheel_version = "0.38.4" r = runfiles.Create() - dist_info_dir = "pip_repository_annotations_example/external/{}/site-packages/wheel-{}.dist-info".format( + dist_info_dir = "{}/site-packages/wheel-{}.dist-info".format( self.wheel_pkg_dir(), current_wheel_version, ) @@ -113,11 +105,8 @@ def test_extra(self): # This test verifies that annotations work correctly for pip packages with extras # specified, in this case requests[security]. r = runfiles.Create() - rpath = r.Rlocation( - "pip_repository_annotations_example/external/{}/generated_file.txt".format( - self.requests_pkg_dir() - ) - ) + path = "{}/generated_file.txt".format(self.requests_pkg_dir()) + rpath = r.Rlocation(path) generated_file = Path(rpath) self.assertTrue(generated_file.exists()) diff --git a/gazelle/WORKSPACE b/gazelle/WORKSPACE index 14a124d5f2..ad428b10cd 100644 --- a/gazelle/WORKSPACE +++ b/gazelle/WORKSPACE @@ -42,6 +42,8 @@ load("//:internal_dev_deps.bzl", "internal_dev_deps") internal_dev_deps() +register_toolchains("@rules_python//python/runtime_env_toolchains:all") + load("//:deps.bzl", _py_gazelle_deps = "gazelle_deps") # gazelle:repository_macro deps.bzl%go_deps diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel index 45354e24d9..872d7d1bda 100644 --- a/python/config_settings/BUILD.bazel +++ b/python/config_settings/BUILD.bazel @@ -11,6 +11,7 @@ load( "PrecompileSourceRetentionFlag", "VenvsSitePackages", "VenvsUseDeclareSymlinkFlag", + rp_string_flag = "string_flag", ) load( "//python/private/pypi:flags.bzl", @@ -87,14 +88,27 @@ string_flag( visibility = ["//visibility:public"], ) -string_flag( +rp_string_flag( name = "bootstrap_impl", build_setting_default = BootstrapImplFlag.SYSTEM_PYTHON, + override = select({ + # Windows doesn't yet support bootstrap=script, so force disable it + ":_is_windows": BootstrapImplFlag.SYSTEM_PYTHON, + "//conditions:default": "", + }), values = sorted(BootstrapImplFlag.__members__.values()), # NOTE: Only public because it's an implicit dependency visibility = ["//visibility:public"], ) +# For some reason, @platforms//os:windows can't be directly used +# in the select() for the flag. But it can be used when put behind +# a config_setting(). +config_setting( + name = "_is_windows", + constraint_values = ["@platforms//os:windows"], +) + # This is used for pip and hermetic toolchain resolution. string_flag( name = "py_linux_libc", diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index b63f446be3..9cc8ffc62c 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -86,6 +86,7 @@ bzl_library( name = "runtime_env_toolchain_bzl", srcs = ["runtime_env_toolchain.bzl"], deps = [ + ":config_settings_bzl", ":py_exec_tools_toolchain_bzl", ":toolchain_types_bzl", "//python:py_runtime_bzl", diff --git a/python/private/config_settings.bzl b/python/private/config_settings.bzl index e5f9d865d1..2cf7968061 100644 --- a/python/private/config_settings.bzl +++ b/python/private/config_settings.bzl @@ -209,3 +209,33 @@ _current_config = rule( "_template": attr.string(default = _DEBUG_ENV_MESSAGE_TEMPLATE), }, ) + +def is_python_version_at_least(name, **kwargs): + flag_name = "_{}_flag".format(name) + native.config_setting( + name = name, + flag_values = { + flag_name: "yes", + }, + ) + _python_version_at_least( + name = flag_name, + visibility = ["//visibility:private"], + **kwargs + ) + +def _python_version_at_least_impl(ctx): + at_least = tuple(ctx.attr.at_least.split(".")) + current = tuple( + ctx.attr._major_minor[config_common.FeatureFlagInfo].value.split("."), + ) + value = "yes" if current >= at_least else "no" + return [config_common.FeatureFlagInfo(value = value)] + +_python_version_at_least = rule( + implementation = _python_version_at_least_impl, + attrs = { + "at_least": attr.string(mandatory = True), + "_major_minor": attr.label(default = _PYTHON_VERSION_MAJOR_MINOR_FLAG), + }, +) diff --git a/python/private/flags.bzl b/python/private/flags.bzl index c53e4610ff..40ce63b3b0 100644 --- a/python/private/flags.bzl +++ b/python/private/flags.bzl @@ -35,8 +35,38 @@ AddSrcsToRunfilesFlag = FlagEnum( is_enabled = _AddSrcsToRunfilesFlag_is_enabled, ) +def _string_flag_impl(ctx): + if ctx.attr.override: + value = ctx.attr.override + else: + value = ctx.build_setting_value + + if value not in ctx.attr.values: + fail(( + "Invalid value for {name}: got {value}, must " + + "be one of {allowed}" + ).format( + name = ctx.label, + value = value, + allowed = ctx.attr.values, + )) + + return [ + BuildSettingInfo(value = value), + config_common.FeatureFlagInfo(value = value), + ] + +string_flag = rule( + implementation = _string_flag_impl, + build_setting = config.string(flag = True), + attrs = { + "override": attr.string(), + "values": attr.string_list(), + }, +) + def _bootstrap_impl_flag_get_value(ctx): - return ctx.attr._bootstrap_impl_flag[BuildSettingInfo].value + return ctx.attr._bootstrap_impl_flag[config_common.FeatureFlagInfo].value # buildifier: disable=name-conventions BootstrapImplFlag = enum( diff --git a/python/private/get_local_runtime_info.py b/python/private/get_local_runtime_info.py index 0207f56bef..19db3a2935 100644 --- a/python/private/get_local_runtime_info.py +++ b/python/private/get_local_runtime_info.py @@ -22,6 +22,7 @@ "micro": sys.version_info.micro, "include": sysconfig.get_path("include"), "implementation_name": sys.implementation.name, + "base_executable": sys._base_executable, } config_vars = [ diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index fb1a8e29ac..ec0643e497 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -84,6 +84,20 @@ def _local_runtime_repo_impl(rctx): info = json.decode(exec_result.stdout) logger.info(lambda: _format_get_info_result(info)) + # We use base_executable because we want the path within a Python + # installation directory ("PYTHONHOME"). The problems with sys.executable + # are: + # * If we're in an activated venv, then we don't want the venv's + # `bin/python3` path to be used -- it isn't an actual Python installation. + # * If sys.executable is a wrapper (e.g. pyenv), then (1) it may not be + # located within an actual Python installation directory, and (2) it + # can interfer with Python recognizing when it's within a venv. + # + # In some cases, it may be a symlink (usually e.g. `python3->python3.12`), + # but we don't realpath() it to respect what it has decided is the + # appropriate path. + interpreter_path = info["base_executable"] + # NOTE: Keep in sync with recursive glob in define_local_runtime_toolchain_impl repo_utils.watch_tree(rctx, rctx.path(info["include"])) diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index b4cda21b1d..a8c669afd9 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -350,6 +350,7 @@ def _create_executable( main_py = main_py, imports = imports, runtime_details = runtime_details, + venv = venv, ) extra_runfiles = ctx.runfiles([stage2_bootstrap] + venv.files_without_interpreter) zip_main = _create_zip_main( @@ -538,11 +539,14 @@ def _create_venv(ctx, output_prefix, imports, runtime_details): ctx.actions.write(pyvenv_cfg, "") runtime = runtime_details.effective_runtime + venvs_use_declare_symlink_enabled = ( VenvsUseDeclareSymlinkFlag.get_value(ctx) == VenvsUseDeclareSymlinkFlag.YES ) + recreate_venv_at_runtime = False - if not venvs_use_declare_symlink_enabled: + if not venvs_use_declare_symlink_enabled or not runtime.supports_build_time_venv: + recreate_venv_at_runtime = True if runtime.interpreter: interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path) else: @@ -557,6 +561,8 @@ def _create_venv(ctx, output_prefix, imports, runtime_details): ctx.actions.write(interpreter, "actual:{}".format(interpreter_actual_path)) elif runtime.interpreter: + # Some wrappers around the interpreter (e.g. pyenv) use the program + # name to decide what to do, so preserve the name. py_exe_basename = paths.basename(runtime.interpreter.short_path) # Even though ctx.actions.symlink() is used, using @@ -594,7 +600,8 @@ def _create_venv(ctx, output_prefix, imports, runtime_details): if "t" in runtime.abi_flags: version += "t" - site_packages = "{}/lib/python{}/site-packages".format(venv, version) + venv_site_packages = "lib/python{}/site-packages".format(version) + site_packages = "{}/{}".format(venv, venv_site_packages) pth = ctx.actions.declare_file("{}/bazel.pth".format(site_packages)) ctx.actions.write(pth, "import _bazel_site_init\n") @@ -616,10 +623,12 @@ def _create_venv(ctx, output_prefix, imports, runtime_details): return struct( interpreter = interpreter, - recreate_venv_at_runtime = not venvs_use_declare_symlink_enabled, + recreate_venv_at_runtime = recreate_venv_at_runtime, # Runfiles root relative path or absolute path interpreter_actual_path = interpreter_actual_path, files_without_interpreter = [pyvenv_cfg, pth, site_init] + site_packages_symlinks, + # string; venv-relative path to the site-packages directory. + venv_site_packages = venv_site_packages, ) def _create_site_packages_symlinks(ctx, site_packages): @@ -716,7 +725,8 @@ def _create_stage2_bootstrap( output_sibling, main_py, imports, - runtime_details): + runtime_details, + venv = None): output = ctx.actions.declare_file( # Prepend with underscore to prevent pytest from trying to # process the bootstrap for files starting with `test_` @@ -731,6 +741,14 @@ def _create_stage2_bootstrap( main_py_path = "{}/{}".format(ctx.workspace_name, main_py.short_path) else: main_py_path = "" + + # The stage2 bootstrap uses the venv site-packages location to fix up issues + # that occur when the toolchain doesn't support the build-time venv. + if venv and not runtime.supports_build_time_venv: + venv_rel_site_packages = venv.venv_site_packages + else: + venv_rel_site_packages = "" + ctx.actions.expand_template( template = template, output = output, @@ -741,6 +759,7 @@ def _create_stage2_bootstrap( "%main%": main_py_path, "%main_module%": ctx.attr.main_module, "%target%": str(ctx.label), + "%venv_rel_site_packages%": venv_rel_site_packages, "%workspace_name%": ctx.workspace_name, }, is_executable = True, @@ -766,6 +785,12 @@ def _create_stage1_bootstrap( python_binary_actual = venv.interpreter_actual_path if venv else "" + # Runtime may be None on Windows due to the --python_path flag. + if runtime and runtime.supports_build_time_venv: + resolve_python_binary_at_runtime = "0" + else: + resolve_python_binary_at_runtime = "1" + subs = { "%interpreter_args%": "\n".join([ '"{}"'.format(v) @@ -775,7 +800,9 @@ def _create_stage1_bootstrap( "%python_binary%": python_binary_path, "%python_binary_actual%": python_binary_actual, "%recreate_venv_at_runtime%": str(int(venv.recreate_venv_at_runtime)) if venv else "0", + "%resolve_python_binary_at_runtime%": resolve_python_binary_at_runtime, "%target%": str(ctx.label), + "%venv_rel_site_packages%": venv.venv_site_packages if venv else "", "%workspace_name%": ctx.workspace_name, } diff --git a/python/private/py_runtime_info.bzl b/python/private/py_runtime_info.bzl index 4297391068..d2ae17e360 100644 --- a/python/private/py_runtime_info.bzl +++ b/python/private/py_runtime_info.bzl @@ -67,7 +67,8 @@ def _PyRuntimeInfo_init( stage2_bootstrap_template = None, zip_main_template = None, abi_flags = "", - site_init_template = None): + site_init_template = None, + supports_build_time_venv = True): if (interpreter_path and interpreter) or (not interpreter_path and not interpreter): fail("exactly one of interpreter or interpreter_path must be specified") @@ -119,6 +120,7 @@ def _PyRuntimeInfo_init( "site_init_template": site_init_template, "stage2_bootstrap_template": stage2_bootstrap_template, "stub_shebang": stub_shebang, + "supports_build_time_venv": supports_build_time_venv, "zip_main_template": zip_main_template, } @@ -312,6 +314,28 @@ The following substitutions are made during template expansion: "Shebang" expression prepended to the bootstrapping Python stub script used when executing {obj}`py_binary` targets. Does not apply to Windows. +""", + "supports_build_time_venv": """ +:type: bool + +True if this toolchain supports the build-time created virtual environment. +False if not or unknown. If build-time venv creation isn't supported, then binaries may +fallback to non-venv solutions or creating a venv at runtime. + +In order to use the build-time created virtual environment, a toolchain needs +to meet two criteria: +1. Specifying the underlying executable (e.g. `/usr/bin/python3`, as reported by + `sys._base_executable`) for the venv executable (`$venv/bin/python3`, as reported + by `sys.executable`). This typically requires relative symlinking the venv + path to the underlying path at build time, or using the `PYTHONEXECUTABLE` + environment variable (Python 3.11+) at runtime. +2. Having the build-time created site-packages directory + (`/lib/python{version}/site-packages`) recognized by the runtime + interpreter. This typically requires the Python version to be known at + build-time and match at runtime. + +:::{versionadded} VERSION_NEXT_FEATURE +::: """, "zip_main_template": """ :type: File diff --git a/python/private/py_runtime_rule.bzl b/python/private/py_runtime_rule.bzl index a85f5b25f2..6dadcfeac3 100644 --- a/python/private/py_runtime_rule.bzl +++ b/python/private/py_runtime_rule.bzl @@ -130,6 +130,7 @@ def _py_runtime_impl(ctx): zip_main_template = ctx.file.zip_main_template, abi_flags = abi_flags, site_init_template = ctx.file.site_init_template, + supports_build_time_venv = ctx.attr.supports_build_time_venv, )) if not IS_BAZEL_7_OR_HIGHER: @@ -353,6 +354,17 @@ motivation. Does not apply to Windows. """, ), + "supports_build_time_venv": attr.bool( + doc = """ +Whether this runtime supports virtualenvs created at build time. + +See {obj}`PyRuntimeInfo.supports_build_time_venv` for docs. + +:::{versionadded} VERSION_NEXT_FEATURE +::: +""", + default = True, + ), "zip_main_template": attr.label( default = "//python/private:zip_main_template", allow_single_file = True, diff --git a/python/private/runtime_env_toolchain.bzl b/python/private/runtime_env_toolchain.bzl index 2116012c03..1956ad5e95 100644 --- a/python/private/runtime_env_toolchain.bzl +++ b/python/private/runtime_env_toolchain.bzl @@ -17,6 +17,7 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") load("//python:py_runtime.bzl", "py_runtime") load("//python:py_runtime_pair.bzl", "py_runtime_pair") load("//python/cc:py_cc_toolchain.bzl", "py_cc_toolchain") +load("//python/private:config_settings.bzl", "is_python_version_at_least") load(":py_exec_tools_toolchain.bzl", "py_exec_tools_toolchain") load(":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "PY_CC_TOOLCHAIN_TYPE", "TARGET_TOOLCHAIN_TYPE") @@ -38,6 +39,11 @@ def define_runtime_env_toolchain(name): """ base_name = name.replace("_toolchain", "") + supports_build_time_venv = select({ + ":_is_at_least_py3.11": True, + "//conditions:default": False, + }) + py_runtime( name = "_runtime_env_py3_runtime", interpreter = "//python/private:runtime_env_toolchain_interpreter.sh", @@ -45,6 +51,7 @@ def define_runtime_env_toolchain(name): stub_shebang = "#!/usr/bin/env python3", visibility = ["//visibility:private"], tags = ["manual"], + supports_build_time_venv = supports_build_time_venv, ) # This is a dummy runtime whose interpreter_path triggers the native rule @@ -56,6 +63,7 @@ def define_runtime_env_toolchain(name): python_version = "PY3", visibility = ["//visibility:private"], tags = ["manual"], + supports_build_time_venv = supports_build_time_venv, ) py_runtime_pair( @@ -110,3 +118,7 @@ def define_runtime_env_toolchain(name): toolchain_type = PY_CC_TOOLCHAIN_TYPE, visibility = ["//visibility:public"], ) + is_python_version_at_least( + name = "_is_at_least_py3.11", + at_least = "3.11", + ) diff --git a/python/private/runtime_env_toolchain_interpreter.sh b/python/private/runtime_env_toolchain_interpreter.sh index b09bc53e5c..6159d4f38c 100755 --- a/python/private/runtime_env_toolchain_interpreter.sh +++ b/python/private/runtime_env_toolchain_interpreter.sh @@ -53,5 +53,29 @@ documentation for py_runtime_pair \ (https://github.com/bazel-contrib/rules_python/blob/master/docs/python.md#py_runtime_pair)." fi -exec "$PYTHON_BIN" "$@" +# Because this is a wrapper script that invokes Python, it prevents Python from +# detecting virtualenvs like normal (i.e. using the venv symlink to find the +# real interpreter). To work around this, we have to manually detect the venv, +# then trick the interpreter into understanding we're in a virtual env. +self_dir=$(dirname "$0") +if [ -e "$self_dir/pyvenv.cfg" ] || [ -e "$self_dir/../pyvenv.cfg" ]; then + case "$0" in + /*) + venv_bin="$0" + ;; + *) + venv_bin="$PWD/$0" + ;; + esac + # PYTHONEXECUTABLE is also used because `exec -a` doesn't fully trick the + # pyenv wrappers. + # NOTE: The PYTHONEXECUTABLE envvar only works for non-Mac starting in Python 3.11 + export PYTHONEXECUTABLE="$venv_bin" + # Python looks at argv[0] to determine sys.executable, so use exec -a + # to make it think it's the venv's binary, not the actual one invoked. + # NOTE: exec -a isn't strictly posix-compatible, but very widespread + exec -a "$venv_bin" "$PYTHON_BIN" "$@" +else + exec "$PYTHON_BIN" "$@" +fi diff --git a/python/private/site_init_template.py b/python/private/site_init_template.py index 40fb4e4139..a87a0d2a8f 100644 --- a/python/private/site_init_template.py +++ b/python/private/site_init_template.py @@ -125,6 +125,14 @@ def _search_path(name): def _setup_sys_path(): + """Perform Bazel/binary specific sys.path setup. + + NOTE: We do not add _RUNFILES_ROOT to sys.path for two reasons: + 1. Under workspace, it makes every external repository importable. If a Bazel + repository matches a Python import name, they conflict. + 2. Under bzlmod, the repo names in the runfiles directory aren't importable + Python names, so there's no point in adding the runfiles root to sys.path. + """ seen = set(sys.path) python_path_entries = [] @@ -195,5 +203,27 @@ def _maybe_add_path(path): return coverage_setup +def _fixup_sys_base_executable(): + """Fixup sys._base_executable to account for Bazel-specific pyvenv.cfg + + The pyvenv.cfg created for py_binary leaves the `home` key unset. A + side-effect of this is `sys._base_executable` points to the venv executable, + not the actual executable. This mostly doesn't matter, but does affect + using the venv module to create venvs (they point to the venv executable, not + the actual executable). + """ + # Must have been set correctly? + if sys.executable != sys._base_executable: + return + # Not in a venv, so don't touch anything. + if sys.prefix == sys.base_prefix: + return + exe = os.path.realpath(sys.executable) + _print_verbose("setting sys._base_executable:", exe) + sys._base_executable = exe + + +_fixup_sys_base_executable() + COVERAGE_SETUP = _setup_sys_path() _print_verbose("DONE") diff --git a/python/private/stage1_bootstrap_template.sh b/python/private/stage1_bootstrap_template.sh index c487624934..d992b55cae 100644 --- a/python/private/stage1_bootstrap_template.sh +++ b/python/private/stage1_bootstrap_template.sh @@ -9,7 +9,8 @@ fi # runfiles-relative path STAGE2_BOOTSTRAP="%stage2_bootstrap%" -# runfiles-relative path to python interpreter to use +# runfiles-relative path to python interpreter to use. +# This is the `bin/python3` path in the binary's venv. PYTHON_BINARY='%python_binary%' # The path that PYTHON_BINARY should symlink to. # runfiles-relative path, absolute path, or single word. @@ -18,8 +19,17 @@ PYTHON_BINARY_ACTUAL="%python_binary_actual%" # 0 or 1 IS_ZIPFILE="%is_zipfile%" -# 0 or 1 +# 0 or 1. +# If 1, then a venv will be created at runtime that replicates what would have +# been the build-time structure. RECREATE_VENV_AT_RUNTIME="%recreate_venv_at_runtime%" +# 0 or 1 +# If 1, then the path to python will be resolved by running +# PYTHON_BINARY_ACTUAL to determine the actual underlying interpreter. +RESOLVE_PYTHON_BINARY_AT_RUNTIME="%resolve_python_binary_at_runtime%" +# venv-relative path to the site-packages +# e.g. lib/python3.12t/site-packages +VENV_REL_SITE_PACKAGES="%venv_rel_site_packages%" # array of strings declare -a INTERPRETER_ARGS_FROM_TARGET=( @@ -152,34 +162,72 @@ elif [[ "$RECREATE_VENV_AT_RUNTIME" == "1" ]]; then fi fi - if [[ "$PYTHON_BINARY_ACTUAL" == /* ]]; then - # An absolute path, i.e. platform runtime, e.g. /usr/bin/python3 - symlink_to=$PYTHON_BINARY_ACTUAL - elif [[ "$PYTHON_BINARY_ACTUAL" == */* ]]; then - # A runfiles-relative path - symlink_to="$RUNFILES_DIR/$PYTHON_BINARY_ACTUAL" - else - # A plain word, e.g. "python3". Symlink to where PATH leads - symlink_to=$(which $PYTHON_BINARY_ACTUAL) - # Guard against trying to symlink to an empty value - if [[ $? -ne 0 ]]; then - echo >&2 "ERROR: Python to use not found on PATH: $PYTHON_BINARY_ACTUAL" - exit 1 - fi - fi - mkdir -p "$venv/bin" # Match the basename; some tools, e.g. pyvenv key off the executable name python_exe="$venv/bin/$(basename $PYTHON_BINARY_ACTUAL)" + if [[ ! -e "$python_exe" ]]; then - ln -s "$symlink_to" "$python_exe" + if [[ "$PYTHON_BINARY_ACTUAL" == /* ]]; then + # An absolute path, i.e. platform runtime, e.g. /usr/bin/python3 + python_exe_actual=$PYTHON_BINARY_ACTUAL + elif [[ "$PYTHON_BINARY_ACTUAL" == */* ]]; then + # A runfiles-relative path + python_exe_actual="$RUNFILES_DIR/$PYTHON_BINARY_ACTUAL" + else + # A plain word, e.g. "python3". Symlink to where PATH leads + python_exe_actual=$(which $PYTHON_BINARY_ACTUAL) + # Guard against trying to symlink to an empty value + if [[ $? -ne 0 ]]; then + echo >&2 "ERROR: Python to use not found on PATH: $PYTHON_BINARY_ACTUAL" + exit 1 + fi + fi + + runfiles_venv="$RUNFILES_DIR/$(dirname $(dirname $PYTHON_BINARY))" + # When RESOLVE_PYTHON_BINARY_AT_RUNTIME is true, it means the toolchain + # has thrown two complications at us: + # 1. The build-time assumption of the Python version may not match the + # runtime Python version. The site-packages directory path includes the + # Python version, so when the versions don't match, the runtime won't + # find it. + # 2. The interpreter might be a wrapper script, which interferes with Python's + # ability to detect when it's within a venv. Starting in Python 3.11, + # the PYTHONEXECUTABLE environment variable can fix this, but due to (1), + # we don't know if that is supported without running Python. + # To fix (1), we symlink the desired site-packages path to the build-time + # directory. Hopefully the version mismatch is OK :D. + # To fix (2), we determine the actual underlying interpreter and symlink + # to that. + if [[ "$RESOLVE_PYTHON_BINARY_AT_RUNTIME" == "1" ]]; then + { + read -r resolved_py_exe + read -r resolved_site_packages + } < <("$python_exe_actual" -I < Date: Mon, 21 Apr 2025 17:00:40 -0700 Subject: [PATCH 044/156] fix: escape more invalid repo string characters (#2801) Also escape plus and percent when generating the repo name from the wheel version. Sometimes they have such characters in them. Fixes https://github.com/bazel-contrib/rules_python/issues/2799 Co-authored-by: Richard Levasseur --- python/private/pypi/whl_repo_name.bzl | 2 +- tests/pypi/whl_repo_name/whl_repo_name_tests.bzl | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/python/private/pypi/whl_repo_name.bzl b/python/private/pypi/whl_repo_name.bzl index 02a7c8142c..2b3b5418aa 100644 --- a/python/private/pypi/whl_repo_name.bzl +++ b/python/private/pypi/whl_repo_name.bzl @@ -44,7 +44,7 @@ def whl_repo_name(filename, sha256): else: parsed = parse_whl_name(filename) name = normalize_name(parsed.distribution) - version = parsed.version.replace(".", "_").replace("!", "_") + version = parsed.version.replace(".", "_").replace("!", "_").replace("+", "_").replace("%", "_") python_tag, _, _ = parsed.python_tag.partition(".") abi_tag, _, _ = parsed.abi_tag.partition(".") platform_tag, _, _ = parsed.platform_tag.partition(".") diff --git a/tests/pypi/whl_repo_name/whl_repo_name_tests.bzl b/tests/pypi/whl_repo_name/whl_repo_name_tests.bzl index f0d1d059e1..35e6bcdf9f 100644 --- a/tests/pypi/whl_repo_name/whl_repo_name_tests.bzl +++ b/tests/pypi/whl_repo_name/whl_repo_name_tests.bzl @@ -54,6 +54,18 @@ def _test_platform_whl(env): _tests.append(_test_platform_whl) +def _test_name_with_plus(env): + got = whl_repo_name("gptqmodel-2.0.0+cu126torch2.6-cp312-cp312-linux_x86_64.whl", "") + env.expect.that_str(got).equals("gptqmodel_2_0_0_cu126torch2_6_cp312_cp312_linux_x86_64") + +_tests.append(_test_name_with_plus) + +def _test_name_with_percent(env): + got = whl_repo_name("gptqmodel-2.0.0%2Bcu126torch2.6-cp312-cp312-linux_x86_64.whl", "") + env.expect.that_str(got).equals("gptqmodel_2_0_0_2Bcu126torch2_6_cp312_cp312_linux_x86_64") + +_tests.append(_test_name_with_percent) + def whl_repo_name_test_suite(name): """Create the test suite. From 1d69ad68d7959570acde61d8705f1f437c0691b0 Mon Sep 17 00:00:00 2001 From: Keith Smiley Date: Tue, 22 Apr 2025 05:49:15 -0700 Subject: [PATCH 045/156] fix: parsing metadata with inline licenses (#2806) The wheel `METADATA` parsing implemented in 1.4 missed the fact that whitespace is significant and sometimes License is included inline in the `METADATA` file itself. This change ensures that we stop parsing the `METADATA` file only on first completely empty line. Fixes https://github.com/bazel-contrib/rules_python/issues/2796 --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- python/private/pypi/whl_metadata.bzl | 2 +- .../pypi/whl_metadata/whl_metadata_tests.bzl | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/python/private/pypi/whl_metadata.bzl b/python/private/pypi/whl_metadata.bzl index 8a86ffbff1..cf2d51afda 100644 --- a/python/private/pypi/whl_metadata.bzl +++ b/python/private/pypi/whl_metadata.bzl @@ -52,7 +52,7 @@ def parse_whl_metadata(contents): "version": "", } for line in contents.strip().split("\n"): - if not line.strip(): + if not line: # Stop parsing on first empty line, which marks the end of the # headers containing the metadata. break diff --git a/tests/pypi/whl_metadata/whl_metadata_tests.bzl b/tests/pypi/whl_metadata/whl_metadata_tests.bzl index 4acbc9213d..329423a26c 100644 --- a/tests/pypi/whl_metadata/whl_metadata_tests.bzl +++ b/tests/pypi/whl_metadata/whl_metadata_tests.bzl @@ -140,6 +140,37 @@ Requires-Dist: this will be ignored _tests.append(_test_parse_metadata_all) +def _test_parse_metadata_multiline_license(env): + got = _parse_whl_metadata( + env, + # NOTE: The trailing whitespace here is meaningful as an empty line + # denotes the end of the header. + contents = """\ +Name: foo +Version: 0.0.1 +License: some License + + some line + + another line + +Requires-Dist: bar; extra == "all" +Provides-Extra: all + +Requires-Dist: this will be ignored +""", + ) + got.name().equals("foo") + got.version().equals("0.0.1") + got.requires_dist().contains_exactly([ + "bar; extra == \"all\"", + ]) + got.provides_extra().contains_exactly([ + "all", + ]) + +_tests.append(_test_parse_metadata_multiline_license) + def whl_metadata_test_suite(name): # buildifier: disable=function-docstring test_suite( name = name, From 830261e4b1c427c7f646f689fedf45117dd54aad Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Wed, 23 Apr 2025 01:45:10 +0900 Subject: [PATCH 046/156] test(pypi): add a test case for simpleapi html parsing with % (#2811) In addition to #2801 I wanted to ensure that we are getting the correct filename when downloading wheels. It seems that the `%` in the wheel filename might get through wheels that get referenced via direct URL in the requirements.txt files. --------- Co-authored-by: Richard Levasseur Co-authored-by: Richard Levasseur --- .../parse_simpleapi_html_tests.bzl | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl b/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl index abaa7a6a49..191079d214 100644 --- a/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl +++ b/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl @@ -303,6 +303,25 @@ def _test_whls(env): yanked = False, ), ), + ( + struct( + attrs = [ + 'href="/whl/cpu/torch-2.6.0%2Bcpu-cp39-cp39-manylinux_2_28_aarch64.whl#sha256=deadbeef"', + ], + filename = "torch-2.6.0+cpu-cp39-cp39-manylinux_2_28_aarch64.whl", + url = "https://example.org/", + ), + struct( + filename = "torch-2.6.0+cpu-cp39-cp39-manylinux_2_28_aarch64.whl", + metadata_sha256 = "", + metadata_url = "", + sha256 = "deadbeef", + version = "2.6.0+cpu", + # A URL with % could occur if directly written in requirements. + url = "https://example.org/whl/cpu/torch-2.6.0%2Bcpu-cp39-cp39-manylinux_2_28_aarch64.whl", + yanked = False, + ), + ), ] for (input, want) in tests: From fe88b2381b5d272437593dc3604fc834114e4a15 Mon Sep 17 00:00:00 2001 From: Brandon Chinn Date: Tue, 22 Apr 2025 23:39:02 -0700 Subject: [PATCH 047/156] build: Run pre-commit everywhere (#2808) Fix pre-commit issues. Would be nice to run `pre-commit run -a` in CI, but won't fix that now --------- Co-authored-by: Douglas Thor --- .bazelrc | 4 ++-- .pre-commit-config.yaml | 2 +- .../foo_external/py_binary_with_proto.py | 1 + .../wheel/lib/module_with_type_annotations.py | 1 + examples/wheel/test_publish.py | 2 +- examples/wheel/wheel_test.py | 17 +++++++++-------- .../dependency_resolution_order/__init__.py | 3 +-- .../py312_syntax/pep_695_type_parameter.py | 1 - .../dependency_resolver/dependency_resolver.py | 6 ++---- tests/integration/runner.py | 5 ++++- tests/no_unsafe_paths/test.py | 4 ++-- tools/wheelmaker.py | 12 ++++++++---- 12 files changed, 32 insertions(+), 26 deletions(-) diff --git a/.bazelrc b/.bazelrc index 4e6f2fa187..d2e0721526 100644 --- a/.bazelrc +++ b/.bazelrc @@ -4,8 +4,8 @@ # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it) # To update these lines, execute # `bazel run @rules_bazel_integration_test//tools:update_deleted_packages` -build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma -query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma +build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma +query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma test --test_output=errors diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b451e89fa..67a02fc6c0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: - --profile - black - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 25.1.0 hooks: - id: black - repo: local diff --git a/examples/bzlmod/py_proto_library/foo_external/py_binary_with_proto.py b/examples/bzlmod/py_proto_library/foo_external/py_binary_with_proto.py index be34264b5a..67e798bb8f 100644 --- a/examples/bzlmod/py_proto_library/foo_external/py_binary_with_proto.py +++ b/examples/bzlmod/py_proto_library/foo_external/py_binary_with_proto.py @@ -2,4 +2,5 @@ if __name__ == "__main__": import my_proto_pb2 + sys.exit(0) diff --git a/examples/wheel/lib/module_with_type_annotations.py b/examples/wheel/lib/module_with_type_annotations.py index 13e0895160..eda57bae6a 100644 --- a/examples/wheel/lib/module_with_type_annotations.py +++ b/examples/wheel/lib/module_with_type_annotations.py @@ -12,5 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. + def function(): return "qux" diff --git a/examples/wheel/test_publish.py b/examples/wheel/test_publish.py index 47134d11f3..e6ec80721b 100644 --- a/examples/wheel/test_publish.py +++ b/examples/wheel/test_publish.py @@ -104,7 +104,7 @@ def test_upload_and_query_simple_api(self):

Links for example-minimal-library

- example_minimal_library-0.0.1-py3-none-any.whl
+ example_minimal_library-0.0.1-py3-none-any.whl
""" self.assertEqual( diff --git a/examples/wheel/wheel_test.py b/examples/wheel/wheel_test.py index 9ec150301d..35803da742 100644 --- a/examples/wheel/wheel_test.py +++ b/examples/wheel/wheel_test.py @@ -85,7 +85,7 @@ def test_py_library_wheel(self): ], ) self.assertFileSha256Equal( - filename, "0cbf4ec574676015af595f570caf4ae2812f994f6338e247b002b4e496b6fbd5" + filename, "a73acae23590c7a8d4365c888c1f12f0399b7af27169ea99fc7a00f402833926" ) def test_py_package_wheel(self): @@ -110,7 +110,7 @@ def test_py_package_wheel(self): ], ) self.assertFileSha256Equal( - filename, "22aff90dd3c8c30c3ce2b729bb793cab0bd2668a6810de232677a0354ce79cae" + filename, "a76001500453dbd1d778821dcaba165d56db502c854cef9381dd3f8f89caee11" ) def test_customized_wheel(self): @@ -144,6 +144,7 @@ def test_customized_wheel(self): "example_customized-0.0.1.dist-info/entry_points.txt" ) + print(record_contents) self.assertEqual( record_contents, # The entries are guaranteed to be sorted. @@ -151,7 +152,7 @@ def test_customized_wheel(self): "examples/wheel/lib/data,with,commas.txt",sha256=9vJKEdfLu8bZRArKLroPZJh1XKkK3qFMXiM79MBL2Sg,12 examples/wheel/lib/data.txt,sha256=9vJKEdfLu8bZRArKLroPZJh1XKkK3qFMXiM79MBL2Sg,12 examples/wheel/lib/module_with_data.py,sha256=8s0Khhcqz3yVsBKv2IB5u4l4TMKh7-c_V6p65WVHPms,637 -examples/wheel/lib/module_with_type_annotations.py,sha256=MM2cFQsCBaUnzGiEGT5r07jhKSaCVRh5Paw_YLyrS-w,636 +examples/wheel/lib/module_with_type_annotations.py,sha256=2p_0YFT0TBUufbGCAR_u2vtxF1nM0lf3dX4VGeUtYq0,637 examples/wheel/lib/module_with_type_annotations.pyi,sha256=fja3ql_WRJ1qO8jyZjWWrTTMcg1J7EpOQivOHY_8vI4,630 examples/wheel/lib/simple_module.py,sha256=z2hwciab_XPNIBNH8B1Q5fYgnJvQTeYf0ZQJpY8yLLY,637 examples/wheel/main.py,sha256=mFiRfzQEDwCHr-WVNQhOH26M42bw1UMF6IoqvtuDTrw,1047 @@ -205,7 +206,7 @@ def test_customized_wheel(self): second = second.main:s""", ) self.assertFileSha256Equal( - filename, "657a938a6fdd6f38bf73d1d91016ffff85d68cf29ca390692a3e9d923dd0e39e" + filename, "941c0d79f4ca67cfa0028248bd0606db7fc69953ff9c7c73ac26a3e6d3c23587" ) def test_filename_escaping(self): @@ -277,7 +278,7 @@ def test_custom_package_root_wheel(self): for line in record_contents.splitlines(): self.assertFalse(line.startswith("/")) self.assertFileSha256Equal( - filename, "d415edbf8f326161674c1fa260e364dd44f2a0311e2f596284320ea52d2a8bdb" + filename, "7bd959b7efe9e325b30a6559177a1a4f22ac7a68fade310845916276110e9287" ) def test_custom_package_root_multi_prefix_wheel(self): @@ -311,7 +312,7 @@ def test_custom_package_root_multi_prefix_wheel(self): for line in record_contents.splitlines(): self.assertFalse(line.startswith("/")) self.assertFileSha256Equal( - filename, "6b76a1178c90996feaf3f9417f350c4a67f90f4247647fd4fd552858dc372d4b" + filename, "caf51e22bdcd3c6c766c8903319ce717daeb6caac577d14e16326a8597981854" ) def test_custom_package_root_multi_prefix_reverse_order_wheel(self): @@ -345,7 +346,7 @@ def test_custom_package_root_multi_prefix_reverse_order_wheel(self): for line in record_contents.splitlines(): self.assertFalse(line.startswith("/")) self.assertFileSha256Equal( - filename, "f976f0bb1c7d753e8c41629d6b79fb09908c6ecd2fec006816879fc86b664f3f" + filename, "9e8c0baa408b829dec691a5e8d3bc040be0bbfcc95c0eee19e1e5ffadea4a059" ) def test_python_requires_wheel(self): @@ -370,7 +371,7 @@ def test_python_requires_wheel(self): """, ) self.assertFileSha256Equal( - filename, "f3b74ce429c3324b87f8d1cc7dc33be1493f54bb88d546a7d53be7587b82c1a7" + filename, "b47f3eaf4f9fa4685a58c7415ba1feddd39635ae26c18473504f7d7e62e8ce07" ) def test_python_abi3_binary_wheel(self): diff --git a/gazelle/python/testdata/dependency_resolution_order/__init__.py b/gazelle/python/testdata/dependency_resolution_order/__init__.py index e2d0a8a979..4b40aa9f54 100644 --- a/gazelle/python/testdata/dependency_resolution_order/__init__.py +++ b/gazelle/python/testdata/dependency_resolution_order/__init__.py @@ -22,9 +22,8 @@ # we can still override "third_party.foo.bar" import third_party.foo.bar -from third_party import baz - import third_party +from third_party import baz _ = sys _ = bar diff --git a/gazelle/python/testdata/py312_syntax/pep_695_type_parameter.py b/gazelle/python/testdata/py312_syntax/pep_695_type_parameter.py index eff06de5a7..eb6263b334 100644 --- a/gazelle/python/testdata/py312_syntax/pep_695_type_parameter.py +++ b/gazelle/python/testdata/py312_syntax/pep_695_type_parameter.py @@ -17,6 +17,5 @@ def search_one_more_level[T]( import _other_module - if __name__ == "__main__": pass diff --git a/python/private/pypi/dependency_resolver/dependency_resolver.py b/python/private/pypi/dependency_resolver/dependency_resolver.py index 293377dc6d..89c9123a61 100644 --- a/python/private/pypi/dependency_resolver/dependency_resolver.py +++ b/python/private/pypi/dependency_resolver/dependency_resolver.py @@ -185,11 +185,9 @@ def main( # and we should copy the updated requirements back to the source tree. if not absolute_output_file.samefile(requirements_file_tree): atexit.register( - lambda: shutil.copy( - absolute_output_file, requirements_file_tree - ) + lambda: shutil.copy(absolute_output_file, requirements_file_tree) ) - cli(argv, standalone_mode = False) + cli(argv, standalone_mode=False) requirements_file_relative_path = Path(requirements_file_relative) content = requirements_file_relative_path.read_text() content = content.replace(absolute_path_prefix, "") diff --git a/tests/integration/runner.py b/tests/integration/runner.py index 9414a865c0..2534ab2d90 100644 --- a/tests/integration/runner.py +++ b/tests/integration/runner.py @@ -23,12 +23,15 @@ _logger = logging.getLogger(__name__) + class ExecuteError(Exception): def __init__(self, result): self.result = result + def __str__(self): return self.result.describe() + class ExecuteResult: def __init__( self, @@ -83,7 +86,7 @@ def setUp(self): "TMP": str(self.tmp_dir), # For some reason, this is necessary for Bazel 6.4 to work. # If not present, it can't find some bash helpers in @bazel_tools - "RUNFILES_DIR": os.environ["TEST_SRCDIR"] + "RUNFILES_DIR": os.environ["TEST_SRCDIR"], } def run_bazel(self, *args: str, check: bool = True) -> ExecuteResult: diff --git a/tests/no_unsafe_paths/test.py b/tests/no_unsafe_paths/test.py index 893add2f62..4727a02995 100644 --- a/tests/no_unsafe_paths/test.py +++ b/tests/no_unsafe_paths/test.py @@ -40,5 +40,5 @@ def test_no_unsafe_paths_in_search_path(self): self.assertEqual(os.path.basename(sys.path[0]), archive) -if __name__ == '__main__': - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() diff --git a/tools/wheelmaker.py b/tools/wheelmaker.py index 908b3fe956..28ec039741 100644 --- a/tools/wheelmaker.py +++ b/tools/wheelmaker.py @@ -217,9 +217,11 @@ def add_recordfile(self): filename = filename.lstrip("/") writer.writerow( ( - c - if isinstance(c, str) - else c.decode("utf-8", "surrogateescape") + ( + c + if isinstance(c, str) + else c.decode("utf-8", "surrogateescape") + ) for c in (filename, digest, size) ) ) @@ -604,7 +606,9 @@ def get_new_requirement_line(reqs_text, extra): # File is empty # So replace the meta_line entirely, including removing newline chars else: - metadata = re.sub(re.escape(meta_line) + r"(?:\r?\n)?", "", metadata, count=1) + metadata = re.sub( + re.escape(meta_line) + r"(?:\r?\n)?", "", metadata, count=1 + ) maker.add_metadata( metadata=metadata, From e32b08f2b01b972aed2e94def5c22512604ded93 Mon Sep 17 00:00:00 2001 From: Brandon Chinn Date: Wed, 23 Apr 2025 09:31:08 -0700 Subject: [PATCH 048/156] refactor/docs: improve compile_pip_requirements error message and docs (#2792) Resolution failure is the most common error from pip-compile, so we should make sure the error message is as clean as it can be. Previously, the output was cluttered with the exception traceback, which makes the actual error hard to see (several nested traceback). The new output shortens it with a nicer message: ``` Checking _main/requirements_lock.txt ERROR: Cannot install requests<2.24 and requests~=2.25.1 because these package versions have conflicting dependencies. ResolutionImpossible: for help visit https://pip.pypa.io/en/latest/topics/dependency-resolution/#dealing-with-dependency-conflicts ``` Fixes #2763 --------- Co-authored-by: Richard Levasseur --- docs/pypi-dependencies.md | 39 +++++- .../dependency_resolver.py | 111 +++++++++++------- python/private/pypi/pip_compile.bzl | 2 +- 3 files changed, 105 insertions(+), 47 deletions(-) diff --git a/docs/pypi-dependencies.md b/docs/pypi-dependencies.md index 6cc0da6cb4..4ec40bc889 100644 --- a/docs/pypi-dependencies.md +++ b/docs/pypi-dependencies.md @@ -5,8 +5,40 @@ Using PyPI packages (aka "pip install") involves two main steps. -1. [Installing third party packages](#installing-third-party-packages) -2. [Using third party packages as dependencies](#using-third-party-packages) +1. [Generating requirements file](#generating-requirements-file) +2. [Installing third party packages](#installing-third-party-packages) +3. [Using third party packages as dependencies](#using-third-party-packages) + +{#generating-requirements-file} +## Generating requirements file + +Generally, when working on a Python project, you'll have some dependencies that themselves have other dependencies. You might also specify dependency bounds instead of specific versions. So you'll need to generate a full list of all transitive dependencies and pinned versions for every dependency. + +Typically, you'd have your dependencies specified in `pyproject.toml` or `requirements.in` and generate the full pinned list of dependencies in `requirements_lock.txt`, which you can manage with the `compile_pip_requirements` Bazel rule: + +```starlark +load("@rules_python//python:pip.bzl", "compile_pip_requirements") + +compile_pip_requirements( + name = "requirements", + src = "requirements.in", + requirements_txt = "requirements_lock.txt", +) +``` + +This rule generates two targets: +- `bazel run [name].update` will regenerate the `requirements_txt` file +- `bazel test [name]_test` will test that the `requirements_txt` file is up to date + +For more documentation, see the API docs under {obj}`@rules_python//python:pip.bzl`. + +Once you generate this fully specified list of requirements, you can install the requirements with the instructions in [Installing third party packages](#installing-third-party-packages). + +:::{warning} +If you're specifying dependencies in `pyproject.toml`, make sure to include the `[build-system]` configuration, with pinned dependencies. `compile_pip_requirements` will use the build system specified to read your project's metadata, and you might see non-hermetic behavior if you don't pin the build system. + +Not specifying `[build-system]` at all will result in using a default `[build-system]` configuration, which uses unpinned versions ([ref](https://peps.python.org/pep-0518/#build-system-table)). +::: {#installing-third-party-packages} ## Installing third party packages @@ -27,8 +59,7 @@ pip.parse( ) use_repo(pip, "my_deps") ``` -For more documentation, including how the rules can update/create a requirements -file, see the bzlmod examples under the {gh-path}`examples` folder or the documentation +For more documentation, see the bzlmod examples under the {gh-path}`examples` folder or the documentation for the {obj}`@rules_python//python/extensions:pip.bzl` extension. ```{note} diff --git a/python/private/pypi/dependency_resolver/dependency_resolver.py b/python/private/pypi/dependency_resolver/dependency_resolver.py index 89c9123a61..ada0763558 100644 --- a/python/private/pypi/dependency_resolver/dependency_resolver.py +++ b/python/private/pypi/dependency_resolver/dependency_resolver.py @@ -15,14 +15,17 @@ "Set defaults for the pip-compile command to run it under Bazel" import atexit +import functools import os import shutil import sys from pathlib import Path -from typing import Optional, Tuple +from typing import List, Optional, Tuple import click import piptools.writer as piptools_writer +from pip._internal.exceptions import DistributionNotFound +from pip._vendor.resolvelib.resolvers import ResolutionImpossible from piptools.scripts.compile import cli from python.runfiles import runfiles @@ -82,7 +85,7 @@ def _locate(bazel_runfiles, file): @click.command(context_settings={"ignore_unknown_options": True}) @click.option("--src", "srcs", multiple=True, required=True) @click.argument("requirements_txt") -@click.argument("update_target_label") +@click.argument("target_label_prefix") @click.option("--requirements-linux") @click.option("--requirements-darwin") @click.option("--requirements-windows") @@ -90,7 +93,7 @@ def _locate(bazel_runfiles, file): def main( srcs: Tuple[str, ...], requirements_txt: str, - update_target_label: str, + target_label_prefix: str, requirements_linux: Optional[str], requirements_darwin: Optional[str], requirements_windows: Optional[str], @@ -152,9 +155,10 @@ def main( # or shutil.copyfile, as they will fail with OSError: [Errno 18] Invalid cross-device link. shutil.copy(resolved_requirements_file, requirements_out) - update_command = os.getenv("CUSTOM_COMPILE_COMMAND") or "bazel run %s" % ( - update_target_label, + update_command = ( + os.getenv("CUSTOM_COMPILE_COMMAND") or f"bazel run {target_label_prefix}.update" ) + test_command = f"bazel test {target_label_prefix}_test" os.environ["CUSTOM_COMPILE_COMMAND"] = update_command os.environ["PIP_CONFIG_FILE"] = os.getenv("PIP_CONFIG_FILE") or os.devnull @@ -168,6 +172,12 @@ def main( ) argv.extend(extra_args) + _run_pip_compile = functools.partial( + run_pip_compile, + argv, + srcs_relative=srcs_relative, + ) + if UPDATE: print("Updating " + requirements_file_relative) @@ -187,49 +197,66 @@ def main( atexit.register( lambda: shutil.copy(absolute_output_file, requirements_file_tree) ) - cli(argv, standalone_mode=False) + _run_pip_compile(verbose_command=f"{update_command} -- --verbose") requirements_file_relative_path = Path(requirements_file_relative) content = requirements_file_relative_path.read_text() content = content.replace(absolute_path_prefix, "") requirements_file_relative_path.write_text(content) else: - # cli will exit(0) on success - try: - print("Checking " + requirements_file) - cli(argv) - print("cli() should exit", file=sys.stderr) + print("Checking " + requirements_file) + sys.stdout.flush() + _run_pip_compile(verbose_command=f"{test_command} --test_arg=--verbose") + golden = open(_locate(bazel_runfiles, requirements_file)).readlines() + out = open(requirements_out).readlines() + out = [line.replace(absolute_path_prefix, "") for line in out] + if golden != out: + import difflib + + print("".join(difflib.unified_diff(golden, out)), file=sys.stderr) + print( + f"Lock file out of date. Run '{update_command}' to update.", + file=sys.stderr, + ) + sys.exit(1) + + +def run_pip_compile( + args: List[str], + *, + srcs_relative: List[str], + verbose_command: str, +) -> None: + try: + cli(args, standalone_mode=False) + except DistributionNotFound as e: + if isinstance(e.__cause__, ResolutionImpossible): + # pip logs an informative error to stderr already + # just render the error and exit + print(e) + sys.exit(1) + else: + raise + except SystemExit as e: + if e.code == 0: + return # shouldn't happen, but just in case + elif e.code == 2: + print( + "pip-compile exited with code 2. This means that pip-compile found " + "incompatible requirements or could not find a version that matches " + f"the install requirement in one of {srcs_relative}.\n" + "Try re-running with verbose:\n" + f" {verbose_command}", + file=sys.stderr, + ) + sys.exit(1) + else: + print( + f"pip-compile unexpectedly exited with code {e.code}.\n" + "Try re-running with verbose:\n" + f" {verbose_command}", + file=sys.stderr, + ) sys.exit(1) - except SystemExit as e: - if e.code == 2: - print( - "pip-compile exited with code 2. This means that pip-compile found " - "incompatible requirements or could not find a version that matches " - f"the install requirement in one of {srcs_relative}.", - file=sys.stderr, - ) - sys.exit(1) - elif e.code == 0: - golden = open(_locate(bazel_runfiles, requirements_file)).readlines() - out = open(requirements_out).readlines() - out = [line.replace(absolute_path_prefix, "") for line in out] - if golden != out: - import difflib - - print("".join(difflib.unified_diff(golden, out)), file=sys.stderr) - print( - "Lock file out of date. Run '" - + update_command - + "' to update.", - file=sys.stderr, - ) - sys.exit(1) - sys.exit(0) - else: - print( - f"pip-compile unexpectedly exited with code {e.code}.", - file=sys.stderr, - ) - sys.exit(1) if __name__ == "__main__": diff --git a/python/private/pypi/pip_compile.bzl b/python/private/pypi/pip_compile.bzl index 8e46947b99..7edbf7dc2c 100644 --- a/python/private/pypi/pip_compile.bzl +++ b/python/private/pypi/pip_compile.bzl @@ -110,7 +110,7 @@ def pip_compile( args = ["--src=%s" % loc.format(src) for src in srcs] + [ loc.format(requirements_txt), - "//%s:%s.update" % (native.package_name(), name), + "//%s:%s" % (native.package_name(), name), "--resolver=backtracking", "--allow-unsafe", ] From b7e58d1795d9f7858d3e1ba669cd84422fedc6f1 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Wed, 23 Apr 2025 13:59:11 -0700 Subject: [PATCH 049/156] feat: Have `pip_compile` generate a `*.test` target; deprecate `*_test` (#2812) Fixes #2794. The `pip_compile` macro generates `*_test` and `*.update` targets. This pattern does not match with other macros that generate similar targets, namely `gazelle_python_manifest` and uv `lock` (though that's `.run` instead of `.test` but either way, it uses a dot `.` instead of underscore `_`). Adjust the macro so that a `.test` target is made. The `_test` target is aliased with a deprecation warning, to be removed in the next major version. --- CHANGELOG.md | 3 +++ python/private/pypi/pip_compile.bzl | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f696cefde2..b1767664ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,9 @@ END_UNRELEASED_TEMPLATE * (rules) On Windows, {obj}`--bootstrap_impl=system_python` is forced. This allows setting `--bootstrap_impl=script` in bazelrc for mixed-platform environments. +* (rules) {obj}`pip_compile` now generates a `.test` target. The `_test` target is deprecated + and will be removed in the next major release. + ([#2794](https://github.com/bazel-contrib/rules_python/issues/2794) {#v0-0-0-fixed} ### Fixed diff --git a/python/private/pypi/pip_compile.bzl b/python/private/pypi/pip_compile.bzl index 7edbf7dc2c..e5b62c4ab0 100644 --- a/python/private/pypi/pip_compile.bzl +++ b/python/private/pypi/pip_compile.bzl @@ -47,7 +47,7 @@ def pip_compile( It also generates two targets for running pip-compile: - - validate with `bazel test [name]_test` + - validate with `bazel test [name].test` - update with `bazel run [name].update` If you are using a version control system, the requirements.txt generated by this rule should @@ -166,7 +166,7 @@ def pip_compile( timeout = kwargs.pop("timeout", "short") py_test( - name = name + "_test", + name = name + ".test", timeout = timeout, # setuptools (the default python build tool) attempts to find user # configuration in the user's home direcotory. This seems to work fine on @@ -180,3 +180,9 @@ def pip_compile( # kwargs could contain test-specific attributes like size **dict(attrs, **kwargs) ) + + native.alias( + name = "{}_test".format(name), + actual = ":{}.test".format(name), + deprecation = "Use '{}.test' instead. The '*_test' target will be removed in the next major release.".format(name), + ) From bb7b164fc1214b319a085222f5ce2a8ef41841c9 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Wed, 23 Apr 2025 16:36:30 -0700 Subject: [PATCH 050/156] fix: try multiple times to get win32 version to handle flakes (#2814) The Google tensorflow/jax devinfra team reported that Windows 2022 with Python 3.12.8 has a tendency to be flaky when calling the platform.win32 APIs. I'm very certain I saw similar behavior in the past myself. To fix, just call the APIs a couple times; it seems to fix itself. cc @vam-google --- CHANGELOG.md | 2 ++ python/private/python_bootstrap_template.txt | 10 +++++++++- python/private/stage2_bootstrap_template.py | 10 +++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1767664ef..8d11187cdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,8 @@ END_UNRELEASED_TEMPLATE * The `sys._base_executable` value will reflect the underlying interpreter, not venv interpreter. * The {obj}`//python/runtime_env_toolchains:all` toolchain now works with it. +* (rules) Better handle flakey platform.win32_ver() calls by calling them + multiple times. {#v0-0-0-added} ### Added diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt index eb5595f4a1..210987abf9 100644 --- a/python/private/python_bootstrap_template.txt +++ b/python/private/python_bootstrap_template.txt @@ -46,7 +46,15 @@ def GetWindowsPathWithUNCPrefix(path): # removed from common Win32 file and directory functions. # Related doc: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later import platform - if platform.win32_ver()[1] >= '10.0.14393': + win32_version = None + # Windows 2022 with Python 3.12.8 gives flakey errors, so try a couple times. + for _ in range(3): + try: + win32_version = platform.win32_ver()[1] + break + except (ValueError, KeyError): + pass + if win32_version and win32_version >= '10.0.14393': return path # import sysconfig only now to maintain python 2.6 compatibility diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py index fcc323e8ca..689602d3aa 100644 --- a/python/private/stage2_bootstrap_template.py +++ b/python/private/stage2_bootstrap_template.py @@ -58,7 +58,15 @@ def get_windows_path_with_unc_prefix(path): # Related doc: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later import platform - if platform.win32_ver()[1] >= "10.0.14393": + win32_version = None + # Windows 2022 with Python 3.12.8 gives flakey errors, so try a couple times. + for _ in range(3): + try: + win32_version = platform.win32_ver()[1] + break + except (ValueError, KeyError): + pass + if win32_version and win32_version >= '10.0.14393': return path # import sysconfig only now to maintain python 2.6 compatibility From 7164477cc97ea98a72ca3dc769ac63bc2c061de6 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Wed, 23 Apr 2025 23:24:56 -0700 Subject: [PATCH 051/156] refactor: Add log_std(out|err) bools to repo_utils that execute a subprocess (#2817) While making a local patch to work around #2640, I found that I had a need for running a subprocess (`gcloud auth print-access-token`) via `repo_utils.execute_checked_stdout`. However, doing so would log that access token when debug logging was enabled via `RULES_PYTHON_REPO_DEBUG=1`. This is a security concern for us, so I hacked in an option to allow a particular `execute_(un)checked(_stdout)` call to disable logging stdout, stderr, or both. I figure this might be useful to others so I thought I'd upstream it. `execute_(un)checked(_stdout)` now support `log_stdout` and `log_stderr` bools that default to `True` (which is the same behavior as before this PR. When the subprocess writes to stdout and `log_stdout = False`, the logged message will show: ``` ===== stdout start ===== ===== stdout end ===== ``` If the subprocess does not write to stdout, the debug log shows the same as before: ``` ``` The above also applies for stderr, with text adjusted accordingly. --- CHANGELOG.md | 4 +++- python/private/repo_utils.bzl | 31 ++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d11187cdf..88defb8e84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,7 +77,9 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-added} ### Added -* Nothing added. +* Repo utilities `execute_unchecked`, `execute_checked`, and `execute_checked_stdout` now + support `log_stdout` and `log_stderr` keyword arg booleans. When these are `True` + (the default), the subprocess's stdout/stderr will be logged. {#v0-0-0-removed} ### Removed diff --git a/python/private/repo_utils.bzl b/python/private/repo_utils.bzl index 73883a9244..eee56ec86c 100644 --- a/python/private/repo_utils.bzl +++ b/python/private/repo_utils.bzl @@ -98,6 +98,8 @@ def _execute_internal( arguments, environment = {}, logger = None, + log_stdout = True, + log_stderr = True, **kwargs): """Execute a subprocess with debugging instrumentation. @@ -116,6 +118,10 @@ def _execute_internal( logger: optional `Logger` to use for logging execution details. Must be specified when using module_ctx. If not specified, a default will be created. + log_stdout: If True (the default), write stdout to the logged message. Setting + to False can be useful for large stdout messages or for secrets. + log_stderr: If True (the default), write stderr to the logged message. Setting + to False can be useful for large stderr messages or for secrets. **kwargs: additional kwargs to pass onto rctx.execute Returns: @@ -160,7 +166,7 @@ def _execute_internal( cwd = _cwd_to_str(mrctx, kwargs), timeout = _timeout_to_str(kwargs), env_str = _env_to_str(environment), - output = _outputs_to_str(result), + output = _outputs_to_str(result, log_stdout = log_stdout, log_stderr = log_stderr), )) elif _is_repo_debug_enabled(mrctx): logger.debug(( @@ -171,7 +177,7 @@ def _execute_internal( op = op, status = "success" if result.return_code == 0 else "failure", return_code = result.return_code, - output = _outputs_to_str(result), + output = _outputs_to_str(result, log_stdout = log_stdout, log_stderr = log_stderr), )) result_kwargs = {k: getattr(result, k) for k in dir(result)} @@ -183,6 +189,8 @@ def _execute_internal( mrctx = mrctx, kwargs = kwargs, environment = environment, + log_stdout = log_stdout, + log_stderr = log_stderr, ), **result_kwargs ) @@ -220,7 +228,16 @@ def _execute_checked_stdout(*args, **kwargs): """Calls execute_checked, but only returns the stdout value.""" return _execute_checked(*args, **kwargs).stdout -def _execute_describe_failure(*, op, arguments, result, mrctx, kwargs, environment): +def _execute_describe_failure( + *, + op, + arguments, + result, + mrctx, + kwargs, + environment, + log_stdout = True, + log_stderr = True): return ( "repo.execute: {op}: failure:\n" + " command: {cmd}\n" + @@ -236,7 +253,7 @@ def _execute_describe_failure(*, op, arguments, result, mrctx, kwargs, environme cwd = _cwd_to_str(mrctx, kwargs), timeout = _timeout_to_str(kwargs), env_str = _env_to_str(environment), - output = _outputs_to_str(result), + output = _outputs_to_str(result, log_stdout = log_stdout, log_stderr = log_stderr), ) def _which_checked(mrctx, binary_name): @@ -331,11 +348,11 @@ def _env_to_str(environment): def _timeout_to_str(kwargs): return kwargs.get("timeout", "") -def _outputs_to_str(result): +def _outputs_to_str(result, log_stdout = True, log_stderr = True): lines = [] items = [ - ("stdout", result.stdout), - ("stderr", result.stderr), + ("stdout", result.stdout if log_stdout else ""), + ("stderr", result.stderr if log_stderr else ""), ] for name, content in items: if content: From 1e21dbdbba45a3fa7a3bcb2495d72f89eae1fb98 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Thu, 24 Apr 2025 22:05:45 +0900 Subject: [PATCH 052/156] fix: use the python micro version to parse whl metadata in bzlmod (#2793) Add `` version to the target platform. Instead of `cpxy_os_cpu` the target platform string format becomes `cpxy.z_os_cpu`. This is a temporary measure until we get a better API for defining target platforms. Summary: - [x] test `select_whls` function needs to be tested to ensure that the whl selection is not impacted when we have the full version in the target platform. - [ ] `download_only` legacy whl code path in `bzlmod` needs further testing. - [x] test `whl_config_setting` handling and config setting creation. The config settings in the hub repo should not use the full version, because from the outside, the whl is compatible with all `micro` versions of a given `3.` of the Python interpreter. This means that the already documented config setting do not need to be changed. - [x] `pep508_deps` tests for handling the `full_python_version` correctly. - [x] `pep508_deps` tests for ensuring the `default_abi` is being handled correctly. Fixes #2319 --- .bazelrc | 4 +- CHANGELOG.md | 3 ++ examples/bzlmod/entry_points/BUILD.bazel | 8 +-- python/private/pypi/BUILD.bazel | 3 ++ python/private/pypi/config_settings.bzl | 2 + python/private/pypi/extension.bzl | 14 ++++-- python/private/pypi/pep508_deps.bzl | 27 ++++++++-- python/private/pypi/pkg_aliases.bzl | 3 ++ python/private/pypi/render_pkg_aliases.bzl | 14 +++++- .../pypi/requirements_files_by_platform.bzl | 7 ++- python/private/pypi/whl_config_setting.bzl | 12 ++++- python/private/pypi/whl_library_targets.bzl | 16 +++--- python/private/pypi/whl_target_platforms.bzl | 5 +- tests/pypi/extension/extension_tests.bzl | 12 +++-- tests/pypi/pep508/deps_tests.bzl | 49 +++++++++++++------ .../render_pkg_aliases_test.bzl | 9 ++-- .../whl_library_targets_tests.bzl | 30 +++++------- .../whl_target_platforms/select_whl_tests.bzl | 16 ++++++ 18 files changed, 160 insertions(+), 74 deletions(-) diff --git a/.bazelrc b/.bazelrc index d2e0721526..4e6f2fa187 100644 --- a/.bazelrc +++ b/.bazelrc @@ -4,8 +4,8 @@ # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it) # To update these lines, execute # `bazel run @rules_bazel_integration_test//tools:update_deleted_packages` -build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma -query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma +build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma +query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma test --test_output=errors diff --git a/CHANGELOG.md b/CHANGELOG.md index 88defb8e84..984af8bad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -148,6 +148,9 @@ END_UNRELEASED_TEMPLATE * (packaging) An empty `requires_file` is treated as if it were omitted, resulting in a valid `METADATA` file. * (rules) py_wheel and sphinxdocs rules now propagate `target_compatible_with` to all targets they create. [PR #2788](https://github.com/bazel-contrib/rules_python/pull/2788). +* (pypi) Correctly handle `METADATA` entries when `python_full_version` is used in + the environment marker. + Fixes [#2319](https://github.com/bazel-contrib/rules_python/issues/2319). {#1-4-0-added} ### Added diff --git a/examples/bzlmod/entry_points/BUILD.bazel b/examples/bzlmod/entry_points/BUILD.bazel index a0939cb65b..4ca5b53568 100644 --- a/examples/bzlmod/entry_points/BUILD.bazel +++ b/examples/bzlmod/entry_points/BUILD.bazel @@ -1,4 +1,3 @@ -load("@python_versions//3.9:defs.bzl", py_console_script_binary_3_9 = "py_console_script_binary") load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") # This is how you can define a `pylint` entrypoint which uses the default python version. @@ -24,10 +23,11 @@ py_console_script_binary( ], ) -# A specific Python version can be forced by using the generated version-aware -# wrappers, e.g. to force Python 3.9: -py_console_script_binary_3_9( +# A specific Python version can be forced by passing `python_version` +# attribute, e.g. to force Python 3.9: +py_console_script_binary( name = "yamllint", pkg = "@pip//yamllint:pkg", + python_version = "3.9", visibility = ["//entry_points:__subpackages__"], ) diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index a758b3f153..bfb0be2d59 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -103,6 +103,7 @@ bzl_library( "//python/private:version_label_bzl", "@bazel_features//:features", "@pythons_hub//:interpreters_bzl", + "@pythons_hub//:versions_bzl", ], ) @@ -220,7 +221,9 @@ bzl_library( ":pep508_evaluate_bzl", ":pep508_platform_bzl", ":pep508_requirement_bzl", + "//python/private:full_version_bzl", "//python/private:normalize_name_bzl", + "@pythons_hub//:versions_bzl", ], ) diff --git a/python/private/pypi/config_settings.bzl b/python/private/pypi/config_settings.bzl index 1045ffef35..d1b85d16c1 100644 --- a/python/private/pypi/config_settings.bzl +++ b/python/private/pypi/config_settings.bzl @@ -42,6 +42,8 @@ specialized is as follows: * `:is_cp3_abi3_` * `:is_cp3_cp3_` and `:is_cp3_cp3t_` +Optionally instead of `` there sometimes may be `.` used in order to fully specify the versions + The specialization of free-threaded vs non-free-threaded wheels is the same as they are just variants of each other. The same goes for the specialization of `musllinux` vs `manylinux`. diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index d1895ca211..e9eba684f8 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -16,7 +16,9 @@ load("@bazel_features//:features.bzl", "bazel_features") load("@pythons_hub//:interpreters.bzl", "INTERPRETER_LABELS") +load("@pythons_hub//:versions.bzl", "MINOR_MAPPING") load("//python/private:auth.bzl", "AUTH_ATTRS") +load("//python/private:full_version.bzl", "full_version") load("//python/private:normalize_name.bzl", "normalize_name") load("//python/private:repo_utils.bzl", "repo_utils") load("//python/private:semver.bzl", "semver") @@ -68,6 +70,7 @@ def _create_whl_repos( pip_attr, whl_overrides, available_interpreters = INTERPRETER_LABELS, + minor_mapping = MINOR_MAPPING, get_index_urls = None): """create all of the whl repositories @@ -80,6 +83,8 @@ def _create_whl_repos( interpreters that have been registered using the `python` bzlmod extension. The keys are in the form `python_{snake_case_version}_host`. This is to be used during the `repository_rule` and must be always compatible with the host. + minor_mapping: {type}`dict[str, str]` The dictionary needed to resolve the full + python version used to parse package METADATA files. Returns a {type}`struct` with the following attributes: whl_map: {type}`dict[str, list[struct]]` the output is keyed by the @@ -159,8 +164,10 @@ def _create_whl_repos( requirements_osx = pip_attr.requirements_darwin, requirements_windows = pip_attr.requirements_windows, extra_pip_args = pip_attr.extra_pip_args, - # TODO @aignas 2025-04-15: pass the full version into here - python_version = major_minor, + python_version = full_version( + version = pip_attr.python_version, + minor_mapping = minor_mapping, + ), logger = logger, ), extra_pip_args = pip_attr.extra_pip_args, @@ -304,9 +311,6 @@ def _whl_repos(*, requirement, whl_library_args, download_only, netrc, auth_patt if requirement.extra_pip_args: args["extra_pip_args"] = requirement.extra_pip_args - if download_only: - args.setdefault("experimental_target_platforms", requirement.target_platforms) - target_platforms = requirement.target_platforms if multiple_requirements_for_whl else [] repo_name = pypi_repo_name( normalize_name(requirement.distribution), diff --git a/python/private/pypi/pep508_deps.bzl b/python/private/pypi/pep508_deps.bzl index 115bbd78d8..bcc4845cf1 100644 --- a/python/private/pypi/pep508_deps.bzl +++ b/python/private/pypi/pep508_deps.bzl @@ -15,14 +15,23 @@ """This module is for implementing PEP508 compliant METADATA deps parsing. """ -load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION") +load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING") +load("//python/private:full_version.bzl", "full_version") load("//python/private:normalize_name.bzl", "normalize_name") load(":pep508_env.bzl", "env") load(":pep508_evaluate.bzl", "evaluate") load(":pep508_platform.bzl", "platform", "platform_from_str") load(":pep508_requirement.bzl", "requirement") -def deps(name, *, requires_dist, platforms = [], extras = [], excludes = [], default_python_version = None): +def deps( + name, + *, + requires_dist, + platforms = [], + extras = [], + excludes = [], + default_python_version = None, + minor_mapping = MINOR_MAPPING): """Parse the RequiresDist from wheel METADATA Args: @@ -33,6 +42,9 @@ def deps(name, *, requires_dist, platforms = [], extras = [], excludes = [], def extras: {type}`list[str]` the requested extras to generate targets for. platforms: {type}`list[str]` the list of target platform strings. default_python_version: {type}`str` the host python version. + minor_mapping: {type}`type[str, str]` the minor mapping to use when + resolving to the full python version as DEFAULT_PYTHON_VERSION can by + of format `3.x`. Returns: A struct with attributes: @@ -53,6 +65,12 @@ def deps(name, *, requires_dist, platforms = [], extras = [], excludes = [], def excludes = [name] + [normalize_name(x) for x in excludes] default_python_version = default_python_version or DEFAULT_PYTHON_VERSION + if default_python_version: + # if it is not bzlmod, then DEFAULT_PYTHON_VERSION may be unset + default_python_version = full_version( + version = default_python_version, + minor_mapping = minor_mapping, + ) platforms = [ platform_from_str(p, python_version = default_python_version) for p in platforms @@ -60,9 +78,8 @@ def deps(name, *, requires_dist, platforms = [], extras = [], excludes = [], def abis = sorted({p.abi: True for p in platforms if p.abi}) if default_python_version and len(abis) > 1: - _, _, minor_version = default_python_version.partition(".") - minor_version, _, _ = minor_version.partition(".") - default_abi = "cp3" + minor_version + _, _, tail = default_python_version.partition(".") + default_abi = "cp3" + tail elif len(abis) > 1: fail( "all python versions need to be specified explicitly, got: {}".format(platforms), diff --git a/python/private/pypi/pkg_aliases.bzl b/python/private/pypi/pkg_aliases.bzl index a9eee7be88..28d70ff715 100644 --- a/python/private/pypi/pkg_aliases.bzl +++ b/python/private/pypi/pkg_aliases.bzl @@ -371,6 +371,9 @@ def get_filename_config_settings( abi = parsed.abi_tag + # TODO @aignas 2025-04-20: test + abi, _, _ = abi.partition(".") + if parsed.platform_tag == "any": prefixes = ["{}{}_any".format(py, abi)] else: diff --git a/python/private/pypi/render_pkg_aliases.bzl b/python/private/pypi/render_pkg_aliases.bzl index 863d25095c..28f32edc78 100644 --- a/python/private/pypi/render_pkg_aliases.bzl +++ b/python/private/pypi/render_pkg_aliases.bzl @@ -143,6 +143,18 @@ def render_pkg_aliases(*, aliases, requirement_cycles = None, extra_hub_aliases files["_groups/BUILD.bazel"] = generate_group_library_build_bazel("", requirement_cycles) return files +def _major_minor(python_version): + major, _, tail = python_version.partition(".") + minor, _, _ = tail.partition(".") + return "{}.{}".format(major, minor) + +def _major_minor_versions(python_versions): + if not python_versions: + return [] + + # Use a dict as a simple set + return sorted({_major_minor(v): None for v in python_versions}) + def render_multiplatform_pkg_aliases(*, aliases, **kwargs): """Render the multi-platform pkg aliases. @@ -174,7 +186,7 @@ def render_multiplatform_pkg_aliases(*, aliases, **kwargs): glibc_versions = flag_versions.get("glibc_versions", []), muslc_versions = flag_versions.get("muslc_versions", []), osx_versions = flag_versions.get("osx_versions", []), - python_versions = flag_versions.get("python_versions", []), + python_versions = _major_minor_versions(flag_versions.get("python_versions", [])), target_platforms = flag_versions.get("target_platforms", []), visibility = ["//:__subpackages__"], ) diff --git a/python/private/pypi/requirements_files_by_platform.bzl b/python/private/pypi/requirements_files_by_platform.bzl index e3aafc083f..9165c05bed 100644 --- a/python/private/pypi/requirements_files_by_platform.bzl +++ b/python/private/pypi/requirements_files_by_platform.bzl @@ -91,13 +91,12 @@ def _platforms_from_args(extra_pip_args): return list(platforms.keys()) def _platform(platform_string, python_version = None): - if not python_version or platform_string.startswith("cp3"): + if not python_version or platform_string.startswith("cp"): return platform_string - _, _, tail = python_version.partition(".") - minor, _, _ = tail.partition(".") + major, _, tail = python_version.partition(".") - return "cp3{}_{}".format(minor, platform_string) + return "cp{}{}_{}".format(major, tail, platform_string) def requirements_files_by_platform( *, diff --git a/python/private/pypi/whl_config_setting.bzl b/python/private/pypi/whl_config_setting.bzl index d966206372..6e10eb4d27 100644 --- a/python/private/pypi/whl_config_setting.bzl +++ b/python/private/pypi/whl_config_setting.bzl @@ -35,10 +35,20 @@ def whl_config_setting(*, version = None, config_setting = None, filename = None a struct with the validated and parsed values. """ if target_platforms: - for p in target_platforms: + target_platforms_input = target_platforms + target_platforms = [] + for p in target_platforms_input: if not p.startswith("cp"): fail("target_platform should start with 'cp' denoting the python version, got: " + p) + abi, _, tail = p.partition("_") + + # drop the micro version here, currently there is no usecase to use + # multiple python interpreters with the same minor version but + # different micro version. + abi, _, _ = abi.partition(".") + target_platforms.append("{}_{}".format(abi, tail)) + return struct( config_setting = config_setting, filename = filename, diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl index cf3df133c4..21e4a54a3a 100644 --- a/python/private/pypi/whl_library_targets.bzl +++ b/python/private/pypi/whl_library_targets.bzl @@ -369,26 +369,22 @@ def _config_settings(dependencies_by_platform, native = native, **kwargs): if p.startswith("@") or p.endswith("default"): continue + # TODO @aignas 2025-04-20: add tests here abi, _, tail = p.partition("_") if not abi.startswith("cp"): tail = p abi = "" - os, _, arch = tail.partition("_") - os = "" if os == "anyos" else os - arch = "" if arch == "anyarch" else arch _kwargs = dict(kwargs) - if arch: - _kwargs.setdefault("constraint_values", []).append("@platforms//cpu:{}".format(arch)) - if os: - _kwargs.setdefault("constraint_values", []).append("@platforms//os:{}".format(os)) + _kwargs["constraint_values"] = [ + "@platforms//cpu:{}".format(arch), + "@platforms//os:{}".format(os), + ] if abi: _kwargs["flag_values"] = { - "@rules_python//python/config_settings:python_version_major_minor": "3.{minor_version}".format( - minor_version = abi[len("cp3"):], - ), + Label("//python/config_settings:python_version"): "3.{}".format(abi[len("cp3"):]), } native.config_setting( diff --git a/python/private/pypi/whl_target_platforms.bzl b/python/private/pypi/whl_target_platforms.bzl index 9f47e625b3..6ea3f120c3 100644 --- a/python/private/pypi/whl_target_platforms.bzl +++ b/python/private/pypi/whl_target_platforms.bzl @@ -75,8 +75,11 @@ def select_whls(*, whls, want_platforms = [], logger = None): fail("expected all platforms to start with ABI, but got: {}".format(p)) abi, _, os_cpu = p.partition("_") + abi, _, _ = abi.partition(".") _want_platforms[os_cpu] = None - _want_platforms[p] = None + + # TODO @aignas 2025-04-20: add a test + _want_platforms["{}_{}".format(abi, os_cpu)] = None version_limit_candidate = int(abi[3:]) if not version_limit: diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index ce5474e35b..5de3bb58d3 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -157,6 +157,7 @@ def _test_simple(env): available_interpreters = { "python_3_15_host": "unit_test_interpreter_target", }, + minor_mapping = {"3.15": "3.15.19"}, ) pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) @@ -204,6 +205,7 @@ def _test_simple_multiple_requirements(env): available_interpreters = { "python_3_15_host": "unit_test_interpreter_target", }, + minor_mapping = {"3.15": "3.15.19"}, ) pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) @@ -270,6 +272,7 @@ torch==2.4.1 ; platform_machine != 'x86_64' \ available_interpreters = { "python_3_15_host": "unit_test_interpreter_target", }, + minor_mapping = {"3.15": "3.15.19"}, ) pypi.exposed_packages().contains_exactly({"pypi": ["torch"]}) @@ -392,6 +395,7 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ available_interpreters = { "python_3_12_host": "unit_test_interpreter_target", }, + minor_mapping = {"3.12": "3.12.19"}, simpleapi_download = mocksimpleapi_download, ) @@ -515,6 +519,7 @@ simple==0.0.3 \ available_interpreters = { "python_3_15_host": "unit_test_interpreter_target", }, + minor_mapping = {"3.15": "3.15.19"}, ) pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) @@ -544,7 +549,8 @@ simple==0.0.3 \ "pypi_315_extra": { "dep_template": "@pypi//{name}:{target}", "download_only": True, - "experimental_target_platforms": ["cp315_linux_x86_64"], + # TODO @aignas 2025-04-20: ensure that this is in the hub repo + # "experimental_target_platforms": ["cp315_linux_x86_64"], "extra_pip_args": ["--platform=manylinux_2_17_x86_64", "--python-version=315", "--implementation=cp", "--abi=cp315"], "python_interpreter_target": "unit_test_interpreter_target", "requirement": "extra==0.0.1 --hash=sha256:deadb00f", @@ -552,7 +558,6 @@ simple==0.0.3 \ "pypi_315_simple_linux_x86_64": { "dep_template": "@pypi//{name}:{target}", "download_only": True, - "experimental_target_platforms": ["cp315_linux_x86_64"], "extra_pip_args": ["--platform=manylinux_2_17_x86_64", "--python-version=315", "--implementation=cp", "--abi=cp315"], "python_interpreter_target": "unit_test_interpreter_target", "requirement": "simple==0.0.1 --hash=sha256:deadbeef", @@ -560,7 +565,6 @@ simple==0.0.3 \ "pypi_315_simple_osx_aarch64": { "dep_template": "@pypi//{name}:{target}", "download_only": True, - "experimental_target_platforms": ["cp315_osx_aarch64"], "extra_pip_args": ["--platform=macosx_10_9_arm64", "--python-version=315", "--implementation=cp", "--abi=cp315"], "python_interpreter_target": "unit_test_interpreter_target", "requirement": "simple==0.0.3 --hash=sha256:deadbaaf", @@ -648,6 +652,7 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef available_interpreters = { "python_3_15_host": "unit_test_interpreter_target", }, + minor_mapping = {"3.15": "3.15.19"}, simpleapi_download = mocksimpleapi_download, ) @@ -850,6 +855,7 @@ optimum[onnxruntime-gpu]==1.17.1 ; sys_platform == 'linux' available_interpreters = { "python_3_15_host": "unit_test_interpreter_target", }, + minor_mapping = {"3.15": "3.15.19"}, ) pypi.exposed_packages().contains_exactly({"pypi": []}) diff --git a/tests/pypi/pep508/deps_tests.bzl b/tests/pypi/pep508/deps_tests.bzl index d362925080..118cd50092 100644 --- a/tests/pypi/pep508/deps_tests.bzl +++ b/tests/pypi/pep508/deps_tests.bzl @@ -48,6 +48,15 @@ def test_can_add_os_specific_deps(env): ], python_version = "", ), + struct( + platforms = [ + "cp33.1_linux_x86_64", + "cp33.1_osx_x86_64", + "cp33.1_osx_aarch64", + "cp33.1_windows_x86_64", + ], + python_version = "", + ), ]: got = deps( "foo", @@ -154,7 +163,7 @@ _tests.append(test_self_dependencies_can_come_in_any_order) def _test_can_get_deps_based_on_specific_python_version(env): requires_dist = [ "bar", - "baz; python_version < '3.8'", + "baz; python_full_version < '3.7.3'", "posix_dep; os_name=='posix' and python_version >= '3.8'", ] @@ -163,6 +172,11 @@ def _test_can_get_deps_based_on_specific_python_version(env): requires_dist = requires_dist, platforms = ["cp38_linux_x86_64"], ) + py373 = deps( + "foo", + requires_dist = requires_dist, + platforms = ["cp37.3_linux_x86_64"], + ) py37 = deps( "foo", requires_dist = requires_dist, @@ -174,6 +188,8 @@ def _test_can_get_deps_based_on_specific_python_version(env): env.expect.that_dict(py37.deps_select).contains_exactly({}) env.expect.that_collection(py38.deps).contains_exactly(["bar", "posix_dep"]) env.expect.that_dict(py38.deps_select).contains_exactly({}) + env.expect.that_collection(py373.deps).contains_exactly(["bar"]) + env.expect.that_dict(py373.deps_select).contains_exactly({}) _tests.append(_test_can_get_deps_based_on_specific_python_version) @@ -210,27 +226,29 @@ def _test_can_get_version_select(env): "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", "arch_dep; platform_machine=='x86_64' and python_version < '3.8'", ] - default_python_version = "3.7.4" got = deps( "foo", requires_dist = requires_dist, platforms = [ "cp3{}_{}_x86_64".format(minor, os) - for minor in [7, 8, 9] + for minor in ["7.4", "8.8", "9.8"] for os in ["linux", "windows"] ], - default_python_version = default_python_version, + default_python_version = "3.7", + minor_mapping = { + "3.7": "3.7.4", + }, ) env.expect.that_collection(got.deps).contains_exactly(["bar"]) env.expect.that_dict(got.deps_select).contains_exactly({ - "cp37_linux_x86_64": ["arch_dep", "baz", "posix_dep"], - "cp37_windows_x86_64": ["arch_dep", "baz"], - "cp38_linux_x86_64": ["baz_new", "posix_dep", "posix_dep_with_version"], - "cp38_windows_x86_64": ["baz_new"], - "cp39_linux_x86_64": ["baz_new", "posix_dep", "posix_dep_with_version"], - "cp39_windows_x86_64": ["baz_new"], + "cp37.4_linux_x86_64": ["arch_dep", "baz", "posix_dep"], + "cp37.4_windows_x86_64": ["arch_dep", "baz"], + "cp38.8_linux_x86_64": ["baz_new", "posix_dep", "posix_dep_with_version"], + "cp38.8_windows_x86_64": ["baz_new"], + "cp39.8_linux_x86_64": ["baz_new", "posix_dep", "posix_dep_with_version"], + "cp39.8_windows_x86_64": ["baz_new"], "linux_x86_64": ["arch_dep", "baz", "posix_dep"], "windows_x86_64": ["arch_dep", "baz"], }) @@ -294,8 +312,6 @@ def _test_deps_are_not_duplicated(env): _tests.append(_test_deps_are_not_duplicated) def _test_deps_are_not_duplicated_when_encountering_platform_dep_first(env): - default_python_version = "3.7.1" - # Note, that we are sorting the incoming `requires_dist` and we need to ensure that we are not getting any # issues even if the platform-specific line comes first. requires_dist = [ @@ -307,19 +323,20 @@ def _test_deps_are_not_duplicated_when_encountering_platform_dep_first(env): "foo", requires_dist = requires_dist, platforms = [ - "cp37_linux_aarch64", - "cp37_linux_x86_64", + "cp37.1_linux_aarch64", + "cp37.1_linux_x86_64", "cp310_linux_aarch64", "cp310_linux_x86_64", ], - default_python_version = default_python_version, + default_python_version = "3.7.1", + minor_mapping = {}, ) env.expect.that_collection(got.deps).contains_exactly([]) env.expect.that_dict(got.deps_select).contains_exactly({ "cp310_linux_aarch64": ["bar"], "cp310_linux_x86_64": ["bar"], - "cp37_linux_aarch64": ["bar"], + "cp37.1_linux_aarch64": ["bar"], "linux_aarch64": ["bar"], }) diff --git a/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl b/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl index c60761bed7..416d50bd80 100644 --- a/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl +++ b/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl @@ -68,7 +68,8 @@ def _test_bzlmod_aliases(env): aliases = { "bar-baz": { whl_config_setting( - version = "3.2", + # Add one with micro version to mimic construction in the extension + version = "3.2.2", config_setting = "//:my_config_setting", ): "pypi_32_bar_baz", whl_config_setting( @@ -83,10 +84,10 @@ def _test_bzlmod_aliases(env): filename = "foo-0.0.0-py3-none-any.whl", ): "filename_repo", whl_config_setting( - version = "3.2", + version = "3.2.2", filename = "foo-0.0.0-py3-none-any.whl", target_platforms = [ - "cp32_linux_x86_64", + "cp32.2_linux_x86_64", ], ): "filename_repo_linux_x86_64", }, @@ -117,7 +118,7 @@ pkg_aliases( whl_config_setting( filename = "foo-0.0.0-py3-none-any.whl", target_platforms = ("cp32_linux_x86_64",), - version = "3.2", + version = "3.2.2", ): "filename_repo_linux_x86_64", }, extra_aliases = ["foo"], diff --git a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl index 61e5441050..432cdbfa1b 100644 --- a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl +++ b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl @@ -68,9 +68,8 @@ def _test_platforms(env): "@//python/config_settings:is_python_3.9": ["py39_dep"], "@platforms//cpu:aarch64": ["arm_dep"], "@platforms//os:windows": ["win_dep"], + "cp310.11_linux_ppc64le": ["full_version_dep"], "cp310_linux_ppc64le": ["py310_linux_ppc64le_dep"], - "cp39_anyos_aarch64": ["py39_arm_dep"], - "cp39_linux_anyarch": ["py39_linux_dep"], "linux_x86_64": ["linux_intel_dep"], }, filegroups = {}, @@ -82,39 +81,34 @@ def _test_platforms(env): env.expect.that_collection(calls).contains_exactly([ { - "name": "is_python_3.10_linux_ppc64le", - "flag_values": { - "@rules_python//python/config_settings:python_version_major_minor": "3.10", - }, + "name": "is_python_3.10.11_linux_ppc64le", + "visibility": ["//visibility:private"], "constraint_values": [ "@platforms//cpu:ppc64le", "@platforms//os:linux", ], - "visibility": ["//visibility:private"], - }, - { - "name": "is_python_3.9_anyos_aarch64", "flag_values": { - "@rules_python//python/config_settings:python_version_major_minor": "3.9", + Label("//python/config_settings:python_version"): "3.10.11", }, - "constraint_values": ["@platforms//cpu:aarch64"], - "visibility": ["//visibility:private"], }, { - "name": "is_python_3.9_linux_anyarch", + "name": "is_python_3.10_linux_ppc64le", + "visibility": ["//visibility:private"], + "constraint_values": [ + "@platforms//cpu:ppc64le", + "@platforms//os:linux", + ], "flag_values": { - "@rules_python//python/config_settings:python_version_major_minor": "3.9", + Label("//python/config_settings:python_version"): "3.10", }, - "constraint_values": ["@platforms//os:linux"], - "visibility": ["//visibility:private"], }, { "name": "is_linux_x86_64", + "visibility": ["//visibility:private"], "constraint_values": [ "@platforms//cpu:x86_64", "@platforms//os:linux", ], - "visibility": ["//visibility:private"], }, ]) # buildifier: @unsorted-dict-items diff --git a/tests/pypi/whl_target_platforms/select_whl_tests.bzl b/tests/pypi/whl_target_platforms/select_whl_tests.bzl index 8ab24138d1..1674ac5ef2 100644 --- a/tests/pypi/whl_target_platforms/select_whl_tests.bzl +++ b/tests/pypi/whl_target_platforms/select_whl_tests.bzl @@ -289,6 +289,22 @@ def _test_freethreaded_wheels(env): _tests.append(_test_freethreaded_wheels) +def _test_micro_version_freethreaded(env): + # Check we prefer platform specific wheels + got = _select_whls(whls = WHL_LIST, want_platforms = ["cp313.3_linux_x86_64"]) + _match( + env, + got, + "pkg-0.0.1-cp313-cp313t-musllinux_1_1_x86_64.whl", + "pkg-0.0.1-cp313-cp313-musllinux_1_1_x86_64.whl", + "pkg-0.0.1-cp313-abi3-musllinux_1_1_x86_64.whl", + "pkg-0.0.1-cp313-none-musllinux_1_1_x86_64.whl", + "pkg-0.0.1-cp39-abi3-any.whl", + "pkg-0.0.1-py3-none-any.whl", + ) + +_tests.append(_test_micro_version_freethreaded) + def select_whl_test_suite(name): """Create the test suite. From ee3440986f422c6a02d52d594816e571d0c633d8 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 25 Apr 2025 03:37:31 +0900 Subject: [PATCH 053/156] fix(pypi): call python --version before marker eval (#2819) `bzlmod` has the full python version information statically and we don't need to call Python to get its version, but for `WORKSPACE` that is not the case and we have to call it before evaluating the markers in universal requirements files. This also fixes transitions in the `compile_pip_requirements` macro where the `.update` target would not transition correctly based on the `python_version` parameter. Fixes #2818 --- CHANGELOG.md | 4 +++ .../requirements/requirements.in | 2 +- .../requirements/requirements_lock_3_10.txt | 2 +- .../requirements/requirements_lock_3_11.txt | 2 +- .../requirements/requirements_lock_3_9.txt | 2 +- python/private/pypi/BUILD.bazel | 1 + python/private/pypi/evaluate_markers.bzl | 7 +++--- python/private/pypi/pip_compile.bzl | 1 + python/private/pypi/pip_repository.bzl | 25 +++++++++++++++++-- 9 files changed, 37 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 984af8bad2..8fc00ca25f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -151,6 +151,10 @@ END_UNRELEASED_TEMPLATE * (pypi) Correctly handle `METADATA` entries when `python_full_version` is used in the environment marker. Fixes [#2319](https://github.com/bazel-contrib/rules_python/issues/2319). +* (pypi) Correctly handle `python_version` parameter and transition the requirement + locking to the right interpreter version when using + {obj}`compile_pip_requirements` rule. + See [#2819](https://github.com/bazel-contrib/rules_python/pull/2819). {#1-4-0-added} ### Added diff --git a/examples/multi_python_versions/requirements/requirements.in b/examples/multi_python_versions/requirements/requirements.in index 14774b465e..4d1474b9a2 100644 --- a/examples/multi_python_versions/requirements/requirements.in +++ b/examples/multi_python_versions/requirements/requirements.in @@ -1 +1 @@ -websockets +websockets ; python_full_version > "3.9.1" diff --git a/examples/multi_python_versions/requirements/requirements_lock_3_10.txt b/examples/multi_python_versions/requirements/requirements_lock_3_10.txt index 4910d13844..3a8453223f 100644 --- a/examples/multi_python_versions/requirements/requirements_lock_3_10.txt +++ b/examples/multi_python_versions/requirements/requirements_lock_3_10.txt @@ -4,7 +4,7 @@ # # bazel run //requirements:requirements_3_10.update # -websockets==11.0.3 \ +websockets==11.0.3 ; python_full_version > "3.9.1" \ --hash=sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd \ --hash=sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f \ --hash=sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998 \ diff --git a/examples/multi_python_versions/requirements/requirements_lock_3_11.txt b/examples/multi_python_versions/requirements/requirements_lock_3_11.txt index 35666b54b1..f1fa8f56f5 100644 --- a/examples/multi_python_versions/requirements/requirements_lock_3_11.txt +++ b/examples/multi_python_versions/requirements/requirements_lock_3_11.txt @@ -4,7 +4,7 @@ # # bazel run //requirements:requirements_3_11.update # -websockets==11.0.3 \ +websockets==11.0.3 ; python_full_version > "3.9.1" \ --hash=sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd \ --hash=sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f \ --hash=sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998 \ diff --git a/examples/multi_python_versions/requirements/requirements_lock_3_9.txt b/examples/multi_python_versions/requirements/requirements_lock_3_9.txt index 0001f88d48..3c696a865e 100644 --- a/examples/multi_python_versions/requirements/requirements_lock_3_9.txt +++ b/examples/multi_python_versions/requirements/requirements_lock_3_9.txt @@ -4,7 +4,7 @@ # # bazel run //requirements:requirements_3_9.update # -websockets==11.0.3 \ +websockets==11.0.3 ; python_full_version > "3.9.1" \ --hash=sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd \ --hash=sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f \ --hash=sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998 \ diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index bfb0be2d59..9216134857 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -283,6 +283,7 @@ bzl_library( ":evaluate_markers_bzl", ":parse_requirements_bzl", ":pip_repository_attrs_bzl", + ":pypi_repo_utils_bzl", ":render_pkg_aliases_bzl", ":whl_config_setting_bzl", "//python/private:normalize_name_bzl", diff --git a/python/private/pypi/evaluate_markers.bzl b/python/private/pypi/evaluate_markers.bzl index a0223abdc8..f966aa32be 100644 --- a/python/private/pypi/evaluate_markers.bzl +++ b/python/private/pypi/evaluate_markers.bzl @@ -19,11 +19,12 @@ load(":pep508_evaluate.bzl", "evaluate") load(":pep508_platform.bzl", "platform_from_str") load(":pep508_requirement.bzl", "requirement") -def evaluate_markers(requirements): +def evaluate_markers(requirements, python_version = None): """Return the list of supported platforms per requirements line. Args: - requirements: dict[str, list[str]] of the requirement file lines to evaluate. + requirements: {type}`dict[str, list[str]]` of the requirement file lines to evaluate. + python_version: {type}`str | None` the version that can be used when evaluating the markers. Returns: dict of string lists with target platforms @@ -32,7 +33,7 @@ def evaluate_markers(requirements): for req_string, platforms in requirements.items(): req = requirement(req_string) for platform in platforms: - if evaluate(req.marker, env = env(platform_from_str(platform, None))): + if evaluate(req.marker, env = env(platform_from_str(platform, python_version))): ret.setdefault(req_string, []).append(platform) return ret diff --git a/python/private/pypi/pip_compile.bzl b/python/private/pypi/pip_compile.bzl index e5b62c4ab0..9782d3ce21 100644 --- a/python/private/pypi/pip_compile.bzl +++ b/python/private/pypi/pip_compile.bzl @@ -160,6 +160,7 @@ def pip_compile( py_binary( name = name + ".update", env = env, + python_version = kwargs.get("python_version", None), **attrs ) diff --git a/python/private/pypi/pip_repository.bzl b/python/private/pypi/pip_repository.bzl index 01a541cf2f..b7ed1659d1 100644 --- a/python/private/pypi/pip_repository.bzl +++ b/python/private/pypi/pip_repository.bzl @@ -16,11 +16,12 @@ load("@bazel_skylib//lib:sets.bzl", "sets") load("//python/private:normalize_name.bzl", "normalize_name") -load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR") +load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") load("//python/private:text_util.bzl", "render") load(":evaluate_markers.bzl", "evaluate_markers") load(":parse_requirements.bzl", "host_platform", "parse_requirements", "select_requirement") load(":pip_repository_attrs.bzl", "ATTRS") +load(":pypi_repo_utils.bzl", "pypi_repo_utils") load(":render_pkg_aliases.bzl", "render_pkg_aliases") load(":requirements_files_by_platform.bzl", "requirements_files_by_platform") @@ -70,7 +71,27 @@ package(default_visibility = ["//visibility:public"]) exports_files(["requirements.bzl"]) """ +def _evaluate_markers(rctx, requirements, logger = None): + python_interpreter = _get_python_interpreter_attr(rctx) + stdout = pypi_repo_utils.execute_checked_stdout( + rctx, + op = "GetPythonVersionForMarkerEval", + python = python_interpreter, + arguments = [ + # Run the interpreter in isolated mode, this options implies -E, -P and -s. + # Ensures environment variables are ignored that are set in userspace, such as PYTHONPATH, + # which may interfere with this invocation. + "-I", + "-c", + "import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]}', end='')", + ], + srcs = [], + logger = logger, + ) + return evaluate_markers(requirements, python_version = stdout) + def _pip_repository_impl(rctx): + logger = repo_utils.logger(rctx) requirements_by_platform = parse_requirements( rctx, requirements_by_platform = requirements_files_by_platform( @@ -82,7 +103,7 @@ def _pip_repository_impl(rctx): extra_pip_args = rctx.attr.extra_pip_args, ), extra_pip_args = rctx.attr.extra_pip_args, - evaluate_markers = evaluate_markers, + evaluate_markers = lambda requirements: _evaluate_markers(rctx, requirements, logger), ) selected_requirements = {} options = None From 070aa43745810950d572367f7fd6acbf517a76c7 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Thu, 24 Apr 2025 16:41:45 -0700 Subject: [PATCH 054/156] docs: add xrefs for local toolchains rules (#2823) This is to make it easier to find the API docs for the rules the docs talk about. --- docs/toolchains.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/toolchains.md b/docs/toolchains.md index 320e16335b..2f8db66595 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -339,9 +339,10 @@ runtime metadata (Python version, headers, ABI flags, etc) that the regular remotely downloaded runtimes contain, which makes it possible to build e.g. C extensions (unlike the autodetecting and runtime environment toolchains). -For simple cases, some rules are provided that will introspect -a Python installation and create an appropriate Bazel definition from -it. To do this, three pieces need to be wired together: +For simple cases, the {obj}`local_runtime_repo` and +{obj}`local_runtime_toolchains_repo` rules are provided that will introspect a +Python installation and create an appropriate Bazel definition from it. To do +this, three pieces need to be wired together: 1. Specify a path or command to a Python interpreter (multiple can be defined). 2. Create toolchains for the runtimes in (1) From 7234ddae6debeea091d88233c9d974756e64d6e4 Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Fri, 25 Apr 2025 17:05:44 +0200 Subject: [PATCH 055/156] docs: Improve bazel-runfiles docs (#2824) --- python/runfiles/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/runfiles/README.md b/python/runfiles/README.md index 2a57c76846..b5315a48f5 100644 --- a/python/runfiles/README.md +++ b/python/runfiles/README.md @@ -59,6 +59,8 @@ with open(r.Rlocation("my_workspace/path/to/my/data.txt"), "r") as f: # ... ``` +Here `my_workspace` is the name you specified via `module(name = "...")` in your `MODULE.bazel` file (with `--enable_bzlmod`, default as of Bazel 7) or `workspace(name = "...")` in `WORKSPACE` (with `--noenable_bzlmod`). + The code above creates a manifest- or directory-based implementation based on the environment variables in `os.environ`. See `Runfiles.Create()` for more info. If you want to explicitly create a manifest- or directory-based @@ -70,9 +72,7 @@ r1 = Runfiles.CreateManifestBased("path/to/foo.runfiles_manifest") r2 = Runfiles.CreateDirectoryBased("path/to/foo.runfiles/") ``` -If you want to start subprocesses, and the subprocess can't automatically -find the correct runfiles directory, you can explicitly set the right -environment variables for them: +If you want to start subprocesses that access runfiles, you have to set the right environment variables for them: ```python import subprocess From 61c91fe9bd322f91af77db2f57e5b6b40792628f Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 27 Apr 2025 12:43:38 +0900 Subject: [PATCH 056/156] revert(pypi): bring back Python PEP508 code with tests (#2831) This just adds the code back at the original state before the following PRs have been made to remove them: #2629, #2781. This has not been hooked up yet in `evaluate_markers` and `whl_library` yet and I'll need extra PRs to do that. No CHANGELOG entries for now, will be done once the integration is back. Work towards #2830 --- .../pypi/requirements_parser/BUILD.bazel | 0 .../resolve_target_platforms.py | 63 +++ python/private/pypi/whl_installer/BUILD.bazel | 1 + .../private/pypi/whl_installer/arguments.py | 8 + python/private/pypi/whl_installer/platform.py | 304 ++++++++++++++ python/private/pypi/whl_installer/wheel.py | 281 +++++++++++++ .../pypi/whl_installer/wheel_installer.py | 38 +- tests/pypi/whl_installer/BUILD.bazel | 24 ++ tests/pypi/whl_installer/arguments_test.py | 14 +- tests/pypi/whl_installer/platform_test.py | 154 ++++++++ .../whl_installer/wheel_installer_test.py | 41 +- tests/pypi/whl_installer/wheel_test.py | 371 ++++++++++++++++++ 12 files changed, 1285 insertions(+), 14 deletions(-) create mode 100644 python/private/pypi/requirements_parser/BUILD.bazel create mode 100755 python/private/pypi/requirements_parser/resolve_target_platforms.py create mode 100644 python/private/pypi/whl_installer/platform.py create mode 100644 tests/pypi/whl_installer/platform_test.py create mode 100644 tests/pypi/whl_installer/wheel_test.py diff --git a/python/private/pypi/requirements_parser/BUILD.bazel b/python/private/pypi/requirements_parser/BUILD.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/private/pypi/requirements_parser/resolve_target_platforms.py b/python/private/pypi/requirements_parser/resolve_target_platforms.py new file mode 100755 index 0000000000..c899a943cc --- /dev/null +++ b/python/private/pypi/requirements_parser/resolve_target_platforms.py @@ -0,0 +1,63 @@ +"""A CLI to evaluate env markers for requirements files. + +A simple script to evaluate the `requirements.txt` files. Currently it is only +handling environment markers in the requirements files, but in the future it +may handle more things. We require a `python` interpreter that can run on the +host platform and then we depend on the [packaging] PyPI wheel. + +In order to be able to resolve requirements files for any platform, we are +re-using the same code that is used in the `whl_library` installer. See +[here](../whl_installer/wheel.py). + +Requirements for the code are: +- Depends only on `packaging` and core Python. +- Produces the same result irrespective of the Python interpreter platform or version. + +[packaging]: https://packaging.pypa.io/en/stable/ +""" + +import argparse +import json +import pathlib + +from packaging.requirements import Requirement + +from python.private.pypi.whl_installer.platform import Platform + +INPUT_HELP = """\ +Input path to read the requirements as a json file, the keys in the dictionary +are the requirements lines and the values are strings of target platforms. +""" +OUTPUT_HELP = """\ +Output to write the requirements as a json filepath, the keys in the dictionary +are the requirements lines and the values are strings of target platforms, which +got changed based on the evaluated markers. +""" + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("input_path", type=pathlib.Path, help=INPUT_HELP.strip()) + parser.add_argument("output_path", type=pathlib.Path, help=OUTPUT_HELP.strip()) + args = parser.parse_args() + + with args.input_path.open() as f: + reqs = json.load(f) + + response = {} + for requirement_line, target_platforms in reqs.items(): + entry, prefix, hashes = requirement_line.partition("--hash") + hashes = prefix + hashes + + req = Requirement(entry) + for p in target_platforms: + (platform,) = Platform.from_string(p) + if not req.marker or req.marker.evaluate(platform.env_markers("")): + response.setdefault(requirement_line, []).append(p) + + with args.output_path.open("w") as f: + json.dump(response, f) + + +if __name__ == "__main__": + main() diff --git a/python/private/pypi/whl_installer/BUILD.bazel b/python/private/pypi/whl_installer/BUILD.bazel index 49f1a119c1..5fb617004d 100644 --- a/python/private/pypi/whl_installer/BUILD.bazel +++ b/python/private/pypi/whl_installer/BUILD.bazel @@ -6,6 +6,7 @@ py_library( srcs = [ "arguments.py", "namespace_pkgs.py", + "platform.py", "wheel.py", "wheel_installer.py", ], diff --git a/python/private/pypi/whl_installer/arguments.py b/python/private/pypi/whl_installer/arguments.py index bb841ea9ab..29bea8026e 100644 --- a/python/private/pypi/whl_installer/arguments.py +++ b/python/private/pypi/whl_installer/arguments.py @@ -17,6 +17,8 @@ import pathlib from typing import Any, Dict, Set +from python.private.pypi.whl_installer.platform import Platform + def parser(**kwargs: Any) -> argparse.ArgumentParser: """Create a parser for the wheel_installer tool.""" @@ -39,6 +41,12 @@ def parser(**kwargs: Any) -> argparse.ArgumentParser: action="store", help="Extra arguments to pass down to pip.", ) + parser.add_argument( + "--platform", + action="extend", + type=Platform.from_string, + help="Platforms to target dependencies. Can be used multiple times.", + ) parser.add_argument( "--pip_data_exclude", action="store", diff --git a/python/private/pypi/whl_installer/platform.py b/python/private/pypi/whl_installer/platform.py new file mode 100644 index 0000000000..11dd6e37ab --- /dev/null +++ b/python/private/pypi/whl_installer/platform.py @@ -0,0 +1,304 @@ +# Copyright 2024 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. + +"""Utility class to inspect an extracted wheel directory""" + +import platform +import sys +from dataclasses import dataclass +from enum import Enum +from typing import Any, Dict, Iterator, List, Optional, Union + + +class OS(Enum): + linux = 1 + osx = 2 + windows = 3 + darwin = osx + win32 = windows + + @classmethod + def interpreter(cls) -> "OS": + "Return the interpreter operating system." + return cls[sys.platform.lower()] + + def __str__(self) -> str: + return self.name.lower() + + +class Arch(Enum): + x86_64 = 1 + x86_32 = 2 + aarch64 = 3 + ppc = 4 + ppc64le = 5 + s390x = 6 + arm = 7 + amd64 = x86_64 + arm64 = aarch64 + i386 = x86_32 + i686 = x86_32 + x86 = x86_32 + + @classmethod + def interpreter(cls) -> "Arch": + "Return the currently running interpreter architecture." + # FIXME @aignas 2023-12-13: Hermetic toolchain on Windows 3.11.6 + # is returning an empty string here, so lets default to x86_64 + return cls[platform.machine().lower() or "x86_64"] + + def __str__(self) -> str: + return self.name.lower() + + +def _as_int(value: Optional[Union[OS, Arch]]) -> int: + """Convert one of the enums above to an int for easier sorting algorithms. + + Args: + value: The value of an enum or None. + + Returns: + -1 if we get None, otherwise, the numeric value of the given enum. + """ + if value is None: + return -1 + + return int(value.value) + + +def host_interpreter_minor_version() -> int: + return sys.version_info.minor + + +@dataclass(frozen=True) +class Platform: + os: Optional[OS] = None + arch: Optional[Arch] = None + minor_version: Optional[int] = None + + @classmethod + def all( + cls, + want_os: Optional[OS] = None, + minor_version: Optional[int] = None, + ) -> List["Platform"]: + return sorted( + [ + cls(os=os, arch=arch, minor_version=minor_version) + for os in OS + for arch in Arch + if not want_os or want_os == os + ] + ) + + @classmethod + def host(cls) -> List["Platform"]: + """Use the Python interpreter to detect the platform. + + We extract `os` from sys.platform and `arch` from platform.machine + + Returns: + A list of parsed values which makes the signature the same as + `Platform.all` and `Platform.from_string`. + """ + return [ + Platform( + os=OS.interpreter(), + arch=Arch.interpreter(), + minor_version=host_interpreter_minor_version(), + ) + ] + + def all_specializations(self) -> Iterator["Platform"]: + """Return the platform itself and all its unambiguous specializations. + + For more info about specializations see + https://bazel.build/docs/configurable-attributes + """ + yield self + if self.arch is None: + for arch in Arch: + yield Platform(os=self.os, arch=arch, minor_version=self.minor_version) + if self.os is None: + for os in OS: + yield Platform(os=os, arch=self.arch, minor_version=self.minor_version) + if self.arch is None and self.os is None: + for os in OS: + for arch in Arch: + yield Platform(os=os, arch=arch, minor_version=self.minor_version) + + def __lt__(self, other: Any) -> bool: + """Add a comparison method, so that `sorted` returns the most specialized platforms first.""" + if not isinstance(other, Platform) or other is None: + raise ValueError(f"cannot compare {other} with Platform") + + self_arch, self_os = _as_int(self.arch), _as_int(self.os) + other_arch, other_os = _as_int(other.arch), _as_int(other.os) + + if self_os == other_os: + return self_arch < other_arch + else: + return self_os < other_os + + def __str__(self) -> str: + if self.minor_version is None: + if self.os is None and self.arch is None: + return "//conditions:default" + + if self.arch is None: + return f"@platforms//os:{self.os}" + else: + return f"{self.os}_{self.arch}" + + if self.arch is None and self.os is None: + return f"@//python/config_settings:is_python_3.{self.minor_version}" + + if self.arch is None: + return f"cp3{self.minor_version}_{self.os}_anyarch" + + if self.os is None: + return f"cp3{self.minor_version}_anyos_{self.arch}" + + return f"cp3{self.minor_version}_{self.os}_{self.arch}" + + @classmethod + def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]: + """Parse a string and return a list of platforms""" + platform = [platform] if isinstance(platform, str) else list(platform) + ret = set() + for p in platform: + if p == "host": + ret.update(cls.host()) + continue + + abi, _, tail = p.partition("_") + if not abi.startswith("cp"): + # The first item is not an abi + tail = p + abi = "" + os, _, arch = tail.partition("_") + arch = arch or "*" + + minor_version = int(abi[len("cp3") :]) if abi else None + + if arch != "*": + ret.add( + cls( + os=OS[os] if os != "*" else None, + arch=Arch[arch], + minor_version=minor_version, + ) + ) + + else: + ret.update( + cls.all( + want_os=OS[os] if os != "*" else None, + minor_version=minor_version, + ) + ) + + return sorted(ret) + + # NOTE @aignas 2023-12-05: below is the minimum number of accessors that are defined in + # https://peps.python.org/pep-0496/ to make rules_python generate dependencies. + # + # WARNING: It may not work in cases where the python implementation is different between + # different platforms. + + # derived from OS + @property + def os_name(self) -> str: + if self.os == OS.linux or self.os == OS.osx: + return "posix" + elif self.os == OS.windows: + return "nt" + else: + return "" + + @property + def sys_platform(self) -> str: + if self.os == OS.linux: + return "linux" + elif self.os == OS.osx: + return "darwin" + elif self.os == OS.windows: + return "win32" + else: + return "" + + @property + def platform_system(self) -> str: + if self.os == OS.linux: + return "Linux" + elif self.os == OS.osx: + return "Darwin" + elif self.os == OS.windows: + return "Windows" + else: + return "" + + # derived from OS and Arch + @property + def platform_machine(self) -> str: + """Guess the target 'platform_machine' marker. + + NOTE @aignas 2023-12-05: this may not work on really new systems, like + Windows if they define the platform markers in a different way. + """ + if self.arch == Arch.x86_64: + return "x86_64" + elif self.arch == Arch.x86_32 and self.os != OS.osx: + return "i386" + elif self.arch == Arch.x86_32: + return "" + elif self.arch == Arch.aarch64 and self.os == OS.linux: + return "aarch64" + elif self.arch == Arch.aarch64: + # Assuming that OSX and Windows use this one since the precedent is set here: + # https://github.com/cgohlke/win_arm64-wheels + return "arm64" + elif self.os != OS.linux: + return "" + elif self.arch == Arch.ppc: + return "ppc" + elif self.arch == Arch.ppc64le: + return "ppc64le" + elif self.arch == Arch.s390x: + return "s390x" + else: + return "" + + def env_markers(self, extra: str) -> Dict[str, str]: + # If it is None, use the host version + minor_version = self.minor_version or host_interpreter_minor_version() + + return { + "extra": extra, + "os_name": self.os_name, + "sys_platform": self.sys_platform, + "platform_machine": self.platform_machine, + "platform_system": self.platform_system, + "platform_release": "", # unset + "platform_version": "", # unset + "python_version": f"3.{minor_version}", + # FIXME @aignas 2024-01-14: is putting zero last a good idea? Maybe we should + # use `20` or something else to avoid having weird issues where the full version is used for + # matching and the author decides to only support 3.y.5 upwards. + "implementation_version": f"3.{minor_version}.0", + "python_full_version": f"3.{minor_version}.0", + # we assume that the following are the same as the interpreter used to setup the deps: + # "implementation_name": "cpython" + # "platform_python_implementation: "CPython", + } diff --git a/python/private/pypi/whl_installer/wheel.py b/python/private/pypi/whl_installer/wheel.py index da81b5ea9f..d95b33a194 100644 --- a/python/private/pypi/whl_installer/wheel.py +++ b/python/private/pypi/whl_installer/wheel.py @@ -25,6 +25,275 @@ from packaging.requirements import Requirement from pip._vendor.packaging.utils import canonicalize_name +from python.private.pypi.whl_installer.platform import ( + Platform, + host_interpreter_minor_version, +) + + +@dataclass(frozen=True) +class FrozenDeps: + deps: List[str] + deps_select: Dict[str, List[str]] + + +class Deps: + """Deps is a dependency builder that has a build() method to return FrozenDeps.""" + + def __init__( + self, + name: str, + requires_dist: List[str], + *, + extras: Optional[Set[str]] = None, + platforms: Optional[Set[Platform]] = None, + ): + """Create a new instance and parse the requires_dist + + Args: + name (str): The name of the whl distribution + requires_dist (list[Str]): The Requires-Dist from the METADATA of the whl + distribution. + extras (set[str], optional): The list of requested extras, defaults to None. + platforms (set[Platform], optional): The list of target platforms, defaults to + None. If the list of platforms has multiple `minor_version` values, it + will change the code to generate the select statements using + `@rules_python//python/config_settings:is_python_3.y` conditions. + """ + self.name: str = Deps._normalize(name) + self._platforms: Set[Platform] = platforms or set() + self._target_versions = {p.minor_version for p in platforms or {}} + self._default_minor_version = None + if platforms and len(self._target_versions) > 2: + # TODO @aignas 2024-06-23: enable this to be set via a CLI arg + # for being more explicit. + self._default_minor_version = host_interpreter_minor_version() + + if None in self._target_versions and len(self._target_versions) > 2: + raise ValueError( + f"all python versions need to be specified explicitly, got: {platforms}" + ) + + # Sort so that the dictionary order in the FrozenDeps is deterministic + # without the final sort because Python retains insertion order. That way + # the sorting by platform is limited within the Platform class itself and + # the unit-tests for the Deps can be simpler. + reqs = sorted( + (Requirement(wheel_req) for wheel_req in requires_dist), + key=lambda x: f"{x.name}:{sorted(x.extras)}", + ) + + want_extras = self._resolve_extras(reqs, extras) + + # Then add all of the requirements in order + self._deps: Set[str] = set() + self._select: Dict[Platform, Set[str]] = defaultdict(set) + for req in reqs: + self._add_req(req, want_extras) + + def _add(self, dep: str, platform: Optional[Platform]): + dep = Deps._normalize(dep) + + # Self-edges are processed in _resolve_extras + if dep == self.name: + return + + if not platform: + self._deps.add(dep) + + # If the dep is in the platform-specific list, remove it from the select. + pop_keys = [] + for p, deps in self._select.items(): + if dep not in deps: + continue + + deps.remove(dep) + if not deps: + pop_keys.append(p) + + for p in pop_keys: + self._select.pop(p) + return + + if dep in self._deps: + # If the dep is already in the main dependency list, no need to add it in the + # platform-specific dependency list. + return + + # Add the platform-specific dep + self._select[platform].add(dep) + + # Add the dep to specializations of the given platform if they + # exist in the select statement. + for p in platform.all_specializations(): + if p not in self._select: + continue + + self._select[p].add(dep) + + if len(self._select[platform]) == 1: + # We are adding a new item to the select and we need to ensure that + # existing dependencies from less specialized platforms are propagated + # to the newly added dependency set. + for p, deps in self._select.items(): + # Check if the existing platform overlaps with the given platform + if p == platform or platform not in p.all_specializations(): + continue + + self._select[platform].update(self._select[p]) + + def _maybe_add_common_dep(self, dep): + if len(self._target_versions) < 2: + return + + platforms = [Platform()] + [ + Platform(minor_version=v) for v in self._target_versions + ] + + # If the dep is targeting all target python versions, lets add it to + # the common dependency list to simplify the select statements. + for p in platforms: + if p not in self._select: + return + + if dep not in self._select[p]: + return + + # All of the python version-specific branches have the dep, so lets add + # it to the common deps. + self._deps.add(dep) + for p in platforms: + self._select[p].remove(dep) + if not self._select[p]: + self._select.pop(p) + + @staticmethod + def _normalize(name: str) -> str: + return re.sub(r"[-_.]+", "_", name).lower() + + def _resolve_extras( + self, reqs: List[Requirement], extras: Optional[Set[str]] + ) -> Set[str]: + """Resolve extras which are due to depending on self[some_other_extra]. + + Some packages may have cyclic dependencies resulting from extras being used, one example is + `etils`, where we have one set of extras as aliases for other extras + and we have an extra called 'all' that includes all other extras. + + Example: github.com/google/etils/blob/a0b71032095db14acf6b33516bca6d885fe09e35/pyproject.toml#L32. + + When the `requirements.txt` is generated by `pip-tools`, then it is likely that + this step is not needed, but for other `requirements.txt` files this may be useful. + + NOTE @aignas 2023-12-08: the extra resolution is not platform dependent, + but in order for it to become platform dependent we would have to have + separate targets for each extra in extras. + """ + + # Resolve any extra extras due to self-edges, empty string means no + # extras The empty string in the set is just a way to make the handling + # of no extras and a single extra easier and having a set of {"", "foo"} + # is equivalent to having {"foo"}. + extras = extras or {""} + + self_reqs = [] + for req in reqs: + if Deps._normalize(req.name) != self.name: + continue + + if req.marker is None: + # I am pretty sure we cannot reach this code as it does not + # make sense to specify packages in this way, but since it is + # easy to handle, lets do it. + # + # TODO @aignas 2023-12-08: add a test + extras = extras | req.extras + else: + # process these in a separate loop + self_reqs.append(req) + + # A double loop is not strictly optimal, but always correct without recursion + for req in self_reqs: + if any(req.marker.evaluate({"extra": extra}) for extra in extras): + extras = extras | req.extras + else: + continue + + # Iterate through all packages to ensure that we include all of the extras from previously + # visited packages. + for req_ in self_reqs: + if any(req_.marker.evaluate({"extra": extra}) for extra in extras): + extras = extras | req_.extras + + return extras + + def _add_req(self, req: Requirement, extras: Set[str]) -> None: + if req.marker is None: + self._add(req.name, None) + return + + marker_str = str(req.marker) + + if not self._platforms: + if any(req.marker.evaluate({"extra": extra}) for extra in extras): + self._add(req.name, None) + return + + # NOTE @aignas 2023-12-08: in order to have reasonable select statements + # we do have to have some parsing of the markers, so it begs the question + # if packaging should be reimplemented in Starlark to have the best solution + # for now we will implement it in Python and see what the best parsing result + # can be before making this decision. + match_os = any( + tag in marker_str + for tag in [ + "os_name", + "sys_platform", + "platform_system", + ] + ) + match_arch = "platform_machine" in marker_str + match_version = "version" in marker_str + + if not (match_os or match_arch or match_version): + if any(req.marker.evaluate({"extra": extra}) for extra in extras): + self._add(req.name, None) + return + + for plat in self._platforms: + if not any( + req.marker.evaluate(plat.env_markers(extra)) for extra in extras + ): + continue + + if match_arch and self._default_minor_version: + self._add(req.name, plat) + if plat.minor_version == self._default_minor_version: + self._add(req.name, Platform(plat.os, plat.arch)) + elif match_arch: + self._add(req.name, Platform(plat.os, plat.arch)) + elif match_os and self._default_minor_version: + self._add(req.name, Platform(plat.os, minor_version=plat.minor_version)) + if plat.minor_version == self._default_minor_version: + self._add(req.name, Platform(plat.os)) + elif match_os: + self._add(req.name, Platform(plat.os)) + elif match_version and self._default_minor_version: + self._add(req.name, Platform(minor_version=plat.minor_version)) + if plat.minor_version == self._default_minor_version: + self._add(req.name, Platform()) + elif match_version: + self._add(req.name, None) + + # Merge to common if possible after processing all platforms + self._maybe_add_common_dep(req.name) + + def build(self) -> FrozenDeps: + return FrozenDeps( + deps=sorted(self._deps), + deps_select={str(p): sorted(deps) for p, deps in self._select.items()}, + ) + class Wheel: """Representation of the compressed .whl file""" @@ -75,6 +344,18 @@ def entry_points(self) -> Dict[str, Tuple[str, str]]: return entry_points_mapping + def dependencies( + self, + extras_requested: Set[str] = None, + platforms: Optional[Set[Platform]] = None, + ) -> FrozenDeps: + return Deps( + self.name, + extras=extras_requested, + platforms=platforms, + requires_dist=self.metadata.get_all("Requires-Dist", []), + ).build() + def unzip(self, directory: str) -> None: installation_schemes = { "purelib": "/site-packages", diff --git a/python/private/pypi/whl_installer/wheel_installer.py b/python/private/pypi/whl_installer/wheel_installer.py index c7695d92e8..a48df699ba 100644 --- a/python/private/pypi/whl_installer/wheel_installer.py +++ b/python/private/pypi/whl_installer/wheel_installer.py @@ -23,7 +23,7 @@ import sys from pathlib import Path from tempfile import NamedTemporaryFile -from typing import Dict, Optional, Set, Tuple +from typing import Dict, List, Optional, Set, Tuple from pip._vendor.packaging.utils import canonicalize_name @@ -103,7 +103,9 @@ def _setup_namespace_pkg_compatibility(wheel_dir: str) -> None: def _extract_wheel( wheel_file: str, + extras: Dict[str, Set[str]], enable_implicit_namespace_pkgs: bool, + platforms: List[wheel.Platform], installation_dir: Path = Path("."), ) -> None: """Extracts wheel into given directory and creates py_library and filegroup targets. @@ -111,6 +113,7 @@ def _extract_wheel( Args: 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 enable_implicit_namespace_pkgs: if true, disables conversion of implicit namespace packages and will unzip as-is """ @@ -120,19 +123,26 @@ def _extract_wheel( if not enable_implicit_namespace_pkgs: _setup_namespace_pkg_compatibility(installation_dir) - metadata = { - "python_version": sys.version.partition(" ")[0], - "entry_points": [ - { - "name": name, - "module": module, - "attribute": attribute, - } - for name, (module, attribute) in sorted(whl.entry_points().items()) - ], - } + extras_requested = extras[whl.name] if whl.name in extras else set() + + dependencies = whl.dependencies(extras_requested, platforms) with open(os.path.join(installation_dir, "metadata.json"), "w") as f: + metadata = { + "name": whl.name, + "version": whl.version, + "deps": dependencies.deps, + "python_version": f"{sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]}", + "deps_by_platform": dependencies.deps_select, + "entry_points": [ + { + "name": name, + "module": module, + "attribute": attribute, + } + for name, (module, attribute) in sorted(whl.entry_points().items()) + ], + } json.dump(metadata, f) @@ -146,9 +156,13 @@ def main() -> None: if args.whl_file: whl = Path(args.whl_file) + name, extras_for_pkg = _parse_requirement_for_extra(args.requirement) + extras = {name: extras_for_pkg} if extras_for_pkg and name else dict() _extract_wheel( wheel_file=whl, + extras=extras, enable_implicit_namespace_pkgs=args.enable_implicit_namespace_pkgs, + platforms=arguments.get_platforms(args), ) return diff --git a/tests/pypi/whl_installer/BUILD.bazel b/tests/pypi/whl_installer/BUILD.bazel index fea6a46d01..040e4d765f 100644 --- a/tests/pypi/whl_installer/BUILD.bazel +++ b/tests/pypi/whl_installer/BUILD.bazel @@ -27,6 +27,18 @@ py_test( ], ) +py_test( + name = "platform_test", + size = "small", + srcs = [ + "platform_test.py", + ], + data = ["//examples/wheel:minimal_with_py_package"], + deps = [ + ":lib", + ], +) + py_test( name = "wheel_installer_test", size = "small", @@ -38,3 +50,15 @@ py_test( ":lib", ], ) + +py_test( + name = "wheel_test", + size = "small", + srcs = [ + "wheel_test.py", + ], + data = ["//examples/wheel:minimal_with_py_package"], + deps = [ + ":lib", + ], +) diff --git a/tests/pypi/whl_installer/arguments_test.py b/tests/pypi/whl_installer/arguments_test.py index 9f73ae96a9..5538054a59 100644 --- a/tests/pypi/whl_installer/arguments_test.py +++ b/tests/pypi/whl_installer/arguments_test.py @@ -15,7 +15,7 @@ import json import unittest -from python.private.pypi.whl_installer import arguments +from python.private.pypi.whl_installer import arguments, wheel class ArgumentsTestCase(unittest.TestCase): @@ -49,6 +49,18 @@ def test_deserialize_structured_args(self) -> None: self.assertEqual(args["environment"], {"PIP_DO_SOMETHING": "True"}) self.assertEqual(args["extra_pip_args"], []) + def test_platform_aggregation(self) -> None: + parser = arguments.parser() + args = parser.parse_args( + args=[ + "--platform=linux_*", + "--platform=osx_*", + "--platform=windows_*", + "--requirement=foo", + ] + ) + self.assertEqual(set(wheel.Platform.all()), arguments.get_platforms(args)) + if __name__ == "__main__": unittest.main() diff --git a/tests/pypi/whl_installer/platform_test.py b/tests/pypi/whl_installer/platform_test.py new file mode 100644 index 0000000000..2aeb4caa69 --- /dev/null +++ b/tests/pypi/whl_installer/platform_test.py @@ -0,0 +1,154 @@ +import unittest +from random import shuffle + +from python.private.pypi.whl_installer.platform import ( + OS, + Arch, + Platform, + host_interpreter_minor_version, +) + + +class MinorVersionTest(unittest.TestCase): + def test_host(self): + host = host_interpreter_minor_version() + self.assertIsNotNone(host) + + +class PlatformTest(unittest.TestCase): + def test_can_get_host(self): + host = Platform.host() + self.assertIsNotNone(host) + self.assertEqual(1, len(Platform.from_string("host"))) + self.assertEqual(host, Platform.from_string("host")) + + def test_can_get_linux_x86_64_without_py_version(self): + got = Platform.from_string("linux_x86_64") + want = Platform(os=OS.linux, arch=Arch.x86_64) + self.assertEqual(want, got[0]) + + def test_can_get_specific_from_string(self): + got = Platform.from_string("cp33_linux_x86_64") + want = Platform(os=OS.linux, arch=Arch.x86_64, minor_version=3) + self.assertEqual(want, got[0]) + + def test_can_get_all_for_py_version(self): + cp39 = Platform.all(minor_version=9) + self.assertEqual(21, len(cp39), f"Got {cp39}") + self.assertEqual(cp39, Platform.from_string("cp39_*")) + + def test_can_get_all_for_os(self): + linuxes = Platform.all(OS.linux, minor_version=9) + self.assertEqual(7, len(linuxes)) + self.assertEqual(linuxes, Platform.from_string("cp39_linux_*")) + + def test_can_get_all_for_os_for_host_python(self): + linuxes = Platform.all(OS.linux) + self.assertEqual(7, len(linuxes)) + self.assertEqual(linuxes, Platform.from_string("linux_*")) + + def test_specific_version_specializations(self): + any_py33 = Platform(minor_version=3) + + # When + all_specializations = list(any_py33.all_specializations()) + + want = ( + [any_py33] + + [ + Platform(arch=arch, minor_version=any_py33.minor_version) + for arch in Arch + ] + + [Platform(os=os, minor_version=any_py33.minor_version) for os in OS] + + Platform.all(minor_version=any_py33.minor_version) + ) + self.assertEqual(want, all_specializations) + + def test_aarch64_specializations(self): + any_aarch64 = Platform(arch=Arch.aarch64) + all_specializations = list(any_aarch64.all_specializations()) + want = [ + Platform(os=None, arch=Arch.aarch64), + Platform(os=OS.linux, arch=Arch.aarch64), + Platform(os=OS.osx, arch=Arch.aarch64), + Platform(os=OS.windows, arch=Arch.aarch64), + ] + self.assertEqual(want, all_specializations) + + def test_linux_specializations(self): + any_linux = Platform(os=OS.linux) + all_specializations = list(any_linux.all_specializations()) + want = [ + Platform(os=OS.linux, arch=None), + Platform(os=OS.linux, arch=Arch.x86_64), + Platform(os=OS.linux, arch=Arch.x86_32), + Platform(os=OS.linux, arch=Arch.aarch64), + Platform(os=OS.linux, arch=Arch.ppc), + Platform(os=OS.linux, arch=Arch.ppc64le), + Platform(os=OS.linux, arch=Arch.s390x), + Platform(os=OS.linux, arch=Arch.arm), + ] + self.assertEqual(want, all_specializations) + + def test_osx_specializations(self): + any_osx = Platform(os=OS.osx) + all_specializations = list(any_osx.all_specializations()) + # NOTE @aignas 2024-01-14: even though in practice we would only have + # Python on osx aarch64 and osx x86_64, we return all arch posibilities + # to make the code simpler. + want = [ + Platform(os=OS.osx, arch=None), + Platform(os=OS.osx, arch=Arch.x86_64), + Platform(os=OS.osx, arch=Arch.x86_32), + Platform(os=OS.osx, arch=Arch.aarch64), + Platform(os=OS.osx, arch=Arch.ppc), + Platform(os=OS.osx, arch=Arch.ppc64le), + Platform(os=OS.osx, arch=Arch.s390x), + Platform(os=OS.osx, arch=Arch.arm), + ] + self.assertEqual(want, all_specializations) + + def test_platform_sort(self): + platforms = [ + Platform(os=OS.linux, arch=None), + Platform(os=OS.linux, arch=Arch.x86_64), + Platform(os=OS.osx, arch=None), + Platform(os=OS.osx, arch=Arch.x86_64), + Platform(os=OS.osx, arch=Arch.aarch64), + ] + shuffle(platforms) + platforms.sort() + want = [ + Platform(os=OS.linux, arch=None), + Platform(os=OS.linux, arch=Arch.x86_64), + Platform(os=OS.osx, arch=None), + Platform(os=OS.osx, arch=Arch.x86_64), + Platform(os=OS.osx, arch=Arch.aarch64), + ] + + self.assertEqual(want, platforms) + + def test_wheel_os_alias(self): + self.assertEqual("osx", str(OS.osx)) + self.assertEqual(str(OS.darwin), str(OS.osx)) + + def test_wheel_arch_alias(self): + self.assertEqual("x86_64", str(Arch.x86_64)) + self.assertEqual(str(Arch.amd64), str(Arch.x86_64)) + + def test_wheel_platform_alias(self): + give = Platform( + os=OS.darwin, + arch=Arch.amd64, + ) + alias = Platform( + os=OS.osx, + arch=Arch.x86_64, + ) + + self.assertEqual("osx_x86_64", str(give)) + self.assertEqual(str(alias), str(give)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pypi/whl_installer/wheel_installer_test.py b/tests/pypi/whl_installer/wheel_installer_test.py index 3c118af3c4..b736877e81 100644 --- a/tests/pypi/whl_installer/wheel_installer_test.py +++ b/tests/pypi/whl_installer/wheel_installer_test.py @@ -22,6 +22,39 @@ from python.private.pypi.whl_installer import wheel_installer +class TestRequirementExtrasParsing(unittest.TestCase): + def test_parses_requirement_for_extra(self) -> None: + cases = [ + ("name[foo]", ("name", frozenset(["foo"]))), + ("name[ Foo123 ]", ("name", frozenset(["Foo123"]))), + (" name1[ foo ] ", ("name1", frozenset(["foo"]))), + ("Name[foo]", ("name", frozenset(["foo"]))), + ("name_foo[bar]", ("name-foo", frozenset(["bar"]))), + ( + "name [fred,bar] @ http://foo.com ; python_version=='2.7'", + ("name", frozenset(["fred", "bar"])), + ), + ( + "name[quux, strange];python_version<'2.7' and platform_version=='2'", + ("name", frozenset(["quux", "strange"])), + ), + ( + "name; (os_name=='a' or os_name=='b') and os_name=='c'", + (None, None), + ), + ( + "name@http://foo.com", + (None, None), + ), + ] + + for case, expected in cases: + with self.subTest(): + self.assertTupleEqual( + wheel_installer._parse_requirement_for_extra(case), expected + ) + + class TestWhlFilegroup(unittest.TestCase): def setUp(self) -> None: self.wheel_name = "example_minimal_package-0.0.1-py3-none-any.whl" @@ -35,8 +68,10 @@ def tearDown(self): def test_wheel_exists(self) -> None: wheel_installer._extract_wheel( Path(self.wheel_path), - enable_implicit_namespace_pkgs=False, installation_dir=Path(self.wheel_dir), + extras={}, + enable_implicit_namespace_pkgs=False, + platforms=[], ) want_files = [ @@ -57,8 +92,12 @@ def test_wheel_exists(self) -> None: metadata_file_content = json.load(metadata_file) want = dict( + deps=[], + deps_by_platform={}, entry_points=[], + name="example-minimal-package", python_version="3.11.11", + version="0.0.1", ) self.assertEqual(want, metadata_file_content) diff --git a/tests/pypi/whl_installer/wheel_test.py b/tests/pypi/whl_installer/wheel_test.py new file mode 100644 index 0000000000..404218e12b --- /dev/null +++ b/tests/pypi/whl_installer/wheel_test.py @@ -0,0 +1,371 @@ +import unittest +from unittest import mock + +from python.private.pypi.whl_installer import wheel +from python.private.pypi.whl_installer.platform import OS, Arch, Platform + +_HOST_INTERPRETER_FN = ( + "python.private.pypi.whl_installer.wheel.host_interpreter_minor_version" +) + + +class DepsTest(unittest.TestCase): + def test_simple(self): + deps = wheel.Deps("foo", requires_dist=["bar"]) + + got = deps.build() + + self.assertIsInstance(got, wheel.FrozenDeps) + self.assertEqual(["bar"], got.deps) + self.assertEqual({}, got.deps_select) + + def test_can_add_os_specific_deps(self): + deps = wheel.Deps( + "foo", + requires_dist=[ + "bar", + "an_osx_dep; sys_platform=='darwin'", + "posix_dep; os_name=='posix'", + "win_dep; os_name=='nt'", + ], + platforms={ + Platform(os=OS.linux, arch=Arch.x86_64), + Platform(os=OS.osx, arch=Arch.x86_64), + Platform(os=OS.osx, arch=Arch.aarch64), + Platform(os=OS.windows, arch=Arch.x86_64), + }, + ) + + got = deps.build() + + self.assertEqual(["bar"], got.deps) + self.assertEqual( + { + "@platforms//os:linux": ["posix_dep"], + "@platforms//os:osx": ["an_osx_dep", "posix_dep"], + "@platforms//os:windows": ["win_dep"], + }, + got.deps_select, + ) + + def test_can_add_os_specific_deps_with_specific_python_version(self): + deps = wheel.Deps( + "foo", + requires_dist=[ + "bar", + "an_osx_dep; sys_platform=='darwin'", + "posix_dep; os_name=='posix'", + "win_dep; os_name=='nt'", + ], + platforms={ + Platform(os=OS.linux, arch=Arch.x86_64, minor_version=8), + Platform(os=OS.osx, arch=Arch.x86_64, minor_version=8), + Platform(os=OS.osx, arch=Arch.aarch64, minor_version=8), + Platform(os=OS.windows, arch=Arch.x86_64, minor_version=8), + }, + ) + + got = deps.build() + + self.assertEqual(["bar"], got.deps) + self.assertEqual( + { + "@platforms//os:linux": ["posix_dep"], + "@platforms//os:osx": ["an_osx_dep", "posix_dep"], + "@platforms//os:windows": ["win_dep"], + }, + got.deps_select, + ) + + def test_deps_are_added_to_more_specialized_platforms(self): + got = wheel.Deps( + "foo", + requires_dist=[ + "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", + "mac_dep; sys_platform=='darwin'", + ], + platforms={ + Platform(os=OS.osx, arch=Arch.x86_64), + Platform(os=OS.osx, arch=Arch.aarch64), + }, + ).build() + + self.assertEqual( + wheel.FrozenDeps( + deps=[], + deps_select={ + "osx_aarch64": ["m1_dep", "mac_dep"], + "@platforms//os:osx": ["mac_dep"], + }, + ), + got, + ) + + def test_deps_from_more_specialized_platforms_are_propagated(self): + got = wheel.Deps( + "foo", + requires_dist=[ + "a_mac_dep; sys_platform=='darwin'", + "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", + ], + platforms={ + Platform(os=OS.osx, arch=Arch.x86_64), + Platform(os=OS.osx, arch=Arch.aarch64), + }, + ).build() + + self.assertEqual([], got.deps) + self.assertEqual( + { + "osx_aarch64": ["a_mac_dep", "m1_dep"], + "@platforms//os:osx": ["a_mac_dep"], + }, + got.deps_select, + ) + + def test_non_platform_markers_are_added_to_common_deps(self): + got = wheel.Deps( + "foo", + requires_dist=[ + "bar", + "baz; implementation_name=='cpython'", + "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", + ], + platforms={ + Platform(os=OS.linux, arch=Arch.x86_64), + Platform(os=OS.osx, arch=Arch.x86_64), + Platform(os=OS.osx, arch=Arch.aarch64), + Platform(os=OS.windows, arch=Arch.x86_64), + }, + ).build() + + self.assertEqual(["bar", "baz"], got.deps) + self.assertEqual( + { + "osx_aarch64": ["m1_dep"], + }, + got.deps_select, + ) + + def test_self_is_ignored(self): + deps = wheel.Deps( + "foo", + requires_dist=[ + "bar", + "req_dep; extra == 'requests'", + "foo[requests]; extra == 'ssl'", + "ssl_lib; extra == 'ssl'", + ], + extras={"ssl"}, + ) + + got = deps.build() + + self.assertEqual(["bar", "req_dep", "ssl_lib"], got.deps) + self.assertEqual({}, got.deps_select) + + def test_self_dependencies_can_come_in_any_order(self): + deps = wheel.Deps( + "foo", + requires_dist=[ + "bar", + "baz; extra == 'feat'", + "foo[feat2]; extra == 'all'", + "foo[feat]; extra == 'feat2'", + "zdep; extra == 'all'", + ], + extras={"all"}, + ) + + got = deps.build() + + self.assertEqual(["bar", "baz", "zdep"], got.deps) + self.assertEqual({}, got.deps_select) + + def test_can_get_deps_based_on_specific_python_version(self): + requires_dist = [ + "bar", + "baz; python_version < '3.8'", + "posix_dep; os_name=='posix' and python_version >= '3.8'", + ] + + py38_deps = wheel.Deps( + "foo", + requires_dist=requires_dist, + platforms=[ + Platform(os=OS.linux, arch=Arch.x86_64, minor_version=8), + ], + ).build() + py37_deps = wheel.Deps( + "foo", + requires_dist=requires_dist, + platforms=[ + Platform(os=OS.linux, arch=Arch.x86_64, minor_version=7), + ], + ).build() + + self.assertEqual(["bar", "baz"], py37_deps.deps) + self.assertEqual({}, py37_deps.deps_select) + self.assertEqual(["bar"], py38_deps.deps) + self.assertEqual({"@platforms//os:linux": ["posix_dep"]}, py38_deps.deps_select) + + @mock.patch(_HOST_INTERPRETER_FN) + def test_no_version_select_when_single_version(self, mock_host_interpreter_version): + requires_dist = [ + "bar", + "baz; python_version >= '3.8'", + "posix_dep; os_name=='posix'", + "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", + "arch_dep; platform_machine=='x86_64' and python_version >= '3.8'", + ] + mock_host_interpreter_version.return_value = 7 + + self.maxDiff = None + + deps = wheel.Deps( + "foo", + requires_dist=requires_dist, + platforms=[ + Platform(os=os, arch=Arch.x86_64, minor_version=minor) + for minor in [8] + for os in [OS.linux, OS.windows] + ], + ) + got = deps.build() + + self.assertEqual(["bar", "baz"], got.deps) + self.assertEqual( + { + "@platforms//os:linux": ["posix_dep", "posix_dep_with_version"], + "linux_x86_64": ["arch_dep", "posix_dep", "posix_dep_with_version"], + "windows_x86_64": ["arch_dep"], + }, + got.deps_select, + ) + + @mock.patch(_HOST_INTERPRETER_FN) + def test_can_get_version_select(self, mock_host_interpreter_version): + requires_dist = [ + "bar", + "baz; python_version < '3.8'", + "baz_new; python_version >= '3.8'", + "posix_dep; os_name=='posix'", + "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", + "arch_dep; platform_machine=='x86_64' and python_version < '3.8'", + ] + mock_host_interpreter_version.return_value = 7 + + self.maxDiff = None + + deps = wheel.Deps( + "foo", + requires_dist=requires_dist, + platforms=[ + Platform(os=os, arch=Arch.x86_64, minor_version=minor) + for minor in [7, 8, 9] + for os in [OS.linux, OS.windows] + ], + ) + got = deps.build() + + self.assertEqual(["bar"], got.deps) + self.assertEqual( + { + "//conditions:default": ["baz"], + "@//python/config_settings:is_python_3.7": ["baz"], + "@//python/config_settings:is_python_3.8": ["baz_new"], + "@//python/config_settings:is_python_3.9": ["baz_new"], + "@platforms//os:linux": ["baz", "posix_dep"], + "cp37_linux_x86_64": ["arch_dep", "baz", "posix_dep"], + "cp37_windows_x86_64": ["arch_dep", "baz"], + "cp37_linux_anyarch": ["baz", "posix_dep"], + "cp38_linux_anyarch": [ + "baz_new", + "posix_dep", + "posix_dep_with_version", + ], + "cp39_linux_anyarch": [ + "baz_new", + "posix_dep", + "posix_dep_with_version", + ], + "linux_x86_64": ["arch_dep", "baz", "posix_dep"], + "windows_x86_64": ["arch_dep", "baz"], + }, + got.deps_select, + ) + + @mock.patch(_HOST_INTERPRETER_FN) + def test_deps_spanning_all_target_py_versions_are_added_to_common( + self, mock_host_version + ): + requires_dist = [ + "bar", + "baz (<2,>=1.11) ; python_version < '3.8'", + "baz (<2,>=1.14) ; python_version >= '3.8'", + ] + mock_host_version.return_value = 8 + + deps = wheel.Deps( + "foo", + requires_dist=requires_dist, + platforms=Platform.from_string(["cp37_*", "cp38_*", "cp39_*"]), + ) + got = deps.build() + + self.assertEqual(["bar", "baz"], got.deps) + self.assertEqual({}, got.deps_select) + + @mock.patch(_HOST_INTERPRETER_FN) + def test_deps_are_not_duplicated(self, mock_host_version): + mock_host_version.return_value = 7 + + # See an example in + # https://files.pythonhosted.org/packages/76/9e/db1c2d56c04b97981c06663384f45f28950a73d9acf840c4006d60d0a1ff/opencv_python-4.9.0.80-cp37-abi3-win32.whl.metadata + requires_dist = [ + "bar >=0.1.0 ; python_version < '3.7'", + "bar >=0.2.0 ; python_version >= '3.7'", + "bar >=0.4.0 ; python_version >= '3.6' and platform_system == 'Linux' and platform_machine == 'aarch64'", + "bar >=0.4.0 ; python_version >= '3.9'", + "bar >=0.5.0 ; python_version <= '3.9' and platform_system == 'Darwin' and platform_machine == 'arm64'", + "bar >=0.5.0 ; python_version >= '3.10' and platform_system == 'Darwin'", + "bar >=0.5.0 ; python_version >= '3.10'", + "bar >=0.6.0 ; python_version >= '3.11'", + ] + + deps = wheel.Deps( + "foo", + requires_dist=requires_dist, + platforms=Platform.from_string(["cp37_*", "cp310_*"]), + ) + got = deps.build() + + self.assertEqual(["bar"], got.deps) + self.assertEqual({}, got.deps_select) + + @mock.patch(_HOST_INTERPRETER_FN) + def test_deps_are_not_duplicated_when_encountering_platform_dep_first( + self, mock_host_version + ): + mock_host_version.return_value = 7 + + # Note, that we are sorting the incoming `requires_dist` and we need to ensure that we are not getting any + # issues even if the platform-specific line comes first. + requires_dist = [ + "bar >=0.4.0 ; python_version >= '3.6' and platform_system == 'Linux' and platform_machine == 'aarch64'", + "bar >=0.5.0 ; python_version >= '3.9'", + ] + + deps = wheel.Deps( + "foo", + requires_dist=requires_dist, + platforms=Platform.from_string(["cp37_*", "cp310_*"]), + ) + got = deps.build() + + self.assertEqual(["bar"], got.deps) + self.assertEqual({}, got.deps_select) + + +if __name__ == "__main__": + unittest.main() From 9e613d58cecda3f370698f37f7ca26bf38486db3 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 28 Apr 2025 18:44:32 +0900 Subject: [PATCH 057/156] fix(pypi) backport python_full_version fix to Python (#2833) Handling of `python_full_version` correctly has been fixed in the Starlark implementation in #2793 and in this PR I am backporting the changes to handle the full python version target platform strings so that we can have the same behaviour for now. At the same time I have simplified and got rid of the specialization handling in the Python algorithm just like I did in the starlark, which simplifies the tests and makes the algorithm more correct. Summary: * Handle `cp3x.y_os_arch` strings in the `platform.py` * Produce correct strings when the `micro_version` is unset. Note, that we use version `0` in evaluating but we use the default version in the config setting. This is to keep compatibility with the current behaviour when the target platform is not fully specified (which would be the case for WORKSPACE users). * Adjust the tests and the code to be more similar to the starlark impl. Work towards #2830 --- python/private/pypi/whl_installer/platform.py | 90 ++++---- python/private/pypi/whl_installer/wheel.py | 140 +++-------- tests/pypi/whl_installer/platform_test.py | 73 +----- tests/pypi/whl_installer/wheel_test.py | 218 ++++++++---------- 4 files changed, 185 insertions(+), 336 deletions(-) diff --git a/python/private/pypi/whl_installer/platform.py b/python/private/pypi/whl_installer/platform.py index 11dd6e37ab..ff267fe4aa 100644 --- a/python/private/pypi/whl_installer/platform.py +++ b/python/private/pypi/whl_installer/platform.py @@ -18,7 +18,7 @@ import sys from dataclasses import dataclass from enum import Enum -from typing import Any, Dict, Iterator, List, Optional, Union +from typing import Any, Dict, Iterator, List, Optional, Tuple, Union class OS(Enum): @@ -77,8 +77,8 @@ def _as_int(value: Optional[Union[OS, Arch]]) -> int: return int(value.value) -def host_interpreter_minor_version() -> int: - return sys.version_info.minor +def host_interpreter_version() -> Tuple[int, int]: + return (sys.version_info.minor, sys.version_info.micro) @dataclass(frozen=True) @@ -86,16 +86,23 @@ class Platform: os: Optional[OS] = None arch: Optional[Arch] = None minor_version: Optional[int] = None + micro_version: Optional[int] = None @classmethod def all( cls, want_os: Optional[OS] = None, minor_version: Optional[int] = None, + micro_version: Optional[int] = None, ) -> List["Platform"]: return sorted( [ - cls(os=os, arch=arch, minor_version=minor_version) + cls( + os=os, + arch=arch, + minor_version=minor_version, + micro_version=micro_version, + ) for os in OS for arch in Arch if not want_os or want_os == os @@ -112,32 +119,16 @@ def host(cls) -> List["Platform"]: A list of parsed values which makes the signature the same as `Platform.all` and `Platform.from_string`. """ + minor, micro = host_interpreter_version() return [ Platform( os=OS.interpreter(), arch=Arch.interpreter(), - minor_version=host_interpreter_minor_version(), + minor_version=minor, + micro_version=micro, ) ] - def all_specializations(self) -> Iterator["Platform"]: - """Return the platform itself and all its unambiguous specializations. - - For more info about specializations see - https://bazel.build/docs/configurable-attributes - """ - yield self - if self.arch is None: - for arch in Arch: - yield Platform(os=self.os, arch=arch, minor_version=self.minor_version) - if self.os is None: - for os in OS: - yield Platform(os=os, arch=self.arch, minor_version=self.minor_version) - if self.arch is None and self.os is None: - for os in OS: - for arch in Arch: - yield Platform(os=os, arch=arch, minor_version=self.minor_version) - def __lt__(self, other: Any) -> bool: """Add a comparison method, so that `sorted` returns the most specialized platforms first.""" if not isinstance(other, Platform) or other is None: @@ -153,24 +144,15 @@ def __lt__(self, other: Any) -> bool: def __str__(self) -> str: if self.minor_version is None: - if self.os is None and self.arch is None: - return "//conditions:default" - - if self.arch is None: - return f"@platforms//os:{self.os}" - else: - return f"{self.os}_{self.arch}" - - if self.arch is None and self.os is None: - return f"@//python/config_settings:is_python_3.{self.minor_version}" + return f"{self.os}_{self.arch}" - if self.arch is None: - return f"cp3{self.minor_version}_{self.os}_anyarch" + minor_version = self.minor_version + micro_version = self.micro_version - if self.os is None: - return f"cp3{self.minor_version}_anyos_{self.arch}" - - return f"cp3{self.minor_version}_{self.os}_{self.arch}" + if micro_version is None: + return f"cp3{minor_version}_{self.os}_{self.arch}" + else: + return f"cp3{minor_version}.{micro_version}_{self.os}_{self.arch}" @classmethod def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]: @@ -190,7 +172,17 @@ def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]: os, _, arch = tail.partition("_") arch = arch or "*" - minor_version = int(abi[len("cp3") :]) if abi else None + if abi: + tail = abi[len("cp3") :] + minor_version, _, micro_version = tail.partition(".") + minor_version = int(minor_version) + if micro_version == "": + micro_version = None + else: + micro_version = int(micro_version) + else: + minor_version = None + micro_version = None if arch != "*": ret.add( @@ -198,6 +190,7 @@ def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]: os=OS[os] if os != "*" else None, arch=Arch[arch], minor_version=minor_version, + micro_version=micro_version, ) ) @@ -206,6 +199,7 @@ def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]: cls.all( want_os=OS[os] if os != "*" else None, minor_version=minor_version, + micro_version=micro_version, ) ) @@ -282,7 +276,12 @@ def platform_machine(self) -> str: def env_markers(self, extra: str) -> Dict[str, str]: # If it is None, use the host version - minor_version = self.minor_version or host_interpreter_minor_version() + if self.minor_version is None: + minor, micro = host_interpreter_version() + else: + minor, micro = self.minor_version, self.micro_version + + micro = micro or 0 return { "extra": extra, @@ -292,12 +291,9 @@ def env_markers(self, extra: str) -> Dict[str, str]: "platform_system": self.platform_system, "platform_release": "", # unset "platform_version": "", # unset - "python_version": f"3.{minor_version}", - # FIXME @aignas 2024-01-14: is putting zero last a good idea? Maybe we should - # use `20` or something else to avoid having weird issues where the full version is used for - # matching and the author decides to only support 3.y.5 upwards. - "implementation_version": f"3.{minor_version}.0", - "python_full_version": f"3.{minor_version}.0", + "python_version": f"3.{minor}", + "implementation_version": f"3.{minor}.{micro}", + "python_full_version": f"3.{minor}.{micro}", # we assume that the following are the same as the interpreter used to setup the deps: # "implementation_name": "cpython" # "platform_python_implementation: "CPython", diff --git a/python/private/pypi/whl_installer/wheel.py b/python/private/pypi/whl_installer/wheel.py index d95b33a194..fce706acfb 100644 --- a/python/private/pypi/whl_installer/wheel.py +++ b/python/private/pypi/whl_installer/wheel.py @@ -27,7 +27,7 @@ from python.private.pypi.whl_installer.platform import ( Platform, - host_interpreter_minor_version, + host_interpreter_version, ) @@ -62,12 +62,13 @@ def __init__( """ self.name: str = Deps._normalize(name) self._platforms: Set[Platform] = platforms or set() - self._target_versions = {p.minor_version for p in platforms or {}} - self._default_minor_version = None - if platforms and len(self._target_versions) > 2: + self._target_versions = {(p.minor_version, p.micro_version) for p in platforms or {}} + if platforms and len(self._target_versions) > 1: # TODO @aignas 2024-06-23: enable this to be set via a CLI arg # for being more explicit. - self._default_minor_version = host_interpreter_minor_version() + self._default_minor_version, _ = host_interpreter_version() + else: + self._default_minor_version = None if None in self._target_versions and len(self._target_versions) > 2: raise ValueError( @@ -88,8 +89,13 @@ def __init__( # Then add all of the requirements in order self._deps: Set[str] = set() self._select: Dict[Platform, Set[str]] = defaultdict(set) + + reqs_by_name = {} for req in reqs: - self._add_req(req, want_extras) + reqs_by_name.setdefault(req.name, []).append(req) + + for reqs in reqs_by_name.values(): + self._add_req(reqs, want_extras) def _add(self, dep: str, platform: Optional[Platform]): dep = Deps._normalize(dep) @@ -123,50 +129,6 @@ def _add(self, dep: str, platform: Optional[Platform]): # Add the platform-specific dep self._select[platform].add(dep) - # Add the dep to specializations of the given platform if they - # exist in the select statement. - for p in platform.all_specializations(): - if p not in self._select: - continue - - self._select[p].add(dep) - - if len(self._select[platform]) == 1: - # We are adding a new item to the select and we need to ensure that - # existing dependencies from less specialized platforms are propagated - # to the newly added dependency set. - for p, deps in self._select.items(): - # Check if the existing platform overlaps with the given platform - if p == platform or platform not in p.all_specializations(): - continue - - self._select[platform].update(self._select[p]) - - def _maybe_add_common_dep(self, dep): - if len(self._target_versions) < 2: - return - - platforms = [Platform()] + [ - Platform(minor_version=v) for v in self._target_versions - ] - - # If the dep is targeting all target python versions, lets add it to - # the common dependency list to simplify the select statements. - for p in platforms: - if p not in self._select: - return - - if dep not in self._select[p]: - return - - # All of the python version-specific branches have the dep, so lets add - # it to the common deps. - self._deps.add(dep) - for p in platforms: - self._select[p].remove(dep) - if not self._select[p]: - self._select.pop(p) - @staticmethod def _normalize(name: str) -> str: return re.sub(r"[-_.]+", "_", name).lower() @@ -227,66 +189,40 @@ def _resolve_extras( return extras - def _add_req(self, req: Requirement, extras: Set[str]) -> None: - if req.marker is None: - self._add(req.name, None) - return + def _add_req(self, reqs: List[Requirement], extras: Set[str]) -> None: + platforms_to_add = set() + for req in reqs: + if req.marker is None: + self._add(req.name, None) + return - marker_str = str(req.marker) + for plat in self._platforms: + if plat in platforms_to_add: + # marker evaluation is more expensive than this check + continue - if not self._platforms: - if any(req.marker.evaluate({"extra": extra}) for extra in extras): - self._add(req.name, None) - return + added = False + for extra in extras: + if added: + break - # NOTE @aignas 2023-12-08: in order to have reasonable select statements - # we do have to have some parsing of the markers, so it begs the question - # if packaging should be reimplemented in Starlark to have the best solution - # for now we will implement it in Python and see what the best parsing result - # can be before making this decision. - match_os = any( - tag in marker_str - for tag in [ - "os_name", - "sys_platform", - "platform_system", - ] - ) - match_arch = "platform_machine" in marker_str - match_version = "version" in marker_str + if req.marker.evaluate(plat.env_markers(extra)): + platforms_to_add.add(plat) + added = True + break - if not (match_os or match_arch or match_version): - if any(req.marker.evaluate({"extra": extra}) for extra in extras): - self._add(req.name, None) + if len(platforms_to_add) == len(self._platforms): + # the dep is in all target platforms, let's just add it to the regular + # list + self._add(req.name, None) return - for plat in self._platforms: - if not any( - req.marker.evaluate(plat.env_markers(extra)) for extra in extras - ): - continue - - if match_arch and self._default_minor_version: + for plat in platforms_to_add: + if self._default_minor_version is not None: self._add(req.name, plat) - if plat.minor_version == self._default_minor_version: - self._add(req.name, Platform(plat.os, plat.arch)) - elif match_arch: - self._add(req.name, Platform(plat.os, plat.arch)) - elif match_os and self._default_minor_version: - self._add(req.name, Platform(plat.os, minor_version=plat.minor_version)) - if plat.minor_version == self._default_minor_version: - self._add(req.name, Platform(plat.os)) - elif match_os: - self._add(req.name, Platform(plat.os)) - elif match_version and self._default_minor_version: - self._add(req.name, Platform(minor_version=plat.minor_version)) - if plat.minor_version == self._default_minor_version: - self._add(req.name, Platform()) - elif match_version: - self._add(req.name, None) - # Merge to common if possible after processing all platforms - self._maybe_add_common_dep(req.name) + if self._default_minor_version is None or plat.minor_version == self._default_minor_version: + self._add(req.name, Platform(os = plat.os, arch = plat.arch)) def build(self) -> FrozenDeps: return FrozenDeps( diff --git a/tests/pypi/whl_installer/platform_test.py b/tests/pypi/whl_installer/platform_test.py index 2aeb4caa69..ad65650779 100644 --- a/tests/pypi/whl_installer/platform_test.py +++ b/tests/pypi/whl_installer/platform_test.py @@ -5,13 +5,13 @@ OS, Arch, Platform, - host_interpreter_minor_version, + host_interpreter_version, ) class MinorVersionTest(unittest.TestCase): def test_host(self): - host = host_interpreter_minor_version() + host = host_interpreter_version() self.assertIsNotNone(host) @@ -32,10 +32,14 @@ def test_can_get_specific_from_string(self): want = Platform(os=OS.linux, arch=Arch.x86_64, minor_version=3) self.assertEqual(want, got[0]) + got = Platform.from_string("cp33.0_linux_x86_64") + want = Platform(os=OS.linux, arch=Arch.x86_64, minor_version=3, micro_version=0) + self.assertEqual(want, got[0]) + def test_can_get_all_for_py_version(self): - cp39 = Platform.all(minor_version=9) + cp39 = Platform.all(minor_version=9, micro_version=0) self.assertEqual(21, len(cp39), f"Got {cp39}") - self.assertEqual(cp39, Platform.from_string("cp39_*")) + self.assertEqual(cp39, Platform.from_string("cp39.0_*")) def test_can_get_all_for_os(self): linuxes = Platform.all(OS.linux, minor_version=9) @@ -47,67 +51,6 @@ def test_can_get_all_for_os_for_host_python(self): self.assertEqual(7, len(linuxes)) self.assertEqual(linuxes, Platform.from_string("linux_*")) - def test_specific_version_specializations(self): - any_py33 = Platform(minor_version=3) - - # When - all_specializations = list(any_py33.all_specializations()) - - want = ( - [any_py33] - + [ - Platform(arch=arch, minor_version=any_py33.minor_version) - for arch in Arch - ] - + [Platform(os=os, minor_version=any_py33.minor_version) for os in OS] - + Platform.all(minor_version=any_py33.minor_version) - ) - self.assertEqual(want, all_specializations) - - def test_aarch64_specializations(self): - any_aarch64 = Platform(arch=Arch.aarch64) - all_specializations = list(any_aarch64.all_specializations()) - want = [ - Platform(os=None, arch=Arch.aarch64), - Platform(os=OS.linux, arch=Arch.aarch64), - Platform(os=OS.osx, arch=Arch.aarch64), - Platform(os=OS.windows, arch=Arch.aarch64), - ] - self.assertEqual(want, all_specializations) - - def test_linux_specializations(self): - any_linux = Platform(os=OS.linux) - all_specializations = list(any_linux.all_specializations()) - want = [ - Platform(os=OS.linux, arch=None), - Platform(os=OS.linux, arch=Arch.x86_64), - Platform(os=OS.linux, arch=Arch.x86_32), - Platform(os=OS.linux, arch=Arch.aarch64), - Platform(os=OS.linux, arch=Arch.ppc), - Platform(os=OS.linux, arch=Arch.ppc64le), - Platform(os=OS.linux, arch=Arch.s390x), - Platform(os=OS.linux, arch=Arch.arm), - ] - self.assertEqual(want, all_specializations) - - def test_osx_specializations(self): - any_osx = Platform(os=OS.osx) - all_specializations = list(any_osx.all_specializations()) - # NOTE @aignas 2024-01-14: even though in practice we would only have - # Python on osx aarch64 and osx x86_64, we return all arch posibilities - # to make the code simpler. - want = [ - Platform(os=OS.osx, arch=None), - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.x86_32), - Platform(os=OS.osx, arch=Arch.aarch64), - Platform(os=OS.osx, arch=Arch.ppc), - Platform(os=OS.osx, arch=Arch.ppc64le), - Platform(os=OS.osx, arch=Arch.s390x), - Platform(os=OS.osx, arch=Arch.arm), - ] - self.assertEqual(want, all_specializations) - def test_platform_sort(self): platforms = [ Platform(os=OS.linux, arch=None), diff --git a/tests/pypi/whl_installer/wheel_test.py b/tests/pypi/whl_installer/wheel_test.py index 404218e12b..6921fe6d3f 100644 --- a/tests/pypi/whl_installer/wheel_test.py +++ b/tests/pypi/whl_installer/wheel_test.py @@ -5,7 +5,7 @@ from python.private.pypi.whl_installer.platform import OS, Arch, Platform _HOST_INTERPRETER_FN = ( - "python.private.pypi.whl_installer.wheel.host_interpreter_minor_version" + "python.private.pypi.whl_installer.wheel.host_interpreter_version" ) @@ -20,108 +20,56 @@ def test_simple(self): self.assertEqual({}, got.deps_select) def test_can_add_os_specific_deps(self): - deps = wheel.Deps( - "foo", - requires_dist=[ - "bar", - "an_osx_dep; sys_platform=='darwin'", - "posix_dep; os_name=='posix'", - "win_dep; os_name=='nt'", - ], - platforms={ + for platforms in [ + { Platform(os=OS.linux, arch=Arch.x86_64), Platform(os=OS.osx, arch=Arch.x86_64), Platform(os=OS.osx, arch=Arch.aarch64), Platform(os=OS.windows, arch=Arch.x86_64), }, - ) - - got = deps.build() - - self.assertEqual(["bar"], got.deps) - self.assertEqual( { - "@platforms//os:linux": ["posix_dep"], - "@platforms//os:osx": ["an_osx_dep", "posix_dep"], - "@platforms//os:windows": ["win_dep"], - }, - got.deps_select, - ) - - def test_can_add_os_specific_deps_with_specific_python_version(self): - deps = wheel.Deps( - "foo", - requires_dist=[ - "bar", - "an_osx_dep; sys_platform=='darwin'", - "posix_dep; os_name=='posix'", - "win_dep; os_name=='nt'", - ], - platforms={ Platform(os=OS.linux, arch=Arch.x86_64, minor_version=8), Platform(os=OS.osx, arch=Arch.x86_64, minor_version=8), Platform(os=OS.osx, arch=Arch.aarch64, minor_version=8), Platform(os=OS.windows, arch=Arch.x86_64, minor_version=8), }, - ) - - got = deps.build() - - self.assertEqual(["bar"], got.deps) - self.assertEqual( { - "@platforms//os:linux": ["posix_dep"], - "@platforms//os:osx": ["an_osx_dep", "posix_dep"], - "@platforms//os:windows": ["win_dep"], - }, - got.deps_select, - ) - - def test_deps_are_added_to_more_specialized_platforms(self): - got = wheel.Deps( - "foo", - requires_dist=[ - "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", - "mac_dep; sys_platform=='darwin'", - ], - platforms={ - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.aarch64), + Platform( + os=OS.linux, arch=Arch.x86_64, minor_version=8, micro_version=1 + ), + Platform(os=OS.osx, arch=Arch.x86_64, minor_version=8, micro_version=1), + Platform( + os=OS.osx, arch=Arch.aarch64, minor_version=8, micro_version=1 + ), + Platform( + os=OS.windows, arch=Arch.x86_64, minor_version=8, micro_version=1 + ), }, - ).build() - - self.assertEqual( - wheel.FrozenDeps( - deps=[], - deps_select={ - "osx_aarch64": ["m1_dep", "mac_dep"], - "@platforms//os:osx": ["mac_dep"], - }, - ), - got, - ) - - def test_deps_from_more_specialized_platforms_are_propagated(self): - got = wheel.Deps( - "foo", - requires_dist=[ - "a_mac_dep; sys_platform=='darwin'", - "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", - ], - platforms={ - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.aarch64), - }, - ).build() - - self.assertEqual([], got.deps) - self.assertEqual( - { - "osx_aarch64": ["a_mac_dep", "m1_dep"], - "@platforms//os:osx": ["a_mac_dep"], - }, - got.deps_select, - ) + ]: + with self.subTest(): + deps = wheel.Deps( + "foo", + requires_dist=[ + "bar", + "an_osx_dep; sys_platform=='darwin'", + "posix_dep; os_name=='posix'", + "win_dep; os_name=='nt'", + ], + platforms=platforms, + ) + + got = deps.build() + + self.assertEqual(["bar"], got.deps) + self.assertEqual( + { + "linux_x86_64": ["posix_dep"], + "osx_aarch64": ["an_osx_dep", "posix_dep"], + "osx_x86_64": ["an_osx_dep", "posix_dep"], + "windows_x86_64": ["win_dep"], + }, + got.deps_select, + ) def test_non_platform_markers_are_added_to_common_deps(self): got = wheel.Deps( @@ -185,7 +133,7 @@ def test_self_dependencies_can_come_in_any_order(self): def test_can_get_deps_based_on_specific_python_version(self): requires_dist = [ "bar", - "baz; python_version < '3.8'", + "baz; python_full_version < '3.7.3'", "posix_dep; os_name=='posix' and python_version >= '3.8'", ] @@ -196,6 +144,15 @@ def test_can_get_deps_based_on_specific_python_version(self): Platform(os=OS.linux, arch=Arch.x86_64, minor_version=8), ], ).build() + py373_deps = wheel.Deps( + "foo", + requires_dist=requires_dist, + platforms=[ + Platform( + os=OS.linux, arch=Arch.x86_64, minor_version=7, micro_version=3 + ), + ], + ).build() py37_deps = wheel.Deps( "foo", requires_dist=requires_dist, @@ -206,11 +163,12 @@ def test_can_get_deps_based_on_specific_python_version(self): self.assertEqual(["bar", "baz"], py37_deps.deps) self.assertEqual({}, py37_deps.deps_select) - self.assertEqual(["bar"], py38_deps.deps) - self.assertEqual({"@platforms//os:linux": ["posix_dep"]}, py38_deps.deps_select) + self.assertEqual(["bar"], py373_deps.deps) + self.assertEqual({}, py37_deps.deps_select) + self.assertEqual(["bar", "posix_dep"], py38_deps.deps) + self.assertEqual({}, py38_deps.deps_select) - @mock.patch(_HOST_INTERPRETER_FN) - def test_no_version_select_when_single_version(self, mock_host_interpreter_version): + def test_no_version_select_when_single_version(self): requires_dist = [ "bar", "baz; python_version >= '3.8'", @@ -218,7 +176,6 @@ def test_no_version_select_when_single_version(self, mock_host_interpreter_versi "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", "arch_dep; platform_machine=='x86_64' and python_version >= '3.8'", ] - mock_host_interpreter_version.return_value = 7 self.maxDiff = None @@ -226,19 +183,19 @@ def test_no_version_select_when_single_version(self, mock_host_interpreter_versi "foo", requires_dist=requires_dist, platforms=[ - Platform(os=os, arch=Arch.x86_64, minor_version=minor) - for minor in [8] + Platform( + os=os, arch=Arch.x86_64, minor_version=minor, micro_version=micro + ) + for minor, micro in [(8, 4)] for os in [OS.linux, OS.windows] ], ) got = deps.build() - self.assertEqual(["bar", "baz"], got.deps) + self.assertEqual(["arch_dep", "bar", "baz"], got.deps) self.assertEqual( { - "@platforms//os:linux": ["posix_dep", "posix_dep_with_version"], - "linux_x86_64": ["arch_dep", "posix_dep", "posix_dep_with_version"], - "windows_x86_64": ["arch_dep"], + "linux_x86_64": ["posix_dep", "posix_dep_with_version"], }, got.deps_select, ) @@ -253,7 +210,7 @@ def test_can_get_version_select(self, mock_host_interpreter_version): "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", "arch_dep; platform_machine=='x86_64' and python_version < '3.8'", ] - mock_host_interpreter_version.return_value = 7 + mock_host_interpreter_version.return_value = (7, 4) self.maxDiff = None @@ -261,8 +218,10 @@ def test_can_get_version_select(self, mock_host_interpreter_version): "foo", requires_dist=requires_dist, platforms=[ - Platform(os=os, arch=Arch.x86_64, minor_version=minor) - for minor in [7, 8, 9] + Platform( + os=os, arch=Arch.x86_64, minor_version=minor, micro_version=micro + ) + for minor, micro in [(7, 4), (8, 8), (9, 8)] for os in [OS.linux, OS.windows] ], ) @@ -271,24 +230,20 @@ def test_can_get_version_select(self, mock_host_interpreter_version): self.assertEqual(["bar"], got.deps) self.assertEqual( { - "//conditions:default": ["baz"], - "@//python/config_settings:is_python_3.7": ["baz"], - "@//python/config_settings:is_python_3.8": ["baz_new"], - "@//python/config_settings:is_python_3.9": ["baz_new"], - "@platforms//os:linux": ["baz", "posix_dep"], - "cp37_linux_x86_64": ["arch_dep", "baz", "posix_dep"], - "cp37_windows_x86_64": ["arch_dep", "baz"], - "cp37_linux_anyarch": ["baz", "posix_dep"], - "cp38_linux_anyarch": [ + "cp37.4_linux_x86_64": ["arch_dep", "baz", "posix_dep"], + "cp37.4_windows_x86_64": ["arch_dep", "baz"], + "cp38.8_linux_x86_64": [ "baz_new", "posix_dep", "posix_dep_with_version", ], - "cp39_linux_anyarch": [ + "cp38.8_windows_x86_64": ["baz_new"], + "cp39.8_linux_x86_64": [ "baz_new", "posix_dep", "posix_dep_with_version", ], + "cp39.8_windows_x86_64": ["baz_new"], "linux_x86_64": ["arch_dep", "baz", "posix_dep"], "windows_x86_64": ["arch_dep", "baz"], }, @@ -304,7 +259,9 @@ def test_deps_spanning_all_target_py_versions_are_added_to_common( "baz (<2,>=1.11) ; python_version < '3.8'", "baz (<2,>=1.14) ; python_version >= '3.8'", ] - mock_host_version.return_value = 8 + mock_host_version.return_value = (8, 4) + + self.maxDiff = None deps = wheel.Deps( "foo", @@ -313,12 +270,12 @@ def test_deps_spanning_all_target_py_versions_are_added_to_common( ) got = deps.build() - self.assertEqual(["bar", "baz"], got.deps) self.assertEqual({}, got.deps_select) + self.assertEqual(["bar", "baz"], got.deps) @mock.patch(_HOST_INTERPRETER_FN) def test_deps_are_not_duplicated(self, mock_host_version): - mock_host_version.return_value = 7 + mock_host_version.return_value = (7, 4) # See an example in # https://files.pythonhosted.org/packages/76/9e/db1c2d56c04b97981c06663384f45f28950a73d9acf840c4006d60d0a1ff/opencv_python-4.9.0.80-cp37-abi3-win32.whl.metadata @@ -347,7 +304,7 @@ def test_deps_are_not_duplicated(self, mock_host_version): def test_deps_are_not_duplicated_when_encountering_platform_dep_first( self, mock_host_version ): - mock_host_version.return_value = 7 + mock_host_version.return_value = (7, 1) # Note, that we are sorting the incoming `requires_dist` and we need to ensure that we are not getting any # issues even if the platform-specific line comes first. @@ -356,15 +313,32 @@ def test_deps_are_not_duplicated_when_encountering_platform_dep_first( "bar >=0.5.0 ; python_version >= '3.9'", ] + self.maxDiff = None + deps = wheel.Deps( "foo", requires_dist=requires_dist, - platforms=Platform.from_string(["cp37_*", "cp310_*"]), + platforms=Platform.from_string( + [ + "cp37.1_linux_x86_64", + "cp37.1_linux_aarch64", + "cp310_linux_x86_64", + "cp310_linux_aarch64", + ] + ), ) got = deps.build() - self.assertEqual(["bar"], got.deps) - self.assertEqual({}, got.deps_select) + self.assertEqual([], got.deps) + self.assertEqual( + { + "cp310_linux_aarch64": ["bar"], + "cp310_linux_x86_64": ["bar"], + "cp37.1_linux_aarch64": ["bar"], + "linux_aarch64": ["bar"], + }, + got.deps_select, + ) if __name__ == "__main__": From 5b9d545220e5956e0686de91a14e6ded89df651a Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 29 Apr 2025 05:37:37 +0900 Subject: [PATCH 058/156] revert(pypi): use Python for marker eval and METADATA parsing (#2834) Summary: - Revert to using Python for marker evaluation during parsing of requirements (partial revert of #2692). - Use Python to parse whl METADATA. - Bugfix the new simpler algorithm and add a new unit test. Fixes #2830 --- CHANGELOG.md | 9 -- python/private/pypi/evaluate_markers.bzl | 62 ++++++++++ python/private/pypi/extension.bzl | 42 ++++++- .../pypi/generate_whl_library_build_bazel.bzl | 35 ++++-- python/private/pypi/parse_requirements.bzl | 4 +- python/private/pypi/pip_repository.bzl | 40 +++---- python/private/pypi/whl_installer/wheel.py | 33 ++++-- python/private/pypi/whl_library.bzl | 59 ++++------ tests/pypi/extension/extension_tests.bzl | 110 ++++++++++++++++++ ...generate_whl_library_build_bazel_tests.bzl | 2 - .../parse_requirements_tests.bzl | 2 +- tests/pypi/whl_installer/wheel_test.py | 2 +- 12 files changed, 304 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fc00ca25f..a8cac4c5cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,8 +103,6 @@ END_UNRELEASED_TEMPLATE * 3.12.9 * 3.13.2 * (pypi) Use `xcrun xcodebuild --showsdks` to find XCode root. -* (pypi) The `bzlmod` extension will now generate smaller lock files for when - using `experimental_index_url`. * (toolchains) Remove all but `3.8.20` versions of the Python `3.8` interpreter who has reached EOL. If users still need other versions of the `3.8` interpreter, please supply the URLs manually {bzl:obj}`python.toolchain` or {bzl:obj}`python_register_toolchains` calls. @@ -120,13 +118,6 @@ END_UNRELEASED_TEMPLATE [PR #2746](https://github.com/bazel-contrib/rules_python/pull/2746). * (rules) {attr}`py_binary.srcs` and {attr}`py_test.srcs` is no longer mandatory when `main_module` is specified (for `--bootstrap_impl=script`) -* (pypi) From now on the `Requires-Dist` from the wheel metadata is analysed in - the loading phase instead of repository rule phase giving better caching - performance when the target platforms are changed (e.g. target python - versions). This is preparatory work for stabilizing the cross-platform wheel - support. From now on the usage of `experimental_target_platforms` should be - avoided and the `requirements_by_platform` values should be instead used to - specify the target platforms for the given dependencies. [20250317]: https://github.com/astral-sh/python-build-standalone/releases/tag/20250317 diff --git a/python/private/pypi/evaluate_markers.bzl b/python/private/pypi/evaluate_markers.bzl index f966aa32be..191933596e 100644 --- a/python/private/pypi/evaluate_markers.bzl +++ b/python/private/pypi/evaluate_markers.bzl @@ -14,10 +14,21 @@ """A simple function that evaluates markers using a python interpreter.""" +load(":deps.bzl", "record_files") load(":pep508_env.bzl", "env") load(":pep508_evaluate.bzl", "evaluate") load(":pep508_platform.bzl", "platform_from_str") load(":pep508_requirement.bzl", "requirement") +load(":pypi_repo_utils.bzl", "pypi_repo_utils") + +# Used as a default value in a rule to ensure we fetch the dependencies. +SRCS = [ + # When the version, or any of the files in `packaging` package changes, + # this file will change as well. + record_files["pypi__packaging"], + Label("//python/private/pypi/requirements_parser:resolve_target_platforms.py"), + Label("//python/private/pypi/whl_installer:platform.py"), +] def evaluate_markers(requirements, python_version = None): """Return the list of supported platforms per requirements line. @@ -37,3 +48,54 @@ def evaluate_markers(requirements, python_version = None): ret.setdefault(req_string, []).append(platform) return ret + +def evaluate_markers_py(mrctx, *, requirements, python_interpreter, python_interpreter_target, srcs, logger = None): + """Return the list of supported platforms per requirements line. + + Args: + mrctx: repository_ctx or module_ctx. + requirements: list[str] of the requirement file lines to evaluate. + python_interpreter: str, path to the python_interpreter to use to + evaluate the env markers in the given requirements files. It will + be only called if the requirements files have env markers. This + should be something that is in your PATH or an absolute path. + python_interpreter_target: Label, same as python_interpreter, but in a + label format. + srcs: list[Label], the value of SRCS passed from the `rctx` or `mctx` to this function. + logger: repo_utils.logger or None, a simple struct to log diagnostic + messages. Defaults to None. + + Returns: + dict of string lists with target platforms + """ + if not requirements: + return {} + + in_file = mrctx.path("requirements_with_markers.in.json") + out_file = mrctx.path("requirements_with_markers.out.json") + mrctx.file(in_file, json.encode(requirements)) + + pypi_repo_utils.execute_checked( + mrctx, + op = "ResolveRequirementEnvMarkers({})".format(in_file), + python = pypi_repo_utils.resolve_python_interpreter( + mrctx, + python_interpreter = python_interpreter, + python_interpreter_target = python_interpreter_target, + ), + arguments = [ + "-m", + "python.private.pypi.requirements_parser.resolve_target_platforms", + in_file, + out_file, + ], + srcs = srcs, + environment = { + "PYTHONPATH": [ + Label("@pypi__packaging//:BUILD.bazel"), + Label("//:BUILD.bazel"), + ], + }, + logger = logger, + ) + return json.decode(mrctx.read(out_file)) diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index e9eba684f8..647407f16f 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -24,7 +24,7 @@ load("//python/private:repo_utils.bzl", "repo_utils") load("//python/private:semver.bzl", "semver") load("//python/private:version_label.bzl", "version_label") load(":attrs.bzl", "use_isolated") -load(":evaluate_markers.bzl", "evaluate_markers") +load(":evaluate_markers.bzl", "evaluate_markers_py", EVALUATE_MARKERS_SRCS = "SRCS") load(":hub_repository.bzl", "hub_repository", "whl_config_settings_to_json") load(":parse_requirements.bzl", "parse_requirements") load(":parse_whl_name.bzl", "parse_whl_name") @@ -71,6 +71,7 @@ def _create_whl_repos( whl_overrides, available_interpreters = INTERPRETER_LABELS, minor_mapping = MINOR_MAPPING, + evaluate_markers = evaluate_markers_py, get_index_urls = None): """create all of the whl repositories @@ -85,6 +86,7 @@ def _create_whl_repos( used during the `repository_rule` and must be always compatible with the host. minor_mapping: {type}`dict[str, str]` The dictionary needed to resolve the full python version used to parse package METADATA files. + evaluate_markers: the function used to evaluate the markers. Returns a {type}`struct` with the following attributes: whl_map: {type}`dict[str, list[struct]]` the output is keyed by the @@ -172,7 +174,28 @@ def _create_whl_repos( ), extra_pip_args = pip_attr.extra_pip_args, get_index_urls = get_index_urls, - evaluate_markers = evaluate_markers, + # NOTE @aignas 2024-08-02: , we will execute any interpreter that we find either + # in the PATH or if specified as a label. We will configure the env + # markers when evaluating the requirement lines based on the output + # from the `requirements_files_by_platform` which should have something + # similar to: + # { + # "//:requirements.txt": ["cp311_linux_x86_64", ...] + # } + # + # We know the target python versions that we need to evaluate the + # markers for and thus we don't need to use multiple python interpreter + # instances to perform this manipulation. This function should be executed + # only once by the underlying code to minimize the overhead needed to + # spin up a Python interpreter. + evaluate_markers = lambda module_ctx, requirements: evaluate_markers( + module_ctx, + requirements = requirements, + python_interpreter = pip_attr.python_interpreter, + python_interpreter_target = python_interpreter_target, + srcs = pip_attr._evaluate_markers_srcs, + logger = logger, + ), logger = logger, ) @@ -193,6 +216,7 @@ def _create_whl_repos( enable_implicit_namespace_pkgs = pip_attr.enable_implicit_namespace_pkgs, environment = pip_attr.environment, envsubst = pip_attr.envsubst, + experimental_target_platforms = pip_attr.experimental_target_platforms, group_deps = group_deps, group_name = group_name, pip_data_exclude = pip_attr.pip_data_exclude, @@ -281,6 +305,13 @@ def _whl_repos(*, requirement, whl_library_args, download_only, netrc, auth_patt args["urls"] = [distribution.url] args["sha256"] = distribution.sha256 args["filename"] = distribution.filename + args["experimental_target_platforms"] = [ + # Get rid of the version fot the target platforms because we are + # passing the interpreter any way. Ideally we should search of ways + # how to pass the target platforms through the hub repo. + p.partition("_")[2] + for p in requirement.target_platforms + ] # Pure python wheels or sdists may need to have a platform here target_platforms = None @@ -775,6 +806,13 @@ EXPERIMENTAL: this may be removed without notice. doc = """\ A dict of labels to wheel names that is typically generated by the whl_modifications. The labels are JSON config files describing the modifications. +""", + ), + "_evaluate_markers_srcs": attr.label_list( + default = EVALUATE_MARKERS_SRCS, + doc = """\ +The list of labels to use as SRCS for the marker evaluation code. This ensures that the +code will be re-evaluated when any of files in the default changes. """, ), }, **ATTRS) diff --git a/python/private/pypi/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl index 7988aca1c4..31c9d4da60 100644 --- a/python/private/pypi/generate_whl_library_build_bazel.bzl +++ b/python/private/pypi/generate_whl_library_build_bazel.bzl @@ -21,11 +21,14 @@ _RENDER = { "copy_files": render.dict, "data": render.list, "data_exclude": render.list, + "dependencies": render.list, + "dependencies_by_platform": lambda x: render.dict(x, value_repr = render.list), "entry_points": render.dict, "extras": render.list, "group_deps": render.list, "requires_dist": render.list, "srcs_exclude": render.list, + "tags": render.list, "target_platforms": lambda x: render.list(x) if x else "target_platforms", } @@ -37,7 +40,7 @@ _TEMPLATE = """\ package(default_visibility = ["//visibility:public"]) -whl_library_targets_from_requires( +{fn}( {kwargs} ) """ @@ -59,17 +62,28 @@ def generate_whl_library_build_bazel( A complete BUILD file as a string """ + fn = "whl_library_targets" + if kwargs.get("tags"): + # legacy path + unsupported_args = [ + "requires", + "metadata_name", + "metadata_version", + ] + else: + fn = "{}_from_requires".format(fn) + unsupported_args = [ + "dependencies", + "dependencies_by_platform", + ] + + for arg in unsupported_args: + if kwargs.get(arg): + fail("BUG, unsupported arg: '{}'".format(arg)) + loads = [ - """load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires")""", + """load("@rules_python//python/private/pypi:whl_library_targets.bzl", "{}")""".format(fn), ] - if not kwargs.setdefault("target_platforms", None): - dep_template = kwargs["dep_template"] - loads.append( - "load(\"{}\", \"{}\")".format( - dep_template.format(name = "", target = "config.bzl"), - "target_platforms", - ), - ) additional_content = [] if annotation: @@ -87,6 +101,7 @@ def generate_whl_library_build_bazel( [ _TEMPLATE.format( loads = "\n".join(loads), + fn = fn, kwargs = render.indent("\n".join([ "{} = {},".format(k, _RENDER.get(k, repr)(v)) for k, v in sorted(kwargs.items()) diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index 1cbf094f5c..5633328cf9 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -80,7 +80,7 @@ def parse_requirements( The second element is extra_pip_args should be passed to `whl_library`. """ - evaluate_markers = evaluate_markers or (lambda _: {}) + evaluate_markers = evaluate_markers or (lambda _ctx, _requirements: {}) options = {} requirements = {} for file, plats in requirements_by_platform.items(): @@ -156,7 +156,7 @@ def parse_requirements( # to do, we could use Python to parse the requirement lines and infer the # URL of the files to download things from. This should be important for # VCS package references. - env_marker_target_platforms = evaluate_markers(reqs_with_env_markers) + env_marker_target_platforms = evaluate_markers(ctx, reqs_with_env_markers) if logger: logger.debug(lambda: "Evaluated env markers from:\n{}\n\nTo:\n{}".format( reqs_with_env_markers, diff --git a/python/private/pypi/pip_repository.bzl b/python/private/pypi/pip_repository.bzl index b7ed1659d1..8ca94f7f9b 100644 --- a/python/private/pypi/pip_repository.bzl +++ b/python/private/pypi/pip_repository.bzl @@ -16,12 +16,11 @@ load("@bazel_skylib//lib:sets.bzl", "sets") load("//python/private:normalize_name.bzl", "normalize_name") -load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") +load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR") load("//python/private:text_util.bzl", "render") -load(":evaluate_markers.bzl", "evaluate_markers") +load(":evaluate_markers.bzl", "evaluate_markers_py", EVALUATE_MARKERS_SRCS = "SRCS") load(":parse_requirements.bzl", "host_platform", "parse_requirements", "select_requirement") load(":pip_repository_attrs.bzl", "ATTRS") -load(":pypi_repo_utils.bzl", "pypi_repo_utils") load(":render_pkg_aliases.bzl", "render_pkg_aliases") load(":requirements_files_by_platform.bzl", "requirements_files_by_platform") @@ -71,27 +70,7 @@ package(default_visibility = ["//visibility:public"]) exports_files(["requirements.bzl"]) """ -def _evaluate_markers(rctx, requirements, logger = None): - python_interpreter = _get_python_interpreter_attr(rctx) - stdout = pypi_repo_utils.execute_checked_stdout( - rctx, - op = "GetPythonVersionForMarkerEval", - python = python_interpreter, - arguments = [ - # Run the interpreter in isolated mode, this options implies -E, -P and -s. - # Ensures environment variables are ignored that are set in userspace, such as PYTHONPATH, - # which may interfere with this invocation. - "-I", - "-c", - "import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]}', end='')", - ], - srcs = [], - logger = logger, - ) - return evaluate_markers(requirements, python_version = stdout) - def _pip_repository_impl(rctx): - logger = repo_utils.logger(rctx) requirements_by_platform = parse_requirements( rctx, requirements_by_platform = requirements_files_by_platform( @@ -103,7 +82,13 @@ def _pip_repository_impl(rctx): extra_pip_args = rctx.attr.extra_pip_args, ), extra_pip_args = rctx.attr.extra_pip_args, - evaluate_markers = lambda requirements: _evaluate_markers(rctx, requirements, logger), + evaluate_markers = lambda rctx, requirements: evaluate_markers_py( + rctx, + requirements = requirements, + python_interpreter = rctx.attr.python_interpreter, + python_interpreter_target = rctx.attr.python_interpreter_target, + srcs = rctx.attr._evaluate_markers_srcs, + ), ) selected_requirements = {} options = None @@ -249,6 +234,13 @@ file](https://github.com/bazel-contrib/rules_python/blob/main/examples/pip_repos _template = attr.label( default = ":requirements.bzl.tmpl.workspace", ), + _evaluate_markers_srcs = attr.label_list( + default = EVALUATE_MARKERS_SRCS, + doc = """\ +The list of labels to use as SRCS for the marker evaluation code. This ensures that the +code will be re-evaluated when any of files in the default changes. +""", + ), **ATTRS ), doc = """Accepts a locked/compiled requirements file and installs the dependencies listed within. diff --git a/python/private/pypi/whl_installer/wheel.py b/python/private/pypi/whl_installer/wheel.py index fce706acfb..25003e6280 100644 --- a/python/private/pypi/whl_installer/wheel.py +++ b/python/private/pypi/whl_installer/wheel.py @@ -62,7 +62,9 @@ def __init__( """ self.name: str = Deps._normalize(name) self._platforms: Set[Platform] = platforms or set() - self._target_versions = {(p.minor_version, p.micro_version) for p in platforms or {}} + self._target_versions = { + (p.minor_version, p.micro_version) for p in platforms or {} + } if platforms and len(self._target_versions) > 1: # TODO @aignas 2024-06-23: enable this to be set via a CLI arg # for being more explicit. @@ -94,8 +96,8 @@ def __init__( for req in reqs: reqs_by_name.setdefault(req.name, []).append(req) - for reqs in reqs_by_name.values(): - self._add_req(reqs, want_extras) + for req_name, reqs in reqs_by_name.items(): + self._add_req(req_name, reqs, want_extras) def _add(self, dep: str, platform: Optional[Platform]): dep = Deps._normalize(dep) @@ -134,7 +136,7 @@ def _normalize(name: str) -> str: return re.sub(r"[-_.]+", "_", name).lower() def _resolve_extras( - self, reqs: List[Requirement], extras: Optional[Set[str]] + self, reqs: List[Requirement], want_extras: Optional[Set[str]] ) -> Set[str]: """Resolve extras which are due to depending on self[some_other_extra]. @@ -156,7 +158,7 @@ def _resolve_extras( # extras The empty string in the set is just a way to make the handling # of no extras and a single extra easier and having a set of {"", "foo"} # is equivalent to having {"foo"}. - extras = extras or {""} + extras: Set[str] = want_extras or {""} self_reqs = [] for req in reqs: @@ -189,13 +191,18 @@ def _resolve_extras( return extras - def _add_req(self, reqs: List[Requirement], extras: Set[str]) -> None: + def _add_req(self, req_name, reqs: List[Requirement], extras: Set[str]) -> None: platforms_to_add = set() for req in reqs: if req.marker is None: self._add(req.name, None) return + if not self._platforms: + if any(req.marker.evaluate({"extra": extra}) for extra in extras): + self._add(req.name, None) + return + for plat in self._platforms: if plat in platforms_to_add: # marker evaluation is more expensive than this check @@ -211,18 +218,24 @@ def _add_req(self, reqs: List[Requirement], extras: Set[str]) -> None: added = True break + if not self._platforms: + return + if len(platforms_to_add) == len(self._platforms): # the dep is in all target platforms, let's just add it to the regular # list - self._add(req.name, None) + self._add(req_name, None) return for plat in platforms_to_add: if self._default_minor_version is not None: - self._add(req.name, plat) + self._add(req_name, plat) - if self._default_minor_version is None or plat.minor_version == self._default_minor_version: - self._add(req.name, Platform(os = plat.os, arch = plat.arch)) + if ( + self._default_minor_version is None + or plat.minor_version == self._default_minor_version + ): + self._add(req_name, Platform(os=plat.os, arch=plat.arch)) def build(self) -> FrozenDeps: return FrozenDeps( diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 630dc8519f..0c09f7960a 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -15,18 +15,16 @@ "" load("//python/private:auth.bzl", "AUTH_ATTRS", "get_auth") -load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") load("//python/private:envsubst.bzl", "envsubst") load("//python/private:is_standalone_interpreter.bzl", "is_standalone_interpreter") load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") load(":attrs.bzl", "ATTRS", "use_isolated") load(":deps.bzl", "all_repo_names", "record_files") load(":generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel") -load(":parse_requirements.bzl", "host_platform") +load(":parse_whl_name.bzl", "parse_whl_name") load(":patch_whl.bzl", "patch_whl") -load(":pep508_requirement.bzl", "requirement") load(":pypi_repo_utils.bzl", "pypi_repo_utils") -load(":whl_metadata.bzl", "whl_metadata") +load(":whl_target_platforms.bzl", "whl_target_platforms") _CPPFLAGS = "CPPFLAGS" _COMMAND_LINE_TOOLS_PATH_SLUG = "commandlinetools" @@ -342,6 +340,21 @@ def _whl_library_impl(rctx): timeout = rctx.attr.timeout, ) + target_platforms = rctx.attr.experimental_target_platforms or [] + if target_platforms: + parsed_whl = parse_whl_name(whl_path.basename) + + # NOTE @aignas 2023-12-04: if the wheel is a platform specific wheel, we + # only include deps for that target platform + if parsed_whl.platform_tag != "any": + target_platforms = [ + p.target_platform + for p in whl_target_platforms( + platform_tag = parsed_whl.platform_tag, + abi_tag = parsed_whl.abi_tag.strip("tm"), + ) + ] + pypi_repo_utils.execute_checked( rctx, op = "whl_library.ExtractWheel({}, {})".format(rctx.attr.name, whl_path), @@ -349,7 +362,7 @@ def _whl_library_impl(rctx): arguments = args + [ "--whl-file", whl_path, - ], + ] + ["--platform={}".format(p) for p in target_platforms], srcs = rctx.attr._python_srcs, environment = environment, quiet = rctx.attr.quiet, @@ -384,45 +397,21 @@ def _whl_library_impl(rctx): ) entry_points[entry_point_without_py] = entry_point_script_name - if BZLMOD_ENABLED: - # The following attributes are unset on bzlmod and we pass data through - # the hub via load statements. - default_python_version = None - target_platforms = [] - else: - # NOTE @aignas 2025-04-16: if BZLMOD_ENABLED, we should use - # DEFAULT_PYTHON_VERSION since platforms always come with the actual - # python version otherwise we should use the version of the interpreter - # here. In WORKSPACE `multi_pip_parse` is using an interpreter for each - # `pip_parse` invocation, so we will have the host target platform - # only. Even if somebody would change the code to support - # `experimental_target_platforms`, they would be for a single python - # version. Hence, using the `default_python_version` that we get from the - # interpreter is correct. Hence, we unset the argument if we are on bzlmod. - default_python_version = metadata["python_version"] - target_platforms = rctx.attr.experimental_target_platforms or [host_platform(rctx)] - - metadata = whl_metadata( - install_dir = rctx.path("site-packages"), - read_fn = rctx.read, - logger = logger, - ) - build_file_contents = generate_whl_library_build_bazel( name = whl_path.basename, - metadata_name = metadata.name, - metadata_version = metadata.version, - requires_dist = metadata.requires_dist, dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix), entry_points = entry_points, - target_platforms = target_platforms, - default_python_version = default_python_version, # TODO @aignas 2025-04-14: load through the hub: + dependencies = metadata["deps"], + dependencies_by_platform = metadata["deps_by_platform"], annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), data_exclude = rctx.attr.pip_data_exclude, - extras = requirement(rctx.attr.requirement).extras, group_deps = rctx.attr.group_deps, group_name = rctx.attr.group_name, + tags = [ + "pypi_name={}".format(metadata["name"]), + "pypi_version={}".format(metadata["version"]), + ], ) rctx.file("BUILD.bazel", build_file_contents) diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index 5de3bb58d3..1cd6869c84 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -136,6 +136,7 @@ def _parse( parallel_download = False, experimental_index_url_overrides = {}, simpleapi_skip = simpleapi_skip, + _evaluate_markers_srcs = [], **kwargs ) @@ -273,6 +274,14 @@ torch==2.4.1 ; platform_machine != 'x86_64' \ "python_3_15_host": "unit_test_interpreter_target", }, minor_mapping = {"3.15": "3.15.19"}, + evaluate_markers = lambda _, requirements, **__: { + key: [ + platform + for platform in platforms + if ("x86_64" in platform and "platform_machine ==" in key) or ("x86_64" not in platform and "platform_machine !=" in key) + ] + for key, platforms in requirements.items() + }, ) pypi.exposed_packages().contains_exactly({"pypi": ["torch"]}) @@ -397,6 +406,15 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ }, minor_mapping = {"3.12": "3.12.19"}, simpleapi_download = mocksimpleapi_download, + evaluate_markers = lambda _, requirements, **__: { + # todo once 2692 is merged, this is going to be easier to test. + key: [ + platform + for platform in platforms + if ("x86_64" in platform and "platform_machine ==" in key) or ("x86_64" not in platform and "platform_machine !=" in key) + ] + for key, platforms in requirements.items() + }, ) pypi.exposed_packages().contains_exactly({"pypi": ["torch"]}) @@ -440,6 +458,11 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ pypi.whl_libraries().contains_exactly({ "pypi_312_torch_cp312_cp312_linux_x86_64_8800deef": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_x86_64", + "osx_x86_64", + "windows_x86_64", + ], "filename": "torch-2.4.1+cpu-cp312-cp312-linux_x86_64.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "torch==2.4.1+cpu", @@ -448,6 +471,13 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ }, "pypi_312_torch_cp312_cp312_manylinux_2_17_aarch64_36109432": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "osx_aarch64", + ], "filename": "torch-2.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "torch==2.4.1", @@ -456,6 +486,11 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ }, "pypi_312_torch_cp312_cp312_win_amd64_3a570e5c": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_x86_64", + "osx_x86_64", + "windows_x86_64", + ], "filename": "torch-2.4.1+cpu-cp312-cp312-win_amd64.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "torch==2.4.1+cpu", @@ -464,6 +499,13 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ }, "pypi_312_torch_cp312_none_macosx_11_0_arm64_72b484d5": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "osx_aarch64", + ], "filename": "torch-2.4.1-cp312-none-macosx_11_0_arm64.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "torch==2.4.1", @@ -751,6 +793,16 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef pypi.whl_libraries().contains_exactly({ "pypi_315_any_name": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "linux_x86_64", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ], "extra_pip_args": ["--extra-args-for-sdist-building"], "filename": "any-name.tar.gz", "python_interpreter_target": "unit_test_interpreter_target", @@ -760,6 +812,16 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }, "pypi_315_direct_without_sha_0_0_1_py3_none_any": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "linux_x86_64", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ], "filename": "direct_without_sha-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "direct_without_sha==0.0.1 @ example-direct.org/direct_without_sha-0.0.1-py3-none-any.whl", @@ -780,6 +842,16 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }, "pypi_315_simple_py3_none_any_deadb00f": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "linux_x86_64", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ], "filename": "simple-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "simple==0.0.1", @@ -788,6 +860,16 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }, "pypi_315_simple_sdist_deadbeef": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "linux_x86_64", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ], "extra_pip_args": ["--extra-args-for-sdist-building"], "filename": "simple-0.0.1.tar.gz", "python_interpreter_target": "unit_test_interpreter_target", @@ -797,6 +879,16 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }, "pypi_315_some_pkg_py3_none_any_deadbaaf": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "linux_x86_64", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ], "filename": "some_pkg-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "some_pkg==0.0.1 @ example-direct.org/some_pkg-0.0.1-py3-none-any.whl --hash=sha256:deadbaaf", @@ -805,6 +897,16 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }, "pypi_315_some_py3_none_any_deadb33f": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "linux_x86_64", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ], "filename": "some-other-pkg-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "some_other_pkg==0.0.1", @@ -856,6 +958,14 @@ optimum[onnxruntime-gpu]==1.17.1 ; sys_platform == 'linux' "python_3_15_host": "unit_test_interpreter_target", }, minor_mapping = {"3.15": "3.15.19"}, + evaluate_markers = lambda _, requirements, **__: { + key: [ + platform + for platform in platforms + if ("darwin" in key and "osx" in platform) or ("linux" in key and "linux" in platform) + ] + for key, platforms in requirements.items() + }, ) pypi.exposed_packages().contains_exactly({"pypi": []}) diff --git a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl index 7bd19b65c1..83be7395d4 100644 --- a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl +++ b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl @@ -86,7 +86,6 @@ _tests.append(_test_all) def _test_all_with_loads(env): want = """\ load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires") -load("@pypi//:config.bzl", "target_platforms") package(default_visibility = ["//visibility:public"]) @@ -119,7 +118,6 @@ whl_library_targets_from_requires( "qux", ], srcs_exclude = ["srcs_exclude_all"], - target_platforms = target_platforms, ) # SOMETHING SPECIAL AT THE END diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index c50482127b..723bb605ce 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -458,7 +458,7 @@ def _test_select_requirement_none_platform(env): _tests.append(_test_select_requirement_none_platform) def _test_env_marker_resolution(env): - def _mock_eval_markers(input): + def _mock_eval_markers(_, input): ret = { "foo[extra]==0.0.1 ;marker --hash=sha256:deadbeef": ["cp311_windows_x86_64"], } diff --git a/tests/pypi/whl_installer/wheel_test.py b/tests/pypi/whl_installer/wheel_test.py index 6921fe6d3f..3599fd1868 100644 --- a/tests/pypi/whl_installer/wheel_test.py +++ b/tests/pypi/whl_installer/wheel_test.py @@ -11,7 +11,7 @@ class DepsTest(unittest.TestCase): def test_simple(self): - deps = wheel.Deps("foo", requires_dist=["bar"]) + deps = wheel.Deps("foo", requires_dist=["bar", 'baz; extra=="foo"']) got = deps.build() From 1c35e4c84674ce25c9d9963125d335258f257ce7 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 28 Apr 2025 20:07:31 -0700 Subject: [PATCH 059/156] feat: implement less/greater operators for string for env marker evaluation (#2827) Right now, if two strings are compared, it results in an error. Per spec, strings are suppose to "use the python behavior". Starlark is going to use Java semantics underneath, but it should behave close enough for the (almost exclusively) ASCII input that will be used. Work towards https://github.com/bazel-contrib/rules_python/issues/2826 --- python/private/pypi/pep508_evaluate.bzl | 8 ++++++++ tests/pypi/pep508/evaluate_tests.bzl | 22 ++++++++++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/python/private/pypi/pep508_evaluate.bzl b/python/private/pypi/pep508_evaluate.bzl index f8ef553034..70840c76c6 100644 --- a/python/private/pypi/pep508_evaluate.bzl +++ b/python/private/pypi/pep508_evaluate.bzl @@ -344,6 +344,14 @@ def _env_expr(left, op, right): return left in right elif op == "not in": return left not in right + elif op == "<": + return left < right + elif op == "<=": + return left <= right + elif op == ">": + return left > right + elif op == ">=": + return left >= right else: return fail("TODO: op unsupported: '{}'".format(op)) diff --git a/tests/pypi/pep508/evaluate_tests.bzl b/tests/pypi/pep508/evaluate_tests.bzl index 14e5e40b43..303c167900 100644 --- a/tests/pypi/pep508/evaluate_tests.bzl +++ b/tests/pypi/pep508/evaluate_tests.bzl @@ -68,18 +68,28 @@ def _evaluate_non_version_env_tests(env): # When for input, want in { - "{} == 'osx'".format(var_name): True, - "{} != 'osx'".format(var_name): False, - "'osx' == {}".format(var_name): True, "'osx' != {}".format(var_name): False, - "'x' in {}".format(var_name): True, + "'osx' < {}".format(var_name): False, + "'osx' <= {}".format(var_name): True, + "'osx' == {}".format(var_name): True, + "'osx' >= {}".format(var_name): True, "'w' not in {}".format(var_name): True, - }.items(): # buildifier: @unsorted-dict-items + "'x' in {}".format(var_name): True, + "{} != 'osx'".format(var_name): False, + "{} < 'osx'".format(var_name): False, + "{} <= 'osx'".format(var_name): True, + "{} == 'osx'".format(var_name): True, + "{} > 'osx'".format(var_name): False, + "{} >= 'osx'".format(var_name): True, + }.items(): got = evaluate( input, env = marker_env, ) - env.expect.that_bool(got).equals(want) + env.expect.where( + expr = input, + env = marker_env, + ).that_bool(got).equals(want) # Check that the non-strict eval gives us back the input when no # env is supplied. From 704ecdd835c8a79ac415c81567eae5785df4b7e3 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 28 Apr 2025 20:07:57 -0700 Subject: [PATCH 060/156] docs: doc version when RULES_PYTHON_ENABLE_PYSTAR was introduced (#2838) While figuring out an upgrade from an old rules_python version, I had to look up when the environment variable first became available. Also note what version it defaulted to 1. --- docs/environment-variables.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 9500fa8295..49fdf766f6 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -46,11 +46,19 @@ When `1`, the rules_python will warn users about deprecated functionality that w be removed in a subsequent major `rules_python` version. Defaults to `0` if unset. ::: -:::{envvar} RULES_PYTHON_ENABLE_PYSTAR +::::{envvar} RULES_PYTHON_ENABLE_PYSTAR When `1`, the rules_python Starlark implementation of the core rules is used -instead of the Bazel-builtin rules. Note this requires Bazel 7+. +instead of the Bazel-builtin rules. Note this requires Bazel 7+. Defaults +to `1`. + +:::{versionadded} 0.26.0 +Defaults to `0` if unspecified. +::: +:::{versionchanged} 0.40.0 +The default became `1` if unspecified ::: +:::: ::::{envvar} RULES_PYTHON_EXTRACT_ROOT From a79bbfaece3e41f361b7d5baf89aec269184eb4d Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:52:46 +0900 Subject: [PATCH 061/156] fix(pypi): handle more URL patterns for requirement sources (#2843) Summary: - Better handle git references for sdists. - Better handle direct whl references. - Add an extra test that turned out to be not needed in the end, but I left it to increase the code coverage. Work towards #2363 Fixes #2828 --- python/private/pypi/parse_requirements.bzl | 5 ++ .../index_sources/index_sources_tests.bzl | 14 ++++- .../parse_requirements_tests.bzl | 59 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index 5633328cf9..1583c89199 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -285,12 +285,17 @@ def _add_dists(*, requirement, index_urls, logger = None): if requirement.srcs.url: url = requirement.srcs.url _, _, filename = url.rpartition("/") + filename, _, _ = filename.partition("#sha256=") if "." not in filename: # detected filename has no extension, it might be an sdist ref # TODO @aignas 2025-04-03: should be handled if the following is fixed: # https://github.com/bazel-contrib/rules_python/issues/2363 return [], None + if "@" in filename: + # this is most likely foo.git@git_sha, skip special handling of these + return [], None + direct_url_dist = struct( url = url, filename = filename, diff --git a/tests/pypi/index_sources/index_sources_tests.bzl b/tests/pypi/index_sources/index_sources_tests.bzl index ffeed87a7b..9d12bc6399 100644 --- a/tests/pypi/index_sources/index_sources_tests.bzl +++ b/tests/pypi/index_sources/index_sources_tests.bzl @@ -21,38 +21,50 @@ _tests = [] def _test_no_simple_api_sources(env): inputs = { + "foo @ git+https://github.com/org/foo.git@deadbeef": struct( + requirement = "foo @ git+https://github.com/org/foo.git@deadbeef", + marker = "", + url = "git+https://github.com/org/foo.git@deadbeef", + shas = [], + version = "", + ), "foo==0.0.1": struct( requirement = "foo==0.0.1", marker = "", url = "", + version = "0.0.1", ), "foo==0.0.1 @ https://someurl.org": struct( requirement = "foo==0.0.1 @ https://someurl.org", marker = "", url = "https://someurl.org", + version = "0.0.1", ), "foo==0.0.1 @ https://someurl.org/package.whl": struct( requirement = "foo==0.0.1 @ https://someurl.org/package.whl", marker = "", url = "https://someurl.org/package.whl", + version = "0.0.1", ), "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef": struct( requirement = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", marker = "", url = "https://someurl.org/package.whl", shas = ["deadbeef"], + version = "0.0.1", ), "foo==0.0.1 @ https://someurl.org/package.whl; python_version < \"2.7\"\\ --hash=sha256:deadbeef": struct( requirement = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", marker = "python_version < \"2.7\"", url = "https://someurl.org/package.whl", shas = ["deadbeef"], + version = "0.0.1", ), } for input, want in inputs.items(): got = index_sources(input) env.expect.that_collection(got.shas).contains_exactly(want.shas if hasattr(want, "shas") else []) - env.expect.that_str(got.version).equals("0.0.1") + env.expect.that_str(got.version).equals(want.version) env.expect.that_str(got.requirement).equals(want.requirement) env.expect.that_str(got.requirement_line).equals(got.requirement) env.expect.that_str(got.marker).equals(want.marker) diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index 723bb605ce..c5b24870ea 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -30,12 +30,16 @@ foo[extra] @ https://some-url/package.whl bar @ https://example.org/bar-1.0.whl --hash=sha256:deadbeef baz @ https://test.com/baz-2.0.whl; python_version < "3.8" --hash=sha256:deadb00f qux @ https://example.org/qux-1.0.tar.gz --hash=sha256:deadbe0f +torch @ https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=5b6ae523bfb67088a17ca7734d131548a2e60346c622621e4248ed09dd0790cc """, "requirements_extra_args": """\ --index-url=example.org foo[extra]==0.0.1 \ --hash=sha256:deadbeef +""", + "requirements_git": """ +foo @ git+https://github.com/org/foo.git@deadbeef """, "requirements_linux": """\ foo==0.0.3 --hash=sha256:deadbaaf @@ -232,6 +236,31 @@ def _test_direct_urls(env): whls = [], ), ], + "torch": [ + struct( + distribution = "torch", + extra_pip_args = [], + is_exposed = True, + sdist = None, + srcs = struct( + marker = "", + requirement = "torch @ https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=5b6ae523bfb67088a17ca7734d131548a2e60346c622621e4248ed09dd0790cc", + requirement_line = "torch @ https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=5b6ae523bfb67088a17ca7734d131548a2e60346c622621e4248ed09dd0790cc", + shas = [], + url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=5b6ae523bfb67088a17ca7734d131548a2e60346c622621e4248ed09dd0790cc", + version = "", + ), + target_platforms = ["linux_x86_64"], + whls = [ + struct( + filename = "torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl", + sha256 = "", + url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=5b6ae523bfb67088a17ca7734d131548a2e60346c622621e4248ed09dd0790cc", + yanked = False, + ), + ], + ), + ], }) _tests.append(_test_direct_urls) @@ -623,6 +652,36 @@ def _test_optional_hash(env): _tests.append(_test_optional_hash) +def _test_git_sources(env): + got = parse_requirements( + ctx = _mock_ctx(), + requirements_by_platform = { + "requirements_git": ["linux_x86_64"], + }, + ) + env.expect.that_dict(got).contains_exactly({ + "foo": [ + struct( + distribution = "foo", + extra_pip_args = [], + is_exposed = True, + sdist = None, + srcs = struct( + marker = "", + requirement = "foo @ git+https://github.com/org/foo.git@deadbeef", + requirement_line = "foo @ git+https://github.com/org/foo.git@deadbeef", + shas = [], + url = "git+https://github.com/org/foo.git@deadbeef", + version = "", + ), + target_platforms = ["linux_x86_64"], + whls = [], + ), + ], + }) + +_tests.append(_test_git_sources) + def parse_requirements_test_suite(name): """Create the test suite. From 189e30df4001d34aba590e0267d3e5f72e6d8b19 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 29 Apr 2025 10:08:37 -0700 Subject: [PATCH 062/156] docs: document some of our project styles/conventions (#2816) Spurred by the discussion to converge on using `.` to separate generated targets, I wrote down some of the conventions we've adopted. --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- .editorconfig | 17 +++++++++++++++++ CONTRIBUTING.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..26bb52ffac --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Set default charset +[*] +charset = utf-8 + +# Line width +[*] +max_line_length = 100 + +# 4 space indentation +[*.{py,bzl}] +indent_style = space +indent_size = 4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 17558e1b23..b087119dc6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -173,6 +173,55 @@ The `legacy_foo` arg was removed ::: ``` +## Style and idioms + +For the most part, we just accept whatever the code formatters do, so there +isn't much style to enforce. + +Some miscellanous style, idioms, and conventions we have are: + +### Markdown/Sphinx Style + +* Use colons for prose sections of text, e.g. `:::{note}`, not backticks. +* Use backticks for code blocks. +* Max line length: 100. + +### BUILD/bzl Style + +* When a macro generates public targets, use a dot (`.`) to separate the + user-provided name from the generted name. e.g. `foo(name="x")` generates + `x.test`. The `.` is our convention to communicate that it's a generated + target, and thus one should look for `name="x"` when searching for the + definition. +* The different build phases shouldn't load code that defines objects that + aren't valid for their phase. e.g. + * The bzlmod phase shouldn't load code defining regular rules or providers. + * The repository phase shouldn't load code defining module extensions, regular + rules, or providers. + * The loading phase shouldn't load code defining module extensions or + repository rules. + * Loading utility libraries or generic code is OK, but should strive to load + code that is usable for its phase. e.g. loading-phase code shouldn't + load utility code that is predominately only usable to the bzlmod phase. +* Providers should be in their own files. This allows implementing a custom rule + that implements the provider without loading a specific implementation. +* One rule per file is preferred, but not required. The goal is that defining an + e.g. library shouldn't incur loading all the code for binaries, tests, + packaging, etc; things that may be niche or uncommonly used. +* Separate files should be used to expose public APIs. This ensures our public + API is well defined and prevents accidentally exposing a package-private + symbol as a public symbol. + + :::{note} + The public API file's docstring becomes part of the user-facing docs. That + file's docstring must be used for module-level API documentation. + ::: +* Repository rules should have name ending in `_repo`. This helps distinguish + them from regular rules. +* Each bzlmod extension, the "X" of `use_repo("//foo:foo.bzl", "X")` should be + in its own file. The path given in the `use_repo()` expression is the identity + Bazel uses and cannot be changed. + ## Generated files Some checked-in files are generated and need to be updated when a new PR is From 76b221e668d7038b8a069bf44b81682876dbea38 Mon Sep 17 00:00:00 2001 From: Vein Kong Date: Thu, 1 May 2025 23:36:56 -0700 Subject: [PATCH 063/156] fix: requires_file preserves extras that package depends on (#2807) When requirements are passed in through `requires_file` the extras are not preserved. eg if the contents of requires file is `example[extras]==1.1.1`, bazel will currently write to the METADATA file `Requires-Dist: example==1.1.1`. This PR attempts to fix that by adding that back if there are any extras. The expected output should be `Requires-Dist: example[extras]==1.1.1` --- .bazelrc | 4 +-- CHANGELOG.md | 2 ++ examples/wheel/BUILD.bazel | 29 +++++++++++++++++++++ examples/wheel/wheel_test.py | 50 ++++++++++++++++++++++++++++++++++++ tools/wheelmaker.py | 7 ++--- 5 files changed, 87 insertions(+), 5 deletions(-) diff --git a/.bazelrc b/.bazelrc index 4e6f2fa187..d2e0721526 100644 --- a/.bazelrc +++ b/.bazelrc @@ -4,8 +4,8 @@ # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it) # To update these lines, execute # `bazel run @rules_bazel_integration_test//tools:update_deleted_packages` -build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma -query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma +build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma +query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma test --test_output=errors diff --git a/CHANGELOG.md b/CHANGELOG.md index a8cac4c5cd..19fe636bc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ END_UNRELEASED_TEMPLATE * The {obj}`//python/runtime_env_toolchains:all` toolchain now works with it. * (rules) Better handle flakey platform.win32_ver() calls by calling them multiple times. +* (tools/wheelmaker.py) Extras are now preserved in Requires-Dist metadata when using requires_file + to specify the requirements. {#v0-0-0-added} ### Added diff --git a/examples/wheel/BUILD.bazel b/examples/wheel/BUILD.bazel index b434e67405..e52e0fc3a3 100644 --- a/examples/wheel/BUILD.bazel +++ b/examples/wheel/BUILD.bazel @@ -313,6 +313,17 @@ wheel; python_version == "3.11" or python_version == "3.12" # Example comment """.splitlines(), ) +write_file( + name = "requires_dist_depends_on_extras_file", + out = "requires_dist_depends_on_extras.txt", + content = """\ +# Requirements file +--index-url https://pypi.com + +extra_requires[example]==0.0.1 +""".splitlines(), +) + # py_wheel can use text files to specify their requirements. This # can be convenient for users of `compile_pip_requirements` who have # granular `requirements.in` files per package. This target shows @@ -374,6 +385,22 @@ py_wheel( deps = [":example_pkg"], ) +py_wheel( + name = "requires_dist_depends_on_extras", + distribution = "requires_dist_depends_on_extras", + requires = [ + "extra_requires[example]==0.0.1", + ], + version = "0.0.1", +) + +py_wheel( + name = "requires_dist_depends_on_extras_using_file", + distribution = "requires_dist_depends_on_extras_using_file", + requires_file = ":requires_dist_depends_on_extras.txt", + version = "0.0.1", +) + py_test( name = "wheel_test", srcs = ["wheel_test.py"], @@ -391,6 +418,8 @@ py_test( ":minimal_with_py_package", ":python_abi3_binary_wheel", ":python_requires_in_a_package", + ":requires_dist_depends_on_extras", + ":requires_dist_depends_on_extras_using_file", ":requires_files", ":use_rule_with_dir_in_outs", ], diff --git a/examples/wheel/wheel_test.py b/examples/wheel/wheel_test.py index 35803da742..43e56cfc17 100644 --- a/examples/wheel/wheel_test.py +++ b/examples/wheel/wheel_test.py @@ -565,6 +565,56 @@ def test_extra_requires(self): requires, ) + def test_requires_dist_depends_on_extras(self): + filename = self._get_path("requires_dist_depends_on_extras-0.0.1-py3-none-any.whl") + + with zipfile.ZipFile(filename) as zf: + self.assertAllEntriesHasReproducibleMetadata(zf) + metadata_file = None + for f in zf.namelist(): + if os.path.basename(f) == "METADATA": + metadata_file = f + self.assertIsNotNone(metadata_file) + + requires = [] + with zf.open(metadata_file) as fp: + for line in fp: + if line.startswith(b"Requires-Dist:"): + requires.append(line.decode("utf-8").strip()) + + print(requires) + self.assertEqual( + [ + "Requires-Dist: extra_requires[example]==0.0.1", + ], + requires, + ) + + def test_requires_dist_depends_on_extras_file(self): + filename = self._get_path("requires_dist_depends_on_extras_using_file-0.0.1-py3-none-any.whl") + + with zipfile.ZipFile(filename) as zf: + self.assertAllEntriesHasReproducibleMetadata(zf) + metadata_file = None + for f in zf.namelist(): + if os.path.basename(f) == "METADATA": + metadata_file = f + self.assertIsNotNone(metadata_file) + + requires = [] + with zf.open(metadata_file) as fp: + for line in fp: + if line.startswith(b"Requires-Dist:"): + requires.append(line.decode("utf-8").strip()) + + print(requires) + self.assertEqual( + [ + "Requires-Dist: extra_requires[example]==0.0.1", + ], + requires, + ) + if __name__ == "__main__": unittest.main() diff --git a/tools/wheelmaker.py b/tools/wheelmaker.py index 28ec039741..de584650d1 100644 --- a/tools/wheelmaker.py +++ b/tools/wheelmaker.py @@ -562,13 +562,14 @@ def main() -> None: def get_new_requirement_line(reqs_text, extra): req = Requirement(reqs_text.strip()) + req_extra_deps = f"[{','.join(req.extras)}]" if req.extras else "" if req.marker: if extra: - return f"Requires-Dist: {req.name}{req.specifier}; ({req.marker}) and {extra}" + return f"Requires-Dist: {req.name}{req_extra_deps}{req.specifier}; ({req.marker}) and {extra}" else: - return f"Requires-Dist: {req.name}{req.specifier}; {req.marker}" + return f"Requires-Dist: {req.name}{req_extra_deps}{req.specifier}; {req.marker}" else: - return f"Requires-Dist: {req.name}{req.specifier}; {extra}".strip(" ;") + return f"Requires-Dist: {req.name}{req_extra_deps}{req.specifier}; {extra}".strip(" ;") for meta_line in metadata.splitlines(): if not meta_line.startswith("Requires-Dist: "): From 8e76bd451a29d2728008a7094e850141a172cfe9 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 2 May 2025 14:46:20 -0700 Subject: [PATCH 064/156] refactor: add rule to do analysis time evaluation of environment markers (#2832) wip/prototype to help bootstrap the impl of an analysis-time flag that evaluates the pep508 dep specs Creating a PR to make collab easier (maintainers can directly edit) TODO: * Remove the todo markers after discussion Work towards https://github.com/bazel-contrib/rules_python/issues/2826 --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- python/private/pypi/env_marker_setting.bzl | 186 ++++++++++++++++++ python/private/pypi/pep508_env.bzl | 94 ++++++++- tests/pypi/env_marker_setting/BUILD.bazel | 5 + .../env_marker_setting_tests.bzl | 69 +++++++ 4 files changed, 351 insertions(+), 3 deletions(-) create mode 100644 python/private/pypi/env_marker_setting.bzl create mode 100644 tests/pypi/env_marker_setting/BUILD.bazel create mode 100644 tests/pypi/env_marker_setting/env_marker_setting_tests.bzl diff --git a/python/private/pypi/env_marker_setting.bzl b/python/private/pypi/env_marker_setting.bzl new file mode 100644 index 0000000000..bbc59ab110 --- /dev/null +++ b/python/private/pypi/env_marker_setting.bzl @@ -0,0 +1,186 @@ +"""Implement a flag for matching the dependency specifiers at analysis time.""" + +load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") +load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") +load( + ":pep508_env.bzl", + "env_aliases", + "os_name_select_map", + "platform_machine_select_map", + "platform_system_select_map", + "sys_platform_select_map", +) +load(":pep508_evaluate.bzl", "evaluate") + +# Use capitals to hint its not an actual boolean type. +_ENV_MARKER_TRUE = "TRUE" +_ENV_MARKER_FALSE = "FALSE" + +def env_marker_setting(*, name, expression, **kwargs): + """Creates an env_marker setting. + + Generated targets: + + * `is_{name}_true`: config_setting that matches when the expression is true. + * `{name}`: env marker target that evalutes the expression. + + Args: + name: {type}`str` target name + expression: {type}`str` the environment marker string to evaluate + **kwargs: {type}`dict` additional common kwargs. + """ + native.config_setting( + name = "is_{}_true".format(name), + flag_values = { + ":{}".format(name): _ENV_MARKER_TRUE, + }, + **kwargs + ) + _env_marker_setting( + name = name, + expression = expression, + os_name = select(os_name_select_map), + sys_platform = select(sys_platform_select_map), + platform_machine = select(platform_machine_select_map), + platform_system = select(platform_system_select_map), + platform_release = select({ + "@platforms//os:osx": "USE_OSX_VERSION_FLAG", + "//conditions:default": "", + }), + **kwargs + ) + +def _env_marker_setting_impl(ctx): + env = {} + + runtime = ctx.toolchains[TARGET_TOOLCHAIN_TYPE].py3_runtime + if runtime.interpreter_version_info: + version_info = runtime.interpreter_version_info + env["python_version"] = "{major}.{minor}".format( + major = version_info.major, + minor = version_info.minor, + ) + full_version = _format_full_version(version_info) + env["python_full_version"] = full_version + env["implementation_version"] = full_version + else: + env["python_version"] = _get_flag(ctx.attr._python_version_major_minor_flag) + full_version = _get_flag(ctx.attr._python_full_version_flag) + env["python_full_version"] = full_version + env["implementation_version"] = full_version + + # We assume cpython if the toolchain doesn't specify because it's most + # likely to be true. + env["implementation_name"] = runtime.implementation_name or "cpython" + env["os_name"] = ctx.attr.os_name + env["sys_platform"] = ctx.attr.sys_platform + env["platform_machine"] = ctx.attr.platform_machine + + # The `platform_python_implementation` marker value is supposed to come + # from `platform.python_implementation()`, however, PEP 421 introduced + # `sys.implementation.name` and the `implementation_name` env marker to + # replace it. Per the platform.python_implementation docs, there's now + # essentially just two possible "registered" values: CPython or PyPy. + # Rather than add a field to the toolchain, we just special case the value + # from `sys.implementation.name` to handle the two documented values. + platform_python_impl = runtime.implementation_name + if platform_python_impl == "cpython": + platform_python_impl = "CPython" + elif platform_python_impl == "pypy": + platform_python_impl = "PyPy" + env["platform_python_implementation"] = platform_python_impl + + # NOTE: Platform release for Android will be Android version: + # https://peps.python.org/pep-0738/#platform + # Similar for iOS: + # https://peps.python.org/pep-0730/#platform + platform_release = ctx.attr.platform_release + if platform_release == "USE_OSX_VERSION_FLAG": + platform_release = _get_flag(ctx.attr._pip_whl_osx_version_flag) + env["platform_release"] = platform_release + env["platform_system"] = ctx.attr.platform_system + + # For lack of a better option, just use an empty string for now. + env["platform_version"] = "" + + env.update(env_aliases()) + + if evaluate(ctx.attr.expression, env = env): + value = _ENV_MARKER_TRUE + else: + value = _ENV_MARKER_FALSE + return [config_common.FeatureFlagInfo(value = value)] + +_env_marker_setting = rule( + doc = """ +Evaluates an environment marker expression using target configuration info. + +See +https://packaging.python.org/en/latest/specifications/dependency-specifiers +for the specification of behavior. +""", + implementation = _env_marker_setting_impl, + attrs = { + "expression": attr.string( + mandatory = True, + doc = "Environment marker expression to evaluate.", + ), + "os_name": attr.string(), + "platform_machine": attr.string(), + "platform_release": attr.string(), + "platform_system": attr.string(), + "sys_platform": attr.string(), + "_pip_whl_osx_version_flag": attr.label( + default = "//python/config_settings:pip_whl_osx_version", + providers = [[BuildSettingInfo], [config_common.FeatureFlagInfo]], + ), + "_python_full_version_flag": attr.label( + default = "//python/config_settings:python_version", + providers = [config_common.FeatureFlagInfo], + ), + "_python_version_major_minor_flag": attr.label( + default = "//python/config_settings:python_version_major_minor", + providers = [config_common.FeatureFlagInfo], + ), + }, + provides = [config_common.FeatureFlagInfo], + toolchains = [ + TARGET_TOOLCHAIN_TYPE, + ], +) + +def _format_full_version(info): + """Format the full python interpreter version. + + Adapted from spec code at: + https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers + + Args: + info: The provider from the Python runtime. + + Returns: + a {type}`str` with the version + """ + kind = info.releaselevel + if kind == "final": + kind = "" + serial = "" + else: + kind = kind[0] if kind else "" + serial = str(info.serial) if info.serial else "" + + return "{major}.{minor}.{micro}{kind}{serial}".format( + v = info, + major = info.major, + minor = info.minor, + micro = info.micro, + kind = kind, + serial = serial, + ) + +def _get_flag(t): + if config_common.FeatureFlagInfo in t: + return t[config_common.FeatureFlagInfo].value + if BuildSettingInfo in t: + return t[BuildSettingInfo].value + fail("Should not occur: {} does not have necessary providers") diff --git a/python/private/pypi/pep508_env.bzl b/python/private/pypi/pep508_env.bzl index 265a8e9b99..3708c46f1d 100644 --- a/python/private/pypi/pep508_env.bzl +++ b/python/private/pypi/pep508_env.bzl @@ -18,7 +18,7 @@ load(":pep508_platform.bzl", "platform_from_str") # See https://stackoverflow.com/a/45125525 -_platform_machine_aliases = { +platform_machine_aliases = { # These pairs mean the same hardware, but different values may be used # on different host platforms. "amd64": "x86_64", @@ -27,6 +27,41 @@ _platform_machine_aliases = { "i686": "x86_32", } +# NOTE: There are many cpus, and unfortunately, the value isn't directly +# accessible to Starlark. Using CcToolchain.cpu might work, though. +platform_machine_select_map = { + "@platforms//cpu:aarch32": "aarch32", + "@platforms//cpu:aarch64": "aarch64", + "@platforms//cpu:arm": "arm", + "@platforms//cpu:arm64": "arm64", + "@platforms//cpu:arm64_32": "arm64_32", + "@platforms//cpu:arm64e": "arm64e", + "@platforms//cpu:armv6-m": "armv6-m", + "@platforms//cpu:armv7": "armv7", + "@platforms//cpu:armv7-m": "armv7-m", + "@platforms//cpu:armv7e-m": "armv7e-m", + "@platforms//cpu:armv7e-mf": "armv7e-mf", + "@platforms//cpu:armv7k": "armv7k", + "@platforms//cpu:armv8-m": "armv8-m", + "@platforms//cpu:cortex-r52": "cortex-r52", + "@platforms//cpu:cortex-r82": "cortex-r82", + "@platforms//cpu:i386": "i386", + "@platforms//cpu:mips64": "mips64", + "@platforms//cpu:ppc": "ppc", + "@platforms//cpu:ppc32": "ppc32", + "@platforms//cpu:ppc64le": "ppc64le", + "@platforms//cpu:riscv32": "riscv32", + "@platforms//cpu:riscv64": "riscv64", + "@platforms//cpu:s390x": "s390x", + "@platforms//cpu:wasm32": "wasm32", + "@platforms//cpu:wasm64": "wasm64", + "@platforms//cpu:x86_32": "x86_32", + "@platforms//cpu:x86_64": "x86_64", + # The value is empty string if it cannot be determined: + # https://docs.python.org/3/library/platform.html#platform.machine + "//conditions:default": "", +} + # Platform system returns results from the `uname` call. _platform_system_values = { "linux": "Linux", @@ -34,6 +69,23 @@ _platform_system_values = { "windows": "Windows", } +platform_system_select_map = { + # See https://peps.python.org/pep-0738/#platform + "@platforms//os:android": "Android", + "@platforms//os:freebsd": "FreeBSD", + # See https://peps.python.org/pep-0730/#platform + # NOTE: Per Pep 730, "iPadOS" is also an acceptable value + "@platforms//os:ios": "iOS", + "@platforms//os:linux": "Linux", + "@platforms//os:netbsd": "NetBSD", + "@platforms//os:openbsd": "OpenBSD", + "@platforms//os:osx": "Darwin", + "@platforms//os:windows": "Windows", + # The value is empty string if it cannot be determined: + # https://docs.python.org/3/library/platform.html#platform.machine + "//conditions:default": "", +} + # The copy of SO [answer](https://stackoverflow.com/a/13874620) containing # all of the platforms: # ┍━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━┑ @@ -64,12 +116,45 @@ _sys_platform_values = { "osx": "darwin", "windows": "win32", } + +# Taken from +# https://docs.python.org/3/library/sys.html#sys.platform +sys_platform_select_map = { + # These values are decided by the sys.platform docs. + "@platforms//os:android": "android", + "@platforms//os:emscripten": "emscripten", + # NOTE: The below values are approximations. The sys.platform() docs + # don't have documented values for these OSes. Per docs, the + # sys.platform() value reflects the OS at the time Python was *built* + # instead of the runtime (target) OS value. + "@platforms//os:freebsd": "freebsd", + "@platforms//os:ios": "ios", + "@platforms//os:linux": "linux", + "@platforms//os:openbsd": "openbsd", + "@platforms//os:osx": "darwin", + "@platforms//os:wasi": "wasi", + "@platforms//os:windows": "win32", + # For lack of a better option, use empty string. No standard doc/spec + # about sys_platform value. + "//conditions:default": "", +} + _os_name_values = { "linux": "posix", "osx": "posix", "windows": "nt", } +os_name_select_map = { + # The "java" value is documented, but with Jython defunct, + # shouldn't occur in practice. + # The os.name value is technically a property of the runtime, not the + # targetted runtime OS, but the distinction shouldn't matter if + # things are properly configured. + "@platforms//os:windows": "nt", + "//conditions:default": "posix", +} + def env(target_platform, *, extra = None): """Return an env target platform @@ -113,8 +198,11 @@ def env(target_platform, *, extra = None): } # This is split by topic - return env | { + return env | env_aliases() + +def env_aliases(): + return { "_aliases": { - "platform_machine": _platform_machine_aliases, + "platform_machine": platform_machine_aliases, }, } diff --git a/tests/pypi/env_marker_setting/BUILD.bazel b/tests/pypi/env_marker_setting/BUILD.bazel new file mode 100644 index 0000000000..9605e650ce --- /dev/null +++ b/tests/pypi/env_marker_setting/BUILD.bazel @@ -0,0 +1,5 @@ +load(":env_marker_setting_tests.bzl", "env_marker_setting_test_suite") + +env_marker_setting_test_suite( + name = "env_marker_setting_tests", +) diff --git a/tests/pypi/env_marker_setting/env_marker_setting_tests.bzl b/tests/pypi/env_marker_setting/env_marker_setting_tests.bzl new file mode 100644 index 0000000000..549c15c20b --- /dev/null +++ b/tests/pypi/env_marker_setting/env_marker_setting_tests.bzl @@ -0,0 +1,69 @@ +"""env_marker_setting tests.""" + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:util.bzl", "TestingAspectInfo") +load("//python/private/pypi:env_marker_setting.bzl", "env_marker_setting") # buildifier: disable=bzl-visibility +load("//tests/support:support.bzl", "PYTHON_VERSION") + +_tests = [] + +def _test_expr(name): + def impl(env, target): + env.expect.where( + expression = target[TestingAspectInfo].attrs.expression, + ).that_str( + target[config_common.FeatureFlagInfo].value, + ).equals( + env.ctx.attr.expected, + ) + + cases = { + "python_full_version_lt_negative": { + "config_settings": { + PYTHON_VERSION: "3.12.0", + }, + "expected": "FALSE", + "expression": "python_full_version < '3.8'", + }, + "python_version_gte": { + "config_settings": { + PYTHON_VERSION: "3.12.0", + }, + "expected": "TRUE", + "expression": "python_version >= '3.12.0'", + }, + } + + tests = [] + for case_name, case in cases.items(): + test_name = name + "_" + case_name + tests.append(test_name) + env_marker_setting( + name = test_name + "_subject", + expression = case["expression"], + ) + analysis_test( + name = test_name, + impl = impl, + target = test_name + "_subject", + config_settings = case["config_settings"], + attr_values = { + "expected": case["expected"], + }, + attrs = { + "expected": attr.string(), + }, + ) + native.test_suite( + name = name, + tests = tests, + ) + +_tests.append(_test_expr) + +def env_marker_setting_test_suite(name): + test_suite( + name = name, + tests = _tests, + ) From ccbe5dcdb84a2c194deaf34165e43201e17a3826 Mon Sep 17 00:00:00 2001 From: Tobias Fuchs <9053039+devtbi@users.noreply.github.com> Date: Sat, 3 May 2025 05:22:48 +0200 Subject: [PATCH 065/156] py_wheel: always generate zip64-capable wheels (#2711) Currently, there is no possibility to pass the force zip64 option to the wheel creation. This hinders creation of packages that contain >2Gb files (e.g. large projects with debug symbols). To fix, always generate zip64 capable wheels. zip64 support is wide spread. Fixes https://github.com/bazel-contrib/rules_python/issues/2852 --------- Co-authored-by: Richard Levasseur Co-authored-by: Richard Levasseur --- CHANGELOG.md | 2 ++ examples/wheel/test_publish.py | 2 +- examples/wheel/wheel_test.py | 16 ++++++++-------- tools/wheelmaker.py | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19fe636bc3..17e3cd3c86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,12 +54,14 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-changed} ### Changed + * (rules) On Windows, {obj}`--bootstrap_impl=system_python` is forced. This allows setting `--bootstrap_impl=script` in bazelrc for mixed-platform environments. * (rules) {obj}`pip_compile` now generates a `.test` target. The `_test` target is deprecated and will be removed in the next major release. ([#2794](https://github.com/bazel-contrib/rules_python/issues/2794) +* (py_wheel) py_wheel always creates zip64-capable wheel zips {#v0-0-0-fixed} ### Fixed diff --git a/examples/wheel/test_publish.py b/examples/wheel/test_publish.py index e6ec80721b..7665629c19 100644 --- a/examples/wheel/test_publish.py +++ b/examples/wheel/test_publish.py @@ -104,7 +104,7 @@ def test_upload_and_query_simple_api(self):

Links for example-minimal-library

- example_minimal_library-0.0.1-py3-none-any.whl
+ example_minimal_library-0.0.1-py3-none-any.whl
""" self.assertEqual( diff --git a/examples/wheel/wheel_test.py b/examples/wheel/wheel_test.py index 43e56cfc17..7f19ecd9f9 100644 --- a/examples/wheel/wheel_test.py +++ b/examples/wheel/wheel_test.py @@ -85,7 +85,7 @@ def test_py_library_wheel(self): ], ) self.assertFileSha256Equal( - filename, "a73acae23590c7a8d4365c888c1f12f0399b7af27169ea99fc7a00f402833926" + filename, "ef5afd9f6c3ff569ef7e5b2799d3a2ec9675d029414f341e0abd7254d6b9a25d" ) def test_py_package_wheel(self): @@ -110,7 +110,7 @@ def test_py_package_wheel(self): ], ) self.assertFileSha256Equal( - filename, "a76001500453dbd1d778821dcaba165d56db502c854cef9381dd3f8f89caee11" + filename, "39bec133cf79431e8d057eae550cd91aa9dfbddfedb53d98ebd36e3ade2753d0" ) def test_customized_wheel(self): @@ -206,7 +206,7 @@ def test_customized_wheel(self): second = second.main:s""", ) self.assertFileSha256Equal( - filename, "941c0d79f4ca67cfa0028248bd0606db7fc69953ff9c7c73ac26a3e6d3c23587" + filename, "685f68fc6665f53c9b769fd1ba12cce9937ab7f40ef4e60c82ef2de8653935de" ) def test_filename_escaping(self): @@ -278,7 +278,7 @@ def test_custom_package_root_wheel(self): for line in record_contents.splitlines(): self.assertFalse(line.startswith("/")) self.assertFileSha256Equal( - filename, "7bd959b7efe9e325b30a6559177a1a4f22ac7a68fade310845916276110e9287" + filename, "2fbfc3baaf6fccca0f97d02316b8344507fe6c8136991a66ee5f162235adb19f" ) def test_custom_package_root_multi_prefix_wheel(self): @@ -312,7 +312,7 @@ def test_custom_package_root_multi_prefix_wheel(self): for line in record_contents.splitlines(): self.assertFalse(line.startswith("/")) self.assertFileSha256Equal( - filename, "caf51e22bdcd3c6c766c8903319ce717daeb6caac577d14e16326a8597981854" + filename, "3e67971ca1e8a9ba36a143df7532e641f5661c56235e41d818309316c955ba58" ) def test_custom_package_root_multi_prefix_reverse_order_wheel(self): @@ -346,7 +346,7 @@ def test_custom_package_root_multi_prefix_reverse_order_wheel(self): for line in record_contents.splitlines(): self.assertFalse(line.startswith("/")) self.assertFileSha256Equal( - filename, "9e8c0baa408b829dec691a5e8d3bc040be0bbfcc95c0eee19e1e5ffadea4a059" + filename, "372ef9e11fb79f1952172993718a326b5adda192d94884b54377c34b44394982" ) def test_python_requires_wheel(self): @@ -371,7 +371,7 @@ def test_python_requires_wheel(self): """, ) self.assertFileSha256Equal( - filename, "b47f3eaf4f9fa4685a58c7415ba1feddd39635ae26c18473504f7d7e62e8ce07" + filename, "10a325ba8f77428b5cfcff6345d508f5eb77c140889eb62490d7382f60d4ebfe" ) def test_python_abi3_binary_wheel(self): @@ -436,7 +436,7 @@ def test_rule_creates_directory_and_is_included_in_wheel(self): ], ) self.assertFileSha256Equal( - filename, "d8e874b807e5574bd11a9312c58ce7fe7055afb80412d0d0e7ed21fc9223cd53" + filename, "85e44c43cc19ccae9fe2e1d629230203aa11791bed1f7f68a069fb58d1c93cd2" ) def test_rule_expands_workspace_status_keys_in_wheel_metadata(self): diff --git a/tools/wheelmaker.py b/tools/wheelmaker.py index de584650d1..8b775e1541 100644 --- a/tools/wheelmaker.py +++ b/tools/wheelmaker.py @@ -154,7 +154,7 @@ def arcname_from(name): hash = hashlib.sha256() size = 0 with open(real_filename, "rb") as fsrc: - with self.open(zinfo, "w") as fdst: + with self.open(zinfo, "w", force_zip64=True) as fdst: while True: block = fsrc.read(2**20) if not block: From 4ccf5b23be8e2396b1fc358f1d83d1b7923c5ea7 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 3 May 2025 01:37:15 -0700 Subject: [PATCH 066/156] feat: allow specifying arbitrary constraints for local toolchains (#2829) This adds the ability for local toolchains to have arbitrary constraints set on them. This allows accomplishing two goals: 1. Makes it easier to enable/disable them on the command line, instead of having them entirely override an existing config and having to comment/uncomment the MODULE.bazel file sections. 2. Allows configuring them so that the repository is never initialized, which avoids the repository from being initialized during toolchain resolution, even if it will never match because of (1). --- .bazelci/presubmit.yml | 2 + CHANGELOG.md | 2 + docs/toolchains.md | 73 +++++++++++- .../private/local_runtime_toolchains_repo.bzl | 109 ++++++++++++++++++ python/private/py_toolchain_suite.bzl | 71 ++++++++++-- python/private/text_util.bzl | 5 + tests/integration/local_toolchains/.bazelrc | 2 + .../integration/local_toolchains/BUILD.bazel | 15 +++ .../integration/local_toolchains/MODULE.bazel | 13 +++ 9 files changed, 278 insertions(+), 14 deletions(-) diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 3b70734eff..7e9d4dea53 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -51,9 +51,11 @@ buildifier: test_flags: - "--noenable_bzlmod" - "--enable_workspace" + - "--test_tag_filters=-integration-test" build_flags: - "--noenable_bzlmod" - "--enable_workspace" + - "--build_tag_filters=-integration-test" bazel: 7.x .common_bazelinbazel_config: &common_bazelinbazel_config build_flags: diff --git a/CHANGELOG.md b/CHANGELOG.md index 17e3cd3c86..d9cb14459d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,8 @@ END_UNRELEASED_TEMPLATE * Repo utilities `execute_unchecked`, `execute_checked`, and `execute_checked_stdout` now support `log_stdout` and `log_stderr` keyword arg booleans. When these are `True` (the default), the subprocess's stdout/stderr will be logged. +* (toolchains) Local toolchains can be activated with custom flags. See + [Conditionally using local toolchains] docs for how to configure. {#v0-0-0-removed} ### Removed diff --git a/docs/toolchains.md b/docs/toolchains.md index 2f8db66595..c8305e8f0d 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -377,15 +377,14 @@ local_runtime_repo( local_runtime_toolchains_repo( name = "local_toolchains", runtimes = ["local_python3"], + # TIP: The `target_settings` arg can be used to activate them based on + # command line flags; see docs below. ) # Step 3: Register the toolchains register_toolchains("@local_toolchains//:all", dev_dependency = True) ``` -Note that `register_toolchains` will insert the local toolchain earlier in the -toolchain ordering, so it will take precedence over other registered toolchains. - :::{important} Be sure to set `dev_dependency = True`. Using a local toolchain only makes sense for the root module. @@ -397,6 +396,72 @@ downstream modules. Multiple runtimes and/or toolchains can be defined, which allows for multiple Python versions and/or platforms to be configured in a single `MODULE.bazel`. +Note that `register_toolchains` will insert the local toolchain earlier in the +toolchain ordering, so it will take precedence over other registered toolchains. +To better control when the toolchain is used, see [Conditionally using local +toolchains] + +### Conditionally using local toolchains + +By default, a local toolchain has few constraints and is early in the toolchain +ordering, which means it will usually be used no matter what. This can be +problematic for CI (where it shouldn't be used), expensive for CI (CI must +initialize/download the repository to determine its Python version), and +annoying for iterative development (enabling/disabling it requires modifying +MODULE.bazel). + +These behaviors can be mitigated, but it requires additional configuration +to avoid triggering the local toolchain repository to initialize (i.e. run +local commands and perform downloads). + +The two settings to change are +{obj}`local_runtime_toolchains_repo.target_compatible_with` and +{obj}`local_runtime_toolchains_repo.target_settings`, which control how Bazel +decides if a toolchain should match. By default, they point to targets *within* +the local runtime repository (trigger repo initialization). We have to override +them to *not* reference the local runtime repository at all. + +In the example below, we reconfigure the local toolchains so they are only +activated if the custom flag `--//:py=local` is set and the target platform +matches the Bazel host platform. The net effect is CI won't use the local +toolchain (nor initialize its repository), and developers can easily +enable/disable the local toolchain with a command line flag. + +``` +# File: MODULE.bazel +bazel_dep(name = "bazel_skylib", version = "1.7.1") + +local_runtime_toolchains_repo( + name = "local_toolchains", + runtimes = ["local_python3"], + target_compatible_with = { + "local_python3": ["HOST_CONSTRAINTS"], + }, + target_settings = { + "local_python3": ["@//:is_py_local"] + } +) + +# File: BUILD.bazel +load("@bazel_skylib//rules:common_settings.bzl", "string_flag") + +config_setting( + name = "is_py_local", + flag_values = {":py": "local"}, +) + +string_flag( + name = "py", + build_setting_default = "", +) +``` + +:::{tip} +Easily switching between *multiple* local toolchains can be accomplished by +adding additional `:is_py_X` targets and setting `--//:py` to match. +to easily switch between different local toolchains. +::: + ## Runtime environment toolchain @@ -425,7 +490,7 @@ locally installed Python. ### Autodetecting toolchain The autodetecting toolchain is a deprecated toolchain that is built into Bazel. -It's name is a bit misleading: it doesn't autodetect anything. All it does is +**It's name is a bit misleading: it doesn't autodetect anything**. All it does is use `python3` from the environment a binary runs within. This provides extremely limited functionality to the rules (at build time, nothing is knowable about the Python runtime). diff --git a/python/private/local_runtime_toolchains_repo.bzl b/python/private/local_runtime_toolchains_repo.bzl index adb3bb560d..004ca664ad 100644 --- a/python/private/local_runtime_toolchains_repo.bzl +++ b/python/private/local_runtime_toolchains_repo.bzl @@ -26,6 +26,9 @@ define_local_toolchain_suites( name = "toolchains", version_aware_repo_names = {version_aware_names}, version_unaware_repo_names = {version_unaware_names}, + repo_exec_compatible_with = {repo_exec_compatible_with}, + repo_target_compatible_with = {repo_target_compatible_with}, + repo_target_settings = {repo_target_settings}, ) """ @@ -39,6 +42,9 @@ def _local_runtime_toolchains_repo(rctx): rctx.file("BUILD.bazel", _TOOLCHAIN_TEMPLATE.format( version_aware_names = render.list(rctx.attr.runtimes), + repo_target_settings = render.string_list_dict(rctx.attr.target_settings), + repo_target_compatible_with = render.string_list_dict(rctx.attr.target_compatible_with), + repo_exec_compatible_with = render.string_list_dict(rctx.attr.exec_compatible_with), version_unaware_names = render.list(rctx.attr.default_runtimes or rctx.attr.runtimes), )) @@ -62,8 +68,36 @@ These will be defined as *version-unaware* toolchains. This means they will match any Python version. As such, they are registered after the version-aware toolchains defined by the `runtimes` attribute. +If not set, then the `runtimes` values will be used. + Note that order matters: it determines the toolchain priority within the package. +""", + ), + "exec_compatible_with": attr.string_list_dict( + doc = """ +Constraints that must be satisfied by an exec platform for a toolchain to be used. + +This is a `dict[str, list[str]]`, where the keys are repo names from the +`runtimes` or `default_runtimes` args, and the values are constraint +target labels (e.g. OS, CPU, etc). + +:::{note} +Specify `@//foo:bar`, not simply `//foo:bar` or `:bar`. The additional `@` is +needed because the strings are evaluated in a different context than where +they originate. +::: + +The list of settings become the {obj}`toolchain.exec_compatible_with` value for +each respective repo. + +This allows a local toolchain to only be used if certain exec platform +conditions are met, typically values from `@platforms`. + +See the [Local toolchains] docs for examples and further information. + +:::{versionadded} VERSION_NEXT_FEATURE +::: """, ), "runtimes": attr.string_list( @@ -76,6 +110,81 @@ are registered before `default_runtimes`. Note that order matters: it determines the toolchain priority within the package. +""", + ), + "target_compatible_with": attr.string_list_dict( + doc = """ +Constraints that must be satisfied for a toolchain to be used. + + +This is a `dict[str, list[str]]`, where the keys are repo names from the +`runtimes` or `default_runtimes` args, and the values are constraint +target labels (e.g. OS, CPU, etc), or the special string `"HOST_CONSTRAINTS"` +(which will be replaced with the current Bazel hosts's constraints). + +If a repo's entry is missing or empty, it defaults to the supported OS the +underlying runtime repository detects as compatible. + +:::{note} +Specify `@//foo:bar`, not simply `//foo:bar` or `:bar`. The additional `@` is +needed because the strings are evaluated in a different context than where +they originate. +::: + +The list of settings **becomes the** the {obj}`toolchain.target_compatible_with` +value for each respective repo; i.e. they _replace_ the auto-detected values +the local runtime itself computes. + +This allows a local toolchain to only be used if certain target platform +conditions are met, typically values from `@platforms`. + +See the [Local toolchains] docs for examples and further information. + +:::{seealso} +The `target_settings` attribute, which handles `config_setting` values, +instead of constraints. +::: + +:::{versionadded} VERSION_NEXT_FEATURE +::: +""", + ), + "target_settings": attr.string_list_dict( + doc = """ +Config settings that must be satisfied for a toolchain to be used. + +This is a `dict[str, list[str]]`, where the keys are repo names from the +`runtimes` or `default_runtimes` args, and the values are {obj}`config_setting()` +target labels. + +If a repo's entry is missing or empty, it will default to +`@//:is_match_python_version` (for repos in `runtimes`) or an empty list +(for repos in `default_runtimes`). + +:::{note} +Specify `@//foo:bar`, not simply `//foo:bar` or `:bar`. The additional `@` is +needed because the strings are evaluated in a different context than where +they originate. +::: + +The list of settings will be applied atop of any of the local runtime's +settings that are used for {obj}`toolchain.target_settings`. i.e. they are +evaluated first and guard the checking of the local runtime's auto-detected +conditions. + +This allows a local toolchain to only be used if certain flags or +config setting conditions are met. Such conditions can include user-defined +flags, platform constraints, etc. + +See the [Local toolchains] docs for examples and further information. + +:::{seealso} +The `target_compatible_with` attribute, which handles *constraint* values, +instead of `config_settings`. +::: + +:::{versionadded} VERSION_NEXT_FEATURE +::: """, ), "_rule_name": attr.string(default = "local_toolchains_repo"), diff --git a/python/private/py_toolchain_suite.bzl b/python/private/py_toolchain_suite.bzl index a69be376b4..e71882dafd 100644 --- a/python/private/py_toolchain_suite.bzl +++ b/python/private/py_toolchain_suite.bzl @@ -15,6 +15,7 @@ """Create the toolchain defs in a BUILD.bazel file.""" load("@bazel_skylib//lib:selects.bzl", "selects") +load("@platforms//host:constraints.bzl", "HOST_CONSTRAINTS") load(":text_util.bzl", "render") load( ":toolchain_types.bzl", @@ -95,9 +96,15 @@ def py_toolchain_suite( runtime_repo_name = user_repository_name, target_settings = target_settings, target_compatible_with = target_compatible_with, + exec_compatible_with = [], ) -def _internal_toolchain_suite(prefix, runtime_repo_name, target_compatible_with, target_settings): +def _internal_toolchain_suite( + prefix, + runtime_repo_name, + target_compatible_with, + target_settings, + exec_compatible_with): native.toolchain( name = "{prefix}_toolchain".format(prefix = prefix), toolchain = "@{runtime_repo_name}//:python_runtimes".format( @@ -106,6 +113,7 @@ def _internal_toolchain_suite(prefix, runtime_repo_name, target_compatible_with, toolchain_type = TARGET_TOOLCHAIN_TYPE, target_settings = target_settings, target_compatible_with = target_compatible_with, + exec_compatible_with = exec_compatible_with, ) native.toolchain( @@ -116,6 +124,7 @@ def _internal_toolchain_suite(prefix, runtime_repo_name, target_compatible_with, toolchain_type = PY_CC_TOOLCHAIN_TYPE, target_settings = target_settings, target_compatible_with = target_compatible_with, + exec_compatible_with = exec_compatible_with, ) native.toolchain( @@ -142,7 +151,13 @@ def _internal_toolchain_suite(prefix, runtime_repo_name, target_compatible_with, # call in python/repositories.bzl. Bzlmod doesn't need anything; it will # register `:all`. -def define_local_toolchain_suites(name, version_aware_repo_names, version_unaware_repo_names): +def define_local_toolchain_suites( + name, + version_aware_repo_names, + version_unaware_repo_names, + repo_exec_compatible_with, + repo_target_compatible_with, + repo_target_settings): """Define toolchains for `local_runtime_repo` backed toolchains. This generates `toolchain` targets that can be registered using `:all`. The @@ -156,24 +171,60 @@ def define_local_toolchain_suites(name, version_aware_repo_names, version_unawar version-aware toolchains defined. version_unaware_repo_names: `list[str]` of the repo names that will have version-unaware toolchains defined. + repo_target_settings: {type}`dict[str, list[str]]` mapping of repo names + to string labels that are added to the `target_settings` for the + respective repo's toolchain. + repo_target_compatible_with: {type}`dict[str, list[str]]` mapping of repo names + to string labels that are added to the `target_compatible_with` for + the respective repo's toolchain. + repo_exec_compatible_with: {type}`dict[str, list[str]]` mapping of repo names + to string labels that are added to the `exec_compatible_with` for + the respective repo's toolchain. """ + i = 0 for i, repo in enumerate(version_aware_repo_names, start = i): - prefix = render.left_pad_zero(i, 4) + target_settings = ["@{}//:is_matching_python_version".format(repo)] + + if repo_target_settings.get(repo): + selects.config_setting_group( + name = "_{}_user_guard".format(repo), + match_all = repo_target_settings.get(repo, []) + target_settings, + ) + target_settings = ["_{}_user_guard".format(repo)] _internal_toolchain_suite( - prefix = prefix, + prefix = render.left_pad_zero(i, 4), runtime_repo_name = repo, - target_compatible_with = ["@{}//:os".format(repo)], - target_settings = ["@{}//:is_matching_python_version".format(repo)], + target_compatible_with = _get_local_toolchain_target_compatible_with( + repo, + repo_target_compatible_with, + ), + target_settings = target_settings, + exec_compatible_with = repo_exec_compatible_with.get(repo, []), ) # The version unaware entries must go last because they will match any Python # version. for i, repo in enumerate(version_unaware_repo_names, start = i + 1): - prefix = render.left_pad_zero(i, 4) _internal_toolchain_suite( - prefix = prefix, + prefix = render.left_pad_zero(i, 4) + "_default", runtime_repo_name = repo, - target_settings = [], - target_compatible_with = ["@{}//:os".format(repo)], + target_compatible_with = _get_local_toolchain_target_compatible_with( + repo, + repo_target_compatible_with, + ), + # We don't call _get_local_toolchain_target_settings because that + # will add the version matching condition by default. + target_settings = repo_target_settings.get(repo, []), + exec_compatible_with = repo_exec_compatible_with.get(repo, []), ) + +def _get_local_toolchain_target_compatible_with(repo, repo_target_compatible_with): + if repo in repo_target_compatible_with: + target_compatible_with = repo_target_compatible_with[repo] + if "HOST_CONSTRAINTS" in target_compatible_with: + target_compatible_with.remove("HOST_CONSTRAINTS") + target_compatible_with.extend(HOST_CONSTRAINTS) + else: + target_compatible_with = ["@{}//:os".format(repo)] + return target_compatible_with diff --git a/python/private/text_util.bzl b/python/private/text_util.bzl index a64b5d6243..28979d8981 100644 --- a/python/private/text_util.bzl +++ b/python/private/text_util.bzl @@ -108,6 +108,10 @@ def _render_list(items, *, hanging_indent = ""): def _render_str(value): return repr(value) +def _render_string_list_dict(value): + """Render an attr.string_list_dict value (`dict[str, list[str]`)""" + return _render_dict(value, value_repr = _render_list) + def _render_tuple(items, *, value_repr = repr): if not items: return "tuple()" @@ -166,4 +170,5 @@ render = struct( str = _render_str, toolchain_prefix = _toolchain_prefix, tuple = _render_tuple, + string_list_dict = _render_string_list_dict, ) diff --git a/tests/integration/local_toolchains/.bazelrc b/tests/integration/local_toolchains/.bazelrc index 39df41d9f4..aed08b0790 100644 --- a/tests/integration/local_toolchains/.bazelrc +++ b/tests/integration/local_toolchains/.bazelrc @@ -4,3 +4,5 @@ test --test_output=errors # Windows requires these for multi-python support: build --enable_runfiles common:bazel7.x --incompatible_python_disallow_native_rules +build --//:py=local +common --announce_rc diff --git a/tests/integration/local_toolchains/BUILD.bazel b/tests/integration/local_toolchains/BUILD.bazel index 02b126b0ea..6b731181a6 100644 --- a/tests/integration/local_toolchains/BUILD.bazel +++ b/tests/integration/local_toolchains/BUILD.bazel @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +load("@bazel_skylib//rules:common_settings.bzl", "string_flag") load("@rules_python//python:py_test.bzl", "py_test") py_test( @@ -20,3 +21,17 @@ py_test( # Make this test better respect pyenv env_inherit = ["PYENV_VERSION"], ) + +config_setting( + name = "is_py_local", + flag_values = { + ":py": "local", + }, +) + +# Set `--//:py=local` to use the local toolchain +# (This is set in this example's .bazelrc) +string_flag( + name = "py", + build_setting_default = "", +) diff --git a/tests/integration/local_toolchains/MODULE.bazel b/tests/integration/local_toolchains/MODULE.bazel index 98f1ed9ac4..6c06909cd7 100644 --- a/tests/integration/local_toolchains/MODULE.bazel +++ b/tests/integration/local_toolchains/MODULE.bazel @@ -14,6 +14,9 @@ module(name = "module_under_test") bazel_dep(name = "rules_python", version = "0.0.0") +bazel_dep(name = "bazel_skylib", version = "1.7.1") +bazel_dep(name = "platforms", version = "0.0.11") + local_path_override( module_name = "rules_python", path = "../../..", @@ -32,6 +35,16 @@ local_runtime_repo( local_runtime_toolchains_repo( name = "local_toolchains", runtimes = ["local_python3"], + target_compatible_with = { + "local_python3": [ + "HOST_CONSTRAINTS", + ], + }, + target_settings = { + "local_python3": [ + "@//:is_py_local", + ], + }, ) python = use_extension("@rules_python//python/extensions:python.bzl", "python") From a4b946bbe1b3e83ca4602a0d059fea823b0ded65 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 5 May 2025 14:22:38 +0900 Subject: [PATCH 067/156] feat: add an env variable to toggle pipstar (#2855) This is a flag to start leveraging of the new code paths. The Starlark implementation has been added in 1.4 and has been reverted in the latest release candidates. The `env` variable will be a good way to roll it out more gradually and get more testing. For now we are switching only the `whl_library` internals as the `requirements.txt` files from `uv` may use `*` in `python_full_version` and `platform_version` that are not yet fully supported (#2826). Main goals for this is to start using Starlark implementation so that we don't have any hidden variables. What is more, having this in Starlark is the most maintainable long-term solution for supporting cross-platform builds. Work towards #260 --------- Co-authored-by: Richard Levasseur --- CHANGELOG.md | 3 + docs/environment-variables.md | 9 + python/private/internal_config_repo.bzl | 4 + .../private/pypi/whl_installer/arguments.py | 5 + .../pypi/whl_installer/wheel_installer.py | 44 ++-- python/private/pypi/whl_library.bzl | 207 ++++++++++++------ .../whl_installer/wheel_installer_test.py | 1 + 7 files changed, 187 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9cb14459d..7d73613a07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,9 @@ END_UNRELEASED_TEMPLATE (the default), the subprocess's stdout/stderr will be logged. * (toolchains) Local toolchains can be activated with custom flags. See [Conditionally using local toolchains] docs for how to configure. +* (pypi) `RULES_PYTHON_ENABLE_PIPSTAR` environment variable: when `1`, the Starlark + implementation of wheel METADATA parsing is used (which has improved multi-platform + build support). {#v0-0-0-removed} ### Removed diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 49fdf766f6..26c171095d 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -60,6 +60,15 @@ The default became `1` if unspecified ::: :::: +::::{envvar} RULES_PYTHON_ENABLE_PIPSTAR + +When `1`, the rules_python Starlark implementation of the pypi/pip integration is used +instead of the legacy Python scripts. + +:::{versionadded} VERSION_NEXT_FEATURE +::: +:::: + ::::{envvar} RULES_PYTHON_EXTRACT_ROOT Directory to use as the root for creating files necessary for bootstrapping so diff --git a/python/private/internal_config_repo.bzl b/python/private/internal_config_repo.bzl index a5c4787161..cfe2fdfd77 100644 --- a/python/private/internal_config_repo.bzl +++ b/python/private/internal_config_repo.bzl @@ -20,6 +20,8 @@ settings for rules to later use. load(":repo_utils.bzl", "repo_utils") +_ENABLE_PIPSTAR_ENVVAR_NAME = "RULES_PYTHON_ENABLE_PIPSTAR" +_ENABLE_PIPSTAR_DEFAULT = "0" _ENABLE_PYSTAR_ENVVAR_NAME = "RULES_PYTHON_ENABLE_PYSTAR" _ENABLE_PYSTAR_DEFAULT = "1" _ENABLE_DEPRECATION_WARNINGS_ENVVAR_NAME = "RULES_PYTHON_DEPRECATION_WARNINGS" @@ -28,6 +30,7 @@ _ENABLE_DEPRECATION_WARNINGS_DEFAULT = "0" _CONFIG_TEMPLATE = """\ config = struct( enable_pystar = {enable_pystar}, + enable_pipstar = {enable_pipstar}, enable_deprecation_warnings = {enable_deprecation_warnings}, BuiltinPyInfo = getattr(getattr(native, "legacy_globals", None), "PyInfo", {builtin_py_info_symbol}), BuiltinPyRuntimeInfo = getattr(getattr(native, "legacy_globals", None), "PyRuntimeInfo", {builtin_py_runtime_info_symbol}), @@ -84,6 +87,7 @@ def _internal_config_repo_impl(rctx): rctx.file("rules_python_config.bzl", _CONFIG_TEMPLATE.format( enable_pystar = enable_pystar, + enable_pipstar = _bool_from_environ(rctx, _ENABLE_PIPSTAR_ENVVAR_NAME, _ENABLE_PIPSTAR_DEFAULT), enable_deprecation_warnings = _bool_from_environ(rctx, _ENABLE_DEPRECATION_WARNINGS_ENVVAR_NAME, _ENABLE_DEPRECATION_WARNINGS_DEFAULT), builtin_py_info_symbol = builtin_py_info_symbol, builtin_py_runtime_info_symbol = builtin_py_runtime_info_symbol, diff --git a/python/private/pypi/whl_installer/arguments.py b/python/private/pypi/whl_installer/arguments.py index 29bea8026e..ea609bef9d 100644 --- a/python/private/pypi/whl_installer/arguments.py +++ b/python/private/pypi/whl_installer/arguments.py @@ -47,6 +47,11 @@ def parser(**kwargs: Any) -> argparse.ArgumentParser: type=Platform.from_string, help="Platforms to target dependencies. Can be used multiple times.", ) + parser.add_argument( + "--enable-pipstar", + action="store_true", + help="Disable certain code paths if we expect to process the whl in Starlark.", + ) parser.add_argument( "--pip_data_exclude", action="store", diff --git a/python/private/pypi/whl_installer/wheel_installer.py b/python/private/pypi/whl_installer/wheel_installer.py index a48df699ba..2db03e039d 100644 --- a/python/private/pypi/whl_installer/wheel_installer.py +++ b/python/private/pypi/whl_installer/wheel_installer.py @@ -104,6 +104,7 @@ def _setup_namespace_pkg_compatibility(wheel_dir: str) -> None: def _extract_wheel( wheel_file: str, extras: Dict[str, Set[str]], + enable_pipstar: bool, enable_implicit_namespace_pkgs: bool, platforms: List[wheel.Platform], installation_dir: Path = Path("."), @@ -114,6 +115,7 @@ def _extract_wheel( 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 + enable_pipstar: if true, turns off certain operations. enable_implicit_namespace_pkgs: if true, disables conversion of implicit namespace packages and will unzip as-is """ @@ -123,26 +125,31 @@ def _extract_wheel( if not enable_implicit_namespace_pkgs: _setup_namespace_pkg_compatibility(installation_dir) - extras_requested = extras[whl.name] if whl.name in extras else set() - - dependencies = whl.dependencies(extras_requested, platforms) + metadata = { + "python_version": f"{sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]}", + "entry_points": [ + { + "name": name, + "module": module, + "attribute": attribute, + } + for name, (module, attribute) in sorted(whl.entry_points().items()) + ], + } + if not enable_pipstar: + extras_requested = extras[whl.name] if whl.name in extras else set() + dependencies = whl.dependencies(extras_requested, platforms) + + metadata.update( + { + "name": whl.name, + "version": whl.version, + "deps": dependencies.deps, + "deps_by_platform": dependencies.deps_select, + } + ) with open(os.path.join(installation_dir, "metadata.json"), "w") as f: - metadata = { - "name": whl.name, - "version": whl.version, - "deps": dependencies.deps, - "python_version": f"{sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]}", - "deps_by_platform": dependencies.deps_select, - "entry_points": [ - { - "name": name, - "module": module, - "attribute": attribute, - } - for name, (module, attribute) in sorted(whl.entry_points().items()) - ], - } json.dump(metadata, f) @@ -161,6 +168,7 @@ def main() -> None: _extract_wheel( wheel_file=whl, extras=extras, + enable_pipstar=args.enable_pipstar, enable_implicit_namespace_pkgs=args.enable_implicit_namespace_pkgs, platforms=arguments.get_platforms(args), ) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 0c09f7960a..160bb5b799 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -14,6 +14,7 @@ "" +load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") load("//python/private:auth.bzl", "AUTH_ATTRS", "get_auth") load("//python/private:envsubst.bzl", "envsubst") load("//python/private:is_standalone_interpreter.bzl", "is_standalone_interpreter") @@ -21,9 +22,11 @@ load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") load(":attrs.bzl", "ATTRS", "use_isolated") load(":deps.bzl", "all_repo_names", "record_files") load(":generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel") +load(":parse_requirements.bzl", "host_platform") load(":parse_whl_name.bzl", "parse_whl_name") load(":patch_whl.bzl", "patch_whl") load(":pypi_repo_utils.bzl", "pypi_repo_utils") +load(":whl_metadata.bzl", "whl_metadata") load(":whl_target_platforms.bzl", "whl_target_platforms") _CPPFLAGS = "CPPFLAGS" @@ -340,79 +343,147 @@ def _whl_library_impl(rctx): timeout = rctx.attr.timeout, ) - target_platforms = rctx.attr.experimental_target_platforms or [] - if target_platforms: - parsed_whl = parse_whl_name(whl_path.basename) - - # NOTE @aignas 2023-12-04: if the wheel is a platform specific wheel, we - # only include deps for that target platform - if parsed_whl.platform_tag != "any": - target_platforms = [ - p.target_platform - for p in whl_target_platforms( - platform_tag = parsed_whl.platform_tag, - abi_tag = parsed_whl.abi_tag.strip("tm"), - ) - ] - - pypi_repo_utils.execute_checked( - rctx, - op = "whl_library.ExtractWheel({}, {})".format(rctx.attr.name, whl_path), - python = python_interpreter, - arguments = args + [ - "--whl-file", - whl_path, - ] + ["--platform={}".format(p) for p in target_platforms], - srcs = rctx.attr._python_srcs, - environment = environment, - quiet = rctx.attr.quiet, - timeout = rctx.attr.timeout, - logger = logger, - ) + if rp_config.enable_pipstar: + pypi_repo_utils.execute_checked( + rctx, + op = "whl_library.ExtractWheel({}, {})".format(rctx.attr.name, whl_path), + python = python_interpreter, + arguments = args + [ + "--whl-file", + whl_path, + "--enable-pipstar", + ], + srcs = rctx.attr._python_srcs, + environment = environment, + quiet = rctx.attr.quiet, + timeout = rctx.attr.timeout, + logger = logger, + ) - metadata = json.decode(rctx.read("metadata.json")) - rctx.delete("metadata.json") + metadata = json.decode(rctx.read("metadata.json")) + rctx.delete("metadata.json") + python_version = metadata["python_version"] - # NOTE @aignas 2024-06-22: this has to live on until we stop supporting - # passing `twine` as a `:pkg` library via the `WORKSPACE` builds. - # - # See ../../packaging.bzl line 190 - 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 + # NOTE @aignas 2024-06-22: this has to live on until we stop supporting + # passing `twine` as a `:pkg` library via the `WORKSPACE` builds. + # + # See ../../packaging.bzl line 190 + 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 + + metadata = whl_metadata( + install_dir = whl_path.dirname.get_child("site-packages"), + read_fn = rctx.read, + logger = logger, ) - entry_point_script_name = entry_point_target_name + ".py" - rctx.file( - entry_point_script_name, - _generate_entry_point_contents(module, attribute), + build_file_contents = generate_whl_library_build_bazel( + name = whl_path.basename, + dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix), + entry_points = entry_points, + metadata_name = metadata.name, + metadata_version = metadata.version, + default_python_version = python_version, + requires_dist = metadata.requires_dist, + target_platforms = rctx.attr.experimental_target_platforms or [host_platform(rctx)], + # TODO @aignas 2025-04-14: load through the hub: + annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), + data_exclude = rctx.attr.pip_data_exclude, + group_deps = rctx.attr.group_deps, + group_name = rctx.attr.group_name, ) - entry_points[entry_point_without_py] = entry_point_script_name - - build_file_contents = generate_whl_library_build_bazel( - name = whl_path.basename, - dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix), - entry_points = entry_points, - # TODO @aignas 2025-04-14: load through the hub: - dependencies = metadata["deps"], - dependencies_by_platform = metadata["deps_by_platform"], - annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), - data_exclude = rctx.attr.pip_data_exclude, - group_deps = rctx.attr.group_deps, - group_name = rctx.attr.group_name, - tags = [ - "pypi_name={}".format(metadata["name"]), - "pypi_version={}".format(metadata["version"]), - ], - ) + else: + target_platforms = rctx.attr.experimental_target_platforms or [] + if target_platforms: + parsed_whl = parse_whl_name(whl_path.basename) + + # NOTE @aignas 2023-12-04: if the wheel is a platform specific wheel, we + # only include deps for that target platform + if parsed_whl.platform_tag != "any": + target_platforms = [ + p.target_platform + for p in whl_target_platforms( + platform_tag = parsed_whl.platform_tag, + abi_tag = parsed_whl.abi_tag.strip("tm"), + ) + ] + + pypi_repo_utils.execute_checked( + rctx, + op = "whl_library.ExtractWheel({}, {})".format(rctx.attr.name, whl_path), + python = python_interpreter, + arguments = args + [ + "--whl-file", + whl_path, + ] + ["--platform={}".format(p) for p in target_platforms], + srcs = rctx.attr._python_srcs, + environment = environment, + quiet = rctx.attr.quiet, + timeout = rctx.attr.timeout, + logger = logger, + ) + + metadata = json.decode(rctx.read("metadata.json")) + rctx.delete("metadata.json") + + # NOTE @aignas 2024-06-22: this has to live on until we stop supporting + # passing `twine` as a `:pkg` library via the `WORKSPACE` builds. + # + # See ../../packaging.bzl line 190 + 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( + name = whl_path.basename, + dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix), + entry_points = entry_points, + # TODO @aignas 2025-04-14: load through the hub: + dependencies = metadata["deps"], + dependencies_by_platform = metadata["deps_by_platform"], + annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), + data_exclude = rctx.attr.pip_data_exclude, + group_deps = rctx.attr.group_deps, + group_name = rctx.attr.group_name, + tags = [ + "pypi_name={}".format(metadata["name"]), + "pypi_version={}".format(metadata["version"]), + ], + ) + rctx.file("BUILD.bazel", build_file_contents) return diff --git a/tests/pypi/whl_installer/wheel_installer_test.py b/tests/pypi/whl_installer/wheel_installer_test.py index b736877e81..e838047925 100644 --- a/tests/pypi/whl_installer/wheel_installer_test.py +++ b/tests/pypi/whl_installer/wheel_installer_test.py @@ -72,6 +72,7 @@ def test_wheel_exists(self) -> None: extras={}, enable_implicit_namespace_pkgs=False, platforms=[], + enable_pipstar = False, ) want_files = [ From 78647318f94b3a94e11b77f03e0314bd77e1e0fe Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Mon, 5 May 2025 18:27:27 +0200 Subject: [PATCH 068/156] fix: add target platform to extra exec platforms in analysis tests (#2861) This is required as of https://github.com/bazelbuild/bazel/commit/2780393d35ad0607cf5e344ae082b00a5569a964 as tests now require an execution platform that matches their target constraints by default. Fixes #2850 --- tests/base_rules/py_executable_base_tests.bzl | 2 ++ tests/base_rules/py_test/py_test_tests.bzl | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/base_rules/py_executable_base_tests.bzl b/tests/base_rules/py_executable_base_tests.bzl index 37707831fc..55a8958b82 100644 --- a/tests/base_rules/py_executable_base_tests.bzl +++ b/tests/base_rules/py_executable_base_tests.bzl @@ -51,6 +51,7 @@ def _test_basic_windows(name, config): "//command_line_option:build_python_zip": "true", "//command_line_option:cpu": "windows_x86_64", "//command_line_option:crosstool_top": CROSSTOOL_TOP, + "//command_line_option:extra_execution_platforms": [WINDOWS_X86_64], "//command_line_option:extra_toolchains": [CC_TOOLCHAIN], "//command_line_option:platforms": [WINDOWS_X86_64], }, @@ -96,6 +97,7 @@ def _test_basic_zip(name, config): "//command_line_option:build_python_zip": "true", "//command_line_option:cpu": "linux_x86_64", "//command_line_option:crosstool_top": CROSSTOOL_TOP, + "//command_line_option:extra_execution_platforms": [LINUX_X86_64], "//command_line_option:extra_toolchains": [CC_TOOLCHAIN], "//command_line_option:platforms": [LINUX_X86_64], }, diff --git a/tests/base_rules/py_test/py_test_tests.bzl b/tests/base_rules/py_test/py_test_tests.bzl index d4d839b392..c51aa53a95 100644 --- a/tests/base_rules/py_test/py_test_tests.bzl +++ b/tests/base_rules/py_test/py_test_tests.bzl @@ -59,6 +59,7 @@ def _test_mac_requires_darwin_for_execution(name, config): config_settings = { "//command_line_option:cpu": "darwin_x86_64", "//command_line_option:crosstool_top": CROSSTOOL_TOP, + "//command_line_option:extra_execution_platforms": [MAC_X86_64], "//command_line_option:extra_toolchains": CC_TOOLCHAIN, "//command_line_option:platforms": [MAC_X86_64], }, @@ -92,6 +93,7 @@ def _test_non_mac_doesnt_require_darwin_for_execution(name, config): config_settings = { "//command_line_option:cpu": "k8", "//command_line_option:crosstool_top": CROSSTOOL_TOP, + "//command_line_option:extra_execution_platforms": [LINUX_X86_64], "//command_line_option:extra_toolchains": CC_TOOLCHAIN, "//command_line_option:platforms": [LINUX_X86_64], }, From 1492ae4b53c6ace19cfc67f542b574b2ccd7e40b Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Mon, 5 May 2025 18:29:59 +0200 Subject: [PATCH 069/156] fix: configure coverage helpers for test exec group (#2857) They are run on the test action's execution platform, which is resolved for the `test` exec group, not the default one. --- python/private/attributes.bzl | 4 ++-- python/private/py_executable.bzl | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/python/private/attributes.bzl b/python/private/attributes.bzl index 8543caba7b..98aba4eb23 100644 --- a/python/private/attributes.bzl +++ b/python/private/attributes.bzl @@ -397,14 +397,14 @@ COVERAGE_ATTRS = { "_collect_cc_coverage": lambda: attrb.Label( default = "@bazel_tools//tools/test:collect_cc_coverage", executable = True, - cfg = "exec", + cfg = config.exec(exec_group = "test"), ), # Magic attribute to make coverage work. There's no # docs about this; see TestActionBuilder.java "_lcov_merger": lambda: attrb.Label( default = configuration_field(fragment = "coverage", name = "output_generator"), executable = True, - cfg = "exec", + cfg = config.exec(exec_group = "test"), ), } diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index a8c669afd9..24be8dd2ad 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -78,7 +78,6 @@ EXECUTABLE_ATTRS = dicts.add( AGNOSTIC_EXECUTABLE_ATTRS, PY_SRCS_ATTRS, IMPORTS_ATTRS, - COVERAGE_ATTRS, { "interpreter_args": lambda: attrb.StringList( doc = """ @@ -1903,7 +1902,7 @@ def create_executable_rule_builder(implementation, **kwargs): """ builder = ruleb.Rule( implementation = implementation, - attrs = EXECUTABLE_ATTRS, + attrs = EXECUTABLE_ATTRS | (COVERAGE_ATTRS if kwargs.get("test") else {}), exec_groups = dict(REQUIRED_EXEC_GROUP_BUILDERS), # Mutable copy fragments = ["py", "bazel_py"], provides = [PyExecutableInfo, PyInfo] + _MaybeBuiltinPyInfo, From 63555e1fdf708b6a44f166aa5a3dfa344325e0d0 Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Tue, 6 May 2025 10:34:20 +0200 Subject: [PATCH 070/156] fix: fix test analysis error on macOS arm64 (#2860) Fixes: ``` ERROR: /Users/fmeum/git/rules_python/tests/pypi/env_marker_setting/BUILD.bazel:3:30: Illegal ambiguous match on configurable attribute "platform_machine" in //tests/pypi/env_marker_setting:test_expr_python_full_version_lt_negative_subject: @@platforms//cpu:aarch64 @@platforms//cpu:arm64 Multiple matches are not allowed unless one is unambiguously more specialized or they resolve to the same value. See https://bazel.build/reference/be/functions#select. ``` Work towards #2850. Work towards #2826. --- python/private/pypi/pep508_env.bzl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/private/pypi/pep508_env.bzl b/python/private/pypi/pep508_env.bzl index 3708c46f1d..d618535674 100644 --- a/python/private/pypi/pep508_env.bzl +++ b/python/private/pypi/pep508_env.bzl @@ -29,11 +29,13 @@ platform_machine_aliases = { # NOTE: There are many cpus, and unfortunately, the value isn't directly # accessible to Starlark. Using CcToolchain.cpu might work, though. +# Some targets are aliases and are omitted below as their value is implied +# by the target they resolve to. platform_machine_select_map = { "@platforms//cpu:aarch32": "aarch32", "@platforms//cpu:aarch64": "aarch64", - "@platforms//cpu:arm": "arm", - "@platforms//cpu:arm64": "arm64", + # @platforms//cpu:arm is an alias for @platforms//cpu:aarch32 + # @platforms//cpu:arm64 is an alias for @platforms//cpu:aarch64 "@platforms//cpu:arm64_32": "arm64_32", "@platforms//cpu:arm64e": "arm64e", "@platforms//cpu:armv6-m": "armv6-m", From 0b3d845ed1803ed27083f850c7c542bd2d3fc52c Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 6 May 2025 01:38:02 -0700 Subject: [PATCH 071/156] refactor: make env marker config available through target and flag (#2853) This factors creation of (most of) the env marker dict into a separate target and provides a label flag to allow customizing the target that provides it. This makes it easier for users to override how env marker values are computed. The `env_marker_setting` rule will still, if necessary, compute values from the toolchain, but existing keys (computed from the env marker config target) have precedence. The `EnvMarkerInfo` provider is the interface for implementing a custom env marker config target; it will be publically exposed in a subsequent PR. Along the way, unify how the env dict and defaults are set. Work towards https://github.com/bazel-contrib/rules_python/issues/2826 --- .../python/config_settings/index.md | 12 ++ docs/pypi-dependencies.md | 32 ++++- python/config_settings/BUILD.bazel | 7 ++ python/private/pypi/BUILD.bazel | 19 +++ python/private/pypi/env_marker_info.bzl | 26 ++++ python/private/pypi/env_marker_setting.bzl | 104 +++++----------- python/private/pypi/flags.bzl | 68 ++++++++++ python/private/pypi/pep508_env.bzl | 117 +++++++++++------- .../env_marker_setting_tests.bzl | 37 +++++- tests/support/support.bzl | 1 + 10 files changed, 300 insertions(+), 123 deletions(-) create mode 100644 python/private/pypi/env_marker_info.bzl diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md index ed6444298e..f4618ff967 100644 --- a/docs/api/rules_python/python/config_settings/index.md +++ b/docs/api/rules_python/python/config_settings/index.md @@ -159,6 +159,18 @@ Values: ::: :::: +::::{bzl:flag} pip_env_marker_config +The target that provides the values for pip env marker evaluation. + +Default: `//python/config_settings:_pip_env_marker_default_config` + +This flag points to a target providing {obj}`EnvMarkerInfo`, which determines +the values used when environment markers are resolved at build time. + +:::{versionadded} VERSION_NEXT_FEATURE +::: +:::: + ::::{bzl:flag} pip_whl Set what distributions are used in the `pip` integration. diff --git a/docs/pypi-dependencies.md b/docs/pypi-dependencies.md index 4ec40bc889..b3ae7fe594 100644 --- a/docs/pypi-dependencies.md +++ b/docs/pypi-dependencies.md @@ -338,7 +338,6 @@ leg of the dependency manually. For instance by making perhaps `apache-airflow-providers-common-sql`. -(bazel-downloader)= ### Multi-platform support Multi-platform support of cross-building the wheels can be done in two ways - either @@ -391,6 +390,31 @@ compatible indexes. This is only supported on `bzlmd`. ``` + + (bazel-downloader)= ### Bazel downloader and multi-platform wheel hub repository. @@ -487,3 +511,9 @@ Bazel will call this file like `cred_helper.sh get` and use the returned JSON to into whatever HTTP(S) request it performs against `example.com`. [rfc7617]: https://datatracker.ietf.org/doc/html/rfc7617 + + diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel index 872d7d1bda..24bbe665c7 100644 --- a/python/config_settings/BUILD.bazel +++ b/python/config_settings/BUILD.bazel @@ -220,3 +220,10 @@ string_flag( define_pypi_internal_flags( name = "define_pypi_internal_flags", ) + +label_flag( + name = "pip_env_marker_config", + build_setting_default = ":_pip_env_marker_default_config", + # NOTE: Only public because it is used in pip hub repos. + visibility = ["//visibility:public"], +) diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 9216134857..d5d897ef8c 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -71,6 +71,23 @@ bzl_library( ], ) +bzl_library( + name = "env_marker_info_bzl", + srcs = ["env_marker_info.bzl"], +) + +bzl_library( + name = "env_marker_setting_bzl", + srcs = ["env_marker_setting.bzl"], + deps = [ + ":env_marker_info_bzl", + ":pep508_env_bzl", + ":pep508_evaluate_bzl", + "//python/private:toolchain_types_bzl", + "@bazel_skylib//rules:common_settings", + ], +) + bzl_library( name = "evaluate_markers_bzl", srcs = ["evaluate_markers.bzl"], @@ -111,6 +128,8 @@ bzl_library( name = "flags_bzl", srcs = ["flags.bzl"], deps = [ + ":env_marker_info.bzl", + ":pep508_env_bzl", "//python/private:enum_bzl", "@bazel_skylib//rules:common_settings", ], diff --git a/python/private/pypi/env_marker_info.bzl b/python/private/pypi/env_marker_info.bzl new file mode 100644 index 0000000000..b483436d98 --- /dev/null +++ b/python/private/pypi/env_marker_info.bzl @@ -0,0 +1,26 @@ +"""Provider for implementing environment marker values.""" + +EnvMarkerInfo = provider( + doc = """ +The values to use during environment marker evaluation. + +:::{seealso} +The {obj}`--//python/config_settings:pip_env_marker_config` flag. +::: + +:::{versionadded} VERSION_NEXT_FEATURE +""", + fields = { + "env": """ +:type: dict[str, str] + +The values to use for environment markers when evaluating an expression. + +The keys and values should be compatible with the [PyPA dependency specifiers +specification](https://packaging.python.org/en/latest/specifications/dependency-specifiers/) + +Missing values will be set to the specification's defaults or computed using +available toolchain information. +""", + }, +) diff --git a/python/private/pypi/env_marker_setting.bzl b/python/private/pypi/env_marker_setting.bzl index bbc59ab110..2bfdf42ef0 100644 --- a/python/private/pypi/env_marker_setting.bzl +++ b/python/private/pypi/env_marker_setting.bzl @@ -2,14 +2,8 @@ load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") -load( - ":pep508_env.bzl", - "env_aliases", - "os_name_select_map", - "platform_machine_select_map", - "platform_system_select_map", - "sys_platform_select_map", -) +load(":env_marker_info.bzl", "EnvMarkerInfo") +load(":pep508_env.bzl", "create_env", "set_missing_env_defaults") load(":pep508_evaluate.bzl", "evaluate") # Use capitals to hint its not an actual boolean type. @@ -39,72 +33,37 @@ def env_marker_setting(*, name, expression, **kwargs): _env_marker_setting( name = name, expression = expression, - os_name = select(os_name_select_map), - sys_platform = select(sys_platform_select_map), - platform_machine = select(platform_machine_select_map), - platform_system = select(platform_system_select_map), - platform_release = select({ - "@platforms//os:osx": "USE_OSX_VERSION_FLAG", - "//conditions:default": "", - }), **kwargs ) def _env_marker_setting_impl(ctx): - env = {} + env = create_env() + env.update( + ctx.attr._env_marker_config_flag[EnvMarkerInfo].env, + ) runtime = ctx.toolchains[TARGET_TOOLCHAIN_TYPE].py3_runtime - if runtime.interpreter_version_info: - version_info = runtime.interpreter_version_info - env["python_version"] = "{major}.{minor}".format( - major = version_info.major, - minor = version_info.minor, - ) - full_version = _format_full_version(version_info) - env["python_full_version"] = full_version - env["implementation_version"] = full_version - else: - env["python_version"] = _get_flag(ctx.attr._python_version_major_minor_flag) - full_version = _get_flag(ctx.attr._python_full_version_flag) - env["python_full_version"] = full_version - env["implementation_version"] = full_version - - # We assume cpython if the toolchain doesn't specify because it's most - # likely to be true. - env["implementation_name"] = runtime.implementation_name or "cpython" - env["os_name"] = ctx.attr.os_name - env["sys_platform"] = ctx.attr.sys_platform - env["platform_machine"] = ctx.attr.platform_machine - - # The `platform_python_implementation` marker value is supposed to come - # from `platform.python_implementation()`, however, PEP 421 introduced - # `sys.implementation.name` and the `implementation_name` env marker to - # replace it. Per the platform.python_implementation docs, there's now - # essentially just two possible "registered" values: CPython or PyPy. - # Rather than add a field to the toolchain, we just special case the value - # from `sys.implementation.name` to handle the two documented values. - platform_python_impl = runtime.implementation_name - if platform_python_impl == "cpython": - platform_python_impl = "CPython" - elif platform_python_impl == "pypy": - platform_python_impl = "PyPy" - env["platform_python_implementation"] = platform_python_impl - - # NOTE: Platform release for Android will be Android version: - # https://peps.python.org/pep-0738/#platform - # Similar for iOS: - # https://peps.python.org/pep-0730/#platform - platform_release = ctx.attr.platform_release - if platform_release == "USE_OSX_VERSION_FLAG": - platform_release = _get_flag(ctx.attr._pip_whl_osx_version_flag) - env["platform_release"] = platform_release - env["platform_system"] = ctx.attr.platform_system - - # For lack of a better option, just use an empty string for now. - env["platform_version"] = "" - - env.update(env_aliases()) + if "python_version" not in env: + if runtime.interpreter_version_info: + version_info = runtime.interpreter_version_info + env["python_version"] = "{major}.{minor}".format( + major = version_info.major, + minor = version_info.minor, + ) + full_version = _format_full_version(version_info) + env["python_full_version"] = full_version + env["implementation_version"] = full_version + else: + env["python_version"] = _get_flag(ctx.attr._python_version_major_minor_flag) + full_version = _get_flag(ctx.attr._python_full_version_flag) + env["python_full_version"] = full_version + env["implementation_version"] = full_version + + if "implementation_name" not in env and runtime.implementation_name: + env["implementation_name"] = runtime.implementation_name + + set_missing_env_defaults(env) if evaluate(ctx.attr.expression, env = env): value = _ENV_MARKER_TRUE else: @@ -125,14 +84,9 @@ for the specification of behavior. mandatory = True, doc = "Environment marker expression to evaluate.", ), - "os_name": attr.string(), - "platform_machine": attr.string(), - "platform_release": attr.string(), - "platform_system": attr.string(), - "sys_platform": attr.string(), - "_pip_whl_osx_version_flag": attr.label( - default = "//python/config_settings:pip_whl_osx_version", - providers = [[BuildSettingInfo], [config_common.FeatureFlagInfo]], + "_env_marker_config_flag": attr.label( + default = "//python/config_settings:pip_env_marker_config", + providers = [EnvMarkerInfo], ), "_python_full_version_flag": attr.label( default = "//python/config_settings:python_version", diff --git a/python/private/pypi/flags.bzl b/python/private/pypi/flags.bzl index a25579a2b8..037383910e 100644 --- a/python/private/pypi/flags.bzl +++ b/python/private/pypi/flags.bzl @@ -20,6 +20,15 @@ unnecessary files when all that are needed are flag definitions. load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo", "string_flag") load("//python/private:enum.bzl", "enum") +load(":env_marker_info.bzl", "EnvMarkerInfo") +load( + ":pep508_env.bzl", + "create_env", + "os_name_select_map", + "platform_machine_select_map", + "platform_system_select_map", + "sys_platform_select_map", +) # Determines if we should use whls for third party # @@ -82,6 +91,10 @@ def define_pypi_internal_flags(name): visibility = ["//visibility:public"], ) + _default_env_marker_config( + name = "_pip_env_marker_default_config", + ) + def _allow_wheels_flag_impl(ctx): input = ctx.attr._setting[BuildSettingInfo].value value = "yes" if input in ["auto", "only"] else "no" @@ -97,3 +110,58 @@ This rule allows us to greatly reduce the number of config setting targets at no if we are duplicating some of the functionality of the `native.config_setting`. """, ) + +def _default_env_marker_config(**kwargs): + _env_marker_config( + os_name = select(os_name_select_map), + sys_platform = select(sys_platform_select_map), + platform_machine = select(platform_machine_select_map), + platform_system = select(platform_system_select_map), + platform_release = select({ + "@platforms//os:osx": "USE_OSX_VERSION_FLAG", + "//conditions:default": "", + }), + **kwargs + ) + +def _env_marker_config_impl(ctx): + env = create_env() + env["os_name"] = ctx.attr.os_name + env["sys_platform"] = ctx.attr.sys_platform + env["platform_machine"] = ctx.attr.platform_machine + + # NOTE: Platform release for Android will be Android version: + # https://peps.python.org/pep-0738/#platform + # Similar for iOS: + # https://peps.python.org/pep-0730/#platform + platform_release = ctx.attr.platform_release + if platform_release == "USE_OSX_VERSION_FLAG": + platform_release = _get_flag(ctx.attr._pip_whl_osx_version_flag) + env["platform_release"] = platform_release + env["platform_system"] = ctx.attr.platform_system + + # NOTE: We intentionally do not call set_missing_env_defaults() here because + # `env_marker_setting()` computes missing values using the toolchain. + return [EnvMarkerInfo(env = env)] + +_env_marker_config = rule( + implementation = _env_marker_config_impl, + attrs = { + "os_name": attr.string(), + "platform_machine": attr.string(), + "platform_release": attr.string(), + "platform_system": attr.string(), + "sys_platform": attr.string(), + "_pip_whl_osx_version_flag": attr.label( + default = "//python/config_settings:pip_whl_osx_version", + providers = [[BuildSettingInfo], [config_common.FeatureFlagInfo]], + ), + }, +) + +def _get_flag(t): + if config_common.FeatureFlagInfo in t: + return t[config_common.FeatureFlagInfo].value + if BuildSettingInfo in t: + return t[BuildSettingInfo].value + fail("Should not occur: {} does not have necessary providers") diff --git a/python/private/pypi/pep508_env.bzl b/python/private/pypi/pep508_env.bzl index d618535674..a6efb3c50c 100644 --- a/python/private/pypi/pep508_env.bzl +++ b/python/private/pypi/pep508_env.bzl @@ -66,23 +66,23 @@ platform_machine_select_map = { # Platform system returns results from the `uname` call. _platform_system_values = { + # See https://peps.python.org/pep-0738/#platform + "android": "Android", + "freebsd": "FreeBSD", + # See https://peps.python.org/pep-0730/#platform + # NOTE: Per Pep 730, "iPadOS" is also an acceptable value + "ios": "iOS", "linux": "Linux", + "netbsd": "NetBSD", + "openbsd": "OpenBSD", "osx": "Darwin", "windows": "Windows", } platform_system_select_map = { - # See https://peps.python.org/pep-0738/#platform - "@platforms//os:android": "Android", - "@platforms//os:freebsd": "FreeBSD", - # See https://peps.python.org/pep-0730/#platform - # NOTE: Per Pep 730, "iPadOS" is also an acceptable value - "@platforms//os:ios": "iOS", - "@platforms//os:linux": "Linux", - "@platforms//os:netbsd": "NetBSD", - "@platforms//os:openbsd": "OpenBSD", - "@platforms//os:osx": "Darwin", - "@platforms//os:windows": "Windows", + "@platforms//os:{}".format(bazel_os): py_system + for bazel_os, py_system in _platform_system_values.items() +} | { # The value is empty string if it cannot be determined: # https://docs.python.org/3/library/platform.html#platform.machine "//conditions:default": "", @@ -114,33 +114,36 @@ platform_system_select_map = { # # We are using only the subset that we actually support. _sys_platform_values = { + # These values are decided by the sys.platform docs. + "android": "android", + "emscripten": "emscripten", + # NOTE: The below values are approximations. The sys.platform() docs + # don't have documented values for these OSes. Per docs, the + # sys.platform() value reflects the OS at the time Python was *built* + # instead of the runtime (target) OS value. + "freebsd": "freebsd", + "ios": "ios", "linux": "linux", + "openbsd": "openbsd", "osx": "darwin", + "wasi": "wasi", "windows": "win32", } -# Taken from -# https://docs.python.org/3/library/sys.html#sys.platform sys_platform_select_map = { - # These values are decided by the sys.platform docs. - "@platforms//os:android": "android", - "@platforms//os:emscripten": "emscripten", - # NOTE: The below values are approximations. The sys.platform() docs - # don't have documented values for these OSes. Per docs, the - # sys.platform() value reflects the OS at the time Python was *built* - # instead of the runtime (target) OS value. - "@platforms//os:freebsd": "freebsd", - "@platforms//os:ios": "ios", - "@platforms//os:linux": "linux", - "@platforms//os:openbsd": "openbsd", - "@platforms//os:osx": "darwin", - "@platforms//os:wasi": "wasi", - "@platforms//os:windows": "win32", + "@platforms//os:{}".format(bazel_os): py_platform + for bazel_os, py_platform in _sys_platform_values.items() +} | { # For lack of a better option, use empty string. No standard doc/spec # about sys_platform value. "//conditions:default": "", } +# The "java" value is documented, but with Jython defunct, +# shouldn't occur in practice. +# The os.name value is technically a property of the runtime, not the +# targetted runtime OS, but the distinction shouldn't matter if +# things are properly configured. _os_name_values = { "linux": "posix", "osx": "posix", @@ -148,18 +151,18 @@ _os_name_values = { } os_name_select_map = { - # The "java" value is documented, but with Jython defunct, - # shouldn't occur in practice. - # The os.name value is technically a property of the runtime, not the - # targetted runtime OS, but the distinction shouldn't matter if - # things are properly configured. - "@platforms//os:windows": "nt", + "@platforms//os:{}".format(bazel_os): py_os + for bazel_os, py_os in _os_name_values.items() +} | { "//conditions:default": "posix", } def env(target_platform, *, extra = None): """Return an env target platform + NOTE: This is for use during the loading phase. For the analysis phase, + `env_marker_setting()` constructs the env dict. + Args: target_platform: {type}`str` the target platform identifier, e.g. `cp33_linux_aarch64` @@ -168,16 +171,9 @@ def env(target_platform, *, extra = None): Returns: A dict that can be used as `env` in the marker evaluation. """ - - # TODO @aignas 2025-02-13: consider moving this into config settings. - - env = {"extra": extra} if extra != None else {} - env = env | { - "implementation_name": "cpython", - "platform_python_implementation": "CPython", - "platform_release": "", - "platform_version": "", - } + env = create_env() + if extra != None: + env["extra"] = extra if type(target_platform) == type(""): target_platform = platform_from_str(target_platform, python_version = "") @@ -198,13 +194,42 @@ def env(target_platform, *, extra = None): "platform_system": _platform_system_values.get(os, ""), "sys_platform": _sys_platform_values.get(os, ""), } + set_missing_env_defaults(env) - # This is split by topic - return env | env_aliases() + return env -def env_aliases(): +def create_env(): return { + # This is split by topic "_aliases": { "platform_machine": platform_machine_aliases, }, } + +def set_missing_env_defaults(env): + """Sets defaults based on existing values. + + Args: + env: dict; NOTE: modified in-place + """ + if "implementation_name" not in env: + # Use cpython as the default because it's likely the correct value. + env["implementation_name"] = "cpython" + if "platform_python_implementation" not in env: + # The `platform_python_implementation` marker value is supposed to come + # from `platform.python_implementation()`, however, PEP 421 introduced + # `sys.implementation.name` and the `implementation_name` env marker to + # replace it. Per the platform.python_implementation docs, there's now + # essentially just two possible "registered" values: CPython or PyPy. + # Rather than add a field to the toolchain, we just special case the value + # from `sys.implementation.name` to handle the two documented values. + platform_python_impl = env["implementation_name"] + if platform_python_impl == "cpython": + platform_python_impl = "CPython" + elif platform_python_impl == "pypy": + platform_python_impl = "PyPy" + env["platform_python_implementation"] = platform_python_impl + if "platform_release" not in env: + env["platform_release"] = "" + if "platform_version" not in env: + env["platform_version"] = "0" diff --git a/tests/pypi/env_marker_setting/env_marker_setting_tests.bzl b/tests/pypi/env_marker_setting/env_marker_setting_tests.bzl index 549c15c20b..e16f2c8ef6 100644 --- a/tests/pypi/env_marker_setting/env_marker_setting_tests.bzl +++ b/tests/pypi/env_marker_setting/env_marker_setting_tests.bzl @@ -3,11 +3,46 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") load("@rules_testing//lib:util.bzl", "TestingAspectInfo") +load("//python/private/pypi:env_marker_info.bzl", "EnvMarkerInfo") # buildifier: disable=bzl-visibility load("//python/private/pypi:env_marker_setting.bzl", "env_marker_setting") # buildifier: disable=bzl-visibility -load("//tests/support:support.bzl", "PYTHON_VERSION") +load("//tests/support:support.bzl", "PIP_ENV_MARKER_CONFIG", "PYTHON_VERSION") + +def _custom_env_markers_impl(ctx): + _ = ctx # @unused + return [EnvMarkerInfo(env = { + "os_name": "testos", + })] + +_custom_env_markers = rule( + implementation = _custom_env_markers_impl, +) _tests = [] +def _test_custom_env_markers(name): + def _impl(env, target): + env.expect.where( + expression = target[TestingAspectInfo].attrs.expression, + ).that_str( + target[config_common.FeatureFlagInfo].value, + ).equals("TRUE") + + env_marker_setting( + name = name + "_subject", + expression = "os_name == 'testos'", + ) + _custom_env_markers(name = name + "_env") + analysis_test( + name = name, + impl = _impl, + target = name + "_subject", + config_settings = { + PIP_ENV_MARKER_CONFIG: str(Label(name + "_env")), + }, + ) + +_tests.append(_test_custom_env_markers) + def _test_expr(name): def impl(env, target): env.expect.where( diff --git a/tests/support/support.bzl b/tests/support/support.bzl index 6330155d8c..7bab263c66 100644 --- a/tests/support/support.bzl +++ b/tests/support/support.bzl @@ -37,6 +37,7 @@ CROSSTOOL_TOP = Label("//tests/support/cc_toolchains:cc_toolchain_suite") ADD_SRCS_TO_RUNFILES = str(Label("//python/config_settings:add_srcs_to_runfiles")) BOOTSTRAP_IMPL = str(Label("//python/config_settings:bootstrap_impl")) EXEC_TOOLS_TOOLCHAIN = str(Label("//python/config_settings:exec_tools_toolchain")) +PIP_ENV_MARKER_CONFIG = str(Label("//python/config_settings:pip_env_marker_config")) PRECOMPILE = str(Label("//python/config_settings:precompile")) PRECOMPILE_SOURCE_RETENTION = str(Label("//python/config_settings:precompile_source_retention")) PYC_COLLECTION = str(Label("//python/config_settings:pyc_collection")) From 9f3512fe0cc6d7229170e45724e22e64be0b8300 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 6 May 2025 11:33:12 -0700 Subject: [PATCH 072/156] feat: default to bootstrap script for non-windows (#2858) This makes non-Windows use the script bootstrap by default. It's been a couple releases without any reported issues, so it seems ready to become the default. Work towards https://github.com/bazel-contrib/rules_python/issues/2156 --- CHANGELOG.md | 8 ++++ MODULE.bazel | 7 +++- .../python/config_settings/index.md | 9 ++++ internal_dev_setup.bzl | 3 ++ python/config_settings/BUILD.bazel | 2 +- python/private/config_settings.bzl | 17 ++++++-- python/private/internal_dev_deps.bzl | 2 + python/private/runtime_env_repo.bzl | 41 +++++++++++++++++++ .../runtime_env_toolchain_interpreter.sh | 3 ++ tests/runtime_env_toolchain/BUILD.bazel | 4 ++ 10 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 python/private/runtime_env_repo.bzl diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d73613a07..8fdb7edd6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,14 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-changed} ### Changed +* If using the (deprecated) autodetecting/runtime_env toolchain, then the Python + version specified at build-time *must* match the Python version used at + runtime (the {obj}`--@rules_python//python/config_settings:python_version` + flag and the {attr}`python_version` attribute control the build-time version + for a target). If they don't match, dependencies won't be importable. (Such a + misconfiguration was unlikely to work to begin with; this is called out as an + FYI). +* (rules) {obj}`--bootstrap_impl=script` is the default for non-Windows. * (rules) On Windows, {obj}`--bootstrap_impl=system_python` is forced. This allows setting `--bootstrap_impl=script` in bazelrc for mixed-platform environments. diff --git a/MODULE.bazel b/MODULE.bazel index c649896344..d0f7cc4afa 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -98,7 +98,12 @@ internal_dev_deps = use_extension( "internal_dev_deps", dev_dependency = True, ) -use_repo(internal_dev_deps, "buildkite_config", "wheel_for_testing") +use_repo( + internal_dev_deps, + "buildkite_config", + "rules_python_runtime_env_tc_info", + "wheel_for_testing", +) # Add gazelle plugin so that we can run the gazelle example as an e2e integration # test and include the distribution files. diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md index f4618ff967..ae84d40b13 100644 --- a/docs/api/rules_python/python/config_settings/index.md +++ b/docs/api/rules_python/python/config_settings/index.md @@ -245,6 +245,10 @@ Values: ::::{bzl:flag} bootstrap_impl Determine how programs implement their startup process. +The default for this depends on the platform: +* Windows: `system_python` (**always** used) +* Other: `script` + Values: * `system_python`: Use a bootstrap that requires a system Python available in order to start programs. This requires @@ -269,6 +273,11 @@ instead. :::{versionadded} 0.33.0 ::: +:::{versionchanged} VERSION_NEXT_FEATURE +* The default for non-Windows changed from `system_python` to `script`. +* On Windows, the value is forced to `system_python`. +::: + :::: ::::{bzl:flag} current_config diff --git a/internal_dev_setup.bzl b/internal_dev_setup.bzl index fc38e3f9c5..f33908049f 100644 --- a/internal_dev_setup.bzl +++ b/internal_dev_setup.bzl @@ -24,6 +24,7 @@ load("@rules_shell//shell:repositories.bzl", "rules_shell_dependencies", "rules_ load("//:version.bzl", "SUPPORTED_BAZEL_VERSIONS") load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS") load("//python/private:pythons_hub.bzl", "hub_repo") # buildifier: disable=bzl-visibility +load("//python/private:runtime_env_repo.bzl", "runtime_env_repo") # buildifier: disable=bzl-visibility load("//python/private/pypi:deps.bzl", "pypi_deps") # buildifier: disable=bzl-visibility def rules_python_internal_setup(): @@ -40,6 +41,8 @@ def rules_python_internal_setup(): python_versions = sorted(TOOL_VERSIONS.keys()), ) + runtime_env_repo(name = "rules_python_runtime_env_tc_info") + pypi_deps() bazel_skylib_workspace() diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel index 24bbe665c7..1772a3403e 100644 --- a/python/config_settings/BUILD.bazel +++ b/python/config_settings/BUILD.bazel @@ -90,7 +90,7 @@ string_flag( rp_string_flag( name = "bootstrap_impl", - build_setting_default = BootstrapImplFlag.SYSTEM_PYTHON, + build_setting_default = BootstrapImplFlag.SCRIPT, override = select({ # Windows doesn't yet support bootstrap=script, so force disable it ":_is_windows": BootstrapImplFlag.SYSTEM_PYTHON, diff --git a/python/private/config_settings.bzl b/python/private/config_settings.bzl index 2cf7968061..1685195b78 100644 --- a/python/private/config_settings.bzl +++ b/python/private/config_settings.bzl @@ -225,10 +225,19 @@ def is_python_version_at_least(name, **kwargs): ) def _python_version_at_least_impl(ctx): - at_least = tuple(ctx.attr.at_least.split(".")) - current = tuple( - ctx.attr._major_minor[config_common.FeatureFlagInfo].value.split("."), - ) + flag_value = ctx.attr._major_minor[config_common.FeatureFlagInfo].value + + # CI is, somehow, getting an empty string for the current flag value. + # How isn't clear. + if not flag_value: + return [config_common.FeatureFlagInfo(value = "no")] + + current = tuple([ + int(x) + for x in flag_value.split(".") + ]) + at_least = tuple([int(x) for x in ctx.attr.at_least.split(".")]) + value = "yes" if current >= at_least else "no" return [config_common.FeatureFlagInfo(value = value)] diff --git a/python/private/internal_dev_deps.bzl b/python/private/internal_dev_deps.bzl index 2a3b84e7df..4f2cca0b42 100644 --- a/python/private/internal_dev_deps.bzl +++ b/python/private/internal_dev_deps.bzl @@ -15,6 +15,7 @@ load("@bazel_ci_rules//:rbe_repo.bzl", "rbe_preconfig") load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") +load(":runtime_env_repo.bzl", "runtime_env_repo") def _internal_dev_deps_impl(mctx): _ = mctx # @unused @@ -37,6 +38,7 @@ def _internal_dev_deps_impl(mctx): name = "buildkite_config", toolchain = "ubuntu1804-bazel-java11", ) + runtime_env_repo(name = "rules_python_runtime_env_tc_info") internal_dev_deps = module_extension( implementation = _internal_dev_deps_impl, diff --git a/python/private/runtime_env_repo.bzl b/python/private/runtime_env_repo.bzl new file mode 100644 index 0000000000..cade1968bb --- /dev/null +++ b/python/private/runtime_env_repo.bzl @@ -0,0 +1,41 @@ +"""Internal setup to help the runtime_env toolchain.""" + +load("//python/private:repo_utils.bzl", "repo_utils") + +def _runtime_env_repo_impl(rctx): + pyenv = repo_utils.which_unchecked(rctx, "pyenv").binary + if pyenv != None: + pyenv_version_file = repo_utils.execute_checked( + rctx, + op = "GetPyenvVersionFile", + arguments = [pyenv, "version-file"], + ).stdout.strip() + + # When pyenv is used, the version file is what decided the + # version used. Watch it so we compute the correct value if the + # user changes it. + rctx.watch(pyenv_version_file) + + version = repo_utils.execute_checked( + rctx, + op = "GetPythonVersion", + arguments = [ + "python3", + "-I", + "-c", + """import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")""", + ], + environment = { + # Prevent the user's current shell from influencing the result. + # This envvar won't be present when a test is run. + # NOTE: This should be None, but Bazel 7 doesn't support None + # values. Thankfully, pyenv treats empty string the same as missing. + "PYENV_VERSION": "", + }, + ).stdout.strip() + rctx.file("info.bzl", "PYTHON_VERSION = '{}'\n".format(version)) + rctx.file("BUILD.bazel", "") + +runtime_env_repo = repository_rule( + implementation = _runtime_env_repo_impl, +) diff --git a/python/private/runtime_env_toolchain_interpreter.sh b/python/private/runtime_env_toolchain_interpreter.sh index 6159d4f38c..7b3ec598b2 100755 --- a/python/private/runtime_env_toolchain_interpreter.sh +++ b/python/private/runtime_env_toolchain_interpreter.sh @@ -68,6 +68,9 @@ if [ -e "$self_dir/pyvenv.cfg" ] || [ -e "$self_dir/../pyvenv.cfg" ]; then ;; esac + if [ ! -e "$PYTHON_BIN" ]; then + die "ERROR: Python interpreter does not exist: $PYTHON_BIN" + fi # PYTHONEXECUTABLE is also used because `exec -a` doesn't fully trick the # pyenv wrappers. # NOTE: The PYTHONEXECUTABLE envvar only works for non-Mac starting in Python 3.11 diff --git a/tests/runtime_env_toolchain/BUILD.bazel b/tests/runtime_env_toolchain/BUILD.bazel index 59ca93ba49..ad2bd4eeb5 100644 --- a/tests/runtime_env_toolchain/BUILD.bazel +++ b/tests/runtime_env_toolchain/BUILD.bazel @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +load("@rules_python_runtime_env_tc_info//:info.bzl", "PYTHON_VERSION") load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") load("//tests/support:support.bzl", "CC_TOOLCHAIN") load(":runtime_env_toolchain_tests.bzl", "runtime_env_toolchain_test_suite") @@ -30,6 +31,9 @@ py_reconfig_test( CC_TOOLCHAIN, ], main = "toolchain_runs_test.py", + # With bootstrap=script, the build version must match the runtime version + # because the venv has the version in the lib/site-packages dir name. + python_version = PYTHON_VERSION, # Our RBE has Python 3.6, which is too old for the language features # we use now. Using the runtime-env toolchain on RBE is pretty # questionable anyways. From 9dfa3abba293488a9a1899832a340f7b44525cad Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Thu, 8 May 2025 16:12:17 +0900 Subject: [PATCH 073/156] fix(pypi): fix a typo in parse_simpleapi_html (#2866) It seems that the integration tests that I thought were covering this had the same time. Added an assertion to the unit tests as well Fixes #2863. --- CHANGELOG.md | 11 +++++++++++ python/private/pypi/parse_simpleapi_html.bzl | 6 +++--- .../parse_simpleapi_html_tests.bzl | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fdb7edd6a..5f67c8a5ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,17 @@ END_UNRELEASED_TEMPLATE ### Removed * Nothing removed. +{#1-4-1} +## [1.4.1] - 2025-05-08 + +[1.4.1]: https://github.com/bazel-contrib/rules_python/releases/tag/1.4.1 + +{#1-4-1-fixed} +### Fixed +* (pypi) Fix a typo not allowing users to benefit from using the downloader when the hashes in the + requirements file are not present. Fixes + [#2863](https://github.com/bazel-contrib/rules_python/issues/2863). + {#1-4-0} ## [1.4.0] - 2025-04-19 diff --git a/python/private/pypi/parse_simpleapi_html.bzl b/python/private/pypi/parse_simpleapi_html.bzl index 8c6f739fe3..a41f0750c4 100644 --- a/python/private/pypi/parse_simpleapi_html.bzl +++ b/python/private/pypi/parse_simpleapi_html.bzl @@ -52,7 +52,7 @@ def parse_simpleapi_html(*, url, content): # Each line follows the following pattern # filename
- sha256_by_version = {} + sha256s_by_version = {} for line in lines[1:]: dist_url, _, tail = line.partition("#sha256=") dist_url = _absolute_url(url, dist_url) @@ -65,7 +65,7 @@ def parse_simpleapi_html(*, url, content): head, _, _ = tail.rpartition("") maybe_metadata, _, filename = head.rpartition(">") version = _version(filename) - sha256_by_version.setdefault(version, []).append(sha256) + sha256s_by_version.setdefault(version, []).append(sha256) metadata_sha256 = "" metadata_url = "" @@ -102,7 +102,7 @@ def parse_simpleapi_html(*, url, content): return struct( sdists = sdists, whls = whls, - sha256_by_version = sha256_by_version, + sha256s_by_version = sha256s_by_version, ) _SDIST_EXTS = [ diff --git a/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl b/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl index 191079d214..b96d02f990 100644 --- a/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl +++ b/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl @@ -86,6 +86,7 @@ def _test_sdist(env): got = parse_simpleapi_html(url = input.url, content = html) env.expect.that_collection(got.sdists).has_size(1) env.expect.that_collection(got.whls).has_size(0) + env.expect.that_collection(got.sha256s_by_version).has_size(1) if not got: fail("expected at least one element, but did not get anything from:\n{}".format(html)) From a2ff7daba62da590d7395701a145acd900f29908 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 10:20:38 +0900 Subject: [PATCH 074/156] build(deps): bump more-itertools from 10.5.0 to 10.7.0 in /tools/publish (#2841) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [more-itertools](https://github.com/more-itertools/more-itertools) from 10.5.0 to 10.7.0.
Release notes

Sourced from more-itertools's releases.

Version 10.7.0

See the change log here for details.

Version 10.6.0

  • New functions:

    • is_prime and nth_prime were added (thanks to JamesParrott and rhettinger)
    • loops was added (thanks to rhettinger)
  • Changes to existing functions:

    • factor was optimized to handle larger inputs and use less memory (thanks to rhettinger)
    • spy was optimized to enable nested calls (thanks to rhettinger)
    • polynomial_from_roots was made non-recursive and able to handle larger numbers of roots (thanks to pochmann3 and rhettinger)
    • is_sorted now only relies on less than comparisons (thanks to rhettinger)
    • The docstring for outer_product was improved (thanks to rhettinger)
    • The type annotations for sample were improved (thanks to rhettinger)
  • Other changes:

    • Python 3.13 is officially supported. Python 3.8 is no longer officially supported. (thanks to hugovk, JamesParrott, and stankudrow)
    • mypy checks were fixed (thanks to JamesParrott)
Commits
  • 28ab736 Merge pull request #977 from more-itertools/version-10.7.0
  • 4c1a0c7 Bump version: 10.6.0 → 10.7.0
  • f2d5c9f Late-breaking changes for 10.7.0
  • 5d5a9e6 Merge remote-tracking branch 'origin/master' into version-10.7.0
  • 8988de6 Merge pull request #975 from rhettinger/groupby_transform_overloads
  • c925c2e Fix inner Iterable types as well
  • cc38c74 Fix #974: Inconsistent @​overload signatures
  • 3742de9 Merge pull request #972 from ricbit/master
  • c904030 Fix some typos
  • 6d0fe02 Merge pull request #971 from rhettinger/small_doc_edits
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=more-itertools&package-manager=pip&previous-version=10.5.0&new-version=10.7.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tools/publish/requirements_darwin.txt | 6 +++--- tools/publish/requirements_linux.txt | 6 +++--- tools/publish/requirements_universal.txt | 6 +++--- tools/publish/requirements_windows.txt | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tools/publish/requirements_darwin.txt b/tools/publish/requirements_darwin.txt index eaec72c01c..483f88444e 100644 --- a/tools/publish/requirements_darwin.txt +++ b/tools/publish/requirements_darwin.txt @@ -142,9 +142,9 @@ mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -more-itertools==10.5.0 \ - --hash=sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef \ - --hash=sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6 +more-itertools==10.7.0 \ + --hash=sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3 \ + --hash=sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e # via # jaraco-classes # jaraco-functools diff --git a/tools/publish/requirements_linux.txt b/tools/publish/requirements_linux.txt index 5fdc742a88..62dbf1eb77 100644 --- a/tools/publish/requirements_linux.txt +++ b/tools/publish/requirements_linux.txt @@ -250,9 +250,9 @@ mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -more-itertools==10.5.0 \ - --hash=sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef \ - --hash=sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6 +more-itertools==10.7.0 \ + --hash=sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3 \ + --hash=sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e # via # jaraco-classes # jaraco-functools diff --git a/tools/publish/requirements_universal.txt b/tools/publish/requirements_universal.txt index 97cbef0221..e4e876b176 100644 --- a/tools/publish/requirements_universal.txt +++ b/tools/publish/requirements_universal.txt @@ -250,9 +250,9 @@ mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -more-itertools==10.5.0 \ - --hash=sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef \ - --hash=sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6 +more-itertools==10.7.0 \ + --hash=sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3 \ + --hash=sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e # via # jaraco-classes # jaraco-functools diff --git a/tools/publish/requirements_windows.txt b/tools/publish/requirements_windows.txt index 458414009e..043de9ecb1 100644 --- a/tools/publish/requirements_windows.txt +++ b/tools/publish/requirements_windows.txt @@ -142,9 +142,9 @@ mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -more-itertools==10.5.0 \ - --hash=sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef \ - --hash=sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6 +more-itertools==10.7.0 \ + --hash=sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3 \ + --hash=sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e # via # jaraco-classes # jaraco-functools From efc7589af6ba7fddf249b082ebfa29d7e260e0e6 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 11 May 2025 11:27:25 +0900 Subject: [PATCH 075/156] fix(pypi): finish PEP508/PEP440 impl for version matching (#2856) This reuses the previous work by @vonschultz who implemented a PEP440 version normalizer. We extend it and use it in the PEP508 marker evaluation. Summary: - Extend the normalization parser to output individual parts of the versions to the parsing context. - Re-implement all of the version comparison calls to use the parsed version. - Add extra validation for `.*` usage in the environment markers - Fallback to non-version matching in the environment markers if one of the sides is not a version. - Rename the original normalizer file to `version.bzl` because as far as Python is concerned this is the only version that there can be. We could in theory probably reuse this in other code where we are parsing the Python interpreter version many times, but this is left for the future. Fixes #2826 Work towards #2821 --------- Co-authored-by: Richard Levasseur Co-authored-by: Richard Levasseur --- .bazelrc | 4 +- CHANGELOG.md | 6 +- python/BUILD.bazel | 2 +- python/private/BUILD.bazel | 7 +- python/private/py_wheel.bzl | 8 +- python/private/pypi/BUILD.bazel | 1 + python/private/pypi/pep508_evaluate.bzl | 58 +-- python/private/semver.bzl | 27 -- ...wheel_normalize_pep440.bzl => version.bzl} | 379 +++++++++++++++++- tests/py_wheel/py_wheel_tests.bzl | 101 ----- tests/pypi/pep508/evaluate_tests.bzl | 127 ++++-- tests/semver/semver_test.bzl | 18 - tests/version/BUILD.bazel | 3 + tests/version/version_test.bzl | 157 ++++++++ 14 files changed, 641 insertions(+), 257 deletions(-) rename python/private/{py_wheel_normalize_pep440.bzl => version.bzl} (52%) create mode 100644 tests/version/BUILD.bazel create mode 100644 tests/version/version_test.bzl diff --git a/.bazelrc b/.bazelrc index d2e0721526..4e6f2fa187 100644 --- a/.bazelrc +++ b/.bazelrc @@ -4,8 +4,8 @@ # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it) # To update these lines, execute # `bazel run @rules_bazel_integration_test//tools:update_deleted_packages` -build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma -query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma +build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma +query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma test --test_output=errors diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f67c8a5ec..aa7fc9d415 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,9 +94,9 @@ END_UNRELEASED_TEMPLATE (the default), the subprocess's stdout/stderr will be logged. * (toolchains) Local toolchains can be activated with custom flags. See [Conditionally using local toolchains] docs for how to configure. -* (pypi) `RULES_PYTHON_ENABLE_PIPSTAR` environment variable: when `1`, the Starlark - implementation of wheel METADATA parsing is used (which has improved multi-platform - build support). +* (pypi) Starlark-based evaluation of environment markers (requirements.txt conditionals) + available (not enabled by default) for improved multi-platform build support. + Set the `RULES_PYTHON_ENABLE_PIPSTAR=1` environment variable to enable it. {#v0-0-0-removed} ### Removed diff --git a/python/BUILD.bazel b/python/BUILD.bazel index 3389a0dacc..867c43478a 100644 --- a/python/BUILD.bazel +++ b/python/BUILD.bazel @@ -93,9 +93,9 @@ bzl_library( "//python/private:bzlmod_enabled_bzl", "//python/private:py_package.bzl", "//python/private:py_wheel_bzl", - "//python/private:py_wheel_normalize_pep440.bzl", "//python/private:stamp_bzl", "//python/private:util_bzl", + "//python/private:version.bzl", "@bazel_skylib//rules:native_binary", ], ) diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index 9cc8ffc62c..e72a8fcaa7 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -658,6 +658,11 @@ bzl_library( ], ) +bzl_library( + name = "version_bzl", + srcs = ["version.bzl"], +) + bzl_library( name = "version_label_bzl", srcs = ["version_label.bzl"], @@ -701,7 +706,7 @@ exports_files( "repack_whl.py", "py_package.bzl", "py_wheel.bzl", - "py_wheel_normalize_pep440.bzl", + "version.bzl", "reexports.bzl", "stamp.bzl", "util.bzl", diff --git a/python/private/py_wheel.bzl b/python/private/py_wheel.bzl index c196ca6ad0..ffc24f6846 100644 --- a/python/private/py_wheel.bzl +++ b/python/private/py_wheel.bzl @@ -16,8 +16,8 @@ load(":py_info.bzl", "PyInfo") load(":py_package.bzl", "py_package_lib") -load(":py_wheel_normalize_pep440.bzl", "normalize_pep440") load(":stamp.bzl", "is_stamping_enabled") +load(":version.bzl", "version") PyWheelInfo = provider( doc = "Information about a wheel produced by `py_wheel`", @@ -306,11 +306,11 @@ def _input_file_to_arg(input_file): def _py_wheel_impl(ctx): abi = _replace_make_variables(ctx.attr.abi, ctx) python_tag = _replace_make_variables(ctx.attr.python_tag, ctx) - version = _replace_make_variables(ctx.attr.version, ctx) + version_str = _replace_make_variables(ctx.attr.version, ctx) filename_segments = [ _escape_filename_distribution_name(ctx.attr.distribution), - normalize_pep440(version), + version.normalize(version_str), _escape_filename_segment(python_tag), _escape_filename_segment(abi), _escape_filename_segment(ctx.attr.platform), @@ -343,7 +343,7 @@ def _py_wheel_impl(ctx): args = ctx.actions.args() args.add("--name", ctx.attr.distribution) - args.add("--version", version) + args.add("--version", version_str) args.add("--python_tag", python_tag) args.add("--abi", abi) args.add("--platform", ctx.attr.platform) diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index d5d897ef8c..f541cbe98b 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -251,6 +251,7 @@ bzl_library( srcs = ["pep508_env.bzl"], deps = [ ":pep508_platform_bzl", + "//python/private:version_bzl", ], ) diff --git a/python/private/pypi/pep508_evaluate.bzl b/python/private/pypi/pep508_evaluate.bzl index 70840c76c6..61a5b19999 100644 --- a/python/private/pypi/pep508_evaluate.bzl +++ b/python/private/pypi/pep508_evaluate.bzl @@ -16,23 +16,11 @@ """ load("//python/private:enum.bzl", "enum") -load("//python/private:semver.bzl", "semver") +load("//python/private:version.bzl", "version") # The expression parsing and resolution for the PEP508 is below # -# Taken from -# https://peps.python.org/pep-0508/#grammar -# -# version_cmp = wsp* '<' | '<=' | '!=' | '==' | '>=' | '>' | '~=' | '===' -_VERSION_CMP = sorted( - [ - i.strip(" '") - for i in "'<' | '<=' | '!=' | '==' | '>=' | '>' | '~=' | '==='".split(" | ") - ], - key = lambda x: (-len(x), x), -) - _STATE = enum( STRING = "string", VAR = "var", @@ -353,36 +341,34 @@ def _env_expr(left, op, right): elif op == ">=": return left >= right else: - return fail("TODO: op unsupported: '{}'".format(op)) + return fail("unsupported op: '{}' {} '{}'".format(left, op, right)) def _version_expr(left, op, right): """Evaluate a version comparison expression""" - left = semver(left) - right = semver(right) - _left = left.key() - _right = right.key() - if op == "<": - return _left < _right + _left = version.parse(left) + _right = version.parse(right) + if _left == None or _right == None: + # Per spec, if either can't be normalized to a version, then + # fallback to simple string comparison. Usually this is `platform_version` + # or `platform_release`, which vary depending on platform. + return _env_expr(left, op, right) + + if op == "===": + return version.is_eeq(_left, _right) + elif op == "!=": + return version.is_ne(_left, _right) + elif op == "==": + return version.is_eq(_left, _right) + elif op == "<": + return version.is_lt(_left, _right) elif op == ">": - return _left > _right + return version.is_gt(_left, _right) elif op == "<=": - return _left <= _right + return version.is_le(_left, _right) elif op == ">=": - return _left >= _right - elif op == "!=": - return _left != _right - elif op == "==": - # Matching of major, minor, patch only - return _left[:3] == _right[:3] + return version.is_ge(_left, _right) elif op == "~=": - right_plus = right.upper() - _right_plus = right_plus.key() - return _left >= _right and _left < _right_plus - elif op == "===": - # Strict matching - return _left == _right - elif op in _VERSION_CMP: - fail("TODO: op unsupported: '{}'".format(op)) + return version.is_compatible(_left, _right) else: return False # Let's just ignore the invalid ops diff --git a/python/private/semver.bzl b/python/private/semver.bzl index cc9ae6ecb6..0cbd172348 100644 --- a/python/private/semver.bzl +++ b/python/private/semver.bzl @@ -43,32 +43,6 @@ def _to_dict(self): "pre_release": self.pre_release, } -def _upper(self): - major = self.major - minor = self.minor - patch = self.patch - build = "" - pre_release = "" - version = self.str() - - if patch != None: - minor = minor + 1 - patch = 0 - elif minor != None: - major = major + 1 - minor = 0 - elif minor == None: - major = major + 1 - - return _new( - major = major, - minor = minor, - patch = patch, - build = build, - pre_release = pre_release, - version = "~" + version, - ) - def _new(*, major, minor, patch, pre_release, build, version = None): # buildifier: disable=uninitialized self = struct( @@ -82,7 +56,6 @@ def _new(*, major, minor, patch, pre_release, build, version = None): key = lambda: _key(self), str = lambda: version, to_dict = lambda: _to_dict(self), - upper = lambda: _upper(self), ) return self diff --git a/python/private/py_wheel_normalize_pep440.bzl b/python/private/version.bzl similarity index 52% rename from python/private/py_wheel_normalize_pep440.bzl rename to python/private/version.bzl index 9566348987..4425cc7661 100644 --- a/python/private/py_wheel_normalize_pep440.bzl +++ b/python/private/version.bzl @@ -59,18 +59,23 @@ def _open_context(self): self.contexts.append(_ctx(_context(self)["start"])) return self.contexts[-1] -def _accept(self): +def _accept(self, key = None): """Close the current ctx successfully and merge the results.""" finished = self.contexts.pop() self.contexts[-1]["norm"] += finished["norm"] + if key: + self.contexts[-1][key] = finished["norm"] + self.contexts[-1]["start"] = finished["start"] return True def _context(self): return self.contexts[-1] -def _discard(self): +def _discard(self, key = None): self.contexts.pop() + if key: + self.contexts[-1][key] = "" return False def _new(input): @@ -313,9 +318,9 @@ def accept_epoch(parser): if accept_digits(parser) and accept(parser, _is("!"), "!"): if ctx["norm"] == "0!": ctx["norm"] = "" - return parser.accept() + return parser.accept("epoch") else: - return parser.discard() + return parser.discard("epoch") def accept_release(parser): """Accept the release segment, numbers separated by dots. @@ -329,10 +334,10 @@ def accept_release(parser): parser.open_context() if not accept_digits(parser): - return parser.discard() + return parser.discard("release") accept_dot_number_sequence(parser) - return parser.accept() + return parser.accept("release") def accept_pre_l(parser): """PEP 440: Pre-release spelling. @@ -374,7 +379,7 @@ def accept_prerelease(parser): accept(parser, _in(["-", "_", "."]), "") if not accept_pre_l(parser): - return parser.discard() + return parser.discard("pre") accept(parser, _in(["-", "_", "."]), "") @@ -382,7 +387,7 @@ def accept_prerelease(parser): # PEP 440: Implicit pre-release number ctx["norm"] += "0" - return parser.accept() + return parser.accept("pre") def accept_implicit_postrelease(parser): """PEP 440: Implicit post releases. @@ -444,9 +449,9 @@ def accept_postrelease(parser): parser.open_context() if accept_implicit_postrelease(parser) or accept_explicit_postrelease(parser): - return parser.accept() + return parser.accept("post") - return parser.discard() + return parser.discard("post") def accept_devrelease(parser): """PEP 440: Developmental releases. @@ -470,9 +475,9 @@ def accept_devrelease(parser): # PEP 440: Implicit development release number ctx["norm"] += "0" - return parser.accept() + return parser.accept("dev") - return parser.discard() + return parser.discard("dev") def accept_local(parser): """PEP 440: Local version identifiers. @@ -487,9 +492,9 @@ def accept_local(parser): if accept(parser, _is("+"), "+") and accept_alnum(parser): accept_separator_alnum_sequence(parser) - return parser.accept() + return parser.accept("local") - return parser.discard() + return parser.discard("local") def normalize_pep440(version): """Escape the version component of a filename. @@ -503,7 +508,31 @@ def normalize_pep440(version): Returns: string containing the normalized version. """ - parser = _new(version.strip()) # PEP 440: Leading and Trailing Whitespace + return _parse(version, strict = True)["norm"] + +def _parse(version_str, strict = True): + """Escape the version component of a filename. + + See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode + and https://peps.python.org/pep-0440/ + + Args: + version_str: version string to be normalized according to PEP 440. + strict: fail if the version is invalid, defaults to True. + + Returns: + string containing the normalized version. + """ + + # https://packaging.python.org/en/latest/specifications/version-specifiers/#leading-and-trailing-whitespace + version = version_str.strip() + is_prefix = False + + if not strict: + is_prefix = version.endswith(".*") + version = version.strip(" .*") # PEP 440: Leading and Trailing Whitespace and ".*" + + parser = _new(version) accept(parser, _is("v"), "") # PEP 440: Preceding v character accept_epoch(parser) accept_release(parser) @@ -511,9 +540,317 @@ def normalize_pep440(version): accept_postrelease(parser) accept_devrelease(parser) accept_local(parser) - if parser.input[parser.context()["start"]:]: - fail( - "Failed to parse PEP 440 version identifier '%s'." % parser.input, - "Parse error at '%s'" % parser.input[parser.context()["start"]:], - ) - return parser.context()["norm"] + + parser_ctx = parser.context() + if parser.input[parser_ctx["start"]:]: + if strict: + fail( + "Failed to parse PEP 440 version identifier '%s'." % parser.input, + "Parse error at '%s'" % parser.input[parser_ctx["start"]:], + ) + + return None + + parser_ctx["is_prefix"] = is_prefix + return parser_ctx + +def parse(version_str, strict = False): + """Parse a PEP4408 compliant version. + + This is similar to `normalize_pep440`, but it parses individual components to + comparable types. + + Args: + version_str: version string to be normalized according to PEP 440. + strict: fail if the version is invalid. + + Returns: + a struct with individual components of a version: + * `epoch` {type}`int`, defaults to `0` + * `release` {type}`tuple[int]` an n-tuple of ints + * `pre` {type}`tuple[str, int] | None` a tuple of a string and an int, + e.g. ("a", 1) + * `post` {type}`tuple[str, int] | None` a tuple of a string and an int, + e.g. ("~", 1) + * `dev` {type}`tuple[str, int] | None` a tuple of a string and an int, + e.g. ("", 1) + * `local` {type}`tuple[str, int] | None` a tuple of components in the local + version, e.g. ("abc", 123). + * `is_prefix` {type}`bool` whether the version_str ends with `.*`. + * `string` {type}`str` normalized value of the input. + """ + + parts = _parse(version_str, strict = strict) + if not parts: + return None + + if parts["is_prefix"] and (parts["local"] or parts["post"] or parts["dev"] or parts["pre"]): + if strict: + fail("local version part has been obtained, but only public segments can have prefix matches") + + # https://peps.python.org/pep-0440/#public-version-identifiers + return None + + return struct( + epoch = _parse_epoch(parts["epoch"]), + release = _parse_release(parts["release"]), + pre = _parse_pre(parts["pre"]), + post = _parse_post(parts["post"]), + dev = _parse_dev(parts["dev"]), + local = _parse_local(parts["local"]), + string = parts["norm"], + is_prefix = parts["is_prefix"], + ) + +def _parse_epoch(value): + if not value: + return 0 + + if not value.endswith("!"): + fail("epoch string segment needs to end with '!', got: {}".format(value)) + + return int(value[:-1]) + +def _parse_release(value): + return tuple([int(d) for d in value.split(".")]) + +def _parse_local(value): + if not value: + return None + + if not value.startswith("+"): + fail("local release identifier must start with '+', got: {}".format(value)) + + # If the part is numerical, handle it as a number + return tuple([int(part) if part.isdigit() else part for part in value[1:].split(".")]) + +def _parse_dev(value): + if not value: + return None + + if not value.startswith(".dev"): + fail("dev release identifier must start with '.dev', got: {}".format(value)) + dev = int(value[len(".dev"):]) + + # Empty string goes first when comparing + return ("", dev) + +def _parse_pre(value): + if not value: + return None + + if value.startswith("rc"): + prefix = "rc" + else: + prefix = value[0] + + return (prefix, int(value[len(prefix):])) + +def _parse_post(value): + if not value: + return None + + if not value.startswith(".post"): + fail("post release identifier must start with '.post', got: {}".format(value)) + post = int(value[len(".post"):]) + + # We choose `~` since almost all of the ASCII characters will be before + # it. Use `ord` and `chr` functions to find a good value. + return ("~", post) + +def _pad_zeros(release, n): + padding = n - len(release) + if padding <= 0: + return release + + release = list(release) + [0] * padding + return tuple(release) + +def _prefix_err(left, op, right): + if left.is_prefix or right.is_prefix: + fail("PEP440: only '==' and '!=' operators can use prefix matching: {} {} {}".format( + left.string, + op, + right.string, + )) + +def _version_eeq(left, right): + """=== operator""" + if left.is_prefix or right.is_prefix: + fail(_prefix_err(left, "===", right)) + + # https://peps.python.org/pep-0440/#arbitrary-equality + # > simple string equality operations + return left.string == right.string + +def _version_eq(left, right): + """== operator""" + if left.is_prefix and right.is_prefix: + fail("Invalid comparison: both versions cannot be prefix matching") + if left.is_prefix: + return right.string.startswith("{}.".format(left.string)) + if right.is_prefix: + return left.string.startswith("{}.".format(right.string)) + + if left.epoch != right.epoch: + return False + + release_len = max(len(left.release), len(right.release)) + left_release = _pad_zeros(left.release, release_len) + right_release = _pad_zeros(right.release, release_len) + + if left_release != right_release: + return False + + return ( + left.pre == right.pre and + left.post == right.post and + left.dev == right.dev + # local is ignored for == checks + ) + +def _version_compatible(left, right): + """~= operator""" + if left.is_prefix or right.is_prefix: + fail(_prefix_err(left, "~=", right)) + + # https://peps.python.org/pep-0440/#compatible-release + # Note, the ~= operator can be also expressed as: + # >= V.N, == V.* + + right_star = ".".join([str(d) for d in right.release[:-1]]) + if right.epoch: + right_star = "{}!{}.".format(right.epoch, right_star) + else: + right_star = "{}.".format(right_star) + + return _version_ge(left, right) and left.string.startswith(right_star) + +def _version_ne(left, right): + """!= operator""" + return not _version_eq(left, right) + +def _version_lt(left, right): + """< operator""" + if left.is_prefix or right.is_prefix: + fail(_prefix_err(left, "<", right)) + + if left.epoch > right.epoch: + return False + elif left.epoch < right.epoch: + return True + + release_len = max(len(left.release), len(right.release)) + left_release = _pad_zeros(left.release, release_len) + right_release = _pad_zeros(right.release, release_len) + + if left_release > right_release: + return False + elif left_release < right_release: + return True + + # From PEP440, this is not a simple ordering check and we need to check the version + # semantically: + # * The exclusive ordered comparison operator""" + if left.is_prefix or right.is_prefix: + fail(_prefix_err(left, ">", right)) + + if left.epoch > right.epoch: + return True + elif left.epoch < right.epoch: + return False + + release_len = max(len(left.release), len(right.release)) + left_release = _pad_zeros(left.release, release_len) + right_release = _pad_zeros(right.release, release_len) + + if left_release > right_release: + return True + elif left_release < right_release: + return False + + # From PEP440, this is not a simple ordering check and we need to check the version + # semantically: + # * The exclusive ordered comparison >V MUST NOT allow a post-release of the given version + # unless V itself is a post release. + # + # * The exclusive ordered comparison >V MUST NOT match a local version of the specified + # version. + + if left.post and right.post: + return left.post > right.post + else: + # ignore the left.post if right is not a post if right is a post, then this evaluates to + # False anyway. + return False + +def _version_le(left, right): + """<= operator""" + if left.is_prefix or right.is_prefix: + fail(_prefix_err(left, "<=", right)) + + # PEP440: simple order check + # https://peps.python.org/pep-0440/#inclusive-ordered-comparison + _left = _version_key(left, local = False) + _right = _version_key(right, local = False) + return _left < _right or _version_eq(left, right) + +def _version_ge(left, right): + """>= operator""" + if left.is_prefix or right.is_prefix: + fail(_prefix_err(left, ">=", right)) + + # PEP440: simple order check + # https://peps.python.org/pep-0440/#inclusive-ordered-comparison + _left = _version_key(left, local = False) + _right = _version_key(right, local = False) + return _left > _right or _version_eq(left, right) + +def _version_key(self, *, local = True): + """This function returns a tuple that can be used in 'sorted' calls. + + This implements the PEP440 version sorting. + """ + release_key = ("z",) + local = self.local if local else [] + local = local or [] + + return ( + self.epoch, + self.release, + # PEP440 Within a pre-release, post-release or development release segment with + # a shared prefix, ordering MUST be by the value of the numeric component. + # PEP440 release ordering: .devN, aN, bN, rcN, , .postN + # We choose to first match the pre-release, then post release, then dev and + # then stable + self.pre or self.post or self.dev or release_key, + # PEP440 local versions go before post versions + tuple([(type(item) == "int", item) for item in local]), + # PEP440 - pre-release ordering: .devN, , .postN + self.post or self.dev or release_key, + # PEP440 - post release ordering: .devN, + self.dev or release_key, + ) + +version = struct( + normalize = normalize_pep440, + parse = parse, + # methods, keep sorted + key = _version_key, + is_compatible = _version_compatible, + is_eq = _version_eq, + is_eeq = _version_eeq, + is_ge = _version_ge, + is_gt = _version_gt, + is_le = _version_le, + is_lt = _version_lt, + is_ne = _version_ne, +) diff --git a/tests/py_wheel/py_wheel_tests.bzl b/tests/py_wheel/py_wheel_tests.bzl index 091e01c37d..43c068e597 100644 --- a/tests/py_wheel/py_wheel_tests.bzl +++ b/tests/py_wheel/py_wheel_tests.bzl @@ -17,7 +17,6 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") load("@rules_testing//lib:truth.bzl", "matching") load("@rules_testing//lib:util.bzl", rt_util = "util") load("//python:packaging.bzl", "py_wheel") -load("//python/private:py_wheel_normalize_pep440.bzl", "normalize_pep440") # buildifier: disable=bzl-visibility _basic_tests = [] _tests = [] @@ -168,106 +167,6 @@ def _test_content_type_from_description_impl(env, target): _tests.append(_test_content_type_from_description) -def _test_pep440_normalization(env): - prefixes = ["v", " v", " \t\r\nv"] - epochs = { - "": ["", "0!", "00!"], - "1!": ["1!", "001!"], - "200!": ["200!", "00200!"], - } - releases = { - "0.1": ["0.1", "0.01"], - "2023.7.19": ["2023.7.19", "2023.07.19"], - } - pres = { - "": [""], - "a0": ["a", ".a", "-ALPHA0", "_alpha0", ".a0"], - "a4": ["alpha4", ".a04"], - "b0": ["b", ".b", "-BETA0", "_beta0", ".b0"], - "b5": ["beta05", ".b5"], - "rc0": ["C", "_c0", "RC", "_rc0", "-preview_0"], - } - explicit_posts = { - "": [""], - ".post0": [], - ".post1": [".post1", "-r1", "_rev1"], - } - implicit_posts = [[".post1", "-1"], [".post2", "-2"]] - devs = { - "": [""], - ".dev0": ["dev", "-DEV", "_Dev-0"], - ".dev9": ["DEV9", ".dev09", ".dev9"], - ".dev{BUILD_TIMESTAMP}": [ - "-DEV{BUILD_TIMESTAMP}", - "_dev_{BUILD_TIMESTAMP}", - ], - } - locals = { - "": [""], - "+ubuntu.7": ["+Ubuntu_7", "+ubuntu-007"], - "+ubuntu.r007": ["+Ubuntu_R007"], - } - epochs = [ - [normalized_epoch, input_epoch] - for normalized_epoch, input_epochs in epochs.items() - for input_epoch in input_epochs - ] - releases = [ - [normalized_release, input_release] - for normalized_release, input_releases in releases.items() - for input_release in input_releases - ] - pres = [ - [normalized_pre, input_pre] - for normalized_pre, input_pres in pres.items() - for input_pre in input_pres - ] - explicit_posts = [ - [normalized_post, input_post] - for normalized_post, input_posts in explicit_posts.items() - for input_post in input_posts - ] - pres_and_posts = [ - [normalized_pre + normalized_post, input_pre + input_post] - for normalized_pre, input_pre in pres - for normalized_post, input_post in explicit_posts - ] + [ - [normalized_pre + normalized_post, input_pre + input_post] - for normalized_pre, input_pre in pres - for normalized_post, input_post in implicit_posts - if input_pre == "" or input_pre[-1].isdigit() - ] - devs = [ - [normalized_dev, input_dev] - for normalized_dev, input_devs in devs.items() - for input_dev in input_devs - ] - locals = [ - [normalized_local, input_local] - for normalized_local, input_locals in locals.items() - for input_local in input_locals - ] - postfixes = ["", " ", " \t\r\n"] - i = 0 - for nepoch, iepoch in epochs: - for nrelease, irelease in releases: - for nprepost, iprepost in pres_and_posts: - for ndev, idev in devs: - for nlocal, ilocal in locals: - prefix = prefixes[i % len(prefixes)] - postfix = postfixes[(i // len(prefixes)) % len(postfixes)] - env.expect.that_str( - normalize_pep440( - prefix + iepoch + irelease + iprepost + - idev + ilocal + postfix, - ), - ).equals( - nepoch + nrelease + nprepost + ndev + nlocal, - ) - i += 1 - -_basic_tests.append(_test_pep440_normalization) - def py_wheel_test_suite(name): test_suite( name = name, diff --git a/tests/pypi/pep508/evaluate_tests.bzl b/tests/pypi/pep508/evaluate_tests.bzl index 303c167900..7b6c064b94 100644 --- a/tests/pypi/pep508/evaluate_tests.bzl +++ b/tests/pypi/pep508/evaluate_tests.bzl @@ -19,6 +19,12 @@ load("//python/private/pypi:pep508_evaluate.bzl", "evaluate", "tokenize") # bui _tests = [] +def _check_evaluate(env, expr, expected, values, strict = True): + env.expect.where( + expression = expr, + values = values, + ).that_bool(evaluate(expr, env = values, strict = strict)).equals(expected) + def _tokenize_tests(env): for input, want in { "": [], @@ -82,23 +88,11 @@ def _evaluate_non_version_env_tests(env): "{} > 'osx'".format(var_name): False, "{} >= 'osx'".format(var_name): True, }.items(): - got = evaluate( - input, - env = marker_env, - ) - env.expect.where( - expr = input, - env = marker_env, - ).that_bool(got).equals(want) + _check_evaluate(env, input, want, marker_env) # Check that the non-strict eval gives us back the input when no # env is supplied. - got = evaluate( - input, - env = {}, - strict = False, - ) - env.expect.that_bool(got).equals(input.replace("'", '"')) + _check_evaluate(env, input, input.replace("'", '"'), {}, strict = False) _tests.append(_evaluate_non_version_env_tests) @@ -123,6 +117,7 @@ def _evaluate_version_env_tests(env): "{} <= '3.7.10'".format(var_name): True, "{} <= '3.7.8'".format(var_name): False, "{} == '3.7.9'".format(var_name): True, + "{} == '3.7.*'".format(var_name): True, "{} != '3.7.9'".format(var_name): False, "{} ~= '3.7.1'".format(var_name): True, "{} ~= '3.7.10'".format(var_name): False, @@ -131,23 +126,32 @@ def _evaluate_version_env_tests(env): "{} === '3.7.9'".format(var_name): True, "{} == '3.7.9+rc2'".format(var_name): True, }.items(): # buildifier: @unsorted-dict-items - got = evaluate( - input, - env = marker_env, - ) - env.expect.that_collection((input, got)).contains_exactly((input, want)) + _check_evaluate(env, input, want, marker_env) # Check that the non-strict eval gives us back the input when no # env is supplied. - got = evaluate( - input, - env = {}, - strict = False, - ) - env.expect.that_bool(got).equals(input.replace("'", '"')) + _check_evaluate(env, input, input.replace("'", '"'), {}, strict = False) _tests.append(_evaluate_version_env_tests) +def _evaluate_platform_version_is_special(env): + # Given + marker_env = {"platform_version": "FooBar Linux v1.2.3"} + + # When the platform version is not + input = "platform_version == '0'" + _check_evaluate(env, input, False, marker_env) + + # And when I compare it as string + input = "'FooBar' in platform_version" + _check_evaluate(env, input, True, marker_env) + + # Check that the non-strict eval gives us back the input when no + # env is supplied. + _check_evaluate(env, input, input.replace("'", '"'), {}, strict = False) + +_tests.append(_evaluate_platform_version_is_special) + def _logical_expression_tests(env): for input, want in { # Basic @@ -195,13 +199,7 @@ def _logical_expression_tests(env): "not not os_name == 'foo'": True, "not not not os_name == 'foo'": False, }.items(): # buildifier: @unsorted-dict-items - got = evaluate( - input, - env = { - "os_name": "foo", - }, - ) - env.expect.that_collection((input, got)).contains_exactly((input, want)) + _check_evaluate(env, input, want, {"os_name": "foo"}) if not input.strip("()"): # These cases will just return True, because they will be evaluated @@ -210,12 +208,7 @@ def _logical_expression_tests(env): # Check that the non-strict eval gives us back the input when no env # is supplied. - got = evaluate( - input, - env = {}, - strict = False, - ) - env.expect.that_bool(got).equals(input.replace("'", '"')) + _check_evaluate(env, input, input.replace("'", '"'), {}, strict = False) _tests.append(_logical_expression_tests) @@ -244,6 +237,7 @@ def _evaluate_partial_only_extra(env): strict = False, ) env.expect.that_bool(got).equals(want) + _check_evaluate(env, input, want, {"extra": extra}, strict = False) _tests.append(_evaluate_partial_only_extra) @@ -268,14 +262,61 @@ def _evaluate_with_aliases(env): }, }.items(): # buildifier: @unsorted-dict-items for input, want in tests.items(): - got = evaluate( - input, - env = pep508_env(target_platform), - ) - env.expect.that_bool(got).equals(want) + _check_evaluate(env, input, want, pep508_env(target_platform)) _tests.append(_evaluate_with_aliases) +def _expr_case(expr, want, env): + return struct(expr = expr.strip(), want = want, env = env) + +_MISC_EXPRESSIONS = [ + _expr_case('python_version == "3.*"', True, {"python_version": "3.10.1"}), + _expr_case('python_version != "3.10.*"', False, {"python_version": "3.10.1"}), + _expr_case('python_version != "3.11.*"', True, {"python_version": "3.10.1"}), + _expr_case('python_version != "3.10"', False, {"python_version": "3.10.0"}), + _expr_case('python_version == "3.10"', True, {"python_version": "3.10.0"}), + # Cases for the '>' operator + # Taken from spec: https://peps.python.org/pep-0440/#exclusive-ordered-comparison + _expr_case('python_version > "1.7"', True, {"python_version": "1.7.1"}), + _expr_case('python_version > "1.7"', False, {"python_version": "1.7.0.post0"}), + _expr_case('python_version > "1.7"', True, {"python_version": "1.7.1"}), + _expr_case('python_version > "1.7.post2"', True, {"python_version": "1.7.1"}), + _expr_case('python_version > "1.7.post2"', True, {"python_version": "1.7.post3"}), + _expr_case('python_version > "1.7.post2"', False, {"python_version": "1.7.0"}), + _expr_case('python_version > "1.7.1+local"', False, {"python_version": "1.7.1"}), + _expr_case('python_version > "1.7.1+local"', True, {"python_version": "1.7.2"}), + # Extra cases for the '<' operator + _expr_case('python_version < "1.7.1"', False, {"python_version": "1.7.2"}), + _expr_case('python_version < "1.7.3"', True, {"python_version": "1.7.2"}), + _expr_case('python_version < "1.7.1"', True, {"python_version": "1.7"}), + _expr_case('python_version < "1.7.1"', False, {"python_version": "1.7.1-rc2"}), + _expr_case('python_version < "1.7.1-rc3"', True, {"python_version": "1.7.1-rc2"}), + _expr_case('python_version < "1.7.1-rc1"', False, {"python_version": "1.7.1-rc2"}), + # Extra tests + _expr_case('python_version <= "1.7.1"', True, {"python_version": "1.7.1"}), + _expr_case('python_version <= "1.7.2"', True, {"python_version": "1.7.1"}), + _expr_case('python_version >= "1.7.1"', True, {"python_version": "1.7.1"}), + _expr_case('python_version >= "1.7.0"', True, {"python_version": "1.7.1"}), + # Compatible version tests: + # https://packaging.python.org/en/latest/specifications/version-specifiers/#compatible-release + _expr_case('python_version ~= "2.2"', True, {"python_version": "2.3"}), + _expr_case('python_version ~= "2.2"', False, {"python_version": "2.1"}), + _expr_case('python_version ~= "2.2.post3"', False, {"python_version": "2.2"}), + _expr_case('python_version ~= "2.2.post3"', True, {"python_version": "2.3"}), + _expr_case('python_version ~= "2.2.post3"', False, {"python_version": "3.0"}), + _expr_case('python_version ~= "1!2.2"', False, {"python_version": "2.7"}), + _expr_case('python_version ~= "0!2.2"', True, {"python_version": "2.7"}), + _expr_case('python_version ~= "1!2.2"', True, {"python_version": "1!2.7"}), + _expr_case('python_version ~= "1.2.3"', True, {"python_version": "1.2.4"}), + _expr_case('python_version ~= "1.2.3"', False, {"python_version": "1.3.2"}), +] + +def _misc_expressions(env): + for case in _MISC_EXPRESSIONS: + _check_evaluate(env, case.expr, case.want, case.env) + +_tests.append(_misc_expressions) + def evaluate_test_suite(name): # buildifier: disable=function-docstring test_suite( name = name, diff --git a/tests/semver/semver_test.bzl b/tests/semver/semver_test.bzl index aef3deca82..9d13402c92 100644 --- a/tests/semver/semver_test.bzl +++ b/tests/semver/semver_test.bzl @@ -104,24 +104,6 @@ def _test_semver_sort(env): _tests.append(_test_semver_sort) -def _test_upper(env): - for input, want in { - # Depending on how many version numbers are specified we will increase - # the upper bound differently. See https://packaging.python.org/en/latest/specifications/version-specifiers/#compatible-release for docs - "0.0.1": "0.1.0", - "0.1": "1.0", - "0.1.0": "0.2.0", - "1": "2", - "1.0.0-pre": "1.1.0", # pre-release info is dropped - "1.2.0": "1.3.0", - "2.0.0+build0": "2.1.0", # build info is dropped - }.items(): - actual = semver(input).upper().key() - want = semver(want).key() - env.expect.that_collection(actual).contains_exactly(want).in_order() - -_tests.append(_test_upper) - def semver_test_suite(name): """Create the test suite. diff --git a/tests/version/BUILD.bazel b/tests/version/BUILD.bazel new file mode 100644 index 0000000000..d6fdecd4cf --- /dev/null +++ b/tests/version/BUILD.bazel @@ -0,0 +1,3 @@ +load(":version_test.bzl", "version_test_suite") + +version_test_suite(name = "version_tests") diff --git a/tests/version/version_test.bzl b/tests/version/version_test.bzl new file mode 100644 index 0000000000..589f9ac05d --- /dev/null +++ b/tests/version/version_test.bzl @@ -0,0 +1,157 @@ +"" + +load("@rules_testing//lib:analysis_test.bzl", "test_suite") +load("//python/private:version.bzl", "version") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_normalization(env): + prefixes = ["v", " v", " \t\r\nv"] + epochs = { + "": ["", "0!", "00!"], + "1!": ["1!", "001!"], + "200!": ["200!", "00200!"], + } + releases = { + "0.1": ["0.1", "0.01"], + "2023.7.19": ["2023.7.19", "2023.07.19"], + } + pres = { + "": [""], + "a0": ["a", ".a", "-ALPHA0", "_alpha0", ".a0"], + "a4": ["alpha4", ".a04"], + "b0": ["b", ".b", "-BETA0", "_beta0", ".b0"], + "b5": ["beta05", ".b5"], + "rc0": ["C", "_c0", "RC", "_rc0", "-preview_0"], + } + explicit_posts = { + "": [""], + ".post0": [], + ".post1": [".post1", "-r1", "_rev1"], + } + implicit_posts = [[".post1", "-1"], [".post2", "-2"]] + devs = { + "": [""], + ".dev0": ["dev", "-DEV", "_Dev-0"], + ".dev9": ["DEV9", ".dev09", ".dev9"], + ".dev{BUILD_TIMESTAMP}": [ + "-DEV{BUILD_TIMESTAMP}", + "_dev_{BUILD_TIMESTAMP}", + ], + } + locals = { + "": [""], + "+ubuntu.7": ["+Ubuntu_7", "+ubuntu-007"], + "+ubuntu.r007": ["+Ubuntu_R007"], + } + epochs = [ + [normalized_epoch, input_epoch] + for normalized_epoch, input_epochs in epochs.items() + for input_epoch in input_epochs + ] + releases = [ + [normalized_release, input_release] + for normalized_release, input_releases in releases.items() + for input_release in input_releases + ] + pres = [ + [normalized_pre, input_pre] + for normalized_pre, input_pres in pres.items() + for input_pre in input_pres + ] + explicit_posts = [ + [normalized_post, input_post] + for normalized_post, input_posts in explicit_posts.items() + for input_post in input_posts + ] + pres_and_posts = [ + [normalized_pre + normalized_post, input_pre + input_post] + for normalized_pre, input_pre in pres + for normalized_post, input_post in explicit_posts + ] + [ + [normalized_pre + normalized_post, input_pre + input_post] + for normalized_pre, input_pre in pres + for normalized_post, input_post in implicit_posts + if input_pre == "" or input_pre[-1].isdigit() + ] + devs = [ + [normalized_dev, input_dev] + for normalized_dev, input_devs in devs.items() + for input_dev in input_devs + ] + locals = [ + [normalized_local, input_local] + for normalized_local, input_locals in locals.items() + for input_local in input_locals + ] + postfixes = ["", " ", " \t\r\n"] + i = 0 + for nepoch, iepoch in epochs: + for nrelease, irelease in releases: + for nprepost, iprepost in pres_and_posts: + for ndev, idev in devs: + for nlocal, ilocal in locals: + prefix = prefixes[i % len(prefixes)] + postfix = postfixes[(i // len(prefixes)) % len(postfixes)] + env.expect.that_str( + version.normalize( + prefix + iepoch + irelease + iprepost + + idev + ilocal + postfix, + ), + ).equals( + nepoch + nrelease + nprepost + ndev + nlocal, + ) + i += 1 + +_tests.append(_test_normalization) + +def _test_ordering(env): + want = [ + # Taken from https://peps.python.org/pep-0440/#summary-of-permitted-suffixes-and-relative-ordering + "1.dev0", + "1.0.dev456", + "1.0a1", + "1.0a2.dev456", + "1.0a12.dev456", + "1.0a12", + "1.0b1.dev456", + "1.0b1.dev457", + "1.0b2", + "1.0b2.post345.dev456", + "1.0b2.post345.dev457", + "1.0b2.post345", + "1.0rc1.dev456", + "1.0rc1", + "1.0", + "1.0+abc.5", + "1.0+abc.7", + "1.0+5", + "1.0.post456.dev34", + "1.0.post456", + "1.0.15", + "1.1.dev1", + "1!0.1", + ] + + for lower, higher in zip(want[:-1], want[1:]): + lower = version.parse(lower, strict = True) + higher = version.parse(higher, strict = True) + + lower_key = version.key(lower) + higher_key = version.key(higher) + + if not lower_key < higher_key: + env.fail("Expected '{}'.key() to be smaller than '{}'.key(), but got otherwise: {} > {}".format( + lower.string, + higher.string, + lower_key, + higher_key, + )) + +_tests.append(_test_ordering) + +def version_test_suite(name): + test_suite( + name = name, + basic_tests = _tests, + ) From e54060b68c5d4fa7a34c6132efbab6761735c25e Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Mon, 12 May 2025 17:46:53 +0200 Subject: [PATCH 076/156] tests: make some analysis tests work for when test's exec platform is required (#2869) An upcoming change in Bazel makes the test toolchain required, which means a compatible exec platform amongst toolchains must be found (https://github.com/bazelbuild/bazel/commit/2780393d35ad0607cf5e344ae082b00a5569a964). Some analysis tests of `py_test` force the target platform to a specific platform, but before this change didn't register a compatible exec platform. This can be fixed by registering the target platform as an exec platform. Since Python targets currently depend on a C++ toolchain through Bazel's `launcher` and `launcher_maker` and the default toolchain can't cross-compile to Linux, the host platform still needs to be kept at highest priority to ensure that cross-compilation isn't needed on macOS. Work towards #2850 --- tests/base_rules/py_executable_base_tests.bzl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/base_rules/py_executable_base_tests.bzl b/tests/base_rules/py_executable_base_tests.bzl index 55a8958b82..49cbb1586c 100644 --- a/tests/base_rules/py_executable_base_tests.bzl +++ b/tests/base_rules/py_executable_base_tests.bzl @@ -356,6 +356,7 @@ def _test_main_module_bootstrap_system_python(name, config): target = name + "_subject", config_settings = { BOOTSTRAP_IMPL: "system_python", + "//command_line_option:extra_execution_platforms": ["@bazel_tools//tools:host_platform", LINUX_X86_64], "//command_line_option:platforms": [LINUX_X86_64], }, expect_failure = True, @@ -380,6 +381,7 @@ def _test_main_module_bootstrap_script(name, config): target = name + "_subject", config_settings = { BOOTSTRAP_IMPL: "script", + "//command_line_option:extra_execution_platforms": ["@bazel_tools//tools:host_platform", LINUX_X86_64], "//command_line_option:platforms": [LINUX_X86_64], }, ) From c383c3b2799b5255783545590482f59d78a42163 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 13 May 2025 08:22:38 +0900 Subject: [PATCH 077/156] fix(pypi): make the URL/filename extraction from requirement more robust (#2871) Summary: - Make the requirement line the same as the one that is used in whls. It only contains extras and the version if it is present. - Add debug log statements if we fail to get the version from a direct URL reference. - Move some tests from `parse_requirements_tests` to `index_sources_tests` to improve test maintenance. - Replace the URL encoded `+` to a regular `+` in the filename. - Correctly handle the case when the `=sha256:` is used in the URL. Once this is merged I plan to tackle #2648 by changing the `parse_requirements` code to de-duplicate entries returned by the `parse_requirements` function. I cannot think of anything else that we can do for this as of now, so will mark the associated issue as resolved. Fixes #2363 Work towards #2648 --- .editorconfig | 4 + CHANGELOG.md | 4 + python/private/pypi/extension.bzl | 4 +- python/private/pypi/index_sources.bzl | 42 ++- python/private/pypi/parse_requirements.bzl | 35 +-- python/private/pypi/pip_repository.bzl | 9 +- python/private/pypi/whl_library.bzl | 12 +- tests/pypi/extension/extension_tests.bzl | 6 +- .../index_sources/index_sources_tests.bzl | 39 ++- .../parse_requirements_tests.bzl | 278 ++---------------- 10 files changed, 132 insertions(+), 301 deletions(-) diff --git a/.editorconfig b/.editorconfig index 26bb52ffac..2737b0f184 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,3 +15,7 @@ max_line_length = 100 [*.{py,bzl}] indent_style = space indent_size = 4 + +# different overrides for git commit messages +[.git/COMMIT_EDITMSG] +max_line_length = 72 diff --git a/CHANGELOG.md b/CHANGELOG.md index aa7fc9d415..b94072d655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,10 @@ END_UNRELEASED_TEMPLATE multiple times. * (tools/wheelmaker.py) Extras are now preserved in Requires-Dist metadata when using requires_file to specify the requirements. +* (pypi) Use bazel downloader for direct URL references and correctly detect the filenames from + various URL formats - URL encoded version strings get correctly resolved, sha256 value can be + also retrieved from the URL as opposed to only the `--hash` parameter. Fixes + [#2363](https://github.com/bazel-contrib/rules_python/issues/2363). {#v0-0-0-added} ### Added diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 647407f16f..9368e6539f 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -301,7 +301,7 @@ def _whl_repos(*, requirement, whl_library_args, download_only, netrc, auth_patt # This is no-op because pip is not used to download the wheel. args.pop("download_only", None) - args["requirement"] = requirement.srcs.requirement + args["requirement"] = requirement.line args["urls"] = [distribution.url] args["sha256"] = distribution.sha256 args["filename"] = distribution.filename @@ -338,7 +338,7 @@ def _whl_repos(*, requirement, whl_library_args, download_only, netrc, auth_patt # Fallback to a pip-installed wheel args = dict(whl_library_args) # make a copy - args["requirement"] = requirement.srcs.requirement_line + args["requirement"] = requirement.line if requirement.extra_pip_args: args["extra_pip_args"] = requirement.extra_pip_args diff --git a/python/private/pypi/index_sources.bzl b/python/private/pypi/index_sources.bzl index e3762d2a48..803670c3e4 100644 --- a/python/private/pypi/index_sources.bzl +++ b/python/private/pypi/index_sources.bzl @@ -16,6 +16,23 @@ A file that houses private functions used in the `bzlmod` extension with the same name. """ +# Just list them here and me super conservative +_KNOWN_EXTS = [ + # Note, the following source in pip has more extensions + # https://github.com/pypa/pip/blob/3c5a189141a965f21a473e46c3107e689eb9f79f/src/pip/_vendor/distlib/locators.py#L90 + # + # ".tar.bz2", + # ".tar", + # ".tgz", + # ".tbz", + # + # But we support only the following, used in 'packaging' + # https://github.com/pypa/pip/blob/3c5a189141a965f21a473e46c3107e689eb9f79f/src/pip/_vendor/packaging/utils.py#L137 + ".whl", + ".tar.gz", + ".zip", +] + def index_sources(line): """Get PyPI sources from a requirements.txt line. @@ -58,11 +75,31 @@ def index_sources(line): ).strip() url = "" + filename = "" if "@" in head: - requirement = requirement_line - _, _, url_and_rest = requirement.partition("@") + maybe_requirement, _, url_and_rest = requirement.partition("@") url = url_and_rest.strip().partition(" ")[0].strip() + url, _, sha256 = url.partition("#sha256=") + if sha256: + shas.append(sha256) + _, _, filename = url.rpartition("/") + + # Replace URL encoded characters and luckily there is only one case + filename = filename.replace("%2B", "+") + is_known_ext = False + for ext in _KNOWN_EXTS: + if filename.endswith(ext): + is_known_ext = True + break + + if is_known_ext: + requirement = maybe_requirement.strip() + else: + # could not detect filename from the URL + filename = "" + requirement = requirement_line + return struct( requirement = requirement, requirement_line = requirement_line, @@ -70,4 +107,5 @@ def index_sources(line): shas = sorted(shas), marker = marker, url = url, + filename = filename, ) diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index 1583c89199..bdfac46ed6 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -40,6 +40,7 @@ def parse_requirements( extra_pip_args = [], get_index_urls = None, evaluate_markers = None, + extract_url_srcs = True, logger = None): """Get the requirements with platforms that the requirements apply to. @@ -58,6 +59,8 @@ def parse_requirements( the platforms stored as values in the input dict. Returns the same dict, but with values being platforms that are compatible with the requirements line. + extract_url_srcs: A boolean to enable extracting URLs from requirement + lines to enable using bazel downloader. logger: repo_utils.logger or None, a simple struct to log diagnostic messages. Returns: @@ -206,7 +209,7 @@ def parse_requirements( ret_requirements.append( struct( distribution = r.distribution, - srcs = r.srcs, + line = r.srcs.requirement if extract_url_srcs and (whls or sdist) else r.srcs.requirement_line, target_platforms = sorted(target_platforms), extra_pip_args = r.extra_pip_args, whls = whls, @@ -281,32 +284,26 @@ def _add_dists(*, requirement, index_urls, logger = None): logger: A logger for printing diagnostic info. """ - # Handle direct URLs in requirements if requirement.srcs.url: - url = requirement.srcs.url - _, _, filename = url.rpartition("/") - filename, _, _ = filename.partition("#sha256=") - if "." not in filename: - # detected filename has no extension, it might be an sdist ref - # TODO @aignas 2025-04-03: should be handled if the following is fixed: - # https://github.com/bazel-contrib/rules_python/issues/2363 + if not requirement.srcs.filename: + if logger: + logger.debug(lambda: "Could not detect the filename from the URL, falling back to pip: {}".format( + requirement.srcs.url, + )) return [], None - if "@" in filename: - # this is most likely foo.git@git_sha, skip special handling of these - return [], None - - direct_url_dist = struct( - url = url, - filename = filename, + # Handle direct URLs in requirements + dist = struct( + url = requirement.srcs.url, + filename = requirement.srcs.filename, sha256 = requirement.srcs.shas[0] if requirement.srcs.shas else "", yanked = False, ) - if filename.endswith(".whl"): - return [direct_url_dist], None + if dist.filename.endswith(".whl"): + return [dist], None else: - return [], direct_url_dist + return [], dist if not index_urls: return [], None diff --git a/python/private/pypi/pip_repository.bzl b/python/private/pypi/pip_repository.bzl index 8ca94f7f9b..c8d23f471f 100644 --- a/python/private/pypi/pip_repository.bzl +++ b/python/private/pypi/pip_repository.bzl @@ -89,19 +89,20 @@ def _pip_repository_impl(rctx): python_interpreter_target = rctx.attr.python_interpreter_target, srcs = rctx.attr._evaluate_markers_srcs, ), + extract_url_srcs = False, ) selected_requirements = {} options = None repository_platform = host_platform(rctx) for name, requirements in requirements_by_platform.items(): - r = select_requirement( + requirement = select_requirement( requirements, platform = None if rctx.attr.download_only else repository_platform, ) - if not r: + if not requirement: continue - options = options or r.extra_pip_args - selected_requirements[name] = r.srcs.requirement_line + options = options or requirement.extra_pip_args + selected_requirements[name] = requirement.line bzl_packages = sorted(selected_requirements.keys()) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 160bb5b799..4427c0e3ef 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -258,19 +258,9 @@ def _whl_library_impl(rctx): # Simulate the behaviour where the whl is present in the current directory. rctx.symlink(whl_path, whl_path.basename) whl_path = rctx.path(whl_path.basename) - elif rctx.attr.urls: + elif rctx.attr.urls and rctx.attr.filename: filename = rctx.attr.filename urls = rctx.attr.urls - if not filename: - _, _, filename = urls[0].rpartition("/") - - if not (filename.endswith(".whl") or filename.endswith("tar.gz") or filename.endswith(".zip")): - if rctx.attr.filename: - msg = "got '{}'".format(filename) - else: - msg = "detected '{}' from url:\n{}".format(filename, urls[0]) - fail("Only '.whl', '.tar.gz' or '.zip' files are supported, {}".format(msg)) - result = rctx.download( url = urls, output = filename, diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index 1cd6869c84..b4a7746271 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -806,7 +806,7 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "extra_pip_args": ["--extra-args-for-sdist-building"], "filename": "any-name.tar.gz", "python_interpreter_target": "unit_test_interpreter_target", - "requirement": "direct_sdist_without_sha @ some-archive/any-name.tar.gz", + "requirement": "direct_sdist_without_sha", "sha256": "", "urls": ["some-archive/any-name.tar.gz"], }, @@ -824,7 +824,7 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef ], "filename": "direct_without_sha-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", - "requirement": "direct_without_sha==0.0.1 @ example-direct.org/direct_without_sha-0.0.1-py3-none-any.whl", + "requirement": "direct_without_sha==0.0.1", "sha256": "", "urls": ["example-direct.org/direct_without_sha-0.0.1-py3-none-any.whl"], }, @@ -891,7 +891,7 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef ], "filename": "some_pkg-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", - "requirement": "some_pkg==0.0.1 @ example-direct.org/some_pkg-0.0.1-py3-none-any.whl --hash=sha256:deadbaaf", + "requirement": "some_pkg==0.0.1", "sha256": "deadbaaf", "urls": ["example-direct.org/some_pkg-0.0.1-py3-none-any.whl"], }, diff --git a/tests/pypi/index_sources/index_sources_tests.bzl b/tests/pypi/index_sources/index_sources_tests.bzl index 9d12bc6399..d4062b47fe 100644 --- a/tests/pypi/index_sources/index_sources_tests.bzl +++ b/tests/pypi/index_sources/index_sources_tests.bzl @@ -23,42 +23,72 @@ def _test_no_simple_api_sources(env): inputs = { "foo @ git+https://github.com/org/foo.git@deadbeef": struct( requirement = "foo @ git+https://github.com/org/foo.git@deadbeef", + requirement_line = "foo @ git+https://github.com/org/foo.git@deadbeef", marker = "", url = "git+https://github.com/org/foo.git@deadbeef", shas = [], version = "", + filename = "", ), "foo==0.0.1": struct( requirement = "foo==0.0.1", + requirement_line = "foo==0.0.1", marker = "", url = "", version = "0.0.1", + filename = "", ), "foo==0.0.1 @ https://someurl.org": struct( requirement = "foo==0.0.1 @ https://someurl.org", + requirement_line = "foo==0.0.1 @ https://someurl.org", marker = "", url = "https://someurl.org", version = "0.0.1", + filename = "", ), "foo==0.0.1 @ https://someurl.org/package.whl": struct( - requirement = "foo==0.0.1 @ https://someurl.org/package.whl", + requirement = "foo==0.0.1", + requirement_line = "foo==0.0.1 @ https://someurl.org/package.whl", marker = "", url = "https://someurl.org/package.whl", version = "0.0.1", + filename = "package.whl", ), "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef": struct( - requirement = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", + requirement = "foo==0.0.1", + requirement_line = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", marker = "", url = "https://someurl.org/package.whl", shas = ["deadbeef"], version = "0.0.1", + filename = "package.whl", ), "foo==0.0.1 @ https://someurl.org/package.whl; python_version < \"2.7\"\\ --hash=sha256:deadbeef": struct( - requirement = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", + requirement = "foo==0.0.1", + requirement_line = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", marker = "python_version < \"2.7\"", url = "https://someurl.org/package.whl", shas = ["deadbeef"], version = "0.0.1", + filename = "package.whl", + ), + "foo[extra] @ https://example.org/foo-1.0.tar.gz --hash=sha256:deadbe0f": struct( + requirement = "foo[extra]", + requirement_line = "foo[extra] @ https://example.org/foo-1.0.tar.gz --hash=sha256:deadbe0f", + marker = "", + url = "https://example.org/foo-1.0.tar.gz", + shas = ["deadbe0f"], + version = "", + filename = "foo-1.0.tar.gz", + ), + "torch @ https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=deadbeef": struct( + requirement = "torch", + requirement_line = "torch @ https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=deadbeef", + marker = "", + url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl", + shas = ["deadbeef"], + version = "", + filename = "torch-2.6.0+cpu-cp311-cp311-linux_x86_64.whl", ), } for input, want in inputs.items(): @@ -66,9 +96,10 @@ def _test_no_simple_api_sources(env): env.expect.that_collection(got.shas).contains_exactly(want.shas if hasattr(want, "shas") else []) env.expect.that_str(got.version).equals(want.version) env.expect.that_str(got.requirement).equals(want.requirement) - env.expect.that_str(got.requirement_line).equals(got.requirement) + env.expect.that_str(got.requirement_line).equals(got.requirement_line) env.expect.that_str(got.marker).equals(want.marker) env.expect.that_str(got.url).equals(want.url) + env.expect.that_str(got.filename).equals(want.filename) _tests.append(_test_no_simple_api_sources) diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index c5b24870ea..497e08361f 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -27,10 +27,6 @@ foo==0.0.1 \ """, "requirements_direct": """\ foo[extra] @ https://some-url/package.whl -bar @ https://example.org/bar-1.0.whl --hash=sha256:deadbeef -baz @ https://test.com/baz-2.0.whl; python_version < "3.8" --hash=sha256:deadb00f -qux @ https://example.org/qux-1.0.tar.gz --hash=sha256:deadbe0f -torch @ https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=5b6ae523bfb67088a17ca7734d131548a2e60346c622621e4248ed09dd0790cc """, "requirements_extra_args": """\ --index-url=example.org @@ -111,14 +107,7 @@ def _test_simple(env): extra_pip_args = [], sdist = None, is_exposed = True, - srcs = struct( - marker = "", - requirement = "foo[extra]==0.0.1", - requirement_line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", - shas = ["deadbeef"], - version = "0.0.1", - url = "", - ), + line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", target_platforms = [ "linux_x86_64", "windows_x86_64", @@ -127,16 +116,11 @@ def _test_simple(env): ), ], }) - env.expect.that_str( - select_requirement( - got["foo"], - platform = "linux_x86_64", - ).srcs.version, - ).equals("0.0.1") _tests.append(_test_simple) -def _test_direct_urls(env): +def _test_direct_urls_integration(env): + """Check that we are using the filename from index_sources.""" got = parse_requirements( ctx = _mock_ctx(), requirements_by_platform = { @@ -144,66 +128,13 @@ def _test_direct_urls(env): }, ) env.expect.that_dict(got).contains_exactly({ - "bar": [ - struct( - distribution = "bar", - extra_pip_args = [], - sdist = None, - is_exposed = True, - srcs = struct( - marker = "", - requirement = "bar @ https://example.org/bar-1.0.whl --hash=sha256:deadbeef", - requirement_line = "bar @ https://example.org/bar-1.0.whl --hash=sha256:deadbeef", - shas = ["deadbeef"], - version = "", - url = "https://example.org/bar-1.0.whl", - ), - target_platforms = ["linux_x86_64"], - whls = [struct( - url = "https://example.org/bar-1.0.whl", - filename = "bar-1.0.whl", - sha256 = "deadbeef", - yanked = False, - )], - ), - ], - "baz": [ - struct( - distribution = "baz", - extra_pip_args = [], - sdist = None, - is_exposed = True, - srcs = struct( - marker = "python_version < \"3.8\"", - requirement = "baz @ https://test.com/baz-2.0.whl --hash=sha256:deadb00f", - requirement_line = "baz @ https://test.com/baz-2.0.whl --hash=sha256:deadb00f", - shas = ["deadb00f"], - version = "", - url = "https://test.com/baz-2.0.whl", - ), - target_platforms = ["linux_x86_64"], - whls = [struct( - url = "https://test.com/baz-2.0.whl", - filename = "baz-2.0.whl", - sha256 = "deadb00f", - yanked = False, - )], - ), - ], "foo": [ struct( distribution = "foo", extra_pip_args = [], sdist = None, is_exposed = True, - srcs = struct( - marker = "", - requirement = "foo[extra] @ https://some-url/package.whl", - requirement_line = "foo[extra] @ https://some-url/package.whl", - shas = [], - version = "", - url = "https://some-url/package.whl", - ), + line = "foo[extra]", target_platforms = ["linux_x86_64"], whls = [struct( url = "https://some-url/package.whl", @@ -213,57 +144,9 @@ def _test_direct_urls(env): )], ), ], - "qux": [ - struct( - distribution = "qux", - extra_pip_args = [], - sdist = struct( - url = "https://example.org/qux-1.0.tar.gz", - filename = "qux-1.0.tar.gz", - sha256 = "deadbe0f", - yanked = False, - ), - is_exposed = True, - srcs = struct( - marker = "", - requirement = "qux @ https://example.org/qux-1.0.tar.gz --hash=sha256:deadbe0f", - requirement_line = "qux @ https://example.org/qux-1.0.tar.gz --hash=sha256:deadbe0f", - shas = ["deadbe0f"], - version = "", - url = "https://example.org/qux-1.0.tar.gz", - ), - target_platforms = ["linux_x86_64"], - whls = [], - ), - ], - "torch": [ - struct( - distribution = "torch", - extra_pip_args = [], - is_exposed = True, - sdist = None, - srcs = struct( - marker = "", - requirement = "torch @ https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=5b6ae523bfb67088a17ca7734d131548a2e60346c622621e4248ed09dd0790cc", - requirement_line = "torch @ https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=5b6ae523bfb67088a17ca7734d131548a2e60346c622621e4248ed09dd0790cc", - shas = [], - url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=5b6ae523bfb67088a17ca7734d131548a2e60346c622621e4248ed09dd0790cc", - version = "", - ), - target_platforms = ["linux_x86_64"], - whls = [ - struct( - filename = "torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl", - sha256 = "", - url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=5b6ae523bfb67088a17ca7734d131548a2e60346c622621e4248ed09dd0790cc", - yanked = False, - ), - ], - ), - ], }) -_tests.append(_test_direct_urls) +_tests.append(_test_direct_urls_integration) def _test_extra_pip_args(env): got = parse_requirements( @@ -280,14 +163,7 @@ def _test_extra_pip_args(env): extra_pip_args = ["--index-url=example.org", "--trusted-host=example.org"], sdist = None, is_exposed = True, - srcs = struct( - marker = "", - requirement = "foo[extra]==0.0.1", - requirement_line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", - shas = ["deadbeef"], - version = "0.0.1", - url = "", - ), + line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", target_platforms = [ "linux_x86_64", ], @@ -295,12 +171,6 @@ def _test_extra_pip_args(env): ), ], }) - env.expect.that_str( - select_requirement( - got["foo"], - platform = "linux_x86_64", - ).srcs.version, - ).equals("0.0.1") _tests.append(_test_extra_pip_args) @@ -318,14 +188,7 @@ def _test_dupe_requirements(env): extra_pip_args = [], sdist = None, is_exposed = True, - srcs = struct( - marker = "", - requirement = "foo[extra,extra_2]==0.0.1", - requirement_line = "foo[extra,extra_2]==0.0.1 --hash=sha256:deadbeef", - shas = ["deadbeef"], - version = "0.0.1", - url = "", - ), + line = "foo[extra,extra_2]==0.0.1 --hash=sha256:deadbeef", target_platforms = ["linux_x86_64"], whls = [], ), @@ -348,14 +211,7 @@ def _test_multi_os(env): struct( distribution = "bar", extra_pip_args = [], - srcs = struct( - marker = "", - requirement = "bar==0.0.1", - requirement_line = "bar==0.0.1 --hash=sha256:deadb00f", - shas = ["deadb00f"], - version = "0.0.1", - url = "", - ), + line = "bar==0.0.1 --hash=sha256:deadb00f", target_platforms = ["windows_x86_64"], whls = [], sdist = None, @@ -366,14 +222,7 @@ def _test_multi_os(env): struct( distribution = "foo", extra_pip_args = [], - srcs = struct( - marker = "", - requirement = "foo==0.0.3", - requirement_line = "foo==0.0.3 --hash=sha256:deadbaaf", - shas = ["deadbaaf"], - version = "0.0.3", - url = "", - ), + line = "foo==0.0.3 --hash=sha256:deadbaaf", target_platforms = ["linux_x86_64"], whls = [], sdist = None, @@ -382,14 +231,7 @@ def _test_multi_os(env): struct( distribution = "foo", extra_pip_args = [], - srcs = struct( - marker = "", - requirement = "foo[extra]==0.0.2", - requirement_line = "foo[extra]==0.0.2 --hash=sha256:deadbeef", - shas = ["deadbeef"], - version = "0.0.2", - url = "", - ), + line = "foo[extra]==0.0.2 --hash=sha256:deadbeef", target_platforms = ["windows_x86_64"], whls = [], sdist = None, @@ -401,8 +243,8 @@ def _test_multi_os(env): select_requirement( got["foo"], platform = "windows_x86_64", - ).srcs.version, - ).equals("0.0.2") + ).line, + ).equals("foo[extra]==0.0.2 --hash=sha256:deadbeef") _tests.append(_test_multi_os) @@ -422,14 +264,7 @@ def _test_multi_os_legacy(env): extra_pip_args = ["--platform=manylinux_2_17_x86_64", "--python-version=39", "--implementation=cp", "--abi=cp39"], is_exposed = False, sdist = None, - srcs = struct( - marker = "", - requirement = "bar==0.0.1", - requirement_line = "bar==0.0.1 --hash=sha256:deadb00f", - shas = ["deadb00f"], - version = "0.0.1", - url = "", - ), + line = "bar==0.0.1 --hash=sha256:deadb00f", target_platforms = ["cp39_linux_x86_64"], whls = [], ), @@ -440,14 +275,7 @@ def _test_multi_os_legacy(env): extra_pip_args = ["--platform=manylinux_2_17_x86_64", "--python-version=39", "--implementation=cp", "--abi=cp39"], is_exposed = True, sdist = None, - srcs = struct( - marker = "", - requirement = "foo==0.0.1", - requirement_line = "foo==0.0.1 --hash=sha256:deadbeef", - shas = ["deadbeef"], - version = "0.0.1", - url = "", - ), + line = "foo==0.0.1 --hash=sha256:deadbeef", target_platforms = ["cp39_linux_x86_64"], whls = [], ), @@ -456,14 +284,7 @@ def _test_multi_os_legacy(env): extra_pip_args = ["--platform=macosx_10_9_arm64", "--python-version=39", "--implementation=cp", "--abi=cp39"], is_exposed = True, sdist = None, - srcs = struct( - marker = "", - requirement_line = "foo==0.0.3 --hash=sha256:deadbaaf", - requirement = "foo==0.0.3", - shas = ["deadbaaf"], - version = "0.0.3", - url = "", - ), + line = "foo==0.0.3 --hash=sha256:deadbaaf", target_platforms = ["cp39_osx_aarch64"], whls = [], ), @@ -510,14 +331,7 @@ def _test_env_marker_resolution(env): extra_pip_args = [], is_exposed = True, sdist = None, - srcs = struct( - marker = "", - requirement = "bar==0.0.1", - requirement_line = "bar==0.0.1 --hash=sha256:deadbeef", - shas = ["deadbeef"], - version = "0.0.1", - url = "", - ), + line = "bar==0.0.1 --hash=sha256:deadbeef", target_platforms = ["cp311_linux_super_exotic", "cp311_windows_x86_64"], whls = [], ), @@ -528,25 +342,12 @@ def _test_env_marker_resolution(env): extra_pip_args = [], is_exposed = False, sdist = None, - srcs = struct( - marker = "marker", - requirement = "foo[extra]==0.0.1", - requirement_line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", - shas = ["deadbeef"], - version = "0.0.1", - url = "", - ), + line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", target_platforms = ["cp311_windows_x86_64"], whls = [], ), ], }) - env.expect.that_str( - select_requirement( - got["foo"], - platform = "windows_x86_64", - ).srcs.version, - ).equals("0.0.1") _tests.append(_test_env_marker_resolution) @@ -564,14 +365,7 @@ def _test_different_package_version(env): extra_pip_args = [], is_exposed = True, sdist = None, - srcs = struct( - marker = "", - requirement = "foo==0.0.1", - requirement_line = "foo==0.0.1 --hash=sha256:deadb00f", - shas = ["deadb00f"], - version = "0.0.1", - url = "", - ), + line = "foo==0.0.1 --hash=sha256:deadb00f", target_platforms = ["linux_x86_64"], whls = [], ), @@ -580,14 +374,7 @@ def _test_different_package_version(env): extra_pip_args = [], is_exposed = True, sdist = None, - srcs = struct( - marker = "", - requirement = "foo==0.0.1+local", - requirement_line = "foo==0.0.1+local --hash=sha256:deadbeef", - shas = ["deadbeef"], - version = "0.0.1+local", - url = "", - ), + line = "foo==0.0.1+local --hash=sha256:deadbeef", target_platforms = ["linux_x86_64"], whls = [], ), @@ -610,14 +397,7 @@ def _test_optional_hash(env): extra_pip_args = [], sdist = None, is_exposed = True, - srcs = struct( - marker = "", - requirement = "foo==0.0.4 @ https://example.org/foo-0.0.4.whl", - requirement_line = "foo==0.0.4 @ https://example.org/foo-0.0.4.whl", - shas = [], - version = "0.0.4", - url = "https://example.org/foo-0.0.4.whl", - ), + line = "foo==0.0.4", target_platforms = ["linux_x86_64"], whls = [struct( url = "https://example.org/foo-0.0.4.whl", @@ -631,14 +411,7 @@ def _test_optional_hash(env): extra_pip_args = [], sdist = None, is_exposed = True, - srcs = struct( - marker = "", - requirement = "foo==0.0.5 @ https://example.org/foo-0.0.5.whl --hash=sha256:deadbeef", - requirement_line = "foo==0.0.5 @ https://example.org/foo-0.0.5.whl --hash=sha256:deadbeef", - shas = ["deadbeef"], - version = "0.0.5", - url = "https://example.org/foo-0.0.5.whl", - ), + line = "foo==0.0.5", target_platforms = ["linux_x86_64"], whls = [struct( url = "https://example.org/foo-0.0.5.whl", @@ -666,14 +439,7 @@ def _test_git_sources(env): extra_pip_args = [], is_exposed = True, sdist = None, - srcs = struct( - marker = "", - requirement = "foo @ git+https://github.com/org/foo.git@deadbeef", - requirement_line = "foo @ git+https://github.com/org/foo.git@deadbeef", - shas = [], - url = "git+https://github.com/org/foo.git@deadbeef", - version = "", - ), + line = "foo @ git+https://github.com/org/foo.git@deadbeef", target_platforms = ["linux_x86_64"], whls = [], ), From a13fcd77cd27bd131e1b4ce227372312614613f2 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Wed, 14 May 2025 07:16:11 +0900 Subject: [PATCH 078/156] feat(pypi): actually start using env_marker_setting (#2873) Summary: - `pep508_deps` is now much simpler, because the hard work is done in analysis phase - `whl_library` BUILD.bazel tests now also have a test for the legacy flow. One thing that I noticed is that now we have an implicit dependency on the python toolchain when getting all of the `whl` target tree. This is a filegroup target that includes dependent wheels. However, we fallback to the flag values if we don't have the toolchain, so we should be good in general. Overall I like how this is turning out because we don't need to pipe the `target_platforms` anymore when we enable `PIPSTAR` feature. This means that we can start creating fewer whl_library instances - e.g. a `py3-none-any` wheel can be fetched once instead of once per python interpreter version. I'll leave this optimization for a later time. Work towards #260 --------- Co-authored-by: Richard Levasseur --- python/private/pypi/BUILD.bazel | 4 - python/private/pypi/config.bzl.tmpl.bzlmod | 2 +- python/private/pypi/extension.bzl | 35 ++- .../pypi/generate_whl_library_build_bazel.bzl | 27 +- python/private/pypi/hub_repository.bzl | 2 +- python/private/pypi/pep508_deps.bzl | 131 +++------- python/private/pypi/pep508_evaluate.bzl | 4 +- .../pypi/whl_installer/wheel_installer.py | 1 - python/private/pypi/whl_library.bzl | 4 - python/private/pypi/whl_library_targets.bzl | 77 +++--- tests/pypi/extension/extension_tests.bzl | 7 +- ...generate_whl_library_build_bazel_tests.bzl | 70 ++++- tests/pypi/pep508/deps_tests.bzl | 241 +++--------------- .../whl_installer/wheel_installer_test.py | 3 +- .../whl_library_targets_tests.bzl | 28 +- 15 files changed, 249 insertions(+), 387 deletions(-) diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index f541cbe98b..06ca3a8e34 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -236,13 +236,9 @@ bzl_library( name = "pep508_deps_bzl", srcs = ["pep508_deps.bzl"], deps = [ - ":pep508_env_bzl", ":pep508_evaluate_bzl", - ":pep508_platform_bzl", ":pep508_requirement_bzl", - "//python/private:full_version_bzl", "//python/private:normalize_name_bzl", - "@pythons_hub//:versions_bzl", ], ) diff --git a/python/private/pypi/config.bzl.tmpl.bzlmod b/python/private/pypi/config.bzl.tmpl.bzlmod index deb53631d1..c3ada70d27 100644 --- a/python/private/pypi/config.bzl.tmpl.bzlmod +++ b/python/private/pypi/config.bzl.tmpl.bzlmod @@ -6,4 +6,4 @@ with your usecase. This may change in between rules_python versions without any @generated by rules_python pip.parse bzlmod extension. """ -target_platforms = %%TARGET_PLATFORMS%% +whl_map = %%WHL_MAP%% diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 9368e6539f..84caa0aee7 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -17,6 +17,7 @@ load("@bazel_features//:features.bzl", "bazel_features") load("@pythons_hub//:interpreters.bzl", "INTERPRETER_LABELS") load("@pythons_hub//:versions.bzl", "MINOR_MAPPING") +load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") load("//python/private:auth.bzl", "AUTH_ATTRS") load("//python/private:full_version.bzl", "full_version") load("//python/private:normalize_name.bzl", "normalize_name") @@ -72,7 +73,8 @@ def _create_whl_repos( available_interpreters = INTERPRETER_LABELS, minor_mapping = MINOR_MAPPING, evaluate_markers = evaluate_markers_py, - get_index_urls = None): + get_index_urls = None, + enable_pipstar = False): """create all of the whl repositories Args: @@ -87,6 +89,7 @@ def _create_whl_repos( minor_mapping: {type}`dict[str, str]` The dictionary needed to resolve the full python version used to parse package METADATA files. evaluate_markers: the function used to evaluate the markers. + enable_pipstar: enable the pipstar feature. Returns a {type}`struct` with the following attributes: whl_map: {type}`dict[str, list[struct]]` the output is keyed by the @@ -216,7 +219,6 @@ def _create_whl_repos( enable_implicit_namespace_pkgs = pip_attr.enable_implicit_namespace_pkgs, environment = pip_attr.environment, envsubst = pip_attr.envsubst, - experimental_target_platforms = pip_attr.experimental_target_platforms, group_deps = group_deps, group_name = group_name, pip_data_exclude = pip_attr.pip_data_exclude, @@ -227,6 +229,9 @@ def _create_whl_repos( for p, args in whl_overrides.get(whl_name, {}).items() }, ) + if not enable_pipstar: + maybe_args["experimental_target_platforms"] = pip_attr.experimental_target_platforms + whl_library_args.update({k: v for k, v in maybe_args.items() if v}) maybe_args_with_default = dict( # The following values have defaults next to them @@ -249,6 +254,7 @@ def _create_whl_repos( auth_patterns = pip_attr.auth_patterns, python_version = major_minor, multiple_requirements_for_whl = len(requirements) > 1., + enable_pipstar = enable_pipstar, ).items(): repo_name = "{}_{}".format(pip_name, repo_name) if repo_name in whl_libraries: @@ -277,7 +283,7 @@ def _create_whl_repos( }, ) -def _whl_repos(*, requirement, whl_library_args, download_only, netrc, auth_patterns, multiple_requirements_for_whl = False, python_version): +def _whl_repos(*, requirement, whl_library_args, download_only, netrc, auth_patterns, multiple_requirements_for_whl = False, python_version, enable_pipstar = False): ret = {} dists = requirement.whls @@ -305,13 +311,14 @@ def _whl_repos(*, requirement, whl_library_args, download_only, netrc, auth_patt args["urls"] = [distribution.url] args["sha256"] = distribution.sha256 args["filename"] = distribution.filename - args["experimental_target_platforms"] = [ - # Get rid of the version fot the target platforms because we are - # passing the interpreter any way. Ideally we should search of ways - # how to pass the target platforms through the hub repo. - p.partition("_")[2] - for p in requirement.target_platforms - ] + if not enable_pipstar: + args["experimental_target_platforms"] = [ + # Get rid of the version fot the target platforms because we are + # passing the interpreter any way. Ideally we should search of ways + # how to pass the target platforms through the hub repo. + p.partition("_")[2] + for p in requirement.target_platforms + ] # Pure python wheels or sdists may need to have a platform here target_platforms = None @@ -357,7 +364,11 @@ def _whl_repos(*, requirement, whl_library_args, download_only, netrc, auth_patt return ret -def parse_modules(module_ctx, _fail = fail, simpleapi_download = simpleapi_download, **kwargs): +def parse_modules( + module_ctx, + _fail = fail, + simpleapi_download = simpleapi_download, + **kwargs): """Implementation of parsing the tag classes for the extension and return a struct for registering repositories. Args: @@ -639,7 +650,7 @@ def _pip_impl(module_ctx): module_ctx: module contents """ - mods = parse_modules(module_ctx) + mods = parse_modules(module_ctx, enable_pipstar = rp_config.enable_pipstar) # Build all of the wheel modifications if the tag class is called. _whl_mods_impl(mods.whl_mods) diff --git a/python/private/pypi/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl index 31c9d4da60..3764e720c0 100644 --- a/python/private/pypi/generate_whl_library_build_bazel.bzl +++ b/python/private/pypi/generate_whl_library_build_bazel.bzl @@ -26,10 +26,11 @@ _RENDER = { "entry_points": render.dict, "extras": render.list, "group_deps": render.list, + "include": str, "requires_dist": render.list, "srcs_exclude": render.list, "tags": render.list, - "target_platforms": lambda x: render.list(x) if x else "target_platforms", + "target_platforms": render.list, } # NOTE @aignas 2024-10-25: We have to keep this so that files in @@ -62,28 +63,44 @@ def generate_whl_library_build_bazel( A complete BUILD file as a string """ - fn = "whl_library_targets" + loads = [] if kwargs.get("tags"): + fn = "whl_library_targets" + # legacy path unsupported_args = [ "requires", "metadata_name", "metadata_version", + "include", ] else: - fn = "{}_from_requires".format(fn) + fn = "whl_library_targets_from_requires" unsupported_args = [ "dependencies", "dependencies_by_platform", + "target_platforms", + "default_python_version", ] + dep_template = kwargs.get("dep_template") + loads.append( + """load("{}", "{}")""".format( + dep_template.format( + name = "", + target = "config.bzl", + ), + "whl_map", + ), + ) + kwargs["include"] = "whl_map" for arg in unsupported_args: if kwargs.get(arg): fail("BUG, unsupported arg: '{}'".format(arg)) - loads = [ + loads.extend([ """load("@rules_python//python/private/pypi:whl_library_targets.bzl", "{}")""".format(fn), - ] + ]) additional_content = [] if annotation: diff --git a/python/private/pypi/hub_repository.bzl b/python/private/pypi/hub_repository.bzl index d2cbf88c24..0a1e772d05 100644 --- a/python/private/pypi/hub_repository.bzl +++ b/python/private/pypi/hub_repository.bzl @@ -49,7 +49,7 @@ def _impl(rctx): "config.bzl", rctx.attr._config_template, substitutions = { - "%%TARGET_PLATFORMS%%": render.list(rctx.attr.target_platforms), + "%%WHL_MAP%%": render.dict(rctx.attr.whl_map, value_repr = lambda x: "None"), }, ) rctx.template("requirements.bzl", rctx.attr._requirements_bzl_template, substitutions = { diff --git a/python/private/pypi/pep508_deps.bzl b/python/private/pypi/pep508_deps.bzl index bcc4845cf1..e73f747bed 100644 --- a/python/private/pypi/pep508_deps.bzl +++ b/python/private/pypi/pep508_deps.bzl @@ -15,23 +15,17 @@ """This module is for implementing PEP508 compliant METADATA deps parsing. """ -load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING") -load("//python/private:full_version.bzl", "full_version") load("//python/private:normalize_name.bzl", "normalize_name") -load(":pep508_env.bzl", "env") load(":pep508_evaluate.bzl", "evaluate") -load(":pep508_platform.bzl", "platform", "platform_from_str") load(":pep508_requirement.bzl", "requirement") def deps( name, *, requires_dist, - platforms = [], extras = [], excludes = [], - default_python_version = None, - minor_mapping = MINOR_MAPPING): + include = []): """Parse the RequiresDist from wheel METADATA Args: @@ -39,12 +33,9 @@ def deps( requires_dist: {type}`list[str]` the list of RequiresDist lines from the METADATA file. excludes: {type}`list[str]` what packages should we exclude. + include: {type}`list[str]` what packages should we exclude. If it is not + specified, then we will include all deps from `requires_dist`. extras: {type}`list[str]` the requested extras to generate targets for. - platforms: {type}`list[str]` the list of target platform strings. - default_python_version: {type}`str` the host python version. - minor_mapping: {type}`type[str, str]` the minor mapping to use when - resolving to the full python version as DEFAULT_PYTHON_VERSION can by - of format `3.x`. Returns: A struct with attributes: @@ -60,39 +51,20 @@ def deps( deps_select = {} name = normalize_name(name) want_extras = _resolve_extras(name, reqs, extras) + include = [normalize_name(n) for n in include] # drop self edges excludes = [name] + [normalize_name(x) for x in excludes] - default_python_version = default_python_version or DEFAULT_PYTHON_VERSION - if default_python_version: - # if it is not bzlmod, then DEFAULT_PYTHON_VERSION may be unset - default_python_version = full_version( - version = default_python_version, - minor_mapping = minor_mapping, - ) - platforms = [ - platform_from_str(p, python_version = default_python_version) - for p in platforms - ] - - abis = sorted({p.abi: True for p in platforms if p.abi}) - if default_python_version and len(abis) > 1: - _, _, tail = default_python_version.partition(".") - default_abi = "cp3" + tail - elif len(abis) > 1: - fail( - "all python versions need to be specified explicitly, got: {}".format(platforms), - ) - else: - default_abi = None - reqs_by_name = {} for req in reqs: if req.name_ in excludes: continue + if include and req.name_ not in include: + continue + reqs_by_name.setdefault(req.name, []).append(req) for name, reqs in reqs_by_name.items(): @@ -102,55 +74,25 @@ def deps( normalize_name(name), reqs, extras = want_extras, - platforms = platforms, - default_abi = default_abi, ) return struct( deps = sorted(deps), deps_select = { - _platform_str(p): sorted(deps) - for p, deps in deps_select.items() + d: markers + for d, markers in sorted(deps_select.items()) }, ) -def _platform_str(self): - if self.abi == None: - return "{}_{}".format(self.os, self.arch) - - return "{}_{}_{}".format( - self.abi, - self.os or "anyos", - self.arch or "anyarch", - ) - -def _add(deps, deps_select, dep, platform): +def _add(deps, deps_select, dep, markers = None): dep = normalize_name(dep) - if platform == None: + if not markers: deps[dep] = True - - # If the dep is in the platform-specific list, remove it from the select. - pop_keys = [] - for p, _deps in deps_select.items(): - if dep not in _deps: - continue - - _deps.pop(dep) - if not _deps: - pop_keys.append(p) - - for p in pop_keys: - deps_select.pop(p) - return - - if dep in deps: - # If the dep is already in the main dependency list, no need to add it in the - # platform-specific dependency list. - return - - # Add the platform-specific branch - deps_select.setdefault(platform, {})[dep] = True + elif len(markers) == 1: + deps_select[dep] = markers[0] + else: + deps_select[dep] = "({})".format(") or (".join(sorted(markers))) def _resolve_extras(self_name, reqs, extras): """Resolve extras which are due to depending on self[some_other_extra]. @@ -207,37 +149,24 @@ def _resolve_extras(self_name, reqs, extras): # Poor mans set return sorted({x: None for x in extras}) -def _add_reqs(deps, deps_select, dep, reqs, *, extras, platforms, default_abi = None): +def _add_reqs(deps, deps_select, dep, reqs, *, extras): for req in reqs: if not req.marker: - _add(deps, deps_select, dep, None) + _add(deps, deps_select, dep) return - platforms_to_add = {} - for plat in platforms: - if plat in platforms_to_add: - # marker evaluation is more expensive than this check - continue - - added = False - for extra in extras: - if added: + markers = {} + for req in reqs: + for x in extras: + m = evaluate(req.marker, env = {"extra": x}, strict = False) + if m == False: + continue + elif m == True: + _add(deps, deps_select, dep) break + else: + markers[m] = None + continue - for req in reqs: - if evaluate(req.marker, env = env(target_platform = plat, extra = extra)): - platforms_to_add[plat] = True - added = True - break - - if len(platforms_to_add) == len(platforms): - # the dep is in all target platforms, let's just add it to the regular - # list - _add(deps, deps_select, dep, None) - return - - for plat in platforms_to_add: - if default_abi: - _add(deps, deps_select, dep, plat) - if plat.abi == default_abi or not default_abi: - _add(deps, deps_select, dep, platform(os = plat.os, arch = plat.arch)) + if markers: + _add(deps, deps_select, dep, sorted(markers)) diff --git a/python/private/pypi/pep508_evaluate.bzl b/python/private/pypi/pep508_evaluate.bzl index 61a5b19999..d4492a75bb 100644 --- a/python/private/pypi/pep508_evaluate.bzl +++ b/python/private/pypi/pep508_evaluate.bzl @@ -122,7 +122,9 @@ def evaluate(marker, *, env, strict = True, **kwargs): **kwargs: Extra kwargs to be passed to the expression evaluator. Returns: - The {type}`bool` If the marker is compatible with the given env. + The {type}`bool | str` If the marker is compatible with the given env. If strict is + `False`, then the output type is `str` which will represent the remaining + expression that has not been evaluated. """ tokens = tokenize(marker) diff --git a/python/private/pypi/whl_installer/wheel_installer.py b/python/private/pypi/whl_installer/wheel_installer.py index 2db03e039d..600d45f940 100644 --- a/python/private/pypi/whl_installer/wheel_installer.py +++ b/python/private/pypi/whl_installer/wheel_installer.py @@ -126,7 +126,6 @@ def _extract_wheel( _setup_namespace_pkg_compatibility(installation_dir) metadata = { - "python_version": f"{sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]}", "entry_points": [ { "name": name, diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 4427c0e3ef..b370de448a 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -22,7 +22,6 @@ load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") load(":attrs.bzl", "ATTRS", "use_isolated") load(":deps.bzl", "all_repo_names", "record_files") load(":generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel") -load(":parse_requirements.bzl", "host_platform") load(":parse_whl_name.bzl", "parse_whl_name") load(":patch_whl.bzl", "patch_whl") load(":pypi_repo_utils.bzl", "pypi_repo_utils") @@ -352,7 +351,6 @@ def _whl_library_impl(rctx): metadata = json.decode(rctx.read("metadata.json")) rctx.delete("metadata.json") - python_version = metadata["python_version"] # NOTE @aignas 2024-06-22: this has to live on until we stop supporting # passing `twine` as a `:pkg` library via the `WORKSPACE` builds. @@ -390,9 +388,7 @@ def _whl_library_impl(rctx): entry_points = entry_points, metadata_name = metadata.name, metadata_version = metadata.version, - default_python_version = python_version, requires_dist = metadata.requires_dist, - target_platforms = rctx.attr.experimental_target_platforms or [host_platform(rctx)], # TODO @aignas 2025-04-14: load through the hub: annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), data_exclude = rctx.attr.pip_data_exclude, diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl index 21e4a54a3a..e0c03a1505 100644 --- a/python/private/pypi/whl_library_targets.bzl +++ b/python/private/pypi/whl_library_targets.bzl @@ -19,6 +19,7 @@ load("//python:py_binary.bzl", "py_binary") load("//python:py_library.bzl", "py_library") load("//python/private:glob_excludes.bzl", "glob_excludes") load("//python/private:normalize_name.bzl", "normalize_name") +load(":env_marker_setting.bzl", "env_marker_setting") load( ":labels.bzl", "DATA_LABEL", @@ -29,9 +30,7 @@ load( "WHEEL_FILE_IMPL_LABEL", "WHEEL_FILE_PUBLIC_LABEL", ) -load(":parse_whl_name.bzl", "parse_whl_name") load(":pep508_deps.bzl", "deps") -load(":whl_target_platforms.bzl", "whl_target_platforms") def whl_library_targets_from_requires( *, @@ -40,8 +39,7 @@ def whl_library_targets_from_requires( metadata_version = "", requires_dist = [], extras = [], - target_platforms = [], - default_python_version = None, + include = [], group_deps = [], **kwargs): """The macro to create whl targets from the METADATA. @@ -57,25 +55,21 @@ def whl_library_targets_from_requires( requires_dist: {type}`list[str]` The list of `Requires-Dist` values from the whl `METADATA`. extras: {type}`list[str]` The list of requested extras. This essentially includes extra transitive dependencies in the final targets depending on the wheel `METADATA`. - target_platforms: {type}`list[str]` The list of target platforms to create - dependency closures for. - default_python_version: {type}`str` The python version to assume when parsing - the `METADATA`. This is only used when the `target_platforms` do not - include the version information. + include: {type}`list[str]` The list of packages to include. **kwargs: Extra args passed to the {obj}`whl_library_targets` """ package_deps = _parse_requires_dist( - name = name, - default_python_version = default_python_version, + name = metadata_name, requires_dist = requires_dist, excludes = group_deps, extras = extras, - target_platforms = target_platforms, + include = include, ) + whl_library_targets( name = name, dependencies = package_deps.deps, - dependencies_by_platform = package_deps.deps_select, + dependencies_with_markers = package_deps.deps_select, tags = [ "pypi_name={}".format(metadata_name), "pypi_version={}".format(metadata_version), @@ -86,31 +80,16 @@ def whl_library_targets_from_requires( def _parse_requires_dist( *, name, - default_python_version, requires_dist, excludes, - extras, - target_platforms): - parsed_whl = parse_whl_name(name) - - # NOTE @aignas 2023-12-04: if the wheel is a platform specific wheel, we - # only include deps for that target platform - if parsed_whl.platform_tag != "any": - target_platforms = [ - p.target_platform - for p in whl_target_platforms( - platform_tag = parsed_whl.platform_tag, - abi_tag = parsed_whl.abi_tag.strip("tm"), - ) - ] - + include, + extras): return deps( - name = normalize_name(parsed_whl.distribution), + name = normalize_name(name), requires_dist = requires_dist, - platforms = target_platforms, excludes = excludes, + include = include, extras = extras, - default_python_version = default_python_version, ) def whl_library_targets( @@ -126,6 +105,7 @@ def whl_library_targets( }, dependencies = [], dependencies_by_platform = {}, + dependencies_with_markers = {}, group_deps = [], group_name = "", data = [], @@ -137,6 +117,7 @@ def whl_library_targets( copy_file = copy_file, py_binary = py_binary, py_library = py_library, + env_marker_setting = env_marker_setting, )): """Create all of the whl_library targets. @@ -149,6 +130,8 @@ def whl_library_targets( dependencies: {type}`list[str]` A list of dependencies. dependencies_by_platform: {type}`dict[str, list[str]]` A list of dependencies by platform key. + dependencies_with_markers: {type}`dict[str, str]` A marker to evaluate + in order for the dep to be included. filegroups: {type}`dict[str, list[str]]` A dictionary of the target names and the glob matches. group_name: {type}`str` name of the dependency group (if any) which @@ -207,10 +190,16 @@ def whl_library_targets( data.append(dest) _config_settings( - dependencies_by_platform.keys(), + dependencies_by_platform = dependencies_by_platform.keys(), + dependencies_with_markers = dependencies_with_markers, native = native, + rules = rules, visibility = ["//visibility:private"], ) + deps_conditional = { + d: "is_include_{}_true".format(d) + for d in dependencies_with_markers + } # TODO @aignas 2024-10-25: remove the entry_point generation once # `py_console_script_binary` is the only way to use entry points. @@ -290,6 +279,7 @@ def whl_library_targets( data = _deps( deps = dependencies, deps_by_platform = dependencies_by_platform, + deps_conditional = deps_conditional, tmpl = dep_template.format(name = "{}", target = WHEEL_FILE_PUBLIC_LABEL), # NOTE @aignas 2024-10-28: Actually, `select` is not part of # `native`, but in order to support bazel 6.4 in unit tests, I @@ -342,6 +332,7 @@ def whl_library_targets( deps = _deps( deps = dependencies, deps_by_platform = dependencies_by_platform, + deps_conditional = deps_conditional, tmpl = dep_template.format(name = "{}", target = PY_LIBRARY_PUBLIC_LABEL), select = getattr(native, "select", select), ), @@ -350,7 +341,7 @@ def whl_library_targets( experimental_venvs_site_packages = Label("@rules_python//python/config_settings:venvs_site_packages"), ) -def _config_settings(dependencies_by_platform, native = native, **kwargs): +def _config_settings(dependencies_by_platform, dependencies_with_markers, rules, native = native, **kwargs): """Generate config settings for the targets. Args: @@ -362,9 +353,19 @@ def _config_settings(dependencies_by_platform, native = native, **kwargs): * `@//python/config_settings:is_python_3.{minor_version}` * `{os}_{cpu}` * `cp3{minor_version}_{os}_{cpu}` + dependencies_with_markers: {type}`dict[str, str]` The markers to evaluate by + each dep. + rules: used for testing native: {type}`native` The native struct for overriding in tests. **kwargs: Extra kwargs to pass to the rule. """ + for dep, expression in dependencies_with_markers.items(): + rules.env_marker_setting( + name = "include_{}".format(dep), + expression = expression, + **kwargs + ) + for p in dependencies_by_platform: if p.startswith("@") or p.endswith("default"): continue @@ -404,9 +405,15 @@ def _plat_label(plat): else: return ":is_" + plat.replace("cp3", "python_3.") -def _deps(deps, deps_by_platform, tmpl, select = select): +def _deps(deps, deps_by_platform, deps_conditional, tmpl, select = select): deps = [tmpl.format(d) for d in sorted(deps)] + for dep, setting in deps_conditional.items(): + deps = deps + select({ + ":{}".format(setting): [tmpl.format(dep)], + "//conditions:default": [], + }) + if not deps_by_platform: return deps diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index b4a7746271..8e325724f4 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -62,7 +62,12 @@ def _mod(*, name, parse = [], override = [], whl_mods = [], is_root = True): def _parse_modules(env, **kwargs): return env.expect.that_struct( - parse_modules(**kwargs), + parse_modules( + # TODO @aignas 2025-05-11: start integration testing the branch which + # includes this. + enable_pipstar = 0, + **kwargs + ), attrs = dict( exposed_packages = subjects.dict, hub_group_map = subjects.dict, diff --git a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl index 83be7395d4..225b296ebf 100644 --- a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl +++ b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl @@ -19,8 +19,73 @@ load("//python/private/pypi:generate_whl_library_build_bazel.bzl", "generate_whl _tests = [] +def _test_all_legacy(env): + want = """\ +load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets") + +package(default_visibility = ["//visibility:public"]) + +whl_library_targets( + copy_executables = { + "exec_src": "exec_dest", + }, + copy_files = { + "file_src": "file_dest", + }, + data = ["extra_target"], + data_exclude = [ + "exclude_via_attr", + "data_exclude_all", + ], + dep_template = "@pypi//{name}:{target}", + dependencies = ["foo"], + dependencies_by_platform = { + "baz": ["bar"], + }, + entry_points = { + "foo": "bar.py", + }, + group_deps = [ + "foo", + "fox", + "qux", + ], + group_name = "qux", + name = "foo.whl", + srcs_exclude = ["srcs_exclude_all"], + tags = ["tag1"], +) + +# SOMETHING SPECIAL AT THE END +""" + actual = generate_whl_library_build_bazel( + dep_template = "@pypi//{name}:{target}", + name = "foo.whl", + dependencies = ["foo"], + dependencies_by_platform = {"baz": ["bar"]}, + entry_points = { + "foo": "bar.py", + }, + data_exclude = ["exclude_via_attr"], + annotation = struct( + copy_files = {"file_src": "file_dest"}, + copy_executables = {"exec_src": "exec_dest"}, + data = ["extra_target"], + data_exclude_glob = ["data_exclude_all"], + srcs_exclude_glob = ["srcs_exclude_all"], + additive_build_content = """# SOMETHING SPECIAL AT THE END""", + ), + group_name = "qux", + group_deps = ["foo", "fox", "qux"], + tags = ["tag1"], + ) + env.expect.that_str(actual.replace("@@", "@")).equals(want) + +_tests.append(_test_all_legacy) + def _test_all(env): want = """\ +load("@pypi//:config.bzl", "whl_map") load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires") package(default_visibility = ["//visibility:public"]) @@ -47,6 +112,7 @@ whl_library_targets_from_requires( "qux", ], group_name = "qux", + include = whl_map, name = "foo.whl", requires_dist = [ "foo", @@ -54,7 +120,6 @@ whl_library_targets_from_requires( "qux", ], srcs_exclude = ["srcs_exclude_all"], - target_platforms = ["foo"], ) # SOMETHING SPECIAL AT THE END @@ -76,7 +141,6 @@ whl_library_targets_from_requires( additive_build_content = """# SOMETHING SPECIAL AT THE END""", ), group_name = "qux", - target_platforms = ["foo"], group_deps = ["foo", "fox", "qux"], ) env.expect.that_str(actual.replace("@@", "@")).equals(want) @@ -85,6 +149,7 @@ _tests.append(_test_all) def _test_all_with_loads(env): want = """\ +load("@pypi//:config.bzl", "whl_map") load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires") package(default_visibility = ["//visibility:public"]) @@ -111,6 +176,7 @@ whl_library_targets_from_requires( "qux", ], group_name = "qux", + include = whl_map, name = "foo.whl", requires_dist = [ "foo", diff --git a/tests/pypi/pep508/deps_tests.bzl b/tests/pypi/pep508/deps_tests.bzl index 118cd50092..aaa3b2f7dd 100644 --- a/tests/pypi/pep508/deps_tests.bzl +++ b/tests/pypi/pep508/deps_tests.bzl @@ -29,101 +29,41 @@ def test_simple_deps(env): _tests.append(test_simple_deps) def test_can_add_os_specific_deps(env): - for target in [ - struct( - platforms = [ - "linux_x86_64", - "osx_x86_64", - "osx_aarch64", - "windows_x86_64", - ], - python_version = "3.3.1", - ), - struct( - platforms = [ - "cp33_linux_x86_64", - "cp33_osx_x86_64", - "cp33_osx_aarch64", - "cp33_windows_x86_64", - ], - python_version = "", - ), - struct( - platforms = [ - "cp33.1_linux_x86_64", - "cp33.1_osx_x86_64", - "cp33.1_osx_aarch64", - "cp33.1_windows_x86_64", - ], - python_version = "", - ), - ]: - got = deps( - "foo", - requires_dist = [ - "bar", - "an_osx_dep; sys_platform=='darwin'", - "posix_dep; os_name=='posix'", - "win_dep; os_name=='nt'", - ], - platforms = target.platforms, - default_python_version = target.python_version, - ) - - env.expect.that_collection(got.deps).contains_exactly(["bar"]) - env.expect.that_dict(got.deps_select).contains_exactly({ - "linux_x86_64": ["posix_dep"], - "osx_aarch64": ["an_osx_dep", "posix_dep"], - "osx_x86_64": ["an_osx_dep", "posix_dep"], - "windows_x86_64": ["win_dep"], - }) - -_tests.append(test_can_add_os_specific_deps) - -def test_deps_are_added_to_more_specialized_platforms(env): got = deps( "foo", requires_dist = [ - "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", - "mac_dep; sys_platform=='darwin'", - ], - platforms = [ - "osx_x86_64", - "osx_aarch64", + "bar", + "an_osx_dep; sys_platform=='darwin'", + "posix_dep; os_name=='posix'", + "win_dep; os_name=='nt'", ], - default_python_version = "3.8.4", ) - env.expect.that_collection(got.deps).contains_exactly(["mac_dep"]) + env.expect.that_collection(got.deps).contains_exactly(["bar"]) env.expect.that_dict(got.deps_select).contains_exactly({ - "osx_aarch64": ["m1_dep"], + "an_osx_dep": "sys_platform == \"darwin\"", + "posix_dep": "os_name == \"posix\"", + "win_dep": "os_name == \"nt\"", }) -_tests.append(test_deps_are_added_to_more_specialized_platforms) +_tests.append(test_can_add_os_specific_deps) -def test_non_platform_markers_are_added_to_common_deps(env): +def test_deps_are_added_to_more_specialized_platforms(env): got = deps( "foo", requires_dist = [ - "bar", - "baz; implementation_name=='cpython'", "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", + "mac_dep; sys_platform=='darwin'", ], - platforms = [ - "linux_x86_64", - "osx_x86_64", - "osx_aarch64", - "windows_x86_64", - ], - default_python_version = "3.8.4", ) - env.expect.that_collection(got.deps).contains_exactly(["bar", "baz"]) + env.expect.that_collection(got.deps).contains_exactly([]) env.expect.that_dict(got.deps_select).contains_exactly({ - "osx_aarch64": ["m1_dep"], + "m1_dep": "sys_platform == \"darwin\" and platform_machine == \"arm64\"", + "mac_dep": "sys_platform == \"darwin\"", }) -_tests.append(test_non_platform_markers_are_added_to_common_deps) +_tests.append(test_deps_are_added_to_more_specialized_platforms) def test_self_is_ignored(env): got = deps( @@ -167,180 +107,59 @@ def _test_can_get_deps_based_on_specific_python_version(env): "posix_dep; os_name=='posix' and python_version >= '3.8'", ] - py38 = deps( - "foo", - requires_dist = requires_dist, - platforms = ["cp38_linux_x86_64"], - ) - py373 = deps( - "foo", - requires_dist = requires_dist, - platforms = ["cp37.3_linux_x86_64"], - ) - py37 = deps( + got = deps( "foo", requires_dist = requires_dist, - platforms = ["cp37_linux_x86_64"], ) # since there is a single target platform, the deps_select will be empty - env.expect.that_collection(py37.deps).contains_exactly(["bar", "baz"]) - env.expect.that_dict(py37.deps_select).contains_exactly({}) - env.expect.that_collection(py38.deps).contains_exactly(["bar", "posix_dep"]) - env.expect.that_dict(py38.deps_select).contains_exactly({}) - env.expect.that_collection(py373.deps).contains_exactly(["bar"]) - env.expect.that_dict(py373.deps_select).contains_exactly({}) - -_tests.append(_test_can_get_deps_based_on_specific_python_version) - -def _test_no_version_select_when_single_version(env): - got = deps( - "foo", - requires_dist = [ - "bar", - "baz; python_version >= '3.8'", - "posix_dep; os_name=='posix'", - "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", - "arch_dep; platform_machine=='x86_64' and python_version >= '3.8'", - ], - platforms = [ - "cp38_linux_x86_64", - "cp38_windows_x86_64", - ], - default_python_version = "", - ) - - env.expect.that_collection(got.deps).contains_exactly(["bar", "baz", "arch_dep"]) + env.expect.that_collection(got.deps).contains_exactly(["bar"]) env.expect.that_dict(got.deps_select).contains_exactly({ - "linux_x86_64": ["posix_dep", "posix_dep_with_version"], + "baz": "python_full_version < \"3.7.3\"", + "posix_dep": "os_name == \"posix\" and python_version >= \"3.8\"", }) -_tests.append(_test_no_version_select_when_single_version) +_tests.append(_test_can_get_deps_based_on_specific_python_version) -def _test_can_get_version_select(env): +def _test_include_only_particular_deps(env): requires_dist = [ "bar", - "baz; python_version < '3.8'", - "baz_new; python_version >= '3.8'", - "posix_dep; os_name=='posix'", - "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", - "arch_dep; platform_machine=='x86_64' and python_version < '3.8'", + "baz; python_full_version < '3.7.3'", + "posix_dep; os_name=='posix' and python_version >= '3.8'", ] got = deps( "foo", requires_dist = requires_dist, - platforms = [ - "cp3{}_{}_x86_64".format(minor, os) - for minor in ["7.4", "8.8", "9.8"] - for os in ["linux", "windows"] - ], - default_python_version = "3.7", - minor_mapping = { - "3.7": "3.7.4", - }, + include = ["bar", "posix_dep"], ) + # since there is a single target platform, the deps_select will be empty env.expect.that_collection(got.deps).contains_exactly(["bar"]) env.expect.that_dict(got.deps_select).contains_exactly({ - "cp37.4_linux_x86_64": ["arch_dep", "baz", "posix_dep"], - "cp37.4_windows_x86_64": ["arch_dep", "baz"], - "cp38.8_linux_x86_64": ["baz_new", "posix_dep", "posix_dep_with_version"], - "cp38.8_windows_x86_64": ["baz_new"], - "cp39.8_linux_x86_64": ["baz_new", "posix_dep", "posix_dep_with_version"], - "cp39.8_windows_x86_64": ["baz_new"], - "linux_x86_64": ["arch_dep", "baz", "posix_dep"], - "windows_x86_64": ["arch_dep", "baz"], + "posix_dep": "os_name == \"posix\" and python_version >= \"3.8\"", }) -_tests.append(_test_can_get_version_select) +_tests.append(_test_include_only_particular_deps) -def _test_deps_spanning_all_target_py_versions_are_added_to_common(env): +def test_all_markers_are_added(env): requires_dist = [ "bar", "baz (<2,>=1.11) ; python_version < '3.8'", "baz (<2,>=1.14) ; python_version >= '3.8'", ] - default_python_version = "3.8.4" got = deps( "foo", requires_dist = requires_dist, - platforms = [ - "cp3{}_linux_x86_64".format(minor) - for minor in [7, 8, 9] - ], - default_python_version = default_python_version, - ) - - env.expect.that_collection(got.deps).contains_exactly(["bar", "baz"]) - env.expect.that_dict(got.deps_select).contains_exactly({}) - -_tests.append(_test_deps_spanning_all_target_py_versions_are_added_to_common) - -def _test_deps_are_not_duplicated(env): - default_python_version = "3.7.4" - - # See an example in - # https://files.pythonhosted.org/packages/76/9e/db1c2d56c04b97981c06663384f45f28950a73d9acf840c4006d60d0a1ff/opencv_python-4.9.0.80-cp37-abi3-win32.whl.metadata - requires_dist = [ - "bar >=0.1.0 ; python_version < '3.7'", - "bar >=0.2.0 ; python_version >= '3.7'", - "bar >=0.4.0 ; python_version >= '3.6' and platform_system == 'Linux' and platform_machine == 'aarch64'", - "bar >=0.4.0 ; python_version >= '3.9'", - "bar >=0.5.0 ; python_version <= '3.9' and platform_system == 'Darwin' and platform_machine == 'arm64'", - "bar >=0.5.0 ; python_version >= '3.10' and platform_system == 'Darwin'", - "bar >=0.5.0 ; python_version >= '3.10'", - "bar >=0.6.0 ; python_version >= '3.11'", - ] - - got = deps( - "foo", - requires_dist = requires_dist, - platforms = [ - "cp3{}_{}_{}".format(minor, os, arch) - for minor in [7, 10] - for os in ["linux", "osx", "windows"] - for arch in ["x86_64", "aarch64"] - ], - default_python_version = default_python_version, ) env.expect.that_collection(got.deps).contains_exactly(["bar"]) - env.expect.that_dict(got.deps_select).contains_exactly({}) - -_tests.append(_test_deps_are_not_duplicated) - -def _test_deps_are_not_duplicated_when_encountering_platform_dep_first(env): - # Note, that we are sorting the incoming `requires_dist` and we need to ensure that we are not getting any - # issues even if the platform-specific line comes first. - requires_dist = [ - "bar >=0.4.0 ; python_version >= '3.6' and platform_system == 'Linux' and platform_machine == 'aarch64'", - "bar >=0.5.0 ; python_version >= '3.9'", - ] - - got = deps( - "foo", - requires_dist = requires_dist, - platforms = [ - "cp37.1_linux_aarch64", - "cp37.1_linux_x86_64", - "cp310_linux_aarch64", - "cp310_linux_x86_64", - ], - default_python_version = "3.7.1", - minor_mapping = {}, - ) - - env.expect.that_collection(got.deps).contains_exactly([]) env.expect.that_dict(got.deps_select).contains_exactly({ - "cp310_linux_aarch64": ["bar"], - "cp310_linux_x86_64": ["bar"], - "cp37.1_linux_aarch64": ["bar"], - "linux_aarch64": ["bar"], + "baz": "(python_version < \"3.8\") or (python_version >= \"3.8\")", }) -_tests.append(_test_deps_are_not_duplicated_when_encountering_platform_dep_first) +_tests.append(test_all_markers_are_added) def deps_test_suite(name): # buildifier: disable=function-docstring test_suite( diff --git a/tests/pypi/whl_installer/wheel_installer_test.py b/tests/pypi/whl_installer/wheel_installer_test.py index e838047925..ef5a2483ab 100644 --- a/tests/pypi/whl_installer/wheel_installer_test.py +++ b/tests/pypi/whl_installer/wheel_installer_test.py @@ -72,7 +72,7 @@ def test_wheel_exists(self) -> None: extras={}, enable_implicit_namespace_pkgs=False, platforms=[], - enable_pipstar = False, + enable_pipstar=False, ) want_files = [ @@ -97,7 +97,6 @@ def test_wheel_exists(self) -> None: deps_by_platform={}, entry_points=[], name="example-minimal-package", - python_version="3.11.11", version="0.0.1", ) self.assertEqual(want, metadata_file_content) diff --git a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl index 432cdbfa1b..f0e5f57ac0 100644 --- a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl +++ b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl @@ -180,18 +180,20 @@ _tests.append(_test_entrypoints) def _test_whl_and_library_deps_from_requires(env): filegroup_calls = [] py_library_calls = [] + env_marker_setting_calls = [] whl_library_targets_from_requires( name = "foo-0-py3-none-any.whl", metadata_name = "Foo", metadata_version = "0", - dep_template = "@pypi_{name}//:{target}", + dep_template = "@pypi//{name}:{target}", requires_dist = [ "foo", # this self-edge will be ignored - "bar-baz", + "bar", + "bar-baz; python_version < \"8.2\"", + "booo", # this is effectively excluded due to the list below ], - target_platforms = ["cp38_linux_x86_64"], - default_python_version = "3.8.1", + include = ["foo", "bar", "bar_baz"], data_exclude = [], # Overrides for testing filegroups = {}, @@ -203,6 +205,7 @@ def _test_whl_and_library_deps_from_requires(env): ), rules = struct( py_library = lambda **kwargs: py_library_calls.append(kwargs), + env_marker_setting = lambda **kwargs: env_marker_setting_calls.append(kwargs), ), ) @@ -210,7 +213,10 @@ def _test_whl_and_library_deps_from_requires(env): { "name": "whl", "srcs": ["foo-0-py3-none-any.whl"], - "data": ["@pypi_bar_baz//:whl"], + "data": ["@pypi//bar:whl"] + _select({ + ":is_include_bar_baz_true": ["@pypi//bar_baz:whl"], + "//conditions:default": [], + }), "visibility": ["//visibility:public"], }, ]) # buildifier: @unsorted-dict-items @@ -233,12 +239,22 @@ def _test_whl_and_library_deps_from_requires(env): ] + glob_excludes.version_dependent_exclusions(), ), "imports": ["site-packages"], - "deps": ["@pypi_bar_baz//:pkg"], + "deps": ["@pypi//bar:pkg"] + _select({ + ":is_include_bar_baz_true": ["@pypi//bar_baz:pkg"], + "//conditions:default": [], + }), "tags": ["pypi_name=Foo", "pypi_version=0"], "visibility": ["//visibility:public"], "experimental_venvs_site_packages": Label("//python/config_settings:venvs_site_packages"), }, ]) # buildifier: @unsorted-dict-items + env.expect.that_collection(env_marker_setting_calls).contains_exactly([ + { + "name": "include_bar_baz", + "expression": "python_version < \"8.2\"", + "visibility": ["//visibility:private"], + }, + ]) # buildifier: @unsorted-dict-items _tests.append(_test_whl_and_library_deps_from_requires) From 367d09ec01ce5640ee9587398b6a8ce56a7eb0ba Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 13 May 2025 15:31:40 -0700 Subject: [PATCH 079/156] refactor: make python extension generate platform toolchains (#2875) This makes the python bzlmod extension handle generating the platform-specific toolchain entries ("python_3_10_{platform}"). This is to eventually allow adding additional toolchains that aren't part of the PLATFORMS mapping in versions.bzl and have their own custom constraints. The main things this refactor does are: 1. The bzlmod phase passes the full list of implementation toolchains to create (previously, it relied on `hub_repo` to generate the implementation names). 2. The name of a toolchain (the toolchain.name arg) and the repo that implements it (the py_toolchain_suite.user_repository_repo arg) are separate. This allows future work to mixin toolchains that point to arbitrary repos. 3. The platform meta data uses a list of target settings instead of dict of flag values. This allows more arbitrary target settings. For now, flag values on the platform metadata is still looked for because it's known that users patch python/versions.bzl. Along the way: * Factor out a platform_info helper in versions.bzl * Factor out a NOT_ACTUALLY_PUBLIC constants to better denote things that are public visibility, but actually internal. * Add some docs to some internals so we don't have to chase down their definitions. Work towards https://github.com/bazel-contrib/rules_python/issues/2081 --- internal_dev_setup.bzl | 12 ++- python/private/config_settings.bzl | 29 +++++- python/private/py_repositories.bzl | 12 ++- python/private/py_toolchain_suite.bzl | 11 ++- python/private/python.bzl | 102 ++++++++++++++------ python/private/pythons_hub.bzl | 116 +++++++++++----------- python/private/toolchains_repo.bzl | 67 ++++++++----- python/versions.bzl | 133 +++++++++++++++----------- 8 files changed, 309 insertions(+), 173 deletions(-) diff --git a/internal_dev_setup.bzl b/internal_dev_setup.bzl index f33908049f..62a11ab1d4 100644 --- a/internal_dev_setup.bzl +++ b/internal_dev_setup.bzl @@ -34,11 +34,15 @@ def rules_python_internal_setup(): name = "pythons_hub", minor_mapping = MINOR_MAPPING, default_python_version = "", - toolchain_prefixes = [], - toolchain_python_versions = [], - toolchain_set_python_version_constraints = [], - toolchain_user_repository_names = [], python_versions = sorted(TOOL_VERSIONS.keys()), + toolchain_names = [], + toolchain_repo_names = {}, + toolchain_target_compatible_with_map = {}, + toolchain_target_settings_map = {}, + toolchain_platform_keys = {}, + toolchain_python_versions = {}, + toolchain_set_python_version_constraints = {}, + base_toolchain_repo_names = [], ) runtime_env_repo(name = "rules_python_runtime_env_tc_info") diff --git a/python/private/config_settings.bzl b/python/private/config_settings.bzl index 1685195b78..5eb858e2e4 100644 --- a/python/private/config_settings.bzl +++ b/python/private/config_settings.bzl @@ -31,6 +31,10 @@ If the value is missing, then the default value is being used, see documentation {docs_url}/python/config_settings """ +# Indicates something needs public visibility so that other generated code can +# access it, but it's not intended for general public usage. +_NOT_ACTUALLY_PUBLIC = ["//visibility:public"] + def construct_config_settings(*, name, default_version, versions, minor_mapping, documented_flags): # buildifier: disable=function-docstring """Create a 'python_version' config flag and construct all config settings used in rules_python. @@ -128,7 +132,30 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping, # `whl_library` in the hub repo created by `pip.parse`. flag_values = {"current_config": "will-never-match"}, # Only public so that PyPI hub repo can access it - visibility = ["//visibility:public"], + visibility = _NOT_ACTUALLY_PUBLIC, + ) + + libc = Label("//python/config_settings:py_linux_libc") + native.config_setting( + name = "_is_py_linux_libc_glibc", + flag_values = {libc: "glibc"}, + visibility = _NOT_ACTUALLY_PUBLIC, + ) + native.config_setting( + name = "_is_py_linux_libc_musl", + flag_values = {libc: "glibc"}, + visibility = _NOT_ACTUALLY_PUBLIC, + ) + freethreaded = Label("//python/config_settings:py_freethreaded") + native.config_setting( + name = "_is_py_freethreaded_yes", + flag_values = {freethreaded: "yes"}, + visibility = _NOT_ACTUALLY_PUBLIC, + ) + native.config_setting( + name = "_is_py_freethreaded_no", + flag_values = {freethreaded: "no"}, + visibility = _NOT_ACTUALLY_PUBLIC, ) def _python_version_flag_impl(ctx): diff --git a/python/private/py_repositories.bzl b/python/private/py_repositories.bzl index 46ca903df4..b5bd93b7c1 100644 --- a/python/private/py_repositories.bzl +++ b/python/private/py_repositories.bzl @@ -39,11 +39,15 @@ def py_repositories(): name = "pythons_hub", minor_mapping = MINOR_MAPPING, default_python_version = "", - toolchain_prefixes = [], - toolchain_python_versions = [], - toolchain_set_python_version_constraints = [], - toolchain_user_repository_names = [], python_versions = sorted(TOOL_VERSIONS.keys()), + toolchain_names = [], + toolchain_repo_names = {}, + toolchain_target_compatible_with_map = {}, + toolchain_target_settings_map = {}, + toolchain_platform_keys = {}, + toolchain_python_versions = {}, + toolchain_set_python_version_constraints = {}, + base_toolchain_repo_names = [], ) http_archive( name = "bazel_skylib", diff --git a/python/private/py_toolchain_suite.bzl b/python/private/py_toolchain_suite.bzl index e71882dafd..fa73d5daa3 100644 --- a/python/private/py_toolchain_suite.bzl +++ b/python/private/py_toolchain_suite.bzl @@ -34,15 +34,20 @@ def py_toolchain_suite( python_version, set_python_version_constraint, flag_values, + target_settings = [], target_compatible_with = []): """For internal use only. Args: prefix: Prefix for toolchain target names. - user_repository_name: The name of the user repository. + user_repository_name: The name of the repository with the toolchain + implementation (it's assumed to have particular target names within + it). Does not include the leading "@". python_version: The full (X.Y.Z) version of the interpreter. set_python_version_constraint: True or False as a string. - flag_values: Extra flag values to match for this toolchain. + flag_values: Extra flag values to match for this toolchain. These + are prepended to target_settings. + target_settings: Extra target_settings to match for this toolchain. target_compatible_with: list constraints the toolchains are compatible with. """ @@ -82,7 +87,7 @@ def py_toolchain_suite( match_any = match_any, visibility = ["//visibility:private"], ) - target_settings = [name] + target_settings = [name] + target_settings else: fail(("Invalid set_python_version_constraint value: got {} {}, wanted " + "either the string 'True' or the string 'False'; " + diff --git a/python/private/python.bzl b/python/private/python.bzl index f49fb26d52..53cd5e9cd2 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -22,15 +22,9 @@ load(":python_register_toolchains.bzl", "python_register_toolchains") load(":pythons_hub.bzl", "hub_repo") load(":repo_utils.bzl", "repo_utils") load(":semver.bzl", "semver") -load(":text_util.bzl", "render") load(":toolchains_repo.bzl", "multi_toolchain_aliases") load(":util.bzl", "IS_BAZEL_6_4_OR_HIGHER") -# This limit can be increased essentially arbitrarily, but doing so will cause a rebuild of all -# targets using any of these toolchains due to the changed repository name. -_MAX_NUM_TOOLCHAINS = 9999 -_TOOLCHAIN_INDEX_PAD_LENGTH = len(str(_MAX_NUM_TOOLCHAINS)) - def parse_modules(*, module_ctx, _fail = fail): """Parse the modules and return a struct for registrations. @@ -240,9 +234,6 @@ def parse_modules(*, module_ctx, _fail = fail): # toolchain. We need the default last. toolchains.append(default_toolchain) - if len(toolchains) > _MAX_NUM_TOOLCHAINS: - fail("more than {} python versions are not supported".format(_MAX_NUM_TOOLCHAINS)) - # sort the toolchains so that the toolchain versions that are in the # `minor_mapping` are coming first. This ensures that `python_version = # "3.X"` transitions work as expected. @@ -275,6 +266,9 @@ def parse_modules(*, module_ctx, _fail = fail): def _python_impl(module_ctx): py = parse_modules(module_ctx = module_ctx) + # dict[str version, list[str] platforms]; where version is full + # python version string ("3.4.5"), and platforms are keys from + # the PLATFORMS global. loaded_platforms = {} for toolchain_info in py.toolchains: # Ensure that we pass the full version here. @@ -297,30 +291,82 @@ def _python_impl(module_ctx): **kwargs ) - # Create the pythons_hub repo for the interpreter meta data and the - # the various toolchains. + # List of the base names ("python_3_10") for the toolchain repos + base_toolchain_repo_names = [] + + # list[str] The infix to use for the resulting toolchain() `name` arg. + toolchain_names = [] + + # dict[str i, str repo]; where repo is the full repo name + # ("python_3_10_unknown-linux-x86_64") for the toolchain + # i corresponds to index `i` in toolchain_names + toolchain_repo_names = {} + + # dict[str i, list[str] constraints]; where constraints is a list + # of labels for target_compatible_with + # i corresponds to index `i` in toolchain_names + toolchain_tcw_map = {} + + # dict[str i, list[str] settings]; where settings is a list + # of labels for target_settings + # i corresponds to index `i` in toolchain_names + toolchain_ts_map = {} + + # dict[str i, str set_constraint]; where set_constraint is the string + # "True" or "False". + # i corresponds to index `i` in toolchain_names + toolchain_set_python_version_constraints = {} + + # dict[str i, str python_version]; where python_version is the full + # python version ("3.4.5"). + toolchain_python_versions = {} + + # dict[str i, str platform_key]; where platform_key is the key within + # the PLATFORMS global for this toolchain + toolchain_platform_keys = {} + + # Split the toolchain info into separate objects so they can be passed onto + # the repository rule. + for i, t in enumerate(py.toolchains): + is_last = (i + 1) == len(py.toolchains) + base_name = t.name + base_toolchain_repo_names.append(base_name) + fv = full_version(version = t.python_version, minor_mapping = py.config.minor_mapping) + for platform in loaded_platforms[fv]: + if platform not in PLATFORMS: + continue + key = str(len(toolchain_names)) + + full_name = "{}_{}".format(base_name, platform) + toolchain_names.append(full_name) + toolchain_repo_names[key] = full_name + toolchain_tcw_map[key] = PLATFORMS[platform].compatible_with + + # The target_settings attribute may not be present for users + # patching python/versions.bzl. + toolchain_ts_map[key] = getattr(PLATFORMS[platform], "target_settings", []) + toolchain_platform_keys[key] = platform + toolchain_python_versions[key] = fv + + # The last toolchain is the default; it can't have version constraints + # Despite the implication of the arg name, the values are strs, not bools + toolchain_set_python_version_constraints[key] = ( + "True" if not is_last else "False" + ) + hub_repo( name = "pythons_hub", - # Last toolchain is default + toolchain_names = toolchain_names, + toolchain_repo_names = toolchain_repo_names, + toolchain_target_compatible_with_map = toolchain_tcw_map, + toolchain_target_settings_map = toolchain_ts_map, + toolchain_platform_keys = toolchain_platform_keys, + toolchain_python_versions = toolchain_python_versions, + toolchain_set_python_version_constraints = toolchain_set_python_version_constraints, + base_toolchain_repo_names = [t.name for t in py.toolchains], default_python_version = py.default_python_version, minor_mapping = py.config.minor_mapping, python_versions = list(py.config.default["tool_versions"].keys()), - toolchain_prefixes = [ - render.toolchain_prefix(index, toolchain.name, _TOOLCHAIN_INDEX_PAD_LENGTH) - for index, toolchain in enumerate(py.toolchains) - ], - toolchain_python_versions = [ - full_version(version = t.python_version, minor_mapping = py.config.minor_mapping) - for t in py.toolchains - ], - # The last toolchain is the default; it can't have version constraints - # Despite the implication of the arg name, the values are strs, not bools - toolchain_set_python_version_constraints = [ - "True" if i != len(py.toolchains) - 1 else "False" - for i in range(len(py.toolchains)) - ], - toolchain_user_repository_names = [t.name for t in py.toolchains], - loaded_platforms = loaded_platforms, ) # This is require in order to support multiple version py_test diff --git a/python/private/pythons_hub.bzl b/python/private/pythons_hub.bzl index b448d53097..53351cacb9 100644 --- a/python/private/pythons_hub.bzl +++ b/python/private/pythons_hub.bzl @@ -16,7 +16,7 @@ load("//python:versions.bzl", "PLATFORMS") load(":text_util.bzl", "render") -load(":toolchains_repo.bzl", "python_toolchain_build_file_content") +load(":toolchains_repo.bzl", "toolchain_suite_content") def _have_same_length(*lists): if not lists: @@ -24,8 +24,10 @@ def _have_same_length(*lists): return len({len(length): None for length in lists}) == 1 _HUB_BUILD_FILE_TEMPLATE = """\ -load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +# Generated by @rules_python//python/private:pythons_hub.bzl + load("@@{rules_python}//python/private:py_toolchain_suite.bzl", "py_toolchain_suite") +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") bzl_library( name = "interpreters_bzl", @@ -42,44 +44,43 @@ bzl_library( {toolchains} """ -def _hub_build_file_content( - prefixes, - python_versions, - set_python_version_constraints, - user_repository_names, - workspace_location, - loaded_platforms): - """This macro iterates over each of the lists and returns the toolchain content. - - python_toolchain_build_file_content is called to generate each of the toolchain - definitions. - """ - - if not _have_same_length(python_versions, set_python_version_constraints, user_repository_names): +def _hub_build_file_content(rctx): + # Verify a precondition. If these don't match, then something went wrong. + if not _have_same_length( + rctx.attr.toolchain_names, + rctx.attr.toolchain_platform_keys, + rctx.attr.toolchain_repo_names, + rctx.attr.toolchain_target_compatible_with_map, + rctx.attr.toolchain_target_settings_map, + rctx.attr.toolchain_set_python_version_constraints, + rctx.attr.toolchain_python_versions, + ): fail("all lists must have the same length") - # Iterate over the length of python_versions and call - # build the toolchain content by calling python_toolchain_build_file_content - toolchains = "\n".join( - [ - python_toolchain_build_file_content( - prefix = prefixes[i], - python_version = python_versions[i], - set_python_version_constraint = set_python_version_constraints[i], - user_repository_name = user_repository_names[i], - loaded_platforms = { - k: v - for k, v in PLATFORMS.items() - if k in loaded_platforms[python_versions[i]] - }, - ) - for i in range(len(python_versions)) - ], - ) + #pad_length = len(str(len(rctx.attr.toolchain_names))) + 1 + pad_length = 4 + toolchains = [] + for i, base_name in enumerate(rctx.attr.toolchain_names): + key = str(i) + platform = rctx.attr.toolchain_platform_keys[key] + if platform in PLATFORMS: + flag_values = PLATFORMS[platform].flag_values + else: + flag_values = {} + + toolchains.append(toolchain_suite_content( + prefix = "_{}_{}".format(render.left_pad_zero(i, pad_length), base_name), + user_repository_name = rctx.attr.toolchain_repo_names[key], + target_compatible_with = rctx.attr.toolchain_target_compatible_with_map[key], + flag_values = flag_values, + target_settings = rctx.attr.toolchain_target_settings_map[key], + set_python_version_constraint = rctx.attr.toolchain_set_python_version_constraints[key], + python_version = rctx.attr.toolchain_python_versions[key], + )) return _HUB_BUILD_FILE_TEMPLATE.format( - toolchains = toolchains, - rules_python = workspace_location.repo_name, + toolchains = "\n".join(toolchains), + rules_python = rctx.attr._rules_python_workspace.repo_name, ) _interpreters_bzl_template = """ @@ -103,14 +104,7 @@ def _hub_repo_impl(rctx): # write them to the BUILD file. rctx.file( "BUILD.bazel", - _hub_build_file_content( - rctx.attr.toolchain_prefixes, - rctx.attr.toolchain_python_versions, - rctx.attr.toolchain_set_python_version_constraints, - rctx.attr.toolchain_user_repository_names, - rctx.attr._rules_python_workspace, - rctx.attr.loaded_platforms, - ), + _hub_build_file_content(rctx), executable = False, ) @@ -118,7 +112,7 @@ def _hub_repo_impl(rctx): # a symlink to a interpreter. interpreter_labels = "".join([ _line_for_hub_template.format(name = name) - for name in rctx.attr.toolchain_user_repository_names + for name in rctx.attr.base_toolchain_repo_names ]) rctx.file( @@ -150,13 +144,15 @@ This rule also writes out the various toolchains for the different Python versio """, implementation = _hub_repo_impl, attrs = { + "base_toolchain_repo_names": attr.string_list( + doc = "The base repo name for toolchains ('python_3_10', no " + + "platform suffix)", + mandatory = True, + ), "default_python_version": attr.string( doc = "Default Python version for the build in `X.Y` or `X.Y.Z` format.", mandatory = True, ), - "loaded_platforms": attr.string_list_dict( - doc = "The list of loaded platforms keyed by the toolchain full python version", - ), "minor_mapping": attr.string_dict( doc = "The minor mapping of the `X.Y` to `X.Y.Z` format that is used in config settings.", mandatory = True, @@ -165,20 +161,32 @@ This rule also writes out the various toolchains for the different Python versio doc = "The list of python versions to include in the `interpreters.bzl` if the toolchains are not specified. Used in `WORKSPACE` builds.", mandatory = False, ), - "toolchain_prefixes": attr.string_list( - doc = "List prefixed for the toolchains", + "toolchain_names": attr.string_list( + doc = "Names of toolchains", + mandatory = True, + ), + "toolchain_platform_keys": attr.string_dict( + doc = "The platform key in PLATFORMS for toolchains.", mandatory = True, ), - "toolchain_python_versions": attr.string_list( + "toolchain_python_versions": attr.string_dict( doc = "List of Python versions for the toolchains. In `X.Y.Z` format.", mandatory = True, ), - "toolchain_set_python_version_constraints": attr.string_list( + "toolchain_repo_names": attr.string_dict( + doc = "The repo names containing toolchain implementations.", + mandatory = True, + ), + "toolchain_set_python_version_constraints": attr.string_dict( doc = "List of version contraints for the toolchains", mandatory = True, ), - "toolchain_user_repository_names": attr.string_list( - doc = "List of the user repo names for the toolchains", + "toolchain_target_compatible_with_map": attr.string_list_dict( + doc = "The target_compatible_with settings for toolchains.", + mandatory = True, + ), + "toolchain_target_settings_map": attr.string_list_dict( + doc = "The target_settings for toolchains", mandatory = True, ), "_rules_python_workspace": attr.label(default = Label("//:does_not_matter_what_this_name_is")), diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl index 23c4643c0a..d0814b66d5 100644 --- a/python/private/toolchains_repo.bzl +++ b/python/private/toolchains_repo.bzl @@ -31,6 +31,18 @@ load( load(":repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") load(":text_util.bzl", "render") +_SUITE_TEMPLATE = """ +py_toolchain_suite( + flag_values = {flag_values}, + target_settings = {target_settings}, + prefix = {prefix}, + python_version = {python_version}, + set_python_version_constraint = {set_python_version_constraint}, + target_compatible_with = {target_compatible_with}, + user_repository_name = {user_repository_name}, +) +""".lstrip() + def python_toolchain_build_file_content( prefix, python_version, @@ -53,29 +65,40 @@ def python_toolchain_build_file_content( build_content: Text containing toolchain definitions """ - return "\n\n".join([ - """\ -py_toolchain_suite( - user_repository_name = "{user_repository_name}_{platform}", - prefix = "{prefix}{platform}", - target_compatible_with = {compatible_with}, - flag_values = {flag_values}, - python_version = "{python_version}", - set_python_version_constraint = "{set_python_version_constraint}", -)""".format( - compatible_with = render.indent(render.list(meta.compatible_with)).lstrip(), - flag_values = render.indent(render.dict( - meta.flag_values, - key_repr = lambda x: repr(str(x)), # this is to correctly display labels - )).lstrip(), - platform = platform, - set_python_version_constraint = set_python_version_constraint, - user_repository_name = user_repository_name, - prefix = prefix, + entries = [] + for platform, meta in loaded_platforms.items(): + entries.append(toolchain_suite_content( + target_compatible_with = meta.compatible_with, + flag_values = meta.flag_values, + prefix = "{}{}".format(prefix, platform), + user_repository_name = "{}_{}".format(user_repository_name, platform), python_version = python_version, - ) - for platform, meta in loaded_platforms.items() - ]) + set_python_version_constraint = set_python_version_constraint, + target_settings = [], + )) + return "\n\n".join(entries) + +def toolchain_suite_content( + *, + flag_values, + prefix, + python_version, + set_python_version_constraint, + target_compatible_with, + target_settings, + user_repository_name): + return _SUITE_TEMPLATE.format( + prefix = render.str(prefix), + user_repository_name = render.str(user_repository_name), + target_compatible_with = render.indent(render.list(target_compatible_with)).lstrip(), + flag_values = render.indent(render.dict( + flag_values, + key_repr = lambda x: repr(str(x)), # this is to correctly display labels + )).lstrip(), + target_settings = render.list(target_settings, hanging_indent = " "), + set_python_version_constraint = render.str(set_python_version_constraint), + python_version = render.str(python_version), + ) def _toolchains_repo_impl(rctx): build_content = """\ diff --git a/python/versions.bzl b/python/versions.bzl index 6343ee49c8..4a2a4cb758 100644 --- a/python/versions.bzl +++ b/python/versions.bzl @@ -682,151 +682,170 @@ MINOR_MAPPING = { "3.13": "3.13.2", } +def _platform_info( + *, + compatible_with = [], + flag_values = {}, + target_settings = [], + os_name, + arch): + """Creates a struct of platform metadata. + + Args: + compatible_with: list[str], where the values are string labels. These + are the target_compatible_with values to use with the toolchain + flag_values: dict[str|Label, Any] of config_setting.flag_values + compatible values. DEPRECATED -- use target_settings instead + target_settings: list[str], where the values are string labels. These + are the target_settings values to use with the toolchain. + os_name: str, the os name; must match the name used in `@platfroms//os` + arch: str, the cpu name; must match the name used in `@platforms//cpu` + + Returns: + A struct with attributes and values matching the args. + """ + return struct( + compatible_with = compatible_with, + flag_values = flag_values, + target_settings = target_settings, + os_name = os_name, + arch = arch, + ) + def _generate_platforms(): - libc = Label("//python/config_settings:py_linux_libc") + is_libc_glibc = str(Label("//python/config_settings:_is_py_linux_libc_glibc")) + is_libc_musl = str(Label("//python/config_settings:_is_py_linux_libc_musl")) platforms = { - "aarch64-apple-darwin": struct( + "aarch64-apple-darwin": _platform_info( compatible_with = [ "@platforms//os:macos", "@platforms//cpu:aarch64", ], - flag_values = {}, os_name = MACOS_NAME, - # Matches the value in @platforms//cpu package arch = "aarch64", ), - "aarch64-unknown-linux-gnu": struct( + "aarch64-unknown-linux-gnu": _platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:aarch64", ], - flag_values = { - libc: "glibc", - }, + target_settings = [ + is_libc_glibc, + ], os_name = LINUX_NAME, - # Matches the value in @platforms//cpu package arch = "aarch64", ), - "armv7-unknown-linux-gnu": struct( + "armv7-unknown-linux-gnu": _platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:armv7", ], - flag_values = { - libc: "glibc", - }, + target_settings = [ + is_libc_glibc, + ], os_name = LINUX_NAME, - # Matches the value in @platforms//cpu package arch = "arm", ), - "i386-unknown-linux-gnu": struct( + "i386-unknown-linux-gnu": _platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:i386", ], - flag_values = { - libc: "glibc", - }, + target_settings = [ + is_libc_glibc, + ], os_name = LINUX_NAME, - # Matches the value in @platforms//cpu package arch = "x86_32", ), - "ppc64le-unknown-linux-gnu": struct( + "ppc64le-unknown-linux-gnu": _platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:ppc", ], - flag_values = { - libc: "glibc", - }, + target_settings = [ + is_libc_glibc, + ], os_name = LINUX_NAME, - # Matches the value in @platforms//cpu package arch = "ppc", ), - "riscv64-unknown-linux-gnu": struct( + "riscv64-unknown-linux-gnu": _platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:riscv64", ], - flag_values = { - Label("//python/config_settings:py_linux_libc"): "glibc", - }, + target_settings = [ + is_libc_glibc, + ], os_name = LINUX_NAME, - # Matches the value in @platforms//cpu package arch = "riscv64", ), - "s390x-unknown-linux-gnu": struct( + "s390x-unknown-linux-gnu": _platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:s390x", ], - flag_values = { - Label("//python/config_settings:py_linux_libc"): "glibc", - }, + target_settings = [ + is_libc_glibc, + ], os_name = LINUX_NAME, - # Matches the value in @platforms//cpu package arch = "s390x", ), - "x86_64-apple-darwin": struct( + "x86_64-apple-darwin": _platform_info( compatible_with = [ "@platforms//os:macos", "@platforms//cpu:x86_64", ], - flag_values = {}, os_name = MACOS_NAME, - # Matches the value in @platforms//cpu package arch = "x86_64", ), - "x86_64-pc-windows-msvc": struct( + "x86_64-pc-windows-msvc": _platform_info( compatible_with = [ "@platforms//os:windows", "@platforms//cpu:x86_64", ], - flag_values = {}, os_name = WINDOWS_NAME, - # Matches the value in @platforms//cpu package arch = "x86_64", ), - "x86_64-unknown-linux-gnu": struct( + "x86_64-unknown-linux-gnu": _platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:x86_64", ], - flag_values = { - libc: "glibc", - }, + target_settings = [ + is_libc_glibc, + ], os_name = LINUX_NAME, - # Matches the value in @platforms//cpu package arch = "x86_64", ), - "x86_64-unknown-linux-musl": struct( + "x86_64-unknown-linux-musl": _platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:x86_64", ], - flag_values = { - libc: "musl", - }, + target_settings = [ + is_libc_musl, + ], os_name = LINUX_NAME, arch = "x86_64", ), } - freethreaded = Label("//python/config_settings:py_freethreaded") + is_freethreaded_yes = str(Label("//python/config_settings:_is_py_freethreaded_yes")) + is_freethreaded_no = str(Label("//python/config_settings:_is_py_freethreaded_no")) return { - p + suffix: struct( + p + suffix: _platform_info( compatible_with = v.compatible_with, - flag_values = { - freethreaded: freethreaded_value, - } | v.flag_values, + target_settings = [ + freethreadedness, + ] + v.target_settings, os_name = v.os_name, arch = v.arch, ) for p, v in platforms.items() - for suffix, freethreaded_value in { - "": "no", - "-" + FREETHREADED: "yes", + for suffix, freethreadedness in { + "": is_freethreaded_no, + "-" + FREETHREADED: is_freethreaded_yes, }.items() } From 61b5a8d738a5478f6ffd354e3dc2459e089c287c Mon Sep 17 00:00:00 2001 From: Garrett Holmstrom Date: Tue, 13 May 2025 21:08:27 -0700 Subject: [PATCH 080/156] Fix whl_library file path inference (#2876) When given .whl file URLs and no file name, `whl_library` writes the wheel it downloads to a file with the same file name as the first URL's. But then at extraction time, it always consults ctx.attr.filename for that file name, leading to failure when that attribute is None. This patch should fix that. Related #2363 --- CHANGELOG.md | 1 + python/private/pypi/whl_library.bzl | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b94072d655..94487219bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ END_UNRELEASED_TEMPLATE various URL formats - URL encoded version strings get correctly resolved, sha256 value can be also retrieved from the URL as opposed to only the `--hash` parameter. Fixes [#2363](https://github.com/bazel-contrib/rules_python/issues/2363). +* (pypi) `whl_library` now infers file names from its `urls` attribute correctly. {#v0-0-0-added} ### Added diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index b370de448a..17ee3d3cfe 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -277,7 +277,7 @@ def _whl_library_impl(rctx): fail("could not download the '{}' from {}:\n{}".format(filename, urls, result)) if filename.endswith(".whl"): - whl_path = rctx.path(rctx.attr.filename) + whl_path = rctx.path(filename) else: # It is an sdist and we need to tell PyPI to use a file in this directory # and, allow getting build dependencies from PYTHONPATH, which we From ee8d7d618cff43811779bb710d330ccd9bd577f2 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 16 May 2025 00:40:29 +0900 Subject: [PATCH 081/156] refactor: consolidate version parsing (#2874) This PR removes all of the custom version parsing functions where we try to make sense about the version (e.g. extracting major/minor versions). Whilst doing this I actually think that I made it easier to support #2837. --- python/private/BUILD.bazel | 9 +- python/private/config_settings.bzl | 6 +- .../private/hermetic_runtime_repo_setup.bzl | 15 ++- python/private/pypi/BUILD.bazel | 4 +- python/private/pypi/extension.bzl | 8 +- python/private/python.bzl | 37 +++--- python/private/semver.bzl | 85 ------------- python/private/version.bzl | 28 +++-- tests/python/python_tests.bzl | 14 +-- tests/semver/BUILD.bazel | 17 --- tests/semver/semver_test.bzl | 113 ------------------ 11 files changed, 61 insertions(+), 275 deletions(-) delete mode 100644 python/private/semver.bzl delete mode 100644 tests/semver/BUILD.bazel delete mode 100644 tests/semver/semver_test.bzl diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index e72a8fcaa7..0b50ccf0b7 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -139,7 +139,7 @@ bzl_library( name = "config_settings_bzl", srcs = ["config_settings.bzl"], deps = [ - ":semver_bzl", + ":version_bzl", "@bazel_skylib//lib:selects", "@bazel_skylib//rules:common_settings", ], @@ -249,9 +249,9 @@ bzl_library( ":python_register_toolchains_bzl", ":pythons_hub_bzl", ":repo_utils_bzl", - ":semver_bzl", ":toolchains_repo_bzl", ":util_bzl", + ":version_bzl", "@bazel_features//:features", ], ) @@ -610,11 +610,6 @@ bzl_library( ], ) -bzl_library( - name = "semver_bzl", - srcs = ["semver.bzl"], -) - bzl_library( name = "sentinel_bzl", srcs = ["sentinel.bzl"], diff --git a/python/private/config_settings.bzl b/python/private/config_settings.bzl index 5eb858e2e4..aff5d016fb 100644 --- a/python/private/config_settings.bzl +++ b/python/private/config_settings.bzl @@ -18,7 +18,7 @@ load("@bazel_skylib//lib:selects.bzl", "selects") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load("//python/private:text_util.bzl", "render") -load(":semver.bzl", "semver") +load(":version.bzl", "version") _PYTHON_VERSION_FLAG = Label("//python/config_settings:python_version") _PYTHON_VERSION_MAJOR_MINOR_FLAG = Label("//python/config_settings:python_version_major_minor") @@ -181,8 +181,8 @@ _python_version_flag = rule( def _python_version_major_minor_flag_impl(ctx): input = _flag_value(ctx.attr._python_version_flag) if input: - version = semver(input) - value = "{}.{}".format(version.major, version.minor) + ver = version.parse(input) + value = "{}.{}".format(ver.release[0], ver.release[1]) else: value = "" diff --git a/python/private/hermetic_runtime_repo_setup.bzl b/python/private/hermetic_runtime_repo_setup.bzl index 64d721ecad..f944b0b914 100644 --- a/python/private/hermetic_runtime_repo_setup.bzl +++ b/python/private/hermetic_runtime_repo_setup.bzl @@ -20,7 +20,7 @@ load("//python:py_runtime_pair.bzl", "py_runtime_pair") load("//python/cc:py_cc_toolchain.bzl", "py_cc_toolchain") load(":glob_excludes.bzl", "glob_excludes") load(":py_exec_tools_toolchain.bzl", "py_exec_tools_toolchain") -load(":semver.bzl", "semver") +load(":version.bzl", "version") _IS_FREETHREADED = Label("//python/config_settings:is_py_freethreaded") @@ -53,8 +53,11 @@ def define_hermetic_runtime_toolchain_impl( use. """ _ = name # @unused - version_info = semver(python_version) - version_dict = version_info.to_dict() + version_info = version.parse(python_version) + version_dict = { + "major": version_info.release[0], + "minor": version_info.release[1], + } native.filegroup( name = "files", srcs = native.glob( @@ -198,9 +201,9 @@ def define_hermetic_runtime_toolchain_impl( files = [":files"], interpreter = python_bin, interpreter_version_info = { - "major": str(version_info.major), - "micro": str(version_info.patch), - "minor": str(version_info.minor), + "major": str(version_info.release[0]), + "micro": str(version_info.release[2]), + "minor": str(version_info.release[1]), }, coverage_tool = select({ # Convert empty string to None diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 06ca3a8e34..84e0535289 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -116,7 +116,7 @@ bzl_library( ":whl_target_platforms_bzl", "//python/private:full_version_bzl", "//python/private:normalize_name_bzl", - "//python/private:semver_bzl", + "//python/private:version_bzl", "//python/private:version_label_bzl", "@bazel_features//:features", "@pythons_hub//:interpreters_bzl", @@ -256,7 +256,7 @@ bzl_library( srcs = ["pep508_evaluate.bzl"], deps = [ "//python/private:enum_bzl", - "//python/private:semver_bzl", + "//python/private:version_bzl", ], ) diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 84caa0aee7..3896f2940a 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -22,7 +22,7 @@ load("//python/private:auth.bzl", "AUTH_ATTRS") load("//python/private:full_version.bzl", "full_version") load("//python/private:normalize_name.bzl", "normalize_name") load("//python/private:repo_utils.bzl", "repo_utils") -load("//python/private:semver.bzl", "semver") +load("//python/private:version.bzl", "version") load("//python/private:version_label.bzl", "version_label") load(":attrs.bzl", "use_isolated") load(":evaluate_markers.bzl", "evaluate_markers_py", EVALUATE_MARKERS_SRCS = "SRCS") @@ -36,9 +36,9 @@ load(":whl_config_setting.bzl", "whl_config_setting") load(":whl_library.bzl", "whl_library") load(":whl_repo_name.bzl", "pypi_repo_name", "whl_repo_name") -def _major_minor_version(version): - version = semver(version) - return "{}.{}".format(version.major, version.minor) +def _major_minor_version(version_str): + ver = version.parse(version_str) + return "{}.{}".format(ver.release[0], ver.release[1]) def _whl_mods_impl(whl_mods_dict): """Implementation of the pip.whl_mods tag class. diff --git a/python/private/python.bzl b/python/private/python.bzl index 53cd5e9cd2..c187904322 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -21,9 +21,9 @@ load(":full_version.bzl", "full_version") load(":python_register_toolchains.bzl", "python_register_toolchains") load(":pythons_hub.bzl", "hub_repo") load(":repo_utils.bzl", "repo_utils") -load(":semver.bzl", "semver") load(":toolchains_repo.bzl", "multi_toolchain_aliases") load(":util.bzl", "IS_BAZEL_6_4_OR_HIGHER") +load(":version.bzl", "version") def parse_modules(*, module_ctx, _fail = fail): """Parse the modules and return a struct for registrations. @@ -458,16 +458,20 @@ def _fail_multiple_default_toolchains(first, second): second = second, )) -def _validate_version(*, version, _fail = fail): - parsed = semver(version) - if parsed.patch == None or parsed.build or parsed.pre_release: - _fail("The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '{}'".format(version)) +def _validate_version(version_str, *, _fail = fail): + v = version.parse(version_str, strict = True, _fail = _fail) + if v == None: + # Only reachable in tests + return False + + if len(v.release) < 3: + _fail("The 'python_version' attribute needs to specify the full version in at least 'X.Y.Z' format, got: '{}'".format(v.string)) return False return True def _process_single_version_overrides(*, tag, _fail = fail, default): - if not _validate_version(version = tag.python_version, _fail = _fail): + if not _validate_version(tag.python_version, _fail = _fail): return available_versions = default["tool_versions"] @@ -517,7 +521,7 @@ def _process_single_version_overrides(*, tag, _fail = fail, default): kwargs.setdefault(tag.python_version, {})["distutils"] = tag.distutils def _process_single_version_platform_overrides(*, tag, _fail = fail, default): - if not _validate_version(version = tag.python_version, _fail = _fail): + if not _validate_version(tag.python_version, _fail = _fail): return available_versions = default["tool_versions"] @@ -558,12 +562,12 @@ def _process_global_overrides(*, tag, default, _fail = fail): if tag.minor_mapping: for minor_version, full_version in tag.minor_mapping.items(): - parsed = semver(minor_version) - if parsed.patch != None or parsed.build or parsed.pre_release: - fail("Expected the key to be of `X.Y` format but got `{}`".format(minor_version)) - parsed = semver(full_version) - if parsed.patch == None: - fail("Expected the value to at least be of `X.Y.Z` format but got `{}`".format(minor_version)) + parsed = version.parse(minor_version, strict = True, _fail = _fail) + if len(parsed.release) > 2 or parsed.pre or parsed.post or parsed.dev or parsed.local: + fail("Expected the key to be of `X.Y` format but got `{}`".format(parsed.string)) + + # Ensure that the version is valid + version.parse(full_version, strict = True, _fail = _fail) default["minor_mapping"] = tag.minor_mapping @@ -651,8 +655,11 @@ def _get_toolchain_config(*, modules, _fail = fail): versions = {} for version_string in available_versions: - v = semver(version_string) - versions.setdefault("{}.{}".format(v.major, v.minor), []).append((int(v.patch), version_string)) + v = version.parse(version_string, strict = True) + versions.setdefault( + "{}.{}".format(v.release[0], v.release[1]), + [], + ).append((version.key(v), v.string)) minor_mapping = { major_minor: max(subset)[1] diff --git a/python/private/semver.bzl b/python/private/semver.bzl deleted file mode 100644 index 0cbd172348..0000000000 --- a/python/private/semver.bzl +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright 2024 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. - -"A semver version parser" - -def _key(version): - return ( - version.major, - version.minor or 0, - version.patch or 0, - # non pre-release versions are higher - version.pre_release == "", - # then we compare each element of the pre_release tag separately - tuple([ - ( - i if not i.isdigit() else "", - # digit values take precedence - int(i) if i.isdigit() else 0, - ) - for i in version.pre_release.split(".") - ]) if version.pre_release else None, - # And build info is just alphabetic - version.build, - ) - -def _to_dict(self): - return { - "build": self.build, - "major": self.major, - "minor": self.minor, - "patch": self.patch, - "pre_release": self.pre_release, - } - -def _new(*, major, minor, patch, pre_release, build, version = None): - # buildifier: disable=uninitialized - self = struct( - major = int(major), - minor = None if minor == None else int(minor), - # NOTE: this is called `micro` in the Python interpreter versioning scheme - patch = None if patch == None else int(patch), - pre_release = pre_release, - build = build, - # buildifier: disable=uninitialized - key = lambda: _key(self), - str = lambda: version, - to_dict = lambda: _to_dict(self), - ) - return self - -def semver(version): - """Parse the semver version and return the values as a struct. - - Args: - version: {type}`str` the version string. - - Returns: - A {type}`struct` with `major`, `minor`, `patch` and `build` attributes. - """ - - # Implement the https://semver.org/ spec - major, _, tail = version.partition(".") - minor, _, tail = tail.partition(".") - patch, _, build = tail.partition("+") - patch, _, pre_release = patch.partition("-") - - return _new( - major = int(major), - minor = int(minor) if minor.isdigit() else None, - patch = int(patch) if patch.isdigit() else None, - build = build, - pre_release = pre_release, - version = version, - ) diff --git a/python/private/version.bzl b/python/private/version.bzl index 4425cc7661..f98165d391 100644 --- a/python/private/version.bzl +++ b/python/private/version.bzl @@ -510,7 +510,7 @@ def normalize_pep440(version): """ return _parse(version, strict = True)["norm"] -def _parse(version_str, strict = True): +def _parse(version_str, strict = True, _fail = fail): """Escape the version component of a filename. See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode @@ -519,6 +519,7 @@ def _parse(version_str, strict = True): Args: version_str: version string to be normalized according to PEP 440. strict: fail if the version is invalid, defaults to True. + _fail: Used for tests Returns: string containing the normalized version. @@ -544,7 +545,7 @@ def _parse(version_str, strict = True): parser_ctx = parser.context() if parser.input[parser_ctx["start"]:]: if strict: - fail( + _fail( "Failed to parse PEP 440 version identifier '%s'." % parser.input, "Parse error at '%s'" % parser.input[parser_ctx["start"]:], ) @@ -554,7 +555,7 @@ def _parse(version_str, strict = True): parser_ctx["is_prefix"] = is_prefix return parser_ctx -def parse(version_str, strict = False): +def parse(version_str, strict = False, _fail = fail): """Parse a PEP4408 compliant version. This is similar to `normalize_pep440`, but it parses individual components to @@ -563,6 +564,7 @@ def parse(version_str, strict = False): Args: version_str: version string to be normalized according to PEP 440. strict: fail if the version is invalid. + _fail: used for tests Returns: a struct with individual components of a version: @@ -580,29 +582,29 @@ def parse(version_str, strict = False): * `string` {type}`str` normalized value of the input. """ - parts = _parse(version_str, strict = strict) + parts = _parse(version_str, strict = strict, _fail = _fail) if not parts: return None if parts["is_prefix"] and (parts["local"] or parts["post"] or parts["dev"] or parts["pre"]): if strict: - fail("local version part has been obtained, but only public segments can have prefix matches") + _fail("local version part has been obtained, but only public segments can have prefix matches") # https://peps.python.org/pep-0440/#public-version-identifiers return None return struct( - epoch = _parse_epoch(parts["epoch"]), + epoch = _parse_epoch(parts["epoch"], _fail), release = _parse_release(parts["release"]), pre = _parse_pre(parts["pre"]), - post = _parse_post(parts["post"]), - dev = _parse_dev(parts["dev"]), - local = _parse_local(parts["local"]), + post = _parse_post(parts["post"], _fail), + dev = _parse_dev(parts["dev"], _fail), + local = _parse_local(parts["local"], _fail), string = parts["norm"], is_prefix = parts["is_prefix"], ) -def _parse_epoch(value): +def _parse_epoch(value, fail): if not value: return 0 @@ -614,7 +616,7 @@ def _parse_epoch(value): def _parse_release(value): return tuple([int(d) for d in value.split(".")]) -def _parse_local(value): +def _parse_local(value, fail): if not value: return None @@ -624,7 +626,7 @@ def _parse_local(value): # If the part is numerical, handle it as a number return tuple([int(part) if part.isdigit() else part for part in value[1:].split(".")]) -def _parse_dev(value): +def _parse_dev(value, fail): if not value: return None @@ -646,7 +648,7 @@ def _parse_pre(value): return (prefix, int(value[len(prefix):])) -def _parse_post(value): +def _parse_post(value, fail): if not value: return None diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl index 97c47b57db..443174c966 100644 --- a/tests/python/python_tests.bzl +++ b/tests/python/python_tests.bzl @@ -746,12 +746,6 @@ def _test_single_version_override_errors(env): ], want_error = "Only a single 'python.single_version_override' can be present for '3.12.4'", ), - struct( - overrides = [ - _single_version_override(python_version = "3.12.4+3", distutils_content = "foo"), - ], - want_error = "The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '3.12.4+3'", - ), ]: errors = [] parse_modules( @@ -781,13 +775,13 @@ def _test_single_version_platform_override_errors(env): overrides = [ _single_version_platform_override(python_version = "3.12", platform = "foo"), ], - want_error = "The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '3.12'", + want_error = "The 'python_version' attribute needs to specify the full version in at least 'X.Y.Z' format, got: '3.12'", ), struct( overrides = [ - _single_version_platform_override(python_version = "3.12.1+my_build", platform = "foo"), + _single_version_platform_override(python_version = "foo", platform = "foo"), ], - want_error = "The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '3.12.1+my_build'", + want_error = "Failed to parse PEP 440 version identifier 'foo'. Parse error at 'foo'", ), ]: errors = [] @@ -799,7 +793,7 @@ def _test_single_version_platform_override_errors(env): single_version_platform_override = test.overrides, ), ), - _fail = errors.append, + _fail = lambda *a: errors.append(" ".join(a)), ) env.expect.that_collection(errors).contains_exactly([test.want_error]) diff --git a/tests/semver/BUILD.bazel b/tests/semver/BUILD.bazel deleted file mode 100644 index e12b1e5300..0000000000 --- a/tests/semver/BUILD.bazel +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2024 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(":semver_test.bzl", "semver_test_suite") - -semver_test_suite(name = "semver_tests") diff --git a/tests/semver/semver_test.bzl b/tests/semver/semver_test.bzl deleted file mode 100644 index 9d13402c92..0000000000 --- a/tests/semver/semver_test.bzl +++ /dev/null @@ -1,113 +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. - -"" - -load("@rules_testing//lib:test_suite.bzl", "test_suite") -load("//python/private:semver.bzl", "semver") # buildifier: disable=bzl-visibility - -_tests = [] - -def _test_semver_from_major(env): - actual = semver("3") - env.expect.that_int(actual.major).equals(3) - env.expect.that_int(actual.minor).equals(None) - env.expect.that_int(actual.patch).equals(None) - env.expect.that_str(actual.build).equals("") - -_tests.append(_test_semver_from_major) - -def _test_semver_from_major_minor_version(env): - actual = semver("4.9") - env.expect.that_int(actual.major).equals(4) - env.expect.that_int(actual.minor).equals(9) - env.expect.that_int(actual.patch).equals(None) - env.expect.that_str(actual.build).equals("") - -_tests.append(_test_semver_from_major_minor_version) - -def _test_semver_with_build_info(env): - actual = semver("1.2.3+mybuild") - env.expect.that_int(actual.major).equals(1) - env.expect.that_int(actual.minor).equals(2) - env.expect.that_int(actual.patch).equals(3) - env.expect.that_str(actual.build).equals("mybuild") - -_tests.append(_test_semver_with_build_info) - -def _test_semver_with_build_info_multiple_pluses(env): - actual = semver("1.2.3-rc0+build+info") - env.expect.that_int(actual.major).equals(1) - env.expect.that_int(actual.minor).equals(2) - env.expect.that_int(actual.patch).equals(3) - env.expect.that_str(actual.pre_release).equals("rc0") - env.expect.that_str(actual.build).equals("build+info") - -_tests.append(_test_semver_with_build_info_multiple_pluses) - -def _test_semver_alpha_beta(env): - actual = semver("1.2.3-alpha.beta") - env.expect.that_int(actual.major).equals(1) - env.expect.that_int(actual.minor).equals(2) - env.expect.that_int(actual.patch).equals(3) - env.expect.that_str(actual.pre_release).equals("alpha.beta") - -_tests.append(_test_semver_alpha_beta) - -def _test_semver_sort(env): - want = [ - semver(item) - for item in [ - # The items are sorted from lowest to highest version - "0.0.1", - "0.1.0-rc", - "0.1.0", - "0.9.11", - "0.9.12", - "1.0.0-alpha", - "1.0.0-alpha.1", - "1.0.0-alpha.beta", - "1.0.0-beta", - "1.0.0-beta.2", - "1.0.0-beta.11", - "1.0.0-rc.1", - "1.0.0-rc.2", - "1.0.0", - # Also handle missing minor and patch version strings - "2.0", - "3", - # Alphabetic comparison for different builds - "3.0.0+build0", - "3.0.0+build1", - ] - ] - actual = sorted(want, key = lambda x: x.key()) - env.expect.that_collection(actual).contains_exactly(want).in_order() - for i, greater in enumerate(want[1:]): - smaller = actual[i] - if greater.key() <= smaller.key(): - env.fail("Expected '{}' to be smaller than '{}', but got otherwise".format( - smaller.str(), - greater.str(), - )) - -_tests.append(_test_semver_sort) - -def semver_test_suite(name): - """Create the test suite. - - Args: - name: the name of the test suite - """ - test_suite(name = name, basic_tests = _tests) From ea4714d33adb82c68739bdfd74038faa4d60b061 Mon Sep 17 00:00:00 2001 From: Philipp Schrader Date: Thu, 15 May 2025 19:11:34 -0700 Subject: [PATCH 082/156] feat: Add support for REPLs (#2723) This patch adds a new target that lets users invoke a REPL for a given `PyInfo` target. For example, the following command will spawn a REPL for any target that provides `PyInfo`: ```console $ bazel run --//python/config_settings:bootstrap_impl=script //python/bin:repl --//python/bin:repl_dep=//tools:wheelmaker Python 3.11.1 (main, Jan 16 2023, 22:41:20) [Clang 15.0.7 ] on linux Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> import tools.wheelmaker >>> ``` If the user wants an IPython shell instead, they can create a file like this: ```python import IPython IPython.start_ipython() ``` Then they can set this up in their `.bazelrc` file: ``` # Allow the REPL stub to import ipython. In this case, @my_deps is the name # of the pip.parse() repository. build --@rules_python//python/bin:repl_stub_dep=@my_deps//ipython # Point the REPL at the stub created above. build --@rules_python//python/bin:repl_stub=//path/to:ipython_stub.py ``` --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- CHANGELOG.md | 2 + docs/index.md | 1 + docs/repl.md | 66 +++++++++++++++++++++++++ docs/toolchains.md | 10 ++++ python/bin/BUILD.bazel | 33 +++++++++++++ python/bin/repl_stub.py | 29 +++++++++++ python/private/BUILD.bazel | 4 ++ python/private/repl.bzl | 84 ++++++++++++++++++++++++++++++++ python/private/repl_template.py | 37 ++++++++++++++ tests/repl/BUILD.bazel | 44 +++++++++++++++++ tests/repl/helper/test_module.py | 5 ++ tests/repl/repl_test.py | 74 ++++++++++++++++++++++++++++ tests/support/sh_py_run_test.bzl | 4 ++ 13 files changed, 393 insertions(+) create mode 100644 docs/repl.md create mode 100644 python/bin/repl_stub.py create mode 100644 python/private/repl.bzl create mode 100644 python/private/repl_template.py create mode 100644 tests/repl/BUILD.bazel create mode 100644 tests/repl/helper/test_module.py create mode 100644 tests/repl/repl_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 94487219bc..a6ba65eb2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,8 @@ END_UNRELEASED_TEMPLATE * (pypi) Starlark-based evaluation of environment markers (requirements.txt conditionals) available (not enabled by default) for improved multi-platform build support. Set the `RULES_PYTHON_ENABLE_PIPSTAR=1` environment variable to enable it. +* (utils) Add a way to run a REPL for any `rules_python` target that returns + a `PyInfo` provider. {#v0-0-0-removed} ### Removed diff --git a/docs/index.md b/docs/index.md index b10b445983..285b1cd66e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -101,6 +101,7 @@ pip coverage precompiling gazelle +REPL Extending Contributing support diff --git a/docs/repl.md b/docs/repl.md new file mode 100644 index 0000000000..edcf37e811 --- /dev/null +++ b/docs/repl.md @@ -0,0 +1,66 @@ +# Getting a REPL or Interactive Shell + +rules_python provides a REPL to help with debugging and developing. The goal of +the REPL is to present an environment identical to what a {bzl:obj}`py_binary` creates +for your code. + +## Usage + +Start the REPL with the following command: +```console +$ bazel run @rules_python//python/bin:repl +Python 3.11.11 (main, Mar 17 2025, 21:02:09) [Clang 20.1.0 ] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> +``` + +Settings like `//python/config_settings:python_version` will influence the exact +behaviour. +```console +$ bazel run @rules_python//python/bin:repl --@rules_python//python/config_settings:python_version=3.13 +Python 3.13.2 (main, Mar 17 2025, 21:02:54) [Clang 20.1.0 ] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> +``` + +See [//python/config_settings](api/rules_python/python/config_settings/index) +and [Environment Variables](environment-variables) for more settings. + +## Importing Python targets + +The `//python/bin:repl_dep` command line flag gives the REPL access to a target +that provides the {bzl:obj}`PyInfo` provider. + +```console +$ bazel run @rules_python//python/bin:repl --@rules_python//python/bin:repl_dep=@rules_python//tools:wheelmaker +Python 3.11.11 (main, Mar 17 2025, 21:02:09) [Clang 20.1.0 ] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> import tools.wheelmaker +>>> +``` + +## Customizing the shell + +By default, the `//python/bin:repl` target will invoke the shell from the `code` +module. It's possible to switch to another shell by writing a custom "stub" and +pointing the target at the necessary dependencies. + +### IPython Example + +For an IPython shell, create a file as follows. + +```python +import IPython +IPython.start_ipython() +``` + +Assuming the file is called `ipython_stub.py` and the `pip.parse` hub's name is +`my_deps`, set this up in the .bazelrc file: +``` +# Allow the REPL stub to import ipython. In this case, @my_deps is the hub name +# of the pip.parse() call. +build --@rules_python//python/bin:repl_stub_dep=@my_deps//ipython + +# Point the REPL at the stub created above. +build --@rules_python//python/bin:repl_stub=//path/to:ipython_stub.py +``` diff --git a/docs/toolchains.md b/docs/toolchains.md index c8305e8f0d..121b398f7a 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -757,3 +757,13 @@ a fixed version. The `python` target does not provide access to any modules from `py_*` targets on its own. Please file a feature request if this is desired. ::: + +### Differences from `//python/bin:repl` + +The `//python/bin:python` target provides access to the underlying interpreter +without any hermeticity guarantees. + +The [`//python/bin:repl` target](repl) provides an environment indentical to +what `py_binary` provides. That means it handles things like the +[`PYTHONSAFEPATH`](https://docs.python.org/3/using/cmdline.html#envvar-PYTHONSAFEPATH) +environment variable automatically. The `//python/bin:python` target will not. diff --git a/python/bin/BUILD.bazel b/python/bin/BUILD.bazel index 57bee34378..30af7d1b9f 100644 --- a/python/bin/BUILD.bazel +++ b/python/bin/BUILD.bazel @@ -1,4 +1,5 @@ load("//python/private:interpreter.bzl", _interpreter_binary = "interpreter_binary") +load("//python/private:repl.bzl", "py_repl_binary") filegroup( name = "distribution", @@ -22,3 +23,35 @@ label_flag( name = "python_src", build_setting_default = "//python:none", ) + +py_repl_binary( + name = "repl", + stub = ":repl_stub", + visibility = ["//visibility:public"], + deps = [ + ":repl_dep", + ":repl_stub_dep", + ], +) + +# The user can replace this with their own stub. E.g. they can use this to +# import ipython instead of the default shell. +label_flag( + name = "repl_stub", + build_setting_default = "repl_stub.py", +) + +# The user can modify this flag to make an interpreter shell library available +# for the stub. E.g. if they switch the stub for an ipython-based one, then they +# can point this at their version of ipython. +label_flag( + name = "repl_stub_dep", + build_setting_default = "//python/private:empty", +) + +# The user can modify this flag to make arbitrary PyInfo targets available for +# import on the REPL. +label_flag( + name = "repl_dep", + build_setting_default = "//python/private:empty", +) diff --git a/python/bin/repl_stub.py b/python/bin/repl_stub.py new file mode 100644 index 0000000000..86452aa869 --- /dev/null +++ b/python/bin/repl_stub.py @@ -0,0 +1,29 @@ +"""Simulates the REPL that Python spawns when invoking the binary with no arguments. + +The code module is responsible for the default shell. + +The import and `ocde.interact()` call here his is equivalent to doing: + + $ python3 -m code + Python 3.11.2 (main, Mar 13 2023, 12:18:29) [GCC 12.2.0] on linux + Type "help", "copyright", "credits" or "license" for more information. + (InteractiveConsole) + >>> + +The logic for PYTHONSTARTUP is handled in python/private/repl_template.py. +""" + +import code +import sys + +if sys.stdin.isatty(): + # Use the default options. + exitmsg = None +else: + # On a non-interactive console, we want to suppress the >>> and the exit message. + exitmsg = "" + sys.ps1 = "" + sys.ps2 = "" + +# We set the banner to an empty string because the repl_template.py file already prints the banner. +code.interact(banner="", exitmsg=exitmsg) diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index 0b50ccf0b7..ce22421300 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -817,6 +817,10 @@ current_interpreter_executable( visibility = ["//visibility:public"], ) +py_library( + name = "empty", +) + sentinel( name = "sentinel", ) diff --git a/python/private/repl.bzl b/python/private/repl.bzl new file mode 100644 index 0000000000..838166a187 --- /dev/null +++ b/python/private/repl.bzl @@ -0,0 +1,84 @@ +"""Implementation of the rules to expose a REPL.""" + +load("//python:py_binary.bzl", _py_binary = "py_binary") + +def _generate_repl_main_impl(ctx): + stub_repo = ctx.attr.stub.label.repo_name or ctx.workspace_name + stub_path = "/".join([stub_repo, ctx.file.stub.short_path]) + + out = ctx.actions.declare_file(ctx.label.name + ".py") + + # Point the generated main file at the stub. + ctx.actions.expand_template( + template = ctx.file._template, + output = out, + substitutions = { + "%stub_path%": stub_path, + }, + ) + + return [DefaultInfo(files = depset([out]))] + +_generate_repl_main = rule( + implementation = _generate_repl_main_impl, + attrs = { + "stub": attr.label( + mandatory = True, + allow_single_file = True, + doc = ("The stub responsible for actually invoking the final shell. " + + "See the \"Customizing the REPL\" docs for details."), + ), + "_template": attr.label( + default = "//python/private:repl_template.py", + allow_single_file = True, + doc = "The template to use for generating `out`.", + ), + }, + doc = """\ +Generates a "main" script for a py_binary target that starts a Python REPL. + +The template is designed to take care of the majority of the logic. The user +customizes the exact shell that will be started via the stub. The stub is a +simple shell script that imports the desired shell and then executes it. + +The target's name is used for the output filename (with a .py extension). +""", +) + +def py_repl_binary(name, stub, deps = [], data = [], **kwargs): + """A py_binary target that executes a REPL when run. + + The stub is the script that ultimately decides which shell the REPL will run. + It can be as simple as this: + + import code + code.interact() + + Or it can load something like IPython instead. + + Args: + name: Name of the generated py_binary target. + stub: The script that invokes the shell. + deps: The dependencies of the py_binary. + data: The runtime dependencies of the py_binary. + **kwargs: Forwarded to the py_binary. + """ + _generate_repl_main( + name = "%s_py" % name, + stub = stub, + ) + + _py_binary( + name = name, + srcs = [ + ":%s_py" % name, + ], + main = "%s_py.py" % name, + data = data + [ + stub, + ], + deps = deps + [ + "//python/runfiles", + ], + **kwargs + ) diff --git a/python/private/repl_template.py b/python/private/repl_template.py new file mode 100644 index 0000000000..0e058b23ae --- /dev/null +++ b/python/private/repl_template.py @@ -0,0 +1,37 @@ +import os +import runpy +import sys +from pathlib import Path + +from python.runfiles import runfiles + +STUB_PATH = "%stub_path%" + + +def start_repl(): + if sys.stdin.isatty(): + # Print the banner similar to how python does it on startup when running interactively. + cprt = 'Type "help", "copyright", "credits" or "license" for more information.' + sys.stderr.write("Python %s on %s\n%s\n" % (sys.version, sys.platform, cprt)) + + # Simulate Python's behavior when a valid startup script is defined by the + # PYTHONSTARTUP variable. If this file path fails to load, print the error + # and revert to the default behavior. + # + # See upstream for more information: + # https://docs.python.org/3/using/cmdline.html#envvar-PYTHONSTARTUP + if startup_file := os.getenv("PYTHONSTARTUP"): + try: + source_code = Path(startup_file).read_text() + except Exception as error: + print(f"{type(error).__name__}: {error}") + else: + compiled_code = compile(source_code, filename=startup_file, mode="exec") + eval(compiled_code, {}) + + bazel_runfiles = runfiles.Create() + runpy.run_path(bazel_runfiles.Rlocation(STUB_PATH), run_name="__main__") + + +if __name__ == "__main__": + start_repl() diff --git a/tests/repl/BUILD.bazel b/tests/repl/BUILD.bazel new file mode 100644 index 0000000000..62c7377d53 --- /dev/null +++ b/tests/repl/BUILD.bazel @@ -0,0 +1,44 @@ +load("//python:py_library.bzl", "py_library") +load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") + +# A library that adds a special import path only when this is specified as a +# dependency. This makes it easy for a dependency to have this import path +# available without the top-level target being able to import the module. +py_library( + name = "helper/test_module", + srcs = [ + "helper/test_module.py", + ], + imports = [ + "helper", + ], +) + +py_reconfig_test( + name = "repl_without_dep_test", + srcs = ["repl_test.py"], + data = [ + "//python/bin:repl", + ], + env = { + # The helper/test_module should _not_ be importable for this test. + "EXPECT_TEST_MODULE_IMPORTABLE": "0", + }, + main = "repl_test.py", + python_version = "3.12", +) + +py_reconfig_test( + name = "repl_with_dep_test", + srcs = ["repl_test.py"], + data = [ + "//python/bin:repl", + ], + env = { + # The helper/test_module _should_ be importable for this test. + "EXPECT_TEST_MODULE_IMPORTABLE": "1", + }, + main = "repl_test.py", + python_version = "3.12", + repl_dep = ":helper/test_module", +) diff --git a/tests/repl/helper/test_module.py b/tests/repl/helper/test_module.py new file mode 100644 index 0000000000..0c4a309b01 --- /dev/null +++ b/tests/repl/helper/test_module.py @@ -0,0 +1,5 @@ +"""This is a file purely intended for validating //python/bin:repl.""" + + +def print_hello(): + print("Hello World") diff --git a/tests/repl/repl_test.py b/tests/repl/repl_test.py new file mode 100644 index 0000000000..51ca951110 --- /dev/null +++ b/tests/repl/repl_test.py @@ -0,0 +1,74 @@ +import os +import subprocess +import sys +import unittest +from typing import Iterable + +from python import runfiles + +rfiles = runfiles.Create() + +# Signals the tests below whether we should be expecting the import of +# helpers/test_module.py on the REPL to work or not. +EXPECT_TEST_MODULE_IMPORTABLE = os.environ["EXPECT_TEST_MODULE_IMPORTABLE"] == "1" + + +class ReplTest(unittest.TestCase): + def setUp(self): + self.repl = rfiles.Rlocation("rules_python/python/bin/repl") + assert self.repl + + def run_code_in_repl(self, lines: Iterable[str]) -> str: + """Runs the lines of code in the REPL and returns the text output.""" + return subprocess.check_output( + [self.repl], + text=True, + stderr=subprocess.STDOUT, + input="\n".join(lines), + ).strip() + + def test_repl_version(self): + """Validates that we can successfully execute arbitrary code on the REPL.""" + + result = self.run_code_in_repl( + [ + "import sys", + "v = sys.version_info", + "print(f'version: {v.major}.{v.minor}')", + ] + ) + self.assertIn("version: 3.12", result) + + def test_cannot_import_test_module_directly(self): + """Validates that we cannot import helper/test_module.py since it's not a direct dep.""" + with self.assertRaises(ModuleNotFoundError): + import test_module + + @unittest.skipIf( + not EXPECT_TEST_MODULE_IMPORTABLE, "test only works without repl_dep set" + ) + def test_import_test_module_success(self): + """Validates that we can import helper/test_module.py when repl_dep is set.""" + result = self.run_code_in_repl( + [ + "import test_module", + "test_module.print_hello()", + ] + ) + self.assertIn("Hello World", result) + + @unittest.skipIf( + EXPECT_TEST_MODULE_IMPORTABLE, "test only works without repl_dep set" + ) + def test_import_test_module_failure(self): + """Validates that we cannot import helper/test_module.py when repl_dep isn't set.""" + result = self.run_code_in_repl( + [ + "import test_module", + ] + ) + self.assertIn("ModuleNotFoundError: No module named 'test_module'", result) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/support/sh_py_run_test.bzl b/tests/support/sh_py_run_test.bzl index 9c8134ff40..f6ebc506cc 100644 --- a/tests/support/sh_py_run_test.bzl +++ b/tests/support/sh_py_run_test.bzl @@ -38,6 +38,8 @@ def _perform_transition_impl(input_settings, attr, base_impl): settings["//command_line_option:extra_toolchains"] = attr.extra_toolchains if attr.python_src: settings["//python/bin:python_src"] = attr.python_src + if attr.repl_dep: + settings["//python/bin:repl_dep"] = attr.repl_dep if attr.venvs_use_declare_symlink: settings["//python/config_settings:venvs_use_declare_symlink"] = attr.venvs_use_declare_symlink if attr.venvs_site_packages: @@ -47,6 +49,7 @@ def _perform_transition_impl(input_settings, attr, base_impl): _RECONFIG_INPUTS = [ "//python/config_settings:bootstrap_impl", "//python/bin:python_src", + "//python/bin:repl_dep", "//command_line_option:extra_toolchains", "//python/config_settings:venvs_use_declare_symlink", "//python/config_settings:venvs_site_packages", @@ -70,6 +73,7 @@ toolchain. """, ), "python_src": attrb.Label(), + "repl_dep": attrb.Label(), "venvs_site_packages": attrb.String(), "venvs_use_declare_symlink": attrb.String(), } From ce50f6a05c85cb93fd9de6f2863d6131fb7609c4 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 04:54:07 -0700 Subject: [PATCH 083/156] cleanup: remove unused sanitize_platform_name function (#2887) The sanitize_platform_name function is unused, so remove it. --- python/private/toolchains_repo.bzl | 3 --- 1 file changed, 3 deletions(-) diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl index d0814b66d5..7557c9f7d0 100644 --- a/python/private/toolchains_repo.bzl +++ b/python/private/toolchains_repo.bzl @@ -404,9 +404,6 @@ multi_toolchain_aliases = repository_rule( }, ) -def sanitize_platform_name(platform): - return platform.replace("-", "_") - def _get_host_platform(*, rctx, logger, python_version, os_name, cpu_name, platforms): """Gets the host platform. From 9ad9ce5ce0d5b2cd7fbde1d3c5b2a241056d0bbf Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 04:55:39 -0700 Subject: [PATCH 084/156] refactor: move inline code strings to top-level constants (#2886) This moves all the inline triple-quote strings of generated code to be in top-level constants. Because they're so large and dedented, they look like regular code. This makes it hard to visually parse. These large inline strings of code also confuse my editor's syntax highlighting (it does local, partial, parsing to highlight tokens, which gets thrown off by the seemingly valid looking code). --- python/private/toolchains_repo.bzl | 277 +++++++++++++++-------------- 1 file changed, 147 insertions(+), 130 deletions(-) diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl index 7557c9f7d0..d00f5ae34d 100644 --- a/python/private/toolchains_repo.bzl +++ b/python/private/toolchains_repo.bzl @@ -43,6 +43,144 @@ py_toolchain_suite( ) """.lstrip() +_WORKSPACE_TOOLCHAINS_BUILD_TEMPLATE = """ +# Generated by python/private/toolchains_repo.bzl +# +# These can be registered in the workspace file or passed to --extra_toolchains +# flag. By default all these toolchains are registered by the +# python_register_toolchains macro so you don't normally need to interact with +# these targets. + +load("@@{rules_python}//python/private:py_toolchain_suite.bzl", "py_toolchain_suite") + +""".lstrip() + +_TOOLCHAIN_ALIASES_BUILD_TEMPLATE = """ +# Generated by python/private/toolchains_repo.bzl +load("@rules_python//python/private:toolchain_aliases.bzl", "toolchain_aliases") + +package(default_visibility = ["//visibility:public"]) + +exports_files(["defs.bzl"]) + +PLATFORMS = [ +{loaded_platforms} +] +toolchain_aliases( + name = "{py_repository}", + platforms = PLATFORMS, +) +""".lstrip() + +_TOOLCHAIN_ALIASES_DEFS_TEMPLATE = """ +# Generated by python/private/toolchains_repo.bzl + +load("@@{rules_python}//python:pip.bzl", _compile_pip_requirements = "compile_pip_requirements") +load("@@{rules_python}//python/private:deprecation.bzl", "with_deprecation") +load("@@{rules_python}//python/private:text_util.bzl", "render") +load("@@{rules_python}//python:py_binary.bzl", _py_binary = "py_binary") +load("@@{rules_python}//python:py_test.bzl", _py_test = "py_test") +load( + "@@{rules_python}//python/entry_points:py_console_script_binary.bzl", + _py_console_script_binary = "py_console_script_binary", +) + +def _with_deprecation(kwargs, *, name): + kwargs["python_version"] = "{python_version}" + return with_deprecation.symbol( + kwargs, + symbol_name = name, + old_load = "@{name}//:defs.bzl", + new_load = "@rules_python//python:{{}}.bzl".format(name), + snippet = render.call(name, **{{k: repr(v) for k,v in kwargs.items()}}) + ) + +def py_binary(**kwargs): + return _py_binary(**_with_deprecation(kwargs, name = "py_binary")) + +def py_console_script_binary(**kwargs): + return _py_console_script_binary(**_with_deprecation(kwargs, name = "py_console_script_binary")) + +def py_test(**kwargs): + return _py_test(**_with_deprecation(kwargs, name = "py_test")) + +def compile_pip_requirements(**kwargs): + return _compile_pip_requirements(**_with_deprecation(kwargs, name = "compile_pip_requirements")) +""".lstrip() + +_HOST_TOOLCHAIN_BUILD_CONTENT = """ +# Generated by python/private/toolchains_repo.bzl + +exports_files(["python"], visibility = ["//visibility:public"]) +""".lstrip() + +_HOST_PYTHON_TESTER_TEMPLATE = """ +from pathlib import Path +import sys + +python = Path(sys.executable) +want_python = str(Path("{python}").resolve()) +got_python = str(Path(sys.executable).resolve()) + +assert want_python == got_python, \ + "Expected to use a different interpreter:\\nwant: '{{}}'\\n got: '{{}}'".format( + want_python, + got_python, + ) +""".lstrip() + +_MULTI_TOOLCHAIN_ALIASES_DEFS_TEMPLATE = """ +# Generated by python/private/toolchains_repo.bzl + +load("@@{rules_python}//python:pip.bzl", _compile_pip_requirements = "compile_pip_requirements") +load("@@{rules_python}//python/private:deprecation.bzl", "with_deprecation") +load("@@{rules_python}//python/private:text_util.bzl", "render") +load("@@{rules_python}//python:py_binary.bzl", _py_binary = "py_binary") +load("@@{rules_python}//python:py_test.bzl", _py_test = "py_test") +load( + "@@{rules_python}//python/entry_points:py_console_script_binary.bzl", + _py_console_script_binary = "py_console_script_binary", +) + +def _with_deprecation(kwargs, *, name): + kwargs["python_version"] = "{python_version}" + return with_deprecation.symbol( + kwargs, + symbol_name = name, + old_load = "@{name}//{python_version}:defs.bzl", + new_load = "@rules_python//python:{{}}.bzl".format(name), + snippet = render.call(name, **{{k: repr(v) for k,v in kwargs.items()}}) + ) + +def py_binary(**kwargs): + return _py_binary(**_with_deprecation(kwargs, name = "py_binary")) + +def py_console_script_binary(**kwargs): + return _py_console_script_binary(**_with_deprecation(kwargs, name = "py_console_script_binary")) + +def py_test(**kwargs): + return _py_test(**_with_deprecation(kwargs, name = "py_test")) + +def compile_pip_requirements(**kwargs): + return _compile_pip_requirements(**_with_deprecation(kwargs, name = "compile_pip_requirements")) +""".lstrip() + +_MULTI_TOOLCHAIN_ALIASES_PIP_TEMPLATE = """ +# Generated by python/private/toolchains_repo.bzl + +load("@@{rules_python}//python:pip.bzl", "pip_parse", _multi_pip_parse = "multi_pip_parse") + +def multi_pip_parse(name, requirements_lock, **kwargs): + return _multi_pip_parse( + name = name, + python_versions = {python_versions}, + requirements_lock = requirements_lock, + minor_mapping = {minor_mapping}, + **kwargs + ) + +""".lstrip() + def python_toolchain_build_file_content( prefix, python_version, @@ -101,17 +239,7 @@ def toolchain_suite_content( ) def _toolchains_repo_impl(rctx): - build_content = """\ -# Generated by python/private/toolchains_repo.bzl -# -# These can be registered in the workspace file or passed to --extra_toolchains -# flag. By default all these toolchains are registered by the -# python_register_toolchains macro so you don't normally need to interact with -# these targets. - -load("@@{rules_python}//python/private:py_toolchain_suite.bzl", "py_toolchain_suite") - -""".format( + build_content = _WORKSPACE_TOOLCHAINS_BUILD_TEMPLATE.format( rules_python = rctx.attr._rules_python_workspace.repo_name, ) @@ -144,22 +272,7 @@ toolchains_repo = repository_rule( def _toolchain_aliases_impl(rctx): # Base BUILD file for this repository. - build_contents = """\ -# Generated by python/private/toolchains_repo.bzl -load("@rules_python//python/private:toolchain_aliases.bzl", "toolchain_aliases") - -package(default_visibility = ["//visibility:public"]) - -exports_files(["defs.bzl"]) - -PLATFORMS = [ -{loaded_platforms} -] -toolchain_aliases( - name = "{py_repository}", - platforms = PLATFORMS, -) -""".format( + build_contents = _TOOLCHAIN_ALIASES_BUILD_TEMPLATE.format( py_repository = rctx.attr.user_repository_name, loaded_platforms = "\n".join([" \"{}\",".format(p) for p in rctx.attr.platforms]), ) @@ -167,41 +280,7 @@ toolchain_aliases( # Expose a Starlark file so rules can know what host platform we used and where to find an interpreter # when using repository_ctx.path, which doesn't understand aliases. - rctx.file("defs.bzl", content = """\ -# Generated by python/private/toolchains_repo.bzl - -load("@@{rules_python}//python:pip.bzl", _compile_pip_requirements = "compile_pip_requirements") -load("@@{rules_python}//python/private:deprecation.bzl", "with_deprecation") -load("@@{rules_python}//python/private:text_util.bzl", "render") -load("@@{rules_python}//python:py_binary.bzl", _py_binary = "py_binary") -load("@@{rules_python}//python:py_test.bzl", _py_test = "py_test") -load( - "@@{rules_python}//python/entry_points:py_console_script_binary.bzl", - _py_console_script_binary = "py_console_script_binary", -) - -def _with_deprecation(kwargs, *, name): - kwargs["python_version"] = "{python_version}" - return with_deprecation.symbol( - kwargs, - symbol_name = name, - old_load = "@{name}//:defs.bzl", - new_load = "@rules_python//python:{{}}.bzl".format(name), - snippet = render.call(name, **{{k: repr(v) for k,v in kwargs.items()}}) - ) - -def py_binary(**kwargs): - return _py_binary(**_with_deprecation(kwargs, name = "py_binary")) - -def py_console_script_binary(**kwargs): - return _py_console_script_binary(**_with_deprecation(kwargs, name = "py_console_script_binary")) - -def py_test(**kwargs): - return _py_test(**_with_deprecation(kwargs, name = "py_test")) - -def compile_pip_requirements(**kwargs): - return _compile_pip_requirements(**_with_deprecation(kwargs, name = "compile_pip_requirements")) -""".format( + rctx.file("defs.bzl", content = _TOOLCHAIN_ALIASES_DEFS_TEMPLATE.format( name = rctx.attr.name, python_version = rctx.attr.python_version, rules_python = rctx.attr._rules_python_workspace.repo_name, @@ -229,11 +308,7 @@ actions.""", ) def _host_toolchain_impl(rctx): - rctx.file("BUILD.bazel", """\ -# Generated by python/private/toolchains_repo.bzl - -exports_files(["python"], visibility = ["//visibility:public"]) -""") + rctx.file("BUILD.bazel", _HOST_TOOLCHAIN_BUILD_CONTENT) os_name = repo_utils.get_platforms_os_name(rctx) host_platform = _get_host_platform( @@ -279,20 +354,10 @@ exports_files(["python"], visibility = ["//visibility:public"]) # Ensure that we can run the interpreter and check that we are not # using the host interpreter. - python_tester_contents = """\ -from pathlib import Path -import sys - -python = Path(sys.executable) -want_python = str(Path("{python}").resolve()) -got_python = str(Path(sys.executable).resolve()) - -assert want_python == got_python, \ - "Expected to use a different interpreter:\\nwant: '{{}}'\\n got: '{{}}'".format( - want_python, - got_python, + python_tester_contents = _HOST_PYTHON_TESTER_TEMPLATE.format( + repo = repo.strip("@"), + python = python_binary, ) -""".format(repo = repo.strip("@"), python = python_binary) python_tester = rctx.path("python_tester.py") rctx.file(python_tester, python_tester_contents) repo_utils.execute_checked( @@ -331,41 +396,7 @@ def _multi_toolchain_aliases_impl(rctx): for python_version, repository_name in rctx.attr.python_versions.items(): file = "{}/defs.bzl".format(python_version) - rctx.file(file, content = """\ -# Generated by python/private/toolchains_repo.bzl - -load("@@{rules_python}//python:pip.bzl", _compile_pip_requirements = "compile_pip_requirements") -load("@@{rules_python}//python/private:deprecation.bzl", "with_deprecation") -load("@@{rules_python}//python/private:text_util.bzl", "render") -load("@@{rules_python}//python:py_binary.bzl", _py_binary = "py_binary") -load("@@{rules_python}//python:py_test.bzl", _py_test = "py_test") -load( - "@@{rules_python}//python/entry_points:py_console_script_binary.bzl", - _py_console_script_binary = "py_console_script_binary", -) - -def _with_deprecation(kwargs, *, name): - kwargs["python_version"] = "{python_version}" - return with_deprecation.symbol( - kwargs, - symbol_name = name, - old_load = "@{name}//{python_version}:defs.bzl", - new_load = "@rules_python//python:{{}}.bzl".format(name), - snippet = render.call(name, **{{k: repr(v) for k,v in kwargs.items()}}) - ) - -def py_binary(**kwargs): - return _py_binary(**_with_deprecation(kwargs, name = "py_binary")) - -def py_console_script_binary(**kwargs): - return _py_console_script_binary(**_with_deprecation(kwargs, name = "py_console_script_binary")) - -def py_test(**kwargs): - return _py_test(**_with_deprecation(kwargs, name = "py_test")) - -def compile_pip_requirements(**kwargs): - return _compile_pip_requirements(**_with_deprecation(kwargs, name = "compile_pip_requirements")) -""".format( + rctx.file(file, content = _MULTI_TOOLCHAIN_ALIASES_DEFS_TEMPLATE.format( repository_name = repository_name, name = rctx.attr.name, python_version = python_version, @@ -373,21 +404,7 @@ def compile_pip_requirements(**kwargs): )) rctx.file("{}/BUILD.bazel".format(python_version), "") - pip_bzl = """\ -# Generated by python/private/toolchains_repo.bzl - -load("@@{rules_python}//python:pip.bzl", "pip_parse", _multi_pip_parse = "multi_pip_parse") - -def multi_pip_parse(name, requirements_lock, **kwargs): - return _multi_pip_parse( - name = name, - python_versions = {python_versions}, - requirements_lock = requirements_lock, - minor_mapping = {minor_mapping}, - **kwargs - ) - -""".format( + pip_bzl = _MULTI_TOOLCHAIN_ALIASES_PIP_TEMPLATE.format( python_versions = rctx.attr.python_versions.keys(), minor_mapping = render.indent(render.dict(rctx.attr.minor_mapping), indent = " " * 8).lstrip(), rules_python = rules_python, From 50d59e5e8ca5bc7fb50c4cec8b706938c003b778 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 04:57:24 -0700 Subject: [PATCH 085/156] dev: add .python-version file so pyenv isn't user/system specific (#2883) The `.python-version` file is read by pyenv to decide what Python version to use. This makes it easier to get started with the project with installing things like pre-comment since you don't have to figure out what version of Python you need. --- .python-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 0000000000..2c20ac9bea --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13.3 From d6af2b795043ce623dfefe80d34a35268b212e1c Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 06:39:36 -0700 Subject: [PATCH 086/156] refactor: have bzlmod pass platforms to python_register_toolchains (#2884) This is to facilitate eventually allowing overrides to add additional platforms. Instead of the PLATFORMS global being used, the python bzlmod extension passing the mapping directly to python_register_toolchains, then receives back the subset of platforms that had repos defined. That subset is then later used when (re)constructing the list of repo names for the toolchains. --- python/private/python.bzl | 54 +++++++++++++------ python/private/python_register_toolchains.bzl | 21 +++++--- tests/python/python_tests.bzl | 1 + 3 files changed, 53 insertions(+), 23 deletions(-) diff --git a/python/private/python.bzl b/python/private/python.bzl index c187904322..c87beefdcc 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -34,12 +34,13 @@ def parse_modules(*, module_ctx, _fail = fail): Returns: A struct with the following attributes: - * `toolchains`: The list of toolchains to register. The last - element is special and is treated as the default toolchain. - * `defaults`: The default `kwargs` passed to - {bzl:obj}`python_register_toolchains`. - * `debug_info`: {type}`None | dict` extra information to be passed - to the debug repo. + * `toolchains`: The list of toolchains to register. The last + element is special and is treated as the default toolchain. + * `config`: Various toolchain config, see `_get_toolchain_config`. + * `debug_info`: {type}`None | dict` extra information to be passed + to the debug repo. + * `platforms`: {type}`dict[str, platform_info]` of the base set of + platforms toolchains should be created for, if possible. """ if module_ctx.os.environ.get("RULES_PYTHON_BZLMOD_DEBUG", "0") == "1": debug_info = { @@ -285,11 +286,12 @@ def _python_impl(module_ctx): kwargs.update(py.config.kwargs.get(toolchain_info.python_version, {})) kwargs.update(py.config.kwargs.get(full_python_version, {})) kwargs.update(py.config.default) - loaded_platforms[full_python_version] = python_register_toolchains( + toolchain_registered_platforms = python_register_toolchains( name = toolchain_info.name, _internal_bzlmod_toolchain_call = True, **kwargs ) + loaded_platforms[full_python_version] = toolchain_registered_platforms # List of the base names ("python_3_10") for the toolchain repos base_toolchain_repo_names = [] @@ -332,20 +334,19 @@ def _python_impl(module_ctx): base_name = t.name base_toolchain_repo_names.append(base_name) fv = full_version(version = t.python_version, minor_mapping = py.config.minor_mapping) - for platform in loaded_platforms[fv]: - if platform not in PLATFORMS: - continue + platforms = loaded_platforms[fv] + for platform_name, platform_info in platforms.items(): key = str(len(toolchain_names)) - full_name = "{}_{}".format(base_name, platform) + full_name = "{}_{}".format(base_name, platform_name) toolchain_names.append(full_name) toolchain_repo_names[key] = full_name - toolchain_tcw_map[key] = PLATFORMS[platform].compatible_with + toolchain_tcw_map[key] = platform_info.compatible_with # The target_settings attribute may not be present for users # patching python/versions.bzl. - toolchain_ts_map[key] = getattr(PLATFORMS[platform], "target_settings", []) - toolchain_platform_keys[key] = platform + toolchain_ts_map[key] = getattr(platform_info, "target_settings", []) + toolchain_platform_keys[key] = platform_name toolchain_python_versions[key] = fv # The last toolchain is the default; it can't have version constraints @@ -483,9 +484,9 @@ def _process_single_version_overrides(*, tag, _fail = fail, default): return for platform in (tag.sha256 or []): - if platform not in PLATFORMS: + if platform not in default["platforms"]: _fail("The platform must be one of {allowed} but got '{got}'".format( - allowed = sorted(PLATFORMS), + allowed = sorted(default["platforms"]), got = platform, )) return @@ -602,6 +603,26 @@ def _override_defaults(*overrides, modules, _fail = fail, default): override.fn(tag = tag, _fail = _fail, default = default) def _get_toolchain_config(*, modules, _fail = fail): + """Computes the configs for toolchains. + + Args: + modules: The modules from module_ctx + _fail: Function to call for failing; only used for testing. + + Returns: + A struct with the following: + * `kwargs`: {type}`dict[str, dict[str, object]` custom kwargs to pass to + `python_register_toolchains`, keyed by python version. + The first key is either a Major.Minor or Major.Minor.Patch + string. + * `minor_mapping`: {type}`dict[str, str]` the mapping of Major.Minor + to Major.Minor.Patch. + * `default`: {type}`dict[str, object]` of kwargs passed along to + `python_register_toolchains`. These keys take final precedence. + * `register_all_versions`: {type}`bool` whether all known versions + should be registered. + """ + # Items that can be overridden available_versions = { version: { @@ -621,6 +642,7 @@ def _get_toolchain_config(*, modules, _fail = fail): } default = { "base_url": DEFAULT_RELEASE_BASE_URL, + "platforms": dict(PLATFORMS), # Copy so it's mutable. "tool_versions": available_versions, } diff --git a/python/private/python_register_toolchains.bzl b/python/private/python_register_toolchains.bzl index cd3e9cbed7..6a4c0c310f 100644 --- a/python/private/python_register_toolchains.bzl +++ b/python/private/python_register_toolchains.bzl @@ -41,6 +41,7 @@ def python_register_toolchains( register_coverage_tool = False, set_python_version_constraint = False, tool_versions = None, + platforms = PLATFORMS, minor_mapping = None, **kwargs): """Convenience macro for users which does typical setup. @@ -70,12 +71,18 @@ def python_register_toolchains( tool_versions: {type}`dict` contains a mapping of version with SHASUM and platform info. If not supplied, the defaults in python/versions.bzl will be used. + platforms: {type}`dict[str, platform_info]` platforms to create toolchain + repositories for. Note that only a subset is created, depending + on what's available in `tool_versions`. minor_mapping: {type}`dict[str, str]` contains a mapping from `X.Y` to `X.Y.Z` version. **kwargs: passed to each {obj}`python_repository` call. Returns: - On bzlmod this returns the loaded platform labels. Otherwise None. + On workspace, returns None. + + On bzlmod, returns a `dict[str, platform_info]`, which is the + subset of `platforms` that it created repositories for. """ bzlmod_toolchain_call = kwargs.pop("_internal_bzlmod_toolchain_call", False) if bzlmod_toolchain_call: @@ -104,13 +111,13 @@ def python_register_toolchains( )) register_coverage_tool = False - loaded_platforms = [] - for platform in PLATFORMS.keys(): + loaded_platforms = {} + for platform in platforms.keys(): sha256 = tool_versions[python_version]["sha256"].get(platform, None) if not sha256: continue - loaded_platforms.append(platform) + loaded_platforms[platform] = platforms[platform] (release_filename, urls, strip_prefix, patches, patch_strip) = get_release_info(platform, python_version, base_url, tool_versions) # allow passing in a tool version @@ -162,7 +169,7 @@ def python_register_toolchains( host_toolchain( name = name + "_host", - platforms = loaded_platforms, + platforms = loaded_platforms.keys(), python_version = python_version, ) @@ -170,7 +177,7 @@ def python_register_toolchains( name = name, python_version = python_version, user_repository_name = name, - platforms = loaded_platforms, + platforms = loaded_platforms.keys(), ) # in bzlmod we write out our own toolchain repos @@ -182,6 +189,6 @@ def python_register_toolchains( python_version = python_version, set_python_version_constraint = set_python_version_constraint, user_repository_name = name, - platforms = loaded_platforms, + platforms = loaded_platforms.keys(), ) return None diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl index 443174c966..19be1c478e 100644 --- a/tests/python/python_tests.bzl +++ b/tests/python/python_tests.bzl @@ -149,6 +149,7 @@ def _test_default(env): "base_url", "ignore_root_user_error", "tool_versions", + "platforms", ]) env.expect.that_bool(py.config.default["ignore_root_user_error"]).equals(True) env.expect.that_str(py.default_python_version).equals("3.11") From 60c1c8ec045ca62d4e9ee9e1e8f651833cf8d4da Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 13:49:23 -0700 Subject: [PATCH 087/156] sphinxdocs: close repo rule directives (#2892) When converting the protos for repo rules to markdown, their blocks weren't being properly closed. To fix, just add the closing colons. Also added a test of the text generation. --- sphinxdocs/private/proto_to_markdown.py | 4 ++- .../proto_to_markdown_test.py | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/sphinxdocs/private/proto_to_markdown.py b/sphinxdocs/private/proto_to_markdown.py index 9dac71d51c..58fb79393d 100644 --- a/sphinxdocs/private/proto_to_markdown.py +++ b/sphinxdocs/private/proto_to_markdown.py @@ -216,7 +216,9 @@ def _render_repository_rule(self, repo_rule: stardoc_output_pb2.RepositoryRuleIn self._render_attributes(repo_rule.attribute) if repo_rule.environ: self._write(":envvars: ", ", ".join(sorted(repo_rule.environ))) - self._write("\n") + self._write("\n\n") + + self._write("::::::\n") def _render_rule(self, rule: stardoc_output_pb2.RuleInfo): rule_name = rule.rule_name diff --git a/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py b/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py index 9d15b830e3..da6edb21d4 100644 --- a/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py +++ b/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py @@ -272,6 +272,41 @@ def test_render_module_extension(self): ::::: +:::::: +""" + self.assertIn(expected, actual) + + def test_render_repo_rule(self): + proto_text = """ +file: "@repo//pkg:foo.bzl" +repository_rule_info: { + rule_name: "repository_rule", + doc_string: "REPOSITORY_RULE_DOC_STRING" + attribute: { + name: "repository_rule_attribute_a", + doc_string: "REPOSITORY_RULE_ATTRIBUTE_A_DOC_STRING" + type: BOOLEAN + default_value: "True" + } + environ: "ENV_VAR_A" +} +""" + actual = self._render(proto_text) + expected = """ +::::::{bzl:repo-rule} repository_rule(repository_rule_attribute_a=True) + +REPOSITORY_RULE_DOC_STRING + +:attr repository_rule_attribute_a: + {bzl:default-value}`True` + {type}`bool` + REPOSITORY_RULE_ATTRIBUTE_A_DOC_STRING + :::{bzl:attr-info} Info + ::: + + +:envvars: ENV_VAR_A + :::::: """ self.assertIn(expected, actual) From d91e9b256f9ea797899ef45a221968f2382cf7f4 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 13:50:00 -0700 Subject: [PATCH 088/156] sphinxdocs: make xrefs to bzl:obj in inventories work (#2894) Apparently, the `object_type` dict controls what object types are recognized from inventory files. The bazel inventory of terms includes several bzl:obj entries for things that don't have a more appropriate type. This fixes xrefs for terms like RBE, config, and some others. --- sphinxdocs/src/sphinx_bzl/bzl.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sphinxdocs/src/sphinx_bzl/bzl.py b/sphinxdocs/src/sphinx_bzl/bzl.py index 90fb109614..169f998749 100644 --- a/sphinxdocs/src/sphinx_bzl/bzl.py +++ b/sphinxdocs/src/sphinx_bzl/bzl.py @@ -1463,6 +1463,8 @@ class _BzlDomain(domains.Domain): # :obj:. # NOTE: We also use these object types for categorizing things in the # generated index page. + # NOTE: The object type keys control what object types are recognized + # in inventory files. object_types = { "arg": domains.ObjType("arg", "arg", "obj"), # macro/function arg "aspect": domains.ObjType("aspect", "aspect", "obj"), @@ -1486,6 +1488,8 @@ class _BzlDomain(domains.Domain): # types are objects that have a constructor and methods/attrs "type": domains.ObjType("type", "type", "obj"), "typedef": domains.ObjType("typedef", "typedef", "type", "obj"), + # generic objs usually come from inventories + "obj": domains.ObjType("object", "obj") } # This controls: From 9cfdfd823d0196a0e33b7004208199f35a19bdd8 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 13:56:52 -0700 Subject: [PATCH 089/156] sphinxdocs: make xrefs to tag class attributes using attr role work (#2895) The role assigned to attributes within the tag class directive were being given the role `arg`, when they should be `attr`. This caused xrefs using the attr role to be unable to find them. To fix, set them to have the correct role, like the repo rule and regular rule directives do. Also add a test. --- sphinxdocs/src/sphinx_bzl/bzl.py | 4 ++-- sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py | 1 + sphinxdocs/tests/sphinx_stardoc/xrefs.md | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/sphinxdocs/src/sphinx_bzl/bzl.py b/sphinxdocs/src/sphinx_bzl/bzl.py index 169f998749..dc922056a6 100644 --- a/sphinxdocs/src/sphinx_bzl/bzl.py +++ b/sphinxdocs/src/sphinx_bzl/bzl.py @@ -1156,10 +1156,10 @@ class _BzlTagClass(_BzlCallable): doc_field_types = [ _BzlGroupedField( - "arg", + "attr", label=_("Attributes"), names=["attr"], - rolename="arg", + rolename="attr", can_collapse=False, ), ] diff --git a/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py b/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py index 6d65c920e1..565c5ef68e 100644 --- a/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py +++ b/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py @@ -63,6 +63,7 @@ def _doc_element(self, doc): ("full_repo_provider", "@testrepo//lang:provider.bzl%LangInfo", "provider.html#LangInfo"), ("full_repo_aspect", "@testrepo//lang:aspect.bzl%myaspect", "aspect.html#myaspect"), ("full_repo_target", "@testrepo//lang:relativetarget", "target.html#relativetarget"), + ("tag_class_attr_using_attr_role", "myext.mytag.ta1", "module_extension.html#myext.mytag.ta1"), # fmt: on ) def test_xrefs(self, text, href): diff --git a/sphinxdocs/tests/sphinx_stardoc/xrefs.md b/sphinxdocs/tests/sphinx_stardoc/xrefs.md index 83f6869a48..8ff3e75d43 100644 --- a/sphinxdocs/tests/sphinx_stardoc/xrefs.md +++ b/sphinxdocs/tests/sphinx_stardoc/xrefs.md @@ -41,3 +41,7 @@ Various tests of cross referencing support ## Any xref * {any}`LangInfo` + +## Tag class refs + +* tag class attribute using attr role: {attr}`myext.mytag.ta1` From dcf0511675a417203e6222d2ead84ce863152977 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 16:57:31 -0700 Subject: [PATCH 090/156] sphinxdocs: allow unqualified arg/attr name for xref (#2896) It's common to simply refer to an arg or attribute by its sole name, especially for ones that are unique. This fixes about ~20 broken xrefs in our docs. Also add test for this behavior. --- sphinxdocs/src/sphinx_bzl/bzl.py | 8 ++++++-- sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py | 2 ++ sphinxdocs/tests/sphinx_stardoc/xrefs.md | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/sphinxdocs/src/sphinx_bzl/bzl.py b/sphinxdocs/src/sphinx_bzl/bzl.py index dc922056a6..44d6cd9994 100644 --- a/sphinxdocs/src/sphinx_bzl/bzl.py +++ b/sphinxdocs/src/sphinx_bzl/bzl.py @@ -390,8 +390,12 @@ def _make_xrefs_for_arg_attr( descr=index_description, ), ), - # This allows referencing an arg as e.g `funcname.argname` - alt_names=[anchor_id], + alt_names=[ + # This allows referencing an arg as e.g `funcname.argname` + anchor_id, + # This allows referencing an arg as simply `argname` + arg_name + ], ) # Two changes to how arg xrefs are created: diff --git a/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py b/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py index 565c5ef68e..042d2bb533 100644 --- a/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py +++ b/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py @@ -64,6 +64,8 @@ def _doc_element(self, doc): ("full_repo_aspect", "@testrepo//lang:aspect.bzl%myaspect", "aspect.html#myaspect"), ("full_repo_target", "@testrepo//lang:relativetarget", "target.html#relativetarget"), ("tag_class_attr_using_attr_role", "myext.mytag.ta1", "module_extension.html#myext.mytag.ta1"), + ("tag_class_attr_using_attr_role_just_attr_name", "ta1", "module_extension.html#myext.mytag.ta1"), + # fmt: on ) def test_xrefs(self, text, href): diff --git a/sphinxdocs/tests/sphinx_stardoc/xrefs.md b/sphinxdocs/tests/sphinx_stardoc/xrefs.md index 8ff3e75d43..85055e1f5c 100644 --- a/sphinxdocs/tests/sphinx_stardoc/xrefs.md +++ b/sphinxdocs/tests/sphinx_stardoc/xrefs.md @@ -45,3 +45,4 @@ Various tests of cross referencing support ## Tag class refs * tag class attribute using attr role: {attr}`myext.mytag.ta1` +* tag class attribute, just attr name, attr role: {attr}`ta1` From 5c20268eee7f45e0d8f783429a33d5274c2df4f3 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 17:46:03 -0700 Subject: [PATCH 091/156] docs: fix xref to toolchain docs from getting starting (#2899) The "toolchains" xref is ambiguous. Create a unique header for the toolchain configuration section and link to that name instead. --- docs/getting-started.md | 2 +- docs/toolchains.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 969716603c..60d5d5e0be 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -7,7 +7,7 @@ and the older way of using `WORKSPACE`. It assumes you have a `requirements.txt` file with your PyPI dependencies. For more details information about configuring `rules_python`, see: -* [Configuring the runtime](toolchains) +* [Configuring the runtime](configuring-toolchains) * [Configuring third party dependencies (pip/pypi)](pypi-dependencies) * [API docs](api/index) diff --git a/docs/toolchains.md b/docs/toolchains.md index 121b398f7a..a2a2b5b63e 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -1,6 +1,7 @@ :::{default-domain} bzl ::: +(configuring-toolchains)= # Configuring Python toolchains and runtimes This documents how to configure the Python toolchain and runtimes for different From 53fd252358d1b2184b0429b816cd9420de0e3651 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 18:14:39 -0700 Subject: [PATCH 092/156] sphinxdocs: allow files to be xref (#2897) This allows files to be specified as xrefs. Also adds a test for the functionality. --- sphinxdocs/src/sphinx_bzl/bzl.py | 36 +++++++++++++++++-- .../sphinx_stardoc/sphinx_output_test.py | 3 +- sphinxdocs/tests/sphinx_stardoc/xrefs.md | 5 +++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/sphinxdocs/src/sphinx_bzl/bzl.py b/sphinxdocs/src/sphinx_bzl/bzl.py index 44d6cd9994..7804e7f5e2 100644 --- a/sphinxdocs/src/sphinx_bzl/bzl.py +++ b/sphinxdocs/src/sphinx_bzl/bzl.py @@ -502,7 +502,39 @@ def run(self) -> list[docutils_nodes.Node]: self.env.ref_context["bzl:file"] = file_label self.env.ref_context["bzl:object_id_stack"] = [] self.env.ref_context["bzl:doc_id_stack"] = [] - return [] + + _, _, basename = file_label.partition(":") + index_description = f"File {label}" + absolute_label = repo + label + self.env.get_domain("bzl").add_object( + _ObjectEntry( + full_id=absolute_label, + display_name=absolute_label, + object_type="obj", + search_priority=1, + index_entry=domains.IndexEntry( + name=basename, + subtype=_INDEX_SUBTYPE_NORMAL, + docname=self.env.docname, + anchor="", + extra="", + qualifier="", + descr=index_description, + ), + ), + alt_names=[ + # Allow xref //foo:bar.bzl + file_label, + # Allow xref bar.bzl + basename, + ], + ) + index_node = addnodes.index( + entries=[ + _index_node_tuple("single", f"File; {label}", ""), + ] + ) + return [index_node] class _BzlAttrInfo(sphinx_docutils.SphinxDirective): @@ -1493,7 +1525,7 @@ class _BzlDomain(domains.Domain): "type": domains.ObjType("type", "type", "obj"), "typedef": domains.ObjType("typedef", "typedef", "type", "obj"), # generic objs usually come from inventories - "obj": domains.ObjType("object", "obj") + "obj": domains.ObjType("object", "obj"), } # This controls: diff --git a/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py b/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py index 042d2bb533..aa21369b40 100644 --- a/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py +++ b/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py @@ -65,7 +65,8 @@ def _doc_element(self, doc): ("full_repo_target", "@testrepo//lang:relativetarget", "target.html#relativetarget"), ("tag_class_attr_using_attr_role", "myext.mytag.ta1", "module_extension.html#myext.mytag.ta1"), ("tag_class_attr_using_attr_role_just_attr_name", "ta1", "module_extension.html#myext.mytag.ta1"), - + ("file_without_repo", "//lang:rule.bzl", "rule.html"), + ("file_with_repo", "@testrepo//lang:rule.bzl", "rule.html"), # fmt: on ) def test_xrefs(self, text, href): diff --git a/sphinxdocs/tests/sphinx_stardoc/xrefs.md b/sphinxdocs/tests/sphinx_stardoc/xrefs.md index 85055e1f5c..a32bb10339 100644 --- a/sphinxdocs/tests/sphinx_stardoc/xrefs.md +++ b/sphinxdocs/tests/sphinx_stardoc/xrefs.md @@ -46,3 +46,8 @@ Various tests of cross referencing support * tag class attribute using attr role: {attr}`myext.mytag.ta1` * tag class attribute, just attr name, attr role: {attr}`ta1` + +## File refs + +* without repo {obj}`//lang:rule.bzl` +* with repo {obj}`@testrepo//lang:rule.bzl` From 8f9ef76d358f2c08ba9558164d333b058915e44d Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 18:14:59 -0700 Subject: [PATCH 093/156] docs: move devguide to sphinx for more powerful markup (#2898) Having the devguide processed by Sphinx will let us use more powerful markup, which will help make it possible to create richer documentation. --- DEVELOPING.md => docs/devguide.md | 2 +- docs/index.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) rename DEVELOPING.md => docs/devguide.md (99%) diff --git a/DEVELOPING.md b/docs/devguide.md similarity index 99% rename from DEVELOPING.md rename to docs/devguide.md index 83026c1dbc..4d88b2817d 100644 --- a/DEVELOPING.md +++ b/docs/devguide.md @@ -1,4 +1,4 @@ -# For Developers +# Dev Guide This document covers tips and guidance for working on the rules_python code base. A primary audience for it is first time contributors. diff --git a/docs/index.md b/docs/index.md index 285b1cd66e..4983a6a029 100644 --- a/docs/index.md +++ b/docs/index.md @@ -104,6 +104,7 @@ gazelle REPL Extending Contributing +devguide support Changelog api/index From 2e96b3f08bb3fd3b626e036c404a99f9fed7218b Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 19:33:52 -0700 Subject: [PATCH 094/156] sphinxdocs: make bazel package xrefs work (#2903) This allows using xrefs like `//python/runtime_env_toolchains` and `runtime_env_toolchains`. --- sphinxdocs/src/sphinx_bzl/bzl.py | 22 ++++++++++++++++--- .../sphinx_stardoc/sphinx_output_test.py | 2 ++ sphinxdocs/tests/sphinx_stardoc/xrefs.md | 5 +++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/sphinxdocs/src/sphinx_bzl/bzl.py b/sphinxdocs/src/sphinx_bzl/bzl.py index 7804e7f5e2..4468ec660c 100644 --- a/sphinxdocs/src/sphinx_bzl/bzl.py +++ b/sphinxdocs/src/sphinx_bzl/bzl.py @@ -394,7 +394,7 @@ def _make_xrefs_for_arg_attr( # This allows referencing an arg as e.g `funcname.argname` anchor_id, # This allows referencing an arg as simply `argname` - arg_name + arg_name, ], ) @@ -503,7 +503,22 @@ def run(self) -> list[docutils_nodes.Node]: self.env.ref_context["bzl:object_id_stack"] = [] self.env.ref_context["bzl:doc_id_stack"] = [] - _, _, basename = file_label.partition(":") + package_label, _, basename = file_label.partition(":") + + # Transform //foo/bar:BUILD.bazel into "bar" + # This allows referencing "bar" as itself + extra_alt_names = [] + if basename in ("BUILD.bazel", "BUILD"): + # Allow xref //foo + extra_alt_names.append(package_label) + basename = os.path.basename(package_label) + # Handle //:BUILD.bazel + if not basename: + # There isn't a convention for referring to the root package + # besides `//:`, which is already the file_label. So just + # use some obvious value + basename = "__ROOT_BAZEL_PACKAGE__" + index_description = f"File {label}" absolute_label = repo + label self.env.get_domain("bzl").add_object( @@ -527,7 +542,8 @@ def run(self) -> list[docutils_nodes.Node]: file_label, # Allow xref bar.bzl basename, - ], + ] + + extra_alt_names, ) index_node = addnodes.index( entries=[ diff --git a/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py b/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py index aa21369b40..c78089ac14 100644 --- a/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py +++ b/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py @@ -67,6 +67,8 @@ def _doc_element(self, doc): ("tag_class_attr_using_attr_role_just_attr_name", "ta1", "module_extension.html#myext.mytag.ta1"), ("file_without_repo", "//lang:rule.bzl", "rule.html"), ("file_with_repo", "@testrepo//lang:rule.bzl", "rule.html"), + ("package_absolute", "//lang", "target.html"), + ("package_basename", "lang", "target.html"), # fmt: on ) def test_xrefs(self, text, href): diff --git a/sphinxdocs/tests/sphinx_stardoc/xrefs.md b/sphinxdocs/tests/sphinx_stardoc/xrefs.md index a32bb10339..bbd415ce19 100644 --- a/sphinxdocs/tests/sphinx_stardoc/xrefs.md +++ b/sphinxdocs/tests/sphinx_stardoc/xrefs.md @@ -51,3 +51,8 @@ Various tests of cross referencing support * without repo {obj}`//lang:rule.bzl` * with repo {obj}`@testrepo//lang:rule.bzl` + +## Package refs + +* absolute label {obj}`//lang` +* package basename {obj}`lang` From 459e1df4a92acfa82e7bfa08b2574dca1437913a Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 19:45:25 -0700 Subject: [PATCH 095/156] docs: fix most broken xrefs in changelog (#2902) The changelog has a variety of broken xrefs. This fixes most of them. --- CHANGELOG.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6ba65eb2f..a76241018d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,8 +66,8 @@ END_UNRELEASED_TEMPLATE * (rules) On Windows, {obj}`--bootstrap_impl=system_python` is forced. This allows setting `--bootstrap_impl=script` in bazelrc for mixed-platform environments. -* (rules) {obj}`pip_compile` now generates a `.test` target. The `_test` target is deprecated - and will be removed in the next major release. +* (rules) {obj}`compile_pip_requirements` now generates a `.test` target. The + `_test` target is deprecated and will be removed in the next major release. ([#2794](https://github.com/bazel-contrib/rules_python/issues/2794) * (py_wheel) py_wheel always creates zip64-capable wheel zips @@ -190,7 +190,7 @@ END_UNRELEASED_TEMPLATE packages through SimpleAPI unless they are pulled through direct URL references. Fixes [#2023](https://github.com/bazel-contrib/rules_python/issues/2023). In case you see issues with `rules_python` being too eager to fetch the SimpleAPI - metadata, you can use the newly added {attr}`pip.parse.experimental_skip_sources` + metadata, you can use the newly added {attr}`pip.parse.simpleapi_skip` to skip metadata fetching for those packages. * (uv) A {obj}`lock` rule that is the replacement for the {obj}`compile_pip_requirements`. This may still have rough corners @@ -251,7 +251,7 @@ END_UNRELEASED_TEMPLATE {#v1-3-0-added} ### Added -* (python) {attr}`python.defaults` has been added to allow users to +* (python) {obj}`python.defaults` has been added to allow users to set the default python version in the root module by reading the default version number from a file or an environment variable. * {obj}`//python/bin:python`: convenience target for directly running an @@ -271,7 +271,7 @@ END_UNRELEASED_TEMPLATE and py_library rules ([#1647](https://github.com/bazel-contrib/rules_python/issues/1647)) * (rules) Added env-var to allow additional interpreter args for stage1 bootstrap. - See {obj}`RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` environment variable. + See {any}`RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` environment variable. Only applicable for {obj}`--bootstrap_impl=script`. * (rules) Added {obj}`interpreter_args` attribute to `py_binary` and `py_test`, which allows pass arguments to the interpreter before the regular args. @@ -377,7 +377,7 @@ END_UNRELEASED_TEMPLATE values. Fixes [#2466](https://github.com/bazel-contrib/rules_python/issues/2466). * (py_proto_library) Fix import paths in Bazel 8. * (whl_library) Now the changes to the dependencies are correctly tracked when - PyPI packages used in {bzl:obj}`whl_library` during the `repository_rule` phase + PyPI packages used in `whl_library` during the repository rule phase change. Fixes [#2468](https://github.com/bazel-contrib/rules_python/issues/2468). + (gazelle) Gazelle no longer ignores `setup.py` files by default. To restore this behavior, apply the `# gazelle:python_ignore_files setup.py` directive. @@ -396,7 +396,7 @@ END_UNRELEASED_TEMPLATE * (pypi) Freethreaded packages are now fully supported in the {obj}`experimental_index_url` usage or the regular `pip.parse` usage. To select the free-threaded interpreter in the repo phase, please use - the documented [env](/environment-variables.html) variables. + the documented [env](environment-variables) variables. Fixes [#2386](https://github.com/bazel-contrib/rules_python/issues/2386). * (toolchains) Use the latest astrahl-sh toolchain release [20241206] for Python versions: * 3.9.21 @@ -490,7 +490,7 @@ Other changes: for the latest toolchain versions for each minor Python version. You can control the toolchain selection by using the {bzl:obj}`//python/config_settings:py_linux_libc` build flag. -* (providers) Added {obj}`py_runtime_info.site_init_template` and +* (providers) Added {obj}`PyRuntimeInfo.site_init_template` and {obj}`PyRuntimeInfo.site_init_template` for specifying the template to use to initialize the interpreter via venv startup hooks. * (runfiles) (Bazel 7.4+) Added support for spaces and newlines in runfiles paths @@ -688,8 +688,8 @@ Other changes: * (bzlmod) The default value for the {obj}`--python_version` flag will now be always set to the default python toolchain version value. * (bzlmod) correctly wire the {attr}`pip.parse.extra_pip_args` all the - way to {obj}`whl_library`. What is more we will pass the `extra_pip_args` to - {obj}`whl_library` for `sdist` distributions when using + way to `whl_library`. What is more we will pass the `extra_pip_args` to + `whl_library` for `sdist` distributions when using {attr}`pip.parse.experimental_index_url`. See [#2239](https://github.com/bazel-contrib/rules_python/issues/2239). * (whl_filegroup): Provide per default also the `RECORD` file @@ -737,8 +737,8 @@ Other changes: {#v0-37-0-removed} ### Removed -* (precompiling) {obj}`--precompile_add_to_runfiles` has been removed. -* (precompiling) {obj}`--pyc_collection` has been removed. The `pyc_collection` +* (precompiling) `--precompile_add_to_runfiles` has been removed. +* (precompiling) `--pyc_collection` has been removed. The `pyc_collection` attribute now bases its default on {obj}`--precompile`. * (precompiling) The {obj}`precompile=if_generated_source` value has been removed. * (precompiling) The {obj}`precompile_source_retention=omit_if_generated_source` value has been removed. @@ -790,7 +790,7 @@ Other changes: in extra_requires in py_wheel rule. * (rules) Prevent pytest from trying run the generated stage2 bootstrap .py file when using {obj}`--bootstrap_impl=script` -* (toolchain) The {bzl:obj}`gen_python_config_settings` has been fixed to include +* (toolchain) The `gen_python_config_settings` has been fixed to include the flag_values from the platform definitions. {#v0-36-0-added} @@ -1205,9 +1205,9 @@ Other changes: depend on legacy labels instead of the hub repo aliases and you use the `experimental_requirement_cycles`, now is a good time to migrate. -[python_default_visibility]: gazelle/README.md#directive-python_default_visibility +[python_default_visibility]: https://github.com/bazel-contrib/rules_python/tree/main/gazelle/README.md#directive-python_default_visibility [test_file_pattern_issue]: https://github.com/bazel-contrib/rules_python/issues/1816 -[test_file_pattern_docs]: gazelle/README.md#directive-python_test_file_pattern +[test_file_pattern_docs]: https://github.com/bazel-contrib/rules_python/tree/main/gazelle/README.md#directive-python_test_file_pattern [20240224]: https://github.com/indygreg/python-build-standalone/releases/tag/20240224. [20240415]: https://github.com/indygreg/python-build-standalone/releases/tag/20240415. From 8e6f73b026af31a4064da55b72fb17eb4d0809c5 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 19:46:17 -0700 Subject: [PATCH 096/156] tests: move py_reconfig rules to their own file (#2900) The py_reconfig code is pretty large, so move it to its own file. It's also be easier to find in its own file rather that part of something named after "shell testing". --- tests/bootstrap_impls/BUILD.bazel | 3 +- tests/bootstrap_impls/a/b/c/BUILD.bazel | 2 +- tests/interpreter/interpreter_tests.bzl | 2 +- tests/no_unsafe_paths/BUILD.bazel | 2 +- tests/packaging/BUILD.bazel | 2 +- tests/repl/BUILD.bazel | 2 +- tests/runtime_env_toolchain/BUILD.bazel | 2 +- tests/support/py_reconfig.bzl | 101 ++++++++++++++++++++++ tests/support/sh_py_run_test.bzl | 88 +------------------ tests/toolchains/defs.bzl | 2 +- tests/uv/lock/lock_tests.bzl | 2 +- tests/venv_site_packages_libs/BUILD.bazel | 2 +- 12 files changed, 116 insertions(+), 94 deletions(-) create mode 100644 tests/support/py_reconfig.bzl diff --git a/tests/bootstrap_impls/BUILD.bazel b/tests/bootstrap_impls/BUILD.bazel index 28a0d21fb7..b669da5669 100644 --- a/tests/bootstrap_impls/BUILD.bazel +++ b/tests/bootstrap_impls/BUILD.bazel @@ -13,7 +13,8 @@ load("@rules_shell//shell:sh_test.bzl", "sh_test") # 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("//tests/support:sh_py_run_test.bzl", "py_reconfig_binary", "py_reconfig_test", "sh_py_run_test") +load("//tests/support:py_reconfig.bzl", "py_reconfig_binary", "py_reconfig_test") +load("//tests/support:sh_py_run_test.bzl", "sh_py_run_test") load("//tests/support:support.bzl", "SUPPORTS_BOOTSTRAP_SCRIPT") load(":venv_relative_path_tests.bzl", "relative_path_test_suite") diff --git a/tests/bootstrap_impls/a/b/c/BUILD.bazel b/tests/bootstrap_impls/a/b/c/BUILD.bazel index 8ffcbcd479..1659ef25bc 100644 --- a/tests/bootstrap_impls/a/b/c/BUILD.bazel +++ b/tests/bootstrap_impls/a/b/c/BUILD.bazel @@ -1,5 +1,5 @@ load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility -load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") +load("//tests/support:py_reconfig.bzl", "py_reconfig_test") _SUPPORTS_BOOTSTRAP_SCRIPT = select({ "@platforms//os:windows": ["@platforms//:incompatible"], diff --git a/tests/interpreter/interpreter_tests.bzl b/tests/interpreter/interpreter_tests.bzl index ad94f43423..3c5882afa0 100644 --- a/tests/interpreter/interpreter_tests.bzl +++ b/tests/interpreter/interpreter_tests.bzl @@ -14,7 +14,7 @@ """This file contains helpers for testing the interpreter rule.""" -load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") +load("//tests/support:py_reconfig.bzl", "py_reconfig_test") # The versions of Python that we want to run the interpreter tests against. PYTHON_VERSIONS_TO_TEST = ( diff --git a/tests/no_unsafe_paths/BUILD.bazel b/tests/no_unsafe_paths/BUILD.bazel index f12d1c9a70..c9a681daa9 100644 --- a/tests/no_unsafe_paths/BUILD.bazel +++ b/tests/no_unsafe_paths/BUILD.bazel @@ -11,7 +11,7 @@ # 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("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") +load("//tests/support:py_reconfig.bzl", "py_reconfig_test") load("//tests/support:support.bzl", "SUPPORTS_BOOTSTRAP_SCRIPT") py_reconfig_test( diff --git a/tests/packaging/BUILD.bazel b/tests/packaging/BUILD.bazel index bb12269e3d..d88a593006 100644 --- a/tests/packaging/BUILD.bazel +++ b/tests/packaging/BUILD.bazel @@ -14,7 +14,7 @@ load("@bazel_skylib//rules:build_test.bzl", "build_test") load("@rules_pkg//pkg:tar.bzl", "pkg_tar") -load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") +load("//tests/support:py_reconfig.bzl", "py_reconfig_test") load("//tests/support:support.bzl", "SUPPORTS_BOOTSTRAP_SCRIPT") build_test( diff --git a/tests/repl/BUILD.bazel b/tests/repl/BUILD.bazel index 62c7377d53..b3986cc023 100644 --- a/tests/repl/BUILD.bazel +++ b/tests/repl/BUILD.bazel @@ -1,5 +1,5 @@ load("//python:py_library.bzl", "py_library") -load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") +load("//tests/support:py_reconfig.bzl", "py_reconfig_test") # A library that adds a special import path only when this is specified as a # dependency. This makes it easy for a dependency to have this import path diff --git a/tests/runtime_env_toolchain/BUILD.bazel b/tests/runtime_env_toolchain/BUILD.bazel index ad2bd4eeb5..2f82d204ff 100644 --- a/tests/runtime_env_toolchain/BUILD.bazel +++ b/tests/runtime_env_toolchain/BUILD.bazel @@ -13,7 +13,7 @@ # limitations under the License. load("@rules_python_runtime_env_tc_info//:info.bzl", "PYTHON_VERSION") -load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") +load("//tests/support:py_reconfig.bzl", "py_reconfig_test") load("//tests/support:support.bzl", "CC_TOOLCHAIN") load(":runtime_env_toolchain_tests.bzl", "runtime_env_toolchain_test_suite") diff --git a/tests/support/py_reconfig.bzl b/tests/support/py_reconfig.bzl new file mode 100644 index 0000000000..b33f679e77 --- /dev/null +++ b/tests/support/py_reconfig.bzl @@ -0,0 +1,101 @@ +# Copyright 2024 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. +"""Run a py_binary/py_test with altered config settings. + +This facilitates verify running binaries with different configuration settings +without the overhead of a bazel-in-bazel integration test. +""" + +load("//python/private:attr_builders.bzl", "attrb") # buildifier: disable=bzl-visibility +load("//python/private:py_binary_macro.bzl", "py_binary_macro") # buildifier: disable=bzl-visibility +load("//python/private:py_binary_rule.bzl", "create_py_binary_rule_builder") # buildifier: disable=bzl-visibility +load("//python/private:py_test_macro.bzl", "py_test_macro") # buildifier: disable=bzl-visibility +load("//python/private:py_test_rule.bzl", "create_py_test_rule_builder") # buildifier: disable=bzl-visibility +load("//tests/support:support.bzl", "VISIBLE_FOR_TESTING") + +def _perform_transition_impl(input_settings, attr, base_impl): + settings = {k: input_settings[k] for k in _RECONFIG_INHERITED_OUTPUTS if k in input_settings} + settings.update(base_impl(input_settings, attr)) + + settings[VISIBLE_FOR_TESTING] = True + settings["//command_line_option:build_python_zip"] = attr.build_python_zip + if attr.bootstrap_impl: + settings["//python/config_settings:bootstrap_impl"] = attr.bootstrap_impl + if attr.extra_toolchains: + settings["//command_line_option:extra_toolchains"] = attr.extra_toolchains + if attr.python_src: + settings["//python/bin:python_src"] = attr.python_src + if attr.repl_dep: + settings["//python/bin:repl_dep"] = attr.repl_dep + if attr.venvs_use_declare_symlink: + settings["//python/config_settings:venvs_use_declare_symlink"] = attr.venvs_use_declare_symlink + if attr.venvs_site_packages: + settings["//python/config_settings:venvs_site_packages"] = attr.venvs_site_packages + return settings + +_RECONFIG_INPUTS = [ + "//python/config_settings:bootstrap_impl", + "//python/bin:python_src", + "//python/bin:repl_dep", + "//command_line_option:extra_toolchains", + "//python/config_settings:venvs_use_declare_symlink", + "//python/config_settings:venvs_site_packages", +] +_RECONFIG_OUTPUTS = _RECONFIG_INPUTS + [ + "//command_line_option:build_python_zip", + VISIBLE_FOR_TESTING, +] +_RECONFIG_INHERITED_OUTPUTS = [v for v in _RECONFIG_OUTPUTS if v in _RECONFIG_INPUTS] + +_RECONFIG_ATTRS = { + "bootstrap_impl": attrb.String(), + "build_python_zip": attrb.String(default = "auto"), + "extra_toolchains": attrb.StringList( + doc = """ +Value for the --extra_toolchains flag. + +NOTE: You'll likely have to also specify //tests/support/cc_toolchains:all (or some CC toolchain) +to make the RBE presubmits happy, which disable auto-detection of a CC +toolchain. +""", + ), + "python_src": attrb.Label(), + "repl_dep": attrb.Label(), + "venvs_site_packages": attrb.String(), + "venvs_use_declare_symlink": attrb.String(), +} + +def _create_reconfig_rule(builder): + builder.attrs.update(_RECONFIG_ATTRS) + + base_cfg_impl = builder.cfg.implementation() + builder.cfg.set_implementation(lambda *args: _perform_transition_impl(base_impl = base_cfg_impl, *args)) + builder.cfg.update_inputs(_RECONFIG_INPUTS) + builder.cfg.update_outputs(_RECONFIG_OUTPUTS) + return builder.build() + +_py_reconfig_binary = _create_reconfig_rule(create_py_binary_rule_builder()) + +_py_reconfig_test = _create_reconfig_rule(create_py_test_rule_builder()) + +def py_reconfig_test(**kwargs): + """Create a py_test with customized build settings for testing. + + Args: + **kwargs: kwargs to pass along to _py_reconfig_test. + """ + py_test_macro(_py_reconfig_test, **kwargs) + +def py_reconfig_binary(**kwargs): + py_binary_macro(_py_reconfig_binary, **kwargs) diff --git a/tests/support/sh_py_run_test.bzl b/tests/support/sh_py_run_test.bzl index f6ebc506cc..1a61de9bd3 100644 --- a/tests/support/sh_py_run_test.bzl +++ b/tests/support/sh_py_run_test.bzl @@ -13,94 +13,14 @@ # limitations under the License. """Run a py_binary with altered config settings in an sh_test. -This facilitates verify running binaries with different configuration settings -without the overhead of a bazel-in-bazel integration test. +This facilitates verify running binaries with different outer environmental +settings and verifying their output without the overhead of a bazel-in-bazel +integration test. """ load("@rules_shell//shell:sh_test.bzl", "sh_test") -load("//python/private:attr_builders.bzl", "attrb") # buildifier: disable=bzl-visibility -load("//python/private:py_binary_macro.bzl", "py_binary_macro") # buildifier: disable=bzl-visibility -load("//python/private:py_binary_rule.bzl", "create_py_binary_rule_builder") # buildifier: disable=bzl-visibility -load("//python/private:py_test_macro.bzl", "py_test_macro") # buildifier: disable=bzl-visibility -load("//python/private:py_test_rule.bzl", "create_py_test_rule_builder") # buildifier: disable=bzl-visibility load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") # buildifier: disable=bzl-visibility -load("//tests/support:support.bzl", "VISIBLE_FOR_TESTING") - -def _perform_transition_impl(input_settings, attr, base_impl): - settings = {k: input_settings[k] for k in _RECONFIG_INHERITED_OUTPUTS if k in input_settings} - settings.update(base_impl(input_settings, attr)) - - settings[VISIBLE_FOR_TESTING] = True - settings["//command_line_option:build_python_zip"] = attr.build_python_zip - if attr.bootstrap_impl: - settings["//python/config_settings:bootstrap_impl"] = attr.bootstrap_impl - if attr.extra_toolchains: - settings["//command_line_option:extra_toolchains"] = attr.extra_toolchains - if attr.python_src: - settings["//python/bin:python_src"] = attr.python_src - if attr.repl_dep: - settings["//python/bin:repl_dep"] = attr.repl_dep - if attr.venvs_use_declare_symlink: - settings["//python/config_settings:venvs_use_declare_symlink"] = attr.venvs_use_declare_symlink - if attr.venvs_site_packages: - settings["//python/config_settings:venvs_site_packages"] = attr.venvs_site_packages - return settings - -_RECONFIG_INPUTS = [ - "//python/config_settings:bootstrap_impl", - "//python/bin:python_src", - "//python/bin:repl_dep", - "//command_line_option:extra_toolchains", - "//python/config_settings:venvs_use_declare_symlink", - "//python/config_settings:venvs_site_packages", -] -_RECONFIG_OUTPUTS = _RECONFIG_INPUTS + [ - "//command_line_option:build_python_zip", - VISIBLE_FOR_TESTING, -] -_RECONFIG_INHERITED_OUTPUTS = [v for v in _RECONFIG_OUTPUTS if v in _RECONFIG_INPUTS] - -_RECONFIG_ATTRS = { - "bootstrap_impl": attrb.String(), - "build_python_zip": attrb.String(default = "auto"), - "extra_toolchains": attrb.StringList( - doc = """ -Value for the --extra_toolchains flag. - -NOTE: You'll likely have to also specify //tests/support/cc_toolchains:all (or some CC toolchain) -to make the RBE presubmits happy, which disable auto-detection of a CC -toolchain. -""", - ), - "python_src": attrb.Label(), - "repl_dep": attrb.Label(), - "venvs_site_packages": attrb.String(), - "venvs_use_declare_symlink": attrb.String(), -} - -def _create_reconfig_rule(builder): - builder.attrs.update(_RECONFIG_ATTRS) - - base_cfg_impl = builder.cfg.implementation() - builder.cfg.set_implementation(lambda *args: _perform_transition_impl(base_impl = base_cfg_impl, *args)) - builder.cfg.update_inputs(_RECONFIG_INPUTS) - builder.cfg.update_outputs(_RECONFIG_OUTPUTS) - return builder.build() - -_py_reconfig_binary = _create_reconfig_rule(create_py_binary_rule_builder()) - -_py_reconfig_test = _create_reconfig_rule(create_py_test_rule_builder()) - -def py_reconfig_test(**kwargs): - """Create a py_test with customized build settings for testing. - - Args: - **kwargs: kwargs to pass along to _py_reconfig_test. - """ - py_test_macro(_py_reconfig_test, **kwargs) - -def py_reconfig_binary(**kwargs): - py_binary_macro(_py_reconfig_binary, **kwargs) +load(":py_reconfig.bzl", "py_reconfig_binary") def sh_py_run_test(*, name, sh_src, py_src, **kwargs): """Run a py_binary within a sh_test. diff --git a/tests/toolchains/defs.bzl b/tests/toolchains/defs.bzl index fbb70820c9..a883b0af33 100644 --- a/tests/toolchains/defs.bzl +++ b/tests/toolchains/defs.bzl @@ -15,7 +15,7 @@ "" load("//python:versions.bzl", "PLATFORMS", "TOOL_VERSIONS") -load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") +load("//tests/support:py_reconfig.bzl", "py_reconfig_test") def define_toolchain_tests(name): """Define the toolchain tests. diff --git a/tests/uv/lock/lock_tests.bzl b/tests/uv/lock/lock_tests.bzl index 35c7c19328..1eb5b1d903 100644 --- a/tests/uv/lock/lock_tests.bzl +++ b/tests/uv/lock/lock_tests.bzl @@ -16,7 +16,7 @@ load("@bazel_skylib//rules:native_binary.bzl", "native_test") load("//python/uv:lock.bzl", "lock") -load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") +load("//tests/support:py_reconfig.bzl", "py_reconfig_test") def lock_test_suite(name): """The test suite with various lock-related integration tests diff --git a/tests/venv_site_packages_libs/BUILD.bazel b/tests/venv_site_packages_libs/BUILD.bazel index 5d02708800..1f48331ff2 100644 --- a/tests/venv_site_packages_libs/BUILD.bazel +++ b/tests/venv_site_packages_libs/BUILD.bazel @@ -1,4 +1,4 @@ -load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") +load("//tests/support:py_reconfig.bzl", "py_reconfig_test") load("//tests/support:support.bzl", "SUPPORTS_BOOTSTRAP_SCRIPT") py_reconfig_test( From 945e46478e8b6af4cbe0ca9faa2d5852fe3f42f0 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 19:48:38 -0700 Subject: [PATCH 097/156] docs: fix link to py_reconfig and sh_py_run_test files (#2901) Our test files aren't part of the sphinx docs, so use the gh-path external link hook to link to the files directly on github. --- docs/devguide.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/devguide.md b/docs/devguide.md index 4d88b2817d..f233611cad 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -39,7 +39,7 @@ more important for tests to balance understandability and maintainability. ### sh_py_run_test -The [`sh_py_run_test`](tests/support/sh_py_run_test.bzl) rule is a helper to +The {gh-path}`sh_py_run_test Date: Sat, 17 May 2025 20:08:04 -0700 Subject: [PATCH 098/156] sphinxdocs: make Any and object types no-ops to avoid missing xrefs (#2905) The "Any" and "object" types are useful in expression starlark types, but aren't actually real things. Treat them like None and make them no-ops so they aren't treated like missing xrefs. --- sphinxdocs/src/sphinx_bzl/bzl.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sphinxdocs/src/sphinx_bzl/bzl.py b/sphinxdocs/src/sphinx_bzl/bzl.py index 4468ec660c..8303b4d2a5 100644 --- a/sphinxdocs/src/sphinx_bzl/bzl.py +++ b/sphinxdocs/src/sphinx_bzl/bzl.py @@ -1766,6 +1766,11 @@ def _on_missing_reference(app, env: environment.BuildEnvironment, node, contnode # There's no Bazel docs for None, so prevent missing xrefs warning if node["reftarget"] == "None": return contnode + + # Any and object are just conventions from Python, but useful for + # indicating what something is in Starlark, so treat them specially. + if node["reftarget"] in ("Any", "object"): + return contnode return None From 1ea9102ed87fc878e152d64df79e34b604c43572 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 17 May 2025 23:29:19 -0700 Subject: [PATCH 099/156] docs: correct some xrefs, add various missing Bazel external xrefs (#2907) Adds a variety of Bazel builtins to the external Bazel inventory. Along the way, fFixes a couple of bad xrefs in rule_builders. --- python/private/rule_builders.bzl | 4 ++-- sphinxdocs/inventories/bazel_inventory.txt | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/python/private/rule_builders.bzl b/python/private/rule_builders.bzl index 9b7c03136c..892f2ea343 100644 --- a/python/private/rule_builders.bzl +++ b/python/private/rule_builders.bzl @@ -253,7 +253,7 @@ def _ToolchainType_build(self): self: implicitly added Returns: - {type}`config_common.toolchain_type` + {type}`toolchain_type` """ kwargs = dict(self.kwargs) name = kwargs.pop("name") # Name must be positional @@ -673,7 +673,7 @@ def _AttrsDict_build(self): """Build an attribute dict for passing to `rule()`. Returns: - {type}`dict[str, attribute]` where the values are `attr.XXX` objects + {type}`dict[str, Attribute]` where the values are `attr.XXX` objects """ attrs = {} for k, v in self.map.items(): diff --git a/sphinxdocs/inventories/bazel_inventory.txt b/sphinxdocs/inventories/bazel_inventory.txt index 458126a849..bbd200ddb5 100644 --- a/sphinxdocs/inventories/bazel_inventory.txt +++ b/sphinxdocs/inventories/bazel_inventory.txt @@ -3,8 +3,10 @@ # Version: 7.3.0 # The remainder of this file is compressed using zlib Action bzl:type 1 rules/lib/Action - +Attribute bzl:type 1 rules/lib/builtins/Attribute - CcInfo bzl:provider 1 rules/lib/providers/CcInfo - CcInfo.linking_context bzl:provider-field 1 rules/lib/providers/CcInfo#linking_context - +DefaultInfo bzl:type 1 rules/lib/providers/DefaultInfo - ExecutionInfo bzl:type 1 rules/lib/providers/ExecutionInfo - File bzl:type 1 rules/lib/File - Label bzl:type 1 rules/lib/Label - @@ -38,6 +40,7 @@ config.string_list bzl:function 1 rules/lib/toplevel/config#string_list - config.target bzl:function 1 rules/lib/toplevel/config#target - config_common.FeatureFlagInfo bzl:type 1 rules/lib/toplevel/config_common#FeatureFlagInfo - config_common.toolchain_type bzl:function 1 rules/lib/toplevel/config_common#toolchain_type - +ctx bzl:type 1 rules/lib/builtins/repository_ctx - ctx.actions bzl:obj 1 rules/lib/builtins/ctx#actions - ctx.aspect_ids bzl:obj 1 rules/lib/builtins/ctx#aspect_ids - ctx.attr bzl:obj 1 rules/lib/builtins/ctx#attr - @@ -96,6 +99,7 @@ module_ctx.report_progress bzl:function 1 rules/lib/builtins/module_ctx#report_p module_ctx.root_module_has_non_dev_dependency bzl:function 1 rules/lib/builtins/module_ctx#root_module_has_non_dev_dependency - module_ctx.watch bzl:function 1 rules/lib/builtins/module_ctx#watch - module_ctx.which bzl:function 1 rules/lib/builtins/module_ctx#which - +native bzl:obj 1 rules/lib/toplevel/native - native.existing_rule bzl:function 1 rules/lib/toplevel/native#existing_rule - native.existing_rules bzl:function 1 rules/lib/toplevel/native#existing_rules - native.exports_files bzl:function 1 rules/lib/toplevel/native#exports_files - @@ -140,6 +144,8 @@ repository_os bzl:type 1 rules/lib/builtins/repository_os - repository_os.arch bzl:obj 1 rules/lib/builtins/repository_os#arch repository_os.environ bzl:obj 1 rules/lib/builtins/repository_os#environ repository_os.name bzl:obj 1 rules/lib/builtins/repository_os#name +rule bzl:type 1 rules/lib/builtins/rule - +rule bzl:function rules/lib/globals/bzl.html#rule - runfiles bzl:type 1 rules/lib/builtins/runfiles - runfiles.empty_filenames bzl:type 1 rules/lib/builtins/runfiles#empty_filenames - runfiles.files bzl:type 1 rules/lib/builtins/runfiles#files - @@ -156,6 +162,8 @@ testing.TestEnvironment bzl:function 1 rules/lib/toplevel/testing#TestEnvironmen testing.analysis_test bzl:rule 1 rules/lib/toplevel/testing#analysis_test - toolchain bzl:rule 1 reference/be/platforms-and-toolchains#toolchain - toolchain.exec_compatible_with bzl:rule 1 reference/be/platforms-and-toolchains#toolchain.exec_compatible_with - -toolchain.target_settings bzl:attr 1 reference/be/platforms-and-toolchains#toolchain.target_settings - toolchain.target_compatible_with bzl:attr 1 reference/be/platforms-and-toolchains#toolchain.target_compatible_with - +toolchain.target_settings bzl:attr 1 reference/be/platforms-and-toolchains#toolchain.target_settings - toolchain_type bzl:type 1 rules/lib/builtins/toolchain_type.html - +transition bzl:type 1 rules/lib/builtins/transition - +tuple bzl:type 1 rules/lib/core/tuple - From 6ffeff643ad75433c3c65aedf45705b8b6c564e0 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 18 May 2025 05:01:58 -0700 Subject: [PATCH 100/156] docs: ignore warnings about missing external py xrefs (#2904) Crossreferencing to py code outside our project isn't setup, so these are just a lot of warning spam. Disable them for now. Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- docs/conf.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index f58baf5183..96bbdb50ab 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -125,6 +125,12 @@ primary_domain = None # The default is 'py', which we don't make much use of nitpicky = True +nitpick_ignore_regex = [ + # External xrefs aren't setup: ignore missing xref warnings + # External xrefs to sphinx isn't setup: ignore missing xref warnings + ("py:.*", "(sphinx|docutils|ast|enum|collections|typing_extensions).*"), +] + # --- Intersphinx configuration intersphinx_mapping = { From 9de326ec8fc6994cf663bcdbdd61677073f25a46 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 18 May 2025 15:29:05 -0700 Subject: [PATCH 101/156] refactor: make bzlmod directly aware of created toolchain repo names (#2885) This makes python_register_toolchains return the repo names it created, which allows the bzlmod code to be directly aware of the repos that were created instead of having to rely on assuming the names via the platform keys. This is to facilitate python_register_toolchains creating a more arbitrary subset of platform-specific repos. Work towards https://github.com/bazel-contrib/rules_python/issues/2081 --- python/private/python.bzl | 79 +++++++++++-------- python/private/python_register_toolchains.bzl | 28 ++++--- 2 files changed, 64 insertions(+), 43 deletions(-) diff --git a/python/private/python.bzl b/python/private/python.bzl index c87beefdcc..1a1fcaab8a 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -267,11 +267,18 @@ def parse_modules(*, module_ctx, _fail = fail): def _python_impl(module_ctx): py = parse_modules(module_ctx = module_ctx) - # dict[str version, list[str] platforms]; where version is full - # python version string ("3.4.5"), and platforms are keys from - # the PLATFORMS global. - loaded_platforms = {} - for toolchain_info in py.toolchains: + # list of structs; see inline struct call within the loop below. + toolchain_impls = [] + + # list[str] of the base names of toolchain repos + base_toolchain_repo_names = [] + + # Create the underlying python_repository repos that contain the + # python runtimes and their toolchain implementation definitions. + for i, toolchain_info in enumerate(py.toolchains): + is_last = (i + 1) == len(py.toolchains) + base_toolchain_repo_names.append(toolchain_info.name) + # Ensure that we pass the full version here. full_python_version = full_version( version = toolchain_info.python_version, @@ -286,12 +293,28 @@ def _python_impl(module_ctx): kwargs.update(py.config.kwargs.get(toolchain_info.python_version, {})) kwargs.update(py.config.kwargs.get(full_python_version, {})) kwargs.update(py.config.default) - toolchain_registered_platforms = python_register_toolchains( + register_result = python_register_toolchains( name = toolchain_info.name, _internal_bzlmod_toolchain_call = True, **kwargs ) - loaded_platforms[full_python_version] = toolchain_registered_platforms + for repo_name, (platform_name, platform_info) in register_result.impl_repos.items(): + toolchain_impls.append(struct( + # str: The base name to use for the toolchain() target + name = repo_name, + # str: The repo name the toolchain() target points to. + impl_repo_name = repo_name, + # str: platform key in the passed-in platforms dict + platform_name = platform_name, + # struct: platform_info() struct + platform = platform_info, + # str: Major.Minor.Micro python version + full_python_version = full_python_version, + # bool: whether to implicitly add the python version constraint + # to the toolchain's target_settings. + # The last toolchain is the default; it can't have version constraints + set_python_version_constraint = is_last, + )) # List of the base names ("python_3_10") for the toolchain repos base_toolchain_repo_names = [] @@ -329,31 +352,23 @@ def _python_impl(module_ctx): # Split the toolchain info into separate objects so they can be passed onto # the repository rule. - for i, t in enumerate(py.toolchains): - is_last = (i + 1) == len(py.toolchains) - base_name = t.name - base_toolchain_repo_names.append(base_name) - fv = full_version(version = t.python_version, minor_mapping = py.config.minor_mapping) - platforms = loaded_platforms[fv] - for platform_name, platform_info in platforms.items(): - key = str(len(toolchain_names)) - - full_name = "{}_{}".format(base_name, platform_name) - toolchain_names.append(full_name) - toolchain_repo_names[key] = full_name - toolchain_tcw_map[key] = platform_info.compatible_with - - # The target_settings attribute may not be present for users - # patching python/versions.bzl. - toolchain_ts_map[key] = getattr(platform_info, "target_settings", []) - toolchain_platform_keys[key] = platform_name - toolchain_python_versions[key] = fv - - # The last toolchain is the default; it can't have version constraints - # Despite the implication of the arg name, the values are strs, not bools - toolchain_set_python_version_constraints[key] = ( - "True" if not is_last else "False" - ) + for entry in toolchain_impls: + key = str(len(toolchain_names)) + + toolchain_names.append(entry.name) + toolchain_repo_names[key] = entry.impl_repo_name + toolchain_tcw_map[key] = entry.platform.compatible_with + + # The target_settings attribute may not be present for users + # patching python/versions.bzl. + toolchain_ts_map[key] = getattr(entry.platform, "target_settings", []) + toolchain_platform_keys[key] = entry.platform_name + toolchain_python_versions[key] = entry.full_python_version + + # Repo rules can't accept dict[str, bool], so encode them as a string value. + toolchain_set_python_version_constraints[key] = ( + "True" if entry.set_python_version_constraint else "False" + ) hub_repo( name = "pythons_hub", diff --git a/python/private/python_register_toolchains.bzl b/python/private/python_register_toolchains.bzl index 6a4c0c310f..e16a96e763 100644 --- a/python/private/python_register_toolchains.bzl +++ b/python/private/python_register_toolchains.bzl @@ -111,13 +111,17 @@ def python_register_toolchains( )) register_coverage_tool = False - loaded_platforms = {} - for platform in platforms.keys(): + # list[str] of the platform names that were used + loaded_platforms = [] + + # dict[str repo name, tuple[str, platform_info]] + impl_repos = {} + for platform, platform_info in platforms.items(): sha256 = tool_versions[python_version]["sha256"].get(platform, None) if not sha256: continue - loaded_platforms[platform] = platforms[platform] + loaded_platforms.append(platform) (release_filename, urls, strip_prefix, patches, patch_strip) = get_release_info(platform, python_version, base_url, tool_versions) # allow passing in a tool version @@ -137,11 +141,10 @@ def python_register_toolchains( )], ) + impl_repo_name = "{}_{}".format(name, platform) + impl_repos[impl_repo_name] = (platform, platform_info) python_repository( - name = "{name}_{platform}".format( - name = name, - platform = platform, - ), + name = impl_repo_name, sha256 = sha256, patches = patches, patch_strip = patch_strip, @@ -169,7 +172,7 @@ def python_register_toolchains( host_toolchain( name = name + "_host", - platforms = loaded_platforms.keys(), + platforms = loaded_platforms, python_version = python_version, ) @@ -177,18 +180,21 @@ def python_register_toolchains( name = name, python_version = python_version, user_repository_name = name, - platforms = loaded_platforms.keys(), + platforms = loaded_platforms, ) # in bzlmod we write out our own toolchain repos if bzlmod_toolchain_call: - return loaded_platforms + return struct( + # dict[str name, tuple[str platform_name, platform_info]] + impl_repos = impl_repos, + ) toolchains_repo( name = toolchain_repo_name, python_version = python_version, set_python_version_constraint = set_python_version_constraint, user_repository_name = name, - platforms = loaded_platforms.keys(), + platforms = loaded_platforms, ) return None From acc8f8202832252aae398cc6aba0b11de62c5179 Mon Sep 17 00:00:00 2001 From: Philipp Schrader Date: Mon, 19 May 2025 01:38:03 -0700 Subject: [PATCH 102/156] fix: Allow PYTHONSTARTUP to define variables (#2911) With the current `//python/bin:repl` implementation, any variables defined in `PYTHONSTARTUP` are not actually available in the REPL itself. I accidentally omitted this in the first patch. This patch fixes the issue and adds appropriate tests. --- python/bin/repl_stub.py | 5 +++- python/private/repl_template.py | 12 ++++++-- tests/repl/repl_test.py | 50 ++++++++++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/python/bin/repl_stub.py b/python/bin/repl_stub.py index 86452aa869..1e21b26dc3 100644 --- a/python/bin/repl_stub.py +++ b/python/bin/repl_stub.py @@ -13,6 +13,9 @@ The logic for PYTHONSTARTUP is handled in python/private/repl_template.py. """ +# Capture the globals from PYTHONSTARTUP so we can pass them on to the console. +console_locals = globals().copy() + import code import sys @@ -26,4 +29,4 @@ sys.ps2 = "" # We set the banner to an empty string because the repl_template.py file already prints the banner. -code.interact(banner="", exitmsg=exitmsg) +code.interact(local=console_locals, banner="", exitmsg=exitmsg) diff --git a/python/private/repl_template.py b/python/private/repl_template.py index 0e058b23ae..37f4529fbe 100644 --- a/python/private/repl_template.py +++ b/python/private/repl_template.py @@ -14,6 +14,10 @@ def start_repl(): cprt = 'Type "help", "copyright", "credits" or "license" for more information.' sys.stderr.write("Python %s on %s\n%s\n" % (sys.version, sys.platform, cprt)) + # If there's a PYTHONSTARTUP script, we need to capture the new variables + # that it defines. + new_globals = {} + # Simulate Python's behavior when a valid startup script is defined by the # PYTHONSTARTUP variable. If this file path fails to load, print the error # and revert to the default behavior. @@ -27,10 +31,14 @@ def start_repl(): print(f"{type(error).__name__}: {error}") else: compiled_code = compile(source_code, filename=startup_file, mode="exec") - eval(compiled_code, {}) + eval(compiled_code, new_globals) bazel_runfiles = runfiles.Create() - runpy.run_path(bazel_runfiles.Rlocation(STUB_PATH), run_name="__main__") + runpy.run_path( + bazel_runfiles.Rlocation(STUB_PATH), + init_globals=new_globals, + run_name="__main__", + ) if __name__ == "__main__": diff --git a/tests/repl/repl_test.py b/tests/repl/repl_test.py index 51ca951110..37c9a37a0d 100644 --- a/tests/repl/repl_test.py +++ b/tests/repl/repl_test.py @@ -1,7 +1,9 @@ import os import subprocess import sys +import tempfile import unittest +from pathlib import Path from typing import Iterable from python import runfiles @@ -13,18 +15,26 @@ EXPECT_TEST_MODULE_IMPORTABLE = os.environ["EXPECT_TEST_MODULE_IMPORTABLE"] == "1" +# An arbitrary piece of code that sets some kind of variable. The variable needs to persist into the +# actual shell. +PYTHONSTARTUP_SETS_VAR = """\ +foo = 1234 +""" + + class ReplTest(unittest.TestCase): def setUp(self): self.repl = rfiles.Rlocation("rules_python/python/bin/repl") assert self.repl - def run_code_in_repl(self, lines: Iterable[str]) -> str: + def run_code_in_repl(self, lines: Iterable[str], *, env=None) -> str: """Runs the lines of code in the REPL and returns the text output.""" return subprocess.check_output( [self.repl], text=True, stderr=subprocess.STDOUT, input="\n".join(lines), + env=env, ).strip() def test_repl_version(self): @@ -69,6 +79,44 @@ def test_import_test_module_failure(self): ) self.assertIn("ModuleNotFoundError: No module named 'test_module'", result) + def test_pythonstartup_gets_executed(self): + """Validates that we can use the variables from PYTHONSTARTUP in the console itself.""" + with tempfile.TemporaryDirectory() as tempdir: + pythonstartup = Path(tempdir) / "pythonstartup.py" + pythonstartup.write_text(PYTHONSTARTUP_SETS_VAR) + + env = os.environ.copy() + env["PYTHONSTARTUP"] = str(pythonstartup) + + result = self.run_code_in_repl( + [ + "print(f'The value of foo is {foo}')", + ], + env=env, + ) + + self.assertIn("The value of foo is 1234", result) + + def test_pythonstartup_doesnt_leak(self): + """Validates that we don't accidentally leak code into the console. + + This test validates that a few of the variables we use in the template and stub are not + accessible in the REPL itself. + """ + with tempfile.TemporaryDirectory() as tempdir: + pythonstartup = Path(tempdir) / "pythonstartup.py" + pythonstartup.write_text(PYTHONSTARTUP_SETS_VAR) + + env = os.environ.copy() + env["PYTHONSTARTUP"] = str(pythonstartup) + + for var_name in ("exitmsg", "sys", "code", "bazel_runfiles", "STUB_PATH"): + with self.subTest(var_name=var_name): + result = self.run_code_in_repl([f"print({var_name})"], env=env) + self.assertIn( + f"NameError: name '{var_name}' is not defined", result + ) + if __name__ == "__main__": unittest.main() From e2e9a43853c7dfb97c408e39903a362dad2d0565 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 19 May 2025 12:24:09 -0700 Subject: [PATCH 103/156] docs: fix some more bad xrefs (#2910) Misc updates to fix doc warnings * Replace `collection` with `list`. Collections aren't a formal type. Just use list as a stand-in. * Create faux AttributeBuilder typedef so that AttributeBuilder references don't give a xref warning. * Change to object-lookup for `python` name (its a module extension, not rule) --- python/private/python_register_toolchains.bzl | 9 +++++---- python/private/rule_builders.bzl | 18 +++++++++++++++--- python/uv/private/uv.bzl | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/python/private/python_register_toolchains.bzl b/python/private/python_register_toolchains.bzl index e16a96e763..29da663844 100644 --- a/python/private/python_register_toolchains.bzl +++ b/python/private/python_register_toolchains.bzl @@ -48,7 +48,7 @@ def python_register_toolchains( With `bzlmod` enabled, this function is not needed since `rules_python` is handling everything. In order to override the default behaviour from the - root module one can see the docs for the {rule}`python` extension. + root module one can see the docs for the {obj}`python` extension. - Create a repository for each built-in platform like "python_3_8_linux_amd64" - this repository is lazily fetched when Python is needed for that platform. @@ -71,9 +71,10 @@ def python_register_toolchains( tool_versions: {type}`dict` contains a mapping of version with SHASUM and platform info. If not supplied, the defaults in python/versions.bzl will be used. - platforms: {type}`dict[str, platform_info]` platforms to create toolchain - repositories for. Note that only a subset is created, depending - on what's available in `tool_versions`. + platforms: {type}`dict[str, struct]` platforms to create toolchain + repositories for. Keys are platform names, and values are platform_info + structs. Note that only a subset is created, depending on what's + available in `tool_versions`. minor_mapping: {type}`dict[str, str]` contains a mapping from `X.Y` to `X.Y.Z` version. **kwargs: passed to each {obj}`python_repository` call. diff --git a/python/private/rule_builders.bzl b/python/private/rule_builders.bzl index 892f2ea343..360503b21b 100644 --- a/python/private/rule_builders.bzl +++ b/python/private/rule_builders.bzl @@ -192,7 +192,7 @@ ExecGroup = struct( ) def _ToolchainType_typedef(): - """Builder for {obj}`config_common.toolchain_type()` + """Builder for {obj}`config_common.toolchain_type` :::{include} /_includes/field_kwargs_doc.md ::: @@ -393,7 +393,7 @@ def _RuleCfg_update_inputs(self, *others): Args: self: implicitly added - *others: {type}`collection[Label]` collection of labels to add to + *others: {type}`list[Label]` collection of labels to add to inputs. Only values not already present are added. Note that a `Label`, not `str`, should be passed to ensure different apparent labels can be properly de-duplicated. @@ -405,7 +405,7 @@ def _RuleCfg_update_outputs(self, *others): Args: self: implicitly added - *others: {type}`collection[Label]` collection of labels to add to + *others: {type}`list[Label]` collection of labels to add to outputs. Only values not already present are added. Note that a `Label`, not `str`, should be passed to ensure different apparent labels can be properly de-duplicated. @@ -680,6 +680,18 @@ def _AttrsDict_build(self): attrs[k] = v.build() if _is_builder(v) else v return attrs +def _AttributeBuilder_typedef(): + """An abstract base typedef for builder for a Bazel {obj}`Attribute` + + Instances of this are a builder for a particular `Attribute` type, + e.g. `attr.label`, `attr.string`, etc. + """ + +# buildifier: disable=name-conventions +AttributeBuilder = struct( + TYPEDEF = _AttributeBuilder_typedef, +) + # buildifier: disable=name-conventions AttrsDict = struct( TYPEDEF = _AttrsDict_typedef, diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 55a05be032..09fb78322f 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -122,7 +122,7 @@ uv.configure( "urls": attr.string_list( doc = """\ The urls to download the binary from. If this is used, {attr}`base_url` and -{attr}`manifest_name` are ignored for the given version. +{attr}`manifest_filename` are ignored for the given version. ::::note If the `urls` are specified, they need to be specified for all of the platforms From 9abd323cdf59248c212032fc7173b9e595f877c9 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 19 May 2025 13:13:18 -0700 Subject: [PATCH 104/156] refactor: make bzlmod create host repos for toolchains (#2888) This moves the creation of the host_toolchain repos into the bzlmod phase. This is to facilitate future work to allow for when a a particular version doesn't provide a host-compatible variant Work towards https://github.com/bazel-contrib/rules_python/issues/2081 --- python/private/python.bzl | 17 ++++++++++++++++- python/private/python_register_toolchains.bzl | 14 +++++++------- python/private/toolchains_repo.bzl | 2 ++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/python/private/python.bzl b/python/private/python.bzl index 1a1fcaab8a..ea794ecf86 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -21,7 +21,7 @@ load(":full_version.bzl", "full_version") load(":python_register_toolchains.bzl", "python_register_toolchains") load(":pythons_hub.bzl", "hub_repo") load(":repo_utils.bzl", "repo_utils") -load(":toolchains_repo.bzl", "multi_toolchain_aliases") +load(":toolchains_repo.bzl", "host_toolchain", "multi_toolchain_aliases") load(":util.bzl", "IS_BAZEL_6_4_OR_HIGHER") load(":version.bzl", "version") @@ -298,6 +298,7 @@ def _python_impl(module_ctx): _internal_bzlmod_toolchain_call = True, **kwargs ) + host_compatible = [] for repo_name, (platform_name, platform_info) in register_result.impl_repos.items(): toolchain_impls.append(struct( # str: The base name to use for the toolchain() target @@ -315,6 +316,15 @@ def _python_impl(module_ctx): # The last toolchain is the default; it can't have version constraints set_python_version_constraint = is_last, )) + if _is_compatible_with_host(module_ctx, platform_info): + host_compatible.append(platform_name) + + host_toolchain( + name = toolchain_info.name + "_host", + # NOTE: Order matters. The first found to be compatible is (usually) used. + platforms = host_compatible, + python_version = full_python_version, + ) # List of the base names ("python_3_10") for the toolchain repos base_toolchain_repo_names = [] @@ -406,6 +416,11 @@ def _python_impl(module_ctx): else: return None +def _is_compatible_with_host(mctx, platform_info): + os_name = repo_utils.get_platforms_os_name(mctx) + cpu_name = repo_utils.get_platforms_cpu_name(mctx) + return platform_info.os_name == os_name and platform_info.arch == cpu_name + def _one_or_the_same(first, second, *, onerror = None): if not first: return second diff --git a/python/private/python_register_toolchains.bzl b/python/private/python_register_toolchains.bzl index 29da663844..e821bae5e7 100644 --- a/python/private/python_register_toolchains.bzl +++ b/python/private/python_register_toolchains.bzl @@ -171,12 +171,6 @@ def python_register_toolchains( platform = platform, )) - host_toolchain( - name = name + "_host", - platforms = loaded_platforms, - python_version = python_version, - ) - toolchain_aliases( name = name, python_version = python_version, @@ -184,13 +178,19 @@ def python_register_toolchains( platforms = loaded_platforms, ) - # in bzlmod we write out our own toolchain repos + # in bzlmod we write out our own toolchain repos and host repos if bzlmod_toolchain_call: return struct( # dict[str name, tuple[str platform_name, platform_info]] impl_repos = impl_repos, ) + host_toolchain( + name = name + "_host", + platforms = loaded_platforms, + python_version = python_version, + ) + toolchains_repo( name = toolchain_repo_name, python_version = python_version, diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl index d00f5ae34d..a4188a739a 100644 --- a/python/private/toolchains_repo.bzl +++ b/python/private/toolchains_repo.bzl @@ -375,6 +375,8 @@ def _host_toolchain_impl(rctx): if not rctx.delete(python_tester): fail("Failed to delete the python tester") +# NOTE: The term "toolchain" is a misnomer for this rule. This doesn't define +# a repo with toolchains or toolchain implementations. host_toolchain = repository_rule( _host_toolchain_impl, doc = """\ From 67c5cf0546d9d22b4ce3a36d6f3badebaafd2e10 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 20 May 2025 05:31:45 +0900 Subject: [PATCH 105/156] refactor: remove unused target_platforms hub_repository attr (#2912) The target_platforms attribute is unused. The attribute gets used, but the values it computes are never used. --- python/private/pypi/extension.bzl | 13 ------------- python/private/pypi/hub_repository.bzl | 4 ---- 2 files changed, 17 deletions(-) diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 3896f2940a..d3a15dfc44 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -275,12 +275,6 @@ def _create_whl_repos( }, extra_aliases = extra_aliases, whl_libraries = whl_libraries, - target_platforms = { - plat: None - for reqs in requirements_by_platform.values() - for req in reqs - for plat in req.target_platforms - }, ) def _whl_repos(*, requirement, whl_library_args, download_only, netrc, auth_patterns, multiple_requirements_for_whl = False, python_version, enable_pipstar = False): @@ -453,7 +447,6 @@ You cannot use both the additive_build_content and additive_build_content_file a hub_group_map = {} exposed_packages = {} extra_aliases = {} - target_platforms = {} whl_libraries = {} for mod in module_ctx.modules: @@ -536,7 +529,6 @@ You cannot use both the additive_build_content and additive_build_content_file a for whl_name, aliases in out.extra_aliases.items(): extra_aliases[hub_name].setdefault(whl_name, {}).update(aliases) exposed_packages.setdefault(hub_name, {}).update(out.exposed_packages) - target_platforms.setdefault(hub_name, {}).update(out.target_platforms) whl_libraries.update(out.whl_libraries) # TODO @aignas 2024-04-05: how do we support different requirement @@ -574,10 +566,6 @@ You cannot use both the additive_build_content and additive_build_content_file a } for hub_name, extra_whl_aliases in extra_aliases.items() }, - target_platforms = { - hub_name: sorted(p) - for hub_name, p in target_platforms.items() - }, whl_libraries = { k: dict(sorted(args.items())) for k, args in sorted(whl_libraries.items()) @@ -669,7 +657,6 @@ def _pip_impl(module_ctx): }, packages = mods.exposed_packages.get(hub_name, []), groups = mods.hub_group_map.get(hub_name), - target_platforms = mods.target_platforms.get(hub_name, []), ) if bazel_features.external_deps.extension_metadata_has_reproducible: diff --git a/python/private/pypi/hub_repository.bzl b/python/private/pypi/hub_repository.bzl index 0a1e772d05..0dbc6c29c2 100644 --- a/python/private/pypi/hub_repository.bzl +++ b/python/private/pypi/hub_repository.bzl @@ -87,10 +87,6 @@ The list of packages that will be exposed via all_*requirements macros. Defaults mandatory = True, doc = "The apparent name of the repo. This is needed because in bzlmod, the name attribute becomes the canonical name.", ), - "target_platforms": attr.string_list( - mandatory = True, - doc = "All of the target platforms for the hub repo", - ), "whl_map": attr.string_dict( mandatory = True, doc = """\ From c7efa25aabc0f74f008e683d7ce266f0f3f4ff38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 13:32:10 -0700 Subject: [PATCH 106/156] build(deps): bump setuptools from 65.6.3 to 78.1.1 in /examples/bzlmod (#2914) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [setuptools](https://github.com/pypa/setuptools) from 65.6.3 to 78.1.1.
Changelog

Sourced from setuptools's changelog.

v78.1.1

Bugfixes

  • More fully sanitized the filename in PackageIndex._download. (#4946)

v78.1.0

Features

  • Restore access to _get_vc_env with a warning. (#4874)

v78.0.2

Bugfixes

  • Postponed removals of deprecated dash-separated and uppercase fields in setup.cfg. All packages with deprecated configurations are advised to move before 2026. (#4911)

v78.0.1

Misc

v78.0.0

Bugfixes

  • Reverted distutils changes that broke the monkey patching of command classes. (#4902)

Deprecations and Removals

  • Setuptools no longer accepts options containing uppercase or dash characters in setup.cfg.

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=setuptools&package-manager=pip&previous-version=65.6.3&new-version=78.1.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/bazel-contrib/rules_python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/bzlmod/requirements_lock_3_9.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/bzlmod/requirements_lock_3_9.txt b/examples/bzlmod/requirements_lock_3_9.txt index c48f406451..8a6d41441a 100644 --- a/examples/bzlmod/requirements_lock_3_9.txt +++ b/examples/bzlmod/requirements_lock_3_9.txt @@ -262,9 +262,9 @@ s3cmd==2.1.0 \ --hash=sha256:49cd23d516b17974b22b611a95ce4d93fe326feaa07320bd1d234fed68cbccfa \ --hash=sha256:966b0a494a916fc3b4324de38f089c86c70ee90e8e1cae6d59102103a4c0cc03 # via -r examples/bzlmod/requirements.in -setuptools==65.6.3 \ - --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ - --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 +setuptools==78.1.1 \ + --hash=sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561 \ + --hash=sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d # via # babel # yamllint From a746b8fba6472afc935b6e018d5364cc33bad90b Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 19 May 2025 14:28:27 -0700 Subject: [PATCH 107/156] refactor: make bzlmod pass platform mapping to host repo creation (#2889) This makes bzlmod pass the platform metadata to the host_toolchain rule instead of the host toolchain rule using the fixed PLATFORMS global. This allows the bzlmod extension to modify the platforms that are available, where the fixed PLATFORM global can't be changed. Work towards https://github.com/bazel-contrib/rules_python/issues/2081 --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- python/private/python.bzl | 13 ++++++++++--- python/private/toolchains_repo.bzl | 23 ++++++++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/python/private/python.bzl b/python/private/python.bzl index ea794ecf86..0f3bbee4ac 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -298,7 +298,9 @@ def _python_impl(module_ctx): _internal_bzlmod_toolchain_call = True, **kwargs ) - host_compatible = [] + host_platforms = [] + host_os_names = {} + host_archs = {} for repo_name, (platform_name, platform_info) in register_result.impl_repos.items(): toolchain_impls.append(struct( # str: The base name to use for the toolchain() target @@ -317,12 +319,17 @@ def _python_impl(module_ctx): set_python_version_constraint = is_last, )) if _is_compatible_with_host(module_ctx, platform_info): - host_compatible.append(platform_name) + host_key = str(len(host_platforms)) + host_platforms.append(platform_name) + host_os_names[host_key] = platform_info.os_name + host_archs[host_key] = platform_info.arch host_toolchain( name = toolchain_info.name + "_host", # NOTE: Order matters. The first found to be compatible is (usually) used. - platforms = host_compatible, + platforms = host_platforms, + os_names = host_os_names, + arch_names = host_archs, python_version = full_python_version, ) diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl index a4188a739a..29ac694fd5 100644 --- a/python/private/toolchains_repo.bzl +++ b/python/private/toolchains_repo.bzl @@ -386,6 +386,16 @@ toolchain_aliases repo because referencing the `python` interpreter target from this repo causes an eager fetch of the toolchain for the host platform. """, attrs = { + "arch_names": attr.string_dict( + doc = """ +If set, overrides the platform metadata. Keyed by index in `platforms` +""", + ), + "os_names": attr.string_dict( + doc = """ +If set, overrides the platform metadata. Keyed by index in `platforms` +""", + ), "platforms": attr.string_list(mandatory = True), "python_version": attr.string(mandatory = True), "_rule_name": attr.string(default = "host_toolchain"), @@ -436,9 +446,20 @@ def _get_host_platform(*, rctx, logger, python_version, os_name, cpu_name, platf Returns: The host platform. """ + if rctx.attr.os_names: + platform_map = {} + for i, platform_name in enumerate(platforms): + key = str(i) + platform_map[platform_name] = struct( + os_name = rctx.attr.os_names[key], + arch = rctx.attr.arch_names[key], + ) + else: + platform_map = PLATFORMS + candidates = [] for platform in platforms: - meta = PLATFORMS[platform] + meta = platform_map[platform] if meta.os_name == os_name and meta.arch == cpu_name: candidates.append(platform) From f36d1205294c9a67a9ac969051ac75e47e27c689 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 19 May 2025 20:59:47 -0700 Subject: [PATCH 108/156] docs: fix xrefs in (#2917) More misc xrefs fixes in: * attr_builders * py_console_script_binary * pypi envvar ref --- python/private/attr_builders.bzl | 5 ++++- python/private/py_console_script_binary.bzl | 14 +++++++------- python/private/pypi/attrs.bzl | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/python/private/attr_builders.bzl b/python/private/attr_builders.bzl index 57fe476109..be9fa22138 100644 --- a/python/private/attr_builders.bzl +++ b/python/private/attr_builders.bzl @@ -1222,7 +1222,7 @@ def _StringList_typedef(): ::: :::{field} default - :type: Value[list[str] | configuration_field] + :type: list[str] | configuration_field ::: :::{function} doc() -> str @@ -1237,6 +1237,9 @@ def _StringList_typedef(): :::{function} set_allow_empty(v: bool) ::: + :::{function} set_default(v: list[str] | configuration_field) + ::: + :::{function} set_doc(v: str) ::: diff --git a/python/private/py_console_script_binary.bzl b/python/private/py_console_script_binary.bzl index 7347ebe16a..154fa3bf2f 100644 --- a/python/private/py_console_script_binary.bzl +++ b/python/private/py_console_script_binary.bzl @@ -56,18 +56,18 @@ def py_console_script_binary( """Generate a py_binary for a console_script entry_point. Args: - name: [`target-name`] The name of the resulting target. - pkg: {any}`simple label` the package for which to generate the script. - entry_points_txt: optional [`label`], the entry_points.txt file to parse + name: {type}`Name` The name of the resulting target. + pkg: {type}`Label` the package for which to generate the script. + entry_points_txt: {type}`label | None`, the entry_points.txt file to parse for available console_script values. It may be a single file, or a group of files, but must contain a file named `entry_points.txt`. If not specified, defaults to the `dist_info` target in the same package as the `pkg` Label. - script: [`str`], The console script name that the py_binary is going to be + script: {type}`str`, The console script name that the py_binary is going to be generated for. Defaults to the normalized name attribute. - binary_rule: {any}`rule callable`, The rule/macro to use to instantiate - the target. It's expected to behave like {any}`py_binary`. - Defaults to {any}`py_binary`. + binary_rule: {type}`callable`, The rule/macro to use to instantiate + the target. It's expected to behave like {obj}`py_binary`. + Defaults to {obj}`py_binary`. **kwargs: Extra parameters forwarded to `binary_rule`. """ main = "rules_python_entry_point_{}.py".format(name) diff --git a/python/private/pypi/attrs.bzl b/python/private/pypi/attrs.bzl index fe35d8bf7d..7ea19d106a 100644 --- a/python/private/pypi/attrs.bzl +++ b/python/private/pypi/attrs.bzl @@ -210,7 +210,7 @@ If True, suppress printing stdout and stderr output to the terminal. If you would like to get more diagnostic output, set {envvar}`RULES_PYTHON_REPO_DEBUG=1 ` or -{envvar}`RULES_PYTHON_REPO_DEBUG_VERBOSITY= ` +{envvar}`RULES_PYTHON_REPO_DEBUG_VERBOSITY=INFO|DEBUG|TRACE ` """, ), # 600 is documented as default here: https://docs.bazel.build/versions/master/skylark/lib/repository_ctx.html#execute From c678623fce4b5213b3c7661c166c0dac1ee22661 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 19 May 2025 21:07:54 -0700 Subject: [PATCH 109/156] refactor: explicitly define host platform ordering (#2890) It turns out the `unsorted-dict-items` disable in versions.bzl is load-bearing: the precedence of what host-compatible runtime is selected depends on the order of keys. Hence, the keys are carefully defined such that freethreaded and musl come after the regular runtimes. Make this subtle and implicit behavior explicit by having an ordering function that sorts keys in the order we want. Work towards https://github.com/bazel-contrib/rules_python/issues/2081 --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- python/private/python.bzl | 25 ++++++++++-------- python/private/toolchains_repo.bzl | 42 +++++++++++++++++++++++++++++- python/versions.bzl | 12 +++++---- 3 files changed, 62 insertions(+), 17 deletions(-) diff --git a/python/private/python.bzl b/python/private/python.bzl index 0f3bbee4ac..0cc19382d0 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -21,7 +21,7 @@ load(":full_version.bzl", "full_version") load(":python_register_toolchains.bzl", "python_register_toolchains") load(":pythons_hub.bzl", "hub_repo") load(":repo_utils.bzl", "repo_utils") -load(":toolchains_repo.bzl", "host_toolchain", "multi_toolchain_aliases") +load(":toolchains_repo.bzl", "host_toolchain", "multi_toolchain_aliases", "sorted_host_platforms") load(":util.bzl", "IS_BAZEL_6_4_OR_HIGHER") load(":version.bzl", "version") @@ -298,9 +298,8 @@ def _python_impl(module_ctx): _internal_bzlmod_toolchain_call = True, **kwargs ) - host_platforms = [] - host_os_names = {} - host_archs = {} + + host_platforms = {} for repo_name, (platform_name, platform_info) in register_result.impl_repos.items(): toolchain_impls.append(struct( # str: The base name to use for the toolchain() target @@ -319,17 +318,21 @@ def _python_impl(module_ctx): set_python_version_constraint = is_last, )) if _is_compatible_with_host(module_ctx, platform_info): - host_key = str(len(host_platforms)) - host_platforms.append(platform_name) - host_os_names[host_key] = platform_info.os_name - host_archs[host_key] = platform_info.arch + host_platforms[platform_name] = platform_info + host_platforms = sorted_host_platforms(host_platforms) host_toolchain( name = toolchain_info.name + "_host", # NOTE: Order matters. The first found to be compatible is (usually) used. - platforms = host_platforms, - os_names = host_os_names, - arch_names = host_archs, + platforms = host_platforms.keys(), + os_names = { + str(i): platform_info.os_name + for i, platform_info in enumerate(host_platforms.values()) + }, + arch_names = { + str(i): platform_info.arch + for i, platform_info in enumerate(host_platforms.values()) + }, python_version = full_python_version, ) diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl index 29ac694fd5..0fd05c6625 100644 --- a/python/private/toolchains_repo.bzl +++ b/python/private/toolchains_repo.bzl @@ -25,6 +25,8 @@ platform-specific repositories. load( "//python:versions.bzl", + "FREETHREADED", + "MUSL", "PLATFORMS", "WINDOWS_NAME", ) @@ -433,6 +435,44 @@ multi_toolchain_aliases = repository_rule( }, ) +def sorted_host_platforms(platform_map): + """Sort the keys in the platform map to give correct precedence. + + The order of keys in the platform mapping matters for the host toolchain + selection. When multiple runtimes are compatible with the host, we take the + first that is compatible (usually; there's also the + `RULES_PYTHON_REPO_TOOLCHAIN_*` environment variables). The historical + behavior carefully constructed the ordering of platform keys such that + the ordering was: + * Regular platforms + * The "-freethreaded" suffix + * The "-musl" suffix + + Here, we formalize that so it isn't subtly encoded in the ordering of keys + in a dict that autoformatters like to clobber and whose only documentation + is an innocous looking formatter disable directive. + + Args: + platform_map: a mapping of platforms and their metadata. + + Returns: + dict; the same values, but with the keys inserted in the desired + order so that iteration happens in the desired order. + """ + + def platform_keyer(name): + # Ascending sort: lower is higher precedence + return ( + 1 if MUSL in name else 0, + 1 if FREETHREADED in name else 0, + ) + + sorted_platform_keys = sorted(platform_map.keys(), key = platform_keyer) + return { + key: platform_map[key] + for key in sorted_platform_keys + } + def _get_host_platform(*, rctx, logger, python_version, os_name, cpu_name, platforms): """Gets the host platform. @@ -455,7 +495,7 @@ def _get_host_platform(*, rctx, logger, python_version, os_name, cpu_name, platf arch = rctx.attr.arch_names[key], ) else: - platform_map = PLATFORMS + platform_map = sorted_host_platforms(PLATFORMS) candidates = [] for platform in platforms: diff --git a/python/versions.bzl b/python/versions.bzl index 4a2a4cb758..166cc98851 100644 --- a/python/versions.bzl +++ b/python/versions.bzl @@ -19,7 +19,9 @@ MACOS_NAME = "osx" LINUX_NAME = "linux" WINDOWS_NAME = "windows" -FREETHREADED = "freethreaded" + +FREETHREADED = "-freethreaded" +MUSL = "-musl" INSTALL_ONLY = "install_only" DEFAULT_RELEASE_BASE_URL = "https://github.com/astral-sh/python-build-standalone/releases/download" @@ -845,7 +847,7 @@ def _generate_platforms(): for p, v in platforms.items() for suffix, freethreadedness in { "": is_freethreaded_no, - "-" + FREETHREADED: is_freethreaded_yes, + FREETHREADED: is_freethreaded_yes, }.items() } @@ -879,11 +881,11 @@ def get_release_info(platform, python_version, base_url = DEFAULT_RELEASE_BASE_U release_filename = None rendered_urls = [] for u in url: - p, _, _ = platform.partition("-" + FREETHREADED) + p, _, _ = platform.partition(FREETHREADED) - if FREETHREADED in platform: + if FREETHREADED.lstrip("-") in platform: build = "{}+{}-full".format( - FREETHREADED, + FREETHREADED.lstrip("-"), { "aarch64-apple-darwin": "pgo+lto", "aarch64-unknown-linux-gnu": "lto", From cd550d9e77989c021c6603f960100818fea6683f Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 20 May 2025 17:03:09 -0700 Subject: [PATCH 110/156] docs: generate docs for py_common, PyInfoBuilder APIs (#2920) I wrote up the docs awhile, but didn't fully wire them through to the doc gen. Fixes some various issues with the generated docs along the way. --- docs/BUILD.bazel | 1 + python/api/api.bzl | 21 ++- python/private/api/api.bzl | 12 ++ python/private/api/py_common_api.bzl | 29 ++- python/private/common.bzl | 2 +- python/private/py_info.bzl | 204 +++++++++++++++++++-- python/private/py_package.bzl | 2 +- tests/base_rules/py_info/py_info_tests.bzl | 2 +- 8 files changed, 255 insertions(+), 18 deletions(-) diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index 25da682012..b3e5f52022 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -113,6 +113,7 @@ sphinx_stardocs( "//python/private:builders_util_bzl", "//python/private:py_binary_rule_bzl", "//python/private:py_cc_toolchain_rule_bzl", + "//python/private:py_info_bzl", "//python/private:py_library_rule_bzl", "//python/private:py_runtime_rule_bzl", "//python/private:py_test_rule_bzl", diff --git a/python/api/api.bzl b/python/api/api.bzl index c8fb921c12..d41ec739cd 100644 --- a/python/api/api.bzl +++ b/python/api/api.bzl @@ -1,4 +1,23 @@ -"""Public, analysis phase APIs for Python rules.""" +"""Public, analysis phase APIs for Python rules. + +To use the analyis-time API, add the attributes to your rule, then +use `py_common.get()` to get the api object: + +``` +load("@rules_python//python/api:api.bzl", "py_common") + +def _impl(ctx): + py_api = py_common.get(ctx) + +myrule = rule( + implementation = _impl, + attrs = {...} | py_common.API_ATTRS +) +``` + +:::{versionadded} 0.37.0 +::: +""" load("//python/private/api:api.bzl", _py_common = "py_common") diff --git a/python/private/api/api.bzl b/python/private/api/api.bzl index 06fb7294b9..44f9ab4e77 100644 --- a/python/private/api/api.bzl +++ b/python/private/api/api.bzl @@ -27,6 +27,17 @@ will depend on the target that is providing the API struct. }, ) +def _py_common_typedef(): + """Typedef for py_common. + + :::{field} API_ATTRS + :type: dict[str, Attribute] + + The attributes that rules must have for `py_common.get()` to work. + ::: + + """ + def _py_common_get(ctx): """Get the py_common API instance. @@ -45,6 +56,7 @@ def _py_common_get(ctx): return ctx.attr._py_common_api[ApiImplInfo].impl py_common = struct( + TYPEDEF = _py_common_typedef, get = _py_common_get, API_ATTRS = { "_py_common_api": attr.label( diff --git a/python/private/api/py_common_api.bzl b/python/private/api/py_common_api.bzl index 401b35973e..6fed245257 100644 --- a/python/private/api/py_common_api.bzl +++ b/python/private/api/py_common_api.bzl @@ -22,17 +22,40 @@ def _py_common_api_impl(ctx): py_common_api = rule( implementation = _py_common_api_impl, - doc = "Rule implementing py_common API.", + doc = "Internal Rule implementing py_common API.", ) +def _py_common_api_typedef(): + """The py_common API implementation. + + An instance of this object is obtained using {obj}`py_common.get()` + """ + def _merge_py_infos(transitive, *, direct = []): - builder = PyInfoBuilder() + """Merge PyInfo objects into a single PyInfo. + + This is a convenience wrapper around {obj}`PyInfoBuilder.merge_all`. For + more control over merging PyInfo objects, use {obj}`PyInfoBuilder`. + + Args: + transitive: {type}`list[PyInfo]` The PyInfo objects with info + considered indirectly provided by something (e.g. via + its deps attribute). + direct: {type}`list[PyInfo]` The PyInfo objects that are + considered directly provided by something (e.g. via + the srcs attribute). + + Returns: + {type}`PyInfo` A PyInfo containing the merged values. + """ + builder = PyInfoBuilder.new() builder.merge_all(transitive, direct = direct) return builder.build() # Exposed for doc generation, not directly used. # buildifier: disable=name-conventions PyCommonApi = struct( + TYPEDEF = _py_common_api_typedef, merge_py_infos = _merge_py_infos, - PyInfoBuilder = PyInfoBuilder, + PyInfoBuilder = PyInfoBuilder.new, ) diff --git a/python/private/common.bzl b/python/private/common.bzl index 072a1bb296..a58a9c00a4 100644 --- a/python/private/common.bzl +++ b/python/private/common.bzl @@ -405,7 +405,7 @@ def create_py_info( transitive sources collected from dependencies (the latter is only necessary for deprecated extra actions support). """ - py_info = PyInfoBuilder() + py_info = PyInfoBuilder.new() py_info.site_packages_symlinks.add(site_packages_symlinks) py_info.direct_original_sources.add(original_sources) py_info.direct_pyc_files.add(required_pyc_files) diff --git a/python/private/py_info.bzl b/python/private/py_info.bzl index dc3cb24c51..d175eefb69 100644 --- a/python/private/py_info.bzl +++ b/python/private/py_info.bzl @@ -82,7 +82,11 @@ def _PyInfo_init( } PyInfo, _unused_raw_py_info_ctor = define_bazel_6_provider( - doc = "Encapsulates information provided by the Python rules.", + doc = """Encapsulates information provided by the Python rules. + +Instead of creating this object directly, use {obj}`PyInfoBuilder` and +the {obj}`PyCommonApi` utilities. +""", init = _PyInfo_init, fields = { "direct_original_sources": """ @@ -265,7 +269,65 @@ This field is currently unused in Bazel and may go away in the future. # The "effective" PyInfo is what the canonical //python:py_info.bzl%PyInfo symbol refers to _EffectivePyInfo = PyInfo if (config.enable_pystar or BuiltinPyInfo == None) else BuiltinPyInfo -def PyInfoBuilder(): +def _PyInfoBuilder_typedef(): + """Builder for PyInfo. + + To create an instance, use {obj}`py_common.get()` and call `PyInfoBuilder()` + + :::{field} direct_original_sources + :type: DepsetBuilder[File] + ::: + + :::{field} direct_pyc_files + :type: DepsetBuilder[File] + ::: + + :::{field} direct_pyi_files + :type: DepsetBuilder[File] + ::: + + :::{field} imports + :type: DepsetBuilder[str] + ::: + + :::{field} transitive_implicit_pyc_files + :type: DepsetBuilder[File] + ::: + + :::{field} transitive_implicit_pyc_source_files + :type: DepsetBuilder[File] + ::: + + :::{field} transitive_original_sources + :type: DepsetBuilder[File] + ::: + + :::{field} transitive_pyc_files + :type: DepsetBuilder[File] + ::: + + :::{field} transitive_pyi_files + :type: DepsetBuilder[File] + ::: + + :::{field} transitive_sources + :type: DepsetBuilder[File] + ::: + + :::{field} site_packages_symlinks + :type: DepsetBuilder[tuple[str | None, str]] + + NOTE: This depset has `topological` order + ::: + """ + +def _PyInfoBuilder_new(): + """Creates an instance. + + Returns: + {type}`PyInfoBuilder` + """ + # buildifier: disable=uninitialized self = struct( _has_py2_only_sources = [False], @@ -301,35 +363,116 @@ def PyInfoBuilder(): return self def _PyInfoBuilder_get_has_py3_only_sources(self): + """Get the `has_py3_only_sources` value. + + Args: + self: implicitly added. + + Returns: + {type}`bool` + """ return self._has_py3_only_sources[0] def _PyInfoBuilder_get_has_py2_only_sources(self): + """Get the `has_py2_only_sources` value. + + Args: + self: implicitly added. + + Returns: + {type}`bool` + """ return self._has_py2_only_sources[0] def _PyInfoBuilder_set_has_py2_only_sources(self, value): + """Sets `has_py2_only_sources` to `value`. + + Args: + self: implicitly added. + value: {type}`bool` The value to set. + + Returns: + {type}`PyInfoBuilder` self + """ self._has_py2_only_sources[0] = value return self def _PyInfoBuilder_set_has_py3_only_sources(self, value): + """Sets `has_py3_only_sources` to `value`. + + Args: + self: implicitly added. + value: {type}`bool` The value to set. + + Returns: + {type}`PyInfoBuilder` self + """ self._has_py3_only_sources[0] = value return self def _PyInfoBuilder_merge_has_py2_only_sources(self, value): + """Sets `has_py2_only_sources` based on current and incoming `value`. + + Args: + self: implicitly added. + value: {type}`bool` Another `has_py2_only_sources` value. It will + be merged into this builder's state. + + Returns: + {type}`PyInfoBuilder` self + """ self._has_py2_only_sources[0] = self._has_py2_only_sources[0] or value return self def _PyInfoBuilder_merge_has_py3_only_sources(self, value): + """Sets `has_py3_only_sources` based on current and incoming `value`. + + Args: + self: implicitly added. + value: {type}`bool` Another `has_py3_only_sources` value. It will + be merged into this builder's state. + + Returns: + {type}`PyInfoBuilder` self + """ self._has_py3_only_sources[0] = self._has_py3_only_sources[0] or value return self def _PyInfoBuilder_merge_uses_shared_libraries(self, value): + """Sets `uses_shared_libraries` based on current and incoming `value`. + + Args: + self: implicitly added. + value: {type}`bool` Another `uses_shared_libraries` value. It will + be merged into this builder's state. + + Returns: + {type}`PyInfoBuilder` self + """ self._uses_shared_libraries[0] = self._uses_shared_libraries[0] or value return self def _PyInfoBuilder_get_uses_shared_libraries(self): + """Get the `uses_shared_libraries` value. + + Args: + self: implicitly added. + + Returns: + {type}`bool` + """ return self._uses_shared_libraries[0] def _PyInfoBuilder_set_uses_shared_libraries(self, value): + """Sets `uses_shared_libraries` to `value`. + + Args: + self: implicitly added. + value: {type}`bool` The value to set. + + Returns: + {type}`PyInfoBuilder` self + """ self._uses_shared_libraries[0] = value return self @@ -344,7 +487,7 @@ def _PyInfoBuilder_merge(self, *infos, direct = []): direct fields into this object's direct fields. Returns: - {type}`PyInfoBuilder` the current object + {type}`PyInfoBuilder` self """ return self.merge_all(list(infos), direct = direct) @@ -359,7 +502,7 @@ def _PyInfoBuilder_merge_all(self, transitive, *, direct = []): direct fields into this object's direct fields. Returns: - {type}`PyInfoBuilder` the current object + {type}`PyInfoBuilder` self """ for info in direct: # BuiltinPyInfo doesn't have this field @@ -392,11 +535,11 @@ def _PyInfoBuilder_merge_target(self, target): Args: self: implicitly added. target: {type}`Target` targets that provide PyInfo, or other relevant - providers, will be merged into this object. If a target doesn't provide - any relevant providers, it is ignored. + providers, will be merged into this object. If a target doesn't provide + any relevant providers, it is ignored. Returns: - {type}`PyInfoBuilder` the current object. + {type}`PyInfoBuilder` self. """ if PyInfo in target: self.merge(target[PyInfo]) @@ -410,18 +553,26 @@ def _PyInfoBuilder_merge_targets(self, targets): Args: self: implicitly added. targets: {type}`list[Target]` - targets that provide PyInfo, or other relevant - providers, will be merged into this object. If a target doesn't provide - any relevant providers, it is ignored. + targets that provide PyInfo, or other relevant + providers, will be merged into this object. If a target doesn't provide + any relevant providers, it is ignored. Returns: - {type}`PyInfoBuilder` the current object. + {type}`PyInfoBuilder` self. """ for t in targets: self.merge_target(t) return self def _PyInfoBuilder_build(self): + """Builds into a {obj}`PyInfo` object. + + Args: + self: implicitly added. + + Returns: + {type}`PyInfo` + """ if config.enable_pystar: kwargs = dict( direct_original_sources = self.direct_original_sources.build(), @@ -447,6 +598,15 @@ def _PyInfoBuilder_build(self): ) def _PyInfoBuilder_build_builtin_py_info(self): + """Builds into a Bazel-builtin PyInfo object, if available. + + Args: + self: implicitly added. + + Returns: + {type}`BuiltinPyInfo | None` None is returned if Bazel's + builtin PyInfo object is disabled. + """ if BuiltinPyInfo == None: return None @@ -457,3 +617,25 @@ def _PyInfoBuilder_build_builtin_py_info(self): transitive_sources = self.transitive_sources.build(), uses_shared_libraries = self._uses_shared_libraries[0], ) + +# Provided for documentation purposes +# buildifier: disable=name-conventions +PyInfoBuilder = struct( + TYPEDEF = _PyInfoBuilder_typedef, + new = _PyInfoBuilder_new, + build = _PyInfoBuilder_build, + build_builtin_py_info = _PyInfoBuilder_build_builtin_py_info, + get_has_py2_only_sources = _PyInfoBuilder_get_has_py2_only_sources, + get_has_py3_only_sources = _PyInfoBuilder_get_has_py3_only_sources, + get_uses_shared_libraries = _PyInfoBuilder_get_uses_shared_libraries, + merge = _PyInfoBuilder_merge, + merge_all = _PyInfoBuilder_merge_all, + merge_has_py2_only_sources = _PyInfoBuilder_merge_has_py2_only_sources, + merge_has_py3_only_sources = _PyInfoBuilder_merge_has_py3_only_sources, + merge_target = _PyInfoBuilder_merge_target, + merge_targets = _PyInfoBuilder_merge_targets, + merge_uses_shared_libraries = _PyInfoBuilder_merge_uses_shared_libraries, + set_has_py2_only_sources = _PyInfoBuilder_set_has_py2_only_sources, + set_has_py3_only_sources = _PyInfoBuilder_set_has_py3_only_sources, + set_uses_shared_libraries = _PyInfoBuilder_set_uses_shared_libraries, +) diff --git a/python/private/py_package.bzl b/python/private/py_package.bzl index 1d866a9d80..adf2b6deef 100644 --- a/python/private/py_package.bzl +++ b/python/private/py_package.bzl @@ -34,7 +34,7 @@ def _path_inside_wheel(input_file): def _py_package_impl(ctx): inputs = builders.DepsetBuilder() - py_info = PyInfoBuilder() + py_info = PyInfoBuilder.new() for dep in ctx.attr.deps: inputs.add(dep[DefaultInfo].data_runfiles.files) inputs.add(dep[DefaultInfo].default_runfiles.files) diff --git a/tests/base_rules/py_info/py_info_tests.bzl b/tests/base_rules/py_info/py_info_tests.bzl index e160e704de..aa252a2937 100644 --- a/tests/base_rules/py_info/py_info_tests.bzl +++ b/tests/base_rules/py_info/py_info_tests.bzl @@ -162,7 +162,7 @@ def _test_py_info_builder_impl(env, targets): direct_pyi, trans_pyi, ) = targets.misc[DefaultInfo].files.to_list() - builder = PyInfoBuilder() + builder = PyInfoBuilder.new() builder.direct_pyc_files.add(direct_pyc) builder.direct_original_sources.add(original_py) builder.direct_pyi_files.add(direct_pyi) From 85fcd7aef8beed5e5fdbc1d65596345badae3e70 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Thu, 22 May 2025 17:56:52 -0700 Subject: [PATCH 111/156] refactor: rename host_toolchain rule to host_compatible_python_repo (#2926) The host_toolchain name is misleading, so rename it to some more accurate. Work towards https://github.com/bazel-contrib/rules_python/issues/2913 --- python/private/python.bzl | 4 ++-- python/private/python_register_toolchains.bzl | 4 ++-- python/private/toolchains_repo.bzl | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/python/private/python.bzl b/python/private/python.bzl index 0cc19382d0..24ce38ad3d 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -21,7 +21,7 @@ load(":full_version.bzl", "full_version") load(":python_register_toolchains.bzl", "python_register_toolchains") load(":pythons_hub.bzl", "hub_repo") load(":repo_utils.bzl", "repo_utils") -load(":toolchains_repo.bzl", "host_toolchain", "multi_toolchain_aliases", "sorted_host_platforms") +load(":toolchains_repo.bzl", "host_compatible_python_repo", "multi_toolchain_aliases", "sorted_host_platforms") load(":util.bzl", "IS_BAZEL_6_4_OR_HIGHER") load(":version.bzl", "version") @@ -321,7 +321,7 @@ def _python_impl(module_ctx): host_platforms[platform_name] = platform_info host_platforms = sorted_host_platforms(host_platforms) - host_toolchain( + host_compatible_python_repo( name = toolchain_info.name + "_host", # NOTE: Order matters. The first found to be compatible is (usually) used. platforms = host_platforms.keys(), diff --git a/python/private/python_register_toolchains.bzl b/python/private/python_register_toolchains.bzl index e821bae5e7..2e0748deb0 100644 --- a/python/private/python_register_toolchains.bzl +++ b/python/private/python_register_toolchains.bzl @@ -28,7 +28,7 @@ load(":full_version.bzl", "full_version") load(":python_repository.bzl", "python_repository") load( ":toolchains_repo.bzl", - "host_toolchain", + "host_compatible_python_repo", "toolchain_aliases", "toolchains_repo", ) @@ -185,7 +185,7 @@ def python_register_toolchains( impl_repos = impl_repos, ) - host_toolchain( + host_compatible_python_repo( name = name + "_host", platforms = loaded_platforms, python_version = python_version, diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl index 0fd05c6625..cf4373932b 100644 --- a/python/private/toolchains_repo.bzl +++ b/python/private/toolchains_repo.bzl @@ -379,7 +379,7 @@ def _host_toolchain_impl(rctx): # NOTE: The term "toolchain" is a misnomer for this rule. This doesn't define # a repo with toolchains or toolchain implementations. -host_toolchain = repository_rule( +host_compatible_python_repo = repository_rule( _host_toolchain_impl, doc = """\ Creates a repository with a shorter name meant to be used in the repository_ctx, @@ -400,7 +400,7 @@ If set, overrides the platform metadata. Keyed by index in `platforms` ), "platforms": attr.string_list(mandatory = True), "python_version": attr.string(mandatory = True), - "_rule_name": attr.string(default = "host_toolchain"), + "_rule_name": attr.string(default = "host_compatible_python_repo"), "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")), }, ) From 46f08dea00288300aaefbb1186074f9d0f0779b5 Mon Sep 17 00:00:00 2001 From: Christian von Schultz Date: Fri, 23 May 2025 02:58:25 +0200 Subject: [PATCH 112/156] docs/refactor: Use python.defaults, not is_default (#2924) When there are multiple Python toolchains, there are currently two ways of setting the default version: the `is_default` attribute of the `python.toolchain()` tag class and the `python.defaults()` tag class. The latter is more powerful, since it also supports files and environment variables. This patch updates the examples and the docs to use `python.defaults()`. Relates to pull request #2588 and issue #2587. --- docs/api/rules_python/python/bin/index.md | 3 ++- docs/toolchains.md | 15 +++++++++++---- examples/bzlmod/MODULE.bazel | 7 +++++-- examples/bzlmod/other_module/MODULE.bazel | 6 ++++-- .../bzlmod_build_file_generation/MODULE.bazel | 6 +++++- examples/multi_python_versions/MODULE.bazel | 2 -- python/extensions/python.bzl | 6 ++---- python/private/python.bzl | 10 ++++------ 8 files changed, 33 insertions(+), 22 deletions(-) diff --git a/docs/api/rules_python/python/bin/index.md b/docs/api/rules_python/python/bin/index.md index 8bea6b54bd..873b644341 100644 --- a/docs/api/rules_python/python/bin/index.md +++ b/docs/api/rules_python/python/bin/index.md @@ -10,7 +10,8 @@ A target to directly run a Python interpreter. By default, it uses the Python version that toolchain resolution matches -(typically the one marked `is_default=True` in `MODULE.bazel`). +(typically the one set with `python.defaults(python_version = ...)` in +`MODULE.bazel`). This runs a Python interpreter in a similar manner as when running `python3` on the command line. It can be invoked using `bazel run`. Remember that in diff --git a/docs/toolchains.md b/docs/toolchains.md index a2a2b5b63e..ada887c945 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -44,7 +44,8 @@ you should read the dev-only library module section. bazel_dep(name="rules_python", version=...) python = use_extension("@rules_python//python/extensions:python.bzl", "python") -python.toolchain(python_version = "3.12", is_default = True) +python.defaults(python_version = "3.12") +python.toolchain(python_version = "3.12") ``` ### Library modules @@ -72,7 +73,8 @@ python = use_extension( dev_dependency = True ) -python.toolchain(python_version = "3.12", is_default=True) +python.defaults(python_version = "3.12") +python.toolchain(python_version = "3.12") ``` #### Library modules without version constraints @@ -161,9 +163,13 @@ Multiple versions can be specified and used within a single build. # MODULE.bazel python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.defaults( + # The environment variable takes precedence if set. + python_version = "3.11", + python_version_env = "BAZEL_PYTHON_VERSION", +) python.toolchain( python_version = "3.11", - is_default = True, ) python.toolchain( @@ -264,7 +270,8 @@ bazel_dep(name = "rules_python", version = "0.40.0") python = use_extension("@rules_python//python/extensions:python.bzl", "python") -python.toolchain(is_default = True, python_version = "3.10") +python.defaults(python_version = "3.10") +python.toolchain(python_version = "3.10") use_repo(python, "python_3_10", "python_3_10_host") ``` diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel index 69e384e42b..841c096dcf 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -28,10 +28,13 @@ bazel_dep(name = "rules_rust", version = "0.54.1") # We next initialize the python toolchain using the extension. # You can set different Python versions in this block. python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.defaults( + # Use python.defaults if you have defined multiple toolchain versions. + python_version = "3.9", + python_version_env = "BAZEL_PYTHON_VERSION", +) python.toolchain( configure_coverage_tool = True, - # Only set when you have multiple toolchain versions. - is_default = True, python_version = "3.9", ) diff --git a/examples/bzlmod/other_module/MODULE.bazel b/examples/bzlmod/other_module/MODULE.bazel index 959501abc2..f9d6706120 100644 --- a/examples/bzlmod/other_module/MODULE.bazel +++ b/examples/bzlmod/other_module/MODULE.bazel @@ -25,14 +25,16 @@ PYTHON_NAME_39 = "python_3_9" PYTHON_NAME_311 = "python_3_11" python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.defaults( + # In a submodule this is ignored + python_version = "3.11", +) python.toolchain( configure_coverage_tool = True, python_version = "3.9", ) python.toolchain( configure_coverage_tool = True, - # In a submodule this is ignored - is_default = True, python_version = "3.11", ) diff --git a/examples/bzlmod_build_file_generation/MODULE.bazel b/examples/bzlmod_build_file_generation/MODULE.bazel index 9bec25fcbb..b9b428d365 100644 --- a/examples/bzlmod_build_file_generation/MODULE.bazel +++ b/examples/bzlmod_build_file_generation/MODULE.bazel @@ -46,9 +46,13 @@ python = use_extension("@rules_python//python/extensions:python.bzl", "python") # We next initialize the python toolchain using the extension. # You can set different Python versions in this block. +python.defaults( + # The environment variable takes precedence if set. + python_version = "3.9", + python_version_env = "BAZEL_PYTHON_VERSION", +) python.toolchain( configure_coverage_tool = True, - is_default = True, python_version = "3.9", ) diff --git a/examples/multi_python_versions/MODULE.bazel b/examples/multi_python_versions/MODULE.bazel index 85140360bb..4e4a0473c2 100644 --- a/examples/multi_python_versions/MODULE.bazel +++ b/examples/multi_python_versions/MODULE.bazel @@ -17,8 +17,6 @@ python.defaults( ) python.toolchain( configure_coverage_tool = True, - # Only set when you have mulitple toolchain versions. - is_default = True, python_version = "3.9", ) python.toolchain( diff --git a/python/extensions/python.bzl b/python/extensions/python.bzl index abd5080dd8..b8b755ebca 100644 --- a/python/extensions/python.bzl +++ b/python/extensions/python.bzl @@ -20,10 +20,8 @@ The simplest way to configure the toolchain with `rules_python` is as follows. ```starlark python = use_extension("@rules_python//python/extensions:python.bzl", "python") -python.toolchain( - is_default = True, - python_version = "3.11", -) +python.defaults(python_version = "3.11") +python.toolchain(python_version = "3.11") use_repo(python, "python_3_11") ``` diff --git a/python/private/python.bzl b/python/private/python.bzl index 24ce38ad3d..a7e257601f 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -223,7 +223,7 @@ def parse_modules(*, module_ctx, _fail = fail): # A default toolchain is required so that the non-version-specific rules # are able to match a toolchain. if default_toolchain == None: - fail("No default Python toolchain configured. Is rules_python missing `is_default=True`?") + fail("No default Python toolchain configured. Is rules_python missing `python.defaults()`?") elif default_toolchain.python_version not in global_toolchain_versions: fail('Default version "{python_version}" selected by module ' + '"{module_name}", but no toolchain with that version registered'.format( @@ -891,10 +891,8 @@ In order to use a different name than the above, you can use the following `MODU syntax: ```starlark python = use_extension("@rules_python//python/extensions:python.bzl", "python") -python.toolchain( - is_default = True, - python_version = "3.11", -) +python.defaults(python_version = "3.11") +python.toolchain(python_version = "3.11") use_repo(python, my_python_name = "python_3_11") ``` @@ -930,7 +928,7 @@ Whether the toolchain is the default version. :::{versionchanged} 1.4.0 This setting is ignored if the default version is set using the `defaults` -tag class. +tag class (encouraged). ::: """, ), From 2036571e90f3af5318cae40bd504b59939923ec2 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 23 May 2025 22:09:15 +0200 Subject: [PATCH 113/156] fix: Normalize main script path in Python bootstrap (#2925) Use `os.path.normpath()` to resolve `_main/../repo/` to `repo/` and convert forward slashes to backward slashes on Windows. This fixes an issue where `_main` doesn't exist within runfiles and in turn the later assertion that the path to main exists fails (~L542). This happens, for example, when packaging a `py_binary` from a foreign repo into a tar/container. --------- Co-authored-by: Richard Levasseur Co-authored-by: Richard Levasseur --- CHANGELOG.md | 1 + internal_dev_deps.bzl | 6 ++--- python/private/python_bootstrap_template.txt | 8 +++++-- tests/bootstrap_impls/BUILD.bazel | 24 +++++++++++++++++-- tests/bootstrap_impls/external_binary_test.sh | 9 +++++++ tests/modules/other/BUILD.bazel | 14 +++++++++++ tests/modules/other/external_main.py | 1 + 7 files changed, 56 insertions(+), 7 deletions(-) create mode 100755 tests/bootstrap_impls/external_binary_test.sh create mode 100644 tests/modules/other/external_main.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a76241018d..9655b90487 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,7 @@ END_UNRELEASED_TEMPLATE also retrieved from the URL as opposed to only the `--hash` parameter. Fixes [#2363](https://github.com/bazel-contrib/rules_python/issues/2363). * (pypi) `whl_library` now infers file names from its `urls` attribute correctly. +* (py_test, py_binary) Allow external files to be used for main {#v0-0-0-added} ### Added diff --git a/internal_dev_deps.bzl b/internal_dev_deps.bzl index 87690be1ad..f2b33e279e 100644 --- a/internal_dev_deps.bzl +++ b/internal_dev_deps.bzl @@ -68,10 +68,10 @@ def rules_python_internal_deps(): http_archive( name = "rules_pkg", urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/rules_pkg/releases/download/0.7.0/rules_pkg-0.7.0.tar.gz", - "https://github.com/bazelbuild/rules_pkg/releases/download/0.7.0/rules_pkg-0.7.0.tar.gz", + "https://mirror.bazel.build/github.com/bazelbuild/rules_pkg/releases/download/1.0.1/rules_pkg-1.0.1.tar.gz", + "https://github.com/bazelbuild/rules_pkg/releases/download/1.0.1/rules_pkg-1.0.1.tar.gz", ], - sha256 = "8a298e832762eda1830597d64fe7db58178aa84cd5926d76d5b744d6558941c2", + sha256 = "d20c951960ed77cb7b341c2a59488534e494d5ad1d30c4818c736d57772a9fef", ) http_archive( diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt index 210987abf9..a979fd4422 100644 --- a/python/private/python_bootstrap_template.txt +++ b/python/private/python_bootstrap_template.txt @@ -499,8 +499,12 @@ def Main(): # The magic string percent-main-percent is replaced with the runfiles-relative # filename of the main file of the Python binary in BazelPythonSemantics.java. main_rel_path = '%main%' - if IsWindows(): - main_rel_path = main_rel_path.replace('/', os.sep) + # NOTE: We call normpath for two reasons: + # 1. Transform Bazel `foo/bar` to Windows `foo\bar` + # 2. Transform `_main/../foo/main.py` to simply `foo/main.py`, which + # matters if `_main` doesn't exist (which can occur if a binary + # is packaged and needs no artifacts from the main repo) + main_rel_path = os.path.normpath(main_rel_path) if IsRunningFromZip(): module_space = CreateModuleSpace() diff --git a/tests/bootstrap_impls/BUILD.bazel b/tests/bootstrap_impls/BUILD.bazel index b669da5669..c3d44df240 100644 --- a/tests/bootstrap_impls/BUILD.bazel +++ b/tests/bootstrap_impls/BUILD.bazel @@ -1,5 +1,3 @@ -load("@rules_shell//shell:sh_test.bzl", "sh_test") - # Copyright 2023 The Bazel Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,6 +11,8 @@ load("@rules_shell//shell:sh_test.bzl", "sh_test") # 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_pkg//pkg:tar.bzl", "pkg_tar") +load("@rules_shell//shell:sh_test.bzl", "sh_test") load("//tests/support:py_reconfig.bzl", "py_reconfig_binary", "py_reconfig_test") load("//tests/support:sh_py_run_test.bzl", "sh_py_run_test") load("//tests/support:support.bzl", "SUPPORTS_BOOTSTRAP_SCRIPT") @@ -158,4 +158,24 @@ py_reconfig_test( target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT, ) +pkg_tar( + name = "external_binary", + testonly = True, + srcs = ["@other//:external_main"], + include_runfiles = True, + tags = ["manual"], # Don't build as part of wildcards +) + +sh_test( + name = "external_binary_test", + srcs = ["external_binary_test.sh"], + data = [":external_binary"], + # For now, skip this test on Windows because it fails for reasons + # other than the code path being tested. + target_compatible_with = select({ + "@platforms//os:windows": ["@platforms//:incompatible"], + "//conditions:default": [], + }), +) + relative_path_test_suite(name = "relative_path_tests") diff --git a/tests/bootstrap_impls/external_binary_test.sh b/tests/bootstrap_impls/external_binary_test.sh new file mode 100755 index 0000000000..e3516af18e --- /dev/null +++ b/tests/bootstrap_impls/external_binary_test.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -euxo pipefail + +tmpdir="${TEST_TMPDIR}/external_binary" +mkdir -p "${tmpdir}" +tar xf "tests/bootstrap_impls/external_binary.tar" -C "${tmpdir}" +test -x "${tmpdir}/external_main" +output="$("${tmpdir}/external_main")" +test "$output" = "token" diff --git a/tests/modules/other/BUILD.bazel b/tests/modules/other/BUILD.bazel index e69de29bb2..46f1b96faa 100644 --- a/tests/modules/other/BUILD.bazel +++ b/tests/modules/other/BUILD.bazel @@ -0,0 +1,14 @@ +load("@rules_python//tests/support:py_reconfig.bzl", "py_reconfig_binary") + +package( + default_visibility = ["//visibility:public"], +) + +py_reconfig_binary( + name = "external_main", + srcs = [":external_main.py"], + # We're testing a system_python specific code path, + # so force using that bootstrap + bootstrap_impl = "system_python", + main = "external_main.py", +) diff --git a/tests/modules/other/external_main.py b/tests/modules/other/external_main.py new file mode 100644 index 0000000000..f742ebab60 --- /dev/null +++ b/tests/modules/other/external_main.py @@ -0,0 +1 @@ +print("token") From 3aea414aa2e5f1e0e915f58a434c01428a90382c Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 23 May 2025 19:52:32 -0700 Subject: [PATCH 114/156] refactor: also rename host toolchain impl function name (#2930) The implementation function name got missed when the repo rule name itself was changed. --- python/private/toolchains_repo.bzl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl index cf4373932b..2476889583 100644 --- a/python/private/toolchains_repo.bzl +++ b/python/private/toolchains_repo.bzl @@ -309,7 +309,7 @@ actions.""", environ = [REPO_DEBUG_ENV_VAR], ) -def _host_toolchain_impl(rctx): +def _host_compatible_python_repo(rctx): rctx.file("BUILD.bazel", _HOST_TOOLCHAIN_BUILD_CONTENT) os_name = repo_utils.get_platforms_os_name(rctx) @@ -380,7 +380,7 @@ def _host_toolchain_impl(rctx): # NOTE: The term "toolchain" is a misnomer for this rule. This doesn't define # a repo with toolchains or toolchain implementations. host_compatible_python_repo = repository_rule( - _host_toolchain_impl, + _host_compatible_python_repo, doc = """\ Creates a repository with a shorter name meant to be used in the repository_ctx, which needs to have `symlinks` for the interpreter. This is separate from the From 28fda8664a1e89f2f055ed7183ad28dbdbeaafc9 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 24 May 2025 13:35:03 -0700 Subject: [PATCH 115/156] tests: refactor py_reconfig rules so less boilerplate is needed to add attrs (#2933) Just some minor refactoring of the py_reconfig rule so that it's easier to add attributes that affect transition state. After this, just two spots have to be modified to add an attribute (map of attrs, map of attr to transition label). --- tests/support/sh_py_run_test.bzl | 82 ++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/tests/support/sh_py_run_test.bzl b/tests/support/sh_py_run_test.bzl index 1a61de9bd3..04a2883fde 100644 --- a/tests/support/sh_py_run_test.bzl +++ b/tests/support/sh_py_run_test.bzl @@ -13,14 +13,88 @@ # limitations under the License. """Run a py_binary with altered config settings in an sh_test. -This facilitates verify running binaries with different outer environmental -settings and verifying their output without the overhead of a bazel-in-bazel -integration test. +This facilitates verify running binaries with different configuration settings +without the overhead of a bazel-in-bazel integration test. """ load("@rules_shell//shell:sh_test.bzl", "sh_test") +load("//python/private:attr_builders.bzl", "attrb") # buildifier: disable=bzl-visibility +load("//python/private:py_binary_macro.bzl", "py_binary_macro") # buildifier: disable=bzl-visibility +load("//python/private:py_binary_rule.bzl", "create_py_binary_rule_builder") # buildifier: disable=bzl-visibility +load("//python/private:py_test_macro.bzl", "py_test_macro") # buildifier: disable=bzl-visibility +load("//python/private:py_test_rule.bzl", "create_py_test_rule_builder") # buildifier: disable=bzl-visibility load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") # buildifier: disable=bzl-visibility -load(":py_reconfig.bzl", "py_reconfig_binary") +load("//tests/support:support.bzl", "VISIBLE_FOR_TESTING") + +def _perform_transition_impl(input_settings, attr, base_impl): + settings = {k: input_settings[k] for k in _RECONFIG_INHERITED_OUTPUTS if k in input_settings} + settings.update(base_impl(input_settings, attr)) + + settings[VISIBLE_FOR_TESTING] = True + settings["//command_line_option:build_python_zip"] = attr.build_python_zip + + for attr_name, setting_label in _RECONFIG_ATTR_SETTING_MAP.items(): + if getattr(attr, attr_name): + settings[setting_label] = getattr(attr, attr_name) + return settings + +# Attributes that, if non-falsey (`if attr.`), will copy their +# value into the output settings +_RECONFIG_ATTR_SETTING_MAP = { + "bootstrap_impl": "//python/config_settings:bootstrap_impl", + "extra_toolchains": "//command_line_option:extra_toolchains", + "python_src": "//python/bin:python_src", + "venvs_site_packages": "//python/config_settings:venvs_site_packages", + "venvs_use_declare_symlink": "//python/config_settings:venvs_use_declare_symlink", +} + +_RECONFIG_INPUTS = _RECONFIG_ATTR_SETTING_MAP.values() +_RECONFIG_OUTPUTS = _RECONFIG_INPUTS + [ + "//command_line_option:build_python_zip", + VISIBLE_FOR_TESTING, +] +_RECONFIG_INHERITED_OUTPUTS = [v for v in _RECONFIG_OUTPUTS if v in _RECONFIG_INPUTS] + +_RECONFIG_ATTRS = { + "bootstrap_impl": attrb.String(), + "build_python_zip": attrb.String(default = "auto"), + "extra_toolchains": attrb.StringList( + doc = """ +Value for the --extra_toolchains flag. + +NOTE: You'll likely have to also specify //tests/support/cc_toolchains:all (or some CC toolchain) +to make the RBE presubmits happy, which disable auto-detection of a CC +toolchain. +""", + ), + "python_src": attrb.Label(), + "venvs_site_packages": attrb.String(), + "venvs_use_declare_symlink": attrb.String(), +} + +def _create_reconfig_rule(builder): + builder.attrs.update(_RECONFIG_ATTRS) + + base_cfg_impl = builder.cfg.implementation() + builder.cfg.set_implementation(lambda *args: _perform_transition_impl(base_impl = base_cfg_impl, *args)) + builder.cfg.update_inputs(_RECONFIG_INPUTS) + builder.cfg.update_outputs(_RECONFIG_OUTPUTS) + return builder.build() + +_py_reconfig_binary = _create_reconfig_rule(create_py_binary_rule_builder()) + +_py_reconfig_test = _create_reconfig_rule(create_py_test_rule_builder()) + +def py_reconfig_test(**kwargs): + """Create a py_test with customized build settings for testing. + + Args: + **kwargs: kwargs to pass along to _py_reconfig_test. + """ + py_test_macro(_py_reconfig_test, **kwargs) + +def py_reconfig_binary(**kwargs): + py_binary_macro(_py_reconfig_binary, **kwargs) def sh_py_run_test(*, name, sh_src, py_src, **kwargs): """Run a py_binary within a sh_test. From e73dccf7b1827b1ea1216646ac82c97ae8d1e64b Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Sun, 25 May 2025 13:21:44 +0800 Subject: [PATCH 116/156] feat: add shebang attribute on py_console_script_binary (#2867) # Background Use case: user is setting up the environment for a docker image, and needs a bash executable from the py_console_script (e.g. to run `ray` from command line without full bazel bootstrapping). User is responsible of setting up the right paths (and hermeticity concerns). There's no change in default behavior per this diff. Previously, prior to Bazel mod, this was possible and simple through the use of `rules_python_wheel_entry_points` ([per here](https://github.com/bazel-contrib/rules_python/blob/9dfa3abba293488a9a1899832a340f7b44525cad/python/private/pypi/whl_library.bzl#L507)) but these are not reachable now via Bazel mod. # Approach Add a shebang attribute that allows users of the console binary to use it like a binary executable. This is similar to the functionality that came with wheel entry points here: https://github.com/bazel-contrib/rules_python/blob/9dfa3abba293488a9a1899832a340f7b44525cad/python/private/pypi/whl_library.bzl#L507 With this change, one can specify a shebang like: ```starlark py_console_script_binary( name = "yamllint", pkg = "@pip//yamllint", shebang = "#!/usr/bin/env python3", ) ``` Summary: - Update tests - Add test for this functionality - Leave default to without shebang so this is a non-breaking change - Documentation (want to hear more about the general approach first, and also want to hear whether this warrants specific docs, or can just leave it to API docs) --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- docs/_includes/py_console_script_binary.md | 23 ++++++++++++- python/private/py_console_script_binary.bzl | 4 +++ python/private/py_console_script_gen.bzl | 5 +++ python/private/py_console_script_gen.py | 11 +++++- .../py_console_script_gen_test.py | 34 +++++++++++++++++++ 5 files changed, 75 insertions(+), 2 deletions(-) diff --git a/docs/_includes/py_console_script_binary.md b/docs/_includes/py_console_script_binary.md index aa356e0e94..d327091630 100644 --- a/docs/_includes/py_console_script_binary.md +++ b/docs/_includes/py_console_script_binary.md @@ -48,6 +48,26 @@ py_console_script_binary( ) ``` +#### Adding a Shebang Line + +You can specify a shebang line for the generated binary, useful for Unix-like +systems where the shebang line determines which interpreter is used to execute +the script, per [PEP441]: + +```starlark +load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") + +py_console_script_binary( + name = "black", + pkg = "@pip//black", + shebang = "#!/usr/bin/env python3", +) +``` + +Note that to execute via the shebang line, you need to ensure the specified +Python interpreter is available in the environment. + + #### Using a specific Python Version directly from a Toolchain :::{deprecated} 1.1.0 The toolchain specific `py_binary` and `py_test` symbols are aliases to the regular rules. @@ -70,4 +90,5 @@ py_console_script_binary( ``` [specification]: https://packaging.python.org/en/latest/specifications/entry-points/ -[`py_console_script_binary.binary_rule`]: #py_console_script_binary_binary_rule \ No newline at end of file +[`py_console_script_binary.binary_rule`]: #py_console_script_binary_binary_rule +[PEP441]: https://peps.python.org/pep-0441/#minimal-tooling-the-zipapp-module diff --git a/python/private/py_console_script_binary.bzl b/python/private/py_console_script_binary.bzl index 154fa3bf2f..d98457dbe1 100644 --- a/python/private/py_console_script_binary.bzl +++ b/python/private/py_console_script_binary.bzl @@ -52,6 +52,7 @@ def py_console_script_binary( entry_points_txt = None, script = None, binary_rule = py_binary, + shebang = "", **kwargs): """Generate a py_binary for a console_script entry_point. @@ -68,6 +69,8 @@ def py_console_script_binary( binary_rule: {type}`callable`, The rule/macro to use to instantiate the target. It's expected to behave like {obj}`py_binary`. Defaults to {obj}`py_binary`. + shebang: {type}`str`, The shebang to use for the entry point python file. + Defaults to empty string. **kwargs: Extra parameters forwarded to `binary_rule`. """ main = "rules_python_entry_point_{}.py".format(name) @@ -81,6 +84,7 @@ def py_console_script_binary( out = main, console_script = script, console_script_guess = name, + shebang = shebang, visibility = ["//visibility:private"], ) diff --git a/python/private/py_console_script_gen.bzl b/python/private/py_console_script_gen.bzl index 7dd4dd2dad..de016036b2 100644 --- a/python/private/py_console_script_gen.bzl +++ b/python/private/py_console_script_gen.bzl @@ -42,6 +42,7 @@ def _py_console_script_gen_impl(ctx): args = ctx.actions.args() args.add("--console-script", ctx.attr.console_script) args.add("--console-script-guess", ctx.attr.console_script_guess) + args.add("--shebang", ctx.attr.shebang) args.add(entry_points_txt) args.add(ctx.outputs.out) @@ -81,6 +82,10 @@ py_console_script_gen = rule( doc = "Output file location.", mandatory = True, ), + "shebang": attr.string( + doc = "The shebang to use for the entry point python file.", + default = "", + ), "_tool": attr.label( default = ":py_console_script_gen_py", executable = True, diff --git a/python/private/py_console_script_gen.py b/python/private/py_console_script_gen.py index ffc4e81b3a..4b4f2f6986 100644 --- a/python/private/py_console_script_gen.py +++ b/python/private/py_console_script_gen.py @@ -44,7 +44,7 @@ _ENTRY_POINTS_TXT = "entry_points.txt" _TEMPLATE = """\ -import sys +{shebang}import sys # See @rules_python//python/private:py_console_script_gen.py for explanation if getattr(sys.flags, "safe_path", False): @@ -87,6 +87,7 @@ def run( out: pathlib.Path, console_script: str, console_script_guess: str, + shebang: str, ): """Run the generator @@ -94,6 +95,8 @@ def run( entry_points: The entry_points.txt file to be parsed. out: The output file. console_script: The console_script entry in the entry_points.txt file. + console_script_guess: The string used for guessing the console_script if it is not provided. + shebang: The shebang to use for the entry point python file. Defaults to empty string (no shebang). """ config = EntryPointsParser() config.read(entry_points) @@ -136,6 +139,7 @@ def run( with open(out, "w") as f: f.write( _TEMPLATE.format( + shebang=f"{shebang}\n" if shebang else "", module=module, attr=attr, entry_point=entry_point, @@ -154,6 +158,10 @@ def main(): required=True, help="The string used for guessing the console_script if it is not provided.", ) + parser.add_argument( + "--shebang", + help="The shebang to use for the entry point python file.", + ) parser.add_argument( "entry_points", metavar="ENTRY_POINTS_TXT", @@ -173,6 +181,7 @@ def main(): out=args.out, console_script=args.console_script, console_script_guess=args.console_script_guess, + shebang=args.shebang, ) diff --git a/tests/entry_points/py_console_script_gen_test.py b/tests/entry_points/py_console_script_gen_test.py index a5fceb67f9..1bbf5fbf25 100644 --- a/tests/entry_points/py_console_script_gen_test.py +++ b/tests/entry_points/py_console_script_gen_test.py @@ -47,6 +47,7 @@ def test_no_console_scripts_error(self): out=outfile, console_script=None, console_script_guess="", + shebang="", ) self.assertEqual( @@ -76,6 +77,7 @@ def test_no_entry_point_selected_error(self): out=outfile, console_script=None, console_script_guess="bar-baz", + shebang="", ) self.assertEqual( @@ -106,6 +108,7 @@ def test_incorrect_entry_point(self): out=outfile, console_script="baz", console_script_guess="", + shebang="", ) self.assertEqual( @@ -134,6 +137,7 @@ def test_a_single_entry_point(self): out=out, console_script=None, console_script_guess="foo", + shebang="", ) got = out.read_text() @@ -185,6 +189,7 @@ def test_a_second_entry_point_class_method(self): out=out, console_script="bar", console_script_guess="", + shebang="", ) got = out.read_text() @@ -192,6 +197,35 @@ def test_a_second_entry_point_class_method(self): self.assertRegex(got, "from foo\.baz import Bar") self.assertRegex(got, "sys\.exit\(Bar\.baz\(\)\)") + def test_shebang_included(self): + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = pathlib.Path(tmpdir) + given_contents = ( + textwrap.dedent( + """ + [console_scripts] + foo = foo.bar:baz + """ + ).strip() + + "\n" + ) + entry_points = tmpdir / "entry_points.txt" + entry_points.write_text(given_contents) + out = tmpdir / "foo.py" + + shebang = "#!/usr/bin/env python3" + run( + entry_points=entry_points, + out=out, + console_script=None, + console_script_guess="foo", + shebang=shebang, + ) + + got = out.read_text() + + self.assertTrue(got.startswith(shebang + "\n")) + if __name__ == "__main__": unittest.main() From b40d96aba36d675c60b03424aa22f31c09e0ea4f Mon Sep 17 00:00:00 2001 From: Kayce Basques Date: Mon, 26 May 2025 06:36:58 -0700 Subject: [PATCH 117/156] fix: update the stub type alias names (#2929) Co-authored-by: Kayce Basques --- tools/precompiler/precompiler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/precompiler/precompiler.py b/tools/precompiler/precompiler.py index 310f2eb097..e7c693c195 100644 --- a/tools/precompiler/precompiler.py +++ b/tools/precompiler/precompiler.py @@ -68,12 +68,12 @@ def _compile(options: "argparse.Namespace") -> None: # A stub type alias for readability. # See the Bazel WorkRequest object definition: # https://github.com/bazelbuild/bazel/blob/master/src/main/protobuf/worker_protocol.proto -JsonWorkerRequest = object +JsonWorkRequest = object # A stub type alias for readability. # See the Bazel WorkResponse object definition: # https://github.com/bazelbuild/bazel/blob/master/src/main/protobuf/worker_protocol.proto -JsonWorkerResponse = object +JsonWorkResponse = object class _SerialPersistentWorker: From dce5120249f62bd04d2bafa48fd053732854e1ad Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Wed, 28 May 2025 00:47:49 +0900 Subject: [PATCH 118/156] refactor: reimplement writing namespace pkgs in Starlark (#2882) With this PR I would like to facilitate the implementation of the venv layouts because we can in theory take the `srcs` and the `data` within the `py_library` and then use the `expand_template` to write the extra Python files if the namespace_pkgs flag is enabled. The old Python code has been removed and the extra generated files are written out with `bazel_skylib` `copy_file`. The implicit `namespace_pkg` init files are included to `py_library` if the `site-packages` config flag is set to false and I think this may help with continuing the implementation, but it currently is still not working as expected (see comment). Work towards #2156 --- python/config_settings/BUILD.bazel | 9 + python/private/pypi/BUILD.bazel | 5 + python/private/pypi/namespace_pkg_tmpl.py | 2 + python/private/pypi/namespace_pkgs.bzl | 83 ++++++++ .../private/pypi/whl_installer/arguments.py | 5 - .../pypi/whl_installer/wheel_installer.py | 32 +-- python/private/pypi/whl_library.bzl | 8 +- python/private/pypi/whl_library_targets.bzl | 50 +++-- tests/pypi/namespace_pkgs/BUILD.bazel | 5 + .../namespace_pkgs/namespace_pkgs_tests.bzl | 167 +++++++++++++++ tests/pypi/whl_installer/BUILD.bazel | 11 - tests/pypi/whl_installer/arguments_test.py | 1 - .../pypi/whl_installer/namespace_pkgs_test.py | 192 ------------------ .../whl_installer/wheel_installer_test.py | 1 - 14 files changed, 311 insertions(+), 260 deletions(-) create mode 100644 python/private/pypi/namespace_pkg_tmpl.py create mode 100644 python/private/pypi/namespace_pkgs.bzl create mode 100644 tests/pypi/namespace_pkgs/BUILD.bazel create mode 100644 tests/pypi/namespace_pkgs/namespace_pkgs_tests.bzl delete mode 100644 tests/pypi/whl_installer/namespace_pkgs_test.py diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel index 1772a3403e..ee15828fa5 100644 --- a/python/config_settings/BUILD.bazel +++ b/python/config_settings/BUILD.bazel @@ -217,6 +217,15 @@ string_flag( visibility = ["//visibility:public"], ) +config_setting( + name = "is_venvs_site_packages", + flag_values = { + ":venvs_site_packages": VenvsSitePackages.YES, + }, + # NOTE: Only public because it is used in whl_library repos. + visibility = ["//visibility:public"], +) + define_pypi_internal_flags( name = "define_pypi_internal_flags", ) diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 84e0535289..e9036c3013 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -18,6 +18,11 @@ package(default_visibility = ["//:__subpackages__"]) licenses(["notice"]) +exports_files( + srcs = ["namespace_pkg_tmpl.py"], + visibility = ["//visibility:public"], +) + filegroup( name = "distribution", srcs = glob( diff --git a/python/private/pypi/namespace_pkg_tmpl.py b/python/private/pypi/namespace_pkg_tmpl.py new file mode 100644 index 0000000000..a21b846e76 --- /dev/null +++ b/python/private/pypi/namespace_pkg_tmpl.py @@ -0,0 +1,2 @@ +# __path__ manipulation added by bazel-contrib/rules_python to support namespace pkgs. +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/python/private/pypi/namespace_pkgs.bzl b/python/private/pypi/namespace_pkgs.bzl new file mode 100644 index 0000000000..bf4689a5ea --- /dev/null +++ b/python/private/pypi/namespace_pkgs.bzl @@ -0,0 +1,83 @@ +"""Utilities to get where we should write namespace pkg paths.""" + +load("@bazel_skylib//rules:copy_file.bzl", "copy_file") + +_ext = struct( + py = ".py", + pyd = ".pyd", + so = ".so", + pyc = ".pyc", +) + +_TEMPLATE = Label("//python/private/pypi:namespace_pkg_tmpl.py") + +def _add_all(dirname, dirs): + dir_path = "." + for dir_name in dirname.split("/"): + dir_path = "{}/{}".format(dir_path, dir_name) + dirs[dir_path[2:]] = None + +def get_files(*, srcs, ignored_dirnames = [], root = None): + """Get the list of filenames to write the namespace pkg files. + + Args: + srcs: {type}`src` a list of files to be passed to {bzl:obj}`py_library` + as `srcs` and `data`. This is usually a result of a {obj}`glob`. + ignored_dirnames: {type}`str` a list of patterns to ignore. + root: {type}`str` the prefix to use as the root. + + Returns: + {type}`src` a list of paths to write the namespace pkg `__init__.py` file. + """ + dirs = {} + ignored = {i: None for i in ignored_dirnames} + + if root: + _add_all(root, ignored) + + for file in srcs: + dirname, _, filename = file.rpartition("/") + + if filename == "__init__.py": + ignored[dirname] = None + dirname, _, _ = dirname.rpartition("/") + elif filename.endswith(_ext.py): + pass + elif filename.endswith(_ext.pyc): + pass + elif filename.endswith(_ext.pyd): + pass + elif filename.endswith(_ext.so): + pass + else: + continue + + if dirname in dirs or not dirname: + continue + + _add_all(dirname, dirs) + + return sorted([d for d in dirs if d not in ignored]) + +def create_inits(**kwargs): + """Create init files and return the list to be included `py_library` srcs. + + Args: + **kwargs: passed to {obj}`get_files`. + + Returns: + {type}`list[str]` to be included as part of `py_library`. + """ + srcs = [] + for out in get_files(**kwargs): + src = "{}/__init__.py".format(out) + srcs.append(srcs) + + copy_file( + name = "_cp_{}_namespace".format(out), + src = _TEMPLATE, + out = src, + **kwargs + ) + + return srcs diff --git a/python/private/pypi/whl_installer/arguments.py b/python/private/pypi/whl_installer/arguments.py index ea609bef9d..57dae45ae9 100644 --- a/python/private/pypi/whl_installer/arguments.py +++ b/python/private/pypi/whl_installer/arguments.py @@ -57,11 +57,6 @@ def parser(**kwargs: Any) -> argparse.ArgumentParser: action="store", help="Additional data exclusion parameters to add to the pip packages BUILD file.", ) - parser.add_argument( - "--enable_implicit_namespace_pkgs", - action="store_true", - help="Disables conversion of implicit namespace packages into pkg-util style packages.", - ) parser.add_argument( "--environment", action="store", diff --git a/python/private/pypi/whl_installer/wheel_installer.py b/python/private/pypi/whl_installer/wheel_installer.py index 600d45f940..a6a9dd0429 100644 --- a/python/private/pypi/whl_installer/wheel_installer.py +++ b/python/private/pypi/whl_installer/wheel_installer.py @@ -27,7 +27,7 @@ from pip._vendor.packaging.utils import canonicalize_name -from python.private.pypi.whl_installer import arguments, namespace_pkgs, wheel +from python.private.pypi.whl_installer import arguments, wheel def _configure_reproducible_wheels() -> None: @@ -77,35 +77,10 @@ def _parse_requirement_for_extra( return None, None -def _setup_namespace_pkg_compatibility(wheel_dir: str) -> None: - """Converts native namespace packages to pkgutil-style packages - - Namespace packages can be created in one of three ways. They are detailed here: - https://packaging.python.org/guides/packaging-namespace-packages/#creating-a-namespace-package - - 'pkgutil-style namespace packages' (2) and 'pkg_resources-style namespace packages' (3) works in Bazel, but - 'native namespace packages' (1) do not. - - We ensure compatibility with Bazel of method 1 by converting them into method 2. - - Args: - wheel_dir: the directory of the wheel to convert - """ - - namespace_pkg_dirs = namespace_pkgs.implicit_namespace_packages( - wheel_dir, - ignored_dirnames=["%s/bin" % wheel_dir], - ) - - for ns_pkg_dir in namespace_pkg_dirs: - namespace_pkgs.add_pkgutil_style_namespace_pkg_init(ns_pkg_dir) - - def _extract_wheel( wheel_file: str, extras: Dict[str, Set[str]], enable_pipstar: bool, - enable_implicit_namespace_pkgs: bool, platforms: List[wheel.Platform], installation_dir: Path = Path("."), ) -> None: @@ -116,15 +91,11 @@ def _extract_wheel( installation_dir: the destination directory for installation of the wheel. extras: a list of extras to add as dependencies for the installed wheel enable_pipstar: if true, turns off certain operations. - enable_implicit_namespace_pkgs: if true, disables conversion of implicit namespace packages and will unzip as-is """ whl = wheel.Wheel(wheel_file) whl.unzip(installation_dir) - if not enable_implicit_namespace_pkgs: - _setup_namespace_pkg_compatibility(installation_dir) - metadata = { "entry_points": [ { @@ -168,7 +139,6 @@ def main() -> None: wheel_file=whl, extras=extras, enable_pipstar=args.enable_pipstar, - enable_implicit_namespace_pkgs=args.enable_implicit_namespace_pkgs, platforms=arguments.get_platforms(args), ) return diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 17ee3d3cfe..c271449b3d 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -173,9 +173,6 @@ def _parse_optional_attrs(rctx, args, extra_pip_args = None): json.encode(struct(arg = rctx.attr.pip_data_exclude)), ] - if rctx.attr.enable_implicit_namespace_pkgs: - args.append("--enable_implicit_namespace_pkgs") - env = {} if rctx.attr.environment != None: for key, value in rctx.attr.environment.items(): @@ -389,6 +386,8 @@ def _whl_library_impl(rctx): metadata_name = metadata.name, metadata_version = metadata.version, requires_dist = metadata.requires_dist, + # TODO @aignas 2025-05-17: maybe have a build flag for this instead + enable_implicit_namespace_pkgs = rctx.attr.enable_implicit_namespace_pkgs, # TODO @aignas 2025-04-14: load through the hub: annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), data_exclude = rctx.attr.pip_data_exclude, @@ -457,6 +456,8 @@ def _whl_library_impl(rctx): name = whl_path.basename, dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix), entry_points = entry_points, + # TODO @aignas 2025-05-17: maybe have a build flag for this instead + enable_implicit_namespace_pkgs = rctx.attr.enable_implicit_namespace_pkgs, # TODO @aignas 2025-04-14: load through the hub: dependencies = metadata["deps"], dependencies_by_platform = metadata["deps_by_platform"], @@ -580,7 +581,6 @@ attr makes `extra_pip_args` and `download_only` ignored.""", Label("//python/private/pypi/whl_installer:wheel.py"), Label("//python/private/pypi/whl_installer:wheel_installer.py"), Label("//python/private/pypi/whl_installer:arguments.py"), - Label("//python/private/pypi/whl_installer:namespace_pkgs.py"), ] + record_files.values(), ), "_rule_name": attr.string(default = "whl_library"), diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl index e0c03a1505..3529566c49 100644 --- a/python/private/pypi/whl_library_targets.bzl +++ b/python/private/pypi/whl_library_targets.bzl @@ -30,6 +30,7 @@ load( "WHEEL_FILE_IMPL_LABEL", "WHEEL_FILE_PUBLIC_LABEL", ) +load(":namespace_pkgs.bzl", "create_inits") load(":pep508_deps.bzl", "deps") def whl_library_targets_from_requires( @@ -113,6 +114,7 @@ def whl_library_targets( copy_executables = {}, entry_points = {}, native = native, + enable_implicit_namespace_pkgs = False, rules = struct( copy_file = copy_file, py_binary = py_binary, @@ -153,6 +155,8 @@ def whl_library_targets( data: {type}`list[str]` A list of labels to include as part of the `data` attribute in `py_library`. entry_points: {type}`dict[str, str]` The mapping between the script name and the python file to use. DEPRECATED. + enable_implicit_namespace_pkgs: {type}`boolean` generate __init__.py + files for namespace pkgs. native: {type}`native` The native struct for overriding in tests. rules: {type}`struct` A struct with references to rules for creating targets. """ @@ -293,6 +297,14 @@ def whl_library_targets( ) if hasattr(rules, "py_library"): + srcs = native.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, + ) + # NOTE: pyi files should probably be excluded because they're carried # by the pyi_srcs attribute. However, historical behavior included # them in data and some tools currently rely on that. @@ -309,23 +321,31 @@ def whl_library_targets( if item not in _data_exclude: _data_exclude.append(item) + data = data + native.glob( + ["site-packages/**/*"], + exclude = _data_exclude, + ) + + pyi_srcs = native.glob( + ["site-packages/**/*.pyi"], + allow_empty = True, + ) + + if enable_implicit_namespace_pkgs: + srcs = srcs + getattr(native, "select", select)({ + Label("//python/config_settings:is_venvs_site_packages"): [], + "//conditions:default": create_inits( + srcs = srcs + data + pyi_srcs, + ignore_dirnames = [], # If you need to ignore certain folders, you can patch rules_python here to do so. + root = "site-packages", + ), + }) + rules.py_library( name = py_library_label, - srcs = native.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, - ), - pyi_srcs = native.glob( - ["site-packages/**/*.pyi"], - allow_empty = True, - ), - data = data + native.glob( - ["site-packages/**/*"], - exclude = _data_exclude, - ), + srcs = srcs, + pyi_srcs = pyi_srcs, + data = data, # This makes this directory a top-level in the python import # search path for anything that depends on this. imports = ["site-packages"], diff --git a/tests/pypi/namespace_pkgs/BUILD.bazel b/tests/pypi/namespace_pkgs/BUILD.bazel new file mode 100644 index 0000000000..57f7962524 --- /dev/null +++ b/tests/pypi/namespace_pkgs/BUILD.bazel @@ -0,0 +1,5 @@ +load(":namespace_pkgs_tests.bzl", "namespace_pkgs_test_suite") + +namespace_pkgs_test_suite( + name = "namespace_pkgs_tests", +) diff --git a/tests/pypi/namespace_pkgs/namespace_pkgs_tests.bzl b/tests/pypi/namespace_pkgs/namespace_pkgs_tests.bzl new file mode 100644 index 0000000000..7ac938ff17 --- /dev/null +++ b/tests/pypi/namespace_pkgs/namespace_pkgs_tests.bzl @@ -0,0 +1,167 @@ +"" + +load("@rules_testing//lib:analysis_test.bzl", "test_suite") +load("//python/private/pypi:namespace_pkgs.bzl", "get_files") # buildifier: disable=bzl-visibility + +_tests = [] + +def test_in_current_dir(env): + srcs = [ + "foo/bar/biz.py", + "foo/bee/boo.py", + "foo/buu/__init__.py", + "foo/buu/bii.py", + ] + got = get_files(srcs = srcs) + expected = [ + "foo", + "foo/bar", + "foo/bee", + ] + env.expect.that_collection(got).contains_exactly(expected) + +_tests.append(test_in_current_dir) + +def test_find_correct_namespace_packages(env): + srcs = [ + "nested/root/foo/bar/biz.py", + "nested/root/foo/bee/boo.py", + "nested/root/foo/buu/__init__.py", + "nested/root/foo/buu/bii.py", + ] + + got = get_files(srcs = srcs, root = "nested/root") + expected = [ + "nested/root/foo", + "nested/root/foo/bar", + "nested/root/foo/bee", + ] + env.expect.that_collection(got).contains_exactly(expected) + +_tests.append(test_find_correct_namespace_packages) + +def test_ignores_empty_directories(_): + # because globs do not add directories, this test is not needed + pass + +_tests.append(test_ignores_empty_directories) + +def test_empty_case(env): + srcs = [ + "foo/__init__.py", + "foo/bar/__init__.py", + "foo/bar/biz.py", + ] + + got = get_files(srcs = srcs) + expected = [] + env.expect.that_collection(got).contains_exactly(expected) + +_tests.append(test_empty_case) + +def test_ignores_non_module_files_in_directories(env): + srcs = [ + "foo/__init__.pyi", + "foo/py.typed", + ] + + got = get_files(srcs = srcs) + expected = [] + env.expect.that_collection(got).contains_exactly(expected) + +_tests.append(test_ignores_non_module_files_in_directories) + +def test_parent_child_relationship_of_namespace_pkgs(env): + srcs = [ + "foo/bar/biff/my_module.py", + "foo/bar/biff/another_module.py", + ] + + got = get_files(srcs = srcs) + expected = [ + "foo", + "foo/bar", + "foo/bar/biff", + ] + env.expect.that_collection(got).contains_exactly(expected) + +_tests.append(test_parent_child_relationship_of_namespace_pkgs) + +def test_parent_child_relationship_of_namespace_and_standard_pkgs(env): + srcs = [ + "foo/bar/biff/__init__.py", + "foo/bar/biff/another_module.py", + ] + + got = get_files(srcs = srcs) + expected = [ + "foo", + "foo/bar", + ] + env.expect.that_collection(got).contains_exactly(expected) + +_tests.append(test_parent_child_relationship_of_namespace_and_standard_pkgs) + +def test_parent_child_relationship_of_namespace_and_nested_standard_pkgs(env): + srcs = [ + "foo/bar/__init__.py", + "foo/bar/biff/another_module.py", + "foo/bar/biff/__init__.py", + "foo/bar/boof/big_module.py", + "foo/bar/boof/__init__.py", + "fim/in_a_ns_pkg.py", + ] + + got = get_files(srcs = srcs) + expected = [ + "foo", + "fim", + ] + env.expect.that_collection(got).contains_exactly(expected) + +_tests.append(test_parent_child_relationship_of_namespace_and_nested_standard_pkgs) + +def test_recognized_all_nonstandard_module_types(env): + srcs = [ + "ayy/my_module.pyc", + "bee/ccc/dee/eee.so", + "eff/jee/aych.pyd", + ] + + expected = [ + "ayy", + "bee", + "bee/ccc", + "bee/ccc/dee", + "eff", + "eff/jee", + ] + got = get_files(srcs = srcs) + env.expect.that_collection(got).contains_exactly(expected) + +_tests.append(test_recognized_all_nonstandard_module_types) + +def test_skips_ignored_directories(env): + srcs = [ + "root/foo/boo/my_module.py", + "root/foo/bar/another_module.py", + ] + + expected = [ + "root/foo", + "root/foo/bar", + ] + got = get_files( + srcs = srcs, + ignored_dirnames = ["root/foo/boo"], + root = "root", + ) + env.expect.that_collection(got).contains_exactly(expected) + +_tests.append(test_skips_ignored_directories) + +def namespace_pkgs_test_suite(name): + test_suite( + name = name, + basic_tests = _tests, + ) diff --git a/tests/pypi/whl_installer/BUILD.bazel b/tests/pypi/whl_installer/BUILD.bazel index 040e4d765f..060d2bce62 100644 --- a/tests/pypi/whl_installer/BUILD.bazel +++ b/tests/pypi/whl_installer/BUILD.bazel @@ -16,17 +16,6 @@ py_test( ], ) -py_test( - name = "namespace_pkgs_test", - size = "small", - srcs = [ - "namespace_pkgs_test.py", - ], - deps = [ - ":lib", - ], -) - py_test( name = "platform_test", size = "small", diff --git a/tests/pypi/whl_installer/arguments_test.py b/tests/pypi/whl_installer/arguments_test.py index 5538054a59..2352d8e48b 100644 --- a/tests/pypi/whl_installer/arguments_test.py +++ b/tests/pypi/whl_installer/arguments_test.py @@ -36,7 +36,6 @@ def test_arguments(self) -> None: 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["extra_pip_args"], extra_pip_args) def test_deserialize_structured_args(self) -> None: diff --git a/tests/pypi/whl_installer/namespace_pkgs_test.py b/tests/pypi/whl_installer/namespace_pkgs_test.py deleted file mode 100644 index fbbd50926a..0000000000 --- a/tests/pypi/whl_installer/namespace_pkgs_test.py +++ /dev/null @@ -1,192 +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 os -import pathlib -import shutil -import tempfile -import unittest -from typing import Optional, Set - -from python.private.pypi.whl_installer import namespace_pkgs - - -class TempDir: - def __init__(self) -> None: - self.dir = tempfile.mkdtemp() - - def root(self) -> str: - return self.dir - - def add_dir(self, rel_path: str) -> None: - d = pathlib.Path(self.dir, rel_path) - d.mkdir(parents=True) - - def add_file(self, rel_path: str, contents: Optional[str] = None) -> None: - f = pathlib.Path(self.dir, rel_path) - f.parent.mkdir(parents=True, exist_ok=True) - if contents: - with open(str(f), "w") as writeable_f: - writeable_f.write(contents) - else: - f.touch() - - def remove(self) -> None: - shutil.rmtree(self.dir) - - -class TestImplicitNamespacePackages(unittest.TestCase): - def assertPathsEqual(self, actual: Set[pathlib.Path], expected: Set[str]) -> None: - self.assertEqual(actual, {pathlib.Path(p) for p in expected}) - - def test_in_current_directory(self) -> None: - directory = TempDir() - directory.add_file("foo/bar/biz.py") - directory.add_file("foo/bee/boo.py") - directory.add_file("foo/buu/__init__.py") - directory.add_file("foo/buu/bii.py") - cwd = os.getcwd() - os.chdir(directory.root()) - expected = { - "foo", - "foo/bar", - "foo/bee", - } - try: - actual = namespace_pkgs.implicit_namespace_packages(".") - self.assertPathsEqual(actual, expected) - finally: - os.chdir(cwd) - directory.remove() - - def test_finds_correct_namespace_packages(self) -> None: - directory = TempDir() - directory.add_file("foo/bar/biz.py") - directory.add_file("foo/bee/boo.py") - directory.add_file("foo/buu/__init__.py") - directory.add_file("foo/buu/bii.py") - - expected = { - directory.root() + "/foo", - directory.root() + "/foo/bar", - directory.root() + "/foo/bee", - } - actual = namespace_pkgs.implicit_namespace_packages(directory.root()) - self.assertPathsEqual(actual, expected) - - def test_ignores_empty_directories(self) -> None: - directory = TempDir() - directory.add_file("foo/bar/biz.py") - directory.add_dir("foo/cat") - - expected = { - directory.root() + "/foo", - directory.root() + "/foo/bar", - } - actual = namespace_pkgs.implicit_namespace_packages(directory.root()) - self.assertPathsEqual(actual, expected) - - def test_empty_case(self) -> None: - directory = TempDir() - directory.add_file("foo/__init__.py") - directory.add_file("foo/bar/__init__.py") - directory.add_file("foo/bar/biz.py") - - actual = namespace_pkgs.implicit_namespace_packages(directory.root()) - self.assertEqual(actual, set()) - - def test_ignores_non_module_files_in_directories(self) -> None: - directory = TempDir() - directory.add_file("foo/__init__.pyi") - directory.add_file("foo/py.typed") - - actual = namespace_pkgs.implicit_namespace_packages(directory.root()) - self.assertEqual(actual, set()) - - def test_parent_child_relationship_of_namespace_pkgs(self): - directory = TempDir() - directory.add_file("foo/bar/biff/my_module.py") - directory.add_file("foo/bar/biff/another_module.py") - - expected = { - directory.root() + "/foo", - directory.root() + "/foo/bar", - directory.root() + "/foo/bar/biff", - } - actual = namespace_pkgs.implicit_namespace_packages(directory.root()) - self.assertPathsEqual(actual, expected) - - def test_parent_child_relationship_of_namespace_and_standard_pkgs(self): - directory = TempDir() - directory.add_file("foo/bar/biff/__init__.py") - directory.add_file("foo/bar/biff/another_module.py") - - expected = { - directory.root() + "/foo", - directory.root() + "/foo/bar", - } - actual = namespace_pkgs.implicit_namespace_packages(directory.root()) - self.assertPathsEqual(actual, expected) - - def test_parent_child_relationship_of_namespace_and_nested_standard_pkgs(self): - directory = TempDir() - directory.add_file("foo/bar/__init__.py") - directory.add_file("foo/bar/biff/another_module.py") - directory.add_file("foo/bar/biff/__init__.py") - directory.add_file("foo/bar/boof/big_module.py") - directory.add_file("foo/bar/boof/__init__.py") - directory.add_file("fim/in_a_ns_pkg.py") - - expected = { - directory.root() + "/foo", - directory.root() + "/fim", - } - actual = namespace_pkgs.implicit_namespace_packages(directory.root()) - self.assertPathsEqual(actual, expected) - - def test_recognized_all_nonstandard_module_types(self): - directory = TempDir() - directory.add_file("ayy/my_module.pyc") - directory.add_file("bee/ccc/dee/eee.so") - directory.add_file("eff/jee/aych.pyd") - - expected = { - directory.root() + "/ayy", - directory.root() + "/bee", - directory.root() + "/bee/ccc", - directory.root() + "/bee/ccc/dee", - directory.root() + "/eff", - directory.root() + "/eff/jee", - } - actual = namespace_pkgs.implicit_namespace_packages(directory.root()) - self.assertPathsEqual(actual, expected) - - def test_skips_ignored_directories(self): - directory = TempDir() - directory.add_file("foo/boo/my_module.py") - directory.add_file("foo/bar/another_module.py") - - expected = { - directory.root() + "/foo", - directory.root() + "/foo/bar", - } - actual = namespace_pkgs.implicit_namespace_packages( - directory.root(), - ignored_dirnames=[directory.root() + "/foo/boo"], - ) - self.assertPathsEqual(actual, expected) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/pypi/whl_installer/wheel_installer_test.py b/tests/pypi/whl_installer/wheel_installer_test.py index ef5a2483ab..7040b0cfd8 100644 --- a/tests/pypi/whl_installer/wheel_installer_test.py +++ b/tests/pypi/whl_installer/wheel_installer_test.py @@ -70,7 +70,6 @@ def test_wheel_exists(self) -> None: Path(self.wheel_path), installation_dir=Path(self.wheel_dir), extras={}, - enable_implicit_namespace_pkgs=False, platforms=[], enable_pipstar=False, ) From c0415c67e6f9c0951176354e0256a55e85e475aa Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Wed, 28 May 2025 00:54:48 +0900 Subject: [PATCH 119/156] cleanup(pycross): remove the partially migrated code (#2906) The migration effort has stalled and we closed the initiative. #1360 --- MODULE.bazel | 1 - WORKSPACE | 13 +- python/private/internal_dev_deps.bzl | 12 -- ...d-new-file-for-testing-patch-support.patch | 17 -- tests/pycross/BUILD.bazel | 64 ------ .../pycross/patched_py_wheel_library_test.py | 40 ---- tests/pycross/py_wheel_library_test.py | 46 ---- third_party/rules_pycross/LICENSE | 201 ------------------ .../rules_pycross/pycross/private/BUILD.bazel | 14 -- .../pycross/private/providers.bzl | 32 --- .../pycross/private/tools/BUILD.bazel | 26 --- .../pycross/private/tools/wheel_installer.py | 196 ----------------- .../pycross/private/wheel_library.bzl | 174 --------------- 13 files changed, 1 insertion(+), 835 deletions(-) delete mode 100644 tests/pycross/0001-Add-new-file-for-testing-patch-support.patch delete mode 100644 tests/pycross/BUILD.bazel delete mode 100644 tests/pycross/patched_py_wheel_library_test.py delete mode 100644 tests/pycross/py_wheel_library_test.py delete mode 100644 third_party/rules_pycross/LICENSE delete mode 100644 third_party/rules_pycross/pycross/private/BUILD.bazel delete mode 100644 third_party/rules_pycross/pycross/private/providers.bzl delete mode 100644 third_party/rules_pycross/pycross/private/tools/BUILD.bazel delete mode 100644 third_party/rules_pycross/pycross/private/tools/wheel_installer.py delete mode 100644 third_party/rules_pycross/pycross/private/wheel_library.bzl diff --git a/MODULE.bazel b/MODULE.bazel index d0f7cc4afa..fa24ed04ba 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -102,7 +102,6 @@ use_repo( internal_dev_deps, "buildkite_config", "rules_python_runtime_env_tc_info", - "wheel_for_testing", ) # Add gazelle plugin so that we can run the gazelle example as an e2e integration diff --git a/WORKSPACE b/WORKSPACE index 3ad83ca04b..dddc5105ed 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -78,7 +78,7 @@ python_register_multi_toolchains( python_versions = PYTHON_VERSIONS, ) -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # Used for Bazel CI http_archive( @@ -155,14 +155,3 @@ pip_parse( load("@dev_pip//:requirements.bzl", docs_install_deps = "install_deps") docs_install_deps() - -# This wheel is purely here to validate the wheel extraction code. It's not -# intended for anything else. -http_file( - name = "wheel_for_testing", - downloaded_file_path = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", - sha256 = "0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2", - urls = [ - "https://files.pythonhosted.org/packages/50/67/3e966d99a07d60a21a21d7ec016e9e4c2642a86fea251ec68677daf71d4d/numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", - ], -) diff --git a/python/private/internal_dev_deps.bzl b/python/private/internal_dev_deps.bzl index 4f2cca0b42..600c934ace 100644 --- a/python/private/internal_dev_deps.bzl +++ b/python/private/internal_dev_deps.bzl @@ -14,23 +14,11 @@ """Module extension for internal dev_dependency=True setup.""" load("@bazel_ci_rules//:rbe_repo.bzl", "rbe_preconfig") -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") load(":runtime_env_repo.bzl", "runtime_env_repo") def _internal_dev_deps_impl(mctx): _ = mctx # @unused - # This wheel is purely here to validate the wheel extraction code. It's not - # intended for anything else. - http_file( - name = "wheel_for_testing", - downloaded_file_path = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", - sha256 = "0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2", - urls = [ - "https://files.pythonhosted.org/packages/50/67/3e966d99a07d60a21a21d7ec016e9e4c2642a86fea251ec68677daf71d4d/numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", - ], - ) - # Creates a default toolchain config for RBE. # Use this as is if you are using the rbe_ubuntu16_04 container, # otherwise refer to RBE docs. diff --git a/tests/pycross/0001-Add-new-file-for-testing-patch-support.patch b/tests/pycross/0001-Add-new-file-for-testing-patch-support.patch deleted file mode 100644 index fcbc3096ef..0000000000 --- a/tests/pycross/0001-Add-new-file-for-testing-patch-support.patch +++ /dev/null @@ -1,17 +0,0 @@ -From b2ebe6fe67ff48edaf2ae937d24b1f0b67c16f81 Mon Sep 17 00:00:00 2001 -From: Philipp Schrader -Date: Thu, 28 Sep 2023 09:02:44 -0700 -Subject: [PATCH] Add new file for testing patch support - ---- - site-packages/numpy/file_added_via_patch.txt | 1 + - 1 file changed, 1 insertion(+) - create mode 100644 site-packages/numpy/file_added_via_patch.txt - -diff --git a/site-packages/numpy/file_added_via_patch.txt b/site-packages/numpy/file_added_via_patch.txt -new file mode 100644 -index 0000000..9d947a4 ---- /dev/null -+++ b/site-packages/numpy/file_added_via_patch.txt -@@ -0,0 +1 @@ -+Hello from a patch! diff --git a/tests/pycross/BUILD.bazel b/tests/pycross/BUILD.bazel deleted file mode 100644 index e90b60e17e..0000000000 --- a/tests/pycross/BUILD.bazel +++ /dev/null @@ -1,64 +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. - -load("//python:py_test.bzl", "py_test") -load("//third_party/rules_pycross/pycross/private:wheel_library.bzl", "py_wheel_library") # buildifier: disable=bzl-visibility - -py_wheel_library( - name = "extracted_wheel_for_testing", - wheel = "@wheel_for_testing//file", -) - -py_test( - name = "py_wheel_library_test", - srcs = [ - "py_wheel_library_test.py", - ], - data = [ - ":extracted_wheel_for_testing", - ], - deps = [ - "//python/runfiles", - ], -) - -py_wheel_library( - name = "patched_extracted_wheel_for_testing", - patch_args = [ - "-p1", - ], - patch_tool = "patch", - patches = [ - "0001-Add-new-file-for-testing-patch-support.patch", - ], - target_compatible_with = select({ - # We don't have `patch` available on the Windows CI machines. - "@platforms//os:windows": ["@platforms//:incompatible"], - "//conditions:default": [], - }), - wheel = "@wheel_for_testing//file", -) - -py_test( - name = "patched_py_wheel_library_test", - srcs = [ - "patched_py_wheel_library_test.py", - ], - data = [ - ":patched_extracted_wheel_for_testing", - ], - deps = [ - "//python/runfiles", - ], -) diff --git a/tests/pycross/patched_py_wheel_library_test.py b/tests/pycross/patched_py_wheel_library_test.py deleted file mode 100644 index e1b404a0ef..0000000000 --- a/tests/pycross/patched_py_wheel_library_test.py +++ /dev/null @@ -1,40 +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 unittest -from pathlib import Path - -from python.runfiles import runfiles - -RUNFILES = runfiles.Create() - - -class TestPyWheelLibrary(unittest.TestCase): - def setUp(self): - self.extraction_dir = Path( - RUNFILES.Rlocation( - "rules_python/tests/pycross/patched_extracted_wheel_for_testing" - ) - ) - self.assertTrue(self.extraction_dir.exists(), self.extraction_dir) - self.assertTrue(self.extraction_dir.is_dir(), self.extraction_dir) - - def test_patched_file_contents(self): - """Validate that the patch got applied correctly.""" - file = self.extraction_dir / "site-packages/numpy/file_added_via_patch.txt" - self.assertEqual(file.read_text(), "Hello from a patch!\n") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/pycross/py_wheel_library_test.py b/tests/pycross/py_wheel_library_test.py deleted file mode 100644 index 25d896a1ae..0000000000 --- a/tests/pycross/py_wheel_library_test.py +++ /dev/null @@ -1,46 +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 unittest -from pathlib import Path - -from python.runfiles import runfiles - -RUNFILES = runfiles.Create() - - -class TestPyWheelLibrary(unittest.TestCase): - def setUp(self): - self.extraction_dir = Path( - RUNFILES.Rlocation("rules_python/tests/pycross/extracted_wheel_for_testing") - ) - self.assertTrue(self.extraction_dir.exists(), self.extraction_dir) - self.assertTrue(self.extraction_dir.is_dir(), self.extraction_dir) - - def test_file_presence(self): - """Validate that the basic file layout looks good.""" - for path in ( - "bin/f2py", - "site-packages/numpy.libs/libgfortran-daac5196.so.5.0.0", - "site-packages/numpy/dtypes.py", - "site-packages/numpy/core/_umath_tests.cpython-311-aarch64-linux-gnu.so", - ): - print(self.extraction_dir / path) - self.assertTrue( - (self.extraction_dir / path).exists(), f"{path} does not exist" - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/third_party/rules_pycross/LICENSE b/third_party/rules_pycross/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/third_party/rules_pycross/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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/third_party/rules_pycross/pycross/private/BUILD.bazel b/third_party/rules_pycross/pycross/private/BUILD.bazel deleted file mode 100644 index f59b087027..0000000000 --- a/third_party/rules_pycross/pycross/private/BUILD.bazel +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2023 Jeremy Volkman. All rights reserved. -# 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/third_party/rules_pycross/pycross/private/providers.bzl b/third_party/rules_pycross/pycross/private/providers.bzl deleted file mode 100644 index 47fc9f7271..0000000000 --- a/third_party/rules_pycross/pycross/private/providers.bzl +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2023 Jeremy Volkman. All rights reserved. -# 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. - -"""Python providers.""" - -PyWheelInfo = provider( - doc = "Information about a Python wheel.", - fields = { - "name_file": "File: A file containing the canonical name of the wheel.", - "wheel_file": "File: The wheel file itself.", - }, -) - -PyTargetEnvironmentInfo = provider( - doc = "A target environment description.", - fields = { - "file": "The JSON file containing target environment information.", - "python_compatible_with": "A list of constraints used to select this platform.", - }, -) diff --git a/third_party/rules_pycross/pycross/private/tools/BUILD.bazel b/third_party/rules_pycross/pycross/private/tools/BUILD.bazel deleted file mode 100644 index 41485c18a3..0000000000 --- a/third_party/rules_pycross/pycross/private/tools/BUILD.bazel +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2023 Jeremy Volkman. All rights reserved. -# 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("//python:defs.bzl", "py_binary") - -py_binary( - name = "wheel_installer", - srcs = ["wheel_installer.py"], - visibility = ["//visibility:public"], - deps = [ - "//python/private/pypi/whl_installer:lib", - "@pypi__installer//:lib", - ], -) diff --git a/third_party/rules_pycross/pycross/private/tools/wheel_installer.py b/third_party/rules_pycross/pycross/private/tools/wheel_installer.py deleted file mode 100644 index a122e67733..0000000000 --- a/third_party/rules_pycross/pycross/private/tools/wheel_installer.py +++ /dev/null @@ -1,196 +0,0 @@ -# Copyright 2023 Jeremy Volkman. All rights reserved. -# 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. - -""" -A tool that invokes pypa/build to build the given sdist tarball. -""" - -import argparse -import os -import shutil -import subprocess -import sys -import tempfile -from pathlib import Path -from typing import Any - -from installer import install -from installer.destinations import SchemeDictionaryDestination -from installer.sources import WheelFile - -from python.private.pypi.whl_installer import namespace_pkgs - - -def setup_namespace_pkg_compatibility(wheel_dir: Path) -> None: - """Converts native namespace packages to pkgutil-style packages - - Namespace packages can be created in one of three ways. They are detailed here: - https://packaging.python.org/guides/packaging-namespace-packages/#creating-a-namespace-package - - 'pkgutil-style namespace packages' (2) and 'pkg_resources-style namespace packages' (3) works in Bazel, but - 'native namespace packages' (1) do not. - - We ensure compatibility with Bazel of method 1 by converting them into method 2. - - Args: - wheel_dir: the directory of the wheel to convert - """ - - namespace_pkg_dirs = namespace_pkgs.implicit_namespace_packages( - str(wheel_dir), - ignored_dirnames=["%s/bin" % wheel_dir], - ) - - for ns_pkg_dir in namespace_pkg_dirs: - namespace_pkgs.add_pkgutil_style_namespace_pkg_init(ns_pkg_dir) - - -def main(args: Any) -> None: - dest_dir = args.directory - lib_dir = dest_dir / "site-packages" - destination = SchemeDictionaryDestination( - scheme_dict={ - "platlib": str(lib_dir), - "purelib": str(lib_dir), - "headers": str(dest_dir / "include"), - "scripts": str(dest_dir / "bin"), - "data": str(dest_dir / "data"), - }, - interpreter="/usr/bin/env python3", # Generic; it's not feasible to run these scripts directly. - script_kind="posix", - bytecode_optimization_levels=[0, 1], - ) - - link_dir = Path(tempfile.mkdtemp()) - if args.wheel_name_file: - with open(args.wheel_name_file, "r") as f: - wheel_name = f.read().strip() - else: - wheel_name = os.path.basename(args.wheel) - - link_path = link_dir / wheel_name - os.symlink(os.path.join(os.getcwd(), args.wheel), link_path) - - try: - with WheelFile.open(link_path) as source: - install( - source=source, - destination=destination, - # Additional metadata that is generated by the installation tool. - additional_metadata={ - "INSTALLER": b"https://github.com/bazel-contrib/rules_python/tree/main/third_party/rules_pycross", - }, - ) - finally: - shutil.rmtree(link_dir, ignore_errors=True) - - setup_namespace_pkg_compatibility(lib_dir) - - if args.patch: - if not args.patch_tool and not args.patch_tool_target: - raise ValueError("Specify one of 'patch_tool' or 'patch_tool_target'.") - - patch_args = [ - args.patch_tool or Path.cwd() / args.patch_tool_target - ] + args.patch_arg - for patch in args.patch: - with patch.open("r") as stdin: - try: - subprocess.run( - patch_args, - stdin=stdin, - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - cwd=args.directory, - ) - except subprocess.CalledProcessError as error: - print(f"Patch {patch} failed to apply:") - print(error.stdout.decode("utf-8")) - raise - - -def parse_flags(argv) -> Any: - parser = argparse.ArgumentParser(description="Extract a Python wheel.") - - parser.add_argument( - "--wheel", - type=Path, - required=True, - help="The wheel file path.", - ) - - parser.add_argument( - "--wheel-name-file", - type=Path, - required=False, - help="A file containing the canonical name of the wheel.", - ) - - parser.add_argument( - "--enable-implicit-namespace-pkgs", - action="store_true", - help="If true, disables conversion of implicit namespace packages and will unzip as-is.", - ) - - parser.add_argument( - "--directory", - type=Path, - help="The output path.", - ) - - parser.add_argument( - "--patch", - type=Path, - default=[], - action="append", - help="A patch file to apply.", - ) - - parser.add_argument( - "--patch-arg", - type=str, - default=[], - action="append", - help="An argument for the patch tool when applying the patches.", - ) - - parser.add_argument( - "--patch-tool", - type=str, - help=( - "The tool from PATH to invoke when applying patches. " - "If set, --patch-tool-target is ignored." - ), - ) - - parser.add_argument( - "--patch-tool-target", - type=Path, - help=( - "The path to the tool to invoke when applying patches. " - "Ignored when --patch-tool is set." - ), - ) - - return parser.parse_args(argv[1:]) - - -if __name__ == "__main__": - # When under `bazel run`, change to the actual working dir. - if "BUILD_WORKING_DIRECTORY" in os.environ: - os.chdir(os.environ["BUILD_WORKING_DIRECTORY"]) - - main(parse_flags(sys.argv)) diff --git a/third_party/rules_pycross/pycross/private/wheel_library.bzl b/third_party/rules_pycross/pycross/private/wheel_library.bzl deleted file mode 100644 index 00d85f71b1..0000000000 --- a/third_party/rules_pycross/pycross/private/wheel_library.bzl +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright 2023 Jeremy Volkman. All rights reserved. -# 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. - -"""Implementation of the py_wheel_library rule.""" - -load("@bazel_skylib//lib:paths.bzl", "paths") -load("//python:py_info.bzl", "PyInfo") -load(":providers.bzl", "PyWheelInfo") - -def _py_wheel_library_impl(ctx): - out = ctx.actions.declare_directory(ctx.attr.name) - - wheel_target = ctx.attr.wheel - if PyWheelInfo in wheel_target: - wheel_file = wheel_target[PyWheelInfo].wheel_file - name_file = wheel_target[PyWheelInfo].name_file - else: - wheel_file = ctx.file.wheel - name_file = None - - args = ctx.actions.args().use_param_file("--flagfile=%s") - args.add("--wheel", wheel_file) - args.add("--directory", out.path) - args.add_all(ctx.files.patches, format_each = "--patch=%s") - args.add_all(ctx.attr.patch_args, format_each = "--patch-arg=%s") - args.add("--patch-tool", ctx.attr.patch_tool) - - tools = [] - inputs = [wheel_file] + ctx.files.patches - if name_file: - inputs.append(name_file) - args.add("--wheel-name-file", name_file) - - if ctx.attr.patch_tool_target: - args.add("--patch-tool-target", ctx.attr.patch_tool_target.files_to_run.executable) - tools.append(ctx.executable.patch_tool_target) - - if ctx.attr.enable_implicit_namespace_pkgs: - args.add("--enable-implicit-namespace-pkgs") - - # We apply patches in the same action as the extraction to minimize the - # number of times we cache the wheel contents. If we were to split this - # into 2 actions, then the wheel contents would be cached twice. - ctx.actions.run( - inputs = inputs, - outputs = [out], - executable = ctx.executable._tool, - tools = tools, - arguments = [args], - # Set environment variables to make generated .pyc files reproducible. - env = { - "PYTHONHASHSEED": "0", - "SOURCE_DATE_EPOCH": "315532800", - }, - mnemonic = "WheelInstall", - progress_message = "Installing %s" % ctx.file.wheel.basename, - ) - - has_py2_only_sources = ctx.attr.python_version == "PY2" - has_py3_only_sources = ctx.attr.python_version == "PY3" - if not has_py2_only_sources: - for d in ctx.attr.deps: - if d[PyInfo].has_py2_only_sources: - has_py2_only_sources = True - break - if not has_py3_only_sources: - for d in ctx.attr.deps: - if d[PyInfo].has_py3_only_sources: - has_py3_only_sources = True - break - - # TODO: Is there a more correct way to get this runfiles-relative import path? - imp = paths.join( - ctx.label.repo_name or ctx.workspace_name, # Default to the local workspace. - ctx.label.package, - ctx.label.name, - "site-packages", # we put lib files in this subdirectory. - ) - - imports = depset( - direct = [imp], - transitive = [d[PyInfo].imports for d in ctx.attr.deps], - ) - transitive_sources = depset( - direct = [out], - transitive = [dep[PyInfo].transitive_sources for dep in ctx.attr.deps if PyInfo in dep], - ) - runfiles = ctx.runfiles(files = [out]) - for d in ctx.attr.deps: - runfiles = runfiles.merge(d[DefaultInfo].default_runfiles) - - return [ - DefaultInfo( - files = depset(direct = [out]), - runfiles = runfiles, - ), - PyInfo( - has_py2_only_sources = has_py2_only_sources, - has_py3_only_sources = has_py3_only_sources, - imports = imports, - transitive_sources = transitive_sources, - uses_shared_libraries = True, # Docs say this is unused - ), - ] - -py_wheel_library = rule( - implementation = _py_wheel_library_impl, - attrs = { - "deps": attr.label_list( - doc = "A list of this wheel's Python library dependencies.", - providers = [DefaultInfo, PyInfo], - ), - "enable_implicit_namespace_pkgs": attr.bool( - default = True, - doc = """ -If true, disables conversion of native namespace packages into pkg-util style namespace packages. When set all py_binary -and py_test targets must specify either `legacy_create_init=False` or the global Bazel option -`--incompatible_default_to_explicit_init_py` to prevent `__init__.py` being automatically generated in every directory. -This option is required to support some packages which cannot handle the conversion to pkg-util style. - """, - ), - "patch_args": attr.string_list( - default = ["-p0"], - doc = - "The arguments given to the patch tool. Defaults to -p0, " + - "however -p1 will usually be needed for patches generated by " + - "git. If multiple -p arguments are specified, the last one will take effect.", - ), - "patch_tool": attr.string( - doc = "The patch(1) utility from the host to use. " + - "If set, overrides `patch_tool_target`. Please note that setting " + - "this means that builds are not completely hermetic.", - ), - "patch_tool_target": attr.label( - executable = True, - cfg = "exec", - doc = "The label of the patch(1) utility to use. " + - "Only used if `patch_tool` is not set.", - ), - "patches": attr.label_list( - allow_files = True, - default = [], - doc = - "A list of files that are to be applied as patches after " + - "extracting the archive. This will use the patch command line tool.", - ), - "python_version": attr.string( - doc = "The python version required for this wheel ('PY2' or 'PY3')", - values = ["PY2", "PY3", ""], - ), - "wheel": attr.label( - doc = "The wheel file.", - allow_single_file = [".whl"], - mandatory = True, - ), - "_tool": attr.label( - default = Label("//third_party/rules_pycross/pycross/private/tools:wheel_installer"), - cfg = "exec", - executable = True, - ), - }, -) From 369ca91fe346a7dac760a883d36352510eac8f1d Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Wed, 28 May 2025 09:50:03 +0900 Subject: [PATCH 120/156] refactor(pypi): return a list from parse_requirements (#2931) The modeling of the data structures returned by the `parse_requirements` function was not optimal and this was because historically there was more logic in the `extension.bzl` and more things were decided there. With the recent refactors it is possible to have a harder to misuse data structure from the `parse_requirements`. For each `package` we will return a struct which will have a `srcs` field that will contain easy to consume values. With this in place we can do the fix that is outlined in the referenced issue. Work towards #2648 --- python/private/pypi/extension.bzl | 172 +++---- python/private/pypi/parse_requirements.bzl | 92 +++- python/private/pypi/pip_repository.bzl | 6 +- .../parse_requirements_tests.bzl | 485 ++++++++++-------- 4 files changed, 424 insertions(+), 331 deletions(-) diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index d3a15dfc44..b79be6e038 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -202,8 +202,12 @@ def _create_whl_repos( logger = logger, ) - for whl_name, requirements in requirements_by_platform.items(): - group_name = whl_group_mapping.get(whl_name) + exposed_packages = {} + for whl in requirements_by_platform: + if whl.is_exposed: + exposed_packages[whl.name] = None + + group_name = whl_group_mapping.get(whl.name) group_deps = requirement_cycles.get(group_name, []) # Construct args separately so that the lock file can be smaller and does not include unused @@ -214,7 +218,7 @@ def _create_whl_repos( maybe_args = dict( # The following values are safe to omit if they have false like values add_libdir_to_library_search_path = pip_attr.add_libdir_to_library_search_path, - annotation = whl_modifications.get(whl_name), + annotation = whl_modifications.get(whl.name), download_only = pip_attr.download_only, enable_implicit_namespace_pkgs = pip_attr.enable_implicit_namespace_pkgs, environment = pip_attr.environment, @@ -226,7 +230,7 @@ def _create_whl_repos( python_interpreter_target = python_interpreter_target, whl_patches = { p: json.encode(args) - for p, args in whl_overrides.get(whl_name, {}).items() + for p, args in whl_overrides.get(whl.name, {}).items() }, ) if not enable_pipstar: @@ -245,119 +249,99 @@ def _create_whl_repos( if v != default }) - for requirement in requirements: - for repo_name, (args, config_setting) in _whl_repos( - requirement = requirement, + for src in whl.srcs: + repo = _whl_repo( + src = src, whl_library_args = whl_library_args, download_only = pip_attr.download_only, netrc = pip_attr.netrc, auth_patterns = pip_attr.auth_patterns, python_version = major_minor, - multiple_requirements_for_whl = len(requirements) > 1., + is_multiple_versions = whl.is_multiple_versions, enable_pipstar = enable_pipstar, - ).items(): - repo_name = "{}_{}".format(pip_name, repo_name) - if repo_name in whl_libraries: - fail("Attempting to creating a duplicate library {} for {}".format( - repo_name, - whl_name, - )) + ) - whl_libraries[repo_name] = args - whl_map.setdefault(whl_name, {})[config_setting] = repo_name + repo_name = "{}_{}".format(pip_name, repo.repo_name) + if repo_name in whl_libraries: + fail("Attempting to creating a duplicate library {} for {}".format( + repo_name, + whl.name, + )) + + whl_libraries[repo_name] = repo.args + whl_map.setdefault(whl.name, {})[repo.config_setting] = repo_name return struct( whl_map = whl_map, - exposed_packages = { - whl_name: None - for whl_name, requirements in requirements_by_platform.items() - if len([r for r in requirements if r.is_exposed]) > 0 - }, + exposed_packages = exposed_packages, extra_aliases = extra_aliases, whl_libraries = whl_libraries, ) -def _whl_repos(*, requirement, whl_library_args, download_only, netrc, auth_patterns, multiple_requirements_for_whl = False, python_version, enable_pipstar = False): - ret = {} - - dists = requirement.whls - if not download_only and requirement.sdist: - dists = dists + [requirement.sdist] - - for distribution in dists: - args = dict(whl_library_args) - if netrc: - args["netrc"] = netrc - if auth_patterns: - args["auth_patterns"] = auth_patterns - - if not distribution.filename.endswith(".whl"): - # pip is not used to download wheels and the python - # `whl_library` helpers are only extracting things, however - # for sdists, they will be built by `pip`, so we still - # need to pass the extra args there. - args["extra_pip_args"] = requirement.extra_pip_args - - # This is no-op because pip is not used to download the wheel. - args.pop("download_only", None) - - args["requirement"] = requirement.line - args["urls"] = [distribution.url] - args["sha256"] = distribution.sha256 - args["filename"] = distribution.filename - if not enable_pipstar: - args["experimental_target_platforms"] = [ - # Get rid of the version fot the target platforms because we are - # passing the interpreter any way. Ideally we should search of ways - # how to pass the target platforms through the hub repo. - p.partition("_")[2] - for p in requirement.target_platforms - ] - - # Pure python wheels or sdists may need to have a platform here - target_platforms = None - if distribution.filename.endswith(".whl") and not distribution.filename.endswith("-any.whl"): - pass - elif multiple_requirements_for_whl: - target_platforms = requirement.target_platforms - - repo_name = whl_repo_name( - distribution.filename, - distribution.sha256, - ) - ret[repo_name] = ( - args, - whl_config_setting( +def _whl_repo(*, src, whl_library_args, is_multiple_versions, download_only, netrc, auth_patterns, python_version, enable_pipstar = False): + args = dict(whl_library_args) + args["requirement"] = src.requirement_line + is_whl = src.filename.endswith(".whl") + + if src.extra_pip_args and not is_whl: + # pip is not used to download wheels and the python + # `whl_library` helpers are only extracting things, however + # for sdists, they will be built by `pip`, so we still + # need to pass the extra args there, so only pop this for whls + args["extra_pip_args"] = src.extra_pip_args + + if not src.url or (not is_whl and download_only): + # Fallback to a pip-installed wheel + target_platforms = src.target_platforms if is_multiple_versions else [] + return struct( + repo_name = pypi_repo_name( + normalize_name(src.distribution), + *target_platforms + ), + args = args, + config_setting = whl_config_setting( version = python_version, - filename = distribution.filename, - target_platforms = target_platforms, + target_platforms = target_platforms or None, ), ) - if ret: - return ret - - # Fallback to a pip-installed wheel - args = dict(whl_library_args) # make a copy - args["requirement"] = requirement.line - if requirement.extra_pip_args: - args["extra_pip_args"] = requirement.extra_pip_args + # This is no-op because pip is not used to download the wheel. + args.pop("download_only", None) + + if netrc: + args["netrc"] = netrc + if auth_patterns: + args["auth_patterns"] = auth_patterns + + args["urls"] = [src.url] + args["sha256"] = src.sha256 + args["filename"] = src.filename + if not enable_pipstar: + args["experimental_target_platforms"] = [ + # Get rid of the version fot the target platforms because we are + # passing the interpreter any way. Ideally we should search of ways + # how to pass the target platforms through the hub repo. + p.partition("_")[2] + for p in src.target_platforms + ] + + # Pure python wheels or sdists may need to have a platform here + target_platforms = None + if is_whl and not src.filename.endswith("-any.whl"): + pass + elif is_multiple_versions: + target_platforms = src.target_platforms - target_platforms = requirement.target_platforms if multiple_requirements_for_whl else [] - repo_name = pypi_repo_name( - normalize_name(requirement.distribution), - *target_platforms - ) - ret[repo_name] = ( - args, - whl_config_setting( + return struct( + repo_name = whl_repo_name(src.filename, src.sha256), + args = args, + config_setting = whl_config_setting( version = python_version, - target_platforms = target_platforms or None, + filename = src.filename, + target_platforms = target_platforms, ), ) - return ret - def parse_modules( module_ctx, _fail = fail, diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index bdfac46ed6..bd2981efc0 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -179,49 +179,91 @@ def parse_requirements( }), ) - ret = {} - for whl_name, reqs in sorted(requirements_by_platform.items()): + ret = [] + for name, reqs in sorted(requirements_by_platform.items()): requirement_target_platforms = {} for r in reqs.values(): target_platforms = env_marker_target_platforms.get(r.requirement_line, r.target_platforms) for p in target_platforms: requirement_target_platforms[p] = None - is_exposed = len(requirement_target_platforms) == len(requirements) - if not is_exposed and logger: + item = struct( + # Return normalized names + name = normalize_name(name), + is_exposed = len(requirement_target_platforms) == len(requirements), + is_multiple_versions = len(reqs.values()) > 1, + srcs = _package_srcs( + name = name, + reqs = reqs, + index_urls = index_urls, + env_marker_target_platforms = env_marker_target_platforms, + extract_url_srcs = extract_url_srcs, + logger = logger, + ), + ) + ret.append(item) + if not item.is_exposed and logger: logger.debug(lambda: "Package '{}' will not be exposed because it is only present on a subset of platforms: {} out of {}".format( - whl_name, + name, sorted(requirement_target_platforms), sorted(requirements), )) - # Return normalized names - ret_requirements = ret.setdefault(normalize_name(whl_name), []) + if logger: + logger.debug(lambda: "Will configure whl repos: {}".format([w.name for w in ret])) - for r in sorted(reqs.values(), key = lambda r: r.requirement_line): - whls, sdist = _add_dists( - requirement = r, - index_urls = index_urls.get(whl_name), - logger = logger, - ) + return ret - target_platforms = env_marker_target_platforms.get(r.requirement_line, r.target_platforms) - ret_requirements.append( +def _package_srcs( + *, + name, + reqs, + index_urls, + logger, + env_marker_target_platforms, + extract_url_srcs): + """A function to return sources for a particular package.""" + srcs = [] + for r in sorted(reqs.values(), key = lambda r: r.requirement_line): + whls, sdist = _add_dists( + requirement = r, + index_urls = index_urls.get(name), + logger = logger, + ) + + target_platforms = env_marker_target_platforms.get(r.requirement_line, r.target_platforms) + target_platforms = sorted(target_platforms) + + all_dists = [] + whls + if sdist: + all_dists.append(sdist) + + if extract_url_srcs and all_dists: + req_line = r.srcs.requirement + else: + all_dists = [struct( + url = "", + filename = "", + sha256 = "", + yanked = False, + )] + req_line = r.srcs.requirement_line + + for dist in all_dists: + srcs.append( struct( - distribution = r.distribution, - line = r.srcs.requirement if extract_url_srcs and (whls or sdist) else r.srcs.requirement_line, - target_platforms = sorted(target_platforms), + distribution = name, extra_pip_args = r.extra_pip_args, - whls = whls, - sdist = sdist, - is_exposed = is_exposed, + requirement_line = req_line, + target_platforms = target_platforms, + filename = dist.filename, + sha256 = dist.sha256, + url = dist.url, + yanked = dist.yanked, ), ) - if logger: - logger.debug(lambda: "Will configure whl repos: {}".format(ret.keys())) - - return ret + return srcs def select_requirement(requirements, *, platform): """A simple function to get a requirement for a particular platform. diff --git a/python/private/pypi/pip_repository.bzl b/python/private/pypi/pip_repository.bzl index c8d23f471f..724fb6ddba 100644 --- a/python/private/pypi/pip_repository.bzl +++ b/python/private/pypi/pip_repository.bzl @@ -94,15 +94,15 @@ def _pip_repository_impl(rctx): selected_requirements = {} options = None repository_platform = host_platform(rctx) - for name, requirements in requirements_by_platform.items(): + for whl in requirements_by_platform: requirement = select_requirement( - requirements, + whl.srcs, platform = None if rctx.attr.download_only else repository_platform, ) if not requirement: continue options = options or requirement.extra_pip_args - selected_requirements[name] = requirement.line + selected_requirements[whl.name] = requirement.requirement_line bzl_packages = sorted(selected_requirements.keys()) diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index 497e08361f..926a7e0c50 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -100,22 +100,28 @@ def _test_simple(env): "requirements_lock": ["linux_x86_64", "windows_x86_64"], }, ) - env.expect.that_dict(got).contains_exactly({ - "foo": [ - struct( - distribution = "foo", - extra_pip_args = [], - sdist = None, - is_exposed = True, - line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", - target_platforms = [ - "linux_x86_64", - "windows_x86_64", - ], - whls = [], - ), - ], - }) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", + target_platforms = [ + "linux_x86_64", + "windows_x86_64", + ], + url = "", + filename = "", + sha256 = "", + yanked = False, + ), + ], + ), + ]) _tests.append(_test_simple) @@ -127,24 +133,25 @@ def _test_direct_urls_integration(env): "requirements_direct": ["linux_x86_64"], }, ) - env.expect.that_dict(got).contains_exactly({ - "foo": [ - struct( - distribution = "foo", - extra_pip_args = [], - sdist = None, - is_exposed = True, - line = "foo[extra]", - target_platforms = ["linux_x86_64"], - whls = [struct( + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[extra]", + target_platforms = ["linux_x86_64"], url = "https://some-url/package.whl", filename = "package.whl", sha256 = "", yanked = False, - )], - ), - ], - }) + ), + ], + ), + ]) _tests.append(_test_direct_urls_integration) @@ -156,21 +163,27 @@ def _test_extra_pip_args(env): }, extra_pip_args = ["--trusted-host=example.org"], ) - env.expect.that_dict(got).contains_exactly({ - "foo": [ - struct( - distribution = "foo", - extra_pip_args = ["--index-url=example.org", "--trusted-host=example.org"], - sdist = None, - is_exposed = True, - line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", - target_platforms = [ - "linux_x86_64", - ], - whls = [], - ), - ], - }) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = ["--index-url=example.org", "--trusted-host=example.org"], + requirement_line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", + target_platforms = [ + "linux_x86_64", + ], + url = "", + filename = "", + sha256 = "", + yanked = False, + ), + ], + ), + ]) _tests.append(_test_extra_pip_args) @@ -181,19 +194,25 @@ def _test_dupe_requirements(env): "requirements_lock_dupe": ["linux_x86_64"], }, ) - env.expect.that_dict(got).contains_exactly({ - "foo": [ - struct( - distribution = "foo", - extra_pip_args = [], - sdist = None, - is_exposed = True, - line = "foo[extra,extra_2]==0.0.1 --hash=sha256:deadbeef", - target_platforms = ["linux_x86_64"], - whls = [], - ), - ], - }) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[extra,extra_2]==0.0.1 --hash=sha256:deadbeef", + target_platforms = ["linux_x86_64"], + url = "", + filename = "", + sha256 = "", + yanked = False, + ), + ], + ), + ]) _tests.append(_test_dupe_requirements) @@ -206,44 +225,57 @@ def _test_multi_os(env): }, ) - env.expect.that_dict(got).contains_exactly({ - "bar": [ - struct( - distribution = "bar", - extra_pip_args = [], - line = "bar==0.0.1 --hash=sha256:deadb00f", - target_platforms = ["windows_x86_64"], - whls = [], - sdist = None, - is_exposed = False, - ), - ], - "foo": [ - struct( - distribution = "foo", - extra_pip_args = [], - line = "foo==0.0.3 --hash=sha256:deadbaaf", - target_platforms = ["linux_x86_64"], - whls = [], - sdist = None, - is_exposed = True, - ), - struct( - distribution = "foo", - extra_pip_args = [], - line = "foo[extra]==0.0.2 --hash=sha256:deadbeef", - target_platforms = ["windows_x86_64"], - whls = [], - sdist = None, - is_exposed = True, - ), - ], - }) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "bar", + is_exposed = False, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "bar", + extra_pip_args = [], + requirement_line = "bar==0.0.1 --hash=sha256:deadb00f", + target_platforms = ["windows_x86_64"], + url = "", + filename = "", + sha256 = "", + yanked = False, + ), + ], + ), + struct( + name = "foo", + is_exposed = True, + is_multiple_versions = True, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.3 --hash=sha256:deadbaaf", + target_platforms = ["linux_x86_64"], + url = "", + filename = "", + sha256 = "", + yanked = False, + ), + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[extra]==0.0.2 --hash=sha256:deadbeef", + target_platforms = ["windows_x86_64"], + url = "", + filename = "", + sha256 = "", + yanked = False, + ), + ], + ), + ]) env.expect.that_str( select_requirement( - got["foo"], + got[1].srcs, platform = "windows_x86_64", - ).line, + ).requirement_line, ).equals("foo[extra]==0.0.2 --hash=sha256:deadbeef") _tests.append(_test_multi_os) @@ -257,39 +289,52 @@ def _test_multi_os_legacy(env): }, ) - env.expect.that_dict(got).contains_exactly({ - "bar": [ - struct( - distribution = "bar", - extra_pip_args = ["--platform=manylinux_2_17_x86_64", "--python-version=39", "--implementation=cp", "--abi=cp39"], - is_exposed = False, - sdist = None, - line = "bar==0.0.1 --hash=sha256:deadb00f", - target_platforms = ["cp39_linux_x86_64"], - whls = [], - ), - ], - "foo": [ - struct( - distribution = "foo", - extra_pip_args = ["--platform=manylinux_2_17_x86_64", "--python-version=39", "--implementation=cp", "--abi=cp39"], - is_exposed = True, - sdist = None, - line = "foo==0.0.1 --hash=sha256:deadbeef", - target_platforms = ["cp39_linux_x86_64"], - whls = [], - ), - struct( - distribution = "foo", - extra_pip_args = ["--platform=macosx_10_9_arm64", "--python-version=39", "--implementation=cp", "--abi=cp39"], - is_exposed = True, - sdist = None, - line = "foo==0.0.3 --hash=sha256:deadbaaf", - target_platforms = ["cp39_osx_aarch64"], - whls = [], - ), - ], - }) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "bar", + is_exposed = False, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "bar", + extra_pip_args = ["--platform=manylinux_2_17_x86_64", "--python-version=39", "--implementation=cp", "--abi=cp39"], + requirement_line = "bar==0.0.1 --hash=sha256:deadb00f", + target_platforms = ["cp39_linux_x86_64"], + url = "", + filename = "", + sha256 = "", + yanked = False, + ), + ], + ), + struct( + name = "foo", + is_exposed = True, + is_multiple_versions = True, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = ["--platform=manylinux_2_17_x86_64", "--python-version=39", "--implementation=cp", "--abi=cp39"], + requirement_line = "foo==0.0.1 --hash=sha256:deadbeef", + target_platforms = ["cp39_linux_x86_64"], + url = "", + filename = "", + sha256 = "", + yanked = False, + ), + struct( + distribution = "foo", + extra_pip_args = ["--platform=macosx_10_9_arm64", "--python-version=39", "--implementation=cp", "--abi=cp39"], + requirement_line = "foo==0.0.3 --hash=sha256:deadbaaf", + target_platforms = ["cp39_osx_aarch64"], + url = "", + filename = "", + sha256 = "", + yanked = False, + ), + ], + ), + ]) _tests.append(_test_multi_os_legacy) @@ -324,30 +369,42 @@ def _test_env_marker_resolution(env): }, evaluate_markers = _mock_eval_markers, ) - env.expect.that_dict(got).contains_exactly({ - "bar": [ - struct( - distribution = "bar", - extra_pip_args = [], - is_exposed = True, - sdist = None, - line = "bar==0.0.1 --hash=sha256:deadbeef", - target_platforms = ["cp311_linux_super_exotic", "cp311_windows_x86_64"], - whls = [], - ), - ], - "foo": [ - struct( - distribution = "foo", - extra_pip_args = [], - is_exposed = False, - sdist = None, - line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", - target_platforms = ["cp311_windows_x86_64"], - whls = [], - ), - ], - }) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "bar", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "bar", + extra_pip_args = [], + requirement_line = "bar==0.0.1 --hash=sha256:deadbeef", + target_platforms = ["cp311_linux_super_exotic", "cp311_windows_x86_64"], + url = "", + filename = "", + sha256 = "", + yanked = False, + ), + ], + ), + struct( + name = "foo", + is_exposed = False, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", + target_platforms = ["cp311_windows_x86_64"], + url = "", + filename = "", + sha256 = "", + yanked = False, + ), + ], + ), + ]) _tests.append(_test_env_marker_resolution) @@ -358,28 +415,35 @@ def _test_different_package_version(env): "requirements_different_package_version": ["linux_x86_64"], }, ) - env.expect.that_dict(got).contains_exactly({ - "foo": [ - struct( - distribution = "foo", - extra_pip_args = [], - is_exposed = True, - sdist = None, - line = "foo==0.0.1 --hash=sha256:deadb00f", - target_platforms = ["linux_x86_64"], - whls = [], - ), - struct( - distribution = "foo", - extra_pip_args = [], - is_exposed = True, - sdist = None, - line = "foo==0.0.1+local --hash=sha256:deadbeef", - target_platforms = ["linux_x86_64"], - whls = [], - ), - ], - }) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + is_exposed = True, + is_multiple_versions = True, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1 --hash=sha256:deadb00f", + target_platforms = ["linux_x86_64"], + url = "", + filename = "", + sha256 = "", + yanked = False, + ), + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1+local --hash=sha256:deadbeef", + target_platforms = ["linux_x86_64"], + url = "", + filename = "", + sha256 = "", + yanked = False, + ), + ], + ), + ]) _tests.append(_test_different_package_version) @@ -390,38 +454,35 @@ def _test_optional_hash(env): "requirements_optional_hash": ["linux_x86_64"], }, ) - env.expect.that_dict(got).contains_exactly({ - "foo": [ - struct( - distribution = "foo", - extra_pip_args = [], - sdist = None, - is_exposed = True, - line = "foo==0.0.4", - target_platforms = ["linux_x86_64"], - whls = [struct( + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + is_exposed = True, + is_multiple_versions = True, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.4", + target_platforms = ["linux_x86_64"], url = "https://example.org/foo-0.0.4.whl", filename = "foo-0.0.4.whl", sha256 = "", yanked = False, - )], - ), - struct( - distribution = "foo", - extra_pip_args = [], - sdist = None, - is_exposed = True, - line = "foo==0.0.5", - target_platforms = ["linux_x86_64"], - whls = [struct( + ), + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.5", + target_platforms = ["linux_x86_64"], url = "https://example.org/foo-0.0.5.whl", filename = "foo-0.0.5.whl", sha256 = "deadbeef", yanked = False, - )], - ), - ], - }) + ), + ], + ), + ]) _tests.append(_test_optional_hash) @@ -432,19 +493,25 @@ def _test_git_sources(env): "requirements_git": ["linux_x86_64"], }, ) - env.expect.that_dict(got).contains_exactly({ - "foo": [ - struct( - distribution = "foo", - extra_pip_args = [], - is_exposed = True, - sdist = None, - line = "foo @ git+https://github.com/org/foo.git@deadbeef", - target_platforms = ["linux_x86_64"], - whls = [], - ), - ], - }) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo @ git+https://github.com/org/foo.git@deadbeef", + target_platforms = ["linux_x86_64"], + url = "", + filename = "", + sha256 = "", + yanked = False, + ), + ], + ), + ]) _tests.append(_test_git_sources) From 3464c14c36e5d20a56e61952c5e06ef608aa0ed9 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Wed, 28 May 2025 22:53:50 +0900 Subject: [PATCH 121/156] fix: symlink root-level python files to the venv (#2908) As found in #2882 testing, packages like `typing-extensions` which have `.py` files at the root of the `site-packages` folder don't work and it seems that the comment about `rules_python` being too eager is only half-correct. Since `namespace_pkgs` are no longer there, we can just include all of the files and if there are collisions, they will be highlighted as build errors. Now the following works: ``` bazel build //docs --@rules_python//python/config_settings:venvs_site_packages=yes ``` Work towards #2156 --- .bazelrc | 4 +-- python/private/py_library.bzl | 19 +++++----- tests/modules/other/nspkg_single/BUILD.bazel | 10 ++++++ .../nspkg_single/site-packages/__init__.py | 1 + .../nspkg_single/site-packages/single_file.py | 5 +++ tests/venv_site_packages_libs/BUILD.bazel | 1 + tests/venv_site_packages_libs/bin.py | 1 + .../nspkg_alpha/BUILD.bazel | 2 +- .../venv_site_packages_pypi_test.py | 36 ------------------- 9 files changed, 32 insertions(+), 47 deletions(-) create mode 100644 tests/modules/other/nspkg_single/BUILD.bazel create mode 100644 tests/modules/other/nspkg_single/site-packages/__init__.py create mode 100644 tests/modules/other/nspkg_single/site-packages/single_file.py delete mode 100644 tests/venv_site_packages_libs/venv_site_packages_pypi_test.py diff --git a/.bazelrc b/.bazelrc index 4e6f2fa187..7e744fb67a 100644 --- a/.bazelrc +++ b/.bazelrc @@ -4,8 +4,8 @@ # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it) # To update these lines, execute # `bazel run @rules_bazel_integration_test//tools:update_deleted_packages` -build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma -query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma +build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma,tests/modules/other/nspkg_single +query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma,tests/modules/other/nspkg_single test --test_output=errors diff --git a/python/private/py_library.bzl b/python/private/py_library.bzl index bf0c25439e..fd9dad9f20 100644 --- a/python/private/py_library.bzl +++ b/python/private/py_library.bzl @@ -253,6 +253,7 @@ def _get_site_packages_symlinks(ctx): repo_runfiles_dirname = None dirs_with_init = {} # dirname -> runfile path + site_packages_symlinks = [] for src in ctx.files.srcs: if src.extension not in PYTHON_FILE_EXTENSIONS: continue @@ -261,16 +262,19 @@ def _get_site_packages_symlinks(ctx): continue path = path.removeprefix(site_packages_root) dir_name, _, filename = path.rpartition("/") - if not dir_name: - # This would be e.g. `site-packages/__init__.py`, which isn't valid - # because it's not within a directory for an importable Python package. - # However, the pypi integration over-eagerly adds a pkgutil-style - # __init__.py file during the repo phase. Just ignore them for now. - continue - if filename.startswith("__init__."): + if dir_name and filename.startswith("__init__."): dirs_with_init[dir_name] = None repo_runfiles_dirname = runfiles_root_path(ctx, src.short_path).partition("/")[0] + elif not dir_name: + repo_runfiles_dirname = runfiles_root_path(ctx, src.short_path).partition("/")[0] + + # This would be files that do not have directories and we just need to add + # direct symlinks to them as is: + site_packages_symlinks.append(( + paths.join(repo_runfiles_dirname, site_packages_root, filename), + filename, + )) # Sort so that we encounter `foo` before `foo/bar`. This ensures we # see the top-most explicit package first. @@ -286,7 +290,6 @@ def _get_site_packages_symlinks(ctx): if not is_sub_package: first_level_explicit_packages.append(d) - site_packages_symlinks = [] for dirname in first_level_explicit_packages: site_packages_symlinks.append(( paths.join(repo_runfiles_dirname, site_packages_root, dirname), diff --git a/tests/modules/other/nspkg_single/BUILD.bazel b/tests/modules/other/nspkg_single/BUILD.bazel new file mode 100644 index 0000000000..08cb4f373e --- /dev/null +++ b/tests/modules/other/nspkg_single/BUILD.bazel @@ -0,0 +1,10 @@ +load("@rules_python//python:py_library.bzl", "py_library") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "nspkg_single", + srcs = glob(["site-packages/**/*.py"]), + experimental_venvs_site_packages = "@rules_python//python/config_settings:venvs_site_packages", + imports = [package_name() + "/site-packages"], +) diff --git a/tests/modules/other/nspkg_single/site-packages/__init__.py b/tests/modules/other/nspkg_single/site-packages/__init__.py new file mode 100644 index 0000000000..bb26c87599 --- /dev/null +++ b/tests/modules/other/nspkg_single/site-packages/__init__.py @@ -0,0 +1 @@ +# empty, will not be added to the site-packages dir diff --git a/tests/modules/other/nspkg_single/site-packages/single_file.py b/tests/modules/other/nspkg_single/site-packages/single_file.py new file mode 100644 index 0000000000..f6d7dfd640 --- /dev/null +++ b/tests/modules/other/nspkg_single/site-packages/single_file.py @@ -0,0 +1,5 @@ +__all__ = [ + "SOMETHING", +] + +SOMETHING = "nothing" diff --git a/tests/venv_site_packages_libs/BUILD.bazel b/tests/venv_site_packages_libs/BUILD.bazel index 1f48331ff2..d5a4fe6750 100644 --- a/tests/venv_site_packages_libs/BUILD.bazel +++ b/tests/venv_site_packages_libs/BUILD.bazel @@ -13,5 +13,6 @@ py_reconfig_test( "//tests/venv_site_packages_libs/nspkg_beta", "@other//nspkg_delta", "@other//nspkg_gamma", + "@other//nspkg_single", ], ) diff --git a/tests/venv_site_packages_libs/bin.py b/tests/venv_site_packages_libs/bin.py index b944be69e3..58572a2a1e 100644 --- a/tests/venv_site_packages_libs/bin.py +++ b/tests/venv_site_packages_libs/bin.py @@ -26,6 +26,7 @@ def test_imported_from_venv(self): self.assert_imported_from_venv("nspkg.subnspkg.beta") self.assert_imported_from_venv("nspkg.subnspkg.gamma") self.assert_imported_from_venv("nspkg.subnspkg.delta") + self.assert_imported_from_venv("single_file") if __name__ == "__main__": diff --git a/tests/venv_site_packages_libs/nspkg_alpha/BUILD.bazel b/tests/venv_site_packages_libs/nspkg_alpha/BUILD.bazel index c40c3b4080..aec415f7a0 100644 --- a/tests/venv_site_packages_libs/nspkg_alpha/BUILD.bazel +++ b/tests/venv_site_packages_libs/nspkg_alpha/BUILD.bazel @@ -1,4 +1,4 @@ -load("@rules_python//python:py_library.bzl", "py_library") +load("//python:py_library.bzl", "py_library") package(default_visibility = ["//visibility:public"]) diff --git a/tests/venv_site_packages_libs/venv_site_packages_pypi_test.py b/tests/venv_site_packages_libs/venv_site_packages_pypi_test.py deleted file mode 100644 index 519b258044..0000000000 --- a/tests/venv_site_packages_libs/venv_site_packages_pypi_test.py +++ /dev/null @@ -1,36 +0,0 @@ -import os -import sys -import unittest - - -class VenvSitePackagesLibraryTest(unittest.TestCase): - def test_imported_from_venv(self): - self.assertNotEqual(sys.prefix, sys.base_prefix, "Not running under a venv") - venv = sys.prefix - - from nspkg.subnspkg import alpha - - self.assertEqual(alpha.whoami, "alpha") - self.assertEqual(alpha.__name__, "nspkg.subnspkg.alpha") - - self.assertTrue( - alpha.__file__.startswith(sys.prefix), - f"\nalpha was imported, not from within the venv.\n" - + f"venv : {venv}\n" - + f"actual: {alpha.__file__}", - ) - - from nspkg.subnspkg import beta - - self.assertEqual(beta.whoami, "beta") - self.assertEqual(beta.__name__, "nspkg.subnspkg.beta") - self.assertTrue( - beta.__file__.startswith(sys.prefix), - f"\nbeta was imported, not from within the venv.\n" - + f"venv : {venv}\n" - + f"actual: {beta.__file__}", - ) - - -if __name__ == "__main__": - unittest.main() From 0d203a95d9ba6ec3365119fc709dc9eb3885f6d7 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Thu, 29 May 2025 11:27:28 +0900 Subject: [PATCH 122/156] docs: split PyPI docs up and add more (#2935) Summary: - Split the PyPI docs per topic. - Move everything to its own folder. - Separate the `bzlmod` and `WORKSPACE` documentation. Some of the features are only available in `bzlmod` and since `bzlmod` is the future having that as the default makes things a little easier. - Fix a few warnings. Fixes #2810. --- CONTRIBUTING.md | 2 +- MODULE.bazel | 1 + docs/BUILD.bazel | 3 + docs/conf.py | 4 + docs/getting-started.md | 10 +- docs/index.md | 3 +- docs/pip.md | 4 - docs/pypi-dependencies.md | 519 ------------------ docs/pypi/circular-dependencies.md | 82 +++ docs/pypi/download-workspace.md | 107 ++++ docs/pypi/download.md | 302 ++++++++++ docs/pypi/index.md | 27 + docs/pypi/lock.md | 46 ++ docs/pypi/patch.md | 10 + docs/pypi/use.md | 133 +++++ docs/requirements.txt | 1 - python/private/pypi/BUILD.bazel | 1 + python/private/pypi/pkg_aliases.bzl | 5 +- python/private/pypi/simpleapi_download.bzl | 1 + python/private/pypi/whl_config_setting.bzl | 8 +- sphinxdocs/inventories/bazel_inventory.txt | 4 + .../simpleapi_download_tests.bzl | 5 + 22 files changed, 740 insertions(+), 538 deletions(-) delete mode 100644 docs/pip.md delete mode 100644 docs/pypi-dependencies.md create mode 100644 docs/pypi/circular-dependencies.md create mode 100644 docs/pypi/download-workspace.md create mode 100644 docs/pypi/download.md create mode 100644 docs/pypi/index.md create mode 100644 docs/pypi/lock.md create mode 100644 docs/pypi/patch.md create mode 100644 docs/pypi/use.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b087119dc6..324801cfc3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,7 +68,7 @@ to the actual rules_python project and begin the code review process. ## Developer guide For more more details, guidance, and tips for working with the code base, -see [DEVELOPING.md](DEVELOPING.md) +see [docs/devguide.md](./devguide) ## Formatting diff --git a/MODULE.bazel b/MODULE.bazel index fa24ed04ba..d3a95350e5 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -134,6 +134,7 @@ dev_pip.parse( download_only = True, experimental_index_url = "https://pypi.org/simple", hub_name = "dev_pip", + parallel_download = False, python_version = "3.11", requirements_lock = "//docs:requirements.txt", ) diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index b3e5f52022..852c4d4fa6 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -120,7 +120,10 @@ sphinx_stardocs( "//python/private:rule_builders_bzl", "//python/private/api:py_common_api_bzl", "//python/private/pypi:config_settings_bzl", + "//python/private/pypi:env_marker_info_bzl", "//python/private/pypi:pkg_aliases_bzl", + "//python/private/pypi:whl_config_setting_bzl", + "//python/private/pypi:whl_library_bzl", "//python/uv:lock_bzl", "//python/uv:uv_bzl", "//python/uv:uv_toolchain_bzl", diff --git a/docs/conf.py b/docs/conf.py index 96bbdb50ab..1d9f526b93 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -91,6 +91,8 @@ "api/sphinxdocs/private/sphinx_docs_library": "/api/sphinxdocs/sphinxdocs/private/sphinx_docs_library.html", "api/sphinxdocs/sphinx_docs_library": "/api/sphinxdocs/sphinxdocs/sphinx_docs_library.html", "api/sphinxdocs/inventories/index": "/api/sphinxdocs/sphinxdocs/inventories/index.html", + "pip.html": "pypi/index.html", + "pypi-dependencies.html": "pypi/index.html", } # Adapted from the template code: @@ -139,7 +141,9 @@ # --- Extlinks configuration extlinks = { + "gh-issue": (f"https://github.com/bazel-contrib/rules_python/issues/%s", "#%s issue"), "gh-path": (f"https://github.com/bazel-contrib/rules_python/tree/main/%s", "%s"), + "gh-pr": (f"https://github.com/bazel-contrib/rules_python/pulls/%s", "#%s PR"), } # --- MyST configuration diff --git a/docs/getting-started.md b/docs/getting-started.md index 60d5d5e0be..7e7b88aa8a 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -8,13 +8,13 @@ It assumes you have a `requirements.txt` file with your PyPI dependencies. For more details information about configuring `rules_python`, see: * [Configuring the runtime](configuring-toolchains) -* [Configuring third party dependencies (pip/pypi)](pypi-dependencies) +* [Configuring third party dependencies (pip/pypi)](./pypi/index) * [API docs](api/index) -## Using bzlmod +## Including dependencies -The first step to using rules_python with bzlmod is to add the dependency to -your MODULE.bazel file: +The first step to using `rules_python` is to add the dependency to +your `MODULE.bazel` file: ```starlark # Update the version "0.0.0" to the release found here: @@ -30,7 +30,7 @@ pip.parse( use_repo(pip, "pypi") ``` -## Using a WORKSPACE file +### Using a WORKSPACE file Using WORKSPACE is deprecated, but still supported, and a bit more involved than using Bzlmod. Here is a simplified setup to download the prebuilt runtimes. diff --git a/docs/index.md b/docs/index.md index 4983a6a029..82023f3ad8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -95,9 +95,8 @@ See {gh-path}`Bzlmod support ` for any behaviour differences :hidden: self getting-started -pypi-dependencies +pypi/index Toolchains -pip coverage precompiling gazelle diff --git a/docs/pip.md b/docs/pip.md deleted file mode 100644 index 43d8fc4978..0000000000 --- a/docs/pip.md +++ /dev/null @@ -1,4 +0,0 @@ -(pip-integration)= -# Pip Integration - -See [PyPI dependencies](./pypi-dependencies). diff --git a/docs/pypi-dependencies.md b/docs/pypi-dependencies.md deleted file mode 100644 index b3ae7fe594..0000000000 --- a/docs/pypi-dependencies.md +++ /dev/null @@ -1,519 +0,0 @@ -:::{default-domain} bzl -::: - -# Using dependencies from PyPI - -Using PyPI packages (aka "pip install") involves two main steps. - -1. [Generating requirements file](#generating-requirements-file) -2. [Installing third party packages](#installing-third-party-packages) -3. [Using third party packages as dependencies](#using-third-party-packages) - -{#generating-requirements-file} -## Generating requirements file - -Generally, when working on a Python project, you'll have some dependencies that themselves have other dependencies. You might also specify dependency bounds instead of specific versions. So you'll need to generate a full list of all transitive dependencies and pinned versions for every dependency. - -Typically, you'd have your dependencies specified in `pyproject.toml` or `requirements.in` and generate the full pinned list of dependencies in `requirements_lock.txt`, which you can manage with the `compile_pip_requirements` Bazel rule: - -```starlark -load("@rules_python//python:pip.bzl", "compile_pip_requirements") - -compile_pip_requirements( - name = "requirements", - src = "requirements.in", - requirements_txt = "requirements_lock.txt", -) -``` - -This rule generates two targets: -- `bazel run [name].update` will regenerate the `requirements_txt` file -- `bazel test [name]_test` will test that the `requirements_txt` file is up to date - -For more documentation, see the API docs under {obj}`@rules_python//python:pip.bzl`. - -Once you generate this fully specified list of requirements, you can install the requirements with the instructions in [Installing third party packages](#installing-third-party-packages). - -:::{warning} -If you're specifying dependencies in `pyproject.toml`, make sure to include the `[build-system]` configuration, with pinned dependencies. `compile_pip_requirements` will use the build system specified to read your project's metadata, and you might see non-hermetic behavior if you don't pin the build system. - -Not specifying `[build-system]` at all will result in using a default `[build-system]` configuration, which uses unpinned versions ([ref](https://peps.python.org/pep-0518/#build-system-table)). -::: - -{#installing-third-party-packages} -## Installing third party packages - -### Using bzlmod - -To add pip dependencies to your `MODULE.bazel` file, use the `pip.parse` -extension, and call it to create the central external repo and individual wheel -external repos. Include in the `MODULE.bazel` the toolchain extension as shown -in the first bzlmod example above. - -```starlark -pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") -pip.parse( - hub_name = "my_deps", - python_version = "3.11", - requirements_lock = "//:requirements_lock_3_11.txt", -) -use_repo(pip, "my_deps") -``` -For more documentation, see the bzlmod examples under the {gh-path}`examples` folder or the documentation -for the {obj}`@rules_python//python/extensions:pip.bzl` extension. - -```{note} -We are using a host-platform compatible toolchain by default to setup pip dependencies. -During the setup phase, we create some symlinks, which may be inefficient on Windows -by default. In that case use the following `.bazelrc` options to improve performance if -you have admin privileges: - - startup --windows_enable_symlinks - -This will enable symlinks on Windows and help with bootstrap performance of setting up the -hermetic host python interpreter on this platform. Linux and OSX users should see no -difference. -``` - -### Using a WORKSPACE file - -To add pip dependencies to your `WORKSPACE`, load the `pip_parse` function and -call it to create the central external repo and individual wheel external repos. - -```starlark -load("@rules_python//python:pip.bzl", "pip_parse") - -# Create a central repo that knows about the dependencies needed from -# requirements_lock.txt. -pip_parse( - name = "my_deps", - requirements_lock = "//path/to:requirements_lock.txt", -) -# Load the starlark macro, which will define your dependencies. -load("@my_deps//:requirements.bzl", "install_deps") -# Call it to define repos for your requirements. -install_deps() -``` - -(vendoring-requirements)= -#### Vendoring the requirements.bzl file - -In some cases you may not want to generate the requirements.bzl file as a repository rule -while Bazel is fetching dependencies. For example, if you produce a reusable Bazel module -such as a ruleset, you may want to include the requirements.bzl file rather than make your users -install the WORKSPACE setup to generate it. -See https://github.com/bazel-contrib/rules_python/issues/608 - -This is the same workflow as Gazelle, which creates `go_repository` rules with -[`update-repos`](https://github.com/bazelbuild/bazel-gazelle#update-repos) - -To do this, use the "write to source file" pattern documented in -https://blog.aspect.dev/bazel-can-write-to-the-source-folder -to put a copy of the generated requirements.bzl into your project. -Then load the requirements.bzl file directly rather than from the generated repository. -See the example in rules_python/examples/pip_parse_vendored. - -(per-os-arch-requirements)= -### Requirements for a specific OS/Architecture - -In some cases you may need to use different requirements files for different OS, Arch combinations. This is enabled via the `requirements_by_platform` attribute in `pip.parse` extension and the `pip_parse` repository rule. The keys of the dictionary are labels to the file and the values are a list of comma separated target (os, arch) tuples. - -For example: -```starlark - # ... - requirements_by_platform = { - "requirements_linux_x86_64.txt": "linux_x86_64", - "requirements_osx.txt": "osx_*", - "requirements_linux_exotic.txt": "linux_exotic", - "requirements_some_platforms.txt": "linux_aarch64,windows_*", - }, - # For the list of standard platforms that the rules_python has toolchains for, default to - # the following requirements file. - requirements_lock = "requirements_lock.txt", -``` - -In case of duplicate platforms, `rules_python` will raise an error as there has -to be unambiguous mapping of the requirement files to the (os, arch) tuples. - -An alternative way is to use per-OS requirement attributes. -```starlark - # ... - requirements_windows = "requirements_windows.txt", - requirements_darwin = "requirements_darwin.txt", - # For the remaining platforms (which is basically only linux OS), use this file. - requirements_lock = "requirements_lock.txt", -) -``` - -### pip rules - -Note that since `pip_parse` and `pip.parse` are executed at evaluation time, -Bazel has no information about the Python toolchain and cannot enforce that the -interpreter used to invoke `pip` matches the interpreter used to run -`py_binary` targets. By default, `pip_parse` uses the system command -`"python3"`. To override this, pass in the `python_interpreter` attribute or -`python_interpreter_target` attribute to `pip_parse`. The `pip.parse` `bzlmod` extension -by default uses the hermetic python toolchain for the host platform. - -You can have multiple `pip_parse`s in the same workspace, or use the pip -extension multiple times when using bzlmod. This configuration will create -multiple external repos that have no relation to one another and may result in -downloading the same wheels numerous times. - -As with any repository rule, if you would like to ensure that `pip_parse` is -re-executed to pick up a non-hermetic change to your environment (e.g., updating -your system `python` interpreter), you can force it to re-execute by running -`bazel sync --only [pip_parse name]`. - -{#using-third-party-packages} -## Using third party packages as dependencies - -Each extracted wheel repo contains a `py_library` target representing -the wheel's contents. There are two ways to access this library. The -first uses the `requirement()` function defined in the central -repo's `//:requirements.bzl` file. This function maps a pip package -name to a label: - -```starlark -load("@my_deps//:requirements.bzl", "requirement") - -py_library( - name = "mylib", - srcs = ["mylib.py"], - deps = [ - ":myotherlib", - requirement("some_pip_dep"), - requirement("another_pip_dep"), - ] -) -``` - -The reason `requirement()` exists is to insulate from -changes to the underlying repository and label strings. However, those -labels have become directly used, so aren't able to easily change regardless. - -On the other hand, using `requirement()` has several drawbacks; see -[this issue][requirements-drawbacks] for an enumeration. If you don't -want to use `requirement()`, you can use the library -labels directly instead. For `pip_parse`, the labels are of the following form: - -```starlark -@{name}//{package} -``` - -Here `name` is the `name` attribute that was passed to `pip_parse` and -`package` is the pip package name with characters that are illegal in -Bazel label names (e.g. `-`, `.`) replaced with `_`. If you need to -update `name` from "old" to "new", then you can run the following -buildozer command: - -```shell -buildozer 'substitute deps @old//([^/]+) @new//${1}' //...:* -``` - -[requirements-drawbacks]: https://github.com/bazel-contrib/rules_python/issues/414 - -### Entry points - -If you would like to access [entry points][whl_ep], see the `py_console_script_binary` rule documentation, -which can help you create a `py_binary` target for a particular console script exposed by a package. - -[whl_ep]: https://packaging.python.org/specifications/entry-points/ - -### 'Extras' dependencies - -Any 'extras' specified in the requirements lock file will be automatically added -as transitive dependencies of the package. In the example above, you'd just put -`requirement("useful_dep")` or `@pypi//useful_dep`. - -### Consuming Wheel Dists Directly - -If you need to depend on the wheel dists themselves, for instance, to pass them -to some other packaging tool, you can get a handle to them with the -`whl_requirement` macro. For example: - -```starlark -load("@pypi//:requirements.bzl", "whl_requirement") - -filegroup( - name = "whl_files", - data = [ - # This is equivalent to "@pypi//boto3:whl" - whl_requirement("boto3"), - ] -) -``` - -### Creating a filegroup of files within a whl - -The rule {obj}`whl_filegroup` exists as an easy way to extract the necessary files -from a whl file without the need to modify the `BUILD.bazel` contents of the -whl repositories generated via `pip_repository`. Use it similarly to the `filegroup` -above. See the API docs for more information. - -(advance-topics)= -## Advanced topics - -(circular-deps)= -### Circular dependencies - -Sometimes PyPi packages contain dependency cycles -- for instance a particular -version `sphinx` (this is no longer the case in the latest version as of -2024-06-02) depends on `sphinxcontrib-serializinghtml`. When using them as -`requirement()`s, ala - -``` -py_binary( - name = "doctool", - ... - deps = [ - requirement("sphinx"), - ], -) -``` - -Bazel will protest because it doesn't support cycles in the build graph -- - -``` -ERROR: .../external/pypi_sphinxcontrib_serializinghtml/BUILD.bazel:44:6: in alias rule @pypi_sphinxcontrib_serializinghtml//:pkg: cycle in dependency graph: - //:doctool (...) - @pypi//sphinxcontrib_serializinghtml:pkg (...) -.-> @pypi_sphinxcontrib_serializinghtml//:pkg (...) -| @pypi_sphinxcontrib_serializinghtml//:_pkg (...) -| @pypi_sphinx//:pkg (...) -| @pypi_sphinx//:_pkg (...) -`-- @pypi_sphinxcontrib_serializinghtml//:pkg (...) -``` - -The `experimental_requirement_cycles` argument allows you to work around these -issues by specifying groups of packages which form cycles. `pip_parse` will -transparently fix the cycles for you and provide the cyclic dependencies -simultaneously. - -```starlark -pip_parse( - ... - experimental_requirement_cycles = { - "sphinx": [ - "sphinx", - "sphinxcontrib-serializinghtml", - ] - }, -) -``` - -`pip_parse` supports fixing multiple cycles simultaneously, however cycles must -be distinct. `apache-airflow` for instance has dependency cycles with a number -of its optional dependencies, which means those optional dependencies must all -be a part of the `airflow` cycle. For instance -- - -```starlark -pip_parse( - ... - experimental_requirement_cycles = { - "airflow": [ - "apache-airflow", - "apache-airflow-providers-common-sql", - "apache-airflow-providers-postgres", - "apache-airflow-providers-sqlite", - ] - } -) -``` - -Alternatively, one could resolve the cycle by removing one leg of it. - -For example while `apache-airflow-providers-sqlite` is "baked into" the Airflow -package, `apache-airflow-providers-postgres` is not and is an optional feature. -Rather than listing `apache-airflow[postgres]` in your `requirements.txt` which -would expose a cycle via the extra, one could either _manually_ depend on -`apache-airflow` and `apache-airflow-providers-postgres` separately as -requirements. Bazel rules which need only `apache-airflow` can take it as a -dependency, and rules which explicitly want to mix in -`apache-airflow-providers-postgres` now can. - -Alternatively, one could use `rules_python`'s patching features to remove one -leg of the dependency manually. For instance by making -`apache-airflow-providers-postgres` not explicitly depend on `apache-airflow` or -perhaps `apache-airflow-providers-common-sql`. - - -### Multi-platform support - -Multi-platform support of cross-building the wheels can be done in two ways - either -using {bzl:attr}`experimental_index_url` for the {bzl:obj}`pip.parse` bzlmod tag class -or by using the {bzl:attr}`pip.parse.download_only` setting. In this section we -are going to outline quickly how one can use the latter option. - -Let's say you have 2 requirements files: -``` -# requirements.linux_x86_64.txt ---platform=manylinux_2_17_x86_64 ---python-version=39 ---implementation=cp ---abi=cp39 - -foo==0.0.1 --hash=sha256:deadbeef -bar==0.0.1 --hash=sha256:deadb00f -``` - -``` -# requirements.osx_aarch64.txt contents ---platform=macosx_10_9_arm64 ---python-version=39 ---implementation=cp ---abi=cp39 - -foo==0.0.3 --hash=sha256:deadbaaf -``` - -With these 2 files your {bzl:obj}`pip.parse` could look like: -``` -pip.parse( - hub_name = "pip", - python_version = "3.9", - # Tell `pip` to ignore sdists - download_only = True, - requirements_by_platform = { - "requirements.linux_x86_64.txt": "linux_x86_64", - "requirements.osx_aarch64.txt": "osx_aarch64", - }, -) -``` - -With this, the `pip.parse` will create a hub repository that is going to -support only two platforms - `cp39_osx_aarch64` and `cp39_linux_x86_64` and it -will only use `wheels` and ignore any sdists that it may find on the PyPI -compatible indexes. - -```{note} -This is only supported on `bzlmd`. -``` - - - -(bazel-downloader)= -### Bazel downloader and multi-platform wheel hub repository. - -The `bzlmod` `pip.parse` call supports pulling information from `PyPI` (or a -compatible mirror) and it will ensure that the [bazel -downloader][bazel_downloader] is used for downloading the wheels. This allows -the users to use the [credential helper](#credential-helper) to authenticate -with the mirror and it also ensures that the distribution downloads are cached. -It also avoids using `pip` altogether and results in much faster dependency -fetching. - -This can be enabled by `experimental_index_url` and related flags as shown in -the {gh-path}`examples/bzlmod/MODULE.bazel` example. - -When using this feature during the `pip` extension evaluation you will see the accessed indexes similar to below: -```console -Loading: 0 packages loaded - currently loading: docs/ - Fetching module extension pip in @@//python/extensions:pip.bzl; starting - Fetching https://pypi.org/simple/twine/ -``` - -This does not mean that `rules_python` is fetching the wheels eagerly, but it -rather means that it is calling the PyPI server to get the Simple API response -to get the list of all available source and wheel distributions. Once it has -got all of the available distributions, it will select the right ones depending -on the `sha256` values in your `requirements_lock.txt` file. If `sha256` hashes -are not present in the requirements file, we will fallback to matching by version -specified in the lock file. The compatible distribution URLs will be then -written to the `MODULE.bazel.lock` file. Currently users wishing to use the -lock file with `rules_python` with this feature have to set an environment -variable `RULES_PYTHON_OS_ARCH_LOCK_FILE=0` which will become default in the -next release. - -Fetching the distribution information from the PyPI allows `rules_python` to -know which `whl` should be used on which target platform and it will determine -that by parsing the `whl` filename based on [PEP600], [PEP656] standards. This -allows the user to configure the behaviour by using the following publicly -available flags: -* {obj}`--@rules_python//python/config_settings:py_linux_libc` for selecting the Linux libc variant. -* {obj}`--@rules_python//python/config_settings:pip_whl` for selecting `whl` distribution preference. -* {obj}`--@rules_python//python/config_settings:pip_whl_osx_arch` for selecting MacOS wheel preference. -* {obj}`--@rules_python//python/config_settings:pip_whl_glibc_version` for selecting the GLIBC version compatibility. -* {obj}`--@rules_python//python/config_settings:pip_whl_muslc_version` for selecting the musl version compatibility. -* {obj}`--@rules_python//python/config_settings:pip_whl_osx_version` for selecting MacOS version compatibility. - -[bazel_downloader]: https://bazel.build/rules/lib/builtins/repository_ctx#download -[pep600]: https://peps.python.org/pep-0600/ -[pep656]: https://peps.python.org/pep-0656/ - -(credential-helper)= -### Credential Helper - -The "use Bazel downloader for python wheels" experimental feature includes support for the Bazel -[Credential Helper][cred-helper-design]. - -Your python artifact registry may provide a credential helper for you. Refer to your index's docs -to see if one is provided. - -See the [Credential Helper Spec][cred-helper-spec] for details. - -[cred-helper-design]: https://github.com/bazelbuild/proposals/blob/main/designs/2022-06-07-bazel-credential-helpers.md -[cred-helper-spec]: https://github.com/EngFlow/credential-helper-spec/blob/main/spec.md - - -#### Basic Example: - -The simplest form of a credential helper is a bash script that accepts an arg and spits out JSON to -stdout. For a service like Google Artifact Registry that uses ['Basic' HTTP Auth][rfc7617] and does -not provide a credential helper that conforms to the [spec][cred-helper-spec], the script might -look like: - -```bash -#!/bin/bash -# cred_helper.sh -ARG=$1 # but we don't do anything with it as it's always "get" - -# formatting is optional -echo '{' -echo ' "headers": {' -echo ' "Authorization": ["Basic dGVzdDoxMjPCow=="]' -echo ' }' -echo '}' -``` - -Configure Bazel to use this credential helper for your python index `example.com`: - -``` -# .bazelrc -build --credential_helper=example.com=/full/path/to/cred_helper.sh -``` - -Bazel will call this file like `cred_helper.sh get` and use the returned JSON to inject headers -into whatever HTTP(S) request it performs against `example.com`. - -[rfc7617]: https://datatracker.ietf.org/doc/html/rfc7617 - - diff --git a/docs/pypi/circular-dependencies.md b/docs/pypi/circular-dependencies.md new file mode 100644 index 0000000000..d22f5b36a7 --- /dev/null +++ b/docs/pypi/circular-dependencies.md @@ -0,0 +1,82 @@ +:::{default-domain} bzl +::: + +# Circular dependencies + +Sometimes PyPi packages contain dependency cycles -- for instance a particular +version `sphinx` (this is no longer the case in the latest version as of +2024-06-02) depends on `sphinxcontrib-serializinghtml`. When using them as +`requirement()`s, ala + +```starlark +py_binary( + name = "doctool", + ... + deps = [ + requirement("sphinx"), + ], +) +``` + +Bazel will protest because it doesn't support cycles in the build graph -- + +``` +ERROR: .../external/pypi_sphinxcontrib_serializinghtml/BUILD.bazel:44:6: in alias rule @pypi_sphinxcontrib_serializinghtml//:pkg: cycle in dependency graph: + //:doctool (...) + @pypi//sphinxcontrib_serializinghtml:pkg (...) +.-> @pypi_sphinxcontrib_serializinghtml//:pkg (...) +| @pypi_sphinxcontrib_serializinghtml//:_pkg (...) +| @pypi_sphinx//:pkg (...) +| @pypi_sphinx//:_pkg (...) +`-- @pypi_sphinxcontrib_serializinghtml//:pkg (...) +``` + +The `experimental_requirement_cycles` attribute allows you to work around these +issues by specifying groups of packages which form cycles. `pip_parse` will +transparently fix the cycles for you and provide the cyclic dependencies +simultaneously. + +```starlark + ... + experimental_requirement_cycles = { + "sphinx": [ + "sphinx", + "sphinxcontrib-serializinghtml", + ] + }, +) +``` + +`pip_parse` supports fixing multiple cycles simultaneously, however cycles must +be distinct. `apache-airflow` for instance has dependency cycles with a number +of its optional dependencies, which means those optional dependencies must all +be a part of the `airflow` cycle. For instance -- + +```starlark + ... + experimental_requirement_cycles = { + "airflow": [ + "apache-airflow", + "apache-airflow-providers-common-sql", + "apache-airflow-providers-postgres", + "apache-airflow-providers-sqlite", + ] + } +) +``` + +Alternatively, one could resolve the cycle by removing one leg of it. + +For example while `apache-airflow-providers-sqlite` is "baked into" the Airflow +package, `apache-airflow-providers-postgres` is not and is an optional feature. +Rather than listing `apache-airflow[postgres]` in your `requirements.txt` which +would expose a cycle via the extra, one could either _manually_ depend on +`apache-airflow` and `apache-airflow-providers-postgres` separately as +requirements. Bazel rules which need only `apache-airflow` can take it as a +dependency, and rules which explicitly want to mix in +`apache-airflow-providers-postgres` now can. + +Alternatively, one could use `rules_python`'s patching features to remove one +leg of the dependency manually. For instance by making +`apache-airflow-providers-postgres` not explicitly depend on `apache-airflow` or +perhaps `apache-airflow-providers-common-sql`. diff --git a/docs/pypi/download-workspace.md b/docs/pypi/download-workspace.md new file mode 100644 index 0000000000..48710095a4 --- /dev/null +++ b/docs/pypi/download-workspace.md @@ -0,0 +1,107 @@ +:::{default-domain} bzl +::: + +# Download (WORKSPACE) + +This documentation page covers how to download the PyPI dependencies in the legacy `WORKSPACE` setup. + +To add pip dependencies to your `WORKSPACE`, load the `pip_parse` function and +call it to create the central external repo and individual wheel external repos. + +```starlark +load("@rules_python//python:pip.bzl", "pip_parse") + +# Create a central repo that knows about the dependencies needed from +# requirements_lock.txt. +pip_parse( + name = "my_deps", + requirements_lock = "//path/to:requirements_lock.txt", +) + +# Load the starlark macro, which will define your dependencies. +load("@my_deps//:requirements.bzl", "install_deps") + +# Call it to define repos for your requirements. +install_deps() +``` + +## Interpreter selection + +Note that pip parse runs before the Bazel before decides which Python toolchain to use, it cannot +enforce that the interpreter used to invoke `pip` matches the interpreter used to run `py_binary` +targets. By default, `pip_parse` uses the system command `"python3"`. To override this, pass in the +{attr}`pip_parse.python_interpreter` attribute or {attr}`pip_parse.python_interpreter_target`. + +You can have multiple `pip_parse`s in the same workspace. This configuration will create multiple +external repos that have no relation to one another and may result in downloading the same wheels +numerous times. + +As with any repository rule, if you would like to ensure that `pip_parse` is +re-executed to pick up a non-hermetic change to your environment (e.g., updating +your system `python` interpreter), you can force it to re-execute by running +`bazel sync --only [pip_parse name]`. + +(per-os-arch-requirements)= +## Requirements for a specific OS/Architecture + +In some cases you may need to use different requirements files for different OS, Arch combinations. +This is enabled via the {attr}`pip_parse.requirements_by_platform` attribute. The keys of the +dictionary are labels to the file and the values are a list of comma separated target (os, arch) +tuples. + +For example: +```starlark + # ... + requirements_by_platform = { + "requirements_linux_x86_64.txt": "linux_x86_64", + "requirements_osx.txt": "osx_*", + "requirements_linux_exotic.txt": "linux_exotic", + "requirements_some_platforms.txt": "linux_aarch64,windows_*", + }, + # For the list of standard platforms that the rules_python has toolchains for, default to + # the following requirements file. + requirements_lock = "requirements_lock.txt", +``` + +In case of duplicate platforms, `rules_python` will raise an error as there has +to be unambiguous mapping of the requirement files to the (os, arch) tuples. + +An alternative way is to use per-OS requirement attributes. +```starlark + # ... + requirements_windows = "requirements_windows.txt", + requirements_darwin = "requirements_darwin.txt", + # For the remaining platforms (which is basically only linux OS), use this file. + requirements_lock = "requirements_lock.txt", +) +``` + +:::{note} +If you are using a universal lock file but want to restrict the list of platforms that +the lock file will be evaluated against, consider using the aforementioned +`requirements_by_platform` attribute and listing the platforms explicitly. +::: + +(vendoring-requirements)= +## Vendoring the requirements.bzl file + +:::{note} +For `bzlmod`, refer to standard `bazel vendor` usage if you want to really vendor it, otherwise +just use the `pip` extension as you would normally. + +However, be aware that there are caveats when doing so. +::: + +In some cases you may not want to generate the requirements.bzl file as a repository rule +while Bazel is fetching dependencies. For example, if you produce a reusable Bazel module +such as a ruleset, you may want to include the `requirements.bzl` file rather than make your users +install the `WORKSPACE` setup to generate it, see {gh-issue}`608`. + +This is the same workflow as Gazelle, which creates `go_repository` rules with +[`update-repos`](https://github.com/bazelbuild/bazel-gazelle#update-repos) + +To do this, use the "write to source file" pattern documented in + +to put a copy of the generated `requirements.bzl` into your project. +Then load the requirements.bzl file directly rather than from the generated repository. +See the example in {gh-path}`examples/pip_parse_vendored`. diff --git a/docs/pypi/download.md b/docs/pypi/download.md new file mode 100644 index 0000000000..18d6699ab3 --- /dev/null +++ b/docs/pypi/download.md @@ -0,0 +1,302 @@ +:::{default-domain} bzl +::: + +# Download (bzlmod) + +:::{seealso} +For WORKSPACE instructions see [here](./download-workspace). +::: + +To add PyPI dependencies to your `MODULE.bazel` file, use the `pip.parse` +extension, and call it to create the central external repo and individual wheel +external repos. Include in the `MODULE.bazel` the toolchain extension as shown +in the first bzlmod example above. + +```starlark +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") + +pip.parse( + hub_name = "my_deps", + python_version = "3.13", + requirements_lock = "//:requirements_lock_3_11.txt", +) + +use_repo(pip, "my_deps") +``` + +For more documentation, see the bzlmod examples under the {gh-path}`examples` folder or the documentation +for the {obj}`@rules_python//python/extensions:pip.bzl` extension. + +:::note} +We are using a host-platform compatible toolchain by default to setup pip dependencies. +During the setup phase, we create some symlinks, which may be inefficient on Windows +by default. In that case use the following `.bazelrc` options to improve performance if +you have admin privileges: + + startup --windows_enable_symlinks + +This will enable symlinks on Windows and help with bootstrap performance of setting up the +hermetic host python interpreter on this platform. Linux and OSX users should see no +difference. +::: + +## Interpreter selection + +The {obj}`pip.parse` `bzlmod` extension by default uses the hermetic python toolchain for the host +platform, but you can customize the interpreter using {attr}`pip.parse.python_interpreter` and +{attr}`pip.parse.python_interpreter_target`. + +You can use the pip extension multiple times. This configuration will create +multiple external repos that have no relation to one another and may result in +downloading the same wheels numerous times. + +As with any repository rule or extension, if you would like to ensure that `pip_parse` is +re-executed to pick up a non-hermetic change to your environment (e.g., updating your system +`python` interpreter), you can force it to re-execute by running `bazel sync --only [pip_parse +name]`. + +(per-os-arch-requirements)= +## Requirements for a specific OS/Architecture + +In some cases you may need to use different requirements files for different OS, Arch combinations. +This is enabled via the `requirements_by_platform` attribute in `pip.parse` extension and the +{obj}`pip.parse` tag class. The keys of the dictionary are labels to the file and the values are a +list of comma separated target (os, arch) tuples. + +For example: +```starlark + # ... + requirements_by_platform = { + "requirements_linux_x86_64.txt": "linux_x86_64", + "requirements_osx.txt": "osx_*", + "requirements_linux_exotic.txt": "linux_exotic", + "requirements_some_platforms.txt": "linux_aarch64,windows_*", + }, + # For the list of standard platforms that the rules_python has toolchains for, default to + # the following requirements file. + requirements_lock = "requirements_lock.txt", +``` + +In case of duplicate platforms, `rules_python` will raise an error as there has +to be unambiguous mapping of the requirement files to the (os, arch) tuples. + +An alternative way is to use per-OS requirement attributes. +```starlark + # ... + requirements_windows = "requirements_windows.txt", + requirements_darwin = "requirements_darwin.txt", + # For the remaining platforms (which is basically only linux OS), use this file. + requirements_lock = "requirements_lock.txt", +) +``` + +:::{note} +If you are using a universal lock file but want to restrict the list of platforms that +the lock file will be evaluated against, consider using the aforementioned +`requirements_by_platform` attribute and listing the platforms explicitly. +::: + +## Multi-platform support + +Historically the {obj}`pip_parse` and {obj}`pip.parse` have been only downloading/building +Python dependencies for the host platform that the `bazel` commands are executed on. Over +the years people started needing support for building containers and usually that involves +fetching dependencies for a particular target platform that may be other than the host +platform. + +Multi-platform support of cross-building the wheels can be done in two ways: +1. using {attr}`experimental_index_url` for the {bzl:obj}`pip.parse` bzlmod tag class +2. using {attr}`pip.parse.download_only` setting. + +:::{warning} +This will not for sdists with C extensions, but pure Python sdists may still work using the first +approach. +::: + +### Using `download_only` attribute + +Let's say you have 2 requirements files: +``` +# requirements.linux_x86_64.txt +--platform=manylinux_2_17_x86_64 +--python-version=39 +--implementation=cp +--abi=cp39 + +foo==0.0.1 --hash=sha256:deadbeef +bar==0.0.1 --hash=sha256:deadb00f +``` + +``` +# requirements.osx_aarch64.txt contents +--platform=macosx_10_9_arm64 +--python-version=39 +--implementation=cp +--abi=cp39 + +foo==0.0.3 --hash=sha256:deadbaaf +``` + +With these 2 files your {bzl:obj}`pip.parse` could look like: +```starlark +pip.parse( + hub_name = "pip", + python_version = "3.9", + # Tell `pip` to ignore sdists + download_only = True, + requirements_by_platform = { + "requirements.linux_x86_64.txt": "linux_x86_64", + "requirements.osx_aarch64.txt": "osx_aarch64", + }, +) +``` + +With this, the `pip.parse` will create a hub repository that is going to +support only two platforms - `cp39_osx_aarch64` and `cp39_linux_x86_64` and it +will only use `wheels` and ignore any sdists that it may find on the PyPI +compatible indexes. + +:::{warning} +Because bazel is not aware what exactly is downloaded, the same wheel may be downloaded +multiple times. +::: + +:::{note} +This will only work for wheel-only setups, i.e. all of your dependencies need to have wheels +available on the PyPI index that you use. +::: + +### Customizing `Requires-Dist` resolution + +:::{note} +Currently this is disabled by default, but you can turn it on using +{envvar}`RULES_PYTHON_ENABLE_PIPSTAR` environment variable. +::: + +In order to understand what dependencies to pull for a particular package +`rules_python` parses the `whl` file [`METADATA`][metadata]. +Packages can express dependencies via `Requires-Dist` and they can add conditions using +"environment markers", which represent the Python version, OS, etc. + +While the PyPI integration provides reasonable defaults to support most +platforms and environment markers, the values it uses can be customized in case +more esoteric configurations are needed. + +To customize the values used, you need to do two things: +1. Define a target that returns {obj}`EnvMarkerInfo` +2. Set the {obj}`//python/config_settings:pip_env_marker_config` flag to + the target defined in (1). + +The keys and values should be compatible with the [PyPA dependency specifiers +specification](https://packaging.python.org/en/latest/specifications/dependency-specifiers/). +This is not strictly enforced, however, so you can return a subset of keys or +additional keys, which become available during dependency evaluation. + +[metadata]: https://packaging.python.org/en/latest/specifications/core-metadata/ + +(bazel-downloader)= +### Bazel downloader and multi-platform wheel hub repository. + +:::{warning} +This is currently still experimental and whilst it has been proven to work in quite a few +environments, the APIs are still being finalized and there may be changes to the APIs for this +feature without much notice. + +The issues that you can subscribe to for updates are: +* {gh-issue}`260` +* {gh-issue}`1357` +::: + +The {obj}`pip` extension supports pulling information from `PyPI` (or a compatible mirror) and it +will ensure that the [bazel downloader][bazel_downloader] is used for downloading the wheels. + +This provides the following benefits: +* Integration with the [credential_helper](#credential-helper) to authenticate with private + mirrors. +* Cache the downloaded wheels speeding up the consecutive re-initialization of the repositories. +* Reuse the same instance of the wheel for multiple target platforms. +* Allow using transitions and targeting free-threaded and musl platforms more easily. +* Avoids `pip` for wheel fetching and results in much faster dependency fetching. + +To enable the feature specify {attr}`pip.parse.experimental_index_url` as shown in +the {gh-path}`examples/bzlmod/MODULE.bazel` example. + +Similar to [uv](https://docs.astral.sh/uv/configuration/indexes/), one can override the +index that is used for a single package. By default we first search in the index specified by +{attr}`pip.parse.experimental_index_url`, then we iterate through the +{attr}`pip.parse.experimental_extra_index_urls` unless there are overrides specified via +{attr}`pip.parse.experimental_index_url_overrides`. + +When using this feature during the `pip` extension evaluation you will see the accessed indexes similar to below: +```console +Loading: 0 packages loaded + Fetching module extension @@//python/extensions:pip.bzl%pip; Fetch package lists from PyPI index + Fetching https://pypi.org/simple/jinja2/ + +``` + +This does not mean that `rules_python` is fetching the wheels eagerly, but it +rather means that it is calling the PyPI server to get the Simple API response +to get the list of all available source and wheel distributions. Once it has +got all of the available distributions, it will select the right ones depending +on the `sha256` values in your `requirements_lock.txt` file. If `sha256` hashes +are not present in the requirements file, we will fallback to matching by version +specified in the lock file. + +Fetching the distribution information from the PyPI allows `rules_python` to +know which `whl` should be used on which target platform and it will determine +that by parsing the `whl` filename based on [PEP600], [PEP656] standards. This +allows the user to configure the behaviour by using the following publicly +available flags: +* {obj}`--@rules_python//python/config_settings:py_linux_libc` for selecting the Linux libc variant. +* {obj}`--@rules_python//python/config_settings:pip_whl` for selecting `whl` distribution preference. +* {obj}`--@rules_python//python/config_settings:pip_whl_osx_arch` for selecting MacOS wheel preference. +* {obj}`--@rules_python//python/config_settings:pip_whl_glibc_version` for selecting the GLIBC version compatibility. +* {obj}`--@rules_python//python/config_settings:pip_whl_muslc_version` for selecting the musl version compatibility. +* {obj}`--@rules_python//python/config_settings:pip_whl_osx_version` for selecting MacOS version compatibility. + +[bazel_downloader]: https://bazel.build/rules/lib/builtins/repository_ctx#download +[pep600]: https://peps.python.org/pep-0600/ +[pep656]: https://peps.python.org/pep-0656/ + +(credential-helper)= +## Credential Helper + +The [Bazel downloader](#bazel-downloader) usage allows for the Bazel +[Credential Helper][cred-helper-design]. +Your python artifact registry may provide a credential helper for you. +Refer to your index's docs to see if one is provided. + +The simplest form of a credential helper is a bash script that accepts an arg and spits out JSON to +stdout. For a service like Google Artifact Registry that uses ['Basic' HTTP Auth][rfc7617] and does +not provide a credential helper that conforms to the [spec][cred-helper-spec], the script might +look like: + +```bash +#!/bin/bash +# cred_helper.sh +ARG=$1 # but we don't do anything with it as it's always "get" + +# formatting is optional +echo '{' +echo ' "headers": {' +echo ' "Authorization": ["Basic dGVzdDoxMjPCow=="]' +echo ' }' +echo '}' +``` + +Configure Bazel to use this credential helper for your python index `example.com`: + +``` +# .bazelrc +build --credential_helper=example.com=/full/path/to/cred_helper.sh +``` + +Bazel will call this file like `cred_helper.sh get` and use the returned JSON to inject headers +into whatever HTTP(S) request it performs against `example.com`. + +See the [Credential Helper Spec][cred-helper-spec] for more details. + +[rfc7617]: https://datatracker.ietf.org/doc/html/rfc7617 +[cred-helper-design]: https://github.com/bazelbuild/proposals/blob/main/designs/2022-06-07-bazel-credential-helpers.md +[cred-helper-spec]: https://github.com/EngFlow/credential-helper-spec/blob/main/spec.md diff --git a/docs/pypi/index.md b/docs/pypi/index.md new file mode 100644 index 0000000000..c300124398 --- /dev/null +++ b/docs/pypi/index.md @@ -0,0 +1,27 @@ +:::{default-domain} bzl +::: + +# Using PyPI + +Using PyPI packages (aka "pip install") involves the following main steps. + +1. [Generating requirements file](./lock) +2. Installing third party packages in [bzlmod](./download) or [WORKSPACE](./download-workspace). +3. [Using third party packages as dependencies](./use) + +With the advanced topics covered separately: +* Dealing with [circular dependencies](./circular-dependencies). + +```{toctree} +lock +download +download-workspace +use +``` + +## Advanced topics + +```{toctree} +circular-dependencies +patch +``` diff --git a/docs/pypi/lock.md b/docs/pypi/lock.md new file mode 100644 index 0000000000..c9376036fb --- /dev/null +++ b/docs/pypi/lock.md @@ -0,0 +1,46 @@ +:::{default-domain} bzl +::: + +# Lock + +:::{note} +Currently `rules_python` only supports `requirements.txt` format. +::: + +## requirements.txt + +### pip compile + +Generally, when working on a Python project, you'll have some dependencies that themselves have other dependencies. You might also specify dependency bounds instead of specific versions. So you'll need to generate a full list of all transitive dependencies and pinned versions for every dependency. + +Typically, you'd have your project dependencies specified in `pyproject.toml` or `requirements.in` and generate the full pinned list of dependencies in `requirements_lock.txt`, which you can manage with the {obj}`compile_pip_requirements`: + +```starlark +load("@rules_python//python:pip.bzl", "compile_pip_requirements") + +compile_pip_requirements( + name = "requirements", + src = "requirements.in", + requirements_txt = "requirements_lock.txt", +) +``` + +This rule generates two targets: +- `bazel run [name].update` will regenerate the `requirements_txt` file +- `bazel test [name]_test` will test that the `requirements_txt` file is up to date + +Once you generate this fully specified list of requirements, you can install the requirements ([bzlmod](./download)/[WORKSPACE](./download-workspace)). + +:::{warning} +If you're specifying dependencies in `pyproject.toml`, make sure to include the `[build-system]` configuration, with pinned dependencies. `compile_pip_requirements` will use the build system specified to read your project's metadata, and you might see non-hermetic behavior if you don't pin the build system. + +Not specifying `[build-system]` at all will result in using a default `[build-system]` configuration, which uses unpinned versions ([ref](https://peps.python.org/pep-0518/#build-system-table)). +::: + +### uv pip compile (bzlmod only) + +We also have experimental setup for the `uv pip compile` way of generating lock files. +This is well tested with the public PyPI index, but you may hit some rough edges with private +mirrors. + +For more documentation see {obj}`lock` documentation. diff --git a/docs/pypi/patch.md b/docs/pypi/patch.md new file mode 100644 index 0000000000..f341bd1091 --- /dev/null +++ b/docs/pypi/patch.md @@ -0,0 +1,10 @@ +:::{default-domain} bzl +::: + +# Patching wheels + +Sometimes the wheels have to be patched to: +* Workaround the lack of a standard `site-packages` layout ({gh-issue}`2156`) +* Include certain PRs of your choice on top of wheels and avoid building from sdist, + +You can patch the wheels by using the {attr}`pip.override.patches` attribute. diff --git a/docs/pypi/use.md b/docs/pypi/use.md new file mode 100644 index 0000000000..7a16b7d9e9 --- /dev/null +++ b/docs/pypi/use.md @@ -0,0 +1,133 @@ +:::{default-domain} bzl +::: + +# Use in BUILD.bazel files + +Once you have setup the dependencies, you are ready to start using them in your `BUILD.bazel` +files. If you haven't done so yet, set it up by following the following docs: +1. [WORKSPACE](./download-workspace) +1. [bzlmod](./download) + +To refer to targets in a hub repo `pypi`, you can do one of two things: +```starlark +py_library( + name = "my_lib", + deps = [ + "@pypi//numpy", + ], +) +``` + +Or use the `requirement` helper that needs to be loaded from the `hub` repo itself: +```starlark +load("@pypi//:requirements.bzl", "requirement") + +py_library( + deps = [ + requirement("numpy") + ], +) +``` + +Note, that the usage of the `requirement` helper is not advised and can be problematic. See the +[notes below](#requirement-helper). + +Note, that the hub repo contains the following targets for each package: +* `@pypi//numpy` which is a shorthand for `@pypi//numpy:numpy`. This is an {obj}`alias` to + `@pypi//numpy:pkg`. +* `@pypi//numpy:pkg` - the {obj}`py_library` target automatically generated by the repository + rules. +* `@pypi//numpy:data` - the {obj}`filegroup` that is for all of the extra files that are included + as data in the `pkg` target. +* `@pypi//numpy:dist_info` - the {obj}`filegroup` that is for all of the files in the `.distinfo` directory. +* `@pypi//numpy:whl` - the {obj}`filegroup` that is the `.whl` file itself which includes all of + the transitive dependencies via the {attr}`filegroup.data` attribute. + +## Entry points + +If you would like to access [entry points][whl_ep], see the `py_console_script_binary` rule documentation, +which can help you create a `py_binary` target for a particular console script exposed by a package. + +[whl_ep]: https://packaging.python.org/specifications/entry-points/ + +## 'Extras' dependencies + +Any 'extras' specified in the requirements lock file will be automatically added +as transitive dependencies of the package. In the example above, you'd just put +`requirement("useful_dep")` or `@pypi//useful_dep`. + +## Consuming Wheel Dists Directly + +If you need to depend on the wheel dists themselves, for instance, to pass them +to some other packaging tool, you can get a handle to them with the +`whl_requirement` macro. For example: + +```starlark +load("@pypi//:requirements.bzl", "whl_requirement") + +filegroup( + name = "whl_files", + data = [ + # This is equivalent to "@pypi//boto3:whl" + whl_requirement("boto3"), + ] +) +``` + +## Creating a filegroup of files within a whl + +The rule {obj}`whl_filegroup` exists as an easy way to extract the necessary files +from a whl file without the need to modify the `BUILD.bazel` contents of the +whl repositories generated via `pip_repository`. Use it similarly to the `filegroup` +above. See the API docs for more information. + +(requirement-helper)= +## A note about using the requirement helper + +Each extracted wheel repo contains a `py_library` target representing +the wheel's contents. There are two ways to access this library. The +first uses the `requirement()` function defined in the central +repo's `//:requirements.bzl` file. This function maps a pip package +name to a label: + +```starlark +load("@my_deps//:requirements.bzl", "requirement") + +py_library( + name = "mylib", + srcs = ["mylib.py"], + deps = [ + ":myotherlib", + requirement("some_pip_dep"), + requirement("another_pip_dep"), + ] +) +``` + +The reason `requirement()` exists is to insulate from +changes to the underlying repository and label strings. However, those +labels have become directly used, so aren't able to easily change regardless. + +On the other hand, using `requirement()` helper has several drawbacks: + +- It doesn't work with `buildifier` +- It doesn't work with `buildozer` +- It adds extra layer on top of normal mechanisms to refer to targets. +- It does not scale well as each type of target needs a new macro to be loaded and imported. + +If you don't want to use `requirement()`, you can use the library labels directly instead. For +`pip_parse`, the labels are of the following form: + +```starlark +@{name}//{package} +``` + +Here `name` is the `name` attribute that was passed to `pip_parse` and +`package` is the pip package name with characters that are illegal in +Bazel label names (e.g. `-`, `.`) replaced with `_`. If you need to +update `name` from "old" to "new", then you can run the following +`buildozer` command: + +```shell +buildozer 'substitute deps @old//([^/]+) @new//${1}' //...:* +``` diff --git a/docs/requirements.txt b/docs/requirements.txt index e4ec16fa5e..87c13aa8ba 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,5 @@ # This file was autogenerated by uv via the following command: # bazel run //docs:requirements.update ---index-url https://pypi.org/simple absl-py==2.2.2 \ --hash=sha256:bf25b2c2eed013ca456918c453d687eab4e8309fba81ee2f4c1a6aa2494175eb \ diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index e9036c3013..d89dc6c228 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -398,6 +398,7 @@ bzl_library( ":pep508_requirement_bzl", ":pypi_repo_utils_bzl", ":whl_metadata_bzl", + ":whl_target_platforms_bzl", "//python/private:auth_bzl", "//python/private:bzlmod_enabled_bzl", "//python/private:envsubst_bzl", diff --git a/python/private/pypi/pkg_aliases.bzl b/python/private/pypi/pkg_aliases.bzl index 28d70ff715..d71c37cb4b 100644 --- a/python/private/pypi/pkg_aliases.bzl +++ b/python/private/pypi/pkg_aliases.bzl @@ -237,9 +237,10 @@ def multiplatform_whl_aliases( Exposed only for unit tests. Args: - aliases: {type}`str | dict[whl_config_setting | str, str]`: The aliases + aliases: {type}`str | dict[struct | str, str]`: The aliases to process. Any aliases that have the filename set will be - converted to a dict of config settings to repo names. + converted to a dict of config settings to repo names. The + struct is created by {func}`whl_config_setting`. glibc_versions: {type}`list[tuple[int, int]]` list of versions that can be used in this hub repo. muslc_versions: {type}`list[tuple[int, int]]` list of versions that can be diff --git a/python/private/pypi/simpleapi_download.bzl b/python/private/pypi/simpleapi_download.bzl index e8d7d0941a..164d4e8dbd 100644 --- a/python/private/pypi/simpleapi_download.bzl +++ b/python/private/pypi/simpleapi_download.bzl @@ -83,6 +83,7 @@ def simpleapi_download( found_on_index = {} warn_overrides = False + ctx.report_progress("Fetch package lists from PyPI index") for i, index_url in enumerate(index_urls): if i != 0: # Warn the user about a potential fix for the overrides diff --git a/python/private/pypi/whl_config_setting.bzl b/python/private/pypi/whl_config_setting.bzl index 6e10eb4d27..3b81e4694f 100644 --- a/python/private/pypi/whl_config_setting.bzl +++ b/python/private/pypi/whl_config_setting.bzl @@ -21,14 +21,14 @@ def whl_config_setting(*, version = None, config_setting = None, filename = None aliases in a hub repository. Args: - version: optional(str), the version of the python toolchain that this + version: {type}`str | None`the version of the python toolchain that this whl alias is for. If not set, then non-version aware aliases will be constructed. This is mainly used for better error messages when there is no match found during a select. - config_setting: optional(Label or str), the config setting that we should use. Defaults + config_setting: {type}`str | Label | None` the config setting that we should use. Defaults to "//_config:is_python_{version}". - filename: optional(str), the distribution filename to derive the config_setting. - target_platforms: optional(list[str]), the list of target_platforms for this + filename: {type}`str | None` the distribution filename to derive the config_setting. + target_platforms: {type}`list[str] | None` the list of target_platforms for this distribution. Returns: diff --git a/sphinxdocs/inventories/bazel_inventory.txt b/sphinxdocs/inventories/bazel_inventory.txt index bbd200ddb5..e14ea76067 100644 --- a/sphinxdocs/inventories/bazel_inventory.txt +++ b/sphinxdocs/inventories/bazel_inventory.txt @@ -15,6 +15,7 @@ RBE bzl:obj 1 remote/rbe - RunEnvironmentInfo bzl:type 1 rules/lib/providers/RunEnvironmentInfo - Target bzl:type 1 rules/lib/builtins/Target - ToolchainInfo bzl:type 1 rules/lib/providers/ToolchainInfo.html - +alias bzl:rule 1 reference/be/general#alias - attr.bool bzl:type 1 rules/lib/toplevel/attr#bool - attr.int bzl:type 1 rules/lib/toplevel/attr#int - attr.int_list bzl:type 1 rules/lib/toplevel/attr#int_list - @@ -40,6 +41,7 @@ config.string_list bzl:function 1 rules/lib/toplevel/config#string_list - config.target bzl:function 1 rules/lib/toplevel/config#target - config_common.FeatureFlagInfo bzl:type 1 rules/lib/toplevel/config_common#FeatureFlagInfo - config_common.toolchain_type bzl:function 1 rules/lib/toplevel/config_common#toolchain_type - +config_setting bzl:rule 1 reference/be/general#config_setting - ctx bzl:type 1 rules/lib/builtins/repository_ctx - ctx.actions bzl:obj 1 rules/lib/builtins/ctx#actions - ctx.aspect_ids bzl:obj 1 rules/lib/builtins/ctx#aspect_ids - @@ -79,6 +81,8 @@ depset bzl:type 1 rules/lib/depset - dict bzl:type 1 rules/lib/dict - exec_compatible_with bzl:attr 1 reference/be/common-definitions#common.exec_compatible_with - exec_group bzl:function 1 rules/lib/globals/bzl#exec_group - +filegroup bzl:rule 1 reference/be/general#filegroup - +filegroup.data bzl:attr 1 reference/be/general#filegroup.data - int bzl:type 1 rules/lib/int - label bzl:type 1 concepts/labels - list bzl:type 1 rules/lib/list - diff --git a/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl b/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl index ce214d6e34..a96815c12c 100644 --- a/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl +++ b/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl @@ -43,6 +43,7 @@ def _test_simple(env): contents = simpleapi_download( ctx = struct( os = struct(environ = {}), + report_progress = lambda _: None, ), attr = struct( index_url_overrides = {}, @@ -95,6 +96,7 @@ def _test_fail(env): simpleapi_download( ctx = struct( os = struct(environ = {}), + report_progress = lambda _: None, ), attr = struct( index_url_overrides = {}, @@ -136,6 +138,7 @@ def _test_download_url(env): ctx = struct( os = struct(environ = {}), download = download, + report_progress = lambda _: None, read = lambda i: "contents of " + i, path = lambda i: "path/for/" + i, ), @@ -171,6 +174,7 @@ def _test_download_url_parallel(env): ctx = struct( os = struct(environ = {}), download = download, + report_progress = lambda _: None, read = lambda i: "contents of " + i, path = lambda i: "path/for/" + i, ), @@ -206,6 +210,7 @@ def _test_download_envsubst_url(env): ctx = struct( os = struct(environ = {"INDEX_URL": "https://example.com/main/simple/"}), download = download, + report_progress = lambda _: None, read = lambda i: "contents of " + i, path = lambda i: "path/for/" + i, ), From fd29d273e41180c56d691a67004ade742f7c7b2f Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Wed, 28 May 2025 23:37:23 -0700 Subject: [PATCH 123/156] refactor: change site_packages_symlinks to venv_symlinks (#2939) This generalizes the ability to populate the venv directory by adding and additional field, `kind`, which tells which directory of the venv to populate. A symbolic constant is used to indicate which directory so that users don't have to re-derive the platform and version specific paths that make up the venv directory names. This follows the design described by https://github.com/bazel-contrib/rules_python/issues/2156#issuecomment-2855580026 This also changes it to a depset of structs to make it more forward compatible. A provider is used because they're slightly more memory efficient than regular structs. Work towards https://github.com/bazel-contrib/rules_python/issues/2156 --- CHANGELOG.md | 4 +- python/features.bzl | 8 +- python/private/attributes.bzl | 2 +- python/private/common.bzl | 6 +- python/private/flags.bzl | 6 +- python/private/py_executable.bzl | 102 ++++++++++++++----------- python/private/py_info.bzl | 127 ++++++++++++++++++++++--------- python/private/py_library.bzl | 40 +++++----- 8 files changed, 187 insertions(+), 108 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9655b90487..4a6bdf0a96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,8 @@ END_UNRELEASED_TEMPLATE `_test` target is deprecated and will be removed in the next major release. ([#2794](https://github.com/bazel-contrib/rules_python/issues/2794) * (py_wheel) py_wheel always creates zip64-capable wheel zips +* (providers) (experimental) {obj}`PyInfo.venv_symlinks` replaces + `PyInfo.site_packages_symlinks` {#v0-0-0-fixed} ### Fixed @@ -203,7 +205,7 @@ END_UNRELEASED_TEMPLATE please check the {obj}`uv.configure` tag class. * Add support for riscv64 linux platform. * (toolchains) Add python 3.13.2 and 3.12.9 toolchains -* (providers) (experimental) {obj}`PyInfo.site_packages_symlinks` field added to +* (providers) (experimental) `PyInfo.site_packages_symlinks` field added to allow specifying links to create within the venv site packages (only applicable with {obj}`--bootstrap_impl=script`) ([#2156](https://github.com/bazelbuild/rules_python/issues/2156)). diff --git a/python/features.bzl b/python/features.bzl index 917bd3800c..b678a45241 100644 --- a/python/features.bzl +++ b/python/features.bzl @@ -31,11 +31,11 @@ def _features_typedef(): ::: :::: - ::::{field} py_info_site_packages_symlinks + ::::{field} py_info_venv_symlinks - True if the `PyInfo.site_packages_symlinks` field is available. + True if the `PyInfo.venv_symlinks` field is available. - :::{versionadded} 1.4.0 + :::{versionadded} VERSION_NEXT_FEATURE ::: :::: @@ -61,7 +61,7 @@ features = struct( TYPEDEF = _features_typedef, # keep sorted precompile = True, - py_info_site_packages_symlinks = True, + py_info_venv_symlinks = True, uses_builtin_rules = not config.enable_pystar, version = _VERSION_PRIVATE if "$Format" not in _VERSION_PRIVATE else "", ) diff --git a/python/private/attributes.bzl b/python/private/attributes.bzl index 98aba4eb23..ad8cba2e6c 100644 --- a/python/private/attributes.bzl +++ b/python/private/attributes.bzl @@ -260,7 +260,7 @@ The order of this list can matter because it affects the order that information from dependencies is merged in, which can be relevant depending on the ordering mode of depsets that are merged. -* {obj}`PyInfo.site_packages_symlinks` uses topological ordering. +* {obj}`PyInfo.venv_symlinks` uses topological ordering. See {obj}`PyInfo` for more information about the ordering of its depsets and how its fields are merged. diff --git a/python/private/common.bzl b/python/private/common.bzl index a58a9c00a4..e49dbad20c 100644 --- a/python/private/common.bzl +++ b/python/private/common.bzl @@ -378,7 +378,7 @@ def create_py_info( implicit_pyc_files, implicit_pyc_source_files, imports, - site_packages_symlinks = []): + venv_symlinks = []): """Create PyInfo provider. Args: @@ -396,7 +396,7 @@ def create_py_info( implicit_pyc_files: {type}`depset[File]` Implicitly generated pyc files that a binary can choose to include. imports: depset of strings; the import path values to propagate. - site_packages_symlinks: {type}`list[tuple[str, str]]` tuples of + venv_symlinks: {type}`list[tuple[str, str]]` tuples of `(runfiles_path, site_packages_path)` for symlinks to create in the consuming binary's venv site packages. @@ -406,7 +406,7 @@ def create_py_info( necessary for deprecated extra actions support). """ py_info = PyInfoBuilder.new() - py_info.site_packages_symlinks.add(site_packages_symlinks) + py_info.venv_symlinks.add(venv_symlinks) py_info.direct_original_sources.add(original_sources) py_info.direct_pyc_files.add(required_pyc_files) py_info.direct_pyi_files.add(ctx.files.pyi_srcs) diff --git a/python/private/flags.bzl b/python/private/flags.bzl index 40ce63b3b0..710402ba68 100644 --- a/python/private/flags.bzl +++ b/python/private/flags.bzl @@ -154,12 +154,12 @@ def _venvs_site_packages_is_enabled(ctx): flag_value = ctx.attr.experimental_venvs_site_packages[BuildSettingInfo].value return flag_value == VenvsSitePackages.YES -# Decides if libraries try to use a site-packages layout using site_packages_symlinks +# Decides if libraries try to use a site-packages layout using venv_symlinks # buildifier: disable=name-conventions VenvsSitePackages = FlagEnum( - # Use site_packages_symlinks + # Use venv_symlinks YES = "yes", - # Don't use site_packages_symlinks + # Don't use venv_symlinks NO = "no", is_enabled = _venvs_site_packages_is_enabled, ) diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index 24be8dd2ad..7c3e0cb757 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -54,7 +54,7 @@ load(":flags.bzl", "BootstrapImplFlag", "VenvsUseDeclareSymlinkFlag") load(":precompile.bzl", "maybe_precompile") load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo") load(":py_executable_info.bzl", "PyExecutableInfo") -load(":py_info.bzl", "PyInfo") +load(":py_info.bzl", "PyInfo", "VenvSymlinkKind") load(":py_internal.bzl", "py_internal") load(":py_runtime_info.bzl", "DEFAULT_STUB_SHEBANG", "PyRuntimeInfo") load(":reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo") @@ -543,6 +543,7 @@ def _create_venv(ctx, output_prefix, imports, runtime_details): VenvsUseDeclareSymlinkFlag.get_value(ctx) == VenvsUseDeclareSymlinkFlag.YES ) recreate_venv_at_runtime = False + bin_dir = "{}/bin".format(venv) if not venvs_use_declare_symlink_enabled or not runtime.supports_build_time_venv: recreate_venv_at_runtime = True @@ -556,7 +557,7 @@ def _create_venv(ctx, output_prefix, imports, runtime_details): # When the venv symlinks are disabled, the $venv/bin/python3 file isn't # needed or used at runtime. However, the zip code uses the interpreter # File object to figure out some paths. - interpreter = ctx.actions.declare_file("{}/bin/{}".format(venv, py_exe_basename)) + interpreter = ctx.actions.declare_file("{}/{}".format(bin_dir, py_exe_basename)) ctx.actions.write(interpreter, "actual:{}".format(interpreter_actual_path)) elif runtime.interpreter: @@ -568,7 +569,7 @@ def _create_venv(ctx, output_prefix, imports, runtime_details): # declare_symlink() is required to ensure that the resulting file # in runfiles is always a symlink. An RBE implementation, for example, # may choose to write what symlink() points to instead. - interpreter = ctx.actions.declare_symlink("{}/bin/{}".format(venv, py_exe_basename)) + interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename)) interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path) rel_path = relative_path( @@ -581,7 +582,7 @@ def _create_venv(ctx, output_prefix, imports, runtime_details): ctx.actions.symlink(output = interpreter, target_path = rel_path) else: py_exe_basename = paths.basename(runtime.interpreter_path) - interpreter = ctx.actions.declare_symlink("{}/bin/{}".format(venv, py_exe_basename)) + interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename)) ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path) interpreter_actual_path = runtime.interpreter_path @@ -618,89 +619,104 @@ def _create_venv(ctx, output_prefix, imports, runtime_details): }, computed_substitutions = computed_subs, ) - site_packages_symlinks = _create_site_packages_symlinks(ctx, site_packages) + + venv_dir_map = { + VenvSymlinkKind.BIN: bin_dir, + VenvSymlinkKind.LIB: site_packages, + } + venv_symlinks = _create_venv_symlinks(ctx, venv_dir_map) return struct( interpreter = interpreter, recreate_venv_at_runtime = recreate_venv_at_runtime, # Runfiles root relative path or absolute path interpreter_actual_path = interpreter_actual_path, - files_without_interpreter = [pyvenv_cfg, pth, site_init] + site_packages_symlinks, + files_without_interpreter = [pyvenv_cfg, pth, site_init] + venv_symlinks, # string; venv-relative path to the site-packages directory. venv_site_packages = venv_site_packages, ) -def _create_site_packages_symlinks(ctx, site_packages): - """Creates symlinks within site-packages. +def _create_venv_symlinks(ctx, venv_dir_map): + """Creates symlinks within the venv. Args: ctx: current rule ctx - site_packages: runfiles-root-relative path to the site-packages directory + venv_dir_map: mapping of VenvSymlinkKind constants to the + venv path. Returns: {type}`list[File]` list of the File symlink objects created. """ - # maps site-package symlink to the runfiles path it should point to + # maps venv-relative path to the runfiles path it should point to entries = depset( # NOTE: Topological ordering is used so that dependencies closer to the # binary have precedence in creating their symlinks. This allows the # binary a modicum of control over the result. order = "topological", transitive = [ - dep[PyInfo].site_packages_symlinks + dep[PyInfo].venv_symlinks for dep in ctx.attr.deps if PyInfo in dep ], ).to_list() + link_map = _build_link_map(entries) + venv_files = [] + for kind, kind_map in link_map.items(): + base = venv_dir_map[kind] + for venv_path, link_to in kind_map.items(): + venv_link = ctx.actions.declare_symlink(paths.join(base, venv_path)) + venv_link_rf_path = runfiles_root_path(ctx, venv_link.short_path) + rel_path = relative_path( + # dirname is necessary because a relative symlink is relative to + # the directory the symlink resides within. + from_ = paths.dirname(venv_link_rf_path), + to = link_to, + ) + ctx.actions.symlink(output = venv_link, target_path = rel_path) + venv_files.append(venv_link) - sp_files = [] - for sp_dir_path, link_to in link_map.items(): - sp_link = ctx.actions.declare_symlink(paths.join(site_packages, sp_dir_path)) - sp_link_rf_path = runfiles_root_path(ctx, sp_link.short_path) - rel_path = relative_path( - # dirname is necessary because a relative symlink is relative to - # the directory the symlink resides within. - from_ = paths.dirname(sp_link_rf_path), - to = link_to, - ) - ctx.actions.symlink(output = sp_link, target_path = rel_path) - sp_files.append(sp_link) - return sp_files + return venv_files def _build_link_map(entries): + # dict[str kind, dict[str rel_path, str link_to_path]] link_map = {} - for link_to_runfiles_path, site_packages_path in entries: - if site_packages_path in link_map: + for entry in entries: + kind = entry.kind + kind_map = link_map.setdefault(kind, {}) + if entry.venv_path in kind_map: # We ignore duplicates by design. The dependency closer to the # binary gets precedence due to the topological ordering. continue else: - link_map[site_packages_path] = link_to_runfiles_path + kind_map[entry.venv_path] = entry.link_to_path # An empty link_to value means to not create the site package symlink. # Because of the topological ordering, this allows binaries to remove # entries by having an earlier dependency produce empty link_to values. - for sp_dir_path, link_to in link_map.items(): - if not link_to: - link_map.pop(sp_dir_path) + for kind, kind_map in link_map.items(): + for dir_path, link_to in kind_map.items(): + if not link_to: + kind_map.pop(dir_path) - # Remove entries that would be a child path of a created symlink. - # Earlier entries have precedence to match how exact matches are handled. + # dict[str kind, dict[str rel_path, str link_to_path]] keep_link_map = {} - for _ in range(len(link_map)): - if not link_map: - break - dirname, value = link_map.popitem() - keep_link_map[dirname] = value - - prefix = dirname + "/" # Add slash to prevent /X matching /XY - for maybe_suffix in link_map.keys(): - maybe_suffix += "/" # Add slash to prevent /X matching /XY - if maybe_suffix.startswith(prefix) or prefix.startswith(maybe_suffix): - link_map.pop(maybe_suffix) + # Remove entries that would be a child path of a created symlink. + # Earlier entries have precedence to match how exact matches are handled. + for kind, kind_map in link_map.items(): + keep_kind_map = keep_link_map.setdefault(kind, {}) + for _ in range(len(kind_map)): + if not kind_map: + break + dirname, value = kind_map.popitem() + keep_kind_map[dirname] = value + prefix = dirname + "/" # Add slash to prevent /X matching /XY + for maybe_suffix in kind_map.keys(): + maybe_suffix += "/" # Add slash to prevent /X matching /XY + if maybe_suffix.startswith(prefix) or prefix.startswith(maybe_suffix): + kind_map.pop(maybe_suffix) return keep_link_map def _map_each_identity(v): diff --git a/python/private/py_info.bzl b/python/private/py_info.bzl index d175eefb69..2a2f4554e3 100644 --- a/python/private/py_info.bzl +++ b/python/private/py_info.bzl @@ -18,6 +18,64 @@ load(":builders.bzl", "builders") load(":reexports.bzl", "BuiltinPyInfo") load(":util.bzl", "define_bazel_6_provider") +def _VenvSymlinkKind_typedef(): + """An enum of types of venv directories. + + :::{field} BIN + :type: object + + Indicates to create paths under the directory that has binaries + within the venv. + ::: + + :::{field} LIB + :type: object + + Indicates to create paths under the venv's site-packages directory. + ::: + + :::{field} INCLUDE + :type: object + + Indicates to create paths under the venv's include directory. + ::: + """ + +# buildifier: disable=name-conventions +VenvSymlinkKind = struct( + TYPEDEF = _VenvSymlinkKind_typedef, + BIN = "BIN", + LIB = "LIB", + INCLUDE = "INCLUDE", +) + +# A provider is used for memory efficiency. +# buildifier: disable=name-conventions +VenvSymlinkEntry = provider( + doc = """ +An entry in `PyInfo.venv_symlinks` +""", + fields = { + "kind": """ +:type: str + +One of the {obj}`VenvSymlinkKind` values. It represents which directory within +the venv to create the path under. +""", + "link_to_path": """ +:type: str | None + +A runfiles-root relative path that `venv_path` will symlink to. If `None`, +it means to not create a symlink. +""", + "venv_path": """ +:type: str + +A path relative to the `kind` directory within the venv. +""", + }, +) + def _check_arg_type(name, required_type, value): """Check that a value is of an expected type.""" value_type = type(value) @@ -43,7 +101,7 @@ def _PyInfo_init( transitive_original_sources = depset(), direct_pyi_files = depset(), transitive_pyi_files = depset(), - site_packages_symlinks = depset()): + venv_symlinks = depset()): _check_arg_type("transitive_sources", "depset", transitive_sources) # Verify it's postorder compatible, but retain is original ordering. @@ -71,7 +129,6 @@ def _PyInfo_init( "has_py2_only_sources": has_py2_only_sources, "has_py3_only_sources": has_py2_only_sources, "imports": imports, - "site_packages_symlinks": site_packages_symlinks, "transitive_implicit_pyc_files": transitive_implicit_pyc_files, "transitive_implicit_pyc_source_files": transitive_implicit_pyc_source_files, "transitive_original_sources": transitive_original_sources, @@ -79,6 +136,7 @@ def _PyInfo_init( "transitive_pyi_files": transitive_pyi_files, "transitive_sources": transitive_sources, "uses_shared_libraries": uses_shared_libraries, + "venv_symlinks": venv_symlinks, } PyInfo, _unused_raw_py_info_ctor = define_bazel_6_provider( @@ -146,34 +204,6 @@ A depset of import path strings to be added to the `PYTHONPATH` of executable Python targets. These are accumulated from the transitive `deps`. The order of the depset is not guaranteed and may be changed in the future. It is recommended to use `default` order (the default). -""", - "site_packages_symlinks": """ -:type: depset[tuple[str | None, str]] - -A depset with `topological` ordering. - -Tuples of `(runfiles_path, site_packages_path)`. Where -* `runfiles_path` is a runfiles-root relative path. It is the path that - has the code to make importable. If `None` or empty string, then it means - to not create a site packages directory with the `site_packages_path` - name. -* `site_packages_path` is a path relative to the site-packages directory of - the venv for whatever creates the venv (typically py_binary). It makes - the code in `runfiles_path` available for import. Note that this - is created as a "raw" symlink (via `declare_symlink`). - -:::{include} /_includes/experimental_api.md -::: - -:::{tip} -The topological ordering means dependencies earlier and closer to the consumer -have precedence. This allows e.g. a binary to add dependencies that override -values from further way dependencies, such as forcing symlinks to point to -specific paths or preventing symlinks from being created. -::: - -:::{versionadded} 1.4.0 -::: """, "transitive_implicit_pyc_files": """ :type: depset[File] @@ -262,6 +292,35 @@ Whether any of this target's transitive `deps` has a shared library file (such as a `.so` file). This field is currently unused in Bazel and may go away in the future. +""", + "venv_symlinks": """ +:type: depset[VenvSymlinkEntry] + +A depset with `topological` ordering. + + +Tuples of `(runfiles_path, site_packages_path)`. Where +* `runfiles_path` is a runfiles-root relative path. It is the path that + has the code to make importable. If `None` or empty string, then it means + to not create a site packages directory with the `site_packages_path` + name. +* `site_packages_path` is a path relative to the site-packages directory of + the venv for whatever creates the venv (typically py_binary). It makes + the code in `runfiles_path` available for import. Note that this + is created as a "raw" symlink (via `declare_symlink`). + +:::{include} /_includes/experimental_api.md +::: + +:::{tip} +The topological ordering means dependencies earlier and closer to the consumer +have precedence. This allows e.g. a binary to add dependencies that override +values from further way dependencies, such as forcing symlinks to point to +specific paths or preventing symlinks from being created. +::: + +:::{versionadded} VERSION_NEXT_FEATURE +::: """, }, ) @@ -314,7 +373,7 @@ def _PyInfoBuilder_typedef(): :type: DepsetBuilder[File] ::: - :::{field} site_packages_symlinks + :::{field} venv_symlinks :type: DepsetBuilder[tuple[str | None, str]] NOTE: This depset has `topological` order @@ -358,7 +417,7 @@ def _PyInfoBuilder_new(): transitive_pyc_files = builders.DepsetBuilder(), transitive_pyi_files = builders.DepsetBuilder(), transitive_sources = builders.DepsetBuilder(), - site_packages_symlinks = builders.DepsetBuilder(order = "topological"), + venv_symlinks = builders.DepsetBuilder(order = "topological"), ) return self @@ -525,7 +584,7 @@ def _PyInfoBuilder_merge_all(self, transitive, *, direct = []): self.transitive_original_sources.add(info.transitive_original_sources) self.transitive_pyc_files.add(info.transitive_pyc_files) self.transitive_pyi_files.add(info.transitive_pyi_files) - self.site_packages_symlinks.add(info.site_packages_symlinks) + self.venv_symlinks.add(info.venv_symlinks) return self @@ -583,7 +642,7 @@ def _PyInfoBuilder_build(self): transitive_original_sources = self.transitive_original_sources.build(), transitive_pyc_files = self.transitive_pyc_files.build(), transitive_pyi_files = self.transitive_pyi_files.build(), - site_packages_symlinks = self.site_packages_symlinks.build(), + venv_symlinks = self.venv_symlinks.build(), ) else: kwargs = {} diff --git a/python/private/py_library.bzl b/python/private/py_library.bzl index fd9dad9f20..fabc880a8d 100644 --- a/python/private/py_library.bzl +++ b/python/private/py_library.bzl @@ -43,7 +43,7 @@ load( load(":flags.bzl", "AddSrcsToRunfilesFlag", "PrecompileFlag", "VenvsSitePackages") load(":precompile.bzl", "maybe_precompile") load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo") -load(":py_info.bzl", "PyInfo") +load(":py_info.bzl", "PyInfo", "VenvSymlinkEntry", "VenvSymlinkKind") load(":py_internal.bzl", "py_internal") load(":reexports.bzl", "BuiltinPyInfo") load(":rule_builders.bzl", "ruleb") @@ -90,9 +90,9 @@ won't be understood as namespace packages; they'll be seen as regular packages. likely lead to conflicts with other targets that contribute to the namespace. :::{tip} -This attributes populates {obj}`PyInfo.site_packages_symlinks`, which is +This attributes populates {obj}`PyInfo.venv_symlinks`, which is a topologically ordered depset. This means dependencies closer and earlier -to a consumer have precedence. See {obj}`PyInfo.site_packages_symlinks` for +to a consumer have precedence. See {obj}`PyInfo.venv_symlinks` for more information. ::: @@ -155,9 +155,9 @@ def py_library_impl(ctx, *, semantics): runfiles = runfiles.build(ctx) imports = [] - site_packages_symlinks = [] + venv_symlinks = [] - imports, site_packages_symlinks = _get_imports_and_site_packages_symlinks(ctx, semantics) + imports, venv_symlinks = _get_imports_and_venv_symlinks(ctx, semantics) cc_info = semantics.get_cc_info_for_library(ctx) py_info, deps_transitive_sources, builtins_py_info = create_py_info( @@ -168,7 +168,7 @@ def py_library_impl(ctx, *, semantics): implicit_pyc_files = implicit_pyc_files, implicit_pyc_source_files = implicit_pyc_source_files, imports = imports, - site_packages_symlinks = site_packages_symlinks, + venv_symlinks = venv_symlinks, ) # TODO(b/253059598): Remove support for extra actions; https://github.com/bazelbuild/bazel/issues/16455 @@ -206,16 +206,16 @@ Source files are no longer added to the runfiles directly. ::: """ -def _get_imports_and_site_packages_symlinks(ctx, semantics): +def _get_imports_and_venv_symlinks(ctx, semantics): imports = depset() - site_packages_symlinks = depset() + venv_symlinks = depset() if VenvsSitePackages.is_enabled(ctx): - site_packages_symlinks = _get_site_packages_symlinks(ctx) + venv_symlinks = _get_venv_symlinks(ctx) else: imports = collect_imports(ctx, semantics) - return imports, site_packages_symlinks + return imports, venv_symlinks -def _get_site_packages_symlinks(ctx): +def _get_venv_symlinks(ctx): imports = ctx.attr.imports if len(imports) == 0: fail("When venvs_site_packages is enabled, exactly one `imports` " + @@ -253,7 +253,7 @@ def _get_site_packages_symlinks(ctx): repo_runfiles_dirname = None dirs_with_init = {} # dirname -> runfile path - site_packages_symlinks = [] + venv_symlinks = [] for src in ctx.files.srcs: if src.extension not in PYTHON_FILE_EXTENSIONS: continue @@ -271,9 +271,10 @@ def _get_site_packages_symlinks(ctx): # This would be files that do not have directories and we just need to add # direct symlinks to them as is: - site_packages_symlinks.append(( - paths.join(repo_runfiles_dirname, site_packages_root, filename), - filename, + venv_symlinks.append(VenvSymlinkEntry( + kind = VenvSymlinkKind.LIB, + link_to_path = paths.join(repo_runfiles_dirname, site_packages_root, filename), + venv_path = filename, )) # Sort so that we encounter `foo` before `foo/bar`. This ensures we @@ -291,11 +292,12 @@ def _get_site_packages_symlinks(ctx): first_level_explicit_packages.append(d) for dirname in first_level_explicit_packages: - site_packages_symlinks.append(( - paths.join(repo_runfiles_dirname, site_packages_root, dirname), - dirname, + venv_symlinks.append(VenvSymlinkEntry( + kind = VenvSymlinkKind.LIB, + link_to_path = paths.join(repo_runfiles_dirname, site_packages_root, dirname), + venv_path = dirname, )) - return site_packages_symlinks + return venv_symlinks def _repo_relative_short_path(short_path): # Convert `../+pypi+foo/some/file.py` to `some/file.py` From bbf3ab8956007f48fc012fb9316debffde8b0495 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Thu, 29 May 2025 06:21:27 -0700 Subject: [PATCH 124/156] docs: fix sphinxdocs mis-redirect (#2940) The redirect was going to a non-existent URL when viewed on the deployed docs. This was happening because the absolute paths `/api/whatever` don't exist in the deployed site -- it's actually `/en/latest/api/whatever`. This went unnoticed because it works locally (where there is no /en/latest prefix). To fix, use a relative url (relative urls are relative to the path that is redirected from) --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 1d9f526b93..8537d9996c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -87,7 +87,7 @@ "api/sphinxdocs/sphinx": "/api/sphinxdocs/sphinxdocs/sphinx.html", "api/sphinxdocs/sphinx_stardoc": "/api/sphinxdocs/sphinxdocs/sphinx_stardoc.html", "api/sphinxdocs/readthedocs": "/api/sphinxdocs/sphinxdocs/readthedocs.html", - "api/sphinxdocs/index": "/api/sphinxdocs/sphinxdocs/index.html", + "api/sphinxdocs/index": "sphinxdocs/index.html", "api/sphinxdocs/private/sphinx_docs_library": "/api/sphinxdocs/sphinxdocs/private/sphinx_docs_library.html", "api/sphinxdocs/sphinx_docs_library": "/api/sphinxdocs/sphinxdocs/sphinx_docs_library.html", "api/sphinxdocs/inventories/index": "/api/sphinxdocs/sphinxdocs/inventories/index.html", From d60cee2623bf6cedb4dbd9899eb99ac84432fb37 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Thu, 29 May 2025 08:43:56 -0700 Subject: [PATCH 125/156] feat: allow custom platform when overriding (#2880) This basically allows using any python-build-standalone archive and using it if custom flags are set. This is done through the `single_version_platform_override()` API, because such archives are inherently version and platform specific. Key changes: * The `platform` arg can be any value (mostly; it ends up in repo names) * Added `target_compatible_with` and `target_settings` args, which become the settings used on the generated toolchain() definition. The platform settings are version specific, i.e. the key `(python_version, platform)` is what maps to the TCW/TS values. If an existing platform is used, it'll override the defaults that normally come from the PLATFORMS global for the particular version. If a new platform is used, it creates a new platform entry with those settings. Along the way: * Added various docs about internal variables so they're easier to grok at a glance. Work towards https://github.com/bazel-contrib/rules_python/issues/2081 --- CHANGELOG.md | 4 + MODULE.bazel | 16 + docs/toolchains.md | 67 ++++ internal_dev_setup.bzl | 2 +- python/BUILD.bazel | 1 + python/private/BUILD.bazel | 6 + python/private/platform_info.bzl | 34 ++ python/private/py_repositories.bzl | 2 +- python/private/python.bzl | 311 +++++++++++++++--- python/private/python_repository.bzl | 3 +- python/private/pythons_hub.bzl | 30 +- python/private/repo_utils.bzl | 20 +- python/private/toolchains_repo.bzl | 131 ++++++-- python/versions.bzl | 56 +--- tests/bootstrap_impls/bin.py | 1 + tests/python/python_tests.bzl | 23 ++ tests/support/BUILD.bazel | 13 + tests/support/sh_py_run_test.bzl | 2 + tests/toolchains/BUILD.bazel | 13 + .../custom_platform_toolchain_test.py | 15 + 20 files changed, 609 insertions(+), 141 deletions(-) create mode 100644 python/private/platform_info.bzl create mode 100644 tests/toolchains/custom_platform_toolchain_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a6bdf0a96..a113c7411f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,6 +107,10 @@ END_UNRELEASED_TEMPLATE Set the `RULES_PYTHON_ENABLE_PIPSTAR=1` environment variable to enable it. * (utils) Add a way to run a REPL for any `rules_python` target that returns a `PyInfo` provider. +* (toolchains) Arbitrary python-build-standalone runtimes can be registered + and activated with custom flags. See the [Registering custom runtimes] + docs and {obj}`single_version_platform_override()` API docs for more + information. {#v0-0-0-removed} ### Removed diff --git a/MODULE.bazel b/MODULE.bazel index d3a95350e5..144e130c1b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -125,6 +125,22 @@ dev_python.override( register_all_versions = True, ) +# For testing an arbitrary runtime triggered by a custom flag. +# See //tests/toolchains:custom_platform_toolchain_test +dev_python.single_version_platform_override( + platform = "linux-x86-install-only-stripped", + python_version = "3.13.1", + sha256 = "56817aa976e4886bec1677699c136cb01c1cdfe0495104c0d8ef546541864bbb", + target_compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ], + target_settings = [ + "@@//tests/support:is_custom_runtime_linux-x86-install-only-stripped", + ], + urls = ["https://github.com/astral-sh/python-build-standalone/releases/download/20250115/cpython-3.13.1+20250115-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"], +) + dev_pip = use_extension( "//python/extensions:pip.bzl", "pip", diff --git a/docs/toolchains.md b/docs/toolchains.md index ada887c945..57d43d27f1 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -243,6 +243,73 @@ existing attributes: * Adding additional Python versions via {bzl:obj}`python.single_version_override` or {bzl:obj}`python.single_version_platform_override`. +### Registering custom runtimes + +Because the python-build-standalone project has _thousands_ of prebuilt runtimes +available, rules_python only includes popular runtimes in its built in +configurations. If you want to use a runtime that isn't already known to +rules_python then {obj}`single_version_platform_override()` can be used to do +so. In short, it allows specifying an arbitrary URL and using custom flags +to control when a runtime is used. + +In the example below, we register a particular python-build-standalone runtime +that is activated for Linux x86 builds when the custom flag +`--//:runtime=my-custom-runtime` is set. + +``` +# File: MODULE.bazel +bazel_dep(name = "bazel_skylib", version = "1.7.1.") +bazel_dep(name = "rules_python", version = "1.5.0") +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.single_version_platform_override( + platform = "my-platform", + python_version = "3.13.3", + sha256 = "01d08b9bc8a96698b9d64c2fc26da4ecc4fa9e708ce0a34fb88f11ab7e552cbd", + os_name = "linux", + arch = "x86_64", + target_settings = [ + "@@//:runtime=my-custom-runtime", + ], + urls = ["https://github.com/astral-sh/python-build-standalone/releases/download/20250409/cpython-3.13.3+20250409-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"], +) +# File: //:BUILD.bazel +load("@bazel_skylib//rules:common_settings.bzl", "string_flag") +string_flag( + name = "custom_runtime", + build_setting_default = "", +) +config_setting( + name = "is_custom_runtime_linux-x86-install-only-stripped", + flag_values = { + ":custom_runtime": "linux-x86-install-only-stripped", + }, +) +``` + +Notes: +- While any URL and archive can be used, it's assumed their content looks how + a python-build-standalone archive looks. +- A "version aware" toolchain is registered, which means the Python version flag + must also match (e.g. `--@rules_python//python/config_settings:python_version=3.13.3` + must be set -- see `minor_mapping` and `is_default` for controls and docs + about version matching and selection). +- The `target_compatible_with` attribute can be used to entirely specify the + arg of the same name the toolchain uses. +- The labels in `target_settings` must be absolute; `@@` refers to the main repo. +- The `target_settings` are `config_setting` targets, which means you can + customize how matching occurs. + +:::{seealso} +See {obj}`//python/config_settings` for flags rules_python already defines +that can be used with `target_settings`. Some particular ones of note are: +{flag}`--py_linux_libc` and {flag}`--py_freethreaded`, among others. +::: + +:::{versionadded} VERSION_NEXT_FEATURE +Added support for custom platform names, `target_compatible_with`, and +`target_settings` with `single_version_platform_override`. +::: + ### Using defined toolchains from WORKSPACE It is possible to use toolchains defined in `MODULE.bazel` in `WORKSPACE`. For example diff --git a/internal_dev_setup.bzl b/internal_dev_setup.bzl index 62a11ab1d4..c37c59a5da 100644 --- a/internal_dev_setup.bzl +++ b/internal_dev_setup.bzl @@ -42,7 +42,7 @@ def rules_python_internal_setup(): toolchain_platform_keys = {}, toolchain_python_versions = {}, toolchain_set_python_version_constraints = {}, - base_toolchain_repo_names = [], + host_compatible_repo_names = [], ) runtime_env_repo(name = "rules_python_runtime_env_tc_info") diff --git a/python/BUILD.bazel b/python/BUILD.bazel index 867c43478a..58cff5b99d 100644 --- a/python/BUILD.bazel +++ b/python/BUILD.bazel @@ -247,6 +247,7 @@ bzl_library( name = "versions_bzl", srcs = ["versions.bzl"], visibility = ["//:__subpackages__"], + deps = ["//python/private:platform_info_bzl"], ) # NOTE: Remember to add bzl_library targets to //tests:bzl_libraries diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index ce22421300..b319919305 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -241,11 +241,17 @@ bzl_library( ], ) +bzl_library( + name = "platform_info_bzl", + srcs = ["platform_info.bzl"], +) + bzl_library( name = "python_bzl", srcs = ["python.bzl"], deps = [ ":full_version_bzl", + ":platform_info_bzl", ":python_register_toolchains_bzl", ":pythons_hub_bzl", ":repo_utils_bzl", diff --git a/python/private/platform_info.bzl b/python/private/platform_info.bzl new file mode 100644 index 0000000000..3f7dc00165 --- /dev/null +++ b/python/private/platform_info.bzl @@ -0,0 +1,34 @@ +"""Helper to define a struct used to define platform metadata.""" + +def platform_info( + *, + compatible_with = [], + flag_values = {}, + target_settings = [], + os_name, + arch): + """Creates a struct of platform metadata. + + This is just a helper to ensure structs are created the same and + the meaning/values are documented. + + Args: + compatible_with: list[str], where the values are string labels. These + are the target_compatible_with values to use with the toolchain + flag_values: dict[str|Label, Any] of config_setting.flag_values + compatible values. DEPRECATED -- use target_settings instead + target_settings: list[str], where the values are string labels. These + are the target_settings values to use with the toolchain. + os_name: str, the os name; must match the name used in `@platfroms//os` + arch: str, the cpu name; must match the name used in `@platforms//cpu` + + Returns: + A struct with attributes and values matching the args. + """ + return struct( + compatible_with = compatible_with, + flag_values = flag_values, + target_settings = target_settings, + os_name = os_name, + arch = arch, + ) diff --git a/python/private/py_repositories.bzl b/python/private/py_repositories.bzl index b5bd93b7c1..10bc06630b 100644 --- a/python/private/py_repositories.bzl +++ b/python/private/py_repositories.bzl @@ -47,7 +47,7 @@ def py_repositories(): toolchain_platform_keys = {}, toolchain_python_versions = {}, toolchain_set_python_version_constraints = {}, - base_toolchain_repo_names = [], + host_compatible_repo_names = [], ) http_archive( name = "bazel_skylib", diff --git a/python/private/python.bzl b/python/private/python.bzl index a7e257601f..8e23668879 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -18,29 +18,44 @@ load("@bazel_features//:features.bzl", "bazel_features") load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URL", "PLATFORMS", "TOOL_VERSIONS") load(":auth.bzl", "AUTH_ATTRS") load(":full_version.bzl", "full_version") +load(":platform_info.bzl", "platform_info") load(":python_register_toolchains.bzl", "python_register_toolchains") load(":pythons_hub.bzl", "hub_repo") load(":repo_utils.bzl", "repo_utils") -load(":toolchains_repo.bzl", "host_compatible_python_repo", "multi_toolchain_aliases", "sorted_host_platforms") +load( + ":toolchains_repo.bzl", + "host_compatible_python_repo", + "multi_toolchain_aliases", + "sorted_host_platform_names", + "sorted_host_platforms", +) load(":util.bzl", "IS_BAZEL_6_4_OR_HIGHER") load(":version.bzl", "version") -def parse_modules(*, module_ctx, _fail = fail): +def parse_modules(*, module_ctx, logger, _fail = fail): """Parse the modules and return a struct for registrations. Args: module_ctx: {type}`module_ctx` module context. + logger: {type}`repo_utils.logger` A logger to use. _fail: {type}`function` the failure function, mainly for testing. Returns: A struct with the following attributes: - * `toolchains`: The list of toolchains to register. The last - element is special and is treated as the default toolchain. + * `toolchains`: {type}`list[ToolchainConfig]` The list of toolchains to + register. The last element is special and is treated as the default + toolchain. * `config`: Various toolchain config, see `_get_toolchain_config`. * `debug_info`: {type}`None | dict` extra information to be passed to the debug repo. * `platforms`: {type}`dict[str, platform_info]` of the base set of platforms toolchains should be created for, if possible. + + ToolchainConfig struct: + * python_version: str, full python version string + * name: str, the base toolchain name, e.g., "python_3_10", no + platform suffix. + * register_coverage_tool: bool """ if module_ctx.os.environ.get("RULES_PYTHON_BZLMOD_DEBUG", "0") == "1": debug_info = { @@ -64,8 +79,6 @@ def parse_modules(*, module_ctx, _fail = fail): ignore_root_user_error = None - logger = repo_utils.logger(module_ctx, "python") - # if the root module does not register any toolchain then the # ignore_root_user_error takes its default value: True if not module_ctx.modules[0].tags.toolchain: @@ -265,19 +278,37 @@ def parse_modules(*, module_ctx, _fail = fail): ) def _python_impl(module_ctx): - py = parse_modules(module_ctx = module_ctx) + logger = repo_utils.logger(module_ctx, "python") + py = parse_modules(module_ctx = module_ctx, logger = logger) + + # Host compatible runtime repos + # dict[str version, struct] where struct has: + # * full_python_version: str + # * platform: platform_info struct + # * platform_name: str platform name + # * impl_repo_name: str repo name of the runtime's python_repository() repo + all_host_compatible_impls = {} + + # Host compatible repos that still need to be created because, when + # creating the actual runtime repo, there wasn't a host-compatible + # variant defined for it. + # dict[str reponame, struct] where struct has: + # * compatible_version: str, e.g. 3.10 or 3.10.1. The version the host + # repo should be compatible with + # * full_python_version: str, e.g. 3.10.1, the full python version of + # the toolchain that still needs a host repo created. + needed_host_repos = {} # list of structs; see inline struct call within the loop below. toolchain_impls = [] - # list[str] of the base names of toolchain repos - base_toolchain_repo_names = [] + # list[str] of the repo names for host compatible repos + all_host_compatible_repo_names = [] # Create the underlying python_repository repos that contain the # python runtimes and their toolchain implementation definitions. for i, toolchain_info in enumerate(py.toolchains): is_last = (i + 1) == len(py.toolchains) - base_toolchain_repo_names.append(toolchain_info.name) # Ensure that we pass the full version here. full_python_version = full_version( @@ -298,6 +329,8 @@ def _python_impl(module_ctx): _internal_bzlmod_toolchain_call = True, **kwargs ) + if not register_result.impl_repos: + continue host_platforms = {} for repo_name, (platform_name, platform_info) in register_result.impl_repos.items(): @@ -318,27 +351,81 @@ def _python_impl(module_ctx): set_python_version_constraint = is_last, )) if _is_compatible_with_host(module_ctx, platform_info): - host_platforms[platform_name] = platform_info + host_compat_entry = struct( + full_python_version = full_python_version, + platform = platform_info, + platform_name = platform_name, + impl_repo_name = repo_name, + ) + host_platforms[platform_name] = host_compat_entry + all_host_compatible_impls.setdefault(full_python_version, []).append( + host_compat_entry, + ) + parsed_version = version.parse(full_python_version) + all_host_compatible_impls.setdefault( + "{}.{}".format(*parsed_version.release[0:2]), + [], + ).append(host_compat_entry) + + host_repo_name = toolchain_info.name + "_host" + if host_platforms: + all_host_compatible_repo_names.append(host_repo_name) + host_platforms = sorted_host_platforms(host_platforms) + entries = host_platforms.values() + host_compatible_python_repo( + name = host_repo_name, + base_name = host_repo_name, + # NOTE: Order matters. The first found to be compatible is + # (usually) used. + platforms = host_platforms.keys(), + os_names = {str(i): e.platform.os_name for i, e in enumerate(entries)}, + arch_names = {str(i): e.platform.arch for i, e in enumerate(entries)}, + python_versions = {str(i): e.full_python_version for i, e in enumerate(entries)}, + impl_repo_names = {str(i): e.impl_repo_name for i, e in enumerate(entries)}, + ) + else: + needed_host_repos[host_repo_name] = struct( + compatible_version = toolchain_info.python_version, + full_python_version = full_python_version, + ) + + if needed_host_repos: + for key, entries in all_host_compatible_impls.items(): + all_host_compatible_impls[key] = sorted( + entries, + reverse = True, + key = lambda e: version.key(version.parse(e.full_python_version)), + ) - host_platforms = sorted_host_platforms(host_platforms) + for host_repo_name, info in needed_host_repos.items(): + choices = [] + if info.compatible_version not in all_host_compatible_impls: + logger.warn("No host compatible runtime found compatible with version {}".format(info.compatible_version)) + continue + + choices = all_host_compatible_impls[info.compatible_version] + platform_keys = [ + # We have to prepend the offset because the same platform + # name might occur across different versions + "{}_{}".format(i, entry.platform_name) + for i, entry in enumerate(choices) + ] + platform_keys = sorted_host_platform_names(platform_keys) + + all_host_compatible_repo_names.append(host_repo_name) host_compatible_python_repo( - name = toolchain_info.name + "_host", - # NOTE: Order matters. The first found to be compatible is (usually) used. - platforms = host_platforms.keys(), - os_names = { - str(i): platform_info.os_name - for i, platform_info in enumerate(host_platforms.values()) - }, - arch_names = { - str(i): platform_info.arch - for i, platform_info in enumerate(host_platforms.values()) + name = host_repo_name, + base_name = host_repo_name, + platforms = platform_keys, + impl_repo_names = { + str(i): entry.impl_repo_name + for i, entry in enumerate(choices) }, - python_version = full_python_version, + os_names = {str(i): entry.platform.os_name for i, entry in enumerate(choices)}, + arch_names = {str(i): entry.platform.arch for i, entry in enumerate(choices)}, + python_versions = {str(i): entry.full_python_version for i, entry in enumerate(choices)}, ) - # List of the base names ("python_3_10") for the toolchain repos - base_toolchain_repo_names = [] - # list[str] The infix to use for the resulting toolchain() `name` arg. toolchain_names = [] @@ -399,7 +486,7 @@ def _python_impl(module_ctx): toolchain_platform_keys = toolchain_platform_keys, toolchain_python_versions = toolchain_python_versions, toolchain_set_python_version_constraints = toolchain_set_python_version_constraints, - base_toolchain_repo_names = [t.name for t in py.toolchains], + host_compatible_repo_names = sorted(all_host_compatible_repo_names), default_python_version = py.default_python_version, minor_mapping = py.config.minor_mapping, python_versions = list(py.config.default["tool_versions"].keys()), @@ -583,9 +670,56 @@ def _process_single_version_platform_overrides(*, tag, _fail = fail, default): available_versions[tag.python_version].setdefault("sha256", {})[tag.platform] = tag.sha256 if tag.strip_prefix: available_versions[tag.python_version].setdefault("strip_prefix", {})[tag.platform] = tag.strip_prefix + if tag.urls: available_versions[tag.python_version].setdefault("url", {})[tag.platform] = tag.urls + # If platform is customized, or doesn't exist, (re)define one. + if ((tag.target_compatible_with or tag.target_settings or tag.os_name or tag.arch) or + tag.platform not in default["platforms"]): + os_name = tag.os_name + arch = tag.arch + + if not tag.target_compatible_with: + target_compatible_with = [] + if os_name: + target_compatible_with.append("@platforms//os:{}".format( + repo_utils.get_platforms_os_name(os_name), + )) + if arch: + target_compatible_with.append("@platforms//cpu:{}".format( + repo_utils.get_platforms_cpu_name(arch), + )) + else: + target_compatible_with = tag.target_compatible_with + + # For lack of a better option, give a bogus value. It only affects + # if the runtime is considered host-compatible. + if not os_name: + os_name = "UNKNOWN_CUSTOM_OS" + if not arch: + arch = "UNKNOWN_CUSTOM_ARCH" + + # Move the override earlier in the ordering -- the platform key ordering + # becomes the toolchain ordering within the version. This allows the + # override to have a superset of constraints from a regular runtimes + # (e.g. same platform, but with a custom flag required). + override_first = { + tag.platform: platform_info( + compatible_with = target_compatible_with, + target_settings = tag.target_settings, + os_name = os_name, + arch = arch, + ), + } + for key, value in default["platforms"].items(): + # Don't replace our override with the old value + if key in override_first: + continue + override_first[key] = value + + default["platforms"] = override_first + def _process_global_overrides(*, tag, default, _fail = fail): if tag.available_python_versions: available_versions = default["tool_versions"] @@ -664,22 +798,29 @@ def _get_toolchain_config(*, modules, _fail = fail): """ # Items that can be overridden - available_versions = { - version: { - # Use a dicts straight away so that we could do URL overrides for a - # single version. - "sha256": dict(item["sha256"]), - "strip_prefix": { - platform: item["strip_prefix"] - for platform in item["sha256"] - } if type(item["strip_prefix"]) == type("") else item["strip_prefix"], - "url": { - platform: [item["url"]] - for platform in item["sha256"] - } if type(item["url"]) == type("") else item["url"], - } - for version, item in TOOL_VERSIONS.items() - } + available_versions = {} + for py_version, item in TOOL_VERSIONS.items(): + available_versions[py_version] = {} + available_versions[py_version]["sha256"] = dict(item["sha256"]) + platforms = item["sha256"].keys() + + strip_prefix = item["strip_prefix"] + if type(strip_prefix) == type(""): + available_versions[py_version]["strip_prefix"] = { + platform: strip_prefix + for platform in platforms + } + else: + available_versions[py_version]["strip_prefix"] = dict(strip_prefix) + url = item["url"] + if type(url) == type(""): + available_versions[py_version]["url"] = { + platform: url + for platform in platforms + } + else: + available_versions[py_version]["url"] = dict(url) + default = { "base_url": DEFAULT_RELEASE_BASE_URL, "platforms": dict(PLATFORMS), # Copy so it's mutable. @@ -1084,10 +1225,48 @@ configuration, please use {obj}`single_version_override`. ::: """, attrs = { + "arch": attr.string( + doc = """ +The arch (cpu) the runtime is compatible with. + +If not set, then the runtime cannot be used as a `python_X_Y_host` runtime. + +If set, the `os_name`, `target_compatible_with` and `target_settings` attributes +should also be set. + +The values should be one of the values in `@platforms//cpu` + +:::{seealso} +Docs for [Registering custom runtimes] +::: + +:::{{versionadded}} VERSION_NEXT_FEATURE +::: +""", + ), "coverage_tool": attr.label( doc = """\ The coverage tool to be used for a particular Python interpreter. This can override `rules_python` defaults. +""", + ), + "os_name": attr.string( + doc = """ +The host OS the runtime is compatible with. + +If not set, then the runtime cannot be used as a `python_X_Y_host` runtime. + +If set, the `os_name`, `target_compatible_with` and `target_settings` attributes +should also be set. + +The values should be one of the values in `@platforms//os` + +:::{seealso} +Docs for [Registering custom runtimes] +::: + +:::{{versionadded}} VERSION_NEXT_FEATURE +::: """, ), "patch_strip": attr.int( @@ -1101,8 +1280,20 @@ The coverage tool to be used for a particular Python interpreter. This can overr ), "platform": attr.string( mandatory = True, - values = PLATFORMS.keys(), - doc = "The platform to override the values for, must be one of:\n{}.".format("\n".join(sorted(["* `{}`".format(p) for p in PLATFORMS]))), + doc = """ +The platform to override the values for, typically one of:\n +{platforms} + +Other values are allowed, in which case, `target_compatible_with`, +`target_settings`, `os_name`, and `arch` should be specified so the toolchain is +only used when appropriate. + +:::{{versionchanged}} VERSION_NEXT_FEATURE +Arbitrary platform strings allowed. +::: +""".format( + platforms = "\n".join(sorted(["* `{}`".format(p) for p in PLATFORMS])), + ), ), "python_version": attr.string( mandatory = True, @@ -1117,6 +1308,36 @@ The coverage tool to be used for a particular Python interpreter. This can overr doc = "The 'strip_prefix' for the archive, defaults to 'python'.", default = "python", ), + "target_compatible_with": attr.string_list( + doc = """ +The `target_compatible_with` values to use for the toolchain definition. + +If not set, then `os_name` and `arch` will be used to populate it. + +If set, `target_settings`, `os_name`, and `arch` should also be set. + +:::{seealso} +Docs for [Registering custom runtimes] +::: + +:::{{versionadded}} VERSION_NEXT_FEATURE +::: +""", + ), + "target_settings": attr.string_list( + doc = """ +The `target_setings` values to use for the toolchain definition. + +If set, `target_compatible_with`, `os_name`, and `arch` should also be set. + +:::{seealso} +Docs for [Registering custom runtimes] +::: + +:::{{versionadded}} VERSION_NEXT_FEATURE +::: +""", + ), "urls": attr.string_list( mandatory = False, doc = "The URL template to fetch releases for this Python version. If the URL template results in a relative fragment, default base URL is going to be used. Occurrences of `{python_version}`, `{platform}` and `{build}` will be interpolated based on the contents in the override and the known {attr}`platform` values.", diff --git a/python/private/python_repository.bzl b/python/private/python_repository.bzl index fd86b415cc..cb0731e6eb 100644 --- a/python/private/python_repository.bzl +++ b/python/private/python_repository.bzl @@ -15,7 +15,7 @@ """This file contains repository rules and macros to support toolchain registration. """ -load("//python:versions.bzl", "FREETHREADED", "INSTALL_ONLY", "PLATFORMS") +load("//python:versions.bzl", "FREETHREADED", "INSTALL_ONLY") load(":auth.bzl", "get_auth") load(":repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") load(":text_util.bzl", "render") @@ -327,7 +327,6 @@ function defaults (e.g. `single_version_override` for `MODULE.bazel` files. "platform": attr.string( doc = "The platform name for the Python interpreter tarball.", mandatory = True, - values = PLATFORMS.keys(), ), "python_version": attr.string( doc = "The Python version.", diff --git a/python/private/pythons_hub.bzl b/python/private/pythons_hub.bzl index 53351cacb9..cc25b4ba1d 100644 --- a/python/private/pythons_hub.bzl +++ b/python/private/pythons_hub.bzl @@ -84,13 +84,7 @@ def _hub_build_file_content(rctx): ) _interpreters_bzl_template = """ -INTERPRETER_LABELS = {{ -{interpreter_labels} -}} -""" - -_line_for_hub_template = """\ - "{name}_host": Label("@{name}_host//:python"), +INTERPRETER_LABELS = {labels} """ _versions_bzl_template = """ @@ -110,15 +104,16 @@ def _hub_repo_impl(rctx): # Create a dict that is later used to create # a symlink to a interpreter. - interpreter_labels = "".join([ - _line_for_hub_template.format(name = name) - for name in rctx.attr.base_toolchain_repo_names - ]) - rctx.file( "interpreters.bzl", _interpreters_bzl_template.format( - interpreter_labels = interpreter_labels, + labels = render.dict( + { + name: 'Label("@{}//:python")'.format(name) + for name in rctx.attr.host_compatible_repo_names + }, + value_repr = str, + ), ), executable = False, ) @@ -144,15 +139,14 @@ This rule also writes out the various toolchains for the different Python versio """, implementation = _hub_repo_impl, attrs = { - "base_toolchain_repo_names": attr.string_list( - doc = "The base repo name for toolchains ('python_3_10', no " + - "platform suffix)", - mandatory = True, - ), "default_python_version": attr.string( doc = "Default Python version for the build in `X.Y` or `X.Y.Z` format.", mandatory = True, ), + "host_compatible_repo_names": attr.string_list( + doc = "Names of `host_compatible_python_repo` repos.", + mandatory = True, + ), "minor_mapping": attr.string_dict( doc = "The minor mapping of the `X.Y` to `X.Y.Z` format that is used in config settings.", mandatory = True, diff --git a/python/private/repo_utils.bzl b/python/private/repo_utils.bzl index eee56ec86c..32a5b70e15 100644 --- a/python/private/repo_utils.bzl +++ b/python/private/repo_utils.bzl @@ -31,13 +31,15 @@ def _is_repo_debug_enabled(mrctx): """ return _getenv(mrctx, REPO_DEBUG_ENV_VAR) == "1" -def _logger(mrctx, name = None): +def _logger(mrctx = None, name = None, verbosity_level = None): """Creates a logger instance for printing messages. Args: mrctx: repository_ctx or module_ctx object. If the attribute `_rule_name` is present, it will be included in log messages. name: name for the logger. Optional for repository_ctx usage. + verbosity_level: {type}`int | None` verbosity level. If not set, + taken from `mrctx` Returns: A struct with attributes logging: trace, debug, info, warn, fail. @@ -46,13 +48,14 @@ def _logger(mrctx, name = None): the logger injected into the function work as expected by terminating on the given line. """ - if _is_repo_debug_enabled(mrctx): - verbosity_level = "DEBUG" - else: - verbosity_level = "WARN" + if verbosity_level == None: + if _is_repo_debug_enabled(mrctx): + verbosity_level = "DEBUG" + else: + verbosity_level = "WARN" - env_var_verbosity = _getenv(mrctx, REPO_VERBOSITY_ENV_VAR) - verbosity_level = env_var_verbosity or verbosity_level + env_var_verbosity = _getenv(mrctx, REPO_VERBOSITY_ENV_VAR) + verbosity_level = env_var_verbosity or verbosity_level verbosity = { "DEBUG": 2, @@ -376,7 +379,7 @@ def _get_platforms_os_name(mrctx): """Return the name in @platforms//os for the host os. Args: - mrctx: module_ctx or repository_ctx. + mrctx: {type}`module_ctx | repository_ctx` Returns: `str`. The target name. @@ -405,6 +408,7 @@ def _get_platforms_cpu_name(mrctx): `str`. The target name. """ arch = mrctx.os.arch.lower() + if arch in ["i386", "i486", "i586", "i686", "i786", "x86"]: return "x86_32" if arch in ["amd64", "x86_64", "x64"]: diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl index 2476889583..93bbb52108 100644 --- a/python/private/toolchains_repo.bzl +++ b/python/private/toolchains_repo.bzl @@ -309,11 +309,11 @@ actions.""", environ = [REPO_DEBUG_ENV_VAR], ) -def _host_compatible_python_repo(rctx): +def _host_compatible_python_repo_impl(rctx): rctx.file("BUILD.bazel", _HOST_TOOLCHAIN_BUILD_CONTENT) os_name = repo_utils.get_platforms_os_name(rctx) - host_platform = _get_host_platform( + impl_repo_name = _get_host_impl_repo_name( rctx = rctx, logger = repo_utils.logger(rctx), python_version = rctx.attr.python_version, @@ -321,10 +321,11 @@ def _host_compatible_python_repo(rctx): cpu_name = repo_utils.get_platforms_cpu_name(rctx), platforms = rctx.attr.platforms, ) - repo = "@@{py_repository}_{host_platform}".format( - py_repository = rctx.attr.name[:-len("_host")], - host_platform = host_platform, - ) + + # Bzlmod quirk: A repository rule can't, in its **implemention function**, + # resolve an apparent repo name referring to a repo created by the same + # bzlmod extension. To work around this, we use a canonical label. + repo = "@@{}".format(impl_repo_name) rctx.report_progress("Symlinking interpreter files to the target platform") host_python_repo = rctx.path(Label("{repo}//:BUILD.bazel".format(repo = repo))) @@ -380,26 +381,76 @@ def _host_compatible_python_repo(rctx): # NOTE: The term "toolchain" is a misnomer for this rule. This doesn't define # a repo with toolchains or toolchain implementations. host_compatible_python_repo = repository_rule( - _host_compatible_python_repo, + implementation = _host_compatible_python_repo_impl, doc = """\ Creates a repository with a shorter name meant to be used in the repository_ctx, which needs to have `symlinks` for the interpreter. This is separate from the toolchain_aliases repo because referencing the `python` interpreter target from this repo causes an eager fetch of the toolchain for the host platform. - """, + +This repo has two ways in which is it called: + +1. Workspace. The `platforms` attribute is set, which are keys into the + PLATFORMS global. It assumes `name` + is a + valid repo name which it can use as the backing repo. + +2. Bzlmod. All platform and backing repo information is passed in via the + arch_names, impl_repo_names, os_names, python_versions attributes. +""", attrs = { "arch_names": attr.string_dict( doc = """ -If set, overrides the platform metadata. Keyed by index in `platforms` +Arch (cpu) names. Only set in bzlmod. Keyed by index in `platforms` +""", + ), + "base_name": attr.string( + doc = """ +The name arg, but without bzlmod canonicalization applied. Only set in bzlmod. +""", + ), + "impl_repo_names": attr.string_dict( + doc = """ +The names of backing runtime repos. Only set in bzlmod. The names must be repos +in the same extension as creates the host repo. Keyed by index in `platforms`. """, ), "os_names": attr.string_dict( doc = """ -If set, overrides the platform metadata. Keyed by index in `platforms` +If set, overrides the platform metadata. Only set in bzlmod. Keyed by +index in `platforms` +""", + ), + "platforms": attr.string_list( + mandatory = True, + doc = """ +Platform names (workspace) or platform name-like keys (bzlmod) + +NOTE: The order of this list matters. The first platform that is compatible +with the host will be selected; this can be customized by using the +`RULES_PYTHON_REPO_TOOLCHAIN_*` env vars. + +The values passed vary depending on workspace vs bzlmod. + +Workspace: the values are keys into the `PLATFORMS` dict and are the suffix +to append to `name` to point to the backing repo name. + +Bzlmod: The values are arbitrary keys to create the platform map from the +other attributes (os_name, arch_names, et al). +""", + ), + "python_version": attr.string( + doc = """ +Full python version, Major.Minor.Micro. + +Only set in workspace calls. +""", + ), + "python_versions": attr.string_dict( + doc = """ +If set, the Python version for the corresponding selected platform. Values in +Major.Minor.Micro format. Keyed by index in `platforms`. """, ), - "platforms": attr.string_list(mandatory = True), - "python_version": attr.string(mandatory = True), "_rule_name": attr.string(default = "host_compatible_python_repo"), "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")), }, @@ -435,8 +486,8 @@ multi_toolchain_aliases = repository_rule( }, ) -def sorted_host_platforms(platform_map): - """Sort the keys in the platform map to give correct precedence. +def sorted_host_platform_names(platform_names): + """Sort platform names to give correct precedence. The order of keys in the platform mapping matters for the host toolchain selection. When multiple runtimes are compatible with the host, we take the @@ -453,11 +504,10 @@ def sorted_host_platforms(platform_map): is an innocous looking formatter disable directive. Args: - platform_map: a mapping of platforms and their metadata. + platform_names: a list of platform names Returns: - dict; the same values, but with the keys inserted in the desired - order so that iteration happens in the desired order. + list[str] the same values, but in the desired order. """ def platform_keyer(name): @@ -467,13 +517,26 @@ def sorted_host_platforms(platform_map): 1 if FREETHREADED in name else 0, ) - sorted_platform_keys = sorted(platform_map.keys(), key = platform_keyer) + return sorted(platform_names, key = platform_keyer) + +def sorted_host_platforms(platform_map): + """Sort the keys in the platform map to give correct precedence. + + See sorted_host_platform_names for explanation. + + Args: + platform_map: a mapping of platforms and their metadata. + + Returns: + dict; the same values, but with the keys inserted in the desired + order so that iteration happens in the desired order. + """ return { key: platform_map[key] - for key in sorted_platform_keys + for key in sorted_host_platform_names(platform_map.keys()) } -def _get_host_platform(*, rctx, logger, python_version, os_name, cpu_name, platforms): +def _get_host_impl_repo_name(*, rctx, logger, python_version, os_name, cpu_name, platforms): """Gets the host platform. Args: @@ -488,24 +551,40 @@ def _get_host_platform(*, rctx, logger, python_version, os_name, cpu_name, platf """ if rctx.attr.os_names: platform_map = {} + base_name = rctx.attr.base_name + if not base_name: + fail("The `base_name` attribute must be set under bzlmod") for i, platform_name in enumerate(platforms): key = str(i) + impl_repo_name = rctx.attr.impl_repo_names[key] + impl_repo_name = rctx.name.replace(base_name, impl_repo_name) platform_map[platform_name] = struct( os_name = rctx.attr.os_names[key], arch = rctx.attr.arch_names[key], + python_version = rctx.attr.python_versions[key], + impl_repo_name = impl_repo_name, ) else: - platform_map = sorted_host_platforms(PLATFORMS) + base_name = rctx.name.removesuffix("_host") + platform_map = {} + for platform_name, info in sorted_host_platforms(PLATFORMS).items(): + platform_map[platform_name] = struct( + os_name = info.os_name, + arch = info.arch, + python_version = python_version, + impl_repo_name = "{}_{}".format(base_name, platform_name), + ) candidates = [] for platform in platforms: meta = platform_map[platform] if meta.os_name == os_name and meta.arch == cpu_name: - candidates.append(platform) + candidates.append((platform, meta)) if len(candidates) == 1: - return candidates[0] + platform_name, meta = candidates[0] + return meta.impl_repo_name if candidates: env_var = "RULES_PYTHON_REPO_TOOLCHAIN_{}_{}_{}".format( @@ -525,7 +604,11 @@ def _get_host_platform(*, rctx, logger, python_version, os_name, cpu_name, platf candidates = [preference] if candidates: - return candidates[0] + platform_name, meta = candidates[0] + suffix = meta.impl_repo_name + if not suffix: + suffix = platform_name + return suffix return logger.fail("Could not find a compatible 'host' python for '{os_name}', '{cpu_name}' from the loaded platforms: {platforms}".format( os_name = os_name, diff --git a/python/versions.bzl b/python/versions.bzl index 166cc98851..e712a2e126 100644 --- a/python/versions.bzl +++ b/python/versions.bzl @@ -15,6 +15,8 @@ """The Python versions we use for the toolchains. """ +load("//python/private:platform_info.bzl", "platform_info") + # Values present in the @platforms//os package MACOS_NAME = "osx" LINUX_NAME = "linux" @@ -684,42 +686,12 @@ MINOR_MAPPING = { "3.13": "3.13.2", } -def _platform_info( - *, - compatible_with = [], - flag_values = {}, - target_settings = [], - os_name, - arch): - """Creates a struct of platform metadata. - - Args: - compatible_with: list[str], where the values are string labels. These - are the target_compatible_with values to use with the toolchain - flag_values: dict[str|Label, Any] of config_setting.flag_values - compatible values. DEPRECATED -- use target_settings instead - target_settings: list[str], where the values are string labels. These - are the target_settings values to use with the toolchain. - os_name: str, the os name; must match the name used in `@platfroms//os` - arch: str, the cpu name; must match the name used in `@platforms//cpu` - - Returns: - A struct with attributes and values matching the args. - """ - return struct( - compatible_with = compatible_with, - flag_values = flag_values, - target_settings = target_settings, - os_name = os_name, - arch = arch, - ) - def _generate_platforms(): is_libc_glibc = str(Label("//python/config_settings:_is_py_linux_libc_glibc")) is_libc_musl = str(Label("//python/config_settings:_is_py_linux_libc_musl")) platforms = { - "aarch64-apple-darwin": _platform_info( + "aarch64-apple-darwin": platform_info( compatible_with = [ "@platforms//os:macos", "@platforms//cpu:aarch64", @@ -727,7 +699,7 @@ def _generate_platforms(): os_name = MACOS_NAME, arch = "aarch64", ), - "aarch64-unknown-linux-gnu": _platform_info( + "aarch64-unknown-linux-gnu": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:aarch64", @@ -738,7 +710,7 @@ def _generate_platforms(): os_name = LINUX_NAME, arch = "aarch64", ), - "armv7-unknown-linux-gnu": _platform_info( + "armv7-unknown-linux-gnu": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:armv7", @@ -749,7 +721,7 @@ def _generate_platforms(): os_name = LINUX_NAME, arch = "arm", ), - "i386-unknown-linux-gnu": _platform_info( + "i386-unknown-linux-gnu": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:i386", @@ -760,7 +732,7 @@ def _generate_platforms(): os_name = LINUX_NAME, arch = "x86_32", ), - "ppc64le-unknown-linux-gnu": _platform_info( + "ppc64le-unknown-linux-gnu": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:ppc", @@ -771,7 +743,7 @@ def _generate_platforms(): os_name = LINUX_NAME, arch = "ppc", ), - "riscv64-unknown-linux-gnu": _platform_info( + "riscv64-unknown-linux-gnu": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:riscv64", @@ -782,7 +754,7 @@ def _generate_platforms(): os_name = LINUX_NAME, arch = "riscv64", ), - "s390x-unknown-linux-gnu": _platform_info( + "s390x-unknown-linux-gnu": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:s390x", @@ -793,7 +765,7 @@ def _generate_platforms(): os_name = LINUX_NAME, arch = "s390x", ), - "x86_64-apple-darwin": _platform_info( + "x86_64-apple-darwin": platform_info( compatible_with = [ "@platforms//os:macos", "@platforms//cpu:x86_64", @@ -801,7 +773,7 @@ def _generate_platforms(): os_name = MACOS_NAME, arch = "x86_64", ), - "x86_64-pc-windows-msvc": _platform_info( + "x86_64-pc-windows-msvc": platform_info( compatible_with = [ "@platforms//os:windows", "@platforms//cpu:x86_64", @@ -809,7 +781,7 @@ def _generate_platforms(): os_name = WINDOWS_NAME, arch = "x86_64", ), - "x86_64-unknown-linux-gnu": _platform_info( + "x86_64-unknown-linux-gnu": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:x86_64", @@ -820,7 +792,7 @@ def _generate_platforms(): os_name = LINUX_NAME, arch = "x86_64", ), - "x86_64-unknown-linux-musl": _platform_info( + "x86_64-unknown-linux-musl": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:x86_64", @@ -836,7 +808,7 @@ def _generate_platforms(): is_freethreaded_yes = str(Label("//python/config_settings:_is_py_freethreaded_yes")) is_freethreaded_no = str(Label("//python/config_settings:_is_py_freethreaded_no")) return { - p + suffix: _platform_info( + p + suffix: platform_info( compatible_with = v.compatible_with, target_settings = [ freethreadedness, diff --git a/tests/bootstrap_impls/bin.py b/tests/bootstrap_impls/bin.py index 1176107384..3d467dcf29 100644 --- a/tests/bootstrap_impls/bin.py +++ b/tests/bootstrap_impls/bin.py @@ -23,3 +23,4 @@ print("sys.flags.safe_path:", sys.flags.safe_path) print("file:", __file__) print("sys.executable:", sys.executable) +print("sys._base_executable:", sys._base_executable) diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl index 19be1c478e..116afa76ad 100644 --- a/tests/python/python_tests.bzl +++ b/tests/python/python_tests.bzl @@ -17,6 +17,7 @@ load("@pythons_hub//:versions.bzl", "MINOR_MAPPING") load("@rules_testing//lib:test_suite.bzl", "test_suite") load("//python/private:python.bzl", "parse_modules") # buildifier: disable=bzl-visibility +load("//python/private:repo_utils.bzl", "repo_utils") # buildifier: disable=bzl-visibility _tests = [] @@ -131,6 +132,10 @@ def _single_version_platform_override( python_version = python_version, patch_strip = patch_strip, patches = patches, + target_compatible_with = [], + target_settings = [], + os_name = "", + arch = "", ) def _test_default(env): @@ -138,6 +143,7 @@ def _test_default(env): module_ctx = _mock_mctx( _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) # The value there should be consistent in bzlmod with the automatically @@ -168,6 +174,7 @@ def _test_default_some_module(env): module_ctx = _mock_mctx( _mod(name = "rules_python", toolchain = [_toolchain("3.11")], is_root = False), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.11") @@ -186,6 +193,7 @@ def _test_default_with_patch_version(env): module_ctx = _mock_mctx( _mod(name = "rules_python", toolchain = [_toolchain("3.11.2")]), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.11.2") @@ -207,6 +215,7 @@ def _test_default_non_rules_python(env): # does not make any calls to the extension. _mod(name = "rules_python", toolchain = [_toolchain("3.11")], is_root = False), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.11") @@ -228,6 +237,7 @@ def _test_default_non_rules_python_ignore_root_user_error(env): ), _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_bool(py.config.default["ignore_root_user_error"]).equals(False) @@ -257,6 +267,7 @@ def _test_default_non_rules_python_ignore_root_user_error_non_root_module(env): _mod(name = "some_module", toolchain = [_toolchain("3.12", ignore_root_user_error = False)]), _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.13") @@ -302,6 +313,7 @@ def _test_toolchain_ordering(env): ), _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) got_versions = [ t.python_version @@ -347,6 +359,7 @@ def _test_default_from_defaults(env): is_root = True, ), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.11") @@ -374,6 +387,7 @@ def _test_default_from_defaults_env(env): ), environ = {"PYENV_VERSION": "3.12"}, ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.12") @@ -401,6 +415,7 @@ def _test_default_from_defaults_file(env): ), mocked_files = {"@@//:.python-version": "3.12\n"}, ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.12") @@ -427,6 +442,7 @@ def _test_first_occurance_of_the_toolchain_wins(env): "RULES_PYTHON_BZLMOD_DEBUG": "1", }, ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.12") @@ -472,6 +488,7 @@ def _test_auth_overrides(env): ), _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_dict(py.config.default).contains_at_least({ @@ -541,6 +558,7 @@ def _test_add_new_version(env): ], ), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.13") @@ -609,6 +627,7 @@ def _test_register_all_versions(env): ], ), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.13") @@ -685,6 +704,7 @@ def _test_add_patches(env): ], ), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.13") @@ -731,6 +751,7 @@ def _test_fail_two_overrides(env): ), ), _fail = errors.append, + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_collection(errors).contains_exactly([ "Only a single 'python.override' can be present", @@ -758,6 +779,7 @@ def _test_single_version_override_errors(env): ), ), _fail = errors.append, + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_collection(errors).contains_exactly([test.want_error]) @@ -795,6 +817,7 @@ def _test_single_version_platform_override_errors(env): ), ), _fail = lambda *a: errors.append(" ".join(a)), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_collection(errors).contains_exactly([test.want_error]) diff --git a/tests/support/BUILD.bazel b/tests/support/BUILD.bazel index 9fb5cd0760..303dbafbdf 100644 --- a/tests/support/BUILD.bazel +++ b/tests/support/BUILD.bazel @@ -18,6 +18,7 @@ # to force them to resolve in the proper context. # ==================== +load("@bazel_skylib//rules:common_settings.bzl", "string_flag") load(":sh_py_run_test.bzl", "current_build_settings") package( @@ -90,3 +91,15 @@ platform( current_build_settings( name = "current_build_settings", ) + +string_flag( + name = "custom_runtime", + build_setting_default = "", +) + +config_setting( + name = "is_custom_runtime_linux-x86-install-only-stripped", + flag_values = { + ":custom_runtime": "linux-x86-install-only-stripped", + }, +) diff --git a/tests/support/sh_py_run_test.bzl b/tests/support/sh_py_run_test.bzl index 04a2883fde..69141fe8a4 100644 --- a/tests/support/sh_py_run_test.bzl +++ b/tests/support/sh_py_run_test.bzl @@ -42,6 +42,7 @@ def _perform_transition_impl(input_settings, attr, base_impl): # value into the output settings _RECONFIG_ATTR_SETTING_MAP = { "bootstrap_impl": "//python/config_settings:bootstrap_impl", + "custom_runtime": "//tests/support:custom_runtime", "extra_toolchains": "//command_line_option:extra_toolchains", "python_src": "//python/bin:python_src", "venvs_site_packages": "//python/config_settings:venvs_site_packages", @@ -58,6 +59,7 @@ _RECONFIG_INHERITED_OUTPUTS = [v for v in _RECONFIG_OUTPUTS if v in _RECONFIG_IN _RECONFIG_ATTRS = { "bootstrap_impl": attrb.String(), "build_python_zip": attrb.String(default = "auto"), + "custom_runtime": attrb.String(), "extra_toolchains": attrb.StringList( doc = """ Value for the --extra_toolchains flag. diff --git a/tests/toolchains/BUILD.bazel b/tests/toolchains/BUILD.bazel index c55dc92a7d..f346651d46 100644 --- a/tests/toolchains/BUILD.bazel +++ b/tests/toolchains/BUILD.bazel @@ -12,8 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility +load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") load(":defs.bzl", "define_toolchain_tests") define_toolchain_tests( name = "toolchain_tests", ) + +py_reconfig_test( + name = "custom_platform_toolchain_test", + srcs = ["custom_platform_toolchain_test.py"], + custom_runtime = "linux-x86-install-only-stripped", + python_version = "3.13.1", + target_compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ] if BZLMOD_ENABLED else ["@platforms//:incompatible"], +) diff --git a/tests/toolchains/custom_platform_toolchain_test.py b/tests/toolchains/custom_platform_toolchain_test.py new file mode 100644 index 0000000000..d6c083a6a2 --- /dev/null +++ b/tests/toolchains/custom_platform_toolchain_test.py @@ -0,0 +1,15 @@ +import sys +import unittest + + +class VerifyCustomPlatformToolchainTest(unittest.TestCase): + + def test_custom_platform_interpreter_used(self): + # We expect the repo name, and thus path, to have the + # platform name in it. + self.assertIn("linux-x86-install-only-stripped", sys._base_executable) + print(sys._base_executable) + + +if __name__ == "__main__": + unittest.main() From ce80db6a8640cc7552e4b5eada891cd19c4550f2 Mon Sep 17 00:00:00 2001 From: Vihang Mehta Date: Thu, 29 May 2025 18:16:03 -0700 Subject: [PATCH 126/156] feat: Support constraints in pip_compile (#2916) This adds in support to pass in a constraints file to pip-compile. This is extremly useful when you want to uprade an indirect/intermediate dependency to pull in security fixes but don't want to add said dependency to the requirements.in file. --------- Signed-off-by: Vihang Mehta Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- CHANGELOG.md | 3 +++ examples/pip_parse/BUILD.bazel | 4 ++++ examples/pip_parse/constraints_certifi.txt | 1 + examples/pip_parse/constraints_urllib3.txt | 1 + examples/pip_parse/requirements_lock.txt | 20 ++++++++++++-------- examples/pip_parse/requirements_windows.txt | 20 ++++++++++++-------- python/private/pypi/pip_compile.bzl | 6 +++++- 7 files changed, 38 insertions(+), 17 deletions(-) create mode 100644 examples/pip_parse/constraints_certifi.txt create mode 100644 examples/pip_parse/constraints_urllib3.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index a113c7411f..355f1fe9ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,9 @@ END_UNRELEASED_TEMPLATE and activated with custom flags. See the [Registering custom runtimes] docs and {obj}`single_version_platform_override()` API docs for more information. +* (rules) Added support for a using constraints files with `compile_pip_requirements`. + Useful when an intermediate dependency needs to be upgraded to pull in + security patches. {#v0-0-0-removed} ### Removed diff --git a/examples/pip_parse/BUILD.bazel b/examples/pip_parse/BUILD.bazel index 8bdbd94b2c..6ed8d26286 100644 --- a/examples/pip_parse/BUILD.bazel +++ b/examples/pip_parse/BUILD.bazel @@ -57,6 +57,10 @@ py_console_script_binary( compile_pip_requirements( name = "requirements", src = "requirements.in", + constraints = [ + "constraints_certifi.txt", + "constraints_urllib3.txt", + ], requirements_txt = "requirements_lock.txt", requirements_windows = "requirements_windows.txt", ) diff --git a/examples/pip_parse/constraints_certifi.txt b/examples/pip_parse/constraints_certifi.txt new file mode 100644 index 0000000000..7dc4eac259 --- /dev/null +++ b/examples/pip_parse/constraints_certifi.txt @@ -0,0 +1 @@ +certifi>=2025.1.31 \ No newline at end of file diff --git a/examples/pip_parse/constraints_urllib3.txt b/examples/pip_parse/constraints_urllib3.txt new file mode 100644 index 0000000000..3818262552 --- /dev/null +++ b/examples/pip_parse/constraints_urllib3.txt @@ -0,0 +1 @@ +urllib3>1.26.18 diff --git a/examples/pip_parse/requirements_lock.txt b/examples/pip_parse/requirements_lock.txt index aeac61eff9..dc34b45a45 100644 --- a/examples/pip_parse/requirements_lock.txt +++ b/examples/pip_parse/requirements_lock.txt @@ -12,10 +12,12 @@ babel==2.13.1 \ --hash=sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900 \ --hash=sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed # via sphinx -certifi==2024.7.4 \ - --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \ - --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90 - # via requests +certifi==2025.4.26 \ + --hash=sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6 \ + --hash=sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3 + # via + # -c ./constraints_certifi.txt + # requests chardet==4.0.0 \ --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \ --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 @@ -218,10 +220,12 @@ sphinxcontrib-serializinghtml==1.1.9 \ # via # -r requirements.in # sphinx -urllib3==1.26.18 \ - --hash=sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07 \ - --hash=sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0 - # via requests +urllib3==1.26.20 \ + --hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \ + --hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32 + # via + # -c ./constraints_urllib3.txt + # requests yamllint==1.28.0 \ --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \ --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b diff --git a/examples/pip_parse/requirements_windows.txt b/examples/pip_parse/requirements_windows.txt index 61a6682047..78c1a45690 100644 --- a/examples/pip_parse/requirements_windows.txt +++ b/examples/pip_parse/requirements_windows.txt @@ -12,10 +12,12 @@ babel==2.13.1 \ --hash=sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900 \ --hash=sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed # via sphinx -certifi==2024.7.4 \ - --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \ - --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90 - # via requests +certifi==2025.4.26 \ + --hash=sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6 \ + --hash=sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3 + # via + # -c ./constraints_certifi.txt + # requests chardet==4.0.0 \ --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \ --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 @@ -222,10 +224,12 @@ sphinxcontrib-serializinghtml==1.1.9 \ # via # -r requirements.in # sphinx -urllib3==1.26.18 \ - --hash=sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07 \ - --hash=sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0 - # via requests +urllib3==1.26.20 \ + --hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \ + --hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32 + # via + # -c ./constraints_urllib3.txt + # requests yamllint==1.28.0 \ --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \ --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b diff --git a/python/private/pypi/pip_compile.bzl b/python/private/pypi/pip_compile.bzl index 9782d3ce21..c9899503d6 100644 --- a/python/private/pypi/pip_compile.bzl +++ b/python/private/pypi/pip_compile.bzl @@ -38,6 +38,7 @@ def pip_compile( requirements_windows = None, visibility = ["//visibility:private"], tags = None, + constraints = [], **kwargs): """Generates targets for managing pip dependencies with pip-compile. @@ -77,6 +78,7 @@ def pip_compile( requirements_windows: File of windows specific resolve output to check validate if requirement.in has changes. tags: tagging attribute common to all build rules, passed to both the _test and .update rules. visibility: passed to both the _test and .update rules. + constraints: a list of files containing constraints to pass to pip-compile with `--constraint`. **kwargs: other bazel attributes passed to the "_test" rule. """ if len([x for x in [srcs, src, requirements_in] if x != None]) > 1: @@ -100,7 +102,7 @@ def pip_compile( visibility = visibility, ) - data = [name, requirements_txt] + srcs + [f for f in (requirements_linux, requirements_darwin, requirements_windows) if f != None] + data = [name, requirements_txt] + srcs + [f for f in (requirements_linux, requirements_darwin, requirements_windows) if f != None] + constraints # Use the Label constructor so this is expanded in the context of the file # where it appears, which is to say, in @rules_python @@ -122,6 +124,8 @@ def pip_compile( args.append("--requirements-darwin={}".format(loc.format(requirements_darwin))) if requirements_windows: args.append("--requirements-windows={}".format(loc.format(requirements_windows))) + for constraint in constraints: + args.append("--constraint=$(location {})".format(constraint)) args.extend(extra_args) deps = [ From af9e959538f34878ca0ccccd97d51dc7b3ffdadd Mon Sep 17 00:00:00 2001 From: rbeasley-avgo Date: Fri, 30 May 2025 05:25:03 -0400 Subject: [PATCH 127/156] fix(pypi): allow pip_compile to work with read-only sources (#2712) The validating `py_test` generated by `compile_pip_requirements` chokes when the source `requirements.txt` is stored read-only, such as when managed by the Perforce Helix Core SCM. Though `dependency_resolver` makes a temporary copy of this file, it does so w/ `shutil.copy` which preserves the original read-only file mode. To address this, this commit replaces `shutil.copy` with a `shutil.copyfileobj` such that the temporary file is created w/ permissions according to the user's umask. Resolves (#2608). --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- CHANGELOG.md | 2 ++ .../pypi/dependency_resolver/dependency_resolver.py | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 355f1fe9ef..0a2dc413ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,8 @@ END_UNRELEASED_TEMPLATE also retrieved from the URL as opposed to only the `--hash` parameter. Fixes [#2363](https://github.com/bazel-contrib/rules_python/issues/2363). * (pypi) `whl_library` now infers file names from its `urls` attribute correctly. +* (pypi) When running under `bazel test`, be sure that temporary `requirements` file + remains writable. * (py_test, py_binary) Allow external files to be used for main {#v0-0-0-added} diff --git a/python/private/pypi/dependency_resolver/dependency_resolver.py b/python/private/pypi/dependency_resolver/dependency_resolver.py index ada0763558..a42821c458 100644 --- a/python/private/pypi/dependency_resolver/dependency_resolver.py +++ b/python/private/pypi/dependency_resolver/dependency_resolver.py @@ -151,9 +151,16 @@ def main( requirements_out = os.path.join( os.environ["TEST_TMPDIR"], os.path.basename(requirements_file) + ".out" ) + # Why this uses shutil.copyfileobj: + # # Those two files won't necessarily be on the same filesystem, so we can't use os.replace # or shutil.copyfile, as they will fail with OSError: [Errno 18] Invalid cross-device link. - shutil.copy(resolved_requirements_file, requirements_out) + # + # Further, shutil.copy preserves the source file's mode, and so if + # our source file is read-only (the default under Perforce Helix), + # this scratch file will also be read-only, defeating its purpose. + with open(resolved_requirements_file, "rb") as fsrc, open(requirements_out, "wb") as fdst: + shutil.copyfileobj(fsrc, fdst) update_command = ( os.getenv("CUSTOM_COMPILE_COMMAND") or f"bazel run {target_label_prefix}.update" From 02198f622ee1b496111bef6b880ea35e0d24b600 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sat, 31 May 2025 15:52:54 +0900 Subject: [PATCH 128/156] feat(uv): handle credential helpers and .netrc (#2872) This allows one to download the uv binaries from private mirrors. The plumbing of the auth attrs allows us to correctly use the `~/.netrc` or the credential helper for downloading from mirrors that require authentication. Testing notes: * When I tested this, it seems that the dist manifest json may not work with private mirrors, but I think it is fine for users in such cases to define the `uv` srcs using the `urls` attribute. Work towards #1975. --- CHANGELOG.md | 2 ++ python/uv/private/BUILD.bazel | 2 ++ python/uv/private/uv.bzl | 35 +++++++++++++++++++++++------ python/uv/private/uv_repository.bzl | 5 ++++- tests/uv/uv/uv_tests.bzl | 23 ++++++++++++++++++- 5 files changed, 58 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a2dc413ae..f82df5aad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,6 +109,8 @@ END_UNRELEASED_TEMPLATE Set the `RULES_PYTHON_ENABLE_PIPSTAR=1` environment variable to enable it. * (utils) Add a way to run a REPL for any `rules_python` target that returns a `PyInfo` provider. +* (uv) Handle `.netrc` and `auth_patterns` auth when downloading `uv`. Work towards + [#1975](https://github.com/bazel-contrib/rules_python/issues/1975). * (toolchains) Arbitrary python-build-standalone runtimes can be registered and activated with custom flags. See the [Registering custom runtimes] docs and {obj}`single_version_platform_override()` API docs for more diff --git a/python/uv/private/BUILD.bazel b/python/uv/private/BUILD.bazel index 587ad9a0f9..a07d8591ad 100644 --- a/python/uv/private/BUILD.bazel +++ b/python/uv/private/BUILD.bazel @@ -62,6 +62,7 @@ bzl_library( ":toolchain_types_bzl", ":uv_repository_bzl", ":uv_toolchains_repo_bzl", + "//python/private:auth_bzl", ], ) @@ -69,6 +70,7 @@ bzl_library( name = "uv_repository_bzl", srcs = ["uv_repository.bzl"], visibility = ["//python/uv:__subpackages__"], + deps = ["//python/private:auth_bzl"], ) bzl_library( diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 09fb78322f..2cc2df1b21 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -18,6 +18,7 @@ EXPERIMENTAL: This is experimental and may be removed without notice A module extension for working with uv. """ +load("//python/private:auth.bzl", "AUTH_ATTRS", "get_auth") load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE") load(":uv_repository.bzl", "uv_repository") load(":uv_toolchains_repo.bzl", "uv_toolchains_repo") @@ -77,7 +78,7 @@ The version of uv to configure the sources for. If this is not specified it will last version used in the module or the default version set by `rules_python`. """, ), -} +} | AUTH_ATTRS default = tag_class( doc = """\ @@ -133,7 +134,7 @@ for a particular version. }, ) -def _configure(config, *, platform, compatible_with, target_settings, urls = [], sha256 = "", override = False, **values): +def _configure(config, *, platform, compatible_with, target_settings, auth_patterns, urls = [], sha256 = "", override = False, **values): """Set the value in the config if the value is provided""" for key, value in values.items(): if not value: @@ -144,6 +145,7 @@ def _configure(config, *, platform, compatible_with, target_settings, urls = [], config[key] = value + config.setdefault("auth_patterns", {}).update(auth_patterns) config.setdefault("platforms", {}) if not platform: if compatible_with or target_settings or urls: @@ -173,7 +175,8 @@ def process_modules( hub_name = "uv", uv_repository = uv_repository, toolchain_type = str(UV_TOOLCHAIN_TYPE), - hub_repo = uv_toolchains_repo): + hub_repo = uv_toolchains_repo, + get_auth = get_auth): """Parse the modules to get the config for 'uv' toolchains. Args: @@ -182,6 +185,7 @@ def process_modules( uv_repository: the rule to create a uv_repository override. toolchain_type: the toolchain type to use here. hub_repo: the hub repo factory function to use. + get_auth: the auth function to use. Returns: the result of the hub_repo. Mainly used for tests. @@ -216,6 +220,8 @@ def process_modules( compatible_with = tag.compatible_with, target_settings = tag.target_settings, override = mod.is_root, + netrc = tag.netrc, + auth_patterns = tag.auth_patterns, ) for key in [ @@ -271,6 +277,8 @@ def process_modules( sha256 = tag.sha256, urls = tag.urls, override = mod.is_root, + netrc = tag.netrc, + auth_patterns = tag.auth_patterns, ) if not versions: @@ -301,6 +309,11 @@ def process_modules( for platform, src in config.get("urls", {}).items() if src.urls } + auth = { + "auth_patterns": config.get("auth_patterns"), + "netrc": config.get("netrc"), + } + auth = {k: v for k, v in auth.items() if v} # Or fallback to fetching them from GH manifest file # Example file: https://github.com/astral-sh/uv/releases/download/0.6.3/dist-manifest.json @@ -313,6 +326,8 @@ def process_modules( ), manifest_filename = config["manifest_filename"], platforms = sorted(platforms), + get_auth = get_auth, + **auth ) for platform_name, platform in platforms.items(): @@ -327,6 +342,7 @@ def process_modules( platform = platform_name, urls = urls[platform_name].urls, sha256 = urls[platform_name].sha256, + **auth ) toolchain_names.append(toolchain_name) @@ -363,7 +379,7 @@ def _overlap(first_collection, second_collection): return False -def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename, platforms): +def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename, platforms, get_auth = get_auth, **auth_attrs): """Download the results about remote tool sources. This relies on the tools using the cargo packaging to infer the actual @@ -431,10 +447,13 @@ def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename "aarch64-apple-darwin" ] """ + auth_attr = struct(**auth_attrs) dist_manifest = module_ctx.path(manifest_filename) + urls = [base_url + "/" + manifest_filename] result = module_ctx.download( - base_url + "/" + manifest_filename, + url = urls, output = dist_manifest, + auth = get_auth(module_ctx, urls, ctx_attr = auth_attr), ) if not result.success: fail(result) @@ -454,11 +473,13 @@ def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename checksum_fname = checksum["name"] checksum_path = module_ctx.path(checksum_fname) + urls = ["{}/{}".format(base_url, checksum_fname)] downloads[checksum_path] = struct( download = module_ctx.download( - "{}/{}".format(base_url, checksum_fname), + url = urls, output = checksum_path, block = False, + auth = get_auth(module_ctx, urls, ctx_attr = auth_attr), ), archive_fname = fname, platforms = checksum["target_triples"], @@ -473,7 +494,7 @@ def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename sha256, _, checksummed_fname = module_ctx.read(checksum_path).partition(" ") checksummed_fname = checksummed_fname.strip(" *\n") - if archive_fname != checksummed_fname: + if checksummed_fname and archive_fname != checksummed_fname: fail("The checksum is for a different file, expected '{}' but got '{}'".format( archive_fname, checksummed_fname, diff --git a/python/uv/private/uv_repository.bzl b/python/uv/private/uv_repository.bzl index ba7d2a766c..fed4f576d3 100644 --- a/python/uv/private/uv_repository.bzl +++ b/python/uv/private/uv_repository.bzl @@ -18,6 +18,8 @@ EXPERIMENTAL: This is experimental and may be removed without notice Create repositories for uv toolchain dependencies """ +load("//python/private:auth.bzl", "AUTH_ATTRS", "get_auth") + UV_BUILD_TMPL = """\ # Generated by repositories.bzl load("@rules_python//python/uv:uv_toolchain.bzl", "uv_toolchain") @@ -43,6 +45,7 @@ def _uv_repo_impl(repository_ctx): url = repository_ctx.attr.urls, sha256 = repository_ctx.attr.sha256, stripPrefix = strip_prefix, + auth = get_auth(repository_ctx, repository_ctx.attr.urls), ) binary = "uv.exe" if is_windows else "uv" @@ -70,5 +73,5 @@ uv_repository = repository_rule( "sha256": attr.string(mandatory = False), "urls": attr.string_list(mandatory = True), "version": attr.string(mandatory = True), - }, + } | AUTH_ATTRS, ) diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index bf0deefa88..b464dab55c 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -100,7 +100,7 @@ def _mod(*, name = None, default = [], configure = [], is_root = True): ) def _process_modules(env, **kwargs): - result = process_modules(hub_repo = struct, **kwargs) + result = process_modules(hub_repo = struct, get_auth = lambda *_, **__: None, **kwargs) return env.expect.that_struct( struct( @@ -124,6 +124,8 @@ def _default( platform = None, target_settings = None, version = None, + netrc = None, + auth_patterns = None, **kwargs): return struct( base_url = base_url, @@ -132,6 +134,8 @@ def _default( platform = platform, target_settings = [] + (target_settings or []), # ensure that the type is correct version = version, + netrc = netrc, + auth_patterns = {} | (auth_patterns or {}), # ensure that the type is correct **kwargs ) @@ -377,6 +381,11 @@ def _test_complex_configuring(env): platform = "linux", compatible_with = ["@platforms//os:linux"], ), + _configure( + version = "1.0.4", + netrc = "~/.my_netrc", + auth_patterns = {"foo": "bar"}, + ), # use auth ], ), ), @@ -388,18 +397,21 @@ def _test_complex_configuring(env): "1_0_1_osx", "1_0_2_osx", "1_0_3_linux", + "1_0_4_osx", ]) uv.implementations().contains_exactly({ "1_0_0_osx": "@uv_1_0_0_osx//:uv_toolchain", "1_0_1_osx": "@uv_1_0_1_osx//:uv_toolchain", "1_0_2_osx": "@uv_1_0_2_osx//:uv_toolchain", "1_0_3_linux": "@uv_1_0_3_linux//:uv_toolchain", + "1_0_4_osx": "@uv_1_0_4_osx//:uv_toolchain", }) uv.compatible_with().contains_exactly({ "1_0_0_osx": ["@platforms//os:os"], "1_0_1_osx": ["@platforms//os:os"], "1_0_2_osx": ["@platforms//os:different"], "1_0_3_linux": ["@platforms//os:linux"], + "1_0_4_osx": ["@platforms//os:os"], }) uv.target_settings().contains_exactly({}) env.expect.that_collection(calls).contains_exactly([ @@ -431,6 +443,15 @@ def _test_complex_configuring(env): "urls": ["https://example.org/1.0.3/linux"], "version": "1.0.3", }, + { + "auth_patterns": {"foo": "bar"}, + "name": "uv_1_0_4_osx", + "netrc": "~/.my_netrc", + "platform": "osx", + "sha256": "deadb00f", + "urls": ["https://example.org/1.0.4/osx"], + "version": "1.0.4", + }, ]) _tests.append(_test_complex_configuring) From 948fcec44edbe12f4edf94db098c761570a72763 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 3 Jun 2025 00:44:57 +0900 Subject: [PATCH 129/156] fix(pypi): correctly aggregate the requirements files (#2932) This implements the actual fix where we are aggregating the whls and sdists correctly from multiple different requirements lines. Fixes #2648. Closes #2658. --- CHANGELOG.md | 2 + python/private/pypi/parse_requirements.bzl | 18 +++- .../parse_requirements_tests.bzl | 84 ++++++++++++++++++- 3 files changed, 97 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f82df5aad0..c9668c507f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,8 @@ END_UNRELEASED_TEMPLATE * (pypi) When running under `bazel test`, be sure that temporary `requirements` file remains writable. * (py_test, py_binary) Allow external files to be used for main +* (pypi) Correctly aggregate the sources when the hashes specified in the lockfile differ + by platform even though the same version is used. Fixes [#2648](https://github.com/bazel-contrib/rules_python/issues/2648). {#v0-0-0-added} ### Added diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index bd2981efc0..e4a8b90acb 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -223,7 +223,7 @@ def _package_srcs( env_marker_target_platforms, extract_url_srcs): """A function to return sources for a particular package.""" - srcs = [] + srcs = {} for r in sorted(reqs.values(), key = lambda r: r.requirement_line): whls, sdist = _add_dists( requirement = r, @@ -249,21 +249,31 @@ def _package_srcs( )] req_line = r.srcs.requirement_line + extra_pip_args = tuple(r.extra_pip_args) for dist in all_dists: - srcs.append( + key = ( + dist.filename, + req_line, + extra_pip_args, + ) + entry = srcs.setdefault( + key, struct( distribution = name, extra_pip_args = r.extra_pip_args, requirement_line = req_line, - target_platforms = target_platforms, + target_platforms = [], filename = dist.filename, sha256 = dist.sha256, url = dist.url, yanked = dist.yanked, ), ) + for p in target_platforms: + if p not in entry.target_platforms: + entry.target_platforms.append(p) - return srcs + return srcs.values() def select_requirement(requirements, *, platform): """A simple function to get a requirement for a particular platform. diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index 926a7e0c50..82fdd0a051 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -38,7 +38,7 @@ foo[extra]==0.0.1 \ foo @ git+https://github.com/org/foo.git@deadbeef """, "requirements_linux": """\ -foo==0.0.3 --hash=sha256:deadbaaf +foo==0.0.3 --hash=sha256:deadbaaf --hash=sha256:5d15t """, # download_only = True "requirements_linux_download_only": """\ @@ -67,7 +67,7 @@ foo==0.0.4 @ https://example.org/foo-0.0.4.whl foo==0.0.5 @ https://example.org/foo-0.0.5.whl --hash=sha256:deadbeef """, "requirements_osx": """\ -foo==0.0.3 --hash=sha256:deadbaaf +foo==0.0.3 --hash=sha256:deadbaaf --hash=sha256:deadb11f --hash=sha256:5d15t """, "requirements_osx_download_only": """\ --platform=macosx_10_9_arm64 @@ -251,7 +251,7 @@ def _test_multi_os(env): struct( distribution = "foo", extra_pip_args = [], - requirement_line = "foo==0.0.3 --hash=sha256:deadbaaf", + requirement_line = "foo==0.0.3 --hash=sha256:deadbaaf --hash=sha256:5d15t", target_platforms = ["linux_x86_64"], url = "", filename = "", @@ -515,6 +515,84 @@ def _test_git_sources(env): _tests.append(_test_git_sources) +def _test_overlapping_shas_with_index_results(env): + got = parse_requirements( + ctx = _mock_ctx(), + requirements_by_platform = { + "requirements_linux": ["cp39_linux_x86_64"], + "requirements_osx": ["cp39_osx_x86_64"], + }, + get_index_urls = lambda _, __: { + "foo": struct( + sdists = { + "5d15t": struct( + url = "sdist", + sha256 = "5d15t", + filename = "foo-0.0.1.tar.gz", + yanked = False, + ), + }, + whls = { + "deadb11f": struct( + url = "super2", + sha256 = "deadb11f", + filename = "foo-0.0.1-py3-none-macosx_14_0_x86_64.whl", + yanked = False, + ), + "deadbaaf": struct( + url = "super2", + sha256 = "deadbaaf", + filename = "foo-0.0.1-py3-none-any.whl", + yanked = False, + ), + }, + ), + }, + ) + + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + is_exposed = True, + # TODO @aignas 2025-05-25: how do we rename this? + is_multiple_versions = True, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + filename = "foo-0.0.1-py3-none-any.whl", + requirement_line = "foo==0.0.3", + sha256 = "deadbaaf", + target_platforms = ["cp39_linux_x86_64", "cp39_osx_x86_64"], + url = "super2", + yanked = False, + ), + struct( + distribution = "foo", + extra_pip_args = [], + filename = "foo-0.0.1.tar.gz", + requirement_line = "foo==0.0.3", + sha256 = "5d15t", + target_platforms = ["cp39_linux_x86_64", "cp39_osx_x86_64"], + url = "sdist", + yanked = False, + ), + struct( + distribution = "foo", + extra_pip_args = [], + filename = "foo-0.0.1-py3-none-macosx_14_0_x86_64.whl", + requirement_line = "foo==0.0.3", + sha256 = "deadb11f", + target_platforms = ["cp39_osx_x86_64"], + url = "super2", + yanked = False, + ), + ], + ), + ]) + +_tests.append(_test_overlapping_shas_with_index_results) + def parse_requirements_test_suite(name): """Create the test suite. From 9429ae6446935059e79047654d3fe53d60aadc31 Mon Sep 17 00:00:00 2001 From: Mike Toldov Date: Tue, 3 Jun 2025 09:13:19 +0200 Subject: [PATCH 130/156] fix(pypi): inherit proxy env variables in compile_pip_requirements test (#2941) Bazel does not pass environment variables implicitly (even running test outside of sandbox). This forces compile_pip_requirements test to fail with timeout when attempting to run it behind the proxy. Also changes test_command in dependency_resolver string helper to use dot instead of underscore following deprecation notice --- CHANGELOG.md | 1 + .../pypi/dependency_resolver/dependency_resolver.py | 2 +- python/private/pypi/pip_compile.bzl | 8 +++++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9668c507f..e48e3d4f3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ END_UNRELEASED_TEMPLATE * (py_test, py_binary) Allow external files to be used for main * (pypi) Correctly aggregate the sources when the hashes specified in the lockfile differ by platform even though the same version is used. Fixes [#2648](https://github.com/bazel-contrib/rules_python/issues/2648). +* (pypi) `compile_pip_requirements` test rule works behind the proxy {#v0-0-0-added} ### Added diff --git a/python/private/pypi/dependency_resolver/dependency_resolver.py b/python/private/pypi/dependency_resolver/dependency_resolver.py index a42821c458..f3a339f929 100644 --- a/python/private/pypi/dependency_resolver/dependency_resolver.py +++ b/python/private/pypi/dependency_resolver/dependency_resolver.py @@ -165,7 +165,7 @@ def main( update_command = ( os.getenv("CUSTOM_COMPILE_COMMAND") or f"bazel run {target_label_prefix}.update" ) - test_command = f"bazel test {target_label_prefix}_test" + test_command = f"bazel test {target_label_prefix}.test" os.environ["CUSTOM_COMPILE_COMMAND"] = update_command os.environ["PIP_CONFIG_FILE"] = os.getenv("PIP_CONFIG_FILE") or os.devnull diff --git a/python/private/pypi/pip_compile.bzl b/python/private/pypi/pip_compile.bzl index c9899503d6..78b681b4ad 100644 --- a/python/private/pypi/pip_compile.bzl +++ b/python/private/pypi/pip_compile.bzl @@ -45,7 +45,6 @@ def pip_compile( By default this rules generates a filegroup named "[name]" which can be included in the data of some other compile_pip_requirements rule that references these requirements (e.g. with `-r ../other/requirements.txt`). - It also generates two targets for running pip-compile: - validate with `bazel test [name].test` @@ -160,6 +159,12 @@ def pip_compile( } env = kwargs.pop("env", {}) + env_inherit = kwargs.pop("env_inherit", []) + proxy_variables = ["https_proxy", "http_proxy", "no_proxy", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY"] + + for var in proxy_variables: + if var not in env_inherit: + env_inherit.append(var) py_binary( name = name + ".update", @@ -182,6 +187,7 @@ def pip_compile( "@@platforms//os:windows": {"USERPROFILE": "Z:\\FakeSetuptoolsHomeDirectoryHack"}, "//conditions:default": {}, }) | env, + env_inherit = env_inherit, # kwargs could contain test-specific attributes like size **dict(attrs, **kwargs) ) From 049866442fee7bb54fcb1a09e920953a0666e4b3 Mon Sep 17 00:00:00 2001 From: Kayce Basques Date: Thu, 5 Jun 2025 14:29:07 -0700 Subject: [PATCH 131/156] feat: add persistent worker for sphinxdocs (#2938) This implements a simple, serialized persistent worker for Sphinxdocs with several optimizations. It is enabled by default. * The worker computes what inputs have changed, allowing Sphinx to only rebuild what is necessary. * Doctrees are written to a separate directory so they are retained between builds. * The worker tells Sphinx to write output to an internal directory, then copies it to the expected Bazel output directory afterwards. This allows Sphinx to only write output files that need to be updated. This works by having the worker compute what files have changed and having a Sphinx extension use the `get-env-outdated` event to tell Sphinx which files have changed. The extension is based on https://pwrev.dev/294057, but re-implemented to be in-memory as part of the worker instead of a separate extension projects must configure. For rules_python's doc building, this reduces incremental building from about 8 seconds to about 0.8 seconds. From what I can tell, about half the time is spent generating doctrees, and the other half generating the output files. Worker mode is enabled by default and can be disabled on the target or by adjusting the Bazel flags controlling execution strategy. Docs added to explain how. Because `--doctree-dir` is now always specified and outside the output dir, non-worker invocations can benefit, too, if run without sandboxing. Docs added to explain how to do this. Along the way: * Remove `--write-all` and `--fresh-env` from run args. This lets direct invocations benefit from the normal caching Sphinx does. * Change the args formatting to `--foo=bar` so they are a single element; just a bit nicer to see when debugging. Work towards https://github.com/bazel-contrib/rules_python/issues/2878, https://github.com/bazel-contrib/rules_python/issues/2879 --------- Co-authored-by: Kayce Basques Co-authored-by: Richard Levasseur --- sphinxdocs/docs/index.md | 23 +++ sphinxdocs/private/sphinx.bzl | 55 +++++-- sphinxdocs/private/sphinx_build.py | 231 ++++++++++++++++++++++++++- sphinxdocs/tests/sphinx_docs/doc1.md | 3 + sphinxdocs/tests/sphinx_docs/doc2.md | 3 + 5 files changed, 302 insertions(+), 13 deletions(-) create mode 100644 sphinxdocs/tests/sphinx_docs/doc1.md create mode 100644 sphinxdocs/tests/sphinx_docs/doc2.md diff --git a/sphinxdocs/docs/index.md b/sphinxdocs/docs/index.md index bd6448ced9..2ea1146e1b 100644 --- a/sphinxdocs/docs/index.md +++ b/sphinxdocs/docs/index.md @@ -11,6 +11,29 @@ documentation. It comes with: While it is primarily oriented towards docgen for Starlark code, the core of it is agnostic as to what is being documented. +### Optimization + +Normally, Sphinx keeps various cache files to improve incremental building. +Unfortunately, programs performing their own caching don't interact well +with Bazel's model of precisely declaring and strictly enforcing what are +inputs, what are outputs, and what files are available when running a program. +The net effect is programs don't have a prior invocation's cache files +available. + +There are two mechanisms available to make some cache available to Sphinx under +Bazel: + +* Disable sandboxing, which allows some files from prior invocations to be + visible to subsequent invocations. This can be done multiple ways: + * Set `tags = ["no-sandbox"]` on the `sphinx_docs` target + * `--modify_execution_info=SphinxBuildDocs=+no-sandbox` (Bazel flag) + * `--strategy=SphinxBuildDocs=local` (Bazel flag) +* Use persistent workers (enabled by default) by setting + `allow_persistent_workers=True` on the `sphinx_docs` target. Note that other + Bazel flags can disable using workers even if an action supports it. Setting + `--strategy=SphinxBuildDocs=dynamic,worker,local,sandbox` should tell Bazel + to use workers if possible, otherwise fallback to non-worker invocations. + ```{toctree} :hidden: diff --git a/sphinxdocs/private/sphinx.bzl b/sphinxdocs/private/sphinx.bzl index 8d19d87052..ee6b994e2e 100644 --- a/sphinxdocs/private/sphinx.bzl +++ b/sphinxdocs/private/sphinx.bzl @@ -103,6 +103,7 @@ def sphinx_docs( strip_prefix = "", extra_opts = [], tools = [], + allow_persistent_workers = True, **kwargs): """Generate docs using Sphinx. @@ -142,6 +143,9 @@ def sphinx_docs( tools: {type}`list[label]` Additional tools that are used by Sphinx and its plugins. This just makes the tools available during Sphinx execution. To locate them, use {obj}`extra_opts` and `$(location)`. + allow_persistent_workers: {type}`bool` (experimental) If true, allow + using persistent workers for running Sphinx, if Bazel decides to do so. + This can improve incremental building of docs. **kwargs: {type}`dict` Common attributes to pass onto rules. """ add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_docs") @@ -165,6 +169,7 @@ def sphinx_docs( source_tree = internal_name + "/_sources", extra_opts = extra_opts, tools = tools, + allow_persistent_workers = allow_persistent_workers, **kwargs ) @@ -209,6 +214,7 @@ def _sphinx_docs_impl(ctx): source_path = source_dir_path, output_prefix = paths.join(ctx.label.name, "_build"), inputs = inputs, + allow_persistent_workers = ctx.attr.allow_persistent_workers, ) outputs[format] = output_dir per_format_args[format] = args_env @@ -229,6 +235,10 @@ def _sphinx_docs_impl(ctx): _sphinx_docs = rule( implementation = _sphinx_docs_impl, attrs = { + "allow_persistent_workers": attr.bool( + doc = "(experimental) Whether to invoke Sphinx as a persistent worker.", + default = False, + ), "extra_opts": attr.string_list( doc = "Additional options to pass onto Sphinx. These are added after " + "other options, but before the source/output args.", @@ -254,16 +264,27 @@ _sphinx_docs = rule( }, ) -def _run_sphinx(ctx, format, source_path, inputs, output_prefix): +def _run_sphinx(ctx, format, source_path, inputs, output_prefix, allow_persistent_workers): output_dir = ctx.actions.declare_directory(paths.join(output_prefix, format)) run_args = [] # Copy of the args to forward along to debug runner args = ctx.actions.args() # Args passed to the action + # An args file is required for persistent workers, but we don't know if + # the action will use worker mode or not (settings we can't see may + # force non-worker mode). For consistency, always use a params file. + args.use_param_file("@%s", use_always = True) + args.set_param_file_format("multiline") + + # NOTE: sphinx_build.py relies on the first two args being the srcdir and + # outputdir, in that order. + args.add(source_path) + args.add(output_dir.path) + args.add("--show-traceback") # Full tracebacks on error run_args.append("--show-traceback") - args.add("--builder", format) - run_args.extend(("--builder", format)) + args.add(format, format = "--builder=%s") + run_args.append("--builder={}".format(format)) if ctx.attr._quiet_flag[BuildSettingInfo].value: # Not added to run_args because run_args is for debugging @@ -271,11 +292,17 @@ def _run_sphinx(ctx, format, source_path, inputs, output_prefix): # Build in parallel, if possible # Don't add to run_args: parallel building breaks interactive debugging - args.add("--jobs", "auto") - args.add("--fresh-env") # Don't try to use cache files. Bazel can't make use of them. - run_args.append("--fresh-env") - args.add("--write-all") # Write all files; don't try to detect "changed" files - run_args.append("--write-all") + args.add("--jobs=auto") + + # Put the doctree dir outside of the output directory. + # This allows it to be reused between invocations when possible; Bazel + # clears the output directory every action invocation. + # * For workers, they can fully re-use it. + # * For non-workers, it can be reused when sandboxing is disabled via + # the `no-sandbox` tag or execution requirement. + # + # We also use a non-dot prefixed name so it shows up more visibly. + args.add(paths.join(output_dir.path + "_doctrees"), format = "--doctree-dir=%s") for opt in ctx.attr.extra_opts: expanded = ctx.expand_location(opt) @@ -287,9 +314,6 @@ def _run_sphinx(ctx, format, source_path, inputs, output_prefix): for define in extra_defines: run_args.extend(("--define", define)) - args.add(source_path) - args.add(output_dir.path) - env = dict([ v.split("=", 1) for v in ctx.attr._extra_env_flag[_FlagInfo].value @@ -299,6 +323,14 @@ def _run_sphinx(ctx, format, source_path, inputs, output_prefix): for tool in ctx.attr.tools: tools.append(tool[DefaultInfo].files_to_run) + # NOTE: Command line flags or RBE capabilities may override the execution + # requirements and disable workers. Thus, we can't assume that these + # exec requirements will actually be respected. + execution_requirements = {} + if allow_persistent_workers: + execution_requirements["supports-workers"] = "1" + execution_requirements["requires-worker-protocol"] = "json" + ctx.actions.run( executable = ctx.executable.sphinx, arguments = [args], @@ -308,6 +340,7 @@ def _run_sphinx(ctx, format, source_path, inputs, output_prefix): mnemonic = "SphinxBuildDocs", progress_message = "Sphinx building {} for %{{label}}".format(format), env = env, + execution_requirements = execution_requirements, ) return output_dir, struct(args = run_args, env = env) diff --git a/sphinxdocs/private/sphinx_build.py b/sphinxdocs/private/sphinx_build.py index 3b7b32eaf6..e9711042f6 100644 --- a/sphinxdocs/private/sphinx_build.py +++ b/sphinxdocs/private/sphinx_build.py @@ -1,8 +1,235 @@ +import contextlib +import io +import json +import logging import os -import pathlib +import shutil import sys +import traceback +import typing +import sphinx.application from sphinx.cmd.build import main +WorkRequest = object +WorkResponse = object + +logger = logging.getLogger("sphinxdocs_build") + +_WORKER_SPHINX_EXT_MODULE_NAME = "bazel_worker_sphinx_ext" + +# Config value name for getting the path to the request info file +_REQUEST_INFO_CONFIG_NAME = "bazel_worker_request_info_path" + + +class Worker: + + def __init__( + self, instream: "typing.TextIO", outstream: "typing.TextIO", exec_root: str + ): + # NOTE: Sphinx performs its own logging re-configuration, so any + # logging config we do isn't respected by Sphinx. Controlling where + # stdout and stderr goes are the main mechanisms. Recall that + # Bazel send worker stderr to the worker log file. + # outputBase=$(bazel info output_base) + # find $outputBase/bazel-workers/ -type f -printf '%T@ %p\n' | sort -n | tail -1 | awk '{print $2}' + logging.basicConfig(level=logging.WARN) + logger.info("Initializing worker") + + # The directory that paths are relative to. + self._exec_root = exec_root + # Where requests are read from. + self._instream = instream + # Where responses are written to. + self._outstream = outstream + + # dict[str srcdir, dict[str path, str digest]] + self._digests = {} + + # Internal output directories the worker gives to Sphinx that need + # to be cleaned up upon exit. + # set[str path] + self._worker_outdirs = set() + self._extension = BazelWorkerExtension() + + sys.modules[_WORKER_SPHINX_EXT_MODULE_NAME] = self._extension + sphinx.application.builtin_extensions += (_WORKER_SPHINX_EXT_MODULE_NAME,) + + def __enter__(self): + return self + + def __exit__(self): + for worker_outdir in self._worker_outdirs: + shutil.rmtree(worker_outdir, ignore_errors=True) + + def run(self) -> None: + logger.info("Worker started") + try: + while True: + request = None + try: + request = self._get_next_request() + if request is None: + logger.info("Empty request: exiting") + break + response = self._process_request(request) + if response: + self._send_response(response) + except Exception: + logger.exception("Unhandled error: request=%s", request) + output = ( + f"Unhandled error:\nRequest id: {request.get('id')}\n" + + traceback.format_exc() + ) + request_id = 0 if not request else request.get("requestId", 0) + self._send_response( + { + "exitCode": 3, + "output": output, + "requestId": request_id, + } + ) + finally: + logger.info("Worker shutting down") + + def _get_next_request(self) -> "object | None": + line = self._instream.readline() + if not line: + return None + return json.loads(line) + + def _send_response(self, response: "WorkResponse") -> None: + self._outstream.write(json.dumps(response) + "\n") + self._outstream.flush() + + def _prepare_sphinx(self, request): + sphinx_args = request["arguments"] + srcdir = sphinx_args[0] + + incoming_digests = {} + current_digests = self._digests.setdefault(srcdir, {}) + changed_paths = [] + request_info = {"exec_root": self._exec_root, "inputs": request["inputs"]} + for entry in request["inputs"]: + path = entry["path"] + digest = entry["digest"] + # Make the path srcdir-relative so Sphinx understands it. + path = path.removeprefix(srcdir + "/") + incoming_digests[path] = digest + + if path not in current_digests: + logger.info("path %s new", path) + changed_paths.append(path) + elif current_digests[path] != digest: + logger.info("path %s changed", path) + changed_paths.append(path) + + self._digests[srcdir] = incoming_digests + self._extension.changed_paths = changed_paths + request_info["changed_sources"] = changed_paths + + bazel_outdir = sphinx_args[1] + worker_outdir = bazel_outdir + ".worker-out.d" + self._worker_outdirs.add(worker_outdir) + sphinx_args[1] = worker_outdir + + request_info_path = os.path.join(srcdir, "_bazel_worker_request_info.json") + with open(request_info_path, "w") as fp: + json.dump(request_info, fp) + sphinx_args.append(f"--define={_REQUEST_INFO_CONFIG_NAME}={request_info_path}") + + return worker_outdir, bazel_outdir, sphinx_args + + @contextlib.contextmanager + def _redirect_streams(self): + out = io.StringIO() + orig_stdout = sys.stdout + try: + sys.stdout = out + yield out + finally: + sys.stdout = orig_stdout + + def _process_request(self, request: "WorkRequest") -> "WorkResponse | None": + logger.info("Request: %s", json.dumps(request, sort_keys=True, indent=2)) + if request.get("cancel"): + return None + + worker_outdir, bazel_outdir, sphinx_args = self._prepare_sphinx(request) + + # Prevent anything from going to stdout because it breaks the worker + # protocol. We have limited control over where Sphinx sends output. + with self._redirect_streams() as stdout: + logger.info("main args: %s", sphinx_args) + exit_code = main(sphinx_args) + + if exit_code: + raise Exception( + "Sphinx main() returned failure: " + + f" exit code: {exit_code}\n" + + "========== STDOUT START ==========\n" + + stdout.getvalue().rstrip("\n") + + "\n" + + "========== STDOUT END ==========\n" + ) + + # Copying is unfortunately necessary because Bazel doesn't know to + # implicily bring along what the symlinks point to. + shutil.copytree(worker_outdir, bazel_outdir, dirs_exist_ok=True) + + response = { + "requestId": request.get("requestId", 0), + "output": stdout.getvalue(), + "exitCode": 0, + } + return response + + +class BazelWorkerExtension: + """A Sphinx extension implemented as a class acting like a module.""" + + def __init__(self): + # Make it look like a Module object + self.__name__ = _WORKER_SPHINX_EXT_MODULE_NAME + # set[str] of src-dir relative path names + self.changed_paths = set() + + def setup(self, app): + app.add_config_value(_REQUEST_INFO_CONFIG_NAME, "", "") + app.connect("env-get-outdated", self._handle_env_get_outdated) + return {"parallel_read_safe": True, "parallel_write_safe": True} + + def _handle_env_get_outdated(self, app, env, added, changed, removed): + changed = { + # NOTE: path2doc returns None if it's not a doc path + env.path2doc(p) + for p in self.changed_paths + } + + logger.info("changed docs: %s", changed) + return changed + + +def _worker_main(stdin, stdout, exec_root): + with Worker(stdin, stdout, exec_root) as worker: + return worker.run() + + +def _non_worker_main(): + args = [] + for arg in sys.argv: + if arg.startswith("@"): + with open(arg.removeprefix("@")) as fp: + lines = [line.strip() for line in fp if line.strip()] + args.extend(lines) + else: + args.append(arg) + sys.argv[:] = args + return main() + + if __name__ == "__main__": - sys.exit(main()) + if "--persistent_worker" in sys.argv: + sys.exit(_worker_main(sys.stdin, sys.stdout, os.getcwd())) + else: + sys.exit(_non_worker_main()) diff --git a/sphinxdocs/tests/sphinx_docs/doc1.md b/sphinxdocs/tests/sphinx_docs/doc1.md new file mode 100644 index 0000000000..f6f70ba28c --- /dev/null +++ b/sphinxdocs/tests/sphinx_docs/doc1.md @@ -0,0 +1,3 @@ +# doc1 + +hello doc 1 diff --git a/sphinxdocs/tests/sphinx_docs/doc2.md b/sphinxdocs/tests/sphinx_docs/doc2.md new file mode 100644 index 0000000000..06eb76a596 --- /dev/null +++ b/sphinxdocs/tests/sphinx_docs/doc2.md @@ -0,0 +1,3 @@ +# doc 2 + +hello doc 3 From d98547e8ec6bdbf4f250dd01c2921c2d91dc6db6 Mon Sep 17 00:00:00 2001 From: Aaron Levy Date: Tue, 10 Jun 2025 01:20:22 -0700 Subject: [PATCH 132/156] fix: Updating setuptools to patch CVE-2025-47273 (#2955) Update setuptools to patch CVE-2025-47273 --- CHANGELOG.md | 1 + python/private/pypi/deps.bzl | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e48e3d4f3d..eeafc70bae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ END_UNRELEASED_TEMPLATE * (py_wheel) py_wheel always creates zip64-capable wheel zips * (providers) (experimental) {obj}`PyInfo.venv_symlinks` replaces `PyInfo.site_packages_symlinks` +* (deps) Updating setuptools to patch CVE-2025-47273. {#v0-0-0-fixed} ### Fixed diff --git a/python/private/pypi/deps.bzl b/python/private/pypi/deps.bzl index 31a5201659..73b30c69ee 100644 --- a/python/private/pypi/deps.bzl +++ b/python/private/pypi/deps.bzl @@ -76,8 +76,8 @@ _RULE_DEPS = [ ), ( "pypi__setuptools", - "https://files.pythonhosted.org/packages/de/88/70c5767a0e43eb4451c2200f07d042a4bcd7639276003a9c54a68cfcc1f8/setuptools-70.0.0-py3-none-any.whl", - "54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4", + "https://files.pythonhosted.org/packages/90/99/158ad0609729111163fc1f674a5a42f2605371a4cf036d0441070e2f7455/setuptools-78.1.1-py3-none-any.whl", + "c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561", ), ( "pypi__tomli", From 013acd944643dfc939639cdc6e8b49ef685ed314 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 10 Jun 2025 19:58:08 +0900 Subject: [PATCH 133/156] feat: data and pyi files in the venv (#2936) This adds the remaining of the files into the venv and should get us reasonably close to handling 99% of the cases. The expected differences from this and a `venv` built by `uv` would be: * The `RECORD` files are excluded from the `venv`s for better cache hit rate in `bazel`. Topological ordering is removed because topo ordering doesn't provide the "closer target first" guarantees desired. For now, just use default ordering and document conflicts as undefined behavior. Internally, it continues to use first-wins (i.e. first in depset.to_list() order) semantics. Work towards #2156 --- .bazelrc | 4 +- MODULE.bazel | 6 + internal_dev_deps.bzl | 5 + python/private/BUILD.bazel | 2 + python/private/attributes.bzl | 2 +- python/private/common.bzl | 7 +- python/private/py_executable.bzl | 65 +++++---- python/private/py_info.bzl | 38 ++--- python/private/py_library.bzl | 137 ++++++++++++------ tests/modules/another_module/BUILD.bazel | 5 + tests/modules/another_module/MODULE.bazel | 1 + .../another_module/another_module_data.txt | 1 + tests/modules/other/MODULE.bazel | 2 + tests/modules/other/simple_v1/BUILD.bazel | 14 ++ .../simple-1.0.0.dist-info/METADATA | 1 + .../site-packages/simple/__init__.py | 1 + .../site-packages/simple_v1_extras/data.txt | 0 tests/modules/other/simple_v2/BUILD.bazel | 15 ++ .../simple-2.0.0.dist-info/METADATA | 1 + .../simple-2.0.0.dist-info/licenses/LICENSE | 1 + .../site-packages/simple.libs/data.so | 2 + .../site-packages/simple/__init__.py | 1 + .../site-packages/simple/__init__.pyi | 1 + .../other/with_external_data/BUILD.bazel | 23 +++ .../site-packages/with_external_data.py | 1 + tests/venv_site_packages_libs/BUILD.bazel | 16 ++ tests/venv_site_packages_libs/bin.py | 49 ++++++- 27 files changed, 296 insertions(+), 105 deletions(-) create mode 100644 tests/modules/another_module/BUILD.bazel create mode 100644 tests/modules/another_module/MODULE.bazel create mode 100644 tests/modules/another_module/another_module_data.txt create mode 100644 tests/modules/other/simple_v1/BUILD.bazel create mode 100644 tests/modules/other/simple_v1/site-packages/simple-1.0.0.dist-info/METADATA create mode 100644 tests/modules/other/simple_v1/site-packages/simple/__init__.py create mode 100644 tests/modules/other/simple_v1/site-packages/simple_v1_extras/data.txt create mode 100644 tests/modules/other/simple_v2/BUILD.bazel create mode 100644 tests/modules/other/simple_v2/site-packages/simple-2.0.0.dist-info/METADATA create mode 100644 tests/modules/other/simple_v2/site-packages/simple-2.0.0.dist-info/licenses/LICENSE create mode 100644 tests/modules/other/simple_v2/site-packages/simple.libs/data.so create mode 100644 tests/modules/other/simple_v2/site-packages/simple/__init__.py create mode 100644 tests/modules/other/simple_v2/site-packages/simple/__init__.pyi create mode 100644 tests/modules/other/with_external_data/BUILD.bazel create mode 100644 tests/modules/other/with_external_data/site-packages/with_external_data.py diff --git a/.bazelrc b/.bazelrc index 7e744fb67a..f7f31aed98 100644 --- a/.bazelrc +++ b/.bazelrc @@ -4,8 +4,8 @@ # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it) # To update these lines, execute # `bazel run @rules_bazel_integration_test//tools:update_deleted_packages` -build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma,tests/modules/other/nspkg_single -query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma,tests/modules/other/nspkg_single +build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/another_module,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma,tests/modules/other/nspkg_single,tests/modules/other/simple_v1,tests/modules/other/simple_v2,tests/modules/other/with_external_data +query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/another_module,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma,tests/modules/other/nspkg_single,tests/modules/other/simple_v1,tests/modules/other/simple_v2,tests/modules/other/with_external_data test --test_output=errors diff --git a/MODULE.bazel b/MODULE.bazel index 144e130c1b..77fa12d113 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -86,6 +86,7 @@ bazel_dep(name = "rules_multirun", version = "0.9.0", dev_dependency = True) bazel_dep(name = "bazel_ci_rules", version = "1.0.0", dev_dependency = True) bazel_dep(name = "rules_pkg", version = "1.0.1", dev_dependency = True) bazel_dep(name = "other", version = "0", dev_dependency = True) +bazel_dep(name = "another_module", version = "0", dev_dependency = True) # Extra gazelle plugin deps so that WORKSPACE.bzlmod can continue including it for e2e tests. # We use `WORKSPACE.bzlmod` because it is impossible to have dev-only local overrides. @@ -116,6 +117,11 @@ local_path_override( path = "tests/modules/other", ) +local_path_override( + module_name = "another_module", + path = "tests/modules/another_module", +) + dev_python = use_extension( "//python/extensions:python.bzl", "python", diff --git a/internal_dev_deps.bzl b/internal_dev_deps.bzl index f2b33e279e..e6ade4035c 100644 --- a/internal_dev_deps.bzl +++ b/internal_dev_deps.bzl @@ -48,6 +48,11 @@ def rules_python_internal_deps(): path = "tests/modules/other", ) + local_repository( + name = "another_module", + path = "tests/modules/another_module", + ) + http_archive( name = "bazel_skylib", sha256 = "bc283cdfcd526a52c3201279cda4bc298652efa898b10b4db0837dc51652756f", diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index b319919305..8bcc6eaebe 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -450,11 +450,13 @@ bzl_library( ":attributes_bzl", ":common_bzl", ":flags_bzl", + ":normalize_name_bzl", ":precompile_bzl", ":py_cc_link_params_info_bzl", ":py_internal_bzl", ":rule_builders_bzl", ":toolchain_types_bzl", + ":version_bzl", "@bazel_skylib//lib:dicts", "@bazel_skylib//rules:common_settings", ], diff --git a/python/private/attributes.bzl b/python/private/attributes.bzl index ad8cba2e6c..c3b1cade91 100644 --- a/python/private/attributes.bzl +++ b/python/private/attributes.bzl @@ -260,7 +260,7 @@ The order of this list can matter because it affects the order that information from dependencies is merged in, which can be relevant depending on the ordering mode of depsets that are merged. -* {obj}`PyInfo.venv_symlinks` uses topological ordering. +* {obj}`PyInfo.venv_symlinks` uses default ordering. See {obj}`PyInfo` for more information about the ordering of its depsets and how its fields are merged. diff --git a/python/private/common.bzl b/python/private/common.bzl index e49dbad20c..163fb54d77 100644 --- a/python/private/common.bzl +++ b/python/private/common.bzl @@ -331,7 +331,7 @@ def collect_runfiles(ctx, files = depset()): # If the target is a File, then add that file to the runfiles. # Otherwise, add the target's **data runfiles** to the runfiles. # - # Note that, contray to best practice, the default outputs of the + # Note that, contrary to best practice, the default outputs of the # targets in `data` are *not* added, nor are the default runfiles. # # This ends up being important for several reasons, some of which are @@ -396,9 +396,8 @@ def create_py_info( implicit_pyc_files: {type}`depset[File]` Implicitly generated pyc files that a binary can choose to include. imports: depset of strings; the import path values to propagate. - venv_symlinks: {type}`list[tuple[str, str]]` tuples of - `(runfiles_path, site_packages_path)` for symlinks to create - in the consuming binary's venv site packages. + venv_symlinks: {type}`list[VenvSymlinkEntry]` instances for + symlinks to create in the consuming binary's venv. Returns: A tuple of the PyInfo instance and a depset of the diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index 7c3e0cb757..7e50247e61 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -650,10 +650,6 @@ def _create_venv_symlinks(ctx, venv_dir_map): # maps venv-relative path to the runfiles path it should point to entries = depset( - # NOTE: Topological ordering is used so that dependencies closer to the - # binary have precedence in creating their symlinks. This allows the - # binary a modicum of control over the result. - order = "topological", transitive = [ dep[PyInfo].venv_symlinks for dep in ctx.attr.deps @@ -680,43 +676,52 @@ def _create_venv_symlinks(ctx, venv_dir_map): return venv_files def _build_link_map(entries): - # dict[str kind, dict[str rel_path, str link_to_path]] - link_map = {} + # dict[str package, dict[str kind, dict[str rel_path, str link_to_path]]] + pkg_link_map = {} + + # dict[str package, str version] + version_by_pkg = {} + for entry in entries: - kind = entry.kind - kind_map = link_map.setdefault(kind, {}) - if entry.venv_path in kind_map: - # We ignore duplicates by design. The dependency closer to the - # binary gets precedence due to the topological ordering. + link_map = pkg_link_map.setdefault(entry.package, {}) + kind_map = link_map.setdefault(entry.kind, {}) + + if version_by_pkg.setdefault(entry.package, entry.version) != entry.version: + # We ignore duplicates by design. + continue + elif entry.venv_path in kind_map: + # We ignore duplicates by design. continue else: kind_map[entry.venv_path] = entry.link_to_path - # An empty link_to value means to not create the site package symlink. - # Because of the topological ordering, this allows binaries to remove - # entries by having an earlier dependency produce empty link_to values. - for kind, kind_map in link_map.items(): - for dir_path, link_to in kind_map.items(): - if not link_to: - kind_map.pop(dir_path) + # An empty link_to value means to not create the site package symlink. Because of the + # ordering, this allows binaries to remove entries by having an earlier dependency produce + # empty link_to values. + for link_map in pkg_link_map.values(): + for kind, kind_map in link_map.items(): + for dir_path, link_to in kind_map.items(): + if not link_to: + kind_map.pop(dir_path) # dict[str kind, dict[str rel_path, str link_to_path]] keep_link_map = {} # Remove entries that would be a child path of a created symlink. # Earlier entries have precedence to match how exact matches are handled. - for kind, kind_map in link_map.items(): - keep_kind_map = keep_link_map.setdefault(kind, {}) - for _ in range(len(kind_map)): - if not kind_map: - break - dirname, value = kind_map.popitem() - keep_kind_map[dirname] = value - prefix = dirname + "/" # Add slash to prevent /X matching /XY - for maybe_suffix in kind_map.keys(): - maybe_suffix += "/" # Add slash to prevent /X matching /XY - if maybe_suffix.startswith(prefix) or prefix.startswith(maybe_suffix): - kind_map.pop(maybe_suffix) + for link_map in pkg_link_map.values(): + for kind, kind_map in link_map.items(): + keep_kind_map = keep_link_map.setdefault(kind, {}) + for _ in range(len(kind_map)): + if not kind_map: + break + dirname, value = kind_map.popitem() + keep_kind_map[dirname] = value + prefix = dirname + "/" # Add slash to prevent /X matching /XY + for maybe_suffix in kind_map.keys(): + maybe_suffix += "/" # Add slash to prevent /X matching /XY + if maybe_suffix.startswith(prefix) or prefix.startswith(maybe_suffix): + kind_map.pop(maybe_suffix) return keep_link_map def _map_each_identity(v): diff --git a/python/private/py_info.bzl b/python/private/py_info.bzl index 2a2f4554e3..17c5e4e79e 100644 --- a/python/private/py_info.bzl +++ b/python/private/py_info.bzl @@ -67,11 +67,24 @@ the venv to create the path under. A runfiles-root relative path that `venv_path` will symlink to. If `None`, it means to not create a symlink. +""", + "package": """ +:type: str | None + +Represents the PyPI package name that the code originates from. It is normalized according to the +PEP440 with all `-` replaced with `_`, i.e. the same as the package name in the hub repository that +it would come from. """, "venv_path": """ :type: str A path relative to the `kind` directory within the venv. +""", + "version": """ +:type: str | None + +Represents the PyPI package version that the code originates from. It is normalized according to the +PEP440 standard. """, }, ) @@ -296,29 +309,9 @@ This field is currently unused in Bazel and may go away in the future. "venv_symlinks": """ :type: depset[VenvSymlinkEntry] -A depset with `topological` ordering. - - -Tuples of `(runfiles_path, site_packages_path)`. Where -* `runfiles_path` is a runfiles-root relative path. It is the path that - has the code to make importable. If `None` or empty string, then it means - to not create a site packages directory with the `site_packages_path` - name. -* `site_packages_path` is a path relative to the site-packages directory of - the venv for whatever creates the venv (typically py_binary). It makes - the code in `runfiles_path` available for import. Note that this - is created as a "raw" symlink (via `declare_symlink`). - :::{include} /_includes/experimental_api.md ::: -:::{tip} -The topological ordering means dependencies earlier and closer to the consumer -have precedence. This allows e.g. a binary to add dependencies that override -values from further way dependencies, such as forcing symlinks to point to -specific paths or preventing symlinks from being created. -::: - :::{versionadded} VERSION_NEXT_FEATURE ::: """, @@ -375,9 +368,6 @@ def _PyInfoBuilder_typedef(): :::{field} venv_symlinks :type: DepsetBuilder[tuple[str | None, str]] - - NOTE: This depset has `topological` order - ::: """ def _PyInfoBuilder_new(): @@ -417,7 +407,7 @@ def _PyInfoBuilder_new(): transitive_pyc_files = builders.DepsetBuilder(), transitive_pyi_files = builders.DepsetBuilder(), transitive_sources = builders.DepsetBuilder(), - venv_symlinks = builders.DepsetBuilder(order = "topological"), + venv_symlinks = builders.DepsetBuilder(), ) return self diff --git a/python/private/py_library.bzl b/python/private/py_library.bzl index fabc880a8d..e727694b32 100644 --- a/python/private/py_library.bzl +++ b/python/private/py_library.bzl @@ -41,6 +41,7 @@ load( "runfiles_root_path", ) load(":flags.bzl", "AddSrcsToRunfilesFlag", "PrecompileFlag", "VenvsSitePackages") +load(":normalize_name.bzl", "normalize_name") load(":precompile.bzl", "maybe_precompile") load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo") load(":py_info.bzl", "PyInfo", "VenvSymlinkEntry", "VenvSymlinkKind") @@ -52,6 +53,7 @@ load( "EXEC_TOOLS_TOOLCHAIN_TYPE", TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE", ) +load(":version.bzl", "version") _py_builtins = py_internal @@ -84,20 +86,22 @@ under the binary's venv site-packages directory that should be made available (i namespace packages]( https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages). However, the *content* of the files cannot be taken into account, merely their -presence or absense. Stated another way: [pkgutil-style namespace packages]( +presence or absence. Stated another way: [pkgutil-style namespace packages]( https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages) won't be understood as namespace packages; they'll be seen as regular packages. This will likely lead to conflicts with other targets that contribute to the namespace. -:::{tip} -This attributes populates {obj}`PyInfo.venv_symlinks`, which is -a topologically ordered depset. This means dependencies closer and earlier -to a consumer have precedence. See {obj}`PyInfo.venv_symlinks` for -more information. +:::{seealso} +This attributes populates {obj}`PyInfo.venv_symlinks`. ::: :::{versionadded} 1.4.0 ::: +:::{versionchanged} VERSION_NEXT_FEATURE +The topological order has been removed and if 2 different versions of the same PyPI +package are observed, the behaviour has no guarantees except that it is deterministic +and that only one package version will be included. +::: """, ), "_add_srcs_to_runfiles_flag": lambda: attrb.Label( @@ -157,7 +161,8 @@ def py_library_impl(ctx, *, semantics): imports = [] venv_symlinks = [] - imports, venv_symlinks = _get_imports_and_venv_symlinks(ctx, semantics) + package, version_str = _get_package_and_version(ctx) + imports, venv_symlinks = _get_imports_and_venv_symlinks(ctx, semantics, package, version_str) cc_info = semantics.get_cc_info_for_library(ctx) py_info, deps_transitive_sources, builtins_py_info = create_py_info( @@ -206,16 +211,46 @@ Source files are no longer added to the runfiles directly. ::: """ -def _get_imports_and_venv_symlinks(ctx, semantics): +def _get_package_and_version(ctx): + """Return package name and version + + If the package comes from PyPI then it will have a `.dist-info` as part of `data`, which + allows us to get the name of the package and its version. + """ + dist_info_metadata = None + for d in ctx.files.data: + # work on case insensitive FSes + if d.basename.lower() != "metadata": + continue + + if d.dirname.endswith(".dist-info"): + dist_info_metadata = d + + if not dist_info_metadata: + return None, None + + # in order to be able to have replacements in the venv, we have to add a + # third value into the venv_symlinks, which would be the normalized + # package name. This allows us to ensure that we can replace the `dist-info` + # directories by checking if the package key is there. + dist_info_dir = paths.basename(dist_info_metadata.dirname) + package, _, _suffix = dist_info_dir.rpartition(".dist-info") + package, _, version_str = package.rpartition("-") + return ( + normalize_name(package), # will have no dashes + version.normalize(version_str), # will have no dashes either + ) + +def _get_imports_and_venv_symlinks(ctx, semantics, package, version_str): imports = depset() - venv_symlinks = depset() + venv_symlinks = [] if VenvsSitePackages.is_enabled(ctx): - venv_symlinks = _get_venv_symlinks(ctx) + venv_symlinks = _get_venv_symlinks(ctx, package, version_str) else: imports = collect_imports(ctx, semantics) return imports, venv_symlinks -def _get_venv_symlinks(ctx): +def _get_venv_symlinks(ctx, package, version_str): imports = ctx.attr.imports if len(imports) == 0: fail("When venvs_site_packages is enabled, exactly one `imports` " + @@ -236,50 +271,61 @@ def _get_venv_symlinks(ctx): # Append slash to prevent incorrectly prefix-string matches site_packages_root += "/" - # We have to build a list of (runfiles path, site-packages path) pairs of - # the files to create in the consuming binary's venv site-packages directory. - # To minimize the number of files to create, we just return the paths - # to the directories containing the code of interest. + # We have to build a list of (runfiles path, site-packages path) pairs of the files to + # create in the consuming binary's venv site-packages directory. To minimize the number of + # files to create, we just return the paths to the directories containing the code of + # interest. + # + # However, namespace packages complicate matters: multiple distributions install in the + # same directory in site-packages. This works out because they don't overlap in their + # files. Typically, they install to different directories within the namespace package + # directory. We also need to ensure that we can handle a case where the main package (e.g. + # airflow) has directories only containing data files and then namespace packages coming + # along and being next to it. # - # However, namespace packages complicate matters: multiple - # distributions install in the same directory in site-packages. This - # works out because they don't overlap in their files. Typically, they - # install to different directories within the namespace package - # directory. Namespace package directories are simply directories - # within site-packages that *don't* have an `__init__.py` file, which - # can be arbitrarily deep. Thus, we simply have to look for the - # directories that _do_ have an `__init__.py` file and treat those as - # the path to symlink to. - - repo_runfiles_dirname = None - dirs_with_init = {} # dirname -> runfile path + # Lastly we have to assume python modules just being `.py` files (e.g. typing-extensions) + # is just a single Python file. + + dir_symlinks = {} # dirname -> runfile path venv_symlinks = [] - for src in ctx.files.srcs: - if src.extension not in PYTHON_FILE_EXTENSIONS: - continue + for src in ctx.files.srcs + ctx.files.data + ctx.files.pyi_srcs: path = _repo_relative_short_path(src.short_path) if not path.startswith(site_packages_root): continue path = path.removeprefix(site_packages_root) dir_name, _, filename = path.rpartition("/") - if dir_name and filename.startswith("__init__."): - dirs_with_init[dir_name] = None - repo_runfiles_dirname = runfiles_root_path(ctx, src.short_path).partition("/")[0] - elif not dir_name: - repo_runfiles_dirname = runfiles_root_path(ctx, src.short_path).partition("/")[0] + if dir_name in dir_symlinks: + # we already have this dir, this allows us to short-circuit since most of the + # ctx.files.data might share the same directories as ctx.files.srcs + continue + runfiles_dir_name, _, _ = runfiles_root_path(ctx, src.short_path).partition("/") + if dir_name: + # This can be either: + # * a directory with libs (e.g. numpy.libs, created by auditwheel) + # * a directory with `__init__.py` file that potentially also needs to be + # symlinked. + # * `.dist-info` directory + # + # This could be also regular files, that just need to be symlinked, so we will + # add the directory here. + dir_symlinks[dir_name] = runfiles_dir_name + elif src.extension in PYTHON_FILE_EXTENSIONS: # This would be files that do not have directories and we just need to add - # direct symlinks to them as is: - venv_symlinks.append(VenvSymlinkEntry( + # direct symlinks to them as is, we only allow Python files in here + entry = VenvSymlinkEntry( kind = VenvSymlinkKind.LIB, - link_to_path = paths.join(repo_runfiles_dirname, site_packages_root, filename), + link_to_path = paths.join(runfiles_dir_name, site_packages_root, filename), + package = package, + version = version_str, venv_path = filename, - )) + ) + venv_symlinks.append(entry) # Sort so that we encounter `foo` before `foo/bar`. This ensures we # see the top-most explicit package first. - dirnames = sorted(dirs_with_init.keys()) + dirnames = sorted(dir_symlinks.keys()) first_level_explicit_packages = [] for d in dirnames: is_sub_package = False @@ -292,11 +338,16 @@ def _get_venv_symlinks(ctx): first_level_explicit_packages.append(d) for dirname in first_level_explicit_packages: - venv_symlinks.append(VenvSymlinkEntry( + prefix = dir_symlinks[dirname] + entry = VenvSymlinkEntry( kind = VenvSymlinkKind.LIB, - link_to_path = paths.join(repo_runfiles_dirname, site_packages_root, dirname), + link_to_path = paths.join(prefix, site_packages_root, dirname), + package = package, + version = version_str, venv_path = dirname, - )) + ) + venv_symlinks.append(entry) + return venv_symlinks def _repo_relative_short_path(short_path): diff --git a/tests/modules/another_module/BUILD.bazel b/tests/modules/another_module/BUILD.bazel new file mode 100644 index 0000000000..3b56b6ee83 --- /dev/null +++ b/tests/modules/another_module/BUILD.bazel @@ -0,0 +1,5 @@ +filegroup( + name = "data", + srcs = ["another_module_data.txt"], + visibility = ["//visibility:public"], +) diff --git a/tests/modules/another_module/MODULE.bazel b/tests/modules/another_module/MODULE.bazel new file mode 100644 index 0000000000..8ed5a5543b --- /dev/null +++ b/tests/modules/another_module/MODULE.bazel @@ -0,0 +1 @@ +module(name = "another_module") diff --git a/tests/modules/another_module/another_module_data.txt b/tests/modules/another_module/another_module_data.txt new file mode 100644 index 0000000000..f742ebab60 --- /dev/null +++ b/tests/modules/another_module/another_module_data.txt @@ -0,0 +1 @@ +print("token") diff --git a/tests/modules/other/MODULE.bazel b/tests/modules/other/MODULE.bazel index 7cd3118b81..11a633d56b 100644 --- a/tests/modules/other/MODULE.bazel +++ b/tests/modules/other/MODULE.bazel @@ -1,3 +1,5 @@ module(name = "other") bazel_dep(name = "rules_python", version = "0") +bazel_dep(name = "bazel_skylib", version = "1.7.1") +bazel_dep(name = "another_module", version = "0") diff --git a/tests/modules/other/simple_v1/BUILD.bazel b/tests/modules/other/simple_v1/BUILD.bazel new file mode 100644 index 0000000000..da5db8164a --- /dev/null +++ b/tests/modules/other/simple_v1/BUILD.bazel @@ -0,0 +1,14 @@ +load("@rules_python//python:py_library.bzl", "py_library") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "simple_v1", + srcs = glob(["site-packages/**/*.py"]), + data = glob( + ["**/*"], + exclude = ["site-packages/**/*.py"], + ), + experimental_venvs_site_packages = "@rules_python//python/config_settings:venvs_site_packages", + imports = [package_name() + "/site-packages"], +) diff --git a/tests/modules/other/simple_v1/site-packages/simple-1.0.0.dist-info/METADATA b/tests/modules/other/simple_v1/site-packages/simple-1.0.0.dist-info/METADATA new file mode 100644 index 0000000000..ee76ec48a4 --- /dev/null +++ b/tests/modules/other/simple_v1/site-packages/simple-1.0.0.dist-info/METADATA @@ -0,0 +1 @@ +inside is v1 diff --git a/tests/modules/other/simple_v1/site-packages/simple/__init__.py b/tests/modules/other/simple_v1/site-packages/simple/__init__.py new file mode 100644 index 0000000000..5becc17c04 --- /dev/null +++ b/tests/modules/other/simple_v1/site-packages/simple/__init__.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/tests/modules/other/simple_v1/site-packages/simple_v1_extras/data.txt b/tests/modules/other/simple_v1/site-packages/simple_v1_extras/data.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modules/other/simple_v2/BUILD.bazel b/tests/modules/other/simple_v2/BUILD.bazel new file mode 100644 index 0000000000..45f83a5a88 --- /dev/null +++ b/tests/modules/other/simple_v2/BUILD.bazel @@ -0,0 +1,15 @@ +load("@rules_python//python:py_library.bzl", "py_library") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "simple_v2", + srcs = glob(["site-packages/**/*.py"]), + data = glob( + ["**/*"], + exclude = ["site-packages/**/*.py"], + ), + experimental_venvs_site_packages = "@rules_python//python/config_settings:venvs_site_packages", + imports = [package_name() + "/site-packages"], + pyi_srcs = glob(["**/*.pyi"]), +) diff --git a/tests/modules/other/simple_v2/site-packages/simple-2.0.0.dist-info/METADATA b/tests/modules/other/simple_v2/site-packages/simple-2.0.0.dist-info/METADATA new file mode 100644 index 0000000000..ee76ec48a4 --- /dev/null +++ b/tests/modules/other/simple_v2/site-packages/simple-2.0.0.dist-info/METADATA @@ -0,0 +1 @@ +inside is v1 diff --git a/tests/modules/other/simple_v2/site-packages/simple-2.0.0.dist-info/licenses/LICENSE b/tests/modules/other/simple_v2/site-packages/simple-2.0.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000000..0cb5e79499 --- /dev/null +++ b/tests/modules/other/simple_v2/site-packages/simple-2.0.0.dist-info/licenses/LICENSE @@ -0,0 +1 @@ +Some License diff --git a/tests/modules/other/simple_v2/site-packages/simple.libs/data.so b/tests/modules/other/simple_v2/site-packages/simple.libs/data.so new file mode 100644 index 0000000000..f023e3b9ae --- /dev/null +++ b/tests/modules/other/simple_v2/site-packages/simple.libs/data.so @@ -0,0 +1,2 @@ +# This is usually created by auditwheel when processing linux wheels and including +# dependencies. diff --git a/tests/modules/other/simple_v2/site-packages/simple/__init__.py b/tests/modules/other/simple_v2/site-packages/simple/__init__.py new file mode 100644 index 0000000000..8c0d5d5bb2 --- /dev/null +++ b/tests/modules/other/simple_v2/site-packages/simple/__init__.py @@ -0,0 +1 @@ +__version__ = "2.0.0" diff --git a/tests/modules/other/simple_v2/site-packages/simple/__init__.pyi b/tests/modules/other/simple_v2/site-packages/simple/__init__.pyi new file mode 100644 index 0000000000..bb7b160deb --- /dev/null +++ b/tests/modules/other/simple_v2/site-packages/simple/__init__.pyi @@ -0,0 +1 @@ +# Intentionally empty diff --git a/tests/modules/other/with_external_data/BUILD.bazel b/tests/modules/other/with_external_data/BUILD.bazel new file mode 100644 index 0000000000..fc047aadab --- /dev/null +++ b/tests/modules/other/with_external_data/BUILD.bazel @@ -0,0 +1,23 @@ +load("@bazel_skylib//rules:copy_file.bzl", "copy_file") +load("@rules_python//python:py_library.bzl", "py_library") + +package(default_visibility = ["//visibility:public"]) + +# The users may include data through other repos via annotations and copy_file +# just add this edge case. +# +# NOTE: if the data is not copied to `site-packages/` then it will not +# appear. +copy_file( + name = "external_data", + src = "@another_module//:data", + out = "site-packages/external_data/another_module_data.txt", +) + +py_library( + name = "with_external_data", + srcs = ["site-packages/with_external_data.py"], + data = [":external_data"], + experimental_venvs_site_packages = "@rules_python//python/config_settings:venvs_site_packages", + imports = [package_name() + "/site-packages"], +) diff --git a/tests/modules/other/with_external_data/site-packages/with_external_data.py b/tests/modules/other/with_external_data/site-packages/with_external_data.py new file mode 100644 index 0000000000..ccd9dcef9e --- /dev/null +++ b/tests/modules/other/with_external_data/site-packages/with_external_data.py @@ -0,0 +1 @@ +# Intentionally blank diff --git a/tests/venv_site_packages_libs/BUILD.bazel b/tests/venv_site_packages_libs/BUILD.bazel index d5a4fe6750..e64299e1ad 100644 --- a/tests/venv_site_packages_libs/BUILD.bazel +++ b/tests/venv_site_packages_libs/BUILD.bazel @@ -1,6 +1,20 @@ +load("//python:py_library.bzl", "py_library") load("//tests/support:py_reconfig.bzl", "py_reconfig_test") load("//tests/support:support.bzl", "SUPPORTS_BOOTSTRAP_SCRIPT") +py_library( + name = "user_lib", + deps = ["@other//simple_v1"], +) + +py_library( + name = "closer_lib", + deps = [ + ":user_lib", + "@other//simple_v2", + ], +) + py_reconfig_test( name = "venvs_site_packages_libs_test", srcs = ["bin.py"], @@ -9,10 +23,12 @@ py_reconfig_test( target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT, venvs_site_packages = "yes", deps = [ + ":closer_lib", "//tests/venv_site_packages_libs/nspkg_alpha", "//tests/venv_site_packages_libs/nspkg_beta", "@other//nspkg_delta", "@other//nspkg_gamma", "@other//nspkg_single", + "@other//with_external_data", ], ) diff --git a/tests/venv_site_packages_libs/bin.py b/tests/venv_site_packages_libs/bin.py index 58572a2a1e..7e5838d2c2 100644 --- a/tests/venv_site_packages_libs/bin.py +++ b/tests/venv_site_packages_libs/bin.py @@ -1,7 +1,7 @@ import importlib -import os import sys import unittest +from pathlib import Path class VenvSitePackagesLibraryTest(unittest.TestCase): @@ -27,6 +27,53 @@ def test_imported_from_venv(self): self.assert_imported_from_venv("nspkg.subnspkg.gamma") self.assert_imported_from_venv("nspkg.subnspkg.delta") self.assert_imported_from_venv("single_file") + self.assert_imported_from_venv("simple") + + def test_data_is_included(self): + self.assert_imported_from_venv("simple") + module = importlib.import_module("simple") + module_path = Path(module.__file__) + + site_packages = module_path.parent.parent + + # Ensure that packages from simple v1 are not present + files = [p.name for p in site_packages.glob("*")] + self.assertIn("simple_v1_extras", files) + + def test_override_pkg(self): + self.assert_imported_from_venv("simple") + module = importlib.import_module("simple") + self.assertEqual( + "1.0.0", + module.__version__, + ) + + def test_dirs_from_replaced_package_are_not_present(self): + self.assert_imported_from_venv("simple") + module = importlib.import_module("simple") + module_path = Path(module.__file__) + + site_packages = module_path.parent.parent + dist_info_dirs = [p.name for p in site_packages.glob("*.dist-info")] + self.assertEqual( + ["simple-1.0.0.dist-info"], + dist_info_dirs, + ) + + # Ensure that packages from simple v1 are not present + files = [p.name for p in site_packages.glob("*")] + self.assertNotIn("simple.libs", files) + + def test_data_from_another_pkg_is_included_via_copy_file(self): + self.assert_imported_from_venv("simple") + module = importlib.import_module("simple") + module_path = Path(module.__file__) + + site_packages = module_path.parent.parent + # Ensure that packages from simple v1 are not present + d = site_packages / "external_data" + files = [p.name for p in d.glob("*")] + self.assertIn("another_module_data.txt", files) if __name__ == "__main__": From cb1c382144f59f7190781fb19e090aee23536e65 Mon Sep 17 00:00:00 2001 From: Ted Kaplan Date: Tue, 10 Jun 2025 19:07:41 -0700 Subject: [PATCH 134/156] fix(pypi): Only show index_url_overrides warnings when they are needed (#2967) Fixes #2966 --- python/private/pypi/simpleapi_download.bzl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/python/private/pypi/simpleapi_download.bzl b/python/private/pypi/simpleapi_download.bzl index 164d4e8dbd..a3ba9691cd 100644 --- a/python/private/pypi/simpleapi_download.bzl +++ b/python/private/pypi/simpleapi_download.bzl @@ -148,10 +148,11 @@ def simpleapi_download( if found_on_index[pkg] != attr.index_url } - # buildifier: disable=print - print("You can use the following `index_url_overrides` to avoid the 404 warnings:\n{}".format( - render.dict(index_url_overrides), - )) + if index_url_overrides: + # buildifier: disable=print + print("You can use the following `index_url_overrides` to avoid the 404 warnings:\n{}".format( + render.dict(index_url_overrides), + )) return contents From 95fb54a5e7146fd9c743f2984814f444798c9233 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 10 Jun 2025 22:11:52 -0700 Subject: [PATCH 135/156] revert: change default bootstrap back to system_python (#2968) Switch the default bootstrap back to system_python, per maintainer discussion. The main reason is downstream consumers are unlikely to be fully ready for the usage of raw symlinks (declare_symlink artifacts). APIs to detect them aren't available until Bazel 8, which makes it difficult for packaging rules, such as rules_pkg, bazel-lib, or tar rules. This reverts the core part of commit 9f3512fe0cc6d7229170e45724e22e64be0b8300 --- CHANGELOG.md | 8 -------- docs/api/rules_python/python/config_settings/index.md | 11 +---------- python/config_settings/BUILD.bazel | 2 +- 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eeafc70bae..e8fa1751c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,14 +55,6 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-changed} ### Changed -* If using the (deprecated) autodetecting/runtime_env toolchain, then the Python - version specified at build-time *must* match the Python version used at - runtime (the {obj}`--@rules_python//python/config_settings:python_version` - flag and the {attr}`python_version` attribute control the build-time version - for a target). If they don't match, dependencies won't be importable. (Such a - misconfiguration was unlikely to work to begin with; this is called out as an - FYI). -* (rules) {obj}`--bootstrap_impl=script` is the default for non-Windows. * (rules) On Windows, {obj}`--bootstrap_impl=system_python` is forced. This allows setting `--bootstrap_impl=script` in bazelrc for mixed-platform environments. diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md index ae84d40b13..7fe25888dd 100644 --- a/docs/api/rules_python/python/config_settings/index.md +++ b/docs/api/rules_python/python/config_settings/index.md @@ -245,12 +245,8 @@ Values: ::::{bzl:flag} bootstrap_impl Determine how programs implement their startup process. -The default for this depends on the platform: -* Windows: `system_python` (**always** used) -* Other: `script` - Values: -* `system_python`: Use a bootstrap that requires a system Python available +* `system_python`: (default) Use a bootstrap that requires a system Python available in order to start programs. This requires {obj}`PyRuntimeInfo.bootstrap_template` to be a Python program. * `script`: Use a bootstrap that uses an arbitrary executable script (usually a @@ -273,11 +269,6 @@ instead. :::{versionadded} 0.33.0 ::: -:::{versionchanged} VERSION_NEXT_FEATURE -* The default for non-Windows changed from `system_python` to `script`. -* On Windows, the value is forced to `system_python`. -::: - :::: ::::{bzl:flag} current_config diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel index ee15828fa5..b11580c4cb 100644 --- a/python/config_settings/BUILD.bazel +++ b/python/config_settings/BUILD.bazel @@ -90,7 +90,7 @@ string_flag( rp_string_flag( name = "bootstrap_impl", - build_setting_default = BootstrapImplFlag.SCRIPT, + build_setting_default = BootstrapImplFlag.SYSTEM_PYTHON, override = select({ # Windows doesn't yet support bootstrap=script, so force disable it ":_is_windows": BootstrapImplFlag.SYSTEM_PYTHON, From fb2298a7f2e6789186d63ef645ceed96261d94a9 Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Wed, 11 Jun 2025 13:55:19 -0700 Subject: [PATCH 136/156] fix: grammar in an error message (#2971) --- python/private/pypi/extension.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index b79be6e038..867abe0898 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -263,7 +263,7 @@ def _create_whl_repos( repo_name = "{}_{}".format(pip_name, repo.repo_name) if repo_name in whl_libraries: - fail("Attempting to creating a duplicate library {} for {}".format( + fail("attempting to create a duplicate library {} for {}".format( repo_name, whl.name, )) From e03b63c725cbef77a5c9af254331086de4649e15 Mon Sep 17 00:00:00 2001 From: Keith Smiley Date: Wed, 11 Jun 2025 15:09:44 -0700 Subject: [PATCH 137/156] refactor: Add missing uses of DefaultInfo (#2972) Required for compatibility with https://github.com/bazelbuild/bazel/issues/20183 --- python/private/common.bzl | 4 ++-- python/private/py_wheel.bzl | 4 ++-- python/uv/private/uv_toolchain.bzl | 2 +- sphinxdocs/private/sphinx.bzl | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/python/private/common.bzl b/python/private/common.bzl index 163fb54d77..96f8ebeab4 100644 --- a/python/private/common.bzl +++ b/python/private/common.bzl @@ -425,7 +425,7 @@ def create_py_info( else: # TODO(b/228692666): Remove this once non-PyInfo targets are no # longer supported in `deps`. - files = target.files.to_list() + files = target[DefaultInfo].files.to_list() for f in files: if f.extension == "py": py_info.transitive_sources.add(f) @@ -449,7 +449,7 @@ def create_py_info( info = _get_py_info(target) py_info.merge_uses_shared_libraries(info.uses_shared_libraries) else: - files = target.files.to_list() + files = target[DefaultInfo].files.to_list() for f in files: py_info.merge_uses_shared_libraries(cc_helper.is_valid_shared_library_artifact(f)) if py_info.get_uses_shared_libraries(): diff --git a/python/private/py_wheel.bzl b/python/private/py_wheel.bzl index ffc24f6846..cfd4efdcda 100644 --- a/python/private/py_wheel.bzl +++ b/python/private/py_wheel.bzl @@ -480,7 +480,7 @@ def _py_wheel_impl(ctx): args.add("--no_compress") for target, filename in ctx.attr.extra_distinfo_files.items(): - target_files = target.files.to_list() + target_files = target[DefaultInfo].files.to_list() if len(target_files) != 1: fail( "Multi-file target listed in extra_distinfo_files %s", @@ -493,7 +493,7 @@ def _py_wheel_impl(ctx): ) for target, filename in ctx.attr.data_files.items(): - target_files = target.files.to_list() + target_files = target[DefaultInfo].files.to_list() if len(target_files) != 1: fail( "Multi-file target listed in data_files %s", diff --git a/python/uv/private/uv_toolchain.bzl b/python/uv/private/uv_toolchain.bzl index 8c7f1b4b8c..bd82e7452f 100644 --- a/python/uv/private/uv_toolchain.bzl +++ b/python/uv/private/uv_toolchain.bzl @@ -24,7 +24,7 @@ def _uv_toolchain_impl(ctx): uv = ctx.attr.uv default_info = DefaultInfo( - files = uv.files, + files = uv[DefaultInfo].files, runfiles = uv[DefaultInfo].default_runfiles, ) uv_toolchain_info = UvToolchainInfo( diff --git a/sphinxdocs/private/sphinx.bzl b/sphinxdocs/private/sphinx.bzl index ee6b994e2e..c1efda3508 100644 --- a/sphinxdocs/private/sphinx.bzl +++ b/sphinxdocs/private/sphinx.bzl @@ -386,7 +386,7 @@ def _sphinx_source_tree_impl(ctx): _relocate(orig_file) for src_target, dest in ctx.attr.renamed_srcs.items(): - src_files = src_target.files.to_list() + src_files = src_target[DefaultInfo].files.to_list() if len(src_files) != 1: fail("A single file must be specified to be renamed. Target {} " + "generate {} files: {}".format( From 108a66cefe3206ba1a15eac4b9dcc586b649aa0b Mon Sep 17 00:00:00 2001 From: honglooker Date: Wed, 11 Jun 2025 18:11:14 -0400 Subject: [PATCH 138/156] docs: fix typo in toolchains.md example code (#2970) Added missing commas in `local toolchains` instructions --- docs/toolchains.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/toolchains.md b/docs/toolchains.md index 57d43d27f1..be85a2471e 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -436,8 +436,8 @@ local_runtime_repo = use_repo_rule( ) local_runtime_toolchains_repo = use_repo_rule( - "@rules_python//python/local_toolchains:repos.bzl" - "local_runtime_toolchains_repo" + "@rules_python//python/local_toolchains:repos.bzl", + "local_runtime_toolchains_repo", dev_dependency = True, ) From ef14ae2143a3707da1b1c865a7b451b154df5353 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Wed, 11 Jun 2025 16:43:26 -0700 Subject: [PATCH 139/156] chore: prepare for 1.5 release (#2973) Update version markers with upcoming version. --- CHANGELOG.md | 14 +++++++------- .../rules_python/python/config_settings/index.md | 2 +- docs/environment-variables.md | 2 +- docs/toolchains.md | 2 +- python/features.bzl | 2 +- python/private/local_runtime_toolchains_repo.bzl | 6 +++--- python/private/py_info.bzl | 2 +- python/private/py_library.bzl | 2 +- python/private/py_runtime_info.bzl | 2 +- python/private/py_runtime_rule.bzl | 2 +- python/private/pypi/env_marker_info.bzl | 2 +- python/private/python.bzl | 10 +++++----- 12 files changed, 24 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8fa1751c2..57001ca44f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,12 +47,12 @@ BEGIN_UNRELEASED_TEMPLATE END_UNRELEASED_TEMPLATE --> -{#v0-0-0} -## Unreleased +{#1-5-0} +## [1.5.0] - 2025-06-11 -[0.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.0.0 +[1.5.0]: https://github.com/bazel-contrib/rules_python/releases/tag/1.5.0 -{#v0-0-0-changed} +{#1-5-0-changed} ### Changed * (rules) On Windows, {obj}`--bootstrap_impl=system_python` is forced. This @@ -66,7 +66,7 @@ END_UNRELEASED_TEMPLATE `PyInfo.site_packages_symlinks` * (deps) Updating setuptools to patch CVE-2025-47273. -{#v0-0-0-fixed} +{#1-5-0-fixed} ### Fixed * (rules) PyInfo provider is now advertised by py_test, py_binary, and py_library; @@ -93,7 +93,7 @@ END_UNRELEASED_TEMPLATE by platform even though the same version is used. Fixes [#2648](https://github.com/bazel-contrib/rules_python/issues/2648). * (pypi) `compile_pip_requirements` test rule works behind the proxy -{#v0-0-0-added} +{#1-5-0-added} ### Added * Repo utilities `execute_unchecked`, `execute_checked`, and `execute_checked_stdout` now support `log_stdout` and `log_stderr` keyword arg booleans. When these are `True` @@ -115,7 +115,7 @@ END_UNRELEASED_TEMPLATE Useful when an intermediate dependency needs to be upgraded to pull in security patches. -{#v0-0-0-removed} +{#1-5-0-removed} ### Removed * Nothing removed. diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md index 7fe25888dd..989ebf1128 100644 --- a/docs/api/rules_python/python/config_settings/index.md +++ b/docs/api/rules_python/python/config_settings/index.md @@ -167,7 +167,7 @@ Default: `//python/config_settings:_pip_env_marker_default_config` This flag points to a target providing {obj}`EnvMarkerInfo`, which determines the values used when environment markers are resolved at build time. -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.5.0 ::: :::: diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 26c171095d..8a51bcbfd2 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -65,7 +65,7 @@ The default became `1` if unspecified When `1`, the rules_python Starlark implementation of the pypi/pip integration is used instead of the legacy Python scripts. -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.5.0 ::: :::: diff --git a/docs/toolchains.md b/docs/toolchains.md index be85a2471e..668a458156 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -305,7 +305,7 @@ that can be used with `target_settings`. Some particular ones of note are: {flag}`--py_linux_libc` and {flag}`--py_freethreaded`, among others. ::: -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.5.0 Added support for custom platform names, `target_compatible_with`, and `target_settings` with `single_version_platform_override`. ::: diff --git a/python/features.bzl b/python/features.bzl index b678a45241..e3d1ffdf61 100644 --- a/python/features.bzl +++ b/python/features.bzl @@ -35,7 +35,7 @@ def _features_typedef(): True if the `PyInfo.venv_symlinks` field is available. - :::{versionadded} VERSION_NEXT_FEATURE + :::{versionadded} 1.5.0 ::: :::: diff --git a/python/private/local_runtime_toolchains_repo.bzl b/python/private/local_runtime_toolchains_repo.bzl index 004ca664ad..8ef5ee9728 100644 --- a/python/private/local_runtime_toolchains_repo.bzl +++ b/python/private/local_runtime_toolchains_repo.bzl @@ -96,7 +96,7 @@ conditions are met, typically values from `@platforms`. See the [Local toolchains] docs for examples and further information. -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.5.0 ::: """, ), @@ -145,7 +145,7 @@ The `target_settings` attribute, which handles `config_setting` values, instead of constraints. ::: -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.5.0 ::: """, ), @@ -183,7 +183,7 @@ The `target_compatible_with` attribute, which handles *constraint* values, instead of `config_settings`. ::: -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.5.0 ::: """, ), diff --git a/python/private/py_info.bzl b/python/private/py_info.bzl index 17c5e4e79e..31df5cfbde 100644 --- a/python/private/py_info.bzl +++ b/python/private/py_info.bzl @@ -312,7 +312,7 @@ This field is currently unused in Bazel and may go away in the future. :::{include} /_includes/experimental_api.md ::: -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.5.0 ::: """, }, diff --git a/python/private/py_library.bzl b/python/private/py_library.bzl index e727694b32..24adb5f3ca 100644 --- a/python/private/py_library.bzl +++ b/python/private/py_library.bzl @@ -97,7 +97,7 @@ This attributes populates {obj}`PyInfo.venv_symlinks`. :::{versionadded} 1.4.0 ::: -:::{versionchanged} VERSION_NEXT_FEATURE +:::{versionchanged} 1.5.0 The topological order has been removed and if 2 different versions of the same PyPI package are observed, the behaviour has no guarantees except that it is deterministic and that only one package version will be included. diff --git a/python/private/py_runtime_info.bzl b/python/private/py_runtime_info.bzl index d2ae17e360..efe14b2c06 100644 --- a/python/private/py_runtime_info.bzl +++ b/python/private/py_runtime_info.bzl @@ -334,7 +334,7 @@ to meet two criteria: interpreter. This typically requires the Python version to be known at build-time and match at runtime. -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.5.0 ::: """, "zip_main_template": """ diff --git a/python/private/py_runtime_rule.bzl b/python/private/py_runtime_rule.bzl index 6dadcfeac3..861014e117 100644 --- a/python/private/py_runtime_rule.bzl +++ b/python/private/py_runtime_rule.bzl @@ -360,7 +360,7 @@ Whether this runtime supports virtualenvs created at build time. See {obj}`PyRuntimeInfo.supports_build_time_venv` for docs. -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.5.0 ::: """, default = True, diff --git a/python/private/pypi/env_marker_info.bzl b/python/private/pypi/env_marker_info.bzl index b483436d98..c3c5ec69ed 100644 --- a/python/private/pypi/env_marker_info.bzl +++ b/python/private/pypi/env_marker_info.bzl @@ -8,7 +8,7 @@ The values to use during environment marker evaluation. The {obj}`--//python/config_settings:pip_env_marker_config` flag. ::: -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.5.0 """, fields = { "env": """ diff --git a/python/private/python.bzl b/python/private/python.bzl index 8e23668879..6eb8a3742e 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -1240,7 +1240,7 @@ The values should be one of the values in `@platforms//cpu` Docs for [Registering custom runtimes] ::: -:::{{versionadded}} VERSION_NEXT_FEATURE +:::{{versionadded}} 1.5.0 ::: """, ), @@ -1265,7 +1265,7 @@ The values should be one of the values in `@platforms//os` Docs for [Registering custom runtimes] ::: -:::{{versionadded}} VERSION_NEXT_FEATURE +:::{{versionadded}} 1.5.0 ::: """, ), @@ -1288,7 +1288,7 @@ Other values are allowed, in which case, `target_compatible_with`, `target_settings`, `os_name`, and `arch` should be specified so the toolchain is only used when appropriate. -:::{{versionchanged}} VERSION_NEXT_FEATURE +:::{{versionchanged}} 1.5.0 Arbitrary platform strings allowed. ::: """.format( @@ -1320,7 +1320,7 @@ If set, `target_settings`, `os_name`, and `arch` should also be set. Docs for [Registering custom runtimes] ::: -:::{{versionadded}} VERSION_NEXT_FEATURE +:::{{versionadded}} 1.5.0 ::: """, ), @@ -1334,7 +1334,7 @@ If set, `target_compatible_with`, `os_name`, and `arch` should also be set. Docs for [Registering custom runtimes] ::: -:::{{versionadded}} VERSION_NEXT_FEATURE +:::{{versionadded}} 1.5.0 ::: """, ), From 9b8f6501e8b814b4120ff23d787f2cb7ba8422c6 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Thu, 12 Jun 2025 11:51:50 +0900 Subject: [PATCH 140/156] fix: support pre-release versions and add new toolchain versions (#2969) Add latest toolchain builds and attempt adding a beta build. This shows/tests that we can handle pre-release versions just fine and we are able to test the toolchain matching. Whilst at it it implements the static advertising of the remaining interpreter information. Fixes #2837 --------- Co-authored-by: Richard Levasseur --- CHANGELOG.md | 9 + .../private/hermetic_runtime_repo_setup.bzl | 10 ++ python/versions.bzl | 160 ++++++++++++++++-- tests/python/python_tests.bzl | 25 +-- tests/toolchains/defs.bzl | 10 +- tests/toolchains/python_toolchain_test.py | 13 +- .../transitions/transitions_tests.bzl | 17 +- 7 files changed, 209 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57001ca44f..488f1054a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,12 @@ END_UNRELEASED_TEMPLATE {#1-5-0-changed} ### Changed +* (toolchain) Bundled toolchain version updates: + * 3.9 now references 3.9.23 + * 3.10 now references 3.10.18 + * 3.11 now references 3.11.13 + * 3.12 now references 3.12.11 + * 3.13 now references 3.13.4 * (rules) On Windows, {obj}`--bootstrap_impl=system_python` is forced. This allows setting `--bootstrap_impl=script` in bazelrc for mixed-platform environments. @@ -92,6 +98,8 @@ END_UNRELEASED_TEMPLATE * (pypi) Correctly aggregate the sources when the hashes specified in the lockfile differ by platform even though the same version is used. Fixes [#2648](https://github.com/bazel-contrib/rules_python/issues/2648). * (pypi) `compile_pip_requirements` test rule works behind the proxy +* (toolchains) The hermetic toolchains now correctly statically advertise the + `releaselevel` and `serial` for pre-release hermetic toolchains ({gh-issue}`2837`). {#1-5-0-added} ### Added @@ -114,6 +122,7 @@ END_UNRELEASED_TEMPLATE * (rules) Added support for a using constraints files with `compile_pip_requirements`. Useful when an intermediate dependency needs to be upgraded to pull in security patches. +* (toolchains): 3.14.0b2 has been added as a preview. {#1-5-0-removed} ### Removed diff --git a/python/private/hermetic_runtime_repo_setup.bzl b/python/private/hermetic_runtime_repo_setup.bzl index f944b0b914..98adba51d0 100644 --- a/python/private/hermetic_runtime_repo_setup.bzl +++ b/python/private/hermetic_runtime_repo_setup.bzl @@ -195,6 +195,14 @@ def define_hermetic_runtime_toolchain_impl( values = {"collect_code_coverage": "true"}, visibility = ["//visibility:private"], ) + if not version_info.pre: + releaselevel = "final" + else: + releaselevel = { + "a": "alpha", + "b": "beta", + "rc": "candidate", + }.get(version_info.pre[0]) py_runtime( name = "py3_runtime", @@ -204,6 +212,8 @@ def define_hermetic_runtime_toolchain_impl( "major": str(version_info.release[0]), "micro": str(version_info.release[2]), "minor": str(version_info.release[1]), + "releaselevel": releaselevel, + "serial": str(version_info.pre[1]) if version_info.pre else "0", }, coverage_tool = select({ # Convert empty string to None diff --git a/python/versions.bzl b/python/versions.bzl index e712a2e126..44af7baf69 100644 --- a/python/versions.bzl +++ b/python/versions.bzl @@ -186,6 +186,21 @@ TOOL_VERSIONS = { }, "strip_prefix": "python", }, + "3.9.23": { + "url": "20250610/cpython-{python_version}+20250610-{platform}-{build}.tar.gz", + "sha256": { + "aarch64-apple-darwin": "f1a60528b6088ee8b8a34ca0e960998f4f664bed300ec0bbfe9d66ccbda74e50", + "aarch64-unknown-linux-gnu": "2871cf240bce3c021de829d73da04026febd7a775d1a1a1b37603ec6419fb6c1", + "ppc64le-unknown-linux-gnu": "2ba44a8e084a4661dbe50c0f0e3cf0a57227c6f1cff13fc2ae2f4d8ceae699fc", + "riscv64-unknown-linux-gnu": "7a735aebfc8b19a8af1f03e28babaf18a46cf8db0a931343dac1269376a1f693", + "s390x-unknown-linux-gnu": "27cfc030f782e2683c664e41dcef36051467c98676e133cbef04d4b7155ac4aa", + "x86_64-apple-darwin": "debd576badb6fdabb793ec9956512102f5a813c837449b1fe007c0af977db36c", + "x86_64-pc-windows-msvc": "28fbf2026929e00a300466220917c7029a69331700badb34b1691f1a99aa38e3", + "x86_64-unknown-linux-gnu": "21440e51aee78f3d92faf9375a90713542d8332e83d94c284f8f3d52c58eb5ca", + "x86_64-unknown-linux-musl": "7a881405a41cb4edf8c0d7c469c2f4759f601bc6f3c47978424a1ab1d0f1fada", + }, + "strip_prefix": "python", + }, "3.10.2": { "url": "20220227/cpython-{python_version}+20220227-{platform}-{build}.tar.gz", "sha256": { @@ -321,6 +336,21 @@ TOOL_VERSIONS = { }, "strip_prefix": "python", }, + "3.10.18": { + "url": "20250610/cpython-{python_version}+20250610-{platform}-{build}.tar.gz", + "sha256": { + "aarch64-apple-darwin": "a6590f71f670c7d121ac4f068dc83e271cf03309b80b1fa5890ee4875b7b691d", + "aarch64-unknown-linux-gnu": "b4d7cfb2cb5163da1ae5955ae8b33ac0b356780483d2993099899cf59efaea70", + "ppc64le-unknown-linux-gnu": "36aeae5cc61ff07c78b061f1b6aac628998a380ad45fadc82b8764185544fd7f", + "riscv64-unknown-linux-gnu": "2f6dd270598b655db5da5d98d1c43e560f6fb46c67a8fd68ff9b11ee9f6d79ff", + "s390x-unknown-linux-gnu": "616e56fe69c97a1d0ff13c00f337b2a91c972323c5d9a1828fdfc4d764b440fa", + "x86_64-apple-darwin": "4d72c1c1dcd2c4fe80055ef1b24fe4146f2de938aea1e3676faf91476f3f17e8", + "x86_64-pc-windows-msvc": "867b6dbcdb71d8ebb709ff54fbca8ad43d05cc21e5c157f39745c4dc44c1f8e2", + "x86_64-unknown-linux-gnu": "58f88ed6117078fdbc98976c9bc83b918f1f9c0c2ec21b80a582104f4839861c", + "x86_64-unknown-linux-musl": "d782c0569d6d7e21a5ed195ad7b41d0af8456b031e0814714d18cdeaa876f262", + }, + "strip_prefix": "python", + }, "3.11.1": { "url": "20230116/cpython-{python_version}+20230116-{platform}-{build}.tar.gz", "sha256": { @@ -436,18 +466,18 @@ TOOL_VERSIONS = { }, "strip_prefix": "python", }, - "3.11.11": { - "url": "20250317/cpython-{python_version}+20250317-{platform}-{build}.tar.gz", + "3.11.13": { + "url": "20250610/cpython-{python_version}+20250610-{platform}-{build}.tar.gz", "sha256": { - "aarch64-apple-darwin": "19b147c7e4b742656da4cb6ba35bc3ea2f15aa5f4d1bbbc38d09e2e85551e927", - "aarch64-unknown-linux-gnu": "7d52b5206afe617de2899af477f5a1d275ecbce80fb8300301b254ebf1da5a90", - "ppc64le-unknown-linux-gnu": "17c049f70ce719adc89dd0ae26f4e6a28f6aaedc63c2efef6bbb9c112ea4d692", - "riscv64-unknown-linux-gnu": "83ed50713409576756f5708e8f0549a15c17071bea22b71f15e11a7084f09481", - "s390x-unknown-linux-gnu": "298507f1f8d962b1bb98cb506c99e7e0d291a63eb9117e1521141e6b3825fd56", - "x86_64-apple-darwin": "a870cd965e7dded5100d13b1d34cab1c32a92811e000d10fbfe9bbdb36cdaa0e", - "x86_64-pc-windows-msvc": "1cf5760eea0a9df3308ca2c4111b5cc18fd638b2a912dbe07606193e3f9aa123", - "x86_64-unknown-linux-gnu": "51e47bc0d1b9f4bf68dd395f7a39f60c58a87cde854cab47264a859eb666bb69", - "x86_64-unknown-linux-musl": "ee4d84f992c6a1df42096e26b970fe5938fd6c1eadd245894bc94c5737ff9977", + "aarch64-apple-darwin": "365037494ba4f53563c22292e49a8e4d0d495bcb6534fca9666bdd1b474abf36", + "aarch64-unknown-linux-gnu": "a5954f147e87d9bff3d9733ebb3e74fe997eec5b38eaf5cb4429038228962a16", + "ppc64le-unknown-linux-gnu": "9214126866418f290fda88832fa3e244630f918ebc8a4a9ee15ba922e9c98afd", + "riscv64-unknown-linux-gnu": "fd99008c3123f50ec2ad407c5c1e17c1a86590daaf88dae8e6f1fd28f099b7c2", + "s390x-unknown-linux-gnu": "e27ab1fff8bf9e507677252a03ed524c685a8629b56475e26ab6dd0f88465179", + "x86_64-apple-darwin": "b49044115a545e67d73f5265a613a25da7c9523431281aa7b94691f1013355af", + "x86_64-pc-windows-msvc": "c0f89e3776211147817d54084fa046e2603571e18ff2ae4a4a8ff84ca4f7defc", + "x86_64-unknown-linux-gnu": "d93a7699505ee0ac7dec0f09324ffb19a31cce3066a287bb1fe95285ce3ea0c7", + "x86_64-unknown-linux-musl": "499121bb917e5baeeb954f76bdbce36bb63af579ff1530966ae2280e8d812c5b", }, "strip_prefix": "python", }, @@ -559,6 +589,21 @@ TOOL_VERSIONS = { }, "strip_prefix": "python", }, + "3.12.11": { + "url": "20250610/cpython-{python_version}+20250610-{platform}-{build}.tar.gz", + "sha256": { + "aarch64-apple-darwin": "9c5826a93ddc15e8aa08de1e6e65b3ae0d45ea8eb0c2e9547b80ff4121b870ce", + "aarch64-unknown-linux-gnu": "eb33bc5a87443daf2fd218109df811bc4e4ea5ef9aec4fad75aa55da0258b96f", + "ppc64le-unknown-linux-gnu": "7b90bc528c5ddf30579dec52926d68fa6d5c90b65e24fc185d5fe283fdf0cbd9", + "riscv64-unknown-linux-gnu": "0f3103675102e351762a8fe574eae20335552a246a45a006d2a9ca14ce0952f8", + "s390x-unknown-linux-gnu": "a7ff0432208450ccebd5d328f69b84cc7c25b4af54fbab44803ddb11a2da5028", + "x86_64-apple-darwin": "199631baa35f3747ddfa2f1e28fc062b97ccd15b94a60c9294d4d129a73c9e53", + "x86_64-pc-windows-msvc": "e05fa165841c416d60365ca2216cad570f05ae5d3d027b9ad3beaad0529dd8cc", + "x86_64-unknown-linux-gnu": "77ab3efe5c6637fe8da0fdfbff5de1730c3b824874fe1368917886908b4c517b", + "x86_64-unknown-linux-musl": "9dd768494c4a34abcec316bc4802e957db98ed283024b527c0c40dfefd08b6fe", + }, + "strip_prefix": "python", + }, "3.13.0": { "url": "20241016/cpython-{python_version}+20241016-{platform}-{build}.{ext}", "sha256": { @@ -674,16 +719,99 @@ TOOL_VERSIONS = { "x86_64-unknown-linux-gnu-freethreaded": "python/install", }, }, + "3.13.4": { + "url": "20250610/cpython-{python_version}+20250610-{platform}-{build}.{ext}", + "sha256": { + "aarch64-apple-darwin": "c2ce6601b2668c7bd1f799986af5ddfbff36e88795741864aba6e578cb02ed7f", + "aarch64-unknown-linux-gnu": "3c2596ece08ffe17e11bc1f27aeb4ce1195d2490a83d695d36ef4933d5c5ca53", + "ppc64le-unknown-linux-gnu": "b3cc13ee177b8db1d3e9b2eac413484e3c6a356f97d91dc59de8d3fd8cf79d6b", + "riscv64-unknown-linux-gnu": "d1b989e57a9ce29f6c945eeffe0e9750c222fdd09e99d2f8d6b0d8532a523053", + "s390x-unknown-linux-gnu": "d1d19fb01961ac6476712fdd6c5031f74c83666f6f11aa066207e9a158f7e3d8", + "x86_64-apple-darwin": "79feb6ca68f3921d07af52d9db06cf134e6f36916941ea850ab0bc20f5ff638b", + "x86_64-pc-windows-msvc": "29ac3585cc2dcfd79e3fe380c272d00e9d34351fc456e149403c86d3fea34057", + "x86_64-unknown-linux-gnu": "44e5477333ebca298a7a0a316985c6c3533b8645f92a83f7f73c44033832bf32", + "x86_64-unknown-linux-musl": "a3afbfa94b9ff4d9fc426b47eb3c8446cada535075b8d51b7bdc9d9ab9911fc2", + "aarch64-apple-darwin-freethreaded": "278dccade56b4bbeecb9a613b77012cf5c1433a5e9b8ef99230d5e61f31d9e02", + "aarch64-unknown-linux-gnu-freethreaded": "b1c1bd6ab9ef95b464d92a6a911cef1a8d9f0b0f6a192f694ef18ed15d882edf", + "ppc64le-unknown-linux-gnu-freethreaded": "ed66ae213a62b286b9b7338b816ccd2815f5248b7a28a185dc8159fe004149ae", + "riscv64-unknown-linux-gnu-freethreaded": "913264545215236660e4178bc3e5b57a20a444a8deb5c11680c95afc960b4016", + "s390x-unknown-linux-gnu-freethreaded": "7556a38ab5e507c1ec22bc38f9859982bc956cab7f4de05a2faac114feb306db", + "x86_64-apple-darwin-freethreaded": "64ab7ac8c88002d9ba20a92f72945bfa350268e944a7922500af75d20330574d", + "x86_64-pc-windows-msvc-freethreaded": "9457504547edb2e0156bf76b53c7e4941c7f61c0eff9fd5f4d816d3df51c58e3", + "x86_64-unknown-linux-gnu-freethreaded": "864df6e6819e8f8e855ce30f34410fdc5867d0616e904daeb9a40e5806e970d7", + }, + "strip_prefix": { + "aarch64-apple-darwin": "python", + "aarch64-unknown-linux-gnu": "python", + "ppc64le-unknown-linux-gnu": "python", + "s390x-unknown-linux-gnu": "python", + "riscv64-unknown-linux-gnu": "python", + "x86_64-apple-darwin": "python", + "x86_64-pc-windows-msvc": "python", + "x86_64-unknown-linux-gnu": "python", + "x86_64-unknown-linux-musl": "python", + "aarch64-apple-darwin-freethreaded": "python/install", + "aarch64-unknown-linux-gnu-freethreaded": "python/install", + "ppc64le-unknown-linux-gnu-freethreaded": "python/install", + "riscv64-unknown-linux-gnu-freethreaded": "python/install", + "s390x-unknown-linux-gnu-freethreaded": "python/install", + "x86_64-apple-darwin-freethreaded": "python/install", + "x86_64-pc-windows-msvc-freethreaded": "python/install", + "x86_64-unknown-linux-gnu-freethreaded": "python/install", + }, + }, + "3.14.0b2": { + "url": "20250610/cpython-{python_version}+20250610-{platform}-{build}.{ext}", + "sha256": { + "aarch64-apple-darwin": "6607351d140e83feb6e11dbde46ab5f99fa9fe039bdbaa12611d26bda0ed9343", + "aarch64-unknown-linux-gnu": "cc388d567f7c23921e0bef8dcae959dfab9ee24d10aeeb23688b21eac402817f", + "ppc64le-unknown-linux-gnu": "f9379ecc5dc71f9c58adf03d5524176ec36e1b40c788d29c260df54d09ad351c", + "riscv64-unknown-linux-gnu": "e6fbe4f7928ec606edee1506752659bf59216fdb208c744d268082ec79b16f42", + "s390x-unknown-linux-gnu": "1cf32c1173adc1cb70952bb47c92177a196f9e83b7a874f09599682e92ba0010", + "x86_64-apple-darwin": "a6d8196b174409e0ce67829c4e4ee5005c4be20a2efb41116e0521ad1fa1a717", + "x86_64-pc-windows-msvc": "0d88ec80c6c3e3ac462368850c19d3930bf2b1a1a5fe89da60c8534d0fac1a01", + "x86_64-unknown-linux-gnu": "93b29eea5214d19f0420ef8e459b007e15ea58349d60811122c78241fe51cb92", + "x86_64-unknown-linux-musl": "90e90a58ebff3416eb5a3f93ecb59b6eda945e2b706f5c13b0ba85f6b2bee130", + "aarch64-apple-darwin-freethreaded": "af0f34aa0dcd02bd3d960a1572a1ed8a17d55b373a22866f05041aaf16f8607d", + "aarch64-unknown-linux-gnu-freethreaded": "e76c7ab98e1c0f86a6996d1ec775ba8497bf46aa8ffa8c7b0f2e761f37305329", + "ppc64le-unknown-linux-gnu-freethreaded": "df2ae00827406e247f1aaaec76ffc7963b909c81075fc9940eee1ea9f753dd16", + "riscv64-unknown-linux-gnu-freethreaded": "09e347cb5f29e0eafd1eba73105ea9d853184b55fbaf4746cebec217430d6db5", + "s390x-unknown-linux-gnu-freethreaded": "f911605eee0eb7845a69acaf8bfb2e1811c76e9a5e3980d97fae93135df4b773", + "x86_64-apple-darwin-freethreaded": "dd27d519cf2a04917cb566366d6539477791d1b2f1fb42037d9179f469ff55a9", + "x86_64-pc-windows-msvc-freethreaded": "da966a17e434094d8f10b719d93c782d82eaf5207f2843cbaa58c3d91a8f0e32", + "x86_64-unknown-linux-gnu-freethreaded": "abd60d3a302e9d9c32ec78581fb3a9903079c56ec7a949ce658a7950423f350a", + }, + "strip_prefix": { + "aarch64-apple-darwin": "python", + "aarch64-unknown-linux-gnu": "python", + "ppc64le-unknown-linux-gnu": "python", + "s390x-unknown-linux-gnu": "python", + "riscv64-unknown-linux-gnu": "python", + "x86_64-apple-darwin": "python", + "x86_64-pc-windows-msvc": "python", + "x86_64-unknown-linux-gnu": "python", + "x86_64-unknown-linux-musl": "python", + "aarch64-apple-darwin-freethreaded": "python/install", + "aarch64-unknown-linux-gnu-freethreaded": "python/install", + "ppc64le-unknown-linux-gnu-freethreaded": "python/install", + "riscv64-unknown-linux-gnu-freethreaded": "python/install", + "s390x-unknown-linux-gnu-freethreaded": "python/install", + "x86_64-apple-darwin-freethreaded": "python/install", + "x86_64-pc-windows-msvc-freethreaded": "python/install", + "x86_64-unknown-linux-gnu-freethreaded": "python/install", + }, + }, } # buildifier: disable=unsorted-dict-items MINOR_MAPPING = { "3.8": "3.8.20", - "3.9": "3.9.21", - "3.10": "3.10.16", - "3.11": "3.11.11", - "3.12": "3.12.9", - "3.13": "3.13.2", + "3.9": "3.9.23", + "3.10": "3.10.18", + "3.11": "3.11.13", + "3.12": "3.12.11", + "3.13": "3.13.4", + "3.14": "3.14.0b2", } def _generate_platforms(): diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl index 116afa76ad..f0dc4825ac 100644 --- a/tests/python/python_tests.bzl +++ b/tests/python/python_tests.bzl @@ -304,11 +304,11 @@ def _test_toolchain_ordering(env): toolchain = [ _toolchain("3.10"), _toolchain("3.10.15"), - _toolchain("3.10.16"), - _toolchain("3.10.11"), + _toolchain("3.10.18"), + _toolchain("3.10.13"), _toolchain("3.11.1"), _toolchain("3.11.10"), - _toolchain("3.11.11", is_default = True), + _toolchain("3.11.13", is_default = True), ], ), _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), @@ -320,14 +320,15 @@ def _test_toolchain_ordering(env): for t in py.toolchains ] - env.expect.that_str(py.default_python_version).equals("3.11.11") + env.expect.that_str(py.default_python_version).equals("3.11.13") env.expect.that_dict(py.config.minor_mapping).contains_exactly({ - "3.10": "3.10.16", - "3.11": "3.11.11", - "3.12": "3.12.9", - "3.13": "3.13.2", + "3.10": "3.10.18", + "3.11": "3.11.13", + "3.12": "3.12.11", + "3.13": "3.13.4", + "3.14": "3.14.0b2", "3.8": "3.8.20", - "3.9": "3.9.21", + "3.9": "3.9.23", }) env.expect.that_collection(got_versions).contains_exactly([ # First the full-version toolchains that are in minor_mapping @@ -336,13 +337,13 @@ def _test_toolchain_ordering(env): # The default version is always set in the `python_version` flag, so know, that # the default match will be somewhere in the first bunch. "3.10", - "3.10.16", + "3.10.18", "3.11", - "3.11.11", + "3.11.13", # Next, the rest, where we will match things based on the `python_version` being # the same "3.10.15", - "3.10.11", + "3.10.13", "3.11.1", "3.11.10", ]).in_order() diff --git a/tests/toolchains/defs.bzl b/tests/toolchains/defs.bzl index a883b0af33..25863d18c4 100644 --- a/tests/toolchains/defs.bzl +++ b/tests/toolchains/defs.bzl @@ -15,6 +15,7 @@ "" load("//python:versions.bzl", "PLATFORMS", "TOOL_VERSIONS") +load("//python/private:version.bzl", "version") # buildifier: disable=bzl-visibility load("//tests/support:py_reconfig.bzl", "py_reconfig_test") def define_toolchain_tests(name): @@ -38,13 +39,20 @@ def define_toolchain_tests(name): is_platform = "_is_{}".format(platform_key) target_compatible_with[is_platform] = [] + parsed = version.parse(python_version, strict = True) + expect_python_version = "{0}.{1}.{2}".format(*parsed.release) + if parsed.pre: + expect_python_version = "{0}{1}{2}".format( + expect_python_version, + *parsed.pre + ) py_reconfig_test( name = "python_{}_test".format(python_version), srcs = ["python_toolchain_test.py"], main = "python_toolchain_test.py", python_version = python_version, env = { - "EXPECT_PYTHON_VERSION": python_version, + "EXPECT_PYTHON_VERSION": expect_python_version, }, deps = ["//python/runfiles"], data = ["//tests/support:current_build_settings"], diff --git a/tests/toolchains/python_toolchain_test.py b/tests/toolchains/python_toolchain_test.py index 591d7dbe8a..63ed42488f 100644 --- a/tests/toolchains/python_toolchain_test.py +++ b/tests/toolchains/python_toolchain_test.py @@ -27,7 +27,18 @@ def test_expected_toolchain_matches(self): ) self.assertIn(expected, settings["toolchain_label"], msg) - actual = "{v.major}.{v.minor}.{v.micro}".format(v=sys.version_info) + if sys.version_info.releaselevel == "final": + actual = "{v.major}.{v.minor}.{v.micro}".format(v=sys.version_info) + elif sys.version_info.releaselevel in ["beta"]: + actual = ( + "{v.major}.{v.minor}.{v.micro}{v.releaselevel[0]}{v.serial}".format( + v=sys.version_info + ) + ) + else: + raise NotImplementedError( + "Unsupported release level, please update the test" + ) self.assertEqual(actual, expect_version) diff --git a/tests/toolchains/transitions/transitions_tests.bzl b/tests/toolchains/transitions/transitions_tests.bzl index bddd1745f0..ef071188bb 100644 --- a/tests/toolchains/transitions/transitions_tests.bzl +++ b/tests/toolchains/transitions/transitions_tests.bzl @@ -56,14 +56,21 @@ def _impl(ctx): exec_tools = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE].exec_tools got_version = exec_tools.exec_interpreter[platform_common.ToolchainInfo].py3_runtime.interpreter_version_info + got = "{}.{}.{}".format( + got_version.major, + got_version.minor, + got_version.micro, + ) + if got_version.releaselevel != "final": + got = "{}{}{}".format( + got, + got_version.releaselevel[0], + got_version.serial, + ) return [ TestInfo( - got = "{}.{}.{}".format( - got_version.major, - got_version.minor, - got_version.micro, - ), + got = got, want = ctx.attr.want_version, ), ] From e225a1eddd6055b08cb832f7d4e73922d5f7d956 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Wed, 11 Jun 2025 22:27:04 -0700 Subject: [PATCH 141/156] chore: Fixup some typos in BuildKite job names (#2977) --- .bazelci/presubmit.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 7e9d4dea53..01af217924 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -90,7 +90,7 @@ tasks: gazelle_extension_min: <<: *common_workspace_flags_min_bazel <<: *minimum_supported_version - name: "Gazelle: workspace, minumum supported Bazel version" + name: "Gazelle: workspace, minimum supported Bazel version" platform: ubuntu2004 build_targets: ["//..."] test_targets: ["//..."] @@ -338,7 +338,7 @@ tasks: integration_test_bzlmod_build_file_generation_windows: <<: *reusable_build_test_all # coverage is not supported on Windows - name: "examples/bzlmod_build_file_generateion: Windows" + name: "examples/bzlmod_build_file_generation: Windows" working_directory: examples/bzlmod_build_file_generation platform: windows From f2fa07a56f575028cd84d4d4d169b734507c34d7 Mon Sep 17 00:00:00 2001 From: John Cater Date: Fri, 13 Jun 2025 12:31:51 -0400 Subject: [PATCH 142/156] refactor: Remove unused CC_TOOLCHAIN definition (#2981) Fixes #2979. The definition appears unused and helps advance the goal of entirely removing current_cc_toolchain: see https://github.com/bazelbuild/bazel/issues/26282. --- python/private/attributes.bzl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/python/private/attributes.bzl b/python/private/attributes.bzl index c3b1cade91..641fa13a23 100644 --- a/python/private/attributes.bzl +++ b/python/private/attributes.bzl @@ -156,12 +156,6 @@ def copy_common_test_kwargs(kwargs): if key in kwargs } -CC_TOOLCHAIN = { - # NOTE: The `cc_helper.find_cpp_toolchain()` function expects the attribute - # name to be this name. - "_cc_toolchain": attr.label(default = "@bazel_tools//tools/cpp:current_cc_toolchain"), -} - # The common "data" attribute definition. DATA_ATTRS = { # NOTE: The "flags" attribute is deprecated, but there isn't an alternative From 94e08f7dfe61962fa50508f01ea05c624307d487 Mon Sep 17 00:00:00 2001 From: Keith Smiley Date: Fri, 13 Jun 2025 19:20:26 -0700 Subject: [PATCH 143/156] Fix argument name typo (#2984) ``` ERROR: Traceback (most recent call last): File ".../rules_python++pip+rules_mypy_pip_312_click/BUILD.bazel", line 5, column 20, in whl_library_targets( File ".../rules_python+/python/private/pypi/whl_library_targets.bzl", line 337, column 53, in whl_library_targets "//conditions:default": create_inits( File ".../rules_python+/python/private/pypi/namespace_pkgs.bzl", line 72, column 25, in create_inits for out in get_files(**kwargs): File ".../rules_python+/python/private/pypi/namespace_pkgs.bzl", line 20, column 5, in get_files def get_files(*, srcs, ignored_dirnames = [], root = None): Error: get_files() got unexpected keyword argument: ignore_dirnames (did you mean 'ignored_dirnames'?) ``` --- python/private/pypi/whl_library_targets.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl index 3529566c49..518d17163f 100644 --- a/python/private/pypi/whl_library_targets.bzl +++ b/python/private/pypi/whl_library_targets.bzl @@ -336,7 +336,7 @@ def whl_library_targets( Label("//python/config_settings:is_venvs_site_packages"): [], "//conditions:default": create_inits( srcs = srcs + data + pyi_srcs, - ignore_dirnames = [], # If you need to ignore certain folders, you can patch rules_python here to do so. + ignored_dirnames = [], # If you need to ignore certain folders, you can patch rules_python here to do so. root = "site-packages", ), }) From ca235368d04fb0ebf39fc9174acfd883ca3e3675 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 16:23:13 +0900 Subject: [PATCH 144/156] build(deps): bump certifi from 2025.1.31 to 2025.6.15 in /tools/publish (#2999) Bumps [certifi](https://github.com/certifi/python-certifi) from 2025.1.31 to 2025.6.15.
Commits
  • e767d59 2025.06.15 (#357)
  • 3e70765 Bump actions/setup-python from 5.5.0 to 5.6.0
  • 9afd2ff Bump actions/download-artifact from 4.2.1 to 4.3.0
  • d7c816c remove code that's no longer required that 3.7 is our minimum (#351)
  • 1899613 Declare setuptools as the build backend in pyproject.toml (#350)
  • c874142 update CI for ubuntu 20.04 deprecation (#348)
  • 275c9eb 2025.04.26 (#347)
  • 3788331 Bump actions/setup-python from 5.4.0 to 5.5.0 (#346)
  • 9d1f1b7 Bump actions/download-artifact from 4.1.9 to 4.2.1 (#344)
  • 96b97a5 Bump actions/upload-artifact from 4.6.1 to 4.6.2 (#343)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=certifi&package-manager=pip&previous-version=2025.1.31&new-version=2025.6.15)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tools/publish/requirements_darwin.txt | 6 +++--- tools/publish/requirements_linux.txt | 6 +++--- tools/publish/requirements_universal.txt | 6 +++--- tools/publish/requirements_windows.txt | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tools/publish/requirements_darwin.txt b/tools/publish/requirements_darwin.txt index 483f88444e..af5bad246d 100644 --- a/tools/publish/requirements_darwin.txt +++ b/tools/publish/requirements_darwin.txt @@ -6,9 +6,9 @@ backports-tarfile==1.2.0 \ --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 # via jaraco-context -certifi==2025.1.31 \ - --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ - --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe +certifi==2025.6.15 \ + --hash=sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057 \ + --hash=sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b # via requests charset-normalizer==3.4.1 \ --hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \ diff --git a/tools/publish/requirements_linux.txt b/tools/publish/requirements_linux.txt index 62dbf1eb77..b2e9ccf5ab 100644 --- a/tools/publish/requirements_linux.txt +++ b/tools/publish/requirements_linux.txt @@ -6,9 +6,9 @@ backports-tarfile==1.2.0 \ --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 # via jaraco-context -certifi==2025.1.31 \ - --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ - --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe +certifi==2025.6.15 \ + --hash=sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057 \ + --hash=sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b # via requests cffi==1.17.1 \ --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ diff --git a/tools/publish/requirements_universal.txt b/tools/publish/requirements_universal.txt index e4e876b176..8a7426e517 100644 --- a/tools/publish/requirements_universal.txt +++ b/tools/publish/requirements_universal.txt @@ -6,9 +6,9 @@ backports-tarfile==1.2.0 ; python_full_version < '3.12' \ --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 # via jaraco-context -certifi==2025.1.31 \ - --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ - --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe +certifi==2025.6.15 \ + --hash=sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057 \ + --hash=sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b # via requests cffi==1.17.1 ; platform_python_implementation != 'PyPy' and sys_platform == 'linux' \ --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ diff --git a/tools/publish/requirements_windows.txt b/tools/publish/requirements_windows.txt index 043de9ecb1..11017aa4f9 100644 --- a/tools/publish/requirements_windows.txt +++ b/tools/publish/requirements_windows.txt @@ -6,9 +6,9 @@ backports-tarfile==1.2.0 \ --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 # via jaraco-context -certifi==2025.1.31 \ - --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ - --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe +certifi==2025.6.15 \ + --hash=sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057 \ + --hash=sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b # via requests charset-normalizer==3.4.1 \ --hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \ From 60b48e2156574ed40e24df32f7dbef59f6f6c4f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:00:40 +0900 Subject: [PATCH 145/156] build(deps): bump certifi from 2025.1.31 to 2025.6.15 in /docs (#3000) Bumps [certifi](https://github.com/certifi/python-certifi) from 2025.1.31 to 2025.6.15.
Commits
  • e767d59 2025.06.15 (#357)
  • 3e70765 Bump actions/setup-python from 5.5.0 to 5.6.0
  • 9afd2ff Bump actions/download-artifact from 4.2.1 to 4.3.0
  • d7c816c remove code that's no longer required that 3.7 is our minimum (#351)
  • 1899613 Declare setuptools as the build backend in pyproject.toml (#350)
  • c874142 update CI for ubuntu 20.04 deprecation (#348)
  • 275c9eb 2025.04.26 (#347)
  • 3788331 Bump actions/setup-python from 5.4.0 to 5.5.0 (#346)
  • 9d1f1b7 Bump actions/download-artifact from 4.1.9 to 4.2.1 (#344)
  • 96b97a5 Bump actions/upload-artifact from 4.6.1 to 4.6.2 (#343)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=certifi&package-manager=pip&previous-version=2025.1.31&new-version=2025.6.15)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 87c13aa8ba..b0a84d476b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -17,9 +17,9 @@ babel==2.17.0 \ --hash=sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d \ --hash=sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2 # via sphinx -certifi==2025.1.31 \ - --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ - --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe +certifi==2025.6.15 \ + --hash=sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057 \ + --hash=sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b # via requests charset-normalizer==3.4.1 \ --hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \ From be86f4acae5571c39c2d6a952e28c435cd722a91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:38:57 +0900 Subject: [PATCH 146/156] build(deps): bump requests from 2.32.3 to 2.32.4 in /docs (#2965) Bumps [requests](https://github.com/psf/requests) from 2.32.3 to 2.32.4.
Release notes

Sourced from requests's releases.

v2.32.4

2.32.4 (2025-06-10)

Security

  • CVE-2024-47081 Fixed an issue where a maliciously crafted URL and trusted environment will retrieve credentials for the wrong hostname/machine from a netrc file. (#6965)

Improvements

  • Numerous documentation improvements

Deprecations

  • Added support for pypy 3.11 for Linux and macOS. (#6926)
  • Dropped support for pypy 3.9 following its end of support. (#6926)
Changelog

Sourced from requests's changelog.

2.32.4 (2025-06-10)

Security

  • CVE-2024-47081 Fixed an issue where a maliciously crafted URL and trusted environment will retrieve credentials for the wrong hostname/machine from a netrc file.

Improvements

  • Numerous documentation improvements

Deprecations

  • Added support for pypy 3.11 for Linux and macOS.
  • Dropped support for pypy 3.9 following its end of support.
Commits
  • 021dc72 Polish up release tooling for last manual release
  • 821770e Bump version and add release notes for v2.32.4
  • 59f8aa2 Add netrc file search information to authentication documentation (#6876)
  • 5b4b64c Add more tests to prevent regression of CVE 2024 47081
  • 7bc4587 Add new test to check netrc auth leak (#6962)
  • 96ba401 Only use hostname to do netrc lookup instead of netloc
  • 7341690 Merge pull request #6951 from tswast/patch-1
  • 6716d7c remove links
  • a7e1c74 Update docs/conf.py
  • c799b81 docs: fix dead links to kenreitz.org
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=requests&package-manager=pip&previous-version=2.32.3&new-version=2.32.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/bazel-contrib/rules_python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index b0a84d476b..cfeb0cbf31 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -291,9 +291,9 @@ readthedocs-sphinx-ext==2.2.5 \ --hash=sha256:ee5fd5b99db9f0c180b2396cbce528aa36671951b9526bb0272dbfce5517bd27 \ --hash=sha256:f8c56184ea011c972dd45a90122568587cc85b0127bc9cf064d17c68bc809daa # via rules-python-docs (docs/pyproject.toml) -requests==2.32.3 \ - --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ - --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 +requests==2.32.4 \ + --hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \ + --hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422 # via # readthedocs-sphinx-ext # sphinx From 107a8781cdd207c9079ecd733c0028d2706a49f2 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Wed, 18 Jun 2025 02:44:55 +0900 Subject: [PATCH 147/156] fix: use platform_info.target_settings in toolchain aliases (#3001) During the refactor we forgot one more place where the `flag_values` on the platform information was used. They were no longer populated and broke. The solution is to use `selects.config_setting_group` to maintain behaviour and in order to smoke test I have added a target to verify that the aliases work. Related to #2875 Fixes #2993 Co-authored-by: Richard Levasseur --- python/config_settings/BUILD.bazel | 12 ++++++---- python/private/config_settings.bzl | 2 +- .../private/hermetic_runtime_repo_setup.bzl | 19 ++++++++-------- python/private/pypi/config_settings.bzl | 22 +++++++++---------- python/private/toolchain_aliases.bzl | 12 +++++++--- tests/toolchains/BUILD.bazel | 8 +++++++ 6 files changed, 47 insertions(+), 28 deletions(-) diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel index b11580c4cb..82a73cee6c 100644 --- a/python/config_settings/BUILD.bazel +++ b/python/config_settings/BUILD.bazel @@ -125,15 +125,19 @@ string_flag( visibility = ["//visibility:public"], ) -config_setting( +alias( name = "is_py_freethreaded", - flag_values = {":py_freethreaded": FreeThreadedFlag.YES}, + actual = ":_is_py_freethreaded_yes", + deprecation = "not actually public, please create your own config_setting using the flag that rules_python exposes", + tags = ["manual"], visibility = ["//visibility:public"], ) -config_setting( +alias( name = "is_py_non_freethreaded", - flag_values = {":py_freethreaded": FreeThreadedFlag.NO}, + actual = ":_is_py_freethreaded_no", + deprecation = "not actually public, please create your own config_setting using the flag that rules_python exposes", + tags = ["manual"], visibility = ["//visibility:public"], ) diff --git a/python/private/config_settings.bzl b/python/private/config_settings.bzl index aff5d016fb..3089b9c6cf 100644 --- a/python/private/config_settings.bzl +++ b/python/private/config_settings.bzl @@ -143,7 +143,7 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping, ) native.config_setting( name = "_is_py_linux_libc_musl", - flag_values = {libc: "glibc"}, + flag_values = {libc: "musl"}, visibility = _NOT_ACTUALLY_PUBLIC, ) freethreaded = Label("//python/config_settings:py_freethreaded") diff --git a/python/private/hermetic_runtime_repo_setup.bzl b/python/private/hermetic_runtime_repo_setup.bzl index 98adba51d0..6910ea14a1 100644 --- a/python/private/hermetic_runtime_repo_setup.bzl +++ b/python/private/hermetic_runtime_repo_setup.bzl @@ -22,7 +22,8 @@ load(":glob_excludes.bzl", "glob_excludes") load(":py_exec_tools_toolchain.bzl", "py_exec_tools_toolchain") load(":version.bzl", "version") -_IS_FREETHREADED = Label("//python/config_settings:is_py_freethreaded") +_IS_FREETHREADED_YES = Label("//python/config_settings:_is_py_freethreaded_yes") +_IS_FREETHREADED_NO = Label("//python/config_settings:_is_py_freethreaded_no") def define_hermetic_runtime_toolchain_impl( *, @@ -87,16 +88,16 @@ def define_hermetic_runtime_toolchain_impl( cc_import( name = "interface", interface_library = select({ - _IS_FREETHREADED: "libs/python{major}{minor}t.lib".format(**version_dict), - "//conditions:default": "libs/python{major}{minor}.lib".format(**version_dict), + _IS_FREETHREADED_YES: "libs/python{major}{minor}t.lib".format(**version_dict), + _IS_FREETHREADED_NO: "libs/python{major}{minor}.lib".format(**version_dict), }), system_provided = True, ) cc_import( name = "abi3_interface", interface_library = select({ - _IS_FREETHREADED: "libs/python3t.lib", - "//conditions:default": "libs/python3.lib", + _IS_FREETHREADED_YES: "libs/python3t.lib", + _IS_FREETHREADED_NO: "libs/python3.lib", }), system_provided = True, ) @@ -115,10 +116,10 @@ def define_hermetic_runtime_toolchain_impl( includes = [ "include", ] + select({ - _IS_FREETHREADED: [ + _IS_FREETHREADED_YES: [ "include/python{major}.{minor}t".format(**version_dict), ], - "//conditions:default": [ + _IS_FREETHREADED_NO: [ "include/python{major}.{minor}".format(**version_dict), "include/python{major}.{minor}m".format(**version_dict), ], @@ -224,8 +225,8 @@ def define_hermetic_runtime_toolchain_impl( implementation_name = "cpython", # See https://peps.python.org/pep-3147/ for pyc tag infix format pyc_tag = select({ - _IS_FREETHREADED: "cpython-{major}{minor}t".format(**version_dict), - "//conditions:default": "cpython-{major}{minor}".format(**version_dict), + _IS_FREETHREADED_YES: "cpython-{major}{minor}t".format(**version_dict), + _IS_FREETHREADED_NO: "cpython-{major}{minor}".format(**version_dict), }), ) diff --git a/python/private/pypi/config_settings.bzl b/python/private/pypi/config_settings.bzl index d1b85d16c1..3e828e59f5 100644 --- a/python/private/pypi/config_settings.bzl +++ b/python/private/pypi/config_settings.bzl @@ -80,8 +80,8 @@ FLAGS = struct( "is_pip_whl_auto", "is_pip_whl_no", "is_pip_whl_only", - "is_py_freethreaded", - "is_py_non_freethreaded", + "_is_py_freethreaded_yes", + "_is_py_freethreaded_no", "pip_whl_glibc_version", "pip_whl_muslc_version", "pip_whl_osx_arch", @@ -205,12 +205,12 @@ def _dist_config_settings(*, suffix, plat_flag_values, python_version, **kwargs) for name, f, compatible_with in [ ("py_none", _flags.whl, None), ("py3_none", _flags.whl_py3, None), - ("py3_abi3", _flags.whl_py3_abi3, (FLAGS.is_py_non_freethreaded,)), + ("py3_abi3", _flags.whl_py3_abi3, (FLAGS._is_py_freethreaded_no,)), ("none", _flags.whl_pycp3x, None), - ("abi3", _flags.whl_pycp3x_abi3, (FLAGS.is_py_non_freethreaded,)), + ("abi3", _flags.whl_pycp3x_abi3, (FLAGS._is_py_freethreaded_no,)), # The below are not specializations of one another, they are variants - (cpv, _flags.whl_pycp3x_abicp, (FLAGS.is_py_non_freethreaded,)), - (cpv + "t", _flags.whl_pycp3x_abicp, (FLAGS.is_py_freethreaded,)), + (cpv, _flags.whl_pycp3x_abicp, (FLAGS._is_py_freethreaded_no,)), + (cpv + "t", _flags.whl_pycp3x_abicp, (FLAGS._is_py_freethreaded_yes,)), ]: if (f, compatible_with) in used_flags: # This should never happen as all of the different whls should have @@ -237,12 +237,12 @@ def _dist_config_settings(*, suffix, plat_flag_values, python_version, **kwargs) for name, f, compatible_with in [ ("py_none", _flags.whl_plat, None), ("py3_none", _flags.whl_plat_py3, None), - ("py3_abi3", _flags.whl_plat_py3_abi3, (FLAGS.is_py_non_freethreaded,)), + ("py3_abi3", _flags.whl_plat_py3_abi3, (FLAGS._is_py_freethreaded_no,)), ("none", _flags.whl_plat_pycp3x, None), - ("abi3", _flags.whl_plat_pycp3x_abi3, (FLAGS.is_py_non_freethreaded,)), + ("abi3", _flags.whl_plat_pycp3x_abi3, (FLAGS._is_py_freethreaded_no,)), # The below are not specializations of one another, they are variants - (cpv, _flags.whl_plat_pycp3x_abicp, (FLAGS.is_py_non_freethreaded,)), - (cpv + "t", _flags.whl_plat_pycp3x_abicp, (FLAGS.is_py_freethreaded,)), + (cpv, _flags.whl_plat_pycp3x_abicp, (FLAGS._is_py_freethreaded_no,)), + (cpv + "t", _flags.whl_plat_pycp3x_abicp, (FLAGS._is_py_freethreaded_yes,)), ]: if (f, compatible_with) in used_flags: # This should never happen as all of the different whls should have @@ -329,7 +329,7 @@ def _dist_config_setting(*, name, compatible_with = None, native = native, **kwa compatible_with: {type}`tuple[Label]` A collection of config settings that are compatible with the given dist config setting. For example, if only non-freethreaded python builds are allowed, add - FLAGS.is_py_non_freethreaded here. + FLAGS._is_py_freethreaded_no here. native (struct): The struct containing alias and config_setting rules to use for creating the objects. Can be overridden for unit tests reasons. diff --git a/python/private/toolchain_aliases.bzl b/python/private/toolchain_aliases.bzl index 31ac4a8fdf..092863260c 100644 --- a/python/private/toolchain_aliases.bzl +++ b/python/private/toolchain_aliases.bzl @@ -14,7 +14,8 @@ """Create toolchain alias targets.""" -load("@rules_python//python:versions.bzl", "PLATFORMS") +load("@bazel_skylib//lib:selects.bzl", "selects") +load("//python:versions.bzl", "PLATFORMS") def toolchain_aliases(*, name, platforms, visibility = None, native = native): """Create toolchain aliases for the python toolchains. @@ -30,12 +31,17 @@ def toolchain_aliases(*, name, platforms, visibility = None, native = native): if platform not in platforms: continue + _platform = "_" + platform native.config_setting( - name = platform, - flag_values = PLATFORMS[platform].flag_values, + name = _platform, constraint_values = PLATFORMS[platform].compatible_with, visibility = ["//visibility:private"], ) + selects.config_setting_group( + name = platform, + match_all = PLATFORMS[platform].target_settings + [_platform], + visibility = ["//visibility:private"], + ) prefix = name for name in [ diff --git a/tests/toolchains/BUILD.bazel b/tests/toolchains/BUILD.bazel index f346651d46..b9952865cb 100644 --- a/tests/toolchains/BUILD.bazel +++ b/tests/toolchains/BUILD.bazel @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +load("@bazel_skylib//rules:build_test.bzl", "build_test") load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") load(":defs.bzl", "define_toolchain_tests") @@ -30,3 +31,10 @@ py_reconfig_test( "@platforms//cpu:x86_64", ] if BZLMOD_ENABLED else ["@platforms//:incompatible"], ) + +build_test( + name = "build_test", + targets = [ + "@python_3_11//:python_headers", + ], +) From 175a33610e853388c83730d9e2b5b2ac3626649d Mon Sep 17 00:00:00 2001 From: yushan26 <107004874+yushan26@users.noreply.github.com> Date: Wed, 18 Jun 2025 09:18:35 -0700 Subject: [PATCH 148/156] refactor(gazelle) Types for exposed members of `python.ParserOutput` are now all public (#2959) Export the members of `python.ParserOutput` struct to make it publicly accessible. This allows other `py` extensions to leverage the Python resolver logic for resolving Python imports, instead of have to duplicate the resolving logic. --------- Co-authored-by: yushan --- CHANGELOG.md | 21 +++++++++++++++++++++ gazelle/python/file_parser.go | 16 ++++++++-------- gazelle/python/file_parser_test.go | 22 +++++++++++----------- gazelle/python/generate.go | 2 +- gazelle/python/parser.go | 14 +++++++------- gazelle/python/resolve.go | 4 ++-- gazelle/python/std_modules.go | 2 +- gazelle/python/std_modules_test.go | 6 +++--- gazelle/python/target.go | 4 ++-- 9 files changed, 56 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 488f1054a1..bf3d25c792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,27 @@ BEGIN_UNRELEASED_TEMPLATE END_UNRELEASED_TEMPLATE --> +{#v0-0-0} +## Unreleased + +[0.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.0.0 + +{#v0-0-0-changed} +### Changed +* (gazelle) Types for exposed members of `python.ParserOutput` are now all public. + +{#v0-0-0-fixed} +### Fixed +* Nothing fixed. + +{#v0-0-0-added} +### Added +* Nothing added. + +{#v0-0-0-removed} +### Removed +* Nothing removed. + {#1-5-0} ## [1.5.0] - 2025-06-11 diff --git a/gazelle/python/file_parser.go b/gazelle/python/file_parser.go index c147984fc3..3f8363fbdf 100644 --- a/gazelle/python/file_parser.go +++ b/gazelle/python/file_parser.go @@ -41,8 +41,8 @@ const ( type ParserOutput struct { FileName string - Modules []module - Comments []comment + Modules []Module + Comments []Comment HasMain bool } @@ -127,24 +127,24 @@ func (p *FileParser) parseMain(ctx context.Context, node *sitter.Node) bool { return false } -// parseImportStatement parses a node for an import statement, returning a `module` and a boolean +// parseImportStatement parses a node for an import statement, returning a `Module` and a boolean // representing if the parse was OK or not. -func parseImportStatement(node *sitter.Node, code []byte) (module, bool) { +func parseImportStatement(node *sitter.Node, code []byte) (Module, bool) { switch node.Type() { case sitterNodeTypeDottedName: - return module{ + return Module{ Name: node.Content(code), LineNumber: node.StartPoint().Row + 1, }, true case sitterNodeTypeAliasedImport: return parseImportStatement(node.Child(0), code) case sitterNodeTypeWildcardImport: - return module{ + return Module{ Name: "*", LineNumber: node.StartPoint().Row + 1, }, true } - return module{}, false + return Module{}, false } // parseImportStatements parses a node for import statements, returning true if the node is @@ -188,7 +188,7 @@ func (p *FileParser) parseImportStatements(node *sitter.Node) bool { // It updates FileParser.output.Comments with the parsed comment. func (p *FileParser) parseComments(node *sitter.Node) bool { if node.Type() == sitterNodeTypeComment { - p.output.Comments = append(p.output.Comments, comment(node.Content(p.code))) + p.output.Comments = append(p.output.Comments, Comment(node.Content(p.code))) return true } return false diff --git a/gazelle/python/file_parser_test.go b/gazelle/python/file_parser_test.go index 3682cff753..20085f0e76 100644 --- a/gazelle/python/file_parser_test.go +++ b/gazelle/python/file_parser_test.go @@ -27,7 +27,7 @@ func TestParseImportStatements(t *testing.T) { name string code string filepath string - result []module + result []Module }{ { name: "not has import", @@ -39,7 +39,7 @@ func TestParseImportStatements(t *testing.T) { name: "has import", code: "import unittest\nimport os.path\nfrom foo.bar import abc.xyz", filepath: "abc.py", - result: []module{ + result: []Module{ { Name: "unittest", LineNumber: 1, @@ -66,7 +66,7 @@ func TestParseImportStatements(t *testing.T) { import unittest `, filepath: "abc.py", - result: []module{ + result: []Module{ { Name: "unittest", LineNumber: 2, @@ -79,7 +79,7 @@ func TestParseImportStatements(t *testing.T) { name: "invalid syntax", code: "import os\nimport", filepath: "abc.py", - result: []module{ + result: []Module{ { Name: "os", LineNumber: 1, @@ -92,7 +92,7 @@ func TestParseImportStatements(t *testing.T) { name: "import as", code: "import os as b\nfrom foo import bar as c# 123", filepath: "abc.py", - result: []module{ + result: []Module{ { Name: "os", LineNumber: 1, @@ -111,7 +111,7 @@ func TestParseImportStatements(t *testing.T) { { name: "complex import", code: "from unittest import *\nfrom foo import (bar as c, baz, qux as d)\nfrom . import abc", - result: []module{ + result: []Module{ { Name: "unittest.*", LineNumber: 1, @@ -152,7 +152,7 @@ func TestParseComments(t *testing.T) { units := []struct { name string code string - result []comment + result []Comment }{ { name: "not has comment", @@ -162,17 +162,17 @@ func TestParseComments(t *testing.T) { { name: "has comment", code: "# a = 1\n# b = 2", - result: []comment{"# a = 1", "# b = 2"}, + result: []Comment{"# a = 1", "# b = 2"}, }, { name: "has comment in if", code: "if True:\n # a = 1\n # b = 2", - result: []comment{"# a = 1", "# b = 2"}, + result: []Comment{"# a = 1", "# b = 2"}, }, { name: "has comment inline", code: "import os# 123\nfrom pathlib import Path as b#456", - result: []comment{"# 123", "#456"}, + result: []Comment{"# 123", "#456"}, }, } for _, u := range units { @@ -248,7 +248,7 @@ func TestParseFull(t *testing.T) { output, err := p.Parse(context.Background()) assert.NoError(t, err) assert.Equal(t, ParserOutput{ - Modules: []module{{Name: "bar.abc", LineNumber: 1, Filepath: "foo/a.py", From: "bar"}}, + Modules: []Module{{Name: "bar.abc", LineNumber: 1, Filepath: "foo/a.py", From: "bar"}}, Comments: nil, HasMain: false, FileName: "a.py", diff --git a/gazelle/python/generate.go b/gazelle/python/generate.go index 27930c1025..5eedbd9601 100644 --- a/gazelle/python/generate.go +++ b/gazelle/python/generate.go @@ -471,7 +471,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes for _, pyTestTarget := range pyTestTargets { if conftest != nil { - pyTestTarget.addModuleDependency(module{Name: strings.TrimSuffix(conftestFilename, ".py")}) + pyTestTarget.addModuleDependency(Module{Name: strings.TrimSuffix(conftestFilename, ".py")}) } pyTest := pyTestTarget.build() diff --git a/gazelle/python/parser.go b/gazelle/python/parser.go index 1b2a90dddf..cf80578220 100644 --- a/gazelle/python/parser.go +++ b/gazelle/python/parser.go @@ -145,9 +145,9 @@ func removeDupesFromStringTreeSetSlice(array []string) []string { return dedupe } -// module represents a fully-qualified, dot-separated, Python module as seen on +// Module represents a fully-qualified, dot-separated, Python module as seen on // the import statement, alongside the line number where it happened. -type module struct { +type Module struct { // The fully-qualified, dot-separated, Python module name as seen on import // statements. Name string `json:"name"` @@ -162,7 +162,7 @@ type module struct { // moduleComparator compares modules by name. func moduleComparator(a, b interface{}) int { - return godsutils.StringComparator(a.(module).Name, b.(module).Name) + return godsutils.StringComparator(a.(Module).Name, b.(Module).Name) } // annotationKind represents Gazelle annotation kinds. @@ -176,12 +176,12 @@ const ( annotationKindIncludeDep annotationKind = "include_dep" ) -// comment represents a Python comment. -type comment string +// Comment represents a Python comment. +type Comment string // asAnnotation returns an annotation object if the comment has the // annotationPrefix. -func (c *comment) asAnnotation() (*annotation, error) { +func (c *Comment) asAnnotation() (*annotation, error) { uncomment := strings.TrimLeft(string(*c), "# ") if !strings.HasPrefix(uncomment, annotationPrefix) { return nil, nil @@ -215,7 +215,7 @@ type annotations struct { // annotationsFromComments returns all the annotations parsed out of the // comments of a Python module. -func annotationsFromComments(comments []comment) (*annotations, error) { +func annotationsFromComments(comments []Comment) (*annotations, error) { ignore := make(map[string]struct{}) includeDeps := []string{} for _, comment := range comments { diff --git a/gazelle/python/resolve.go b/gazelle/python/resolve.go index 7a2ec3d68a..996cbbadc0 100644 --- a/gazelle/python/resolve.go +++ b/gazelle/python/resolve.go @@ -151,7 +151,7 @@ func (py *Resolver) Resolve( hasFatalError := false MODULES_LOOP: for it.Next() { - mod := it.Value().(module) + mod := it.Value().(Module) moduleParts := strings.Split(mod.Name, ".") possibleModules := []string{mod.Name} for len(moduleParts) > 1 { @@ -214,7 +214,7 @@ func (py *Resolver) Resolve( matches := ix.FindRulesByImportWithConfig(c, imp, languageName) if len(matches) == 0 { // Check if the imported module is part of the standard library. - if isStdModule(module{Name: moduleName}) { + if isStdModule(Module{Name: moduleName}) { continue MODULES_LOOP } else if cfg.ValidateImportStatements() { err := fmt.Errorf( diff --git a/gazelle/python/std_modules.go b/gazelle/python/std_modules.go index e10f87b6ea..ecb4f4c454 100644 --- a/gazelle/python/std_modules.go +++ b/gazelle/python/std_modules.go @@ -34,7 +34,7 @@ func init() { } } -func isStdModule(m module) bool { +func isStdModule(m Module) bool { _, ok := stdModules[m.Name] return ok } diff --git a/gazelle/python/std_modules_test.go b/gazelle/python/std_modules_test.go index bc22638e69..dbcd18c9d6 100644 --- a/gazelle/python/std_modules_test.go +++ b/gazelle/python/std_modules_test.go @@ -21,7 +21,7 @@ import ( ) func TestIsStdModule(t *testing.T) { - assert.True(t, isStdModule(module{Name: "unittest"})) - assert.True(t, isStdModule(module{Name: "os.path"})) - assert.False(t, isStdModule(module{Name: "foo"})) + assert.True(t, isStdModule(Module{Name: "unittest"})) + assert.True(t, isStdModule(Module{Name: "os.path"})) + assert.False(t, isStdModule(Module{Name: "foo"})) } diff --git a/gazelle/python/target.go b/gazelle/python/target.go index c40d6fb3b7..1fb9218656 100644 --- a/gazelle/python/target.go +++ b/gazelle/python/target.go @@ -69,7 +69,7 @@ func (t *targetBuilder) addSrcs(srcs *treeset.Set) *targetBuilder { } // addModuleDependency adds a single module dep to the target. -func (t *targetBuilder) addModuleDependency(dep module) *targetBuilder { +func (t *targetBuilder) addModuleDependency(dep Module) *targetBuilder { fileName := dep.Name + ".py" if dep.From != "" { fileName = dep.From + ".py" @@ -87,7 +87,7 @@ func (t *targetBuilder) addModuleDependency(dep module) *targetBuilder { func (t *targetBuilder) addModuleDependencies(deps *treeset.Set) *targetBuilder { it := deps.Iterator() for it.Next() { - t.addModuleDependency(it.Value().(module)) + t.addModuleDependency(it.Value().(Module)) } return t } From 5b1db075d0810d09db7b1411c273a968ee3e4be0 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Thu, 19 Jun 2025 15:49:03 +0900 Subject: [PATCH 149/156] feat(pypi): pip.defaults API for customizing pipstar 1/n (#2987) Parse env markers in pip.parse using starlark Summary: - Allow switching to the Starlark implementation of the marker evaluation function. - Add a way for users to modify the `env` for the marker evaluation when parsing the requirements. This can only be done by `rules_python` or the root module. - Limit the platform selection when parsing the requirements files. Work towards #2747 Work towards #2949 Split out from #2909 --------- Co-authored-by: Richard Levasseur --- CHANGELOG.md | 4 +- python/private/pypi/BUILD.bazel | 5 +- python/private/pypi/env_marker_info.bzl | 2 +- python/private/pypi/evaluate_markers.bzl | 19 +- python/private/pypi/extension.bzl | 258 ++++++++++++++++-- python/private/pypi/pep508_evaluate.bzl | 2 +- python/private/pypi/pip_repository.bzl | 10 + .../pypi/requirements_files_by_platform.bzl | 54 ++-- tests/pypi/extension/extension_tests.bzl | 111 +++++++- .../requirements_files_by_platform_tests.bzl | 41 ++- 10 files changed, 437 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf3d25c792..9897dc9ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,7 +62,9 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-added} ### Added -* Nothing added. +* (pypi) To configure the environment for `requirements.txt` evaluation, use the newly added + developer preview of the `pip.default` tag class. Only `rules_python` and root modules can use + this feature. {#v0-0-0-removed} ### Removed diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index d89dc6c228..b569b2217c 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -97,10 +97,10 @@ bzl_library( name = "evaluate_markers_bzl", srcs = ["evaluate_markers.bzl"], deps = [ - ":pep508_env_bzl", + ":deps_bzl", ":pep508_evaluate_bzl", - ":pep508_platform_bzl", ":pep508_requirement_bzl", + ":pypi_repo_utils_bzl", ], ) @@ -113,6 +113,7 @@ bzl_library( ":hub_repository_bzl", ":parse_requirements_bzl", ":parse_whl_name_bzl", + ":pep508_env_bzl", ":pip_repository_attrs_bzl", ":simpleapi_download_bzl", ":whl_config_setting_bzl", diff --git a/python/private/pypi/env_marker_info.bzl b/python/private/pypi/env_marker_info.bzl index c3c5ec69ed..37eefb2a0f 100644 --- a/python/private/pypi/env_marker_info.bzl +++ b/python/private/pypi/env_marker_info.bzl @@ -17,7 +17,7 @@ The {obj}`--//python/config_settings:pip_env_marker_config` flag. The values to use for environment markers when evaluating an expression. The keys and values should be compatible with the [PyPA dependency specifiers -specification](https://packaging.python.org/en/latest/specifications/dependency-specifiers/) +specification](https://packaging.python.org/en/latest/specifications/dependency-specifiers/). Missing values will be set to the specification's defaults or computed using available toolchain information. diff --git a/python/private/pypi/evaluate_markers.bzl b/python/private/pypi/evaluate_markers.bzl index 191933596e..58a29a9181 100644 --- a/python/private/pypi/evaluate_markers.bzl +++ b/python/private/pypi/evaluate_markers.bzl @@ -15,9 +15,7 @@ """A simple function that evaluates markers using a python interpreter.""" load(":deps.bzl", "record_files") -load(":pep508_env.bzl", "env") load(":pep508_evaluate.bzl", "evaluate") -load(":pep508_platform.bzl", "platform_from_str") load(":pep508_requirement.bzl", "requirement") load(":pypi_repo_utils.bzl", "pypi_repo_utils") @@ -30,22 +28,27 @@ SRCS = [ Label("//python/private/pypi/whl_installer:platform.py"), ] -def evaluate_markers(requirements, python_version = None): +def evaluate_markers(*, requirements, platforms): """Return the list of supported platforms per requirements line. Args: requirements: {type}`dict[str, list[str]]` of the requirement file lines to evaluate. - python_version: {type}`str | None` the version that can be used when evaluating the markers. + platforms: {type}`dict[str, dict[str, str]]` The environments that we for each requirement + file to evaluate. The keys between the platforms and requirements should be shared. Returns: dict of string lists with target platforms """ ret = {} - for req_string, platforms in requirements.items(): + for req_string, platform_strings in requirements.items(): req = requirement(req_string) - for platform in platforms: - if evaluate(req.marker, env = env(platform_from_str(platform, python_version))): - ret.setdefault(req_string, []).append(platform) + for platform_str in platform_strings: + env = platforms.get(platform_str) + if not env: + fail("Please define platform: '{}'".format(platform_str)) + + if evaluate(req.marker, env = env): + ret.setdefault(req_string, []).append(platform_str) return ret diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 867abe0898..97b6825e51 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -25,10 +25,11 @@ load("//python/private:repo_utils.bzl", "repo_utils") load("//python/private:version.bzl", "version") load("//python/private:version_label.bzl", "version_label") load(":attrs.bzl", "use_isolated") -load(":evaluate_markers.bzl", "evaluate_markers_py", EVALUATE_MARKERS_SRCS = "SRCS") +load(":evaluate_markers.bzl", "evaluate_markers_py", EVALUATE_MARKERS_SRCS = "SRCS", evaluate_markers_star = "evaluate_markers") load(":hub_repository.bzl", "hub_repository", "whl_config_settings_to_json") load(":parse_requirements.bzl", "parse_requirements") load(":parse_whl_name.bzl", "parse_whl_name") +load(":pep508_env.bzl", "env") load(":pip_repository_attrs.bzl", "ATTRS") load(":requirements_files_by_platform.bzl", "requirements_files_by_platform") load(":simpleapi_download.bzl", "simpleapi_download") @@ -65,22 +66,36 @@ def _whl_mods_impl(whl_mods_dict): whl_mods = whl_mods, ) +def _platforms(*, python_version, minor_mapping, config): + platforms = {} + python_version = full_version( + version = python_version, + minor_mapping = minor_mapping, + ) + abi = "cp3{}".format(python_version[2:]) + + for platform, values in config.platforms.items(): + key = "{}_{}".format(abi, platform) + platforms[key] = env(key) | values.env + return platforms + def _create_whl_repos( module_ctx, *, pip_attr, whl_overrides, + config, available_interpreters = INTERPRETER_LABELS, minor_mapping = MINOR_MAPPING, - evaluate_markers = evaluate_markers_py, - get_index_urls = None, - enable_pipstar = False): + evaluate_markers = None, + get_index_urls = None): """create all of the whl repositories Args: module_ctx: {type}`module_ctx`. pip_attr: {type}`struct` - the struct that comes from the tag class iteration. whl_overrides: {type}`dict[str, struct]` - per-wheel overrides. + config: The platform configuration. get_index_urls: A function used to get the index URLs available_interpreters: {type}`dict[str, Label]` The dictionary of available interpreters that have been registered using the `python` bzlmod extension. @@ -89,7 +104,6 @@ def _create_whl_repos( minor_mapping: {type}`dict[str, str]` The dictionary needed to resolve the full python version used to parse package METADATA files. evaluate_markers: the function used to evaluate the markers. - enable_pipstar: enable the pipstar feature. Returns a {type}`struct` with the following attributes: whl_map: {type}`dict[str, list[struct]]` the output is keyed by the @@ -160,23 +174,19 @@ def _create_whl_repos( whl_group_mapping = {} requirement_cycles = {} - requirements_by_platform = parse_requirements( - module_ctx, - requirements_by_platform = requirements_files_by_platform( - requirements_by_platform = pip_attr.requirements_by_platform, - requirements_linux = pip_attr.requirements_linux, - requirements_lock = pip_attr.requirements_lock, - requirements_osx = pip_attr.requirements_darwin, - requirements_windows = pip_attr.requirements_windows, - extra_pip_args = pip_attr.extra_pip_args, - python_version = full_version( - version = pip_attr.python_version, + if evaluate_markers: + # This is most likely unit tests + pass + elif config.enable_pipstar: + evaluate_markers = lambda _, requirements: evaluate_markers_star( + requirements = requirements, + platforms = _platforms( + python_version = pip_attr.python_version, minor_mapping = minor_mapping, + config = config, ), - logger = logger, - ), - extra_pip_args = pip_attr.extra_pip_args, - get_index_urls = get_index_urls, + ) + else: # NOTE @aignas 2024-08-02: , we will execute any interpreter that we find either # in the PATH or if specified as a label. We will configure the env # markers when evaluating the requirement lines based on the output @@ -191,14 +201,34 @@ def _create_whl_repos( # instances to perform this manipulation. This function should be executed # only once by the underlying code to minimize the overhead needed to # spin up a Python interpreter. - evaluate_markers = lambda module_ctx, requirements: evaluate_markers( + evaluate_markers = lambda module_ctx, requirements: evaluate_markers_py( module_ctx, requirements = requirements, python_interpreter = pip_attr.python_interpreter, python_interpreter_target = python_interpreter_target, srcs = pip_attr._evaluate_markers_srcs, logger = logger, + ) + + requirements_by_platform = parse_requirements( + module_ctx, + requirements_by_platform = requirements_files_by_platform( + requirements_by_platform = pip_attr.requirements_by_platform, + requirements_linux = pip_attr.requirements_linux, + requirements_lock = pip_attr.requirements_lock, + requirements_osx = pip_attr.requirements_darwin, + requirements_windows = pip_attr.requirements_windows, + extra_pip_args = pip_attr.extra_pip_args, + platforms = sorted(config.platforms), # here we only need keys + python_version = full_version( + version = pip_attr.python_version, + minor_mapping = minor_mapping, + ), + logger = logger, ), + extra_pip_args = pip_attr.extra_pip_args, + get_index_urls = get_index_urls, + evaluate_markers = evaluate_markers, logger = logger, ) @@ -233,7 +263,7 @@ def _create_whl_repos( for p, args in whl_overrides.get(whl.name, {}).items() }, ) - if not enable_pipstar: + if not config.enable_pipstar: maybe_args["experimental_target_platforms"] = pip_attr.experimental_target_platforms whl_library_args.update({k: v for k, v in maybe_args.items() if v}) @@ -258,7 +288,7 @@ def _create_whl_repos( auth_patterns = pip_attr.auth_patterns, python_version = major_minor, is_multiple_versions = whl.is_multiple_versions, - enable_pipstar = enable_pipstar, + enable_pipstar = config.enable_pipstar, ) repo_name = "{}_{}".format(pip_name, repo.repo_name) @@ -342,16 +372,85 @@ def _whl_repo(*, src, whl_library_args, is_multiple_versions, download_only, net ), ) +def _configure(config, *, platform, os_name, arch_name, override = False, env = {}): + """Set the value in the config if the value is provided""" + config.setdefault("platforms", {}) + if platform: + if not override and config.get("platforms", {}).get(platform): + return + + for key in env: + if key not in _SUPPORTED_PEP508_KEYS: + fail("Unsupported key in the PEP508 environment: {}".format(key)) + + config["platforms"][platform] = struct( + name = platform.replace("-", "_").lower(), + os_name = os_name, + arch_name = arch_name, + env = env, + ) + else: + config["platforms"].pop(platform) + +def _create_config(defaults): + if defaults["platforms"]: + return struct(**defaults) + + # NOTE: We have this so that it is easier to maintain unit tests assuming certain + # defaults + for cpu in [ + "x86_64", + "aarch64", + # TODO @aignas 2025-05-19: only leave tier 0-1 cpus when stabilizing the + # `pip.default` extension. i.e. drop the below values - users will have to + # define themselves if they need them. + "arm", + "ppc", + "s390x", + ]: + _configure( + defaults, + arch_name = cpu, + os_name = "linux", + platform = "linux_{}".format(cpu), + env = {"platform_version": "0"}, + ) + for cpu in [ + "aarch64", + "x86_64", + ]: + _configure( + defaults, + arch_name = cpu, + # We choose the oldest non-EOL version at the time when we release `rules_python`. + # See https://endoflife.date/macos + env = {"platform_version": "14.0"}, + os_name = "osx", + platform = "osx_{}".format(cpu), + ) + + _configure( + defaults, + arch_name = "x86_64", + env = {"platform_version": "0"}, + os_name = "windows", + platform = "windows_x86_64", + ) + return struct(**defaults) + def parse_modules( module_ctx, _fail = fail, simpleapi_download = simpleapi_download, + enable_pipstar = False, **kwargs): """Implementation of parsing the tag classes for the extension and return a struct for registering repositories. Args: module_ctx: {type}`module_ctx` module context. simpleapi_download: Used for testing overrides + enable_pipstar: {type}`bool` a flag to enable dropping Python dependency for + evaluation of the extension. _fail: {type}`function` the failure function, mainly for testing. **kwargs: Extra arguments passed to the layers below. @@ -389,6 +488,34 @@ You cannot use both the additive_build_content and additive_build_content_file a srcs_exclude_glob = whl_mod.srcs_exclude_glob, ) + defaults = { + "enable_pipstar": enable_pipstar, + "platforms": {}, + } + for mod in module_ctx.modules: + if not (mod.is_root or mod.name == "rules_python"): + continue + + for tag in mod.tags.default: + _configure( + defaults, + arch_name = tag.arch_name, + env = tag.env, + os_name = tag.os_name, + platform = tag.platform, + override = mod.is_root, + # TODO @aignas 2025-05-19: add more attr groups: + # * for AUTH - the default `netrc` usage could be configured through a common + # attribute. + # * for index/downloader config. This includes all of those attributes for + # overrides, etc. Index overrides per platform could be also used here. + # * for whl selection - selecting preferences of which `platform_tag`s we should use + # for what. We could also model the `cp313t` freethreaded as separate platforms. + ) + + config = _create_config(defaults) + + # TODO @aignas 2025-06-03: Merge override API with the builder? _overriden_whl_set = {} whl_overrides = {} for module in module_ctx.modules: @@ -498,11 +625,13 @@ You cannot use both the additive_build_content and additive_build_content_file a elif pip_attr.experimental_index_url_overrides: fail("'experimental_index_url_overrides' is a no-op unless 'experimental_index_url' is set") + # TODO @aignas 2025-05-19: express pip.parse as a series of configure calls out = _create_whl_repos( module_ctx, pip_attr = pip_attr, get_index_urls = get_index_urls, whl_overrides = whl_overrides, + config = config, **kwargs ) hub_whl_map.setdefault(hub_name, {}) @@ -651,6 +780,72 @@ def _pip_impl(module_ctx): else: return None +_default_attrs = { + "arch_name": attr.string( + doc = """\ +The CPU architecture name to be used. + +:::{note} +Either this or {attr}`env` `platform_machine` key should be specified. +::: +""", + ), + "os_name": attr.string( + doc = """\ +The OS name to be used. + +:::{note} +Either this or the appropriate `env` keys should be specified. +::: +""", + ), + "platform": attr.string( + doc = """\ +A platform identifier which will be used as the unique identifier within the extension evaluation. +If you are defining custom platforms in your project and don't want things to clash, use extension +[isolation] feature. + +[isolation]: https://bazel.build/rules/lib/globals/module#use_extension.isolate +""", + ), +} | { + "env": attr.string_dict( + doc = """\ +The values to use for environment markers when evaluating an expression. + +The keys and values should be compatible with the [PyPA dependency specifiers +specification](https://packaging.python.org/en/latest/specifications/dependency-specifiers/). + +Missing values will be set to the specification's defaults or computed using +available toolchain information. + +Supported keys: +* `implementation_name`, defaults to `cpython`. +* `os_name`, defaults to a value inferred from the {attr}`os_name`. +* `platform_machine`, defaults to a value inferred from the {attr}`arch_name`. +* `platform_release`, defaults to an empty value. +* `platform_system`, defaults to a value inferred from the {attr}`os_name`. +* `platform_version`, defaults to `0`. +* `sys_platform`, defaults to a value inferred from the {attr}`os_name`. + +::::{note} +This is only used if the {envvar}`RULES_PYTHON_ENABLE_PIPSTAR` is enabled. +:::: +""", + ), + # The values for PEP508 env marker evaluation during the lock file parsing +} + +_SUPPORTED_PEP508_KEYS = [ + "implementation_name", + "os_name", + "platform_machine", + "platform_release", + "platform_system", + "platform_version", + "sys_platform", +] + def _pip_parse_ext_attrs(**kwargs): """Get the attributes for the pip extension. @@ -907,6 +1102,23 @@ the BUILD files for wheels. """, implementation = _pip_impl, tag_classes = { + "default": tag_class( + attrs = _default_attrs, + doc = """\ +This tag class allows for more customization of how the configuration for the hub repositories is built. + + +:::{include} /_includes/experimtal_api.md +::: + +:::{seealso} +The [environment markers][environment_markers] specification for the explanation of the +terms used in this extension. + +[environment_markers]: https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers +::: +""", + ), "override": _override_tag, "parse": tag_class( attrs = _pip_parse_ext_attrs(), diff --git a/python/private/pypi/pep508_evaluate.bzl b/python/private/pypi/pep508_evaluate.bzl index d4492a75bb..fe2cac965a 100644 --- a/python/private/pypi/pep508_evaluate.bzl +++ b/python/private/pypi/pep508_evaluate.bzl @@ -117,7 +117,7 @@ def evaluate(marker, *, env, strict = True, **kwargs): Args: marker: {type}`str` The string marker to evaluate. - env: {type}`dict` The environment to evaluate the marker against. + env: {type}`dict[str, str]` The environment to evaluate the marker against. strict: {type}`bool` A setting to not fail on missing values in the env. **kwargs: Extra kwargs to be passed to the expression evaluator. diff --git a/python/private/pypi/pip_repository.bzl b/python/private/pypi/pip_repository.bzl index 724fb6ddba..e63bd6c3d1 100644 --- a/python/private/pypi/pip_repository.bzl +++ b/python/private/pypi/pip_repository.bzl @@ -80,6 +80,16 @@ def _pip_repository_impl(rctx): requirements_osx = rctx.attr.requirements_darwin, requirements_windows = rctx.attr.requirements_windows, extra_pip_args = rctx.attr.extra_pip_args, + platforms = [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "linux_x86_64", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ], ), extra_pip_args = rctx.attr.extra_pip_args, evaluate_markers = lambda rctx, requirements: evaluate_markers_py( diff --git a/python/private/pypi/requirements_files_by_platform.bzl b/python/private/pypi/requirements_files_by_platform.bzl index 9165c05bed..d8d3651461 100644 --- a/python/private/pypi/requirements_files_by_platform.bzl +++ b/python/private/pypi/requirements_files_by_platform.bzl @@ -16,20 +16,7 @@ load(":whl_target_platforms.bzl", "whl_target_platforms") -# TODO @aignas 2024-05-13: consider using the same platform tags as are used in -# the //python:versions.bzl -DEFAULT_PLATFORMS = [ - "linux_aarch64", - "linux_arm", - "linux_ppc", - "linux_s390x", - "linux_x86_64", - "osx_aarch64", - "osx_x86_64", - "windows_x86_64", -] - -def _default_platforms(*, filter): +def _default_platforms(*, filter, platforms): if not filter: fail("Must specific a filter string, got: {}".format(filter)) @@ -48,11 +35,13 @@ def _default_platforms(*, filter): fail("The filter can only contain '*' at the end of it") if not prefix: - return DEFAULT_PLATFORMS + return platforms - return [p for p in DEFAULT_PLATFORMS if p.startswith(prefix)] + match = [p for p in platforms if p.startswith(prefix)] else: - return [p for p in DEFAULT_PLATFORMS if filter in p] + match = [p for p in platforms if filter in p] + + return match def _platforms_from_args(extra_pip_args): platform_values = [] @@ -105,6 +94,7 @@ def requirements_files_by_platform( requirements_linux = None, requirements_lock = None, requirements_windows = None, + platforms, extra_pip_args = None, python_version = None, logger = None, @@ -123,6 +113,8 @@ def requirements_files_by_platform( be joined with args fined in files. python_version: str or None. This is needed when the get_index_urls is specified. It should be of the form "3.x.x", + platforms: {type}`list[str]` the list of human-friendly platform labels that should + be used for the evaluation. logger: repo_utils.logger or None, a simple struct to log diagnostic messages. fail_fn (Callable[[str], None]): A failure function used in testing failure cases. @@ -144,11 +136,13 @@ def requirements_files_by_platform( ) return None - platforms = _platforms_from_args(extra_pip_args) + platforms_from_args = _platforms_from_args(extra_pip_args) if logger: - logger.debug(lambda: "Platforms from pip args: {}".format(platforms)) + logger.debug(lambda: "Platforms from pip args: {}".format(platforms_from_args)) + + default_platforms = [_platform(p, python_version) for p in platforms] - if platforms: + if platforms_from_args: lock_files = [ f for f in [ @@ -168,7 +162,7 @@ def requirements_files_by_platform( return None files_by_platform = [ - (lock_files[0], platforms), + (lock_files[0], platforms_from_args), ] if logger: logger.debug(lambda: "Files by platform with the platform set in the args: {}".format(files_by_platform)) @@ -177,7 +171,7 @@ def requirements_files_by_platform( file: [ platform for filter_or_platform in specifier.split(",") - for platform in (_default_platforms(filter = filter_or_platform) if filter_or_platform.endswith("*") else [filter_or_platform]) + for platform in (_default_platforms(filter = filter_or_platform, platforms = platforms) if filter_or_platform.endswith("*") else [filter_or_platform]) ] for file, specifier in requirements_by_platform.items() }.items() @@ -188,9 +182,9 @@ def requirements_files_by_platform( for f in [ # If the users need a greater span of the platforms, they should consider # using the 'requirements_by_platform' attribute. - (requirements_linux, _default_platforms(filter = "linux_*")), - (requirements_osx, _default_platforms(filter = "osx_*")), - (requirements_windows, _default_platforms(filter = "windows_*")), + (requirements_linux, _default_platforms(filter = "linux_*", platforms = platforms)), + (requirements_osx, _default_platforms(filter = "osx_*", platforms = platforms)), + (requirements_windows, _default_platforms(filter = "windows_*", platforms = platforms)), (requirements_lock, None), ]: if f[0]: @@ -215,8 +209,7 @@ def requirements_files_by_platform( return None configured_platforms[p] = file - else: - default_platforms = [_platform(p, python_version) for p in DEFAULT_PLATFORMS] + elif plats == None: plats = [ p for p in default_platforms @@ -231,6 +224,13 @@ def requirements_files_by_platform( for p in plats: configured_platforms[p] = file + elif logger: + logger.warn(lambda: "File {} will be ignored because there are no configured platforms: {}".format( + file, + default_platforms, + )) + continue + if logger: logger.debug(lambda: "Configured platforms for file {} are {}".format(file, plats)) diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index 8e325724f4..3d205a23c4 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -49,23 +49,22 @@ simple==0.0.1 \ ], ) -def _mod(*, name, parse = [], override = [], whl_mods = [], is_root = True): +def _mod(*, name, default = [], parse = [], override = [], whl_mods = [], is_root = True): return struct( name = name, tags = struct( parse = parse, override = override, whl_mods = whl_mods, + default = default, ), is_root = is_root, ) -def _parse_modules(env, **kwargs): +def _parse_modules(env, enable_pipstar = 0, **kwargs): return env.expect.that_struct( parse_modules( - # TODO @aignas 2025-05-11: start integration testing the branch which - # includes this. - enable_pipstar = 0, + enable_pipstar = enable_pipstar, **kwargs ), attrs = dict( @@ -77,6 +76,26 @@ def _parse_modules(env, **kwargs): ), ) +def _default( + arch_name = None, + constraint_values = None, + os_name = None, + platform = None, + target_settings = None, + env = None, + whl_limit = None, + whl_platforms = None): + return struct( + arch_name = arch_name, + constraint_values = constraint_values, + os_name = os_name, + platform = platform, + target_settings = target_settings, + env = env or {}, + whl_platforms = whl_platforms, + whl_limit = whl_limit, + ) + def _parse( *, hub_name, @@ -1023,6 +1042,88 @@ optimum[onnxruntime-gpu]==1.17.1 ; sys_platform == 'linux' _tests.append(_test_optimum_sys_platform_extra) +def _test_pipstar_platforms(env): + pypi = _parse_modules( + env, + module_ctx = _mock_mctx( + _mod( + name = "rules_python", + default = [ + _default( + platform = "{}_{}".format(os, cpu), + ) + for os, cpu in [ + ("linux", "x86_64"), + ("osx", "aarch64"), + ] + ], + parse = [ + _parse( + hub_name = "pypi", + python_version = "3.15", + requirements_lock = "universal.txt", + ), + ], + ), + read = lambda x: { + "universal.txt": """\ +optimum[onnxruntime]==1.17.1 ; sys_platform == 'darwin' +optimum[onnxruntime-gpu]==1.17.1 ; sys_platform == 'linux' +""", + }[x], + ), + enable_pipstar = True, + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + ) + + pypi.exposed_packages().contains_exactly({"pypi": ["optimum"]}) + pypi.hub_group_map().contains_exactly({"pypi": {}}) + pypi.hub_whl_map().contains_exactly({ + "pypi": { + "optimum": { + "pypi_315_optimum_linux_x86_64": [ + whl_config_setting( + version = "3.15", + target_platforms = [ + "cp315_linux_x86_64", + ], + config_setting = None, + filename = None, + ), + ], + "pypi_315_optimum_osx_aarch64": [ + whl_config_setting( + version = "3.15", + target_platforms = [ + "cp315_osx_aarch64", + ], + config_setting = None, + filename = None, + ), + ], + }, + }, + }) + + pypi.whl_libraries().contains_exactly({ + "pypi_315_optimum_linux_x86_64": { + "dep_template": "@pypi//{name}:{target}", + "python_interpreter_target": "unit_test_interpreter_target", + "requirement": "optimum[onnxruntime-gpu]==1.17.1", + }, + "pypi_315_optimum_osx_aarch64": { + "dep_template": "@pypi//{name}:{target}", + "python_interpreter_target": "unit_test_interpreter_target", + "requirement": "optimum[onnxruntime]==1.17.1", + }, + }) + pypi.whl_mods().contains_exactly({}) + +_tests.append(_test_pipstar_platforms) + def extension_test_suite(name): """Create the test suite. diff --git a/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl b/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl index b729b0eaf0..6688d72ffe 100644 --- a/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl +++ b/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl @@ -15,10 +15,27 @@ "" load("@rules_testing//lib:test_suite.bzl", "test_suite") -load("//python/private/pypi:requirements_files_by_platform.bzl", "requirements_files_by_platform") # buildifier: disable=bzl-visibility +load("//python/private/pypi:requirements_files_by_platform.bzl", _sut = "requirements_files_by_platform") # buildifier: disable=bzl-visibility _tests = [] +requirements_files_by_platform = lambda **kwargs: _sut( + platforms = kwargs.pop( + "platforms", + [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "linux_x86_64", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ], + ), + **kwargs +) + def _test_fail_no_requirements(env): errors = [] requirements_files_by_platform( @@ -86,6 +103,28 @@ def _test_simple(env): _tests.append(_test_simple) +def _test_simple_limited(env): + for got in [ + requirements_files_by_platform( + requirements_lock = "requirements_lock", + platforms = ["linux_x86_64", "osx_x86_64"], + ), + requirements_files_by_platform( + requirements_by_platform = { + "requirements_lock": "*", + }, + platforms = ["linux_x86_64", "osx_x86_64"], + ), + ]: + env.expect.that_dict(got).contains_exactly({ + "requirements_lock": [ + "linux_x86_64", + "osx_x86_64", + ], + }) + +_tests.append(_test_simple_limited) + def _test_simple_with_python_version(env): for got in [ requirements_files_by_platform( From b8d6fa3f135fa7da2eed0c857bc25a43517f21fa Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 20 Jun 2025 09:46:07 +0900 Subject: [PATCH 150/156] feat(pypi): pip.defaults API for customizing repo selection 2/n (#2988) WIP: stacked on #2987 This is adding `constraint_values` attribute to `pip.configure` and is threading it all the way down to the generation of `BUILD.bazel` file of for config settings used in the hub repository. Out of scope: - Passing `flag_values` or target settings. I am torn about it - doing it in this PR would flesh out the design more, but at the same time it might become harder to review. - `whl_target_platforms` and `select_whls` is still unchanged, not sure if it is related to this attribute addition. Work towards #2747 Work towards #2548 Work towards #260 --------- Co-authored-by: Richard Levasseur --- CHANGELOG.md | 2 +- python/private/pypi/config_settings.bzl | 31 +++++++------- python/private/pypi/extension.bzl | 34 +++++++++++++-- python/private/pypi/hub_repository.bzl | 5 +++ python/private/pypi/render_pkg_aliases.bzl | 12 ++++-- .../config_settings/config_settings_tests.bzl | 39 +++++++++++++---- tests/pypi/extension/extension_tests.bzl | 4 ++ tests/pypi/pkg_aliases/pkg_aliases_test.bzl | 42 +++++++++++++++---- .../render_pkg_aliases_test.bzl | 13 +++++- 9 files changed, 140 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9897dc9ec8..da3dcc8efc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,7 +64,7 @@ END_UNRELEASED_TEMPLATE ### Added * (pypi) To configure the environment for `requirements.txt` evaluation, use the newly added developer preview of the `pip.default` tag class. Only `rules_python` and root modules can use - this feature. + this feature. You can also configure `constraint_values` using `pip.default`. {#v0-0-0-removed} ### Removed diff --git a/python/private/pypi/config_settings.bzl b/python/private/pypi/config_settings.bzl index 3e828e59f5..7edc578d7a 100644 --- a/python/private/pypi/config_settings.bzl +++ b/python/private/pypi/config_settings.bzl @@ -111,8 +111,8 @@ def config_settings( glibc_versions = [], muslc_versions = [], osx_versions = [], - target_platforms = [], name = None, + platform_constraint_values = {}, **kwargs): """Generate all of the pip config settings. @@ -126,8 +126,10 @@ def config_settings( configure config settings for. osx_versions (list[str]): The list of OSX OS versions to configure config settings for. - target_platforms (list[str]): The list of "{os}_{cpu}" for deriving - constraint values for each condition. + platform_constraint_values: {type}`dict[str, list[str]]` the constraint + values to use instead of the default ones. Key are platform names + (a human-friendly platform string). Values are lists of + `constraint_value` label strings. **kwargs: Other args passed to the underlying implementations, such as {obj}`native`. """ @@ -135,22 +137,17 @@ def config_settings( glibc_versions = [""] + glibc_versions muslc_versions = [""] + muslc_versions osx_versions = [""] + osx_versions - target_platforms = [("", ""), ("osx", "universal2")] + [ - t.split("_", 1) - for t in target_platforms - ] + target_platforms = { + "": [], + # TODO @aignas 2025-06-15: allowing universal2 and platform specific wheels in one + # closure is making things maybe a little bit too complicated. + "osx_universal2": ["@platforms//os:osx"], + } | platform_constraint_values for python_version in python_versions: - for os, cpu in target_platforms: - constraint_values = [] - suffix = "" - if os: - constraint_values.append("@platforms//os:" + os) - suffix += "_" + os - if cpu: - suffix += "_" + cpu - if cpu != "universal2": - constraint_values.append("@platforms//cpu:" + cpu) + for platform_name, constraint_values in target_platforms.items(): + suffix = "_{}".format(platform_name) if platform_name else "" + os, _, cpu = platform_name.partition("_") _dist_config_settings( suffix = suffix, diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 97b6825e51..78511b4c27 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -372,7 +372,7 @@ def _whl_repo(*, src, whl_library_args, is_multiple_versions, download_only, net ), ) -def _configure(config, *, platform, os_name, arch_name, override = False, env = {}): +def _configure(config, *, platform, os_name, arch_name, constraint_values, env = {}, override = False): """Set the value in the config if the value is provided""" config.setdefault("platforms", {}) if platform: @@ -387,6 +387,7 @@ def _configure(config, *, platform, os_name, arch_name, override = False, env = name = platform.replace("-", "_").lower(), os_name = os_name, arch_name = arch_name, + constraint_values = constraint_values, env = env, ) else: @@ -413,6 +414,10 @@ def _create_config(defaults): arch_name = cpu, os_name = "linux", platform = "linux_{}".format(cpu), + constraint_values = [ + "@platforms//os:linux", + "@platforms//cpu:{}".format(cpu), + ], env = {"platform_version": "0"}, ) for cpu in [ @@ -424,17 +429,25 @@ def _create_config(defaults): arch_name = cpu, # We choose the oldest non-EOL version at the time when we release `rules_python`. # See https://endoflife.date/macos - env = {"platform_version": "14.0"}, os_name = "osx", platform = "osx_{}".format(cpu), + constraint_values = [ + "@platforms//os:osx", + "@platforms//cpu:{}".format(cpu), + ], + env = {"platform_version": "14.0"}, ) _configure( defaults, arch_name = "x86_64", - env = {"platform_version": "0"}, os_name = "windows", platform = "windows_x86_64", + constraint_values = [ + "@platforms//os:windows", + "@platforms//cpu:x86_64", + ], + env = {"platform_version": "0"}, ) return struct(**defaults) @@ -500,6 +513,7 @@ You cannot use both the additive_build_content and additive_build_content_file a _configure( defaults, arch_name = tag.arch_name, + constraint_values = tag.constraint_values, env = tag.env, os_name = tag.os_name, platform = tag.platform, @@ -679,6 +693,13 @@ You cannot use both the additive_build_content and additive_build_content_file a } for hub_name, extra_whl_aliases in extra_aliases.items() }, + platform_constraint_values = { + hub_name: { + platform_name: sorted([str(Label(cv)) for cv in p.constraint_values]) + for platform_name, p in config.platforms.items() + } + for hub_name in hub_whl_map + }, whl_libraries = { k: dict(sorted(args.items())) for k, args in sorted(whl_libraries.items()) @@ -769,6 +790,7 @@ def _pip_impl(module_ctx): for key, values in whl_map.items() }, packages = mods.exposed_packages.get(hub_name, []), + platform_constraint_values = mods.platform_constraint_values.get(hub_name, {}), groups = mods.hub_group_map.get(hub_name), ) @@ -788,6 +810,12 @@ The CPU architecture name to be used. :::{note} Either this or {attr}`env` `platform_machine` key should be specified. ::: +""", + ), + "constraint_values": attr.label_list( + mandatory = True, + doc = """\ +The constraint_values to use in select statements. """, ), "os_name": attr.string( diff --git a/python/private/pypi/hub_repository.bzl b/python/private/pypi/hub_repository.bzl index 0dbc6c29c2..4398d7b597 100644 --- a/python/private/pypi/hub_repository.bzl +++ b/python/private/pypi/hub_repository.bzl @@ -34,6 +34,7 @@ def _impl(rctx): }, extra_hub_aliases = rctx.attr.extra_hub_aliases, requirement_cycles = rctx.attr.groups, + platform_constraint_values = rctx.attr.platform_constraint_values, ) for path, contents in aliases.items(): rctx.file(path, contents) @@ -83,6 +84,10 @@ hub_repository = repository_rule( The list of packages that will be exposed via all_*requirements macros. Defaults to whl_map keys. """, ), + "platform_constraint_values": attr.string_list_dict( + doc = "The constraint values for each platform name. The values are string canonical string Label representations", + mandatory = False, + ), "repo_name": attr.string( mandatory = True, doc = "The apparent name of the repo. This is needed because in bzlmod, the name attribute becomes the canonical name.", diff --git a/python/private/pypi/render_pkg_aliases.bzl b/python/private/pypi/render_pkg_aliases.bzl index 28f32edc78..267d7ce85d 100644 --- a/python/private/pypi/render_pkg_aliases.bzl +++ b/python/private/pypi/render_pkg_aliases.bzl @@ -155,12 +155,14 @@ def _major_minor_versions(python_versions): # Use a dict as a simple set return sorted({_major_minor(v): None for v in python_versions}) -def render_multiplatform_pkg_aliases(*, aliases, **kwargs): +def render_multiplatform_pkg_aliases(*, aliases, platform_constraint_values = {}, **kwargs): """Render the multi-platform pkg aliases. Args: aliases: dict[str, list(whl_config_setting)] A list of aliases that will be transformed from ones having `filename` to ones having `config_setting`. + platform_constraint_values: {type}`dict[str, list[str]]` contains all of the + target platforms and their appropriate `constraint_values`. **kwargs: extra arguments passed to render_pkg_aliases. Returns: @@ -187,18 +189,22 @@ def render_multiplatform_pkg_aliases(*, aliases, **kwargs): muslc_versions = flag_versions.get("muslc_versions", []), osx_versions = flag_versions.get("osx_versions", []), python_versions = _major_minor_versions(flag_versions.get("python_versions", [])), - target_platforms = flag_versions.get("target_platforms", []), + platform_constraint_values = platform_constraint_values, visibility = ["//:__subpackages__"], ) return contents -def _render_config_settings(**kwargs): +def _render_config_settings(platform_constraint_values, **kwargs): return """\ load("@rules_python//python/private/pypi:config_settings.bzl", "config_settings") {}""".format(render.call( "config_settings", name = repr("config_settings"), + platform_constraint_values = render.dict( + platform_constraint_values, + value_repr = render.list, + ), **_repr_dict(value_repr = render.list, **kwargs) )) diff --git a/tests/pypi/config_settings/config_settings_tests.bzl b/tests/pypi/config_settings/config_settings_tests.bzl index f111d0c55c..9551d42d10 100644 --- a/tests/pypi/config_settings/config_settings_tests.bzl +++ b/tests/pypi/config_settings/config_settings_tests.bzl @@ -657,13 +657,34 @@ def config_settings_test_suite(name): # buildifier: disable=function-docstring glibc_versions = [(2, 14), (2, 17)], muslc_versions = [(1, 1)], osx_versions = [(10, 9), (11, 0)], - target_platforms = [ - "windows_x86_64", - "windows_aarch64", - "linux_x86_64", - "linux_ppc", - "linux_aarch64", - "osx_x86_64", - "osx_aarch64", - ], + platform_constraint_values = { + "linux_aarch64": [ + "@platforms//cpu:aarch64", + "@platforms//os:linux", + ], + "linux_ppc": [ + "@platforms//cpu:ppc", + "@platforms//os:linux", + ], + "linux_x86_64": [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], + "osx_aarch64": [ + "@platforms//cpu:aarch64", + "@platforms//os:osx", + ], + "osx_x86_64": [ + "@platforms//cpu:x86_64", + "@platforms//os:osx", + ], + "windows_aarch64": [ + "@platforms//cpu:aarch64", + "@platforms//os:windows", + ], + "windows_x86_64": [ + "@platforms//cpu:x86_64", + "@platforms//os:windows", + ], + }, ) diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index 3d205a23c4..231e8cab41 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -1051,6 +1051,10 @@ def _test_pipstar_platforms(env): default = [ _default( platform = "{}_{}".format(os, cpu), + constraint_values = [ + "@platforms//os:{}".format(os), + "@platforms//cpu:{}".format(cpu), + ], ) for os, cpu in [ ("linux", "x86_64"), diff --git a/tests/pypi/pkg_aliases/pkg_aliases_test.bzl b/tests/pypi/pkg_aliases/pkg_aliases_test.bzl index 71ca811fee..0fbcd4e7a6 100644 --- a/tests/pypi/pkg_aliases/pkg_aliases_test.bzl +++ b/tests/pypi/pkg_aliases/pkg_aliases_test.bzl @@ -419,10 +419,16 @@ def _test_config_settings_exist_legacy(env): alias = _mock_alias(available_config_settings), config_setting = _mock_config_setting(available_config_settings), ), - target_platforms = [ - "linux_aarch64", - "linux_x86_64", - ], + platform_constraint_values = { + "linux_aarch64": [ + "@platforms//cpu:aarch64", + "@platforms//os:linux", + ], + "linux_x86_64": [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], + }, ) got_aliases = multiplatform_whl_aliases( @@ -448,19 +454,39 @@ def _test_config_settings_exist(env): "any": {}, "macosx_11_0_arm64": { "osx_versions": [(11, 0)], - "target_platforms": ["osx_aarch64"], + "platform_constraint_values": { + "osx_aarch64": [ + "@platforms//cpu:aarch64", + "@platforms//os:osx", + ], + }, }, "manylinux_2_17_x86_64": { "glibc_versions": [(2, 17), (2, 18)], - "target_platforms": ["linux_x86_64"], + "platform_constraint_values": { + "linux_x86_64": [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], + }, }, "manylinux_2_18_x86_64": { "glibc_versions": [(2, 17), (2, 18)], - "target_platforms": ["linux_x86_64"], + "platform_constraint_values": { + "linux_x86_64": [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], + }, }, "musllinux_1_1_aarch64": { "muslc_versions": [(1, 2), (1, 1), (1, 0)], - "target_platforms": ["linux_aarch64"], + "platform_constraint_values": { + "linux_aarch64": [ + "@platforms//cpu:aarch64", + "@platforms//os:linux", + ], + }, }, }.items(): aliases = { diff --git a/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl b/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl index 416d50bd80..c262ed6823 100644 --- a/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl +++ b/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl @@ -93,6 +93,12 @@ def _test_bzlmod_aliases(env): }, }, extra_hub_aliases = {"bar_baz": ["foo"]}, + platform_constraint_values = { + "linux_x86_64": [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ], + }, ) want_key = "bar_baz/BUILD.bazel" @@ -130,8 +136,13 @@ load("@rules_python//python/private/pypi:config_settings.bzl", "config_settings" config_settings( name = "config_settings", + platform_constraint_values = { + "linux_x86_64": [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ], + }, python_versions = ["3.2"], - target_platforms = ["linux_x86_64"], visibility = ["//:__subpackages__"], )""", ) From c4543cd193752d0248226dcd07cc027e63ed7b8b Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Thu, 19 Jun 2025 23:28:52 -0700 Subject: [PATCH 151/156] fix(toolchains): use posix-compatible exec -a alternative (#3010) The `exec -a` command doesn't work in dash, the default shell for Ubuntu/debian. To work around, use `sh -c`, which is posix and dash compatible. This allows changing the argv0 while invoking a different command. Also adds a test to verify the the runtime_env toolchain works with bootstrap script. Fixes https://github.com/bazel-contrib/rules_python/issues/3009 --- .../runtime_env_toolchain_interpreter.sh | 13 ++++++----- tests/runtime_env_toolchain/BUILD.bazel | 23 +++++++++++++++++++ .../toolchain_runs_test.py | 9 ++++++++ tests/support/sh_py_run_test.bzl | 6 +++++ 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/python/private/runtime_env_toolchain_interpreter.sh b/python/private/runtime_env_toolchain_interpreter.sh index 7b3ec598b2..dd4d648d12 100755 --- a/python/private/runtime_env_toolchain_interpreter.sh +++ b/python/private/runtime_env_toolchain_interpreter.sh @@ -71,14 +71,15 @@ if [ -e "$self_dir/pyvenv.cfg" ] || [ -e "$self_dir/../pyvenv.cfg" ]; then if [ ! -e "$PYTHON_BIN" ]; then die "ERROR: Python interpreter does not exist: $PYTHON_BIN" fi - # PYTHONEXECUTABLE is also used because `exec -a` doesn't fully trick the - # pyenv wrappers. + # PYTHONEXECUTABLE is also used because switching argv0 doesn't fully trick + # the pyenv wrappers. # NOTE: The PYTHONEXECUTABLE envvar only works for non-Mac starting in Python 3.11 export PYTHONEXECUTABLE="$venv_bin" - # Python looks at argv[0] to determine sys.executable, so use exec -a - # to make it think it's the venv's binary, not the actual one invoked. - # NOTE: exec -a isn't strictly posix-compatible, but very widespread - exec -a "$venv_bin" "$PYTHON_BIN" "$@" + # Python looks at argv[0] to determine sys.executable, so set that to the venv + # binary, not the actual one invoked. + # NOTE: exec -a would be simpler, but isn't posix-compatible, and dash shell + # (Ubuntu/debian default) doesn't support it; see #3009. + exec sh -c "$PYTHON_BIN \$@" "$venv_bin" "$@" else exec "$PYTHON_BIN" "$@" fi diff --git a/tests/runtime_env_toolchain/BUILD.bazel b/tests/runtime_env_toolchain/BUILD.bazel index 2f82d204ff..f1bda251f9 100644 --- a/tests/runtime_env_toolchain/BUILD.bazel +++ b/tests/runtime_env_toolchain/BUILD.bazel @@ -40,3 +40,26 @@ py_reconfig_test( tags = ["no-remote-exec"], deps = ["//python/runfiles"], ) + +py_reconfig_test( + name = "bootstrap_script_test", + srcs = ["toolchain_runs_test.py"], + bootstrap_impl = "script", + data = [ + "//tests/support:current_build_settings", + ], + extra_toolchains = [ + "//python/runtime_env_toolchains:all", + # Necessary for RBE CI + CC_TOOLCHAIN, + ], + main = "toolchain_runs_test.py", + # With bootstrap=script, the build version must match the runtime version + # because the venv has the version in the lib/site-packages dir name. + python_version = PYTHON_VERSION, + # Our RBE has Python 3.6, which is too old for the language features + # we use now. Using the runtime-env toolchain on RBE is pretty + # questionable anyways. + tags = ["no-remote-exec"], + deps = ["//python/runfiles"], +) diff --git a/tests/runtime_env_toolchain/toolchain_runs_test.py b/tests/runtime_env_toolchain/toolchain_runs_test.py index 7be2472e8b..c66b0bbd8a 100644 --- a/tests/runtime_env_toolchain/toolchain_runs_test.py +++ b/tests/runtime_env_toolchain/toolchain_runs_test.py @@ -1,6 +1,7 @@ import json import pathlib import platform +import sys import unittest from python.runfiles import runfiles @@ -23,6 +24,14 @@ def test_ran(self): settings["interpreter"]["short_path"], ) + if settings["bootstrap_impl"] == "script": + # Verify we're running in a venv + self.assertNotEqual(sys.prefix, sys.base_prefix) + # .venv/ occurs for a build-time venv. + # For a runtime created venv, it goes into a temp dir, so + # look for the /bin/ dir as an indicator. + self.assertRegex(sys.executable, r"[.]venv/|/bin/") + if __name__ == "__main__": unittest.main() diff --git a/tests/support/sh_py_run_test.bzl b/tests/support/sh_py_run_test.bzl index 69141fe8a4..49445ed304 100644 --- a/tests/support/sh_py_run_test.bzl +++ b/tests/support/sh_py_run_test.bzl @@ -135,6 +135,7 @@ def _current_build_settings_impl(ctx): ctx.actions.write( output = info, content = json.encode({ + "bootstrap_impl": ctx.attr._bootstrap_impl_flag[config_common.FeatureFlagInfo].value, "interpreter": { "short_path": runtime.interpreter.short_path if runtime.interpreter else None, }, @@ -153,6 +154,11 @@ Writes information about the current build config to JSON for testing. This is so tests can verify information about the build config used for them. """, implementation = _current_build_settings_impl, + attrs = { + "_bootstrap_impl_flag": attr.label( + default = "//python/config_settings:bootstrap_impl", + ), + }, toolchains = [ TARGET_TOOLCHAIN_TYPE, ], From b924c43e0fadc78fe8de7d91c318c5299c8ab68b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 23:56:13 -0700 Subject: [PATCH 152/156] build(deps): bump urllib3 from 2.4.0 to 2.5.0 in /tools/publish (#3008) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.4.0 to 2.5.0.
Release notes

Sourced from urllib3's releases.

2.5.0

🚀 urllib3 is fundraising for HTTP/2 support

urllib3 is raising ~$40,000 USD to release HTTP/2 support and ensure long-term sustainable maintenance of the project after a sharp decline in financial support. If your company or organization uses Python and would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and thousands of other projects please consider contributing financially to ensure HTTP/2 support is developed sustainably and maintained for the long-haul.

Thank you for your support.

Security issues

urllib3 2.5.0 fixes two moderate security issues:

  • Pool managers now properly control redirects when retries is passed — CVE-2025-50181 reported by @​sandumjacob (5.3 Medium, GHSA-pq67-6m6q-mj2v)
  • Redirects are now controlled by urllib3 in the Node.js runtime — CVE-2025-50182 (5.3 Medium, GHSA-48p4-8xcf-vxj5)

Features

  • Added support for the compression.zstd module that is new in Python 3.14. See PEP 784 for more information. (#3610)
  • Added support for version 0.5 of hatch-vcs (#3612)

Bugfixes

  • Raised exception for HTTPResponse.shutdown on a connection already released to the pool. (#3581)
  • Fixed incorrect CONNECT statement when using an IPv6 proxy with connection_from_host. Previously would not be wrapped in []. (#3615)
Changelog

Sourced from urllib3's changelog.

2.5.0 (2025-06-18)

Features

  • Added support for the compression.zstd module that is new in Python 3.14. See PEP 784 <https://peps.python.org/pep-0784/>_ for more information. ([#3610](https://github.com/urllib3/urllib3/issues/3610) <https://github.com/urllib3/urllib3/issues/3610>__)
  • Added support for version 0.5 of hatch-vcs ([#3612](https://github.com/urllib3/urllib3/issues/3612) <https://github.com/urllib3/urllib3/issues/3612>__)

Bugfixes

  • Fixed a security issue where restricting the maximum number of followed redirects at the urllib3.PoolManager level via the retries parameter did not work.
  • Made the Node.js runtime respect redirect parameters such as retries and redirects.
  • Raised exception for HTTPResponse.shutdown on a connection already released to the pool. ([#3581](https://github.com/urllib3/urllib3/issues/3581) <https://github.com/urllib3/urllib3/issues/3581>__)
  • Fixed incorrect CONNECT statement when using an IPv6 proxy with connection_from_host. Previously would not be wrapped in []. ([#3615](https://github.com/urllib3/urllib3/issues/3615) <https://github.com/urllib3/urllib3/issues/3615>__)
Commits
  • aaab4ec Release 2.5.0
  • 7eb4a2a Merge commit from fork
  • f05b132 Merge commit from fork
  • d03fe32 Fix HTTP tunneling with IPv6 in older Python versions
  • 11661e9 Bump github/codeql-action from 3.28.0 to 3.29.0 (#3624)
  • 6a0ecc6 Update v2 migration guide to 2.4.0 (#3621)
  • 8e32e60 Raise exception for shutdown on a connection already released to the pool (#3...
  • 9996e0f Fix emscripten CI for Chrome 137+ (#3599)
  • 4fd1a99 Bump RECENT_DATE (#3617)
  • c4b5917 Add support for the new compression.zstd module in Python 3.14 (#3611)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=urllib3&package-manager=pip&previous-version=2.4.0&new-version=2.5.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/bazel-contrib/rules_python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tools/publish/requirements_darwin.txt | 6 +++--- tools/publish/requirements_linux.txt | 6 +++--- tools/publish/requirements_universal.txt | 6 +++--- tools/publish/requirements_windows.txt | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tools/publish/requirements_darwin.txt b/tools/publish/requirements_darwin.txt index af5bad246d..58973acb6f 100644 --- a/tools/publish/requirements_darwin.txt +++ b/tools/publish/requirements_darwin.txt @@ -202,9 +202,9 @@ twine==5.1.1 \ --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db # via -r tools/publish/requirements.in -urllib3==2.4.0 \ - --hash=sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466 \ - --hash=sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813 +urllib3==2.5.0 \ + --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ + --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc # via # requests # twine diff --git a/tools/publish/requirements_linux.txt b/tools/publish/requirements_linux.txt index b2e9ccf5ab..73edfce02f 100644 --- a/tools/publish/requirements_linux.txt +++ b/tools/publish/requirements_linux.txt @@ -318,9 +318,9 @@ twine==5.1.1 \ --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db # via -r tools/publish/requirements.in -urllib3==2.4.0 \ - --hash=sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466 \ - --hash=sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813 +urllib3==2.5.0 \ + --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ + --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc # via # requests # twine diff --git a/tools/publish/requirements_universal.txt b/tools/publish/requirements_universal.txt index 8a7426e517..c080f1d7de 100644 --- a/tools/publish/requirements_universal.txt +++ b/tools/publish/requirements_universal.txt @@ -322,9 +322,9 @@ twine==5.1.1 \ --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db # via -r tools/publish/requirements.in -urllib3==2.4.0 \ - --hash=sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466 \ - --hash=sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813 +urllib3==2.5.0 \ + --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ + --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc # via # requests # twine diff --git a/tools/publish/requirements_windows.txt b/tools/publish/requirements_windows.txt index 11017aa4f9..a4d5e3e25d 100644 --- a/tools/publish/requirements_windows.txt +++ b/tools/publish/requirements_windows.txt @@ -206,9 +206,9 @@ twine==5.1.1 \ --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db # via -r tools/publish/requirements.in -urllib3==2.4.0 \ - --hash=sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466 \ - --hash=sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813 +urllib3==2.5.0 \ + --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ + --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc # via # requests # twine From 6fd4c0bdc9eca48449c1f2b77a44f59a62a88dde Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 20 Jun 2025 16:10:13 +0900 Subject: [PATCH 153/156] feat: support arbitrary target_settings in our platforms 3/n (#2990) With this PR we can support arbitrary target settings instead of just plain `constraint_values`. We still have custom logic to ensure that all of the tests pass. However, the plan is to remove those tests once we have simplified the wheel selection mechanisms and the `pkg_aliases` macro. I.e. if we have at most 1 wheel per platform that the `pypi` bzlmod extension passes to the `pkg_aliases` macro, then we can just have a simple `selects.with_or` where we list out all of the target platform values. This PR may result in us creating more targets but that is the price that we have to pay if we want to do this incrementally. Work towards #2747 Work towards #2548 Work towards #260 Co-authored-by: Richard Levasseur --- CHANGELOG.md | 2 +- python/private/pypi/BUILD.bazel | 1 + python/private/pypi/config_settings.bzl | 41 ++++++++++++++++--- python/private/pypi/extension.bzl | 26 +++++++----- python/private/pypi/hub_repository.bzl | 4 +- python/private/pypi/render_pkg_aliases.bzl | 14 +++---- .../config_settings/config_settings_tests.bzl | 2 +- tests/pypi/extension/extension_tests.bzl | 8 ++-- tests/pypi/pkg_aliases/pkg_aliases_test.bzl | 23 +++++++---- .../render_pkg_aliases_test.bzl | 4 +- 10 files changed, 83 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da3dcc8efc..f2fa98f73f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,7 +64,7 @@ END_UNRELEASED_TEMPLATE ### Added * (pypi) To configure the environment for `requirements.txt` evaluation, use the newly added developer preview of the `pip.default` tag class. Only `rules_python` and root modules can use - this feature. You can also configure `constraint_values` using `pip.default`. + this feature. You can also configure custom `config_settings` using `pip.default`. {#v0-0-0-removed} ### Removed diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index b569b2217c..2666197786 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -64,6 +64,7 @@ bzl_library( deps = [ ":flags_bzl", "//python/private:flags_bzl", + "@bazel_skylib//lib:selects", ], ) diff --git a/python/private/pypi/config_settings.bzl b/python/private/pypi/config_settings.bzl index 7edc578d7a..f4826007f8 100644 --- a/python/private/pypi/config_settings.bzl +++ b/python/private/pypi/config_settings.bzl @@ -70,6 +70,7 @@ suffix. ::: """ +load("@bazel_skylib//lib:selects.bzl", "selects") load("//python/private:flags.bzl", "LibcFlag") load(":flags.bzl", "INTERNAL_FLAGS", "UniversalWhlFlag") @@ -112,7 +113,7 @@ def config_settings( muslc_versions = [], osx_versions = [], name = None, - platform_constraint_values = {}, + platform_config_settings = {}, **kwargs): """Generate all of the pip config settings. @@ -126,7 +127,7 @@ def config_settings( configure config settings for. osx_versions (list[str]): The list of OSX OS versions to configure config settings for. - platform_constraint_values: {type}`dict[str, list[str]]` the constraint + platform_config_settings: {type}`dict[str, list[str]]` the constraint values to use instead of the default ones. Key are platform names (a human-friendly platform string). Values are lists of `constraint_value` label strings. @@ -142,13 +143,24 @@ def config_settings( # TODO @aignas 2025-06-15: allowing universal2 and platform specific wheels in one # closure is making things maybe a little bit too complicated. "osx_universal2": ["@platforms//os:osx"], - } | platform_constraint_values + } | platform_config_settings for python_version in python_versions: - for platform_name, constraint_values in target_platforms.items(): + for platform_name, config_settings in target_platforms.items(): suffix = "_{}".format(platform_name) if platform_name else "" os, _, cpu = platform_name.partition("_") + # We parse the target settings and if there is a "platforms//os" or + # "platforms//cpu" value in here, we also add it into the constraint_values + # + # this is to ensure that we can still pass all of the unit tests for config + # setting specialization. + constraint_values = [] + for setting in config_settings: + setting_label = Label(setting) + if setting_label.repo_name == "platforms" and setting_label.package in ["os", "cpu"]: + constraint_values.append(setting) + _dist_config_settings( suffix = suffix, plat_flag_values = _plat_flag_values( @@ -158,6 +170,7 @@ def config_settings( glibc_versions = glibc_versions, muslc_versions = muslc_versions, ), + config_settings = config_settings, constraint_values = constraint_values, python_version = python_version, **kwargs @@ -318,7 +331,7 @@ def _plat_flag_values(os, cpu, osx_versions, glibc_versions, muslc_versions): return ret -def _dist_config_setting(*, name, compatible_with = None, native = native, **kwargs): +def _dist_config_setting(*, name, compatible_with = None, selects = selects, native = native, config_settings = None, **kwargs): """A macro to create a target for matching Python binary and source distributions. Args: @@ -327,6 +340,12 @@ def _dist_config_setting(*, name, compatible_with = None, native = native, **kwa compatible with the given dist config setting. For example, if only non-freethreaded python builds are allowed, add FLAGS._is_py_freethreaded_no here. + config_settings: {type}`list[str | Label]` the list of target settings that must + be matched before we try to evaluate the config_setting that we may create in + this function. + selects (struct): The struct containing config_setting_group function + to use for creating config setting groups. Can be overridden for unit tests + reasons. native (struct): The struct containing alias and config_setting rules to use for creating the objects. Can be overridden for unit tests reasons. @@ -346,4 +365,14 @@ def _dist_config_setting(*, name, compatible_with = None, native = native, **kwa ) name = dist_config_setting_name - native.config_setting(name = name, **kwargs) + # first define the config setting that has all of the constraint values + _name = "_" + name + native.config_setting( + name = _name, + **kwargs + ) + selects.config_setting_group( + name = name, + match_all = config_settings + [_name], + visibility = kwargs.get("visibility"), + ) diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 78511b4c27..a0095f8f15 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -372,7 +372,7 @@ def _whl_repo(*, src, whl_library_args, is_multiple_versions, download_only, net ), ) -def _configure(config, *, platform, os_name, arch_name, constraint_values, env = {}, override = False): +def _configure(config, *, platform, os_name, arch_name, config_settings, env = {}, override = False): """Set the value in the config if the value is provided""" config.setdefault("platforms", {}) if platform: @@ -387,7 +387,7 @@ def _configure(config, *, platform, os_name, arch_name, constraint_values, env = name = platform.replace("-", "_").lower(), os_name = os_name, arch_name = arch_name, - constraint_values = constraint_values, + config_settings = config_settings, env = env, ) else: @@ -414,7 +414,7 @@ def _create_config(defaults): arch_name = cpu, os_name = "linux", platform = "linux_{}".format(cpu), - constraint_values = [ + config_settings = [ "@platforms//os:linux", "@platforms//cpu:{}".format(cpu), ], @@ -431,7 +431,7 @@ def _create_config(defaults): # See https://endoflife.date/macos os_name = "osx", platform = "osx_{}".format(cpu), - constraint_values = [ + config_settings = [ "@platforms//os:osx", "@platforms//cpu:{}".format(cpu), ], @@ -443,7 +443,7 @@ def _create_config(defaults): arch_name = "x86_64", os_name = "windows", platform = "windows_x86_64", - constraint_values = [ + config_settings = [ "@platforms//os:windows", "@platforms//cpu:x86_64", ], @@ -513,7 +513,7 @@ You cannot use both the additive_build_content and additive_build_content_file a _configure( defaults, arch_name = tag.arch_name, - constraint_values = tag.constraint_values, + config_settings = tag.config_settings, env = tag.env, os_name = tag.os_name, platform = tag.platform, @@ -693,9 +693,9 @@ You cannot use both the additive_build_content and additive_build_content_file a } for hub_name, extra_whl_aliases in extra_aliases.items() }, - platform_constraint_values = { + platform_config_settings = { hub_name: { - platform_name: sorted([str(Label(cv)) for cv in p.constraint_values]) + platform_name: sorted([str(Label(cv)) for cv in p.config_settings]) for platform_name, p in config.platforms.items() } for hub_name in hub_whl_map @@ -790,7 +790,7 @@ def _pip_impl(module_ctx): for key, values in whl_map.items() }, packages = mods.exposed_packages.get(hub_name, []), - platform_constraint_values = mods.platform_constraint_values.get(hub_name, {}), + platform_config_settings = mods.platform_config_settings.get(hub_name, {}), groups = mods.hub_group_map.get(hub_name), ) @@ -812,10 +812,11 @@ Either this or {attr}`env` `platform_machine` key should be specified. ::: """, ), - "constraint_values": attr.label_list( + "config_settings": attr.label_list( mandatory = True, doc = """\ -The constraint_values to use in select statements. +The list of labels to `config_setting` targets that need to be matched for the platform to be +selected. """, ), "os_name": attr.string( @@ -1145,6 +1146,9 @@ terms used in this extension. [environment_markers]: https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers ::: + +:::{versionadded} VERSION_NEXT_FEATURE +::: """, ), "override": _override_tag, diff --git a/python/private/pypi/hub_repository.bzl b/python/private/pypi/hub_repository.bzl index 4398d7b597..75f3ec98d7 100644 --- a/python/private/pypi/hub_repository.bzl +++ b/python/private/pypi/hub_repository.bzl @@ -34,7 +34,7 @@ def _impl(rctx): }, extra_hub_aliases = rctx.attr.extra_hub_aliases, requirement_cycles = rctx.attr.groups, - platform_constraint_values = rctx.attr.platform_constraint_values, + platform_config_settings = rctx.attr.platform_config_settings, ) for path, contents in aliases.items(): rctx.file(path, contents) @@ -84,7 +84,7 @@ hub_repository = repository_rule( The list of packages that will be exposed via all_*requirements macros. Defaults to whl_map keys. """, ), - "platform_constraint_values": attr.string_list_dict( + "platform_config_settings": attr.string_list_dict( doc = "The constraint values for each platform name. The values are string canonical string Label representations", mandatory = False, ), diff --git a/python/private/pypi/render_pkg_aliases.bzl b/python/private/pypi/render_pkg_aliases.bzl index 267d7ce85d..e743fc20f7 100644 --- a/python/private/pypi/render_pkg_aliases.bzl +++ b/python/private/pypi/render_pkg_aliases.bzl @@ -155,14 +155,14 @@ def _major_minor_versions(python_versions): # Use a dict as a simple set return sorted({_major_minor(v): None for v in python_versions}) -def render_multiplatform_pkg_aliases(*, aliases, platform_constraint_values = {}, **kwargs): +def render_multiplatform_pkg_aliases(*, aliases, platform_config_settings = {}, **kwargs): """Render the multi-platform pkg aliases. Args: aliases: dict[str, list(whl_config_setting)] A list of aliases that will be transformed from ones having `filename` to ones having `config_setting`. - platform_constraint_values: {type}`dict[str, list[str]]` contains all of the - target platforms and their appropriate `constraint_values`. + platform_config_settings: {type}`dict[str, list[str]]` contains all of the + target platforms and their appropriate `target_settings`. **kwargs: extra arguments passed to render_pkg_aliases. Returns: @@ -189,20 +189,20 @@ def render_multiplatform_pkg_aliases(*, aliases, platform_constraint_values = {} muslc_versions = flag_versions.get("muslc_versions", []), osx_versions = flag_versions.get("osx_versions", []), python_versions = _major_minor_versions(flag_versions.get("python_versions", [])), - platform_constraint_values = platform_constraint_values, + platform_config_settings = platform_config_settings, visibility = ["//:__subpackages__"], ) return contents -def _render_config_settings(platform_constraint_values, **kwargs): +def _render_config_settings(platform_config_settings, **kwargs): return """\ load("@rules_python//python/private/pypi:config_settings.bzl", "config_settings") {}""".format(render.call( "config_settings", name = repr("config_settings"), - platform_constraint_values = render.dict( - platform_constraint_values, + platform_config_settings = render.dict( + platform_config_settings, value_repr = render.list, ), **_repr_dict(value_repr = render.list, **kwargs) diff --git a/tests/pypi/config_settings/config_settings_tests.bzl b/tests/pypi/config_settings/config_settings_tests.bzl index 9551d42d10..a15f6b4d32 100644 --- a/tests/pypi/config_settings/config_settings_tests.bzl +++ b/tests/pypi/config_settings/config_settings_tests.bzl @@ -657,7 +657,7 @@ def config_settings_test_suite(name): # buildifier: disable=function-docstring glibc_versions = [(2, 14), (2, 17)], muslc_versions = [(1, 1)], osx_versions = [(10, 9), (11, 0)], - platform_constraint_values = { + platform_config_settings = { "linux_aarch64": [ "@platforms//cpu:aarch64", "@platforms//os:linux", diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index 231e8cab41..146293ee8d 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -78,19 +78,17 @@ def _parse_modules(env, enable_pipstar = 0, **kwargs): def _default( arch_name = None, - constraint_values = None, + config_settings = None, os_name = None, platform = None, - target_settings = None, env = None, whl_limit = None, whl_platforms = None): return struct( arch_name = arch_name, - constraint_values = constraint_values, os_name = os_name, platform = platform, - target_settings = target_settings, + config_settings = config_settings, env = env or {}, whl_platforms = whl_platforms, whl_limit = whl_limit, @@ -1051,7 +1049,7 @@ def _test_pipstar_platforms(env): default = [ _default( platform = "{}_{}".format(os, cpu), - constraint_values = [ + config_settings = [ "@platforms//os:{}".format(os), "@platforms//cpu:{}".format(cpu), ], diff --git a/tests/pypi/pkg_aliases/pkg_aliases_test.bzl b/tests/pypi/pkg_aliases/pkg_aliases_test.bzl index 0fbcd4e7a6..123ee725f8 100644 --- a/tests/pypi/pkg_aliases/pkg_aliases_test.bzl +++ b/tests/pypi/pkg_aliases/pkg_aliases_test.bzl @@ -392,6 +392,9 @@ _tests.append(_test_multiplatform_whl_aliases_filename_versioned) def _mock_alias(container): return lambda name, **kwargs: container.append(name) +def _mock_config_setting_group(container): + return lambda name, **kwargs: container.append(name) + def _mock_config_setting(container): def _inner(name, flag_values = None, constraint_values = None, **_): if flag_values or constraint_values: @@ -417,9 +420,12 @@ def _test_config_settings_exist_legacy(env): python_versions = ["3.11"], native = struct( alias = _mock_alias(available_config_settings), - config_setting = _mock_config_setting(available_config_settings), + config_setting = _mock_config_setting([]), ), - platform_constraint_values = { + selects = struct( + config_setting_group = _mock_config_setting_group(available_config_settings), + ), + platform_config_settings = { "linux_aarch64": [ "@platforms//cpu:aarch64", "@platforms//os:linux", @@ -454,7 +460,7 @@ def _test_config_settings_exist(env): "any": {}, "macosx_11_0_arm64": { "osx_versions": [(11, 0)], - "platform_constraint_values": { + "platform_config_settings": { "osx_aarch64": [ "@platforms//cpu:aarch64", "@platforms//os:osx", @@ -463,7 +469,7 @@ def _test_config_settings_exist(env): }, "manylinux_2_17_x86_64": { "glibc_versions": [(2, 17), (2, 18)], - "platform_constraint_values": { + "platform_config_settings": { "linux_x86_64": [ "@platforms//cpu:x86_64", "@platforms//os:linux", @@ -472,7 +478,7 @@ def _test_config_settings_exist(env): }, "manylinux_2_18_x86_64": { "glibc_versions": [(2, 17), (2, 18)], - "platform_constraint_values": { + "platform_config_settings": { "linux_x86_64": [ "@platforms//cpu:x86_64", "@platforms//os:linux", @@ -481,7 +487,7 @@ def _test_config_settings_exist(env): }, "musllinux_1_1_aarch64": { "muslc_versions": [(1, 2), (1, 1), (1, 0)], - "platform_constraint_values": { + "platform_config_settings": { "linux_aarch64": [ "@platforms//cpu:aarch64", "@platforms//os:linux", @@ -500,7 +506,10 @@ def _test_config_settings_exist(env): python_versions = ["3.11"], native = struct( alias = _mock_alias(available_config_settings), - config_setting = _mock_config_setting(available_config_settings), + config_setting = _mock_config_setting([]), + ), + selects = struct( + config_setting_group = _mock_config_setting_group(available_config_settings), ), **kwargs ) diff --git a/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl b/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl index c262ed6823..ad7f36aed6 100644 --- a/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl +++ b/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl @@ -93,7 +93,7 @@ def _test_bzlmod_aliases(env): }, }, extra_hub_aliases = {"bar_baz": ["foo"]}, - platform_constraint_values = { + platform_config_settings = { "linux_x86_64": [ "@platforms//os:linux", "@platforms//cpu:x86_64", @@ -136,7 +136,7 @@ load("@rules_python//python/private/pypi:config_settings.bzl", "config_settings" config_settings( name = "config_settings", - platform_constraint_values = { + platform_config_settings = { "linux_x86_64": [ "@platforms//os:linux", "@platforms//cpu:x86_64", From 8f8c5b9ba7c7f68f37b7687ebb22931cff075241 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 20 Jun 2025 00:42:42 -0700 Subject: [PATCH 154/156] docs: fix various typos and improve grammar (#3015) Used Jules to do some copy editing. It found a variety of typos. * Consistently use backticks for rules_python, WORKSPACE, and some other terms * Various simple typo and grammar fixes --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- docs/README.md | 16 +-- docs/_includes/py_console_script_binary.md | 25 ++-- docs/coverage.md | 4 +- docs/devguide.md | 28 ++-- docs/environment-variables.md | 24 ++-- docs/extending.md | 12 +- docs/gazelle.md | 4 +- docs/getting-started.md | 10 +- docs/glossary.md | 10 +- docs/index.md | 22 ++-- docs/precompiling.md | 40 +++--- docs/pypi/circular-dependencies.md | 16 +-- docs/pypi/download-workspace.md | 12 +- docs/pypi/download.md | 68 +++++----- docs/pypi/index.md | 6 +- docs/pypi/lock.md | 11 +- docs/pypi/patch.md | 4 +- docs/pypi/use.md | 42 +++--- docs/repl.md | 2 +- docs/support.md | 22 ++-- docs/toolchains.md | 142 ++++++++++----------- 21 files changed, 263 insertions(+), 257 deletions(-) diff --git a/docs/README.md b/docs/README.md index d98be41232..456f1cfd64 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,14 +1,14 @@ # rules_python Sphinx docs generation The docs for rules_python are generated using a combination of Sphinx, Bazel, -and Readthedocs.org. The Markdown files in source control are unlikely to render +and Read the Docs. The Markdown files in source control are unlikely to render properly without the Sphinx processing step because they rely on Sphinx and MyST-specific Markdown functionality. The actual sources that Sphinx consumes are in this directory, with Stardoc -generating additional sources or Sphinx. +generating additional sources for Sphinx. -Manually building the docs isn't necessary -- readthedocs.org will +Manually building the docs isn't necessary -- Read the Docs will automatically build and deploy them when commits are pushed to the repo. ## Generating docs for development @@ -31,8 +31,8 @@ equivalent bazel command if desired. ### Installing ibazel The `ibazel` tool can be used to automatically rebuild the docs as you -development them. See the [ibazel docs](https://github.com/bazelbuild/bazel-watcher) for -how to install it. The quick start for linux is: +develop them. See the [ibazel docs](https://github.com/bazelbuild/bazel-watcher) for +how to install it. The quick start for Linux is: ``` sudo apt install npm @@ -57,9 +57,9 @@ docs/. The Sphinx configuration is `docs/conf.py`. See https://www.sphinx-doc.org/ for details about the configuration file. -## Readthedocs configuration +## Read the Docs configuration -There's two basic parts to the readthedocs configuration: +There's two basic parts to the Read the Docs configuration: * `.readthedocs.yaml`: This configuration file controls most settings, such as the OS version used to build, Python version, dependencies, what Bazel @@ -69,4 +69,4 @@ There's two basic parts to the readthedocs configuration: controls additional settings such as permissions, what versions are published, when to publish changes, etc. -For more readthedocs configuration details, see docs.readthedocs.io. +For more Read the Docs configuration details, see docs.readthedocs.io. diff --git a/docs/_includes/py_console_script_binary.md b/docs/_includes/py_console_script_binary.md index d327091630..cae9f9f2f5 100644 --- a/docs/_includes/py_console_script_binary.md +++ b/docs/_includes/py_console_script_binary.md @@ -1,8 +1,8 @@ This rule is to make it easier to generate `console_script` entry points as per Python [specification]. -Generate a `py_binary` target for a particular console_script `entry_point` -from a PyPI package, e.g. for creating an executable `pylint` target use: +Generate a `py_binary` target for a particular `console_script` entry_point +from a PyPI package, e.g. for creating an executable `pylint` target, use: ```starlark load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") @@ -12,11 +12,12 @@ py_console_script_binary( ) ``` -#### Specifying extra dependencies +#### Specifying extra dependencies You can also specify extra dependencies and the -exact script name you want to call. It is useful for tools like `flake8`, `pylint`, -`pytest`, which have plugin discovery methods and discover dependencies from the -PyPI packages available in the `PYTHONPATH`. +exact script name you want to call. This is useful for tools like `flake8`, +`pylint`, and `pytest`, which have plugin discovery methods and discover +dependencies from the PyPI packages available in the `PYTHONPATH`. + ```starlark load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") @@ -44,13 +45,13 @@ load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_cons py_console_script_binary( name = "yamllint", pkg = "@pip//yamllint", - python_version = "3.9" + python_version = "3.9", ) ``` #### Adding a Shebang Line -You can specify a shebang line for the generated binary, useful for Unix-like +You can specify a shebang line for the generated binary. This is useful for Unix-like systems where the shebang line determines which interpreter is used to execute the script, per [PEP441]: @@ -70,12 +71,12 @@ Python interpreter is available in the environment. #### Using a specific Python Version directly from a Toolchain :::{deprecated} 1.1.0 -The toolchain specific `py_binary` and `py_test` symbols are aliases to the regular rules. -i.e. Deprecated `load("@python_versions//3.11:defs.bzl", "py_binary")` and `load("@python_versions//3.11:defs.bzl", "py_test")` +The toolchain-specific `py_binary` and `py_test` symbols are aliases to the regular rules. +For example, `load("@python_versions//3.11:defs.bzl", "py_binary")` and `load("@python_versions//3.11:defs.bzl", "py_test")` are deprecated. -You should instead specify the desired python version with `python_version`; see above example. +You should instead specify the desired Python version with `python_version`; see the example above. ::: -Alternatively, the [`py_console_script_binary.binary_rule`] arg can be passed +Alternatively, the {obj}`py_console_script_binary.binary_rule` arg can be passed the version-bound `py_binary` symbol, or any other `py_binary`-compatible rule of your choosing: ```starlark diff --git a/docs/coverage.md b/docs/coverage.md index 3e0e67368c..3c7d9e0cfc 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -9,7 +9,7 @@ when configuring toolchains. ## Enabling `rules_python` coverage support Enabling the coverage support bundled with `rules_python` just requires setting an -argument when registerting toolchains. +argument when registering toolchains. For Bzlmod: @@ -32,7 +32,7 @@ python_register_toolchains( This will implicitly add the version of `coverage` bundled with `rules_python` to the dependencies of `py_test` rules when `bazel coverage` is run. If a target already transitively depends on a different version of -`coverage`, then behavior is undefined -- it is undefined which version comes +`coverage`, then the behavior is undefined -- it is undefined which version comes first in the import path. If you find yourself in this situation, then you'll need to manually configure coverage (see below). ::: diff --git a/docs/devguide.md b/docs/devguide.md index f233611cad..345907b374 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -1,7 +1,7 @@ # Dev Guide -This document covers tips and guidance for working on the rules_python code -base. A primary audience for it is first time contributors. +This document covers tips and guidance for working on the `rules_python` code +base. Its primary audience is first-time contributors. ## Running tests @@ -12,8 +12,8 @@ bazel test //... ``` And it will run all the tests it can find. The first time you do this, it will -probably take long time because various dependencies will need to be downloaded -and setup. Subsequent runs will be faster, but there are many tests, and some of +probably take a long time because various dependencies will need to be downloaded +and set up. Subsequent runs will be faster, but there are many tests, and some of them are slow. If you're working on a particular area of code, you can run just the tests in those directories instead, which can speed up your edit-run cycle. @@ -22,14 +22,14 @@ the tests in those directories instead, which can speed up your edit-run cycle. Most code should have tests of some sort. This helps us have confidence that refactors didn't break anything and that releases won't have regressions. -We don't require 100% test coverage, testing certain Bazel functionality is +We don't require 100% test coverage; testing certain Bazel functionality is difficult, and some edge cases are simply too hard to test or not worth the extra complexity. We try to judiciously decide when not having tests is a good idea. Tests go under `tests/`. They are loosely organized into directories for the particular subsystem or functionality they are testing. If an existing directory -doesn't seem like a good match for the functionality being testing, then it's +doesn't seem like a good match for the functionality being tested, then it's fine to create a new directory. Re-usable test helpers and support code go in `tests/support`. Tests don't need @@ -72,9 +72,9 @@ the rule. To have it support setting a new flag: An integration test is one that runs a separate Bazel instance inside the test. These tests are discouraged unless absolutely necessary because they are slow, -require much memory and CPU, and are generally harder to debug. Integration -tests are reserved for things that simple can't be tested otherwise, or for -simple high level verification tests. +require a lot of memory and CPU, and are generally harder to debug. Integration +tests are reserved for things that simply can't be tested otherwise, or for +simple high-level verification tests. Integration tests live in `tests/integration`. When possible, add to an existing integration test. @@ -98,9 +98,9 @@ integration test. ## Updating tool dependencies -It's suggested to routinely update the tool versions within our repo - some of the -tools are using requirement files compiled by `uv` and others use other means. In order -to have everything self-documented, we have a special target - -`//private:requirements.update`, which uses `rules_multirun` to run in sequence all -of the requirement updating scripts in one go. This can be done once per release as +It's suggested to routinely update the tool versions within our repo. Some of the +tools are using requirement files compiled by `uv`, and others use other means. In order +to have everything self-documented, we have a special target, +`//private:requirements.update`, which uses `rules_multirun` to run all +of the requirement-updating scripts in sequence in one go. This can be done once per release as we prepare for releases. diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 8a51bcbfd2..9a8c1dfe99 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -5,16 +5,16 @@ This variable allows for additional arguments to be provided to the Python interpreter at bootstrap time when the `bash` bootstrap is used. If `RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` were provided as `-Xaaa`, then the command -would be; +would be: ``` python -Xaaa /path/to/file.py ``` This feature is likely to be useful for the integration of debuggers. For example, -it would be possible to configure the `RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` to -be set to `/path/to/debugger.py --port 12344 --file` resulting -in the command executed being; +it would be possible to configure `RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` to +be set to `/path/to/debugger.py --port 12344 --file`, resulting +in the command executed being: ``` python /path/to/debugger.py --port 12345 --file /path/to/file.py @@ -42,14 +42,14 @@ doing. This is mostly useful for development to debug errors. :::{envvar} RULES_PYTHON_DEPRECATION_WARNINGS -When `1`, the rules_python will warn users about deprecated functionality that will +When `1`, `rules_python` will warn users about deprecated functionality that will be removed in a subsequent major `rules_python` version. Defaults to `0` if unset. ::: ::::{envvar} RULES_PYTHON_ENABLE_PYSTAR -When `1`, the rules_python Starlark implementation of the core rules is used -instead of the Bazel-builtin rules. Note this requires Bazel 7+. Defaults +When `1`, the `rules_python` Starlark implementation of the core rules is used +instead of the Bazel-builtin rules. Note that this requires Bazel 7+. Defaults to `1`. :::{versionadded} 0.26.0 @@ -62,7 +62,7 @@ The default became `1` if unspecified ::::{envvar} RULES_PYTHON_ENABLE_PIPSTAR -When `1`, the rules_python Starlark implementation of the pypi/pip integration is used +When `1`, the `rules_python` Starlark implementation of the PyPI/pip integration is used instead of the legacy Python scripts. :::{versionadded} 1.5.0 @@ -95,8 +95,8 @@ exit. :::{envvar} RULES_PYTHON_GAZELLE_VERBOSE -When `1`, debug information from gazelle is printed to stderr. -::: +When `1`, debug information from Gazelle is printed to stderr. +:::: :::{envvar} RULES_PYTHON_PIP_ISOLATED @@ -125,9 +125,9 @@ Determines the verbosity of logging output for repo rules. Valid values: :::{envvar} RULES_PYTHON_REPO_TOOLCHAIN_VERSION_OS_ARCH -Determines the python interpreter platform to be used for a particular +Determines the Python interpreter platform to be used for a particular interpreter `(version, os, arch)` triple to be used in repository rules. -Replace the `VERSION_OS_ARCH` part with actual values when using, e.g. +Replace the `VERSION_OS_ARCH` part with actual values when using, e.g., `3_13_0_linux_x86_64`. The version values must have `_` instead of `.` and the os, arch values are the same as the ones mentioned in the `//python:versions.bzl` file. diff --git a/docs/extending.md b/docs/extending.md index 387310e6cf..00018fbd74 100644 --- a/docs/extending.md +++ b/docs/extending.md @@ -41,10 +41,10 @@ wrappers around the keyword arguments eventually passed to the `rule()` function. These builder APIs give access to the _entire_ rule definition and allow arbitrary modifications. -This is level of control is powerful, but also volatile. A rule definition +This level of control is powerful but also volatile. A rule definition contains many details that _must_ change as the implementation changes. What is more or less likely to change isn't known in advance, but some general -rules are: +rules of thumb are: * Additive behavior to public attributes will be less prone to breaking. * Internal attributes that directly support a public attribute are likely @@ -55,7 +55,7 @@ rules are: ## Example: validating a source file -In this example, we derive from `py_library` a custom rule that verifies source +In this example, we derive a custom rule from `py_library` that verifies source code contains the word "snakes". It does this by: * Adding an implicit dependency on a checker program @@ -111,7 +111,7 @@ has_snakes_library = create_has_snakes_rule() ## Example: adding transitions -In this example, we derive from `py_binary` to force building for a particular +In this example, we derive a custom rule from `py_binary` to force building for a particular platform. We do this by: * Adding an additional output to the rule's cfg @@ -136,8 +136,8 @@ def create_rule(): r.cfg.add_output("//command_line_option:platforms") return r.build() -py_linux_binary = create_linux_binary_rule() +py_linux_binary = create_rule() ``` -Users can then use `py_linux_binary` the same as a regular py_binary. It will +Users can then use `py_linux_binary` the same as a regular `py_binary`. It will act as if `--platforms=//my/platforms:linux` was specified when building it. diff --git a/docs/gazelle.md b/docs/gazelle.md index 89f26d67bb..60b46faf2c 100644 --- a/docs/gazelle.md +++ b/docs/gazelle.md @@ -3,7 +3,7 @@ [Gazelle](https://github.com/bazelbuild/bazel-gazelle) is a build file generator for Bazel projects. It can create new `BUILD.bazel` files for a project that follows language conventions and update existing build files to include new sources, dependencies, and options. -Bazel may run Gazelle using the Gazelle rule, or it may be installed and run as a command line tool. +Bazel may run Gazelle using the Gazelle rule, or Gazelle may be installed and run as a command line tool. -See the documentation for Gazelle with rules_python in the {gh-path}`gazelle` +See the documentation for Gazelle with `rules_python` in the {gh-path}`gazelle` directory. diff --git a/docs/getting-started.md b/docs/getting-started.md index 7e7b88aa8a..d81d72f590 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,14 +1,14 @@ # Getting started -This doc is a simplified guide to help get started quickly. It provides +This document is a simplified guide to help you get started quickly. It provides a simplified introduction to having a working Python program for both `bzlmod` and the older way of using `WORKSPACE`. It assumes you have a `requirements.txt` file with your PyPI dependencies. -For more details information about configuring `rules_python`, see: +For more detailed information about configuring `rules_python`, see: * [Configuring the runtime](configuring-toolchains) -* [Configuring third party dependencies (pip/pypi)](./pypi/index) +* [Configuring third-party dependencies (pip/PyPI)](./pypi/index) * [API docs](api/index) ## Including dependencies @@ -32,7 +32,7 @@ use_repo(pip, "pypi") ### Using a WORKSPACE file -Using WORKSPACE is deprecated, but still supported, and a bit more involved than +Using `WORKSPACE` is deprecated but still supported, and it's a bit more involved than using Bzlmod. Here is a simplified setup to download the prebuilt runtimes. ```starlark @@ -72,7 +72,7 @@ pip_parse( ## "Hello World" -Once you've imported the rule set using either Bzlmod or WORKSPACE, you can then +Once you've imported the rule set using either Bzlmod or `WORKSPACE`, you can then load the core rules in your `BUILD` files with the following: ```starlark diff --git a/docs/glossary.md b/docs/glossary.md index 9afbcffb92..c9bd03fd0e 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -5,7 +5,7 @@ common attributes : Every rule has a set of common attributes. See Bazel's [Common attributes](https://bazel.build/reference/be/common-definitions#common-attributes) - for a complete listing + for a complete listing. in-build runtime : An in-build runtime is one where the Python runtime, and all its files, are @@ -21,9 +21,9 @@ which can be a significant number of files. platform runtime : A platform runtime is a Python runtime that is assumed to be installed on the -system where a Python binary runs, whereever that may be. For example, using `/usr/bin/python3` +system where a Python binary runs, wherever that may be. For example, using `/usr/bin/python3` as the interpreter is a platform runtime -- it assumes that, wherever the binary -runs (your local machine, a remote worker, within a container, etc), that path +runs (your local machine, a remote worker, within a container, etc.), that path is available. Such runtimes are _not_ part of a binary's runfiles. The main advantage of platform runtimes is they are lightweight insofar as @@ -42,8 +42,8 @@ rule callable accepted; refer to the respective API accepting this type. simple label -: A `str` or `Label` object but not a _direct_ `select` object. These usually - mean a string manipulation is occuring, which can't be done on `select` + A `str` or `Label` object but not a _direct_ `select` object. This usually + means a string manipulation is occurring, which can't be done on `select` objects. Such attributes are usually still configurable if an alias is used, and a reference to the alias is passed instead. diff --git a/docs/index.md b/docs/index.md index 82023f3ad8..25b423c6c3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # Python Rules for Bazel -`rules_python` is the home for 4 major components with varying maturity levels. +`rules_python` is the home for four major components with varying maturity levels. :::{topic} Core rules @@ -9,8 +9,8 @@ The core Python rules -- `py_library`, `py_binary`, `py_test`, support in Bazel. When using Bazel 6 (or earlier), the core rules are bundled into the Bazel binary, and the symbols -in this repository are simple aliases. On Bazel 7 and above `rules_python` uses -a separate Starlark implementation, +in this repository are simple aliases. On Bazel 7 and above, `rules_python` uses +a separate Starlark implementation; see {ref}`Migrating from the Bundled Rules` below. This repository follows @@ -21,12 +21,12 @@ outlined in the [support](support) page. :::{topic} PyPI integration -Package installation rules for integrating with PyPI and other SimpleAPI +Package installation rules for integrating with PyPI and other Simple API- compatible indexes. These rules work and can be used in production, but the cross-platform building that supports pulling PyPI dependencies for a target platform that is different -from the host platform is still in beta and the APIs that are subject to potential +from the host platform is still in beta, and the APIs that are subject to potential change are marked as `experimental`. ::: @@ -36,9 +36,9 @@ change are marked as `experimental`. `sphinxdocs` rules allow users to generate documentation using Sphinx powered by Bazel, with additional functionality for documenting Starlark and Bazel code. -The functionality is exposed because other projects find it useful, but -it is available as is and **the semantic versioning and -compatibility policy used by `rules_python` does not apply**. +The functionality is exposed because other projects find it useful, but +it is available "as is", and **the semantic versioning and +compatibility policy used by `rules_python` does not apply**. ::: @@ -47,7 +47,7 @@ compatibility policy used by `rules_python` does not apply**. `gazelle` plugin for generating `BUILD.bazel` files based on Python source code. -This is available as is and the semantic versioning used by `rules_python` does +This is available "as is", and the semantic versioning used by `rules_python` does not apply. ::: @@ -78,7 +78,7 @@ appropriate `load()` statements and rewrite uses of `native.py_*`. buildifier --lint=fix --warnings=native-py ``` -Currently, the `WORKSPACE` file needs to be updated manually as per +Currently, the `WORKSPACE` file needs to be updated manually as per [Getting started](getting-started). Note that Starlark-defined bundled symbols underneath @@ -87,7 +87,7 @@ by buildifier. ## Migrating to bzlmod -See {gh-path}`Bzlmod support ` for any behaviour differences between +See {gh-path}`Bzlmod support ` for any behavioral differences between `bzlmod` and `WORKSPACE`. diff --git a/docs/precompiling.md b/docs/precompiling.md index a46608f77e..ea978cddce 100644 --- a/docs/precompiling.md +++ b/docs/precompiling.md @@ -1,6 +1,6 @@ # Precompiling -Precompiling is compiling Python source files (`.py` files) into byte code +Precompiling is compiling Python source files (`.py` files) into bytecode (`.pyc` files) at build time instead of runtime. Doing it at build time can improve performance by skipping that work at runtime. @@ -15,12 +15,12 @@ While precompiling helps runtime performance, it has two main costs: a `.pyc` file. Compiled files are generally around the same size as the source files, so it approximately doubles the disk usage. 2. Precompiling requires running an extra action at build time. While - compiling itself isn't that expensive, the overhead can become noticable + compiling itself isn't that expensive, the overhead can become noticeable as more files need to be compiled. ## Binary-level opt-in -Binary-level opt-in allows enabling precompiling on a per-target basic. This is +Binary-level opt-in allows enabling precompiling on a per-target basis. This is useful for situations such as: * Globally enabling precompiling in your `.bazelrc` isn't feasible. This may @@ -41,7 +41,7 @@ can use an opt-in or opt-out approach by setting its value: ## Pyc-only builds -A pyc-only build (aka "source less" builds) is when only `.pyc` files are +A pyc-only build (aka "sourceless" builds) is when only `.pyc` files are included; the source `.py` files are not included. To enable this, set @@ -55,8 +55,8 @@ The advantage of pyc-only builds are: The disadvantages are: * Error messages will be less precise because the precise line and offset - information isn't in an pyc file. -* pyc files are Python major-version specific. + information isn't in a pyc file. +* pyc files are Python major-version-specific. :::{note} pyc files are not a form of hiding source code. They are trivial to uncompile, @@ -75,11 +75,11 @@ mechanisms are available: the {bzl:attr}`precompiler` attribute. Arbitrary binaries are supported. * The execution requirements can be customized using `--@rules_python//tools/precompiler:execution_requirements`. This is a list - flag that can be repeated. Each entry is a key=value that is added to the + flag that can be repeated. Each entry is a `key=value` pair that is added to the execution requirements of the `PyCompile` action. Note that this flag - is specific to the rules_python precompiler. If a custom binary is used, + is specific to the `rules_python` precompiler. If a custom binary is used, this flag will have to be propagated from the custom binary using the - `testing.ExecutionInfo` provider; refer to the `py_interpreter_program` an + `testing.ExecutionInfo` provider; refer to the `py_interpreter_program` example. The default precompiler implementation is an asynchronous/concurrent implementation. If you find it has bugs or hangs, please report them. In the @@ -90,18 +90,18 @@ as well, but is less likely to have issues. The `execution_requirements` keys of most relevance are: * `supports-workers`: 1 or 0, to indicate if a regular persistent worker is desired. -* `supports-multiplex-workers`: 1 o 0, to indicate if a multiplexed persistent +* `supports-multiplex-workers`: `1` or `0`, to indicate if a multiplexed persistent worker is desired. -* `requires-worker-protocol`: json or proto; the rules_python precompiler - currently only supports json. -* `supports-multiplex-sandboxing`: 1 or 0, to indicate if sanboxing is of the +* `requires-worker-protocol`: `json` or `proto`; the `rules_python` precompiler + currently only supports `json`. +* `supports-multiplex-sandboxing`: `1` or `0`, to indicate if sandboxing of the worker is supported. -* `supports-worker-cancellation`: 1 or 1, to indicate if requests to the worker +* `supports-worker-cancellation`: `1` or `0`, to indicate if requests to the worker can be cancelled. Note that any execution requirements values can be specified in the flag. -## Known issues, caveats, and idiosyncracies +## Known issues, caveats, and idiosyncrasies * Precompiling requires Bazel 7+ with the Pystar rule implementation enabled. * Mixing rules_python PyInfo with Bazel builtin PyInfo will result in pyc files @@ -111,14 +111,14 @@ Note that any execution requirements values can be specified in the flag. causes the module to be found in the workspace source directory instead of within the binary's runfiles directory (where the pyc files are). This can usually be worked around by removing `sys.path[0]` (or otherwise ensuring the - runfiles directory comes before the repos source directory in `sys.path`). -* The pyc filename does not include the optimization level (e.g. - `foo.cpython-39.opt-2.pyc`). This works fine (it's all byte code), but also + runfiles directory comes before the repo's source directory in `sys.path`). +* The pyc filename does not include the optimization level (e.g., + `foo.cpython-39.opt-2.pyc`). This works fine (it's all bytecode), but also means the interpreter `-O` argument can't be used -- doing so will cause the interpreter to look for the non-existent `opt-N` named files. -* Targets with the same source files and different exec properites will result +* Targets with the same source files and different exec properties will result in action conflicts. This most commonly occurs when a `py_binary` and - `py_library` have the same source files. To fix, modify both targets so + a `py_library` have the same source files. To fix this, modify both targets so they have the same exec properties. If this is difficult because unsupported exec groups end up being passed to the Python rules, please file an issue to have those exec groups added to the Python rules. diff --git a/docs/pypi/circular-dependencies.md b/docs/pypi/circular-dependencies.md index d22f5b36a7..62613f489e 100644 --- a/docs/pypi/circular-dependencies.md +++ b/docs/pypi/circular-dependencies.md @@ -3,8 +3,8 @@ # Circular dependencies -Sometimes PyPi packages contain dependency cycles -- for instance a particular -version `sphinx` (this is no longer the case in the latest version as of +Sometimes PyPI packages contain dependency cycles. For instance, a particular +version of `sphinx` (this is no longer the case in the latest version as of 2024-06-02) depends on `sphinxcontrib-serializinghtml`. When using them as `requirement()`s, ala @@ -47,10 +47,10 @@ simultaneously. ) ``` -`pip_parse` supports fixing multiple cycles simultaneously, however cycles must -be distinct. `apache-airflow` for instance has dependency cycles with a number +`pip_parse` supports fixing multiple cycles simultaneously, however, cycles must +be distinct. `apache-airflow`, for instance, has dependency cycles with a number of its optional dependencies, which means those optional dependencies must all -be a part of the `airflow` cycle. For instance -- +be a part of the `airflow` cycle. For instance: ```starlark ... @@ -67,9 +67,9 @@ be a part of the `airflow` cycle. For instance -- Alternatively, one could resolve the cycle by removing one leg of it. -For example while `apache-airflow-providers-sqlite` is "baked into" the Airflow +For example, while `apache-airflow-providers-sqlite` is "baked into" the Airflow package, `apache-airflow-providers-postgres` is not and is an optional feature. -Rather than listing `apache-airflow[postgres]` in your `requirements.txt` which +Rather than listing `apache-airflow[postgres]` in your `requirements.txt`, which would expose a cycle via the extra, one could either _manually_ depend on `apache-airflow` and `apache-airflow-providers-postgres` separately as requirements. Bazel rules which need only `apache-airflow` can take it as a @@ -77,6 +77,6 @@ dependency, and rules which explicitly want to mix in `apache-airflow-providers-postgres` now can. Alternatively, one could use `rules_python`'s patching features to remove one -leg of the dependency manually. For instance by making +leg of the dependency manually, for instance, by making `apache-airflow-providers-postgres` not explicitly depend on `apache-airflow` or perhaps `apache-airflow-providers-common-sql`. diff --git a/docs/pypi/download-workspace.md b/docs/pypi/download-workspace.md index 48710095a4..5dfb0f257a 100644 --- a/docs/pypi/download-workspace.md +++ b/docs/pypi/download-workspace.md @@ -3,7 +3,7 @@ # Download (WORKSPACE) -This documentation page covers how to download the PyPI dependencies in the legacy `WORKSPACE` setup. +This documentation page covers how to download PyPI dependencies in the legacy `WORKSPACE` setup. To add pip dependencies to your `WORKSPACE`, load the `pip_parse` function and call it to create the central external repo and individual wheel external repos. @@ -27,7 +27,7 @@ install_deps() ## Interpreter selection -Note that pip parse runs before the Bazel before decides which Python toolchain to use, it cannot +Note that because `pip_parse` runs before Bazel decides which Python toolchain to use, it cannot enforce that the interpreter used to invoke `pip` matches the interpreter used to run `py_binary` targets. By default, `pip_parse` uses the system command `"python3"`. To override this, pass in the {attr}`pip_parse.python_interpreter` attribute or {attr}`pip_parse.python_interpreter_target`. @@ -44,9 +44,9 @@ your system `python` interpreter), you can force it to re-execute by running (per-os-arch-requirements)= ## Requirements for a specific OS/Architecture -In some cases you may need to use different requirements files for different OS, Arch combinations. +In some cases, you may need to use different requirements files for different OS and architecture combinations. This is enabled via the {attr}`pip_parse.requirements_by_platform` attribute. The keys of the -dictionary are labels to the file and the values are a list of comma separated target (os, arch) +dictionary are labels to the file, and the values are a list of comma-separated target (os, arch) tuples. For example: @@ -63,8 +63,8 @@ For example: requirements_lock = "requirements_lock.txt", ``` -In case of duplicate platforms, `rules_python` will raise an error as there has -to be unambiguous mapping of the requirement files to the (os, arch) tuples. +In case of duplicate platforms, `rules_python` will raise an error, as there has +to be an unambiguous mapping of the requirement files to the (os, arch) tuples. An alternative way is to use per-OS requirement attributes. ```starlark diff --git a/docs/pypi/download.md b/docs/pypi/download.md index 18d6699ab3..7f4e205d84 100644 --- a/docs/pypi/download.md +++ b/docs/pypi/download.md @@ -8,8 +8,8 @@ For WORKSPACE instructions see [here](./download-workspace). ::: To add PyPI dependencies to your `MODULE.bazel` file, use the `pip.parse` -extension, and call it to create the central external repo and individual wheel -external repos. Include in the `MODULE.bazel` the toolchain extension as shown +extension and call it to create the central external repo and individual wheel +external repos. Include the toolchain extension in the `MODULE.bazel` file as shown in the first bzlmod example above. ```starlark @@ -24,7 +24,7 @@ pip.parse( use_repo(pip, "my_deps") ``` -For more documentation, see the bzlmod examples under the {gh-path}`examples` folder or the documentation +For more documentation, see the Bzlmod examples under the {gh-path}`examples` folder or the documentation for the {obj}`@rules_python//python/extensions:pip.bzl` extension. :::note} @@ -42,7 +42,7 @@ difference. ## Interpreter selection -The {obj}`pip.parse` `bzlmod` extension by default uses the hermetic python toolchain for the host +The {obj}`pip.parse` `bzlmod` extension by default uses the hermetic Python toolchain for the host platform, but you can customize the interpreter using {attr}`pip.parse.python_interpreter` and {attr}`pip.parse.python_interpreter_target`. @@ -58,10 +58,10 @@ name]`. (per-os-arch-requirements)= ## Requirements for a specific OS/Architecture -In some cases you may need to use different requirements files for different OS, Arch combinations. -This is enabled via the `requirements_by_platform` attribute in `pip.parse` extension and the -{obj}`pip.parse` tag class. The keys of the dictionary are labels to the file and the values are a -list of comma separated target (os, arch) tuples. +In some cases, you may need to use different requirements files for different OS and architecture combinations. +This is enabled via the `requirements_by_platform` attribute in the `pip.parse` extension and the +{obj}`pip.parse` tag class. The keys of the dictionary are labels to the file, and the values are a +list of comma-separated target (os, arch) tuples. For example: ```starlark @@ -77,8 +77,8 @@ For example: requirements_lock = "requirements_lock.txt", ``` -In case of duplicate platforms, `rules_python` will raise an error as there has -to be unambiguous mapping of the requirement files to the (os, arch) tuples. +In case of duplicate platforms, `rules_python` will raise an error, as there has +to be an unambiguous mapping of the requirement files to the (os, arch) tuples. An alternative way is to use per-OS requirement attributes. ```starlark @@ -98,24 +98,24 @@ the lock file will be evaluated against, consider using the aforementioned ## Multi-platform support -Historically the {obj}`pip_parse` and {obj}`pip.parse` have been only downloading/building +Historically, the {obj}`pip_parse` and {obj}`pip.parse` have only been downloading/building Python dependencies for the host platform that the `bazel` commands are executed on. Over -the years people started needing support for building containers and usually that involves -fetching dependencies for a particular target platform that may be other than the host +the years, people started needing support for building containers, and usually, that involves +fetching dependencies for a particular target platform that may be different from the host platform. -Multi-platform support of cross-building the wheels can be done in two ways: +Multi-platform support for cross-building the wheels can be done in two ways: 1. using {attr}`experimental_index_url` for the {bzl:obj}`pip.parse` bzlmod tag class -2. using {attr}`pip.parse.download_only` setting. +2. using the {attr}`pip.parse.download_only` setting. :::{warning} -This will not for sdists with C extensions, but pure Python sdists may still work using the first +This will not work for sdists with C extensions, but pure Python sdists may still work using the first approach. ::: ### Using `download_only` attribute -Let's say you have 2 requirements files: +Let's say you have two requirements files: ``` # requirements.linux_x86_64.txt --platform=manylinux_2_17_x86_64 @@ -151,9 +151,9 @@ pip.parse( ) ``` -With this, the `pip.parse` will create a hub repository that is going to -support only two platforms - `cp39_osx_aarch64` and `cp39_linux_x86_64` and it -will only use `wheels` and ignore any sdists that it may find on the PyPI +With this, `pip.parse` will create a hub repository that is going to +support only two platforms - `cp39_osx_aarch64` and `cp39_linux_x86_64` - and it +will only use `wheels` and ignore any sdists that it may find on the PyPI- compatible indexes. :::{warning} @@ -162,7 +162,7 @@ multiple times. ::: :::{note} -This will only work for wheel-only setups, i.e. all of your dependencies need to have wheels +This will only work for wheel-only setups, i.e., all of your dependencies need to have wheels available on the PyPI index that you use. ::: @@ -173,9 +173,9 @@ Currently this is disabled by default, but you can turn it on using {envvar}`RULES_PYTHON_ENABLE_PIPSTAR` environment variable. ::: -In order to understand what dependencies to pull for a particular package +In order to understand what dependencies to pull for a particular package, `rules_python` parses the `whl` file [`METADATA`][metadata]. -Packages can express dependencies via `Requires-Dist` and they can add conditions using +Packages can express dependencies via `Requires-Dist`, and they can add conditions using "environment markers", which represent the Python version, OS, etc. While the PyPI integration provides reasonable defaults to support most @@ -198,8 +198,8 @@ additional keys, which become available during dependency evaluation. ### Bazel downloader and multi-platform wheel hub repository. :::{warning} -This is currently still experimental and whilst it has been proven to work in quite a few -environments, the APIs are still being finalized and there may be changes to the APIs for this +This is currently still experimental, and whilst it has been proven to work in quite a few +environments, the APIs are still being finalized, and there may be changes to the APIs for this feature without much notice. The issues that you can subscribe to for updates are: @@ -207,7 +207,7 @@ The issues that you can subscribe to for updates are: * {gh-issue}`1357` ::: -The {obj}`pip` extension supports pulling information from `PyPI` (or a compatible mirror) and it +The {obj}`pip` extension supports pulling information from `PyPI` (or a compatible mirror), and it will ensure that the [bazel downloader][bazel_downloader] is used for downloading the wheels. This provides the following benefits: @@ -222,7 +222,7 @@ To enable the feature specify {attr}`pip.parse.experimental_index_url` as shown the {gh-path}`examples/bzlmod/MODULE.bazel` example. Similar to [uv](https://docs.astral.sh/uv/configuration/indexes/), one can override the -index that is used for a single package. By default we first search in the index specified by +index that is used for a single package. By default, we first search in the index specified by {attr}`pip.parse.experimental_index_url`, then we iterate through the {attr}`pip.parse.experimental_extra_index_urls` unless there are overrides specified via {attr}`pip.parse.experimental_index_url_overrides`. @@ -235,12 +235,12 @@ Loading: 0 packages loaded ``` -This does not mean that `rules_python` is fetching the wheels eagerly, but it -rather means that it is calling the PyPI server to get the Simple API response +This does not mean that `rules_python` is fetching the wheels eagerly; rather, +it means that it is calling the PyPI server to get the Simple API response to get the list of all available source and wheel distributions. Once it has -got all of the available distributions, it will select the right ones depending +gotten all of the available distributions, it will select the right ones depending on the `sha256` values in your `requirements_lock.txt` file. If `sha256` hashes -are not present in the requirements file, we will fallback to matching by version +are not present in the requirements file, we will fall back to matching by version specified in the lock file. Fetching the distribution information from the PyPI allows `rules_python` to @@ -264,10 +264,10 @@ available flags: The [Bazel downloader](#bazel-downloader) usage allows for the Bazel [Credential Helper][cred-helper-design]. -Your python artifact registry may provide a credential helper for you. +Your Python artifact registry may provide a credential helper for you. Refer to your index's docs to see if one is provided. -The simplest form of a credential helper is a bash script that accepts an arg and spits out JSON to +The simplest form of a credential helper is a bash script that accepts an argument and spits out JSON to stdout. For a service like Google Artifact Registry that uses ['Basic' HTTP Auth][rfc7617] and does not provide a credential helper that conforms to the [spec][cred-helper-spec], the script might look like: @@ -285,7 +285,7 @@ echo ' }' echo '}' ``` -Configure Bazel to use this credential helper for your python index `example.com`: +Configure Bazel to use this credential helper for your Python index `example.com`: ``` # .bazelrc diff --git a/docs/pypi/index.md b/docs/pypi/index.md index c300124398..c32bafc609 100644 --- a/docs/pypi/index.md +++ b/docs/pypi/index.md @@ -3,11 +3,11 @@ # Using PyPI -Using PyPI packages (aka "pip install") involves the following main steps. +Using PyPI packages (aka "pip install") involves the following main steps: 1. [Generating requirements file](./lock) -2. Installing third party packages in [bzlmod](./download) or [WORKSPACE](./download-workspace). -3. [Using third party packages as dependencies](./use) +2. Installing third-party packages in [bzlmod](./download) or [WORKSPACE](./download-workspace). +3. [Using third-party packages as dependencies](./use) With the advanced topics covered separately: * Dealing with [circular dependencies](./circular-dependencies). diff --git a/docs/pypi/lock.md b/docs/pypi/lock.md index c9376036fb..db557fe594 100644 --- a/docs/pypi/lock.md +++ b/docs/pypi/lock.md @@ -11,9 +11,14 @@ Currently `rules_python` only supports `requirements.txt` format. ### pip compile -Generally, when working on a Python project, you'll have some dependencies that themselves have other dependencies. You might also specify dependency bounds instead of specific versions. So you'll need to generate a full list of all transitive dependencies and pinned versions for every dependency. - -Typically, you'd have your project dependencies specified in `pyproject.toml` or `requirements.in` and generate the full pinned list of dependencies in `requirements_lock.txt`, which you can manage with the {obj}`compile_pip_requirements`: +Generally, when working on a Python project, you'll have some dependencies that themselves have +other dependencies. You might also specify dependency bounds instead of specific versions. +So you'll need to generate a full list of all transitive dependencies and pinned versions +for every dependency. + +Typically, you'd have your project dependencies specified in `pyproject.toml` or `requirements.in` +and generate the full pinned list of dependencies in `requirements_lock.txt`, which you can +manage with {obj}`compile_pip_requirements`: ```starlark load("@rules_python//python:pip.bzl", "compile_pip_requirements") diff --git a/docs/pypi/patch.md b/docs/pypi/patch.md index f341bd1091..7e3cb41981 100644 --- a/docs/pypi/patch.md +++ b/docs/pypi/patch.md @@ -4,7 +4,7 @@ # Patching wheels Sometimes the wheels have to be patched to: -* Workaround the lack of a standard `site-packages` layout ({gh-issue}`2156`) -* Include certain PRs of your choice on top of wheels and avoid building from sdist, +* Workaround the lack of a standard `site-packages` layout ({gh-issue}`2156`). +* Include certain PRs of your choice on top of wheels and avoid building from sdist. You can patch the wheels by using the {attr}`pip.override.patches` attribute. diff --git a/docs/pypi/use.md b/docs/pypi/use.md index 7a16b7d9e9..6212097f86 100644 --- a/docs/pypi/use.md +++ b/docs/pypi/use.md @@ -3,10 +3,10 @@ # Use in BUILD.bazel files -Once you have setup the dependencies, you are ready to start using them in your `BUILD.bazel` -files. If you haven't done so yet, set it up by following the following docs: +Once you have set up the dependencies, you are ready to start using them in your `BUILD.bazel` +files. If you haven't done so yet, set it up by following these docs: 1. [WORKSPACE](./download-workspace) -1. [bzlmod](./download) +2. [bzlmod](./download) To refer to targets in a hub repo `pypi`, you can do one of two things: ```starlark @@ -29,19 +29,19 @@ py_library( ) ``` -Note, that the usage of the `requirement` helper is not advised and can be problematic. See the +Note that the usage of the `requirement` helper is not advised and can be problematic. See the [notes below](#requirement-helper). -Note, that the hub repo contains the following targets for each package: -* `@pypi//numpy` which is a shorthand for `@pypi//numpy:numpy`. This is an {obj}`alias` to +Note that the hub repo contains the following targets for each package: +* `@pypi//numpy` - shorthand for `@pypi//numpy:numpy`. This is an {obj}`alias` to `@pypi//numpy:pkg`. * `@pypi//numpy:pkg` - the {obj}`py_library` target automatically generated by the repository rules. -* `@pypi//numpy:data` - the {obj}`filegroup` that is for all of the extra files that are included +* `@pypi//numpy:data` - the {obj}`filegroup` for all of the extra files that are included as data in the `pkg` target. -* `@pypi//numpy:dist_info` - the {obj}`filegroup` that is for all of the files in the `.distinfo` directory. -* `@pypi//numpy:whl` - the {obj}`filegroup` that is the `.whl` file itself which includes all of - the transitive dependencies via the {attr}`filegroup.data` attribute. +* `@pypi//numpy:dist_info` - the {obj}`filegroup` for all of the files in the `.distinfo` directory. +* `@pypi//numpy:whl` - the {obj}`filegroup` that is the `.whl` file itself, which includes all + transitive dependencies via the {attr}`filegroup.data` attribute. ## Entry points @@ -52,14 +52,14 @@ which can help you create a `py_binary` target for a particular console script e ## 'Extras' dependencies -Any 'extras' specified in the requirements lock file will be automatically added +Any "extras" specified in the requirements lock file will be automatically added as transitive dependencies of the package. In the example above, you'd just put `requirement("useful_dep")` or `@pypi//useful_dep`. ## Consuming Wheel Dists Directly -If you need to depend on the wheel dists themselves, for instance, to pass them -to some other packaging tool, you can get a handle to them with the +If you need to depend on the wheel dists themselves (for instance, to pass them +to some other packaging tool), you can get a handle to them with the `whl_requirement` macro. For example: ```starlark @@ -77,7 +77,7 @@ filegroup( ## Creating a filegroup of files within a whl The rule {obj}`whl_filegroup` exists as an easy way to extract the necessary files -from a whl file without the need to modify the `BUILD.bazel` contents of the +from a whl file without needing to modify the `BUILD.bazel` contents of the whl repositories generated via `pip_repository`. Use it similarly to the `filegroup` above. See the API docs for more information. @@ -104,16 +104,16 @@ py_library( ) ``` -The reason `requirement()` exists is to insulate from +The reason `requirement()` exists is to insulate users from changes to the underlying repository and label strings. However, those -labels have become directly used, so aren't able to easily change regardless. +labels have become directly used, so they aren't able to easily change regardless. -On the other hand, using `requirement()` helper has several drawbacks: +On the other hand, using the `requirement()` helper has several drawbacks: -- It doesn't work with `buildifier` -- It doesn't work with `buildozer` -- It adds extra layer on top of normal mechanisms to refer to targets. -- It does not scale well as each type of target needs a new macro to be loaded and imported. +- It doesn't work with `buildifier`. +- It doesn't work with `buildozer`. +- It adds an extra layer on top of normal mechanisms to refer to targets. +- It does not scale well, as each type of target needs a new macro to be loaded and imported. If you don't want to use `requirement()`, you can use the library labels directly instead. For `pip_parse`, the labels are of the following form: diff --git a/docs/repl.md b/docs/repl.md index edcf37e811..1434097fdf 100644 --- a/docs/repl.md +++ b/docs/repl.md @@ -1,6 +1,6 @@ # Getting a REPL or Interactive Shell -rules_python provides a REPL to help with debugging and developing. The goal of +`rules_python` provides a REPL to help with debugging and developing. The goal of the REPL is to present an environment identical to what a {bzl:obj}`py_binary` creates for your code. diff --git a/docs/support.md b/docs/support.md index 5e6de57fcb..ad943b3845 100644 --- a/docs/support.md +++ b/docs/support.md @@ -8,7 +8,7 @@ page for information on our development workflow. ## Supported rules_python Versions In general, only the latest version is supported. Backporting changes is -done on a best effort basis based on severity, risk of regressions, and +done on a best-effort basis based on severity, risk of regressions, and the willingness of volunteers. If you want or need particular functionality backported, then the best way @@ -33,24 +33,24 @@ for what versions are the rolling, active, and prior releases. ## Supported Python versions -As a general rule we test all released non-EOL Python versions. Different +As a general rule, we test all released non-EOL Python versions. Different interpreter versions may work but are not guaranteed. We are interested in staying compatible with upcoming unreleased versions, so if you see that things stop working, please create tickets or, more preferably, pull requests. ## Supported Platforms -We only support the platforms that our continuous integration jobs run, which -is Linux, Mac, and Windows. +We only support the platforms that our continuous integration jobs run on, which +are Linux, Mac, and Windows. -In order to better describe different support levels, the below acts as a rough +In order to better describe different support levels, the following acts as a rough guideline for different platform tiers: -* Tier 0 - The platforms that our CI runs: `linux_x86_64`, `osx_x86_64`, `RBE linux_x86_64`. -* Tier 1 - The platforms that are similar enough to what the CI runs: `linux_aarch64`, `osx_arm64`. - What is more, `windows_x86_64` is in this list as we run tests in CI but - developing for Windows is more challenging and features may come later to +* Tier 0 - The platforms that our CI runs on: `linux_x86_64`, `osx_x86_64`, `RBE linux_x86_64`. +* Tier 1 - The platforms that are similar enough to what the CI runs on: `linux_aarch64`, `osx_arm64`. + What is more, `windows_x86_64` is in this list, as we run tests in CI, but + developing for Windows is more challenging, and features may come later to this platform. -* Tier 2 - The rest of the platforms that may have varying level of support, e.g. +* Tier 2 - The rest of the platforms that may have a varying level of support, e.g., `linux_s390x`, `linux_ppc64le`, `windows_arm64`. :::{note} @@ -75,7 +75,7 @@ a series of releases to so users can still incrementally upgrade. See the ## Experimental Features -An experimental features is functionality that may not be ready for general +An experimental feature is functionality that may not be ready for general use and may change quickly and/or significantly. Such features are denoted in their name or API docs as "experimental". They may have breaking changes made at any time. diff --git a/docs/toolchains.md b/docs/toolchains.md index 668a458156..de819cb515 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -4,13 +4,13 @@ (configuring-toolchains)= # Configuring Python toolchains and runtimes -This documents how to configure the Python toolchain and runtimes for different +This document explains how to configure the Python toolchain and runtimes for different use cases. ## Bzlmod MODULE configuration -How to configure `rules_python` in your MODULE.bazel file depends on how and why -you're using Python. There are 4 basic use cases: +How to configure `rules_python` in your `MODULE.bazel` file depends on how and why +you're using Python. There are four basic use cases: 1. A root module that always uses Python. For example, you're building a Python application. @@ -51,7 +51,7 @@ python.toolchain(python_version = "3.12") ### Library modules A library module is a module that can show up in arbitrary locations in the -bzlmod module graph -- it's unknown where in the breadth-first search order the +Bzlmod module graph -- it's unknown where in the breadth-first search order the module will be relative to other modules. For example, `rules_python` is a library module. @@ -84,9 +84,9 @@ used for the Python programs it runs isn't chosen by the module itself. Instead, it's up to the root module to pick an appropriate version of Python. For this case, configuration is simple: just depend on `rules_python` and use -the normal `//python:py_binary.bzl` et al rules. There is no need to call -`python.toolchain` -- rules_python ensures _some_ Python version is available, -but more often the root module will specify some version. +the normal `//python:py_binary.bzl` et al. rules. There is no need to call +`python.toolchain` -- `rules_python` ensures _some_ Python version is available, +but more often, the root module will specify some version. ``` # MODULE.bazel @@ -108,7 +108,7 @@ specific Python version be used with its tools. This has some pros/cons: * It has higher build overhead because additional runtimes and libraries need to be downloaded, and Bazel has to keep additional configuration state. -To configure this, request the Python versions needed in MODULE.bazel and use +To configure this, request the Python versions needed in `MODULE.bazel` and use the version-aware rules for `py_binary`. ``` @@ -132,7 +132,7 @@ is most useful for two cases: 1. For submodules to ensure they run with the appropriate Python version 2. To allow incremental, per-target, upgrading to newer Python versions, - typically in a mono-repo situation. + typically in a monorepo situation. To configure a submodule with the version-aware rules, request the particular version you need when defining the toolchain: @@ -147,7 +147,7 @@ python.toolchain( use_repo(python) ``` -Then use the `@rules_python` repo in your BUILD file to explicity pin the Python version when calling the rule: +Then use the `@rules_python` repo in your `BUILD` file to explicitly pin the Python version when calling the rule: ```starlark # BUILD.bazel @@ -202,29 +202,29 @@ The `python.toolchain()` call makes its contents available under a repo named `python_X_Y`, where X and Y are the major and minor versions. For example, `python.toolchain(python_version="3.11")` creates the repo `@python_3_11`. Remember to call `use_repo()` to make repos visible to your module: -`use_repo(python, "python_3_11")` +`use_repo(python, "python_3_11")`. :::{deprecated} 1.1.0 -The toolchain specific `py_binary` and `py_test` symbols are aliases to the regular rules. -i.e. Deprecated `load("@python_versions//3.11:defs.bzl", "py_binary")` & `load("@python_versions//3.11:defs.bzl", "py_test")` +The toolchain-specific `py_binary` and `py_test` symbols are aliases to the regular rules. +For example, `load("@python_versions//3.11:defs.bzl", "py_binary")` & `load("@python_versions//3.11:defs.bzl", "py_test")` are deprecated. -Usages of them should be changed to load the regular rules directly; -i.e. Use `load("@rules_python//python:py_binary.bzl", "py_binary")` & `load("@rules_python//python:py_test.bzl", "py_test")` and then specify the `python_version` when using the rules corresponding to the python version you defined in your toolchain. {ref}`Library modules with version constraints` +Usages of them should be changed to load the regular rules directly. +For example, use `load("@rules_python//python:py_binary.bzl", "py_binary")` & `load("@rules_python//python:py_test.bzl", "py_test")` and then specify the `python_version` when using the rules corresponding to the Python version you defined in your toolchain. {ref}`Library modules with version constraints` ::: #### Toolchain usage in other rules -Python toolchains can be utilized in other bazel rules, such as `genrule()`, by +Python toolchains can be utilized in other Bazel rules, such as `genrule()`, by adding the `toolchains=["@rules_python//python:current_py_toolchain"]` attribute. You can obtain the path to the Python interpreter using the `$(PYTHON2)` and `$(PYTHON3)` ["Make" Variables](https://bazel.build/reference/be/make-variables). See the {gh-path}`test_current_py_toolchain ` target -for an example. We also make available `$(PYTHON2_ROOTPATH)` and `$(PYTHON3_ROOTPATH)` +for an example. We also make available `$(PYTHON2_ROOTPATH)` and `$(PYTHON3_ROOTPATH)`, which are Make Variable equivalents of `$(PYTHON2)` and `$(PYTHON3)` but for runfiles -locations. These will be helpful if you need to set env vars of binary/test rules +locations. These will be helpful if you need to set environment variables of binary/test rules while using [`--nolegacy_external_runfiles`](https://bazel.build/reference/command-line-reference#flag--legacy_external_runfiles). The original make variables still work in exec contexts such as genrules. @@ -246,9 +246,9 @@ existing attributes: ### Registering custom runtimes Because the python-build-standalone project has _thousands_ of prebuilt runtimes -available, rules_python only includes popular runtimes in its built in +available, `rules_python` only includes popular runtimes in its built-in configurations. If you want to use a runtime that isn't already known to -rules_python then {obj}`single_version_platform_override()` can be used to do +`rules_python`, then {obj}`single_version_platform_override()` can be used to do so. In short, it allows specifying an arbitrary URL and using custom flags to control when a runtime is used. @@ -287,21 +287,21 @@ config_setting( ``` Notes: -- While any URL and archive can be used, it's assumed their content looks how - a python-build-standalone archive looks. -- A "version aware" toolchain is registered, which means the Python version flag - must also match (e.g. `--@rules_python//python/config_settings:python_version=3.13.3` +- While any URL and archive can be used, it's assumed their content looks like + a python-build-standalone archive. +- A "version-aware" toolchain is registered, which means the Python version flag + must also match (e.g., `--@rules_python//python/config_settings:python_version=3.13.3` must be set -- see `minor_mapping` and `is_default` for controls and docs about version matching and selection). - The `target_compatible_with` attribute can be used to entirely specify the - arg of the same name the toolchain uses. + argument of the same name that the toolchain uses. - The labels in `target_settings` must be absolute; `@@` refers to the main repo. - The `target_settings` are `config_setting` targets, which means you can customize how matching occurs. :::{seealso} -See {obj}`//python/config_settings` for flags rules_python already defines -that can be used with `target_settings`. Some particular ones of note are: +See {obj}`//python/config_settings` for flags `rules_python` already defines +that can be used with `target_settings`. Some particular ones of note are {flag}`--py_linux_libc` and {flag}`--py_freethreaded`, among others. ::: @@ -312,7 +312,7 @@ Added support for custom platform names, `target_compatible_with`, and ### Using defined toolchains from WORKSPACE -It is possible to use toolchains defined in `MODULE.bazel` in `WORKSPACE`. For example +It is possible to use toolchains defined in `MODULE.bazel` in `WORKSPACE`. For example, the following `MODULE.bazel` and `WORKSPACE` provides a working {bzl:obj}`pip_parse` setup: ```starlark # File: WORKSPACE @@ -343,16 +343,16 @@ python.toolchain(python_version = "3.10") use_repo(python, "python_3_10", "python_3_10_host") ``` -Note, the user has to import the `*_host` repository to use the python interpreter in the -{bzl:obj}`pip_parse` and `whl_library` repository rules and once that is done +Note, the user has to import the `*_host` repository to use the Python interpreter in the +{bzl:obj}`pip_parse` and `whl_library` repository rules, and once that is done, users should be able to ensure the setting of the default toolchain even during the transition period when some of the code is still defined in `WORKSPACE`. ## Workspace configuration -To import rules_python in your project, you first need to add it to your +To import `rules_python` in your project, you first need to add it to your `WORKSPACE` file, using the snippet provided in the -[release you choose](https://github.com/bazel-contrib/rules_python/releases) +[release you choose](https://github.com/bazel-contrib/rules_python/releases). To depend on a particular unreleased version, you can do the following: @@ -403,15 +403,15 @@ pip_parse( ``` After registration, your Python targets will use the toolchain's interpreter during execution, but a system-installed interpreter -is still used to 'bootstrap' Python targets (see https://github.com/bazel-contrib/rules_python/issues/691). +is still used to "bootstrap" Python targets (see https://github.com/bazel-contrib/rules_python/issues/691). You may also find some quirks while using this toolchain. Please refer to [python-build-standalone documentation's _Quirks_ section](https://gregoryszorc.com/docs/python-build-standalone/main/quirks.html). ## Local toolchain It's possible to use a locally installed Python runtime instead of the regular prebuilt, remotely downloaded ones. A local toolchain contains the Python -runtime metadata (Python version, headers, ABI flags, etc) that the regular -remotely downloaded runtimes contain, which makes it possible to build e.g. C +runtime metadata (Python version, headers, ABI flags, etc.) that the regular +remotely downloaded runtimes contain, which makes it possible to build, e.g., C extensions (unlike the autodetecting and runtime environment toolchains). For simple cases, the {obj}`local_runtime_repo` and @@ -420,10 +420,10 @@ Python installation and create an appropriate Bazel definition from it. To do this, three pieces need to be wired together: 1. Specify a path or command to a Python interpreter (multiple can be defined). -2. Create toolchains for the runtimes in (1) -3. Register the toolchains created by (2) +2. Create toolchains for the runtimes in (1). +3. Register the toolchains created by (2). -The below is an example that will use `python3` from PATH to find the +The following is an example that will use `python3` from `PATH` to find the interpreter, then introspect its installation to generate a full toolchain. ```starlark @@ -474,7 +474,7 @@ Python versions and/or platforms to be configured in a single `MODULE.bazel`. Note that `register_toolchains` will insert the local toolchain earlier in the toolchain ordering, so it will take precedence over other registered toolchains. To better control when the toolchain is used, see [Conditionally using local -toolchains] +toolchains]. ### Conditionally using local toolchains @@ -483,22 +483,22 @@ ordering, which means it will usually be used no matter what. This can be problematic for CI (where it shouldn't be used), expensive for CI (CI must initialize/download the repository to determine its Python version), and annoying for iterative development (enabling/disabling it requires modifying -MODULE.bazel). +`MODULE.bazel`). These behaviors can be mitigated, but it requires additional configuration -to avoid triggering the local toolchain repository to initialize (i.e. run +to avoid triggering the local toolchain repository to initialize (i.e., run local commands and perform downloads). The two settings to change are {obj}`local_runtime_toolchains_repo.target_compatible_with` and {obj}`local_runtime_toolchains_repo.target_settings`, which control how Bazel decides if a toolchain should match. By default, they point to targets *within* -the local runtime repository (trigger repo initialization). We have to override +the local runtime repository (triggering repo initialization). We have to override them to *not* reference the local runtime repository at all. In the example below, we reconfigure the local toolchains so they are only activated if the custom flag `--//:py=local` is set and the target platform -matches the Bazel host platform. The net effect is CI won't use the local +matches the Bazel host platform. The net effect is that CI won't use the local toolchain (nor initialize its repository), and developers can easily enable/disable the local toolchain with a command line flag. @@ -545,9 +545,9 @@ information about Python at build time. In particular, this means it is not able to build C extensions -- doing so requires knowing, at build time, what Python headers to use. -In effect, all it does is generate a small wrapper script that simply calls e.g. +In effect, all it does is generate a small wrapper script that simply calls, e.g., `/usr/bin/env python3` to run a program. This makes it easy to change what -Python is used to run a program, but also makes it easy to use a Python version +Python is used to run a program but also makes it easy to use a Python version that isn't compatible with build-time assumptions. ``` @@ -565,26 +565,26 @@ locally installed Python. ### Autodetecting toolchain The autodetecting toolchain is a deprecated toolchain that is built into Bazel. -**It's name is a bit misleading: it doesn't autodetect anything**. All it does is +**Its name is a bit misleading: it doesn't autodetect anything.** All it does is use `python3` from the environment a binary runs within. This provides extremely limited functionality to the rules (at build time, nothing is knowable about the Python runtime). Bazel itself automatically registers `@bazel_tools//tools/python:autodetecting_toolchain` -as the lowest priority toolchain. For WORKSPACE builds, if no other toolchain -is registered, that toolchain will be used. For bzlmod builds, rules_python +as the lowest priority toolchain. For `WORKSPACE` builds, if no other toolchain +is registered, that toolchain will be used. For Bzlmod builds, `rules_python` automatically registers a higher-priority toolchain; it won't be used unless there is a toolchain misconfiguration somewhere. -To aid migration off the Bazel-builtin toolchain, rules_python provides +To aid migration off the Bazel-builtin toolchain, `rules_python` provides {bzl:obj}`@rules_python//python/runtime_env_toolchains:all`. This is an equivalent -toolchain, but is implemented using rules_python's objects. +toolchain but is implemented using `rules_python`'s objects. ## Custom toolchains -While rules_python provides toolchains by default, it is not required to use +While `rules_python` provides toolchains by default, it is not required to use them, and you can define your own toolchains to use instead. This section -gives an introduction for how to define them yourself. +gives an introduction to how to define them yourself. :::{note} * Defining your own toolchains is an advanced feature. @@ -599,7 +599,7 @@ toolchains a "toolchain suite". One of the underlying design goals of the toolchains is to support complex and bespoke environments. Such environments may use an arbitrary combination of {bzl:obj}`RBE`, cross-platform building, multiple Python versions, -building Python from source, embeding Python (as opposed to building separate +building Python from source, embedding Python (as opposed to building separate interpreters), using prebuilt binaries, or using binaries built from source. To that end, many of the attributes they accept, and fields they provide, are optional. @@ -610,7 +610,7 @@ The target toolchain type is {obj}`//python:toolchain_type`, and it is for _target configuration_ runtime information, e.g., the Python version and interpreter binary that a program will use. -The is typically implemented using {obj}`py_runtime()`, which +This is typically implemented using {obj}`py_runtime()`, which provides the {obj}`PyRuntimeInfo` provider. For historical reasons from the Python 2 transition, `py_runtime` is wrapped in {obj}`py_runtime_pair`, which provides {obj}`ToolchainInfo` with the field `py3_runtime`, which is an @@ -625,7 +625,7 @@ set {external:bzl:obj}`toolchain.exec_compatible_with`. ### Python C toolchain type The Python C toolchain type ("py cc") is {obj}`//python/cc:toolchain_type`, and -it has C/C++ information for the _target configuration_, e.g. the C headers that +it has C/C++ information for the _target configuration_, e.g., the C headers that provide `Python.h`. This is typically implemented using {obj}`py_cc_toolchain()`, which provides @@ -642,7 +642,7 @@ set {external:bzl:obj}`toolchain.exec_compatible_with`. ### Exec tools toolchain type The exec tools toolchain type is {obj}`//python:exec_tools_toolchain_type`, -and it is for supporting tools for _building_ programs, e.g. the binary to +and it is for supporting tools for _building_ programs, e.g., the binary to precompile code at build time. This toolchain type is intended to hold only _exec configuration_ values -- @@ -661,7 +661,7 @@ target configuration (e.g. Python version), then for one to be chosen based on finding one compatible with the available host platforms to run the tool on. However, what `target_compatible_with`/`target_settings` and -`exec_compatible_with` values to use depend on details of the tools being used. +`exec_compatible_with` values to use depends on the details of the tools being used. For example: * If you had a precompiler that supported any version of Python, then putting the Python version in `target_settings` is unnecessary. @@ -672,9 +672,9 @@ This can work because, when the rules invoke these build tools, they pass along all necessary information so that the tool can be entirely independent of the target configuration being built for. -Alternatively, if you had a precompiler that only ran on linux, and only -produced valid output for programs intended to run on linux, then _both_ -`exec_compatible_with` and `target_compatible_with` must be set to linux. +Alternatively, if you had a precompiler that only ran on Linux and only +produced valid output for programs intended to run on Linux, then _both_ +`exec_compatible_with` and `target_compatible_with` must be set to Linux. ### Custom toolchain example @@ -684,9 +684,9 @@ Here, we show an example for a semi-complicated toolchain suite, one that is: * For Python version 3.12.0 * Using an in-build interpreter built from source * That only runs on Linux -* Using a prebuilt precompiler that only runs on Linux, and only produces byte - code valid for 3.12 -* With the exec tools interpreter disabled (unnecessary with a prebuild +* Using a prebuilt precompiler that only runs on Linux and only produces + bytecode valid for 3.12 +* With the exec tools interpreter disabled (unnecessary with a prebuilt precompiler) * Providing C headers and libraries @@ -748,13 +748,13 @@ toolchain( name = "runtime_toolchain", toolchain = "//toolchain_impl:runtime_pair", toolchain_type = "@rules_python//python:toolchain_type", - target_compatible_with = ["@platforms/os:linux"] + target_compatible_with = ["@platforms/os:linux"], ) toolchain( name = "py_cc_toolchain", toolchain = "//toolchain_impl:py_cc_toolchain_impl", toolchain_type = "@rules_python//python/cc:toolchain_type", - target_compatible_with = ["@platforms/os:linux"] + target_compatible_with = ["@platforms/os:linux"], ) toolchain( @@ -764,19 +764,19 @@ toolchain( target_settings = [ "@rules_python//python/config_settings:is_python_3.12", ], - exec_comaptible_with = ["@platforms/os:linux"] + exec_compatible_with = ["@platforms/os:linux"], ) # ----------------------------------------------- # File: MODULE.bazel or WORKSPACE.bazel -# These toolchains will considered before others. +# These toolchains will be considered before others. # ----------------------------------------------- register_toolchains("//toolchains:all") ``` -When registering custom toolchains, be aware of the the [toolchain registration +When registering custom toolchains, be aware of the [toolchain registration order](https://bazel.build/extending/toolchains#toolchain-resolution). In brief, -toolchain order is the BFS-order of the modules; see the bazel docs for a more +toolchain order is the BFS-order of the modules; see the Bazel docs for a more detailed description. :::{note} @@ -796,7 +796,7 @@ Currently the following flags are used to influence toolchain selection: To run the interpreter that Bazel will use, you can use the `@rules_python//python/bin:python` target. This is a binary target with -the executable pointing at the `python3` binary plus its relevent runfiles. +the executable pointing at the `python3` binary plus its relevant runfiles. ```console $ bazel run @rules_python//python/bin:python @@ -838,7 +838,7 @@ targets on its own. Please file a feature request if this is desired. The `//python/bin:python` target provides access to the underlying interpreter without any hermeticity guarantees. -The [`//python/bin:repl` target](repl) provides an environment indentical to +The [`//python/bin:repl` target](repl) provides an environment identical to what `py_binary` provides. That means it handles things like the [`PYTHONSAFEPATH`](https://docs.python.org/3/using/cmdline.html#envvar-PYTHONSAFEPATH) environment variable automatically. The `//python/bin:python` target will not. From 036e8c5af1258cf1a0b318a51c75f88ea4c93f11 Mon Sep 17 00:00:00 2001 From: yushan26 <107004874+yushan26@users.noreply.github.com> Date: Sat, 21 Jun 2025 19:01:39 -0700 Subject: [PATCH 155/156] feat(gazelle): For package mode, resolve dependencies when imports are relative to the package path (#2865) When `# gazelle:python_generation_mode package` is enabled, relative imports are currently not being added to the `deps` field of the generated target. For example, given the following Python code: ``` from .library import add as _add from .library import divide as _divide from .library import multiply as _multiply from .library import subtract as _subtract ``` The expected py_library rule should include a dependency on the local library package: ``` py_library( name = "py_default_library", srcs = ["__init__.py"], visibility = ["//visibility:public"], deps = [ "//example/library:py_default_library", ], ) ``` However, the actual generated rule is missing the deps entry: ``` py_library( name = "py_default_library", srcs = ["__init__.py"], visibility = ["//visibility:public"], ) ``` This change updates file_parser.go to ensure that relative imports (those starting with a .) are parsed and preserved. In `Resolve()`, logic is added to correctly interpret relative paths: A single dot (.) refers to the current package. Multiple dots (.., ..., etc.) traverse up parent directories. The relative import is resolved against the current label.Pkg path that imports the module and converted into an path relative to the root before dependency resolution. As a result, dependencies for relative imports are now correctly added to the deps field in package generation mode. Added a directive `# gazelle:experimental_allow_relative_imports true` to allow this feature to be opt in. --------- Co-authored-by: yushan Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Co-authored-by: Douglas Thor --- CHANGELOG.md | 3 ++ gazelle/README.md | 48 +++++++++++++++-- gazelle/python/configure.go | 8 +++ gazelle/python/file_parser.go | 4 +- gazelle/python/resolve.go | 53 ++++++++++++++++++- .../testdata/relative_imports/README.md | 4 -- .../relative_imports_package_mode/BUILD.in | 2 + .../relative_imports_package_mode/BUILD.out | 15 ++++++ .../relative_imports_package_mode/README.md | 6 +++ .../WORKSPACE | 0 .../relative_imports_package_mode/__main__.py | 5 ++ .../package1}/BUILD.in | 0 .../package1/BUILD.out | 11 ++++ .../package1/__init__.py | 2 + .../package1/module1.py | 0 .../package1/module2.py | 0 .../package1/my_library/BUILD.in | 7 +++ .../package1/my_library/BUILD.out | 7 +++ .../package1/my_library/__init__.py | 2 + .../package1/my_library/foo/BUILD.in | 0 .../package1/my_library/foo/BUILD.out | 7 +++ .../package1/my_library/foo/__init__.py | 2 + .../package1/subpackage1/BUILD.in | 10 ++++ .../package1/subpackage1/BUILD.out | 10 ++++ .../package1/subpackage1/__init__.py | 3 ++ .../package1/subpackage1/some_module.py | 3 ++ .../package1/subpackage1/subpackage2/BUILD.in | 0 .../subpackage1/subpackage2/BUILD.out | 16 ++++++ .../subpackage1/subpackage2/__init__.py | 0 .../subpackage1/subpackage2/library/BUILD.in | 0 .../subpackage1/subpackage2/library/BUILD.out | 7 +++ .../subpackage2/library/other_module.py | 0 .../subpackage1/subpackage2/script.py | 11 ++++ .../package2/BUILD.in | 0 .../package2/BUILD.out | 12 +++++ .../package2/__init__.py | 20 +++++++ .../package2/library/BUILD.in | 0 .../package2/library/BUILD.out | 7 +++ .../package2/library/__init__.py | 14 +++++ .../package2/module3.py | 5 ++ .../package2/module4.py | 2 + .../test.yaml | 0 .../BUILD.in | 1 + .../BUILD.out | 7 +-- .../relative_imports_project_mode/README.md | 5 ++ .../relative_imports_project_mode/WORKSPACE | 1 + .../__main__.py | 0 .../package1/module1.py | 19 +++++++ .../package1/module2.py | 17 ++++++ .../package2/BUILD.in | 0 .../package2/BUILD.out | 0 .../package2/__init__.py | 0 .../package2/module3.py | 0 .../package2/module4.py | 0 .../package2/subpackage1/module5.py | 0 .../relative_imports_project_mode/test.yaml | 15 ++++++ gazelle/pythonconfig/pythonconfig.go | 16 ++++++ 57 files changed, 373 insertions(+), 14 deletions(-) delete mode 100644 gazelle/python/testdata/relative_imports/README.md create mode 100644 gazelle/python/testdata/relative_imports_package_mode/BUILD.in create mode 100644 gazelle/python/testdata/relative_imports_package_mode/BUILD.out create mode 100644 gazelle/python/testdata/relative_imports_package_mode/README.md rename gazelle/python/testdata/{relative_imports => relative_imports_package_mode}/WORKSPACE (100%) create mode 100644 gazelle/python/testdata/relative_imports_package_mode/__main__.py rename gazelle/python/testdata/{relative_imports/package2 => relative_imports_package_mode/package1}/BUILD.in (100%) create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/BUILD.out create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/__init__.py rename gazelle/python/testdata/{relative_imports => relative_imports_package_mode}/package1/module1.py (100%) rename gazelle/python/testdata/{relative_imports => relative_imports_package_mode}/package1/module2.py (100%) create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/my_library/BUILD.in create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/my_library/BUILD.out create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/my_library/__init__.py create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/my_library/foo/BUILD.in create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/my_library/foo/BUILD.out create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/my_library/foo/__init__.py create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/BUILD.in create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/BUILD.out create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/__init__.py create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/some_module.py create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/BUILD.in create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/BUILD.out create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/__init__.py create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/library/BUILD.in create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/library/BUILD.out create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/library/other_module.py create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/script.py create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package2/BUILD.in create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package2/BUILD.out create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package2/__init__.py create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package2/library/BUILD.in create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package2/library/BUILD.out create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package2/library/__init__.py create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package2/module3.py create mode 100644 gazelle/python/testdata/relative_imports_package_mode/package2/module4.py rename gazelle/python/testdata/{relative_imports => relative_imports_package_mode}/test.yaml (100%) rename gazelle/python/testdata/{relative_imports => relative_imports_project_mode}/BUILD.in (61%) rename gazelle/python/testdata/{relative_imports => relative_imports_project_mode}/BUILD.out (70%) create mode 100644 gazelle/python/testdata/relative_imports_project_mode/README.md create mode 100644 gazelle/python/testdata/relative_imports_project_mode/WORKSPACE rename gazelle/python/testdata/{relative_imports => relative_imports_project_mode}/__main__.py (100%) create mode 100644 gazelle/python/testdata/relative_imports_project_mode/package1/module1.py create mode 100644 gazelle/python/testdata/relative_imports_project_mode/package1/module2.py create mode 100644 gazelle/python/testdata/relative_imports_project_mode/package2/BUILD.in rename gazelle/python/testdata/{relative_imports => relative_imports_project_mode}/package2/BUILD.out (100%) rename gazelle/python/testdata/{relative_imports => relative_imports_project_mode}/package2/__init__.py (100%) rename gazelle/python/testdata/{relative_imports => relative_imports_project_mode}/package2/module3.py (100%) rename gazelle/python/testdata/{relative_imports => relative_imports_project_mode}/package2/module4.py (100%) rename gazelle/python/testdata/{relative_imports => relative_imports_project_mode}/package2/subpackage1/module5.py (100%) create mode 100644 gazelle/python/testdata/relative_imports_project_mode/test.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index f2fa98f73f..ecdc129502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,9 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-changed} ### Changed +* (gazelle) For package mode, resolve dependencies when imports are relative + to the package path. This is enabled via the + `# gazelle:experimental_allow_relative_imports` true directive ({gh-issue}`2203`). * (gazelle) Types for exposed members of `python.ParserOutput` are now all public. {#v0-0-0-fixed} diff --git a/gazelle/README.md b/gazelle/README.md index 89ebaef4cd..58ec55eb11 100644 --- a/gazelle/README.md +++ b/gazelle/README.md @@ -121,12 +121,12 @@ gazelle_python_manifest( requirements = "//:requirements_lock.txt", # include_stub_packages: bool (default: False) # If set to True, this flag automatically includes any corresponding type stub packages - # for the third-party libraries that are present and used. For example, if you have + # for the third-party libraries that are present and used. For example, if you have # `boto3` as a dependency, and this flag is enabled, the corresponding `boto3-stubs` # package will be automatically included in the BUILD file. # - # Enabling this feature helps ensure that type hints and stubs are readily available - # for tools like type checkers and IDEs, improving the development experience and + # Enabling this feature helps ensure that type hints and stubs are readily available + # for tools like type checkers and IDEs, improving the development experience and # reducing manual overhead in managing separate stub packages. include_stub_packages = True ) @@ -220,6 +220,8 @@ Python-specific directives are as follows: | Defines the format of the distribution name in labels to third-party deps. Useful for using Gazelle plugin with other rules with different repository conventions (e.g. `rules_pycross`). Full label is always prepended with (pip) repository name, e.g. `@pip//numpy`. | | `# gazelle:python_label_normalization` | `snake_case` | | Controls how distribution names in labels to third-party deps are normalized. Useful for using Gazelle plugin with other rules with different label conventions (e.g. `rules_pycross` uses PEP-503). Can be "snake_case", "none", or "pep503". | +| `# gazelle:experimental_allow_relative_imports` | `false` | +| Controls whether Gazelle resolves dependencies for import statements that use paths relative to the current package. Can be "true" or "false".| #### Directive: `python_root`: @@ -468,7 +470,7 @@ def py_test(name, main=None, **kwargs): name = "__test__", deps = ["@pip_pytest//:pkg"], # change this to the pytest target in your repo. ) - + deps.append(":__test__") main = ":__test__.py" @@ -581,6 +583,44 @@ deps = [ ] ``` +#### Directive: `experimental_allow_relative_imports` +Enables experimental support for resolving relative imports in +`python_generation_mode package`. + +By default, when `# gazelle:python_generation_mode package` is enabled, +relative imports (e.g., from .library import foo) are not added to the +deps field of the generated target. This results in incomplete py_library +rules that lack required dependencies on sibling packages. + +Example: +Given this Python file import: +```python +from .library import add as _add +from .library import subtract as _subtract +``` + +Expected BUILD file output: +```starlark +py_library( + name = "py_default_library", + srcs = ["__init__.py"], + deps = [ + "//example/library:py_default_library", + ], + visibility = ["//visibility:public"], +) +``` + +Actual output without this annotation: +```starlark +py_library( + name = "py_default_library", + srcs = ["__init__.py"], + visibility = ["//visibility:public"], +) +``` +If the directive is set to `true`, gazelle will resolve imports +that are relative to the current package. ### Libraries diff --git a/gazelle/python/configure.go b/gazelle/python/configure.go index a00b0ba0ba..ae0f7ee1d1 100644 --- a/gazelle/python/configure.go +++ b/gazelle/python/configure.go @@ -68,6 +68,7 @@ func (py *Configurer) KnownDirectives() []string { pythonconfig.TestFilePattern, pythonconfig.LabelConvention, pythonconfig.LabelNormalization, + pythonconfig.ExperimentalAllowRelativeImports, } } @@ -222,6 +223,13 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) { default: config.SetLabelNormalization(pythonconfig.DefaultLabelNormalizationType) } + case pythonconfig.ExperimentalAllowRelativeImports: + v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) + if err != nil { + log.Printf("invalid value for gazelle:%s in %q: %q", + pythonconfig.ExperimentalAllowRelativeImports, rel, d.Value) + } + config.SetExperimentalAllowRelativeImports(v) } } diff --git a/gazelle/python/file_parser.go b/gazelle/python/file_parser.go index 3f8363fbdf..cb82cb93b4 100644 --- a/gazelle/python/file_parser.go +++ b/gazelle/python/file_parser.go @@ -165,7 +165,9 @@ func (p *FileParser) parseImportStatements(node *sitter.Node) bool { } } else if node.Type() == sitterNodeTypeImportFromStatement { from := node.Child(1).Content(p.code) - if strings.HasPrefix(from, ".") { + // If the import is from the current package, we don't need to add it to the modules i.e. from . import Class1. + // If the import is from a different relative package i.e. from .package1 import foo, we need to add it to the modules. + if from == "." { return true } for j := 3; j < int(node.ChildCount()); j++ { diff --git a/gazelle/python/resolve.go b/gazelle/python/resolve.go index 996cbbadc0..413e69b289 100644 --- a/gazelle/python/resolve.go +++ b/gazelle/python/resolve.go @@ -148,12 +148,61 @@ func (py *Resolver) Resolve( modules := modulesRaw.(*treeset.Set) it := modules.Iterator() explainDependency := os.Getenv("EXPLAIN_DEPENDENCY") + // Resolve relative paths for package generation + isPackageGeneration := !cfg.PerFileGeneration() && !cfg.CoarseGrainedGeneration() hasFatalError := false MODULES_LOOP: for it.Next() { mod := it.Value().(Module) - moduleParts := strings.Split(mod.Name, ".") - possibleModules := []string{mod.Name} + moduleName := mod.Name + // Transform relative imports `.` or `..foo.bar` into the package path from root. + if strings.HasPrefix(mod.From, ".") { + if !cfg.ExperimentalAllowRelativeImports() || !isPackageGeneration { + continue MODULES_LOOP + } + + // Count number of leading dots in mod.From (e.g., ".." = 2, "...foo.bar" = 3) + relativeDepth := strings.IndexFunc(mod.From, func(r rune) bool { return r != '.' }) + if relativeDepth == -1 { + relativeDepth = len(mod.From) + } + + // Extract final symbol (e.g., "some_function") from mod.Name + imported := mod.Name + if idx := strings.LastIndex(mod.Name, "."); idx >= 0 { + imported = mod.Name[idx+1:] + } + + // Optional subpath in 'from' clause, e.g. "from ...my_library.foo import x" + fromPath := strings.TrimLeft(mod.From, ".") + var fromParts []string + if fromPath != "" { + fromParts = strings.Split(fromPath, ".") + } + + // Current Bazel package as path segments + pkgParts := strings.Split(from.Pkg, "/") + + if relativeDepth-1 > len(pkgParts) { + log.Printf("ERROR: Invalid relative import %q in %q: exceeds package root.", mod.Name, mod.Filepath) + continue MODULES_LOOP + } + + // Go up relativeDepth - 1 levels + baseParts := pkgParts + if relativeDepth > 1 { + baseParts = pkgParts[:len(pkgParts)-(relativeDepth-1)] + } + // Build absolute module path + absParts := append([]string{}, baseParts...) // base path + absParts = append(absParts, fromParts...) // subpath from 'from' + absParts = append(absParts, imported) // actual imported symbol + + moduleName = strings.Join(absParts, ".") + } + + moduleParts := strings.Split(moduleName, ".") + possibleModules := []string{moduleName} for len(moduleParts) > 1 { // Iterate back through the possible imports until // a match is found. diff --git a/gazelle/python/testdata/relative_imports/README.md b/gazelle/python/testdata/relative_imports/README.md deleted file mode 100644 index 1937cbcf4a..0000000000 --- a/gazelle/python/testdata/relative_imports/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Relative imports - -This test case asserts that the generated targets handle relative imports in -Python correctly. diff --git a/gazelle/python/testdata/relative_imports_package_mode/BUILD.in b/gazelle/python/testdata/relative_imports_package_mode/BUILD.in new file mode 100644 index 0000000000..78ef0a7863 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/BUILD.in @@ -0,0 +1,2 @@ +# gazelle:python_generation_mode package +# gazelle:experimental_allow_relative_imports true diff --git a/gazelle/python/testdata/relative_imports_package_mode/BUILD.out b/gazelle/python/testdata/relative_imports_package_mode/BUILD.out new file mode 100644 index 0000000000..f51b516cab --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/BUILD.out @@ -0,0 +1,15 @@ +load("@rules_python//python:defs.bzl", "py_binary") + +# gazelle:python_generation_mode package +# gazelle:experimental_allow_relative_imports true + +py_binary( + name = "relative_imports_package_mode_bin", + srcs = ["__main__.py"], + main = "__main__.py", + visibility = ["//:__subpackages__"], + deps = [ + "//package1", + "//package2", + ], +) diff --git a/gazelle/python/testdata/relative_imports_package_mode/README.md b/gazelle/python/testdata/relative_imports_package_mode/README.md new file mode 100644 index 0000000000..eb9f8c096c --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/README.md @@ -0,0 +1,6 @@ +# Resolve deps for relative imports + +This test case verifies that the generated targets correctly handle relative imports in +Python. Specifically, when the Python generation mode is set to "package," it ensures +that relative import statements such as from .foo import X are properly resolved to +their corresponding modules. diff --git a/gazelle/python/testdata/relative_imports/WORKSPACE b/gazelle/python/testdata/relative_imports_package_mode/WORKSPACE similarity index 100% rename from gazelle/python/testdata/relative_imports/WORKSPACE rename to gazelle/python/testdata/relative_imports_package_mode/WORKSPACE diff --git a/gazelle/python/testdata/relative_imports_package_mode/__main__.py b/gazelle/python/testdata/relative_imports_package_mode/__main__.py new file mode 100644 index 0000000000..4fb887a803 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/__main__.py @@ -0,0 +1,5 @@ +from package1.module1 import function1 +from package2.module3 import function3 + +print(function1()) +print(function3()) diff --git a/gazelle/python/testdata/relative_imports/package2/BUILD.in b/gazelle/python/testdata/relative_imports_package_mode/package1/BUILD.in similarity index 100% rename from gazelle/python/testdata/relative_imports/package2/BUILD.in rename to gazelle/python/testdata/relative_imports_package_mode/package1/BUILD.in diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/BUILD.out b/gazelle/python/testdata/relative_imports_package_mode/package1/BUILD.out new file mode 100644 index 0000000000..c562ff07de --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package1/BUILD.out @@ -0,0 +1,11 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "package1", + srcs = [ + "__init__.py", + "module1.py", + "module2.py", + ], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/__init__.py b/gazelle/python/testdata/relative_imports_package_mode/package1/__init__.py new file mode 100644 index 0000000000..11ffb98647 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package1/__init__.py @@ -0,0 +1,2 @@ +def some_function(): + pass diff --git a/gazelle/python/testdata/relative_imports/package1/module1.py b/gazelle/python/testdata/relative_imports_package_mode/package1/module1.py similarity index 100% rename from gazelle/python/testdata/relative_imports/package1/module1.py rename to gazelle/python/testdata/relative_imports_package_mode/package1/module1.py diff --git a/gazelle/python/testdata/relative_imports/package1/module2.py b/gazelle/python/testdata/relative_imports_package_mode/package1/module2.py similarity index 100% rename from gazelle/python/testdata/relative_imports/package1/module2.py rename to gazelle/python/testdata/relative_imports_package_mode/package1/module2.py diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/BUILD.in b/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/BUILD.in new file mode 100644 index 0000000000..80a4a22348 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/BUILD.in @@ -0,0 +1,7 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "my_library", + srcs = ["__init__.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/BUILD.out b/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/BUILD.out new file mode 100644 index 0000000000..80a4a22348 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/BUILD.out @@ -0,0 +1,7 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "my_library", + srcs = ["__init__.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/__init__.py b/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/__init__.py new file mode 100644 index 0000000000..aaa161cd59 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/__init__.py @@ -0,0 +1,2 @@ +def some_function(): + return "some_function" diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/foo/BUILD.in b/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/foo/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/foo/BUILD.out b/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/foo/BUILD.out new file mode 100644 index 0000000000..58498ee3b3 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/foo/BUILD.out @@ -0,0 +1,7 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "foo", + srcs = ["__init__.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/foo/__init__.py b/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/foo/__init__.py new file mode 100644 index 0000000000..aaa161cd59 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package1/my_library/foo/__init__.py @@ -0,0 +1,2 @@ +def some_function(): + return "some_function" diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/BUILD.in b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/BUILD.in new file mode 100644 index 0000000000..0a5b665c8d --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/BUILD.in @@ -0,0 +1,10 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "subpackage1", + srcs = [ + "__init__.py", + "some_module.py", + ], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/BUILD.out b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/BUILD.out new file mode 100644 index 0000000000..0a5b665c8d --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/BUILD.out @@ -0,0 +1,10 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "subpackage1", + srcs = [ + "__init__.py", + "some_module.py", + ], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/__init__.py b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/__init__.py new file mode 100644 index 0000000000..02feaeb848 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/__init__.py @@ -0,0 +1,3 @@ + +def some_init(): + return "some_init" diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/some_module.py b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/some_module.py new file mode 100644 index 0000000000..3cae706242 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/some_module.py @@ -0,0 +1,3 @@ + +def some_function(): + return "some_function" diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/BUILD.in b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/BUILD.out b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/BUILD.out new file mode 100644 index 0000000000..8c34081210 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/BUILD.out @@ -0,0 +1,16 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "subpackage2", + srcs = [ + "__init__.py", + "script.py", + ], + visibility = ["//:__subpackages__"], + deps = [ + "//package1/my_library", + "//package1/my_library/foo", + "//package1/subpackage1", + "//package1/subpackage1/subpackage2/library", + ], +) diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/__init__.py b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/library/BUILD.in b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/library/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/library/BUILD.out b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/library/BUILD.out new file mode 100644 index 0000000000..9fe2e3d1d7 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/library/BUILD.out @@ -0,0 +1,7 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "library", + srcs = ["other_module.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/library/other_module.py b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/library/other_module.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/script.py b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/script.py new file mode 100644 index 0000000000..e93f07719a --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package1/subpackage1/subpackage2/script.py @@ -0,0 +1,11 @@ +from ...my_library import ( + some_function, +) # Import path should be package1.my_library.some_function +from ...my_library.foo import ( + some_function, +) # Import path should be package1.my_library.foo.some_function +from .library import ( + other_module, +) # Import path should be package1.subpackage1.subpackage2.library.other_module +from .. import some_module # Import path should be package1.subpackage1.some_module +from .. import some_function # Import path should be package1.subpackage1.some_function diff --git a/gazelle/python/testdata/relative_imports_package_mode/package2/BUILD.in b/gazelle/python/testdata/relative_imports_package_mode/package2/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/relative_imports_package_mode/package2/BUILD.out b/gazelle/python/testdata/relative_imports_package_mode/package2/BUILD.out new file mode 100644 index 0000000000..bd78108159 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package2/BUILD.out @@ -0,0 +1,12 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "package2", + srcs = [ + "__init__.py", + "module3.py", + "module4.py", + ], + visibility = ["//:__subpackages__"], + deps = ["//package2/library"], +) diff --git a/gazelle/python/testdata/relative_imports_package_mode/package2/__init__.py b/gazelle/python/testdata/relative_imports_package_mode/package2/__init__.py new file mode 100644 index 0000000000..3d19d80e21 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package2/__init__.py @@ -0,0 +1,20 @@ +from .library import add as _add +from .library import divide as _divide +from .library import multiply as _multiply +from .library import subtract as _subtract + + +def add(a, b): + return _add(a, b) + + +def divide(a, b): + return _divide(a, b) + + +def multiply(a, b): + return _multiply(a, b) + + +def subtract(a, b): + return _subtract(a, b) diff --git a/gazelle/python/testdata/relative_imports_package_mode/package2/library/BUILD.in b/gazelle/python/testdata/relative_imports_package_mode/package2/library/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/relative_imports_package_mode/package2/library/BUILD.out b/gazelle/python/testdata/relative_imports_package_mode/package2/library/BUILD.out new file mode 100644 index 0000000000..d704b7fe93 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package2/library/BUILD.out @@ -0,0 +1,7 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "library", + srcs = ["__init__.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/relative_imports_package_mode/package2/library/__init__.py b/gazelle/python/testdata/relative_imports_package_mode/package2/library/__init__.py new file mode 100644 index 0000000000..5f8fc62492 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package2/library/__init__.py @@ -0,0 +1,14 @@ +def add(a, b): + return a + b + + +def divide(a, b): + return a / b + + +def multiply(a, b): + return a * b + + +def subtract(a, b): + return a - b diff --git a/gazelle/python/testdata/relative_imports_package_mode/package2/module3.py b/gazelle/python/testdata/relative_imports_package_mode/package2/module3.py new file mode 100644 index 0000000000..6b955cfda6 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package2/module3.py @@ -0,0 +1,5 @@ +from .library import function5 + + +def function3(): + return "function3 " + function5() diff --git a/gazelle/python/testdata/relative_imports_package_mode/package2/module4.py b/gazelle/python/testdata/relative_imports_package_mode/package2/module4.py new file mode 100644 index 0000000000..6e69699985 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_package_mode/package2/module4.py @@ -0,0 +1,2 @@ +def function4(): + return "function4" diff --git a/gazelle/python/testdata/relative_imports/test.yaml b/gazelle/python/testdata/relative_imports_package_mode/test.yaml similarity index 100% rename from gazelle/python/testdata/relative_imports/test.yaml rename to gazelle/python/testdata/relative_imports_package_mode/test.yaml diff --git a/gazelle/python/testdata/relative_imports/BUILD.in b/gazelle/python/testdata/relative_imports_project_mode/BUILD.in similarity index 61% rename from gazelle/python/testdata/relative_imports/BUILD.in rename to gazelle/python/testdata/relative_imports_project_mode/BUILD.in index c04b5e5434..1059942bfb 100644 --- a/gazelle/python/testdata/relative_imports/BUILD.in +++ b/gazelle/python/testdata/relative_imports_project_mode/BUILD.in @@ -1 +1,2 @@ # gazelle:resolve py resolved_package //package2:resolved_package +# gazelle:python_generation_mode project diff --git a/gazelle/python/testdata/relative_imports/BUILD.out b/gazelle/python/testdata/relative_imports_project_mode/BUILD.out similarity index 70% rename from gazelle/python/testdata/relative_imports/BUILD.out rename to gazelle/python/testdata/relative_imports_project_mode/BUILD.out index bf9524480a..acdc914541 100644 --- a/gazelle/python/testdata/relative_imports/BUILD.out +++ b/gazelle/python/testdata/relative_imports_project_mode/BUILD.out @@ -1,9 +1,10 @@ load("@rules_python//python:defs.bzl", "py_binary", "py_library") # gazelle:resolve py resolved_package //package2:resolved_package +# gazelle:python_generation_mode project py_library( - name = "relative_imports", + name = "relative_imports_project_mode", srcs = [ "package1/module1.py", "package1/module2.py", @@ -12,12 +13,12 @@ py_library( ) py_binary( - name = "relative_imports_bin", + name = "relative_imports_project_mode_bin", srcs = ["__main__.py"], main = "__main__.py", visibility = ["//:__subpackages__"], deps = [ - ":relative_imports", + ":relative_imports_project_mode", "//package2", ], ) diff --git a/gazelle/python/testdata/relative_imports_project_mode/README.md b/gazelle/python/testdata/relative_imports_project_mode/README.md new file mode 100644 index 0000000000..3c95a36e62 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_project_mode/README.md @@ -0,0 +1,5 @@ +# Relative imports + +This test case asserts that the generated targets handle relative imports in +Python correctly. This tests that if python generation mode is project, +the relative paths are included in the subdirectories. diff --git a/gazelle/python/testdata/relative_imports_project_mode/WORKSPACE b/gazelle/python/testdata/relative_imports_project_mode/WORKSPACE new file mode 100644 index 0000000000..4959898cdd --- /dev/null +++ b/gazelle/python/testdata/relative_imports_project_mode/WORKSPACE @@ -0,0 +1 @@ +# This is a test data Bazel workspace. diff --git a/gazelle/python/testdata/relative_imports/__main__.py b/gazelle/python/testdata/relative_imports_project_mode/__main__.py similarity index 100% rename from gazelle/python/testdata/relative_imports/__main__.py rename to gazelle/python/testdata/relative_imports_project_mode/__main__.py diff --git a/gazelle/python/testdata/relative_imports_project_mode/package1/module1.py b/gazelle/python/testdata/relative_imports_project_mode/package1/module1.py new file mode 100644 index 0000000000..28502f1f84 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_project_mode/package1/module1.py @@ -0,0 +1,19 @@ +# 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. + +from .module2 import function2 + + +def function1(): + return "function1 " + function2() diff --git a/gazelle/python/testdata/relative_imports_project_mode/package1/module2.py b/gazelle/python/testdata/relative_imports_project_mode/package1/module2.py new file mode 100644 index 0000000000..0cbc5f0be0 --- /dev/null +++ b/gazelle/python/testdata/relative_imports_project_mode/package1/module2.py @@ -0,0 +1,17 @@ +# 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. + + +def function2(): + return "function2" diff --git a/gazelle/python/testdata/relative_imports_project_mode/package2/BUILD.in b/gazelle/python/testdata/relative_imports_project_mode/package2/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/relative_imports/package2/BUILD.out b/gazelle/python/testdata/relative_imports_project_mode/package2/BUILD.out similarity index 100% rename from gazelle/python/testdata/relative_imports/package2/BUILD.out rename to gazelle/python/testdata/relative_imports_project_mode/package2/BUILD.out diff --git a/gazelle/python/testdata/relative_imports/package2/__init__.py b/gazelle/python/testdata/relative_imports_project_mode/package2/__init__.py similarity index 100% rename from gazelle/python/testdata/relative_imports/package2/__init__.py rename to gazelle/python/testdata/relative_imports_project_mode/package2/__init__.py diff --git a/gazelle/python/testdata/relative_imports/package2/module3.py b/gazelle/python/testdata/relative_imports_project_mode/package2/module3.py similarity index 100% rename from gazelle/python/testdata/relative_imports/package2/module3.py rename to gazelle/python/testdata/relative_imports_project_mode/package2/module3.py diff --git a/gazelle/python/testdata/relative_imports/package2/module4.py b/gazelle/python/testdata/relative_imports_project_mode/package2/module4.py similarity index 100% rename from gazelle/python/testdata/relative_imports/package2/module4.py rename to gazelle/python/testdata/relative_imports_project_mode/package2/module4.py diff --git a/gazelle/python/testdata/relative_imports/package2/subpackage1/module5.py b/gazelle/python/testdata/relative_imports_project_mode/package2/subpackage1/module5.py similarity index 100% rename from gazelle/python/testdata/relative_imports/package2/subpackage1/module5.py rename to gazelle/python/testdata/relative_imports_project_mode/package2/subpackage1/module5.py diff --git a/gazelle/python/testdata/relative_imports_project_mode/test.yaml b/gazelle/python/testdata/relative_imports_project_mode/test.yaml new file mode 100644 index 0000000000..fcea77710f --- /dev/null +++ b/gazelle/python/testdata/relative_imports_project_mode/test.yaml @@ -0,0 +1,15 @@ +# 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/gazelle/pythonconfig/pythonconfig.go b/gazelle/pythonconfig/pythonconfig.go index 866339d449..e0a2b8a469 100644 --- a/gazelle/pythonconfig/pythonconfig.go +++ b/gazelle/pythonconfig/pythonconfig.go @@ -91,6 +91,9 @@ const ( // names of labels to third-party dependencies are normalized. Supported values // are 'none', 'pep503' and 'snake_case' (default). See LabelNormalizationType. LabelNormalization = "python_label_normalization" + // ExperimentalAllowRelativeImports represents the directive that controls + // whether relative imports are allowed. + ExperimentalAllowRelativeImports = "experimental_allow_relative_imports" ) // GenerationModeType represents one of the generation modes for the Python @@ -177,6 +180,7 @@ type Config struct { testFilePattern []string labelConvention string labelNormalization LabelNormalizationType + experimentalAllowRelativeImports bool } type LabelNormalizationType int @@ -212,6 +216,7 @@ func New( testFilePattern: strings.Split(DefaultTestFilePatternString, ","), labelConvention: DefaultLabelConvention, labelNormalization: DefaultLabelNormalizationType, + experimentalAllowRelativeImports: false, } } @@ -244,6 +249,7 @@ func (c *Config) NewChild() *Config { testFilePattern: c.testFilePattern, labelConvention: c.labelConvention, labelNormalization: c.labelNormalization, + experimentalAllowRelativeImports: c.experimentalAllowRelativeImports, } } @@ -520,6 +526,16 @@ func (c *Config) LabelNormalization() LabelNormalizationType { return c.labelNormalization } +// SetExperimentalAllowRelativeImports sets whether relative imports are allowed. +func (c *Config) SetExperimentalAllowRelativeImports(allowRelativeImports bool) { + c.experimentalAllowRelativeImports = allowRelativeImports +} + +// ExperimentalAllowRelativeImports returns whether relative imports are allowed. +func (c *Config) ExperimentalAllowRelativeImports() bool { + return c.experimentalAllowRelativeImports +} + // FormatThirdPartyDependency returns a label to a third-party dependency performing all formating and normalization. func (c *Config) FormatThirdPartyDependency(repositoryName string, distributionName string) label.Label { conventionalDistributionName := strings.ReplaceAll(c.labelConvention, distributionNameLabelConventionSubstitution, distributionName) From f6feca1e00d9ae768243f05e677b7b636b9ad7ba Mon Sep 17 00:00:00 2001 From: armandomontanez Date: Sat, 21 Jun 2025 19:18:35 -0700 Subject: [PATCH 156/156] fix: Fix bazel vendor support for requirements with environment markers (#2997) Fixes `bazel vendor` support for requirements files that contain environment markers. During a vendored `bazel build`, when evaluate_markers_py() is run it needs PYTHONHOME set to properly find the home of the vendored libraries. Resolves #2996 --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- .bazelci/presubmit.yml | 9 +++++++++ CHANGELOG.md | 4 +++- examples/bzlmod/.bazelignore | 1 + examples/bzlmod/.gitignore | 1 + python/private/pypi/evaluate_markers.bzl | 13 ++++++++----- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 01af217924..07ffa4eaac 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -272,6 +272,15 @@ tasks: working_directory: examples/bzlmod platform: debian11 bazel: 7.x + integration_test_bzlmod_ubuntu_vendor: + <<: *reusable_build_test_all + name: "examples/bzlmod: bazel vendor" + working_directory: examples/bzlmod + platform: ubuntu2004 + shell_commands: + - "bazel vendor --vendor_dir=./vendor //..." + - "bazel build --vendor_dir=./vendor //..." + - "rm -rf ./vendor" integration_test_bzlmod_macos: <<: *reusable_build_test_all <<: *coverage_targets_example_bzlmod diff --git a/CHANGELOG.md b/CHANGELOG.md index ecdc129502..4facff4917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,7 +61,9 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-fixed} ### Fixed -* Nothing fixed. +* (pypi) Fixes an issue where builds using a `bazel vendor` vendor directory + would fail if the constraints file contained environment markers. Fixes + [#2996](https://github.com/bazel-contrib/rules_python/issues/2996). {#v0-0-0-added} ### Added diff --git a/examples/bzlmod/.bazelignore b/examples/bzlmod/.bazelignore index 3927f8e910..536ded93a6 100644 --- a/examples/bzlmod/.bazelignore +++ b/examples/bzlmod/.bazelignore @@ -1,2 +1,3 @@ other_module py_proto_library/foo_external +vendor diff --git a/examples/bzlmod/.gitignore b/examples/bzlmod/.gitignore index ac51a054d2..0f6c6316dd 100644 --- a/examples/bzlmod/.gitignore +++ b/examples/bzlmod/.gitignore @@ -1 +1,2 @@ bazel-* +vendor/ diff --git a/python/private/pypi/evaluate_markers.bzl b/python/private/pypi/evaluate_markers.bzl index 58a29a9181..2b805c33e6 100644 --- a/python/private/pypi/evaluate_markers.bzl +++ b/python/private/pypi/evaluate_markers.bzl @@ -78,14 +78,16 @@ def evaluate_markers_py(mrctx, *, requirements, python_interpreter, python_inter out_file = mrctx.path("requirements_with_markers.out.json") mrctx.file(in_file, json.encode(requirements)) + interpreter = pypi_repo_utils.resolve_python_interpreter( + mrctx, + python_interpreter = python_interpreter, + python_interpreter_target = python_interpreter_target, + ) + pypi_repo_utils.execute_checked( mrctx, op = "ResolveRequirementEnvMarkers({})".format(in_file), - python = pypi_repo_utils.resolve_python_interpreter( - mrctx, - python_interpreter = python_interpreter, - python_interpreter_target = python_interpreter_target, - ), + python = interpreter, arguments = [ "-m", "python.private.pypi.requirements_parser.resolve_target_platforms", @@ -94,6 +96,7 @@ def evaluate_markers_py(mrctx, *, requirements, python_interpreter, python_inter ], srcs = srcs, environment = { + "PYTHONHOME": str(interpreter.dirname), "PYTHONPATH": [ Label("@pypi__packaging//:BUILD.bazel"), Label("//:BUILD.bazel"),