Skip to content

Commit 2e3ac7e

Browse files
committed
seemed to have found a reasonable workaround
1 parent 8ff9b2a commit 2e3ac7e

File tree

8 files changed

+119
-15
lines changed

8 files changed

+119
-15
lines changed

python/private/py_executable.bzl

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -539,11 +539,14 @@ def _create_venv(ctx, output_prefix, imports, runtime_details):
539539
ctx.actions.write(pyvenv_cfg, "")
540540

541541
runtime = runtime_details.effective_runtime
542+
542543
venvs_use_declare_symlink_enabled = (
543544
VenvsUseDeclareSymlinkFlag.get_value(ctx) == VenvsUseDeclareSymlinkFlag.YES
544545
)
546+
recreate_venv_at_runtime = False
545547

546-
if not venvs_use_declare_symlink_enabled:
548+
if not venvs_use_declare_symlink_enabled or not runtime.supports_build_time_venv:
549+
recreate_venv_at_runtime = True
547550
if runtime.interpreter:
548551
interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path)
549552
else:
@@ -618,13 +621,17 @@ def _create_venv(ctx, output_prefix, imports, runtime_details):
618621
)
619622
site_packages_symlinks = _create_site_packages_symlinks(ctx, site_packages)
620623

624+
##ctx.actions.write(site_customize,
625+
## "import site\nsite.addsitedir('{}')\n".format(
626+
621627
return struct(
622628
interpreter = interpreter,
623-
recreate_venv_at_runtime = not venvs_use_declare_symlink_enabled,
629+
recreate_venv_at_runtime = recreate_venv_at_runtime,
624630
# Runfiles root relative path or absolute path
625631
interpreter_actual_path = interpreter_actual_path,
626632
files_without_interpreter = [pyvenv_cfg, pth, site_init] + site_packages_symlinks,
627633
venv_site_packages = venv_site_packages,
634+
##runtime_symlinks = runtime_symlinks,
628635
)
629636

630637
def _create_site_packages_symlinks(ctx, site_packages):
@@ -784,6 +791,8 @@ def _create_stage1_bootstrap(
784791
"%recreate_venv_at_runtime%": str(int(venv.recreate_venv_at_runtime)) if venv else "0",
785792
"%target%": str(ctx.label),
786793
"%workspace_name%": ctx.workspace_name,
794+
"%venv_rel_site_packages%": venv.venv_site_packages if venv else "",
795+
"%resolve_python_binary_at_runtime%": "0" if runtime.supports_build_time_venv else "1",
787796
}
788797

789798
if stage2_bootstrap:

python/private/py_runtime_info.bzl

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ def _PyRuntimeInfo_init(
6767
stage2_bootstrap_template = None,
6868
zip_main_template = None,
6969
abi_flags = "",
70-
site_init_template = None):
70+
site_init_template = None,
71+
supports_build_time_venv = True):
7172
if (interpreter_path and interpreter) or (not interpreter_path and not interpreter):
7273
fail("exactly one of interpreter or interpreter_path must be specified")
7374

@@ -117,6 +118,7 @@ def _PyRuntimeInfo_init(
117118
"pyc_tag": pyc_tag,
118119
"python_version": python_version,
119120
"site_init_template": site_init_template,
121+
"supports_build_time_venv": supports_build_time_venv,
120122
"stage2_bootstrap_template": stage2_bootstrap_template,
121123
"stub_shebang": stub_shebang,
122124
"zip_main_template": zip_main_template,
@@ -333,6 +335,26 @@ The following substitutions are made during template expansion:
333335
334336
:::{versionadded} 0.33.0
335337
:::
338+
""",
339+
"supports_build_time_venv": """
340+
:type: bool
341+
342+
True if this toolchain supports creating a virtual env at runtime. False if not
343+
or unknown. If build-time venv creation isn't supported, then binaries may
344+
fallback to non-venv solutions or creating a venv at runtime.
345+
346+
In order to have a functionalal virtualenv created at build time, two criteria
347+
must be met:
348+
1. Specifying that the invoked interpreter (`$venv/bin/python3`, as reported by
349+
`sys.executable`) isn't the real interpreter (e.g. `/usr/bin/python3`). This
350+
typically requires relative symlinks or support for the `PYTHONEXECUTABLE`
351+
environment variable.
352+
2. Creating a site-packages directory (`sys.prefix`) that the runtime
353+
interpreter will recognize (e.g. `$venv/lib/python{version}/site-packages`).
354+
This typically requires knowing the Python version at build time.
355+
356+
:::{versionadded} VERSION_NEXT_FEATURE
357+
:::
336358
""",
337359
},
338360
)

python/private/py_runtime_rule.bzl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def _py_runtime_impl(ctx):
130130
zip_main_template = ctx.file.zip_main_template,
131131
abi_flags = abi_flags,
132132
site_init_template = ctx.file.site_init_template,
133+
supports_build_time_venv = ctx.attr.supports_build_time_venv,
133134
))
134135

135136
if not IS_BAZEL_7_OR_HIGHER:
@@ -307,6 +308,17 @@ from `implementation_name` and `interpreter_version_info`. If no pyc_tag is
307308
available, then only source-less pyc generation will function correctly.
308309
""",
309310
),
311+
"supports_build_time_venv": attr.bool(
312+
doc = """
313+
Whether this runtime supports virtualenvs created at build time.
314+
315+
See {obj}`PyRuntimeInfo.supports_build_time_venv` for docs.
316+
317+
:::{versionadded} VERSION_NEXT_FEATURE
318+
:::
319+
""",
320+
default = True,
321+
),
310322
"python_version": attr.string(
311323
default = "PY3",
312324
values = ["PY2", "PY3"],

python/private/runtime_env_toolchain.bzl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def define_runtime_env_toolchain(name):
4545
stub_shebang = "#!/usr/bin/env python3",
4646
visibility = ["//visibility:private"],
4747
tags = ["manual"],
48+
supports_build_time_venv = False,
4849
)
4950

