diff --git a/python/private/get_local_runtime_info.py b/python/private/get_local_runtime_info.py index d176b1a7c6..7cf9607074 100644 --- a/python/private/get_local_runtime_info.py +++ b/python/private/get_local_runtime_info.py @@ -205,11 +205,12 @@ def _get_base_executable(): # is missing. return sys.executable - data = { "major": sys.version_info.major, "minor": sys.version_info.minor, "micro": sys.version_info.micro, + "releaselevel": sys.version_info.releaselevel, + "serial": sys.version_info.serial, "include": sysconfig.get_path("include"), "implementation_name": sys.implementation.name, "base_executable": _get_base_executable(), diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index 583926b15f..c8c66662f5 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -16,6 +16,7 @@ load(":enum.bzl", "enum") load(":repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") +load(":text_util.bzl", "render") # buildifier: disable=name-conventions _OnFailure = enum( @@ -24,10 +25,18 @@ _OnFailure = enum( FAIL = "fail", ) -_TOOLCHAIN_IMPL_TEMPLATE = """\ +_BUILD_BAZEL_TEMPLATE = """\ # Generated by python/private/local_runtime_repo.bzl -load("@rules_python//python/private:local_runtime_repo_setup.bzl", "define_local_runtime_toolchain_impl") +load( + "@rules_python//python/private:local_runtime_repo_setup.bzl", + "define_local_runtime_toolchain_impl", + "define_local_runtime_targets", +) + +package( + default_visibility = ["//visibility:public"] +) define_local_runtime_toolchain_impl( name = "local_runtime", @@ -40,6 +49,18 @@ define_local_runtime_toolchain_impl( implementation_name = "{implementation_name}", os = "{os}", ) + +define_local_runtime_targets() +""" + +RUNTIME_INFO_BZL_TEMPLATE = """ +# Generated by python/private/local_runtime_repo.bzl + +load("@rules_python//python/private:local_runtime_repo_setup.bzl", "define_local_runtime_toolchain_impl") + +info = create_info_struct( +{info} +) """ def _norm_path(path): @@ -164,7 +185,7 @@ def _local_runtime_repo_impl(rctx): else: logger.warn("No external python libraries found.") - build_bazel = _TOOLCHAIN_IMPL_TEMPLATE.format( + build_bazel = _BUILD_BAZEL_TEMPLATE.format( major = info["major"], minor = info["minor"], micro = info["micro"], @@ -181,6 +202,19 @@ def _local_runtime_repo_impl(rctx): rctx.file("REPO.bazel", "") rctx.file("BUILD.bazel", build_bazel) + # JSON format for repo-phase code + rctx.file("runtime_info.json", json.encode_indent(info)) + + # bzl format for loading-phase code + rctx.file("runtime_info.bzl", RUNTIME_INFO_BZL_TEMPLATE.format( + info = render.dict(info), + )) + + # Text format for `python.toolchain.python_version_file` + # The name `python-version` is used to match pyenv and uv naming that looks + # for a `.python-version` file. + rctx.file("python-version", "{major}.{minor}".format(**info)) + local_runtime_repo = repository_rule( implementation = _local_runtime_repo_impl, doc = """ @@ -260,7 +294,7 @@ How to handle errors when trying to automatically determine settings. ) def _expand_incompatible_template(): - return _TOOLCHAIN_IMPL_TEMPLATE.format( + return _BUILD_BAZEL_TEMPLATE.format( interpreter_path = "/incompatible", implementation_name = "incompatible", interface_library = "None", diff --git a/python/private/local_runtime_repo_setup.bzl b/python/private/local_runtime_repo_setup.bzl index 6cff1aea43..013b37adfa 100644 --- a/python/private/local_runtime_repo_setup.bzl +++ b/python/private/local_runtime_repo_setup.bzl @@ -14,6 +14,7 @@ """Setup code called by the code generated by `local_runtime_repo`.""" +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") 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") @@ -167,3 +168,38 @@ def define_local_runtime_toolchain_impl( ], visibility = ["//visibility:public"], ) + +def define_local_runtime_targets(): + native.filegroup( + name = "python-version", + srcs = ["python-version"], + ) + native.filegroup( + name = "runtime_info", + srcs = ["runtime_info.json"], + ) + bzl_library( + name = "runtime_info_bzl", + srcs = ["runtime_info.bzl"], + deps = [ + Label("//python/private:local_runtime_repo_setup.bzl"), + ], + ) + +def create_info_struct(info): + self = struct( + _info = info, + get_info = lambda *a, **k: _info_get_info(self, *a, **k), + get_version_major_minor = lambda *a, **k: _info_get_version_major_minor(self, *a, **k), + get_version_full = lambda *a, **k: _info_get_version_full(self, *a, **k), + ) + return self + +def _info_get_info(self): + return self._info + +def _info_get_version_major_minor(self): + return "{major}.{minor}".format(**self._info) + +def _info_get_version_full(self): + return "{major}.{minor}.{micro}".format(**self._info) diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index be1a8e4d03..3e4f2a220c 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -16,7 +16,7 @@ load("@bazel_features//:features.bzl", "bazel_features") load("@pythons_hub//:interpreters.bzl", "INTERPRETER_LABELS") -load("@pythons_hub//:versions.bzl", "MINOR_MAPPING") +load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING") load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") load("//python/private:auth.bzl", "AUTH_ATTRS") load("//python/private:normalize_name.bzl", "normalize_name") @@ -80,7 +80,11 @@ def build_config( evaluation of the extension. Returns: - A struct with the configuration. + A struct with the configuration, attributes: + * `auth_patterns`: dict of authentication patterns + * `netrc`: netrc file or None + * `platforms`: dict[str, ??] of platform configs + * `enable_pipstar`: bool """ defaults = { "platforms": {}, @@ -229,6 +233,7 @@ You cannot use both the additive_build_content and additive_build_content_file a whl_overrides = whl_overrides, simpleapi_download_fn = simpleapi_download, simpleapi_cache = simpleapi_cache, + default_python_version = DEFAULT_PYTHON_VERSION, # TODO @aignas 2025-09-06: do not use kwargs minor_mapping = kwargs.get("minor_mapping", MINOR_MAPPING), evaluate_markers_fn = kwargs.get("evaluate_markers", None), @@ -647,7 +652,7 @@ find in case extra indexes are specified. default = True, ), "python_version": attr.string( - mandatory = True, + ##mandatory = True, doc = """ The Python version the dependencies are targetting, in Major.Minor format (e.g., "3.11") or patch level granularity (e.g. "3.11.1"). diff --git a/python/private/pypi/hub_builder.bzl b/python/private/pypi/hub_builder.bzl index b6088e4ded..614ec17384 100644 --- a/python/private/pypi/hub_builder.bzl +++ b/python/private/pypi/hub_builder.bzl @@ -24,6 +24,7 @@ def hub_builder( module_name, config, whl_overrides, + default_python_version, minor_mapping, available_interpreters, simpleapi_download_fn, @@ -69,8 +70,10 @@ def hub_builder( _get_index_urls = {}, _use_downloader = {}, _simpleapi_cache = simpleapi_cache, + _get_python_version = lambda *a, **k: _get_python_version(self, *a, **k), # instance constants _config = config, + _default_python_version = default_python_version, _whl_overrides = whl_overrides, _evaluate_markers_fn = evaluate_markers_fn, _logger = logger, @@ -102,7 +105,7 @@ def _build(self): ) def _pip_parse(self, module_ctx, pip_attr): - python_version = pip_attr.python_version + python_version = self._get_python_version(pip_attr) if python_version in self._platforms: fail(( "Duplicate pip python version '{version}' for hub " + @@ -230,7 +233,7 @@ def _set_get_index_urls(self, pip_attr): # here return - python_version = pip_attr.python_version + python_version = self._get_python_version(pip_attr) self._use_downloader.setdefault(python_version, {}).update({ normalize_name(s): False for s in pip_attr.simpleapi_skip @@ -259,7 +262,7 @@ def _detect_interpreter(self, pip_attr): python_interpreter_target = pip_attr.python_interpreter_target if python_interpreter_target == None and not pip_attr.python_interpreter: python_name = "python_{}_host".format( - pip_attr.python_version.replace(".", "_"), + self._get_python_version(pip_attr).replace(".", "_"), ) if python_name not in self._available_interpreters: fail(( @@ -269,7 +272,7 @@ def _detect_interpreter(self, pip_attr): "Expected to find {python_name} among registered versions:\n {labels}" ).format( hub_name = self.name, - version = pip_attr.python_version, + version = self._get_python_version(pip_attr), python_name = python_name, labels = " \n".join(self._available_interpreters), )) @@ -332,7 +335,7 @@ def _evaluate_markers(self, pip_attr): if self._config.enable_pipstar: return lambda _, requirements: evaluate_markers_star( requirements = requirements, - platforms = self._platforms[pip_attr.python_version], + platforms = self._platforms[self._get_python_version(pip_attr)], ) interpreter = _detect_interpreter(self, pip_attr) @@ -355,7 +358,7 @@ def _evaluate_markers(self, pip_attr): module_ctx, requirements = { k: { - p: self._platforms[pip_attr.python_version][p].triple + p: self._platforms[self._get_python_version(pip_attr)][p].triple for p in plats } for k, plats in requirements.items() @@ -379,7 +382,7 @@ def _create_whl_repos( pip_attr: {type}`struct` - the struct that comes from the tag class iteration. """ logger = self._logger - platforms = self._platforms[pip_attr.python_version] + platforms = self._platforms[self._get_python_version(pip_attr)] requirements_by_platform = parse_requirements( module_ctx, requirements_by_platform = requirements_files_by_platform( @@ -391,14 +394,14 @@ def _create_whl_repos( extra_pip_args = pip_attr.extra_pip_args, platforms = sorted(platforms), # here we only need keys python_version = full_version( - version = pip_attr.python_version, + version = self._get_python_version(pip_attr), minor_mapping = self._minor_mapping, ), logger = logger, ), platforms = platforms, extra_pip_args = pip_attr.extra_pip_args, - get_index_urls = self._get_index_urls.get(pip_attr.python_version), + get_index_urls = self._get_index_urls.get(self._get_python_version(pip_attr)), evaluate_markers = _evaluate_markers(self, pip_attr), logger = logger, ) @@ -431,15 +434,15 @@ def _create_whl_repos( whl_library_args = whl_library_args, download_only = pip_attr.download_only, netrc = self._config.netrc or pip_attr.netrc, - use_downloader = _use_downloader(self, pip_attr.python_version, whl.name), + use_downloader = _use_downloader(self, self._get_python_version(pip_attr), whl.name), auth_patterns = self._config.auth_patterns or pip_attr.auth_patterns, - python_version = _major_minor_version(pip_attr.python_version), + python_version = _major_minor_version(self._get_python_version(pip_attr)), is_multiple_versions = whl.is_multiple_versions, enable_pipstar = self._config.enable_pipstar, ) _add_whl_library( self, - python_version = pip_attr.python_version, + python_version = self._get_python_version(pip_attr), whl = whl, repo = repo, ) @@ -579,3 +582,10 @@ def _use_downloader(self, python_version, whl_name): normalize_name(whl_name), self._get_index_urls.get(python_version) != None, ) + +def _get_python_version(self, pip_attr): + python_version = pip_attr.python_version + if python_version: + return python_version + else: + return self._default_python_version diff --git a/tests/integration/local_toolchains/BUILD.bazel b/tests/integration/local_toolchains/BUILD.bazel index bf47316027..dc766ba2bd 100644 --- a/tests/integration/local_toolchains/BUILD.bazel +++ b/tests/integration/local_toolchains/BUILD.bazel @@ -14,9 +14,14 @@ load("@bazel_skylib//rules:common_settings.bzl", "string_flag") load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_python//python:py_binary.bzl", "py_binary") load("@rules_python//python:py_test.bzl", "py_test") load(":py_extension.bzl", "py_extension") +package( + default_visibility = ["//:__subpackages__"], +) + py_test( name = "local_runtime_test", srcs = ["local_runtime_test.py"], @@ -35,6 +40,14 @@ py_test( }, ) +py_binary( + name = "bin", + srcs = ["bin.py"], + deps = [ + "@pypi//more_itertools", + ], +) + config_setting( name = "is_py_local", flag_values = { @@ -56,6 +69,11 @@ string_flag( build_setting_default = "", ) +filegroup( + name = "pyproject", + srcs = ["pyproject.toml"], +) + # Build rules to generate a python extension. cc_library( name = "echo_ext_cc", diff --git a/tests/integration/local_toolchains/MODULE.bazel b/tests/integration/local_toolchains/MODULE.bazel index 6c821c5bb0..13204b1f9c 100644 --- a/tests/integration/local_toolchains/MODULE.bazel +++ b/tests/integration/local_toolchains/MODULE.bazel @@ -96,3 +96,34 @@ use_repo(python, "rules_python_bzlmod_debug") # Step 3: Register the toolchains register_toolchains("@local_toolchains//:all") + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.defaults( + python_version_file = "@local_python3//:python-version", +) + +# todo: loosen toolchain checking. These lines are only necessary to satisfy +# some python.toolchain logic that expects the default to be registered through +# its apis. But this is problematic becaue the default changes based on the env +python.toolchain(python_version = "3.13") +python.toolchain(python_version = "3.12") +python.toolchain(python_version = "3.11") +use_repo(python, "rules_python_bzlmod_debug") + +register_toolchains("@local_toolchains//:all") + +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") +pip.parse( + hub_name = "pypi", + requirements_lock = "//requirements:requirements-local.txt", +) +use_repo(pip, "pypi") + +uv_dev = use_extension( + "@rules_python//python/uv:uv.bzl", + "uv", + dev_dependency = True, +) +uv_dev.configure( + version = "0.8.22", +) diff --git a/tests/integration/local_toolchains/bin.py b/tests/integration/local_toolchains/bin.py new file mode 100644 index 0000000000..1f55560377 --- /dev/null +++ b/tests/integration/local_toolchains/bin.py @@ -0,0 +1,9 @@ +import sys + +print(f""" +{sys.version=} +{sys.executable=} +""") + +import more_itertools +print(f"{more_itertools.__file__=}") diff --git a/tests/integration/local_toolchains/pyproject.toml b/tests/integration/local_toolchains/pyproject.toml new file mode 100644 index 0000000000..9da9051a78 --- /dev/null +++ b/tests/integration/local_toolchains/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "local_toolchains" +version = "0.0.0" + +dependencies = [ + # NOTE: This is only used as input to create the resolved requirements.txt + # file, which is what builds, both Bazel and Readthedocs, both use. + "more_itertools" +] + diff --git a/tests/integration/local_toolchains/requirements/BUILD.bazel b/tests/integration/local_toolchains/requirements/BUILD.bazel new file mode 100644 index 0000000000..32f98db623 --- /dev/null +++ b/tests/integration/local_toolchains/requirements/BUILD.bazel @@ -0,0 +1,12 @@ +load("@rules_python//python/uv:lock.bzl", "lock") + +lock( + name = "requirements", + srcs = ["//:pyproject"], + out = "requirements-local.txt", + args = [ + "--emit-index-url", + "--universal", + "--upgrade", + ], +) diff --git a/tests/integration/local_toolchains/requirements/requirements-local.txt b/tests/integration/local_toolchains/requirements/requirements-local.txt new file mode 100644 index 0000000000..12b418085c --- /dev/null +++ b/tests/integration/local_toolchains/requirements/requirements-local.txt @@ -0,0 +1,8 @@ +# This file was autogenerated by uv via the following command: +# bazel run //requirements:requirements.update +--index-url https://pypi.org/simple + +more-itertools==10.8.0 \ + --hash=sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b \ + --hash=sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd + # via local-toolchains (pyproject.toml)