Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion python/private/get_local_runtime_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
42 changes: 38 additions & 4 deletions python/private/local_runtime_repo.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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",
Expand All @@ -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):
Expand Down Expand Up @@ -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"],
Expand All @@ -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 = """
Expand Down Expand Up @@ -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",
Expand Down
36 changes: 36 additions & 0 deletions python/private/local_runtime_repo_setup.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
11 changes: 8 additions & 3 deletions python/private/pypi/extension.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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": {},
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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").
Expand Down
34 changes: 22 additions & 12 deletions python/private/pypi/hub_builder.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def hub_builder(
module_name,
config,
whl_overrides,
default_python_version,
minor_mapping,
available_interpreters,
simpleapi_download_fn,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 " +
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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((
Expand All @@ -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),
))
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand All @@ -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(
Expand All @@ -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,
)
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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
18 changes: 18 additions & 0 deletions tests/integration/local_toolchains/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand All @@ -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 = {
Expand All @@ -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",
Expand Down
31 changes: 31 additions & 0 deletions tests/integration/local_toolchains/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
Loading