5051
# This is a dummy runtime whose interpreter_path triggers the native rule
@@ -56,6 +57,7 @@ def define_runtime_env_toolchain(name):
5657
python_version = "PY3",
5758
visibility = ["//visibility:private"],
5859
tags = ["manual"],
60+
supports_build_time_venv = False,
5961
)
6062

6163
py_runtime_pair(

python/private/runtime_env_toolchain_interpreter.sh

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,8 @@ if [[ -e "$self_dir/pyvenv.cfg" || -e "$self_dir/../pyvenv.cfg" ]]; then
6767

6868
# PYTHONEXECUTABLE is also used because `exec -a` doesn't fully trick the
6969
# pyenv wrappers.
70-
# NOTE: The PYTHONEXECUTABLE envvar docs say it's only for Mac, however,
71-
# emperically it also works on e.g. Linux
70+
# NOTE: The PYTHONEXECUTABLE only works for non-Mac starting in Python 3.11
7271
export PYTHONEXECUTABLE="$venv_bin"
73-
# Python uses to det
7472
# Use exec -a so that Python looks at the venv's binary, not the
7573
# actual one invoked.
7674
exec -a "$venv_bin" "$PYTHON_BIN" "$@"

python/private/stage1_bootstrap_template.sh

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ set -e
55
if [[ -n "${RULES_PYTHON_BOOTSTRAP_VERBOSE:-}" ]]; then
66
set -x
77
fi
8+
set -x
89

910
# runfiles-relative path
1011
STAGE2_BOOTSTRAP="%stage2_bootstrap%"
1112

12-
# runfiles-relative path to python interpreter to use
13+
# runfiles-relative path to python interpreter to use.
14+
# This is the `bin/python3` file in the binary's venv.
1315
PYTHON_BINARY='%python_binary%'
1416
# The path that PYTHON_BINARY should symlink to.
1517
# runfiles-relative path, absolute path, or single word.
@@ -18,8 +20,17 @@ PYTHON_BINARY_ACTUAL="%python_binary_actual%"
1820

1921
# 0 or 1
2022
IS_ZIPFILE="%is_zipfile%"
21-
# 0 or 1
23+
# 0 or 1.
24+
# If 1, then a venv will be created at runtime that replicates what would have
25+
# been the build-time structure.
2226
RECREATE_VENV_AT_RUNTIME="%recreate_venv_at_runtime%"
27+
# 0 or 1
28+
# If 1, then the path to python will be resolved by running
29+
# PYTHON_BINARY_ACTUAL to determine the actual underlying interpreter.
30+
RESOLVE_PYTHON_BINARY_AT_RUNTIME="%resolve_python_binary_at_runtime%"
31+
# venv-relative path to the site-packages
32+
# e.g. lib/python3.12t/site-packages
33+
VENV_REL_SITE_PACKAGES="%venv_rel_site_packages%"
2334

2435
# array of strings
2536
declare -a INTERPRETER_ARGS_FROM_TARGET=(
@@ -136,6 +147,7 @@ if [[ "$IS_ZIPFILE" == "1" ]]; then
136147
mkdir -p "$(dirname $python_exe)"
137148
ln -s "$symlink_to" "$python_exe"
138149
elif [[ "$RECREATE_VENV_AT_RUNTIME" == "1" ]]; then
150+
139151
if [[ -n "$RULES_PYTHON_EXTRACT_ROOT" ]]; then
140152
use_exec=1
141153
# Use our runfiles path as a unique, reusable, location for the
@@ -167,19 +179,50 @@ elif [[ "$RECREATE_VENV_AT_RUNTIME" == "1" ]]; then
167179
exit 1
168180
fi
169181
fi
170-
mkdir -p "$venv/bin"
182+
183+
runfiles_venv="$RUNFILES_DIR/$(dirname $(dirname $PYTHON_BINARY))"
184+
185+
# If the interpreter is a wrapper script that invokes Python, and
186+
# is prior to Python 3.11 (PYTHONEXECUTABLE environment variable), then
187+
# we can only trigger venv behavior by symlinking to the actual underlying
188+
# interpreter.
189+
# Additionally, if the build-time lib/pythonX.Y/site-packages path doesn't
190+
# match what the runtime interpreter is looking for (i.e. version mismatch
191+
# between build vs runtime), then pretend the version mismatch is OK :D,
192+
# and create the desired path in the temporary venv.
193+
if [[ "$RESOLVE_PYTHON_BINARY_AT_RUNTIME" == "1" ]]; then
194+
{
195+
read -r resolved_py_exe
196+
read -r resolved_site_packages
197+
} < <("$symlink_to" -I <<EOF
198+
import sys, site, os
199+
print(sys.executable)
200+
print(site.getsitepackages(["$venv"])[-1])
201+
EOF
202+
)
203+
symlink_to="$resolved_py_exe"
204+
runfiles_venv_site_packages=$runfiles_venv/$VENV_REL_SITE_PACKAGES
205+
venv_site_packages="$resolved_site_packages"
206+
else
207+
# For simplicity, just symlink to the whole lib directory.
208+
runfiles_venv_site_packages=$runfiles_venv/lib
209+
venv_site_packages=$venv/lib
210+
fi
211+
171212
# Match the basename; some tools, e.g. pyvenv key off the executable name
172213
python_exe="$venv/bin/$(basename $PYTHON_BINARY_ACTUAL)"
173214
if [[ ! -e "$python_exe" ]]; then
215+
mkdir -p "$venv/bin"
174216
ln -s "$symlink_to" "$python_exe"
175217
fi
176-
runfiles_venv="$RUNFILES_DIR/$(dirname $(dirname $PYTHON_BINARY))"
177218
if [[ ! -e "$venv/pyvenv.cfg" ]]; then
178219
ln -s "$runfiles_venv/pyvenv.cfg" "$venv/pyvenv.cfg"
179220
fi
180-
if [[ ! -e "$venv/lib" ]]; then
181-
ln -s "$runfiles_venv/lib" "$venv/lib"
221+
if [[ ! -e "$venv_site_packages" ]]; then
222+
mkdir -p $(dirname $venv_site_packages)
223+
ln -s "$runfiles_venv_site_packages" "$venv_site_packages"
182224
fi
225+
183226
else
184227
use_exec=1
185228
fi

python/private/stage2_bootstrap_template.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ def main():
370370
print_verbose("initial sys.path:", values=sys.path)
371371

372372
site_packages = os.path.join(sys.prefix, VENV_SITE_PACKAGES)
373-
if site_packages not in sys.path:
373+
if site_packages not in sys.path and os.path.exists(site_packages):
374374
# NOTE: if this happens, it likely means we're running with a different
375375
# Python version than was built with. Things may or may not work.
376376
# Such a situation is likely due to the runtime_env toolchain, or some

tests/runtime_env_toolchain/toolchain_runs_test.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,34 @@
33
import platform
44
import unittest
55

6-
from python.runfiles import runfiles
7-
86

97
class RunTest(unittest.TestCase):
108
def test_ran(self):
9+
import os
10+
##os.system("which python3")
11+
##for x, y in sorted(os.environ.items()):
12+
## print(f"{x}={y}")
13+
import sys
14+
print(f"{sys.executable=}")
15+
print(f"{sys.prefix=}")
16+
print(f"{sys.base_prefix=}")
17+
for i, x in enumerate(sys.path):
18+
print(i, x)
19+
20+
breakpoint()
21+
22+
##todo trying to get bazel to use py 3.9
23+
## Use pyenv global to change the version used when shell isn't
24+
## inherited
25+
##print(sys.base_executable)
26+
print(sys.version)
27+
from python.runfiles import runfiles
1128
rf = runfiles.Create()
1229
settings_path = rf.Rlocation(
1330
"rules_python/tests/support/current_build_settings.json"
1431
)
1532
settings = json.loads(pathlib.Path(settings_path).read_text())
33+
print(settings)
1634
if platform.system() == "Windows":
1735
self.assertEqual(
1836
"/_magic_pyruntime_sentinel_do_not_use", settings["interpreter_path"]

0 commit comments

Comments
 (0)