diff --git a/news/4184.fixed.md b/news/4184.fixed.md new file mode 100644 index 0000000000..edd4167df7 --- /dev/null +++ b/news/4184.fixed.md @@ -0,0 +1,6 @@ +(pypi) A {obj}`pip.parse` call for a Python version that has no interpreter for +the host platform is now skipped, like a version with no registered toolchain, +instead of failing the whole {obj}`pip` extension. A dependency that lists, +say, Python 3.9 no longer breaks every other hub on hosts that have no CPython +3.9 build, such as Windows on ARM64 +([#4184](https://github.com/bazel-contrib/rules_python/issues/4184)). diff --git a/python/private/pypi/hub_builder.bzl b/python/private/pypi/hub_builder.bzl index 596cfb2cbe..d1b583dc32 100644 --- a/python/private/pypi/hub_builder.bzl +++ b/python/private/pypi/hub_builder.bzl @@ -179,6 +179,25 @@ def _pip_parse(self, module_ctx, pip_attr, python_version = None): )) return + if (pip_attr.python_interpreter_target == None and + not pip_attr.python_interpreter and + _host_interpreter_name(python_version) not in self._available_interpreters): + # The version has a toolchain, but none of its interpreters runs on + # this host, e.g. there is no CPython build for this os and cpu. Treat + # it like a version missing from minor_mapping instead of failing the + # extension, which would also take down every other version and hub, + # including ones this host can use. + self._logger.info(lambda: ( + "Ignoring pip python version '{version}' for hub " + + "'{hub}' in module '{module}' because there is no " + + "interpreter for it that runs on this host." + ).format( + hub = self.name, + module = self.module_name, + version = python_version, + )) + return + _set_get_index_urls(self, module_ctx, pip_attr) self._platforms[python_version] = _platforms( module_ctx, @@ -395,12 +414,13 @@ def _set_get_index_urls(self, mctx, pip_attr): ) return True +def _host_interpreter_name(python_version): + return "python_{}_host".format(python_version.replace(".", "_")) + def _detect_interpreter(self, pip_attr, python_version): python_interpreter_target = pip_attr.python_interpreter_target if python_interpreter_target == None and not pip_attr.python_interpreter: - python_name = "python_{}_host".format( - python_version.replace(".", "_"), - ) + python_name = _host_interpreter_name(python_version) if python_name not in self._available_interpreters: fail(( "Unable to find interpreter for pip hub '{hub_name}' for " + diff --git a/tests/pypi/hub_builder/hub_builder_tests.bzl b/tests/pypi/hub_builder/hub_builder_tests.bzl index f421c7e351..1d4a602dc9 100644 --- a/tests/pypi/hub_builder/hub_builder_tests.bzl +++ b/tests/pypi/hub_builder/hub_builder_tests.bzl @@ -529,6 +529,75 @@ new-package==0.0.1 --hash=sha256:deadb00f2 _tests.append(_test_simple_multiple_python_versions) +def _test_skip_python_version_without_host_interpreter(env): + # A module can ask for a Python version that has a toolchain but no + # interpreter for this host, e.g. a dependency listing 3.9 when rules_python + # ships no 3.9 CPython for aarch64-pc-windows-msvc. That version must be + # skipped instead of failing the whole extension, like a version missing + # from minor_mapping is. + builder = hub_builder( + env, + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + minor_mapping = { + "3.15": "3.15.19", + "3.16": "3.16.9", + }, + ) + builder.pip_parse( + mocks.mctx( + mock_files = { + "requirements_3_15.txt": """ +simple==0.0.1 --hash=sha256:deadbeef +""", + }, + os_name = "linux", + arch_name = "amd64", + ), + _parse( + hub_name = "pypi", + python_version = "3.15", + requirements_lock = "requirements_3_15.txt", + ), + ) + builder.pip_parse( + mocks.mctx( + mock_files = { + "requirements_3_16.txt": """ +simple==0.0.2 --hash=sha256:deadb00f +""", + }, + os_name = "linux", + arch_name = "amd64", + ), + _parse( + hub_name = "pypi", + python_version = "3.16", + requirements_lock = "requirements_3_16.txt", + ), + ) + pypi = builder.build() + + pypi.exposed_packages().contains_exactly(["simple"]) + pypi.whl_map().contains_exactly({ + "simple": { + "pypi_315_simple": [ + whl_config_setting(version = "3.15"), + ], + }, + }) + pypi.whl_libraries().contains_exactly({ + "pypi_315_simple": { + "config_load": "@pypi//:config.bzl", + "dep_template": "@pypi//{name}:{target}", + "python_interpreter_target": "unit_test_interpreter_target", + "requirement": "simple==0.0.1 --hash=sha256:deadbeef", + }, + }) + +_tests.append(_test_skip_python_version_without_host_interpreter) + def _test_simple_with_markers(env): sub_tests = { ("osx", "aarch64"): "torch==2.4.1 --hash=sha256:deadbeef",