Skip to content

Commit 8f1c04a

Browse files
committed
restructuring toolchains_resolver.bzl
1 parent ecf80fb commit 8f1c04a

1 file changed

Lines changed: 132 additions & 121 deletions

File tree

Lines changed: 132 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,157 @@
1-
"""Toolchain-derived path and flag resolution for venv assembly.
1+
"""Toolchain-derived path and flag resolution for venv assembly."""
22

3-
Encapsulates every value that depends on ``py_toolchain`` + ``ctx`` and
4-
was formerly computed inline in ``assemble_venv``: version strings,
5-
relative escape paths, the ``pyvenv.cfg`` home line, the ``bin/python``
6-
symlink target, and the set of versioned python symlinks.
3+
load("@bazel_lib//lib:paths.bzl", "to_rlocation_path")
74

8-
Extracted from the former ``venv.bzl`` monolith.
9-
"""
5+
def _relative_up(depth):
6+
"""Join ``depth`` repetitions of ``..`` into a single relative path."""
7+
return "/".join([".."] * depth)
108

11-
load("@bazel_lib//lib:paths.bzl", "to_rlocation_path")
9+
def _package_depth(ctx):
10+
"""Count the ``/``-separated segments in the target's Bazel package."""
11+
return len(ctx.label.package.split("/")) if ctx.label.package else 0
1212

