Skip to content

Commit 499fca6

Browse files
committed
local toolchain with pip
along the way: * exposes python version via files in local toolchain
1 parent f5ab3bc commit 499fca6

File tree

11 files changed

+194
-20
lines changed

11 files changed

+194
-20
lines changed

python/private/get_local_runtime_info.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,12 @@ def _get_base_executable():
205205
# is missing.
206206
return sys.executable
207207

208-
209208
data = {
210209
"major": sys.version_info.major,
211210
"minor": sys.version_info.minor,
212211
"micro": sys.version_info.micro,
212+
"releaselevel": sys.version_info.releaselevel,
213+
"serial": sys.version_info.serial,
213214
"include": sysconfig.get_path("include"),
214215
"implementation_name": sys.implementation.name,
215216
"base_executable": _get_base_executable(),

python/private/local_runtime_repo.bzl

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
load(":enum.bzl", "enum")
1818
load(":repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils")
19+
load(":text_util.bzl", "render")
1920

2021
# buildifier: disable=name-conventions
2122
_OnFailure = enum(
@@ -24,10 +25,18 @@ _OnFailure = enum(
2425
FAIL = "fail",
2526
)
2627

27-
_TOOLCHAIN_IMPL_TEMPLATE = """\
28+
_BUILD_BAZEL_TEMPLATE = """\
2829
# Generated by python/private/local_runtime_repo.bzl
2930
30-
load("@rules_python//python/private:local_runtime_repo_setup.bzl", "define_local_runtime_toolchain_impl")
31+
load(
32+
"@rules_python//python/private:local_runtime_repo_setup.bzl",
33+
"define_local_runtime_toolchain_impl",
34+
"define_local_runtime_targets",
35+
)
36+
37+
package(
38+
default_visibility = ["//visibility:public"]
39+
)
3140
3241
define_local_runtime_toolchain_impl(
3342
name = "local_runtime",
@@ -40,6 +49,18 @@ define_local_runtime_toolchain_impl(
4049
implementation_name = "{implementation_name}",
4150
os = "{os}",
4251
)
52+
53+
define_local_runtime_targets()
54+
"""
55+
56+
RUNTIME_INFO_BZL_TEMPLATE = """
57+
# Generated by python/private/local_runtime_repo.bzl
58+
59+
load("@rules_python//python/private:local_runtime_repo_setup.bzl", "define_local_runtime_toolchain_impl")
60+
61+
info = create_info_struct(
62+
{info}
63+
)
4364
"""
4465

4566
def _norm_path(path):
@@ -164,7 +185,7 @@ def _local_runtime_repo_impl(rctx):
164185
else:
165186
logger.warn("No external python libraries found.")
166187

167-
build_bazel = _TOOLCHAIN_IMPL_TEMPLATE.format(
188+
build_bazel = _BUILD_BAZEL_TEMPLATE.format(
168189
major = info["major"],
169190
minor = info["minor"],
170191
micro = info["micro"],
@@ -181,6 +202,19 @@ def _local_runtime_repo_impl(rctx):
181202
rctx.file("REPO.bazel", "")
182203
rctx.file("BUILD.bazel", build_bazel)
183204

205+
# JSON format for repo-phase code
206+
rctx.file("runtime_info.json", json.encode_indent(info))
207+
208+
# bzl format for loading-phase code
209+
rctx.file("runtime_info.bzl", RUNTIME_INFO_BZL_TEMPLATE.format(
210+
info = render.dict(info),
211+
))
212+
213+
# Text format for `python.toolchain.python_version_file`
214+
# The name `python-version` is used to match pyenv and uv naming that looks
215+
# for a `.python-version` file.
216+
rctx.file("python-version", "{major}.{minor}".format(**info))
217+
184218
local_runtime_repo = repository_rule(
185219
implementation = _local_runtime_repo_impl,
186220
doc = """
@@ -260,7 +294,7 @@ How to handle errors when trying to automatically determine settings.
260294
)
261295

262296
def _expand_incompatible_template():
263-
return _TOOLCHAIN_IMPL_TEMPLATE.format(
297+
return _BUILD_BAZEL_TEMPLATE.format(
264298
interpreter_path = "/incompatible",
265299
implementation_name = "incompatible",
266300
interface_library = "None",

python/private/local_runtime_repo_setup.bzl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
"""Setup code called by the code generated by `local_runtime_repo`."""
1616

17+
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
1718
load("@bazel_skylib//lib:selects.bzl", "selects")
1819
load("@rules_cc//cc:cc_import.bzl", "cc_import")
1920
load("@rules_cc//cc:cc_library.bzl", "cc_library")
@@ -167,3 +168,38 @@ def define_local_runtime_toolchain_impl(
167168
],
168169
visibility = ["//visibility:public"],
169170
)
171+
172+
def define_local_runtime_targets():
173+
native.filegroup(
174+
name = "python-version",
175+
srcs = ["python-version"],
176+
)
177+
native.filegroup(
178+
name = "runtime_info",
179+
srcs = ["runtime_info.json"],
180+
)
181+
bzl_library(
182+
name = "runtime_info_bzl",
183+
srcs = ["runtime_info.bzl"],
184+
deps = [
185+
Label("//python/private:local_runtime_repo_setup.bzl"),
186+
],
187+
)
188+
189+
def create_info_struct(info):
190+
self = struct(
191+
_info = info,
192+
get_info = lambda *a, **k: _info_get_info(self, *a, **k),
193+
get_version_major_minor = lambda *a, **k: _info_get_version_major_minor(self, *a, **k),
194+
get_version_full = lambda *a, **k: _info_get_version_full(self, *a, **k),
195+
)
196+
return self
197+
198+
def _info_get_info(self):
199+
return self._info
200+
201+
def _info_get_version_major_minor(self):
202+
return "{major}.{minor}".format(**self._info)
203+
204+
def _info_get_version_full(self):
205+
return "{major}.{minor}.{micro}".format(**self._info)

python/private/pypi/extension.bzl

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
load("@bazel_features//:features.bzl", "bazel_features")
1818
load("@pythons_hub//:interpreters.bzl", "INTERPRETER_LABELS")
19-
load("@pythons_hub//:versions.bzl", "MINOR_MAPPING")
19+
load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING")
2020
load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config")
2121
load("//python/private:auth.bzl", "AUTH_ATTRS")
2222
load("//python/private:normalize_name.bzl", "normalize_name")
@@ -80,7 +80,11 @@ def build_config(
8080
evaluation of the extension.
8181
8282
Returns:
83-
A struct with the configuration.
83+
A struct with the configuration, attributes:
84+
* `auth_patterns`: dict of authentication patterns
85+
* `netrc`: netrc file or None
86+
* `platforms`: dict[str, ??] of platform configs
87+
* `enable_pipstar`: bool
8488
"""
8589
defaults = {
8690
"platforms": {},
@@ -229,6 +233,7 @@ You cannot use both the additive_build_content and additive_build_content_file a
229233
whl_overrides = whl_overrides,
230234
simpleapi_download_fn = simpleapi_download,
231235
simpleapi_cache = simpleapi_cache,
236+
default_python_version = DEFAULT_PYTHON_VERSION,
232237
# TODO @aignas 2025-09-06: do not use kwargs
233238
minor_mapping = kwargs.get("minor_mapping", MINOR_MAPPING),
234239
evaluate_markers_fn = kwargs.get("evaluate_markers", None),
@@ -647,7 +652,7 @@ find in case extra indexes are specified.
647652
default = True,
648653
),
649654
"python_version": attr.string(
650-
mandatory = True,
655+
##mandatory = True,
651656
doc = """
652657
The Python version the dependencies are targetting, in Major.Minor format
653658
(e.g., "3.11") or patch level granularity (e.g. "3.11.1").

python/private/pypi/hub_builder.bzl

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def hub_builder(
2424
module_name,
2525
config,
2626
whl_overrides,
27+
default_python_version,
2728
minor_mapping,
2829
available_interpreters,
2930
simpleapi_download_fn,
@@ -69,8 +70,10 @@ def hub_builder(
6970
_get_index_urls = {},
7071
_use_downloader = {},
7172
_simpleapi_cache = simpleapi_cache,
73+
_get_python_version = lambda *a, **k: _get_python_version(self, *a, **k),
7274
# instance constants
7375
_config = config,
76+
_default_python_version = default_python_version,
7477
_whl_overrides = whl_overrides,
7578
_evaluate_markers_fn = evaluate_markers_fn,
7679
_logger = logger,
@@ -102,7 +105,7 @@ def _build(self):
102105
)
103106

104107
def _pip_parse(self, module_ctx, pip_attr):
105-
python_version = pip_attr.python_version
108+
python_version = self._get_python_version(pip_attr)
106109
if python_version in self._platforms:
107110
fail((
108111
"Duplicate pip python version '{version}' for hub " +
@@ -230,7 +233,7 @@ def _set_get_index_urls(self, pip_attr):
230233
# here
231234
return
232235

233-
python_version = pip_attr.python_version
236+
python_version = self._get_python_version(pip_attr)
234237
self._use_downloader.setdefault(python_version, {}).update({
235238
normalize_name(s): False
236239
for s in pip_attr.simpleapi_skip
@@ -259,7 +262,7 @@ def _detect_interpreter(self, pip_attr):
259262
python_interpreter_target = pip_attr.python_interpreter_target
260263
if python_interpreter_target == None and not pip_attr.python_interpreter:
261264
python_name = "python_{}_host".format(
262-
pip_attr.python_version.replace(".", "_"),
265+
self._get_python_version(pip_attr).replace(".", "_"),
263266
)
264267
if python_name not in self._available_interpreters:
265268
fail((
@@ -269,7 +272,7 @@ def _detect_interpreter(self, pip_attr):
269272
"Expected to find {python_name} among registered versions:\n {labels}"
270273
).format(
271274
hub_name = self.name,
272-
version = pip_attr.python_version,
275+
version = self._get_python_version(pip_attr),
273276
python_name = python_name,
274277
labels = " \n".join(self._available_interpreters),
275278
))
@@ -332,7 +335,7 @@ def _evaluate_markers(self, pip_attr):
332335
if self._config.enable_pipstar:
333336
return lambda _, requirements: evaluate_markers_star(
334337
requirements = requirements,
335-
platforms = self._platforms[pip_attr.python_version],
338+
platforms = self._platforms[self._get_python_version(pip_attr)],
336339
)
337340

338341
interpreter = _detect_interpreter(self, pip_attr)
@@ -355,7 +358,7 @@ def _evaluate_markers(self, pip_attr):
355358
module_ctx,
356359
requirements = {
357360
k: {
358-
p: self._platforms[pip_attr.python_version][p].triple
361+
p: self._platforms[self._get_python_version(pip_attr)][p].triple
359362
for p in plats
360363
}
361364
for k, plats in requirements.items()
@@ -379,7 +382,7 @@ def _create_whl_repos(
379382
pip_attr: {type}`struct` - the struct that comes from the tag class iteration.
380383
"""
381384
logger = self._logger
382-
platforms = self._platforms[pip_attr.python_version]
385+
platforms = self._platforms[self._get_python_version(pip_attr)]
383386
requirements_by_platform = parse_requirements(
384387
module_ctx,
385388
requirements_by_platform = requirements_files_by_platform(
@@ -391,14 +394,14 @@ def _create_whl_repos(
391394
extra_pip_args = pip_attr.extra_pip_args,
392395
platforms = sorted(platforms), # here we only need keys
393396
python_version = full_version(
394-
version = pip_attr.python_version,
397+
version = self._get_python_version(pip_attr),
395398
minor_mapping = self._minor_mapping,
396399
),
397400
logger = logger,
398401
),
399402
platforms = platforms,
400403
extra_pip_args = pip_attr.extra_pip_args,
401-
get_index_urls = self._get_index_urls.get(pip_attr.python_version),
404+
get_index_urls = self._get_index_urls.get(self._get_python_version(pip_attr)),
402405
evaluate_markers = _evaluate_markers(self, pip_attr),
403406
logger = logger,
404407
)
@@ -431,15 +434,15 @@ def _create_whl_repos(
431434
whl_library_args = whl_library_args,
432435
download_only = pip_attr.download_only,
433436
netrc = self._config.netrc or pip_attr.netrc,
434-
use_downloader = _use_downloader(self, pip_attr.python_version, whl.name),
437+
use_downloader = _use_downloader(self, self._get_python_version(pip_attr), whl.name),
435438
auth_patterns = self._config.auth_patterns or pip_attr.auth_patterns,
436-
python_version = _major_minor_version(pip_attr.python_version),
439+
python_version = _major_minor_version(self._get_python_version(pip_attr)),
437440
is_multiple_versions = whl.is_multiple_versions,
438441
enable_pipstar = self._config.enable_pipstar,
439442
)
440443
_add_whl_library(
441444
self,
442-
python_version = pip_attr.python_version,
445+
python_version = self._get_python_version(pip_attr),
443446
whl = whl,
444447
repo = repo,
445448
)
@@ -579,3 +582,10 @@ def _use_downloader(self, python_version, whl_name):
579582
normalize_name(whl_name),
580583
self._get_index_urls.get(python_version) != None,
581584
)
585+
586+
def _get_python_version(self, pip_attr):
587+
python_version = pip_attr.python_version
588+
if python_version:
589+
return python_version
590+
else:
591+
return self._default_python_version

tests/integration/local_toolchains/BUILD.bazel

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@
1414

1515
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
1616
load("@rules_cc//cc:cc_library.bzl", "cc_library")
17+
load("@rules_python//python:py_binary.bzl", "py_binary")
1718
load("@rules_python//python:py_test.bzl", "py_test")
1819
load(":py_extension.bzl", "py_extension")
1920

21+
package(
22+
default_visibility = ["//:__subpackages__"],
23+
)
24+
2025
py_test(
2126
name = "local_runtime_test",
2227
srcs = ["local_runtime_test.py"],
@@ -35,6 +40,14 @@ py_test(
3540
},
3641
)
3742

43+
py_binary(
44+
name = "bin",
45+
srcs = ["bin.py"],
46+
deps = [
47+
"@pypi//more_itertools",
48+
],
49+
)
50+
3851
config_setting(
3952
name = "is_py_local",
4053
flag_values = {
@@ -56,6 +69,11 @@ string_flag(
5669
build_setting_default = "",
5770
)
5871

72+
filegroup(
73+
name = "pyproject",
74+
srcs = ["pyproject.toml"],
75+
)
76+
5977
# Build rules to generate a python extension.
6078
cc_library(
6179
name = "echo_ext_cc",

tests/integration/local_toolchains/MODULE.bazel

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,34 @@ use_repo(python, "rules_python_bzlmod_debug")
9696

9797
# Step 3: Register the toolchains
9898
register_toolchains("@local_toolchains//:all")
99+
100+
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
101+
python.defaults(
102+
python_version_file = "@local_python3//:python-version",
103+
)
104+
105+
# todo: loosen toolchain checking. These lines are only necessary to satisfy
106+
# some python.toolchain logic that expects the default to be registered through
107+
# its apis. But this is problematic becaue the default changes based on the env
108+
python.toolchain(python_version = "3.13")
109+
python.toolchain(python_version = "3.12")
110+
python.toolchain(python_version = "3.11")
111+
use_repo(python, "rules_python_bzlmod_debug")
112+
113+
register_toolchains("@local_toolchains//:all")
114+
115+
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
116+
pip.parse(
117+
hub_name = "pypi",
118+
requirements_lock = "//requirements:requirements-local.txt",
119+
)
120+
use_repo(pip, "pypi")
121+
122+
uv_dev = use_extension(
123+
"@rules_python//python/uv:uv.bzl",
124+
"uv",
125+
dev_dependency = True,
126+
)
127+
uv_dev.configure(
128+
version = "0.8.22",
129+
)

0 commit comments

Comments
 (0)