diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 3b70734e..7e9d4dea 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/.bazelrc b/.bazelrc index 4e6f2fa1..d2e07215 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/.editorconfig b/.editorconfig new file mode 100644 index 00000000..26bb52ff --- /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/.gitignore b/.gitignore index 863b0e9c..5a34cd45 100644 --- a/.gitignore +++ b/.gitignore @@ -32,11 +32,11 @@ *~ # Bazel directories -/bazel-* -/bazel-bin -/bazel-genfiles -/bazel-out -/bazel-testlogs +bazel-* +bazel-bin +bazel-genfiles +bazel-out +bazel-testlogs user.bazelrc # vim swap files @@ -51,4 +51,4 @@ user.bazelrc # MODULE.bazel.lock is ignored for now as per recommendation from upstream. # See https://github.com/bazelbuild/bazel/issues/20369 -MODULE.bazel.lock +MODULE.bazel.lock \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fc00ca2..15b3a3ea 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 @@ -74,12 +76,20 @@ 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. +* (toolchains) Local toolchains now supports Windows. {#v0-0-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` (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 @@ -103,8 +113,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 +128,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/CONTRIBUTING.md b/CONTRIBUTING.md index 17558e1b..b087119d 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 diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md index ed644429..f4618ff9 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/environment-variables.md b/docs/environment-variables.md index 9500fa82..26c17109 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -46,11 +46,28 @@ 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_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 diff --git a/docs/pypi-dependencies.md b/docs/pypi-dependencies.md index 4ec40bc8..b3ae7fe5 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/docs/toolchains.md b/docs/toolchains.md index 2f8db665..c8305e8f 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/examples/wheel/BUILD.bazel b/examples/wheel/BUILD.bazel index b434e674..e52e0fc3 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/test_publish.py b/examples/wheel/test_publish.py index e6ec8072..7665629c 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 35803da7..7f19ecd9 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): @@ -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/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel index 872d7d1b..24bbe665 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/BUILD.bazel b/python/private/BUILD.bazel index 9cc8ffc6..34273898 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -212,6 +212,7 @@ bzl_library( deps = [ ":enum_bzl", ":repo_utils.bzl", + "@bazel_skylib//lib:paths", ], ) diff --git a/python/private/attributes.bzl b/python/private/attributes.bzl index 8543caba..98aba4eb 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/get_local_runtime_info.py b/python/private/get_local_runtime_info.py index 19db3a29..57a5a4d3 100644 --- a/python/private/get_local_runtime_info.py +++ b/python/private/get_local_runtime_info.py @@ -36,6 +36,9 @@ # https://stackoverflow.com/questions/47423246/get-pythons-lib-path # For now, it seems LIBDIR has what is needed, so just use that. "LIBDIR", + # LIBDEST is the directory for platform-specific files; it's usually + # a subdirectory of LIBDIR. We use it in case LIBDIR isn't set. + "LIBDEST", # The versioned libpythonX.Y.so.N file. Usually? # It might be a static archive (.a) file instead. "INSTSONAME", @@ -45,6 +48,9 @@ # The platform-specific filename suffix for library files. # Includes the dot, e.g. `.so` "SHLIB_SUFFIX", + # A flag for whether this is a free threaded implementation. + # Set to t when free threading is enabled, '' or None otherwise. + "abi_thread", ] data.update(zip(config_vars, sysconfig.get_config_vars(*config_vars))) print(json.dumps(data)) diff --git a/python/private/internal_config_repo.bzl b/python/private/internal_config_repo.bzl index a5c47871..cfe2fdfd 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/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index ec0643e4..2eb2d8c8 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -14,6 +14,7 @@ """Create a repository for a locally installed Python runtime.""" +load("@bazel_skylib//lib:paths.bzl", "paths") load(":enum.bzl", "enum") load(":repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") @@ -112,6 +113,17 @@ def _local_runtime_repo_impl(rctx): info["INSTSONAME"], ] + if repo_utils.get_platforms_os_name(rctx) == "windows": + # Fall back to assuming no threading when no information is available + if info.get("abi_thread") == None: + info["abi_thread"] = "" + + # Windows requires these library definitions to be linked in with the headers + shared_lib_names.append("python{major}{minor}{abi_thread}.lib".format(**info)) + shared_lib_names.append("python{major}{abi_thread}.lib".format(**info)) + + interpreter_path = interpreter_path.replace("\\", "/") + # In some cases, the value may be empty. Not clear why. shared_lib_names = [v for v in shared_lib_names if v] @@ -119,6 +131,9 @@ def _local_runtime_repo_impl(rctx): shared_lib_names = {v: None for v in shared_lib_names}.keys() shared_lib_dir = info["LIBDIR"] + if shared_lib_dir == None: + shared_lib_dir = paths.dirname(info["LIBDEST"]) + "/libs" + # The specific files are symlinked instead of the whole directory # because it can point to a directory that has more than just # the Python runtime shared libraries, e.g. /usr/lib, or a Python diff --git a/python/private/local_runtime_repo_setup.bzl b/python/private/local_runtime_repo_setup.bzl index 37eab595..96f8571d 100644 --- a/python/private/local_runtime_repo_setup.bzl +++ b/python/private/local_runtime_repo_setup.bzl @@ -15,6 +15,7 @@ """Setup code called by the code generated by `local_runtime_repo`.""" load("@bazel_skylib//lib:selects.bzl", "selects") +load("@rules_cc//cc:cc_import.bzl", "cc_import") load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@rules_python//python:py_runtime.bzl", "py_runtime") load("@rules_python//python:py_runtime_pair.bzl", "py_runtime_pair") @@ -58,6 +59,20 @@ def define_local_runtime_toolchain_impl( major_minor = "{}.{}".format(major, minor) major_minor_micro = "{}.{}".format(major_minor, micro) + version_dict = {"major": major, "minor": minor} + + cc_import( + name = "interface", + interface_library = "lib/python{major}{minor}.lib".format(**version_dict), + system_provided = True, + ) + + cc_import( + name = "abi3_interface", + interface_library = "lib/python3.lib", + system_provided = True, + ) + cc_library( name = "_python_headers", # NOTE: Keep in sync with watch_tree() called in local_runtime_repo @@ -67,6 +82,10 @@ def define_local_runtime_toolchain_impl( allow_empty = True, ), includes = ["include"], + deps = select({ + "@platforms//os:windows": [":interface", ":abi3_interface"], + "//conditions:default": None, + }), ) cc_library( diff --git a/python/private/local_runtime_toolchains_repo.bzl b/python/private/local_runtime_toolchains_repo.bzl index adb3bb56..004ca664 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_executable.bzl b/python/private/py_executable.bzl index a8c669af..24be8dd2 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, diff --git a/python/private/py_toolchain_suite.bzl b/python/private/py_toolchain_suite.bzl index a69be376..e71882da 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/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 92161348..d5d897ef 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 00000000..b483436d --- /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 new file mode 100644 index 00000000..2bfdf42e --- /dev/null +++ b/python/private/pypi/env_marker_setting.bzl @@ -0,0 +1,140 @@ +"""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(":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. +_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, + **kwargs + ) + +def _env_marker_setting_impl(ctx): + env = create_env() + env.update( + ctx.attr._env_marker_config_flag[EnvMarkerInfo].env, + ) + + runtime = ctx.toolchains[TARGET_TOOLCHAIN_TYPE].py3_runtime + + 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: + 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.", + ), + "_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", + 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/evaluate_markers.bzl b/python/private/pypi/evaluate_markers.bzl index f966aa32..19193359 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 e9eba684..647407f1 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/flags.bzl b/python/private/pypi/flags.bzl index a25579a2..03738391 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/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl index 7988aca1..31c9d4da 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 1cbf094f..1583c891 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, @@ -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/python/private/pypi/pep508_env.bzl b/python/private/pypi/pep508_env.bzl index 265a8e9b..a6efb3c5 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,13 +27,67 @@ _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. +# 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 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", + "@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 = { + # 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 = { + "@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": "", +} + # The copy of SO [answer](https://stackoverflow.com/a/13874620) containing # all of the platforms: # ┍━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━┑ @@ -60,19 +114,55 @@ _platform_system_values = { # # 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", } + +sys_platform_select_map = { + "@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", "windows": "nt", } +os_name_select_map = { + "@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` @@ -81,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 = "") @@ -111,10 +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) + + return env - # This is split by topic - return env | { +def create_env(): + return { + # This is split by topic "_aliases": { - "platform_machine": _platform_machine_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/python/private/pypi/pep508_evaluate.bzl b/python/private/pypi/pep508_evaluate.bzl index f8ef5530..70840c76 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/python/private/pypi/pip_repository.bzl b/python/private/pypi/pip_repository.bzl index b7ed1659..8ca94f7f 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/arguments.py b/python/private/pypi/whl_installer/arguments.py index 29bea802..ea609bef 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/platform.py b/python/private/pypi/whl_installer/platform.py index 11dd6e37..ff267fe4 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 d95b33a1..25003e62 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,15 @@ 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 +91,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 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) @@ -123,56 +131,12 @@ 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() 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]. @@ -194,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: @@ -227,66 +191,51 @@ 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, 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 - 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 - 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 + continue - # 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 + added = False + for extra in extras: + if added: + 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 req.marker.evaluate(plat.env_markers(extra)): + platforms_to_add.add(plat) + added = True + break + + if not self._platforms: return - for plat in self._platforms: - if not any( - req.marker.evaluate(plat.env_markers(extra)) for extra in extras - ): - continue + 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 - 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) + for plat in platforms_to_add: + if self._default_minor_version is not None: + self._add(req_name, plat) - # 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/python/private/pypi/whl_installer/wheel_installer.py b/python/private/pypi/whl_installer/wheel_installer.py index a48df699..2db03e03 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 630dc851..160bb5b7 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -14,8 +14,8 @@ "" +load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") 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") @@ -23,10 +23,11 @@ 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,88 +343,147 @@ def _whl_library_impl(rctx): timeout = rctx.attr.timeout, ) - 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, - ], - 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 - ) - entry_point_script_name = entry_point_target_name + ".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), + 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_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 = [] + 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, + ) 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, - ) + 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"]), + ], + ) - 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: - 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) return diff --git a/python/private/text_util.bzl b/python/private/text_util.bzl index a64b5d62..28979d89 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/base_rules/py_executable_base_tests.bzl b/tests/base_rules/py_executable_base_tests.bzl index 37707831..55a8958b 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 d4d839b3..c51aa53a 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], }, diff --git a/tests/integration/local_toolchains/.bazelrc b/tests/integration/local_toolchains/.bazelrc index 39df41d9..aed08b07 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 02b126b0..6b731181 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 98f1ed9a..6c06909c 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") diff --git a/tests/pypi/env_marker_setting/BUILD.bazel b/tests/pypi/env_marker_setting/BUILD.bazel new file mode 100644 index 00000000..9605e650 --- /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 00000000..e16f2c8e --- /dev/null +++ b/tests/pypi/env_marker_setting/env_marker_setting_tests.bzl @@ -0,0 +1,104 @@ +"""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_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", "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( + 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, + ) diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index 5de3bb58..1cd6869c 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 7bd19b65..83be7395 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/index_sources/index_sources_tests.bzl b/tests/pypi/index_sources/index_sources_tests.bzl index ffeed87a..9d12bc63 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 c5048212..c5b24870 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) @@ -458,7 +487,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"], } @@ -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. diff --git a/tests/pypi/pep508/evaluate_tests.bzl b/tests/pypi/pep508/evaluate_tests.bzl index 14e5e40b..303c1679 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. diff --git a/tests/pypi/whl_installer/platform_test.py b/tests/pypi/whl_installer/platform_test.py index 2aeb4caa..ad656507 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_installer_test.py b/tests/pypi/whl_installer/wheel_installer_test.py index b736877e..e8380479 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 = [ diff --git a/tests/pypi/whl_installer/wheel_test.py b/tests/pypi/whl_installer/wheel_test.py index 404218e1..3599fd18 100644 --- a/tests/pypi/whl_installer/wheel_test.py +++ b/tests/pypi/whl_installer/wheel_test.py @@ -5,13 +5,13 @@ 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" ) 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() @@ -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__": diff --git a/tests/support/support.bzl b/tests/support/support.bzl index 6330155d..7bab263c 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")) diff --git a/tools/wheelmaker.py b/tools/wheelmaker.py index 28ec0397..8b775e15 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: @@ -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: "):