13-
def resolve_venv_toolchain(ctx, *, py_toolchain, is_windows, venv_name):
14-
"""Compute toolchain-derived paths and flags for venv assembly.
13+
def _py_versions(py_toolchain):
14+
"""Compute the ``lib/`` directory names used by wheels and the venv.
1515
16-
Args:
17-
ctx: The rule context.
18-
py_toolchain: Resolved Python toolchain struct from py_semantics.
19-
is_windows: Bool — whether the venv targets Windows.
20-
venv_name: The venv dir basename (e.g. ".safe_name").
16+
Wheels always use ``python<MAJ>.<MIN>`` regardless of freethreaded
17+
status — the unpacker hardcodes this layout. Freethreaded CPython
18+
3.13+ expects site-packages under ``python<MAJ>.<MIN>t/``, so the
19+
venv directory name carries the ``t`` suffix when applicable.
2120
2221
Returns:
23-
struct with:
24-
wheel_py_ver: lib dir name inside wheels (no freethreaded 't' suffix).
25-
venv_py_ver: lib dir name inside our venv (+ 't' if freethreaded).
26-
escape: relative path from site-packages dir up to runfiles root.
27-
venv_to_runfiles_escape: relative path from venv root up to runfiles root.
28-
site_packages_rel: site-packages path relative to venv root.
29-
pyvenv_home: ``home =`` line value for pyvenv.cfg.
30-
versioned_python_names: ``[python3, python3.X, python3.Xt?]``.
31-
bin_python_target_path: ``target_path`` for the ``bin/python`` symlink.
22+
``(wheel_py_ver, venv_py_ver)``
3223
"""
33-
34-
# py_ver controls two distinct layouts that agree most of the time
35-
# but diverge for freethreaded interpreters:
36-
#
37-
# * venv_py_ver — the lib-dir name inside OUR venv. Freethreaded
38-
# Python 3.13+ (and onwards) expects its site-packages at
39-
# `lib/python<M>.<m>t/site-packages/`. If we put ours at
40-
# `python<M>.<m>/`, the interpreter never finds our symlinks.
41-
# * wheel_py_ver — the lib-dir name inside a wheel's `install_tree`.
42-
# The unpacker hardcodes `lib/python<M>.<m>/site-packages/`
43-
# regardless of freethreaded status (same wheel install layout for
44-
# both non-t and t interpreters). Keep this without the `t`.
45-
wheel_py_ver = "python{}.{}".format(
24+
base = "python{}.{}".format(
4625
py_toolchain.interpreter_version_info.major,
4726
py_toolchain.interpreter_version_info.minor,
4827
)
49-
venv_py_ver = wheel_py_ver + ("t" if py_toolchain.freethreaded else "")
50-
package_depth = len(ctx.label.package.split("/")) if ctx.label.package else 0
51-
52-
# From .pth / symlink dir up to runfiles root.
53-
# Components: 1 workspace + N package + 1 venv + 3 (lib, pythonX.Y, site-packages)
54-
escape_count = 1 + package_depth + 1 + 3
55-
escape = "/".join([".."] * escape_count)
56-
57-
# From venv root (= sys.prefix at runtime) up to runfiles root.
58-
venv_to_runfiles_escape = "/".join([".."] * (2 + package_depth))
59-
60-
site_packages_rel = "{}/lib/{}/site-packages".format(venv_name, venv_py_ver)
61-
62-
# `relocatable = true` below is a rules_py extension: for an in-build
63-
# CPython 3.11/3.12 runtime that supports build-time venvs, keep `home`
64-
# empty so getpath resolves the relocatable bin/python symlink into the
65-
# base executable without forcing prefix discovery from a relative path.
66-
# Omitting the key leaves sys._base_executable at the outer venv symlink,
67-
# so a stdlib-created nested venv writes the outer venv's bin/ as home.
68-
# A relative `home` instead resolves from the startup cwd:
69-
# https://github.com/python/cpython/blob/3bb231a6/Modules/getpath.py#L362-L365
70-
# https://github.com/python/cpython/blob/3bb231a6/Modules/getpath.py#L431-L432
71-
# https://github.com/python/cpython/blob/3bb231a6/Lib/venv/__init__.py#L158-L166
72-
# Direct runtimes use that symlink. The capability also permits wrappers
73-
# that set PYTHONEXECUTABLE to preserve the underlying base executable:
74-
# https://github.com/bazel-contrib/rules_python/blob/bac54949/python/private/py_runtime_info.bzl#L316-L337
75-
use_empty_venv_home = (
28+
return base, base + ("t" if py_toolchain.freethreaded else "")
29+
30+
def _site_packages_escape(package_depth):
31+
"""Relative path from ``<venv>/lib/pythonX.Y/site-packages/`` to the
32+
runfiles root.
33+
34+
Ascends: workspace (1) + package segments (N) + venv dir (1) +
35+
lib/pythonX.Y/site-packages (3) = 5 + N.
36+
"""
37+
return _relative_up(5 + package_depth)
38+
39+
def _venv_root_escape(package_depth):
40+
"""Relative path from the venv root (``sys.prefix``) to the runfiles
41+
root.
42+
43+
Ascends: workspace (1) + package segments (N) + venv dir (1) = 2 + N.
44+
"""
45+
return _relative_up(2 + package_depth)
46+
47+
def _uses_empty_venv_home(py_toolchain, is_windows):
48+
"""Whether to leave ``home`` empty in ``pyvenv.cfg``.
49+
50+
CPython 3.11/3.12 runfiles interpreters with build-time venv support
51+
on non-Windows resolve a relocatable ``bin/python`` symlink correctly
52+
only when ``home`` is empty and ``relocatable = true``. With a
53+
non-empty relative ``home``, ``getpath.py`` resolves from the startup
54+
cwd, which breaks under sandbox/RBE where the cwd is not the
55+
execroot.
56+
57+
Omitting the key entirely leaves ``sys._base_executable`` at the
58+
outer venv symlink, so a stdlib-created nested venv writes the outer
59+
venv's ``bin/`` as ``home`` — also incorrect.
60+
61+
See ``getpath.py`` L362-365, L431-432 and ``Lib/venv/__init__.py``
62+
L158-166:
63+
https://github.com/python/cpython/blob/3bb231a6/Modules/getpath.py#L362-L365
64+
"""
65+
return (
7666
py_toolchain.runfiles_interpreter and
7767
not is_windows and
7868
getattr(py_toolchain.toolchain, "implementation_name", None) == "cpython" and
7969
getattr(py_toolchain.toolchain, "supports_build_time_venv", False) and
8070
py_toolchain.interpreter_version_info.major == 3 and
8171
py_toolchain.interpreter_version_info.minor in [11, 12]
8272
)
83-
if use_empty_venv_home:
84-
pyvenv_home = ""
85-
elif py_toolchain.runfiles_interpreter:
86-
pbs_rlocation = to_rlocation_path(ctx, py_toolchain.python)
87-
pbs_bin_dir = "/".join(pbs_rlocation.split("/")[:-1])
88-
pyvenv_home = "{}/{}".format(venv_to_runfiles_escape, pbs_bin_dir)
89-
else:
90-
pyvenv_home = py_toolchain.python.path.rsplit("/", 1)[0]
91-
92-
# Versioned python symlinks: python3, python3.<MAJ>.<MIN>, and on
93-
# freethreaded interpreters also python3.<MAJ>.<MIN>t (the name the
94-
# interpreter looks itself up under). All point at the sibling `python`.
95-
versioned_python_names = [
96-
"python{}".format(py_toolchain.interpreter_version_info.major),
97-
"python{}.{}".format(
98-
py_toolchain.interpreter_version_info.major,
99-
py_toolchain.interpreter_version_info.minor,
100-
),
101-
]
102-
if py_toolchain.freethreaded:
103-
versioned_python_names.append("python{}.{}t".format(
104-
py_toolchain.interpreter_version_info.major,
105-
py_toolchain.interpreter_version_info.minor,
106-
))
107-
108-
# bin/python — the symlink the launcher exec's and that Python reads
109-
# to compute sys.base_prefix. We emit an UNRESOLVED symlink
110-
# (`declare_symlink` + `target_path`) with an explicit relative
111-
# target rather than a `declare_file` + `target_file` on
112-
# `py_toolchain.python` for two reasons:
113-
#
114-
# 1. `target_file` lets Bazel pick the symlink target, and the
115-
# choice differs across Bazel versions (Bazel 8 tends to write
116-
# relative, Bazel 9 absolute). Downstream tools that repack the
117-
# tar (`py_image_layer`) need a stable, runfiles-correct target.
118-
# 2. Absolute targets bake in the build-host execroot path — in an
119-
# OCI container that path doesn't exist, bin/python becomes a
120-
# dangling symlink, Python falls back to its compile-time
121-
# `/install` base_prefix, then fails to locate the stdlib.
122-
#
123-
# From `<venv>/bin/`, walk up to the runfiles root (`.runfiles/`)
124-
# and then down through the interpreter's rlocation path.
125-
# Up count: 1 (bin) + 1 (venv) + package_depth + 1 (workspace) = 3 + pkg.
126-
# For system-interpreter (no runfiles_interpreter), fall back to the
127-
# absolute path — these are already non-hermetic by construction.
73+
74+
def _pyvenv_home(ctx, py_toolchain, is_windows, venv_root_escape):
75+
"""Resolve the ``home =`` line for ``pyvenv.cfg``.
76+
77+
Three branches:
78+
79+
- Empty for qualifying CPython 3.11/3.12 (see ``_uses_empty_venv_home``).
80+
- A venv-root-relative path to the interpreter's ``bin/`` for other
81+
runfiles interpreters.
82+
- The absolute filesystem path for system interpreters (already
83+
non-hermetic by construction).
84+
"""
85+
if _uses_empty_venv_home(py_toolchain, is_windows):
86+
return ""
12887
if py_toolchain.runfiles_interpreter:
129-
bin_to_runfiles_root = "/".join([".."] * (3 + package_depth))
130-
bin_python_target_path = "{}/{}".format(
131-
bin_to_runfiles_root,
132-
to_rlocation_path(ctx, py_toolchain.python),
133-
)
134-
else:
135-
bin_python_target_path = py_toolchain.python.path
88+
interpreter_rlocation = to_rlocation_path(ctx, py_toolchain.python)
89+
interpreter_bin_dir = "/".join(interpreter_rlocation.split("/")[:-1])
90+
return "{}/{}".format(venv_root_escape, interpreter_bin_dir)
91+
return py_toolchain.python.path.rsplit("/", 1)[0]
92+
93+
def _versioned_python_names(py_toolchain):
94+
"""Symlink names under which CPython looks itself up.
95+
96+
``python3``, ``python3.<MIN>``, and ``python3.<MIN>t`` for
97+
freethreaded builds. All point at the sibling ``python`` symlink.
98+
"""
99+
major = py_toolchain.interpreter_version_info.major
100+
minor = py_toolchain.interpreter_version_info.minor
101+
names = ["python{}".format(major), "python{}.{}".format(major, minor)]
102+
if py_toolchain.freethreaded:
103+
names.append("python{}.{}t".format(major, minor))
104+
return names
105+
106+
def _bin_python_target_path(ctx, py_toolchain, package_depth):
107+
"""Compute the ``target_path`` for the ``bin/python`` unresolved
108+
symlink.
109+
110+
Uses an explicit relative path through the runfiles tree instead of
111+
Bazel's ``target_file`` mechanism. ``target_file`` lets Bazel choose
112+
the symlink target, and the choice (relative vs absolute) differs
113+
across Bazel versions — Bazel 8 tends relative, Bazel 9 absolute.
114+
Absolute targets bake in the build-host execroot path, which does
115+
not exist inside an OCI container, leaving ``bin/python`` as a
116+
dangling symlink.
117+
118+
From ``<venv>/bin/``, ascends bin (1) + venv (1) + package (N) +
119+
workspace (1) = 3 + N, then descends into the interpreter's
120+
rlocation path. System interpreters fall back to the absolute path.
121+
"""
122+
if not py_toolchain.runfiles_interpreter:
123+
return py_toolchain.python.path
124+
return "{}/{}".format(
125+
_relative_up(3 + package_depth),
126+
to_rlocation_path(ctx, py_toolchain.python),
127+
)
128+
129+
def resolve_venv_toolchain(ctx, *, py_toolchain, is_windows, venv_name):
130+
"""Compute toolchain-derived paths and flags for venv assembly.
131+
132+
Args:
133+
ctx: The rule context.
134+
py_toolchain: Resolved Python toolchain struct from py_semantics.
135+
is_windows: Whether the venv targets Windows.
136+
venv_name: The venv dir basename (e.g. ``.safe_name``).
137+
138+
Returns:
139+
struct with ``wheel_py_ver``, ``venv_py_ver``, ``escape``,
140+
``venv_to_runfiles_escape``, ``site_packages_rel``,
141+
``pyvenv_home``, ``versioned_python_names``,
142+
``bin_python_target_path``.
143+
"""
144+
wheel_py_ver, venv_py_ver = _py_versions(py_toolchain)
145+
depth = _package_depth(ctx)
146+
venv_to_runfiles_escape = _venv_root_escape(depth)
136147

137148
return struct(
138149
wheel_py_ver = wheel_py_ver,
139150
venv_py_ver = venv_py_ver,
140-
escape = escape,
151+
escape = _site_packages_escape(depth),
141152
venv_to_runfiles_escape = venv_to_runfiles_escape,
142-
site_packages_rel = site_packages_rel,
143-
pyvenv_home = pyvenv_home,
144-
versioned_python_names = versioned_python_names,
145-
bin_python_target_path = bin_python_target_path,
153+
site_packages_rel = "{}/lib/{}/site-packages".format(venv_name, venv_py_ver),
154+
pyvenv_home = _pyvenv_home(ctx, py_toolchain, is_windows, venv_to_runfiles_escape),
155+
versioned_python_names = _versioned_python_names(py_toolchain),
156+
bin_python_target_path = _bin_python_target_path(ctx, py_toolchain, depth),
146157
)

0 commit comments

Comments
 (0)