|
1 | | -"""Toolchain-derived path and flag resolution for venv assembly. |
| 1 | +"""Toolchain-derived path and flag resolution for venv assembly.""" |
2 | 2 |
|
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") |
7 | 4 |
|
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) |
10 | 8 |
|
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 |
12 | 12 |
|
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. |
15 | 15 |
|
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. |
21 | 20 |
|
22 | 21 | 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)`` |
32 | 23 | """ |
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( |
46 | 25 | py_toolchain.interpreter_version_info.major, |
47 | 26 | py_toolchain.interpreter_version_info.minor, |
48 | 27 | ) |
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 ( |
76 | 66 | py_toolchain.runfiles_interpreter and |
77 | 67 | not is_windows and |
78 | 68 | getattr(py_toolchain.toolchain, "implementation_name", None) == "cpython" and |
79 | 69 | getattr(py_toolchain.toolchain, "supports_build_time_venv", False) and |
80 | 70 | py_toolchain.interpreter_version_info.major == 3 and |
81 | 71 | py_toolchain.interpreter_version_info.minor in [11, 12] |
82 | 72 | ) |
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 "" |
128 | 87 | 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) |
136 | 147 |
|
137 | 148 | return struct( |
138 | 149 | wheel_py_ver = wheel_py_ver, |
139 | 150 | venv_py_ver = venv_py_ver, |
140 | | - escape = escape, |
| 151 | + escape = _site_packages_escape(depth), |
141 | 152 | 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), |
146 | 157 | ) |
0 commit comments