Skip to content

Commit 8ff4386

Browse files
rickeylevaignas
andauthored
fix: make sys.executable work with script bootstrap (bazel-contrib#2409)
When `--bootstrap_impl=script` is used, `PYTHONPATH` is no longer used to set the import paths, which means subprocesses no longer inherit the Bazel paths. This is generally a good thing, but breaks when `sys.executable` is used to directly invoke the interpreter. Such an invocation assumes the interpreter will have the same packages available and works with the system_python bootstrap. To fix, have the script bootstrap use a basic virtual env. This allows it to intercept interpreter startup even when the Bazel executable isn't invoked. Under the hood, there's two pieces to make this work. The first is a binary uses `declare_symlink()` to write a relative-path based symlink that points to the underlying Python interpreter. The second piece is a site init hook (triggered by a `.pth` file using an `import` line) performs sys.path setup as part of site (`import site`) initialization. Fixes bazel-contrib#2169 --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com>
1 parent 4a55ef4 commit 8ff4386

15 files changed

+617
-159
lines changed

.bazelrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
# (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it)
55
# To update these lines, execute
66
# `bazel run @rules_bazel_integration_test//tools:update_deleted_packages`
7-
build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered
8-
query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered
7+
build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered
8+
query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered
99

1010
test --test_output=errors
1111

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ Unreleased changes template.
7474
Other changes:
7575
* (python_repository) Start honoring the `strip_prefix` field for `zstd` archives.
7676
* (pypi) {bzl:obj}`pip_parse.extra_hub_aliases` now works in WORKSPACE files.
77+
* (binaries/tests) For {obj}`--bootstrap_impl=script`, a binary-specific (but
78+
otherwise empty) virtual env is used to customize `sys.path` initialization.
7779

7880
{#v0-0-0-fixed}
7981
### Fixed
@@ -83,6 +85,9 @@ Other changes:
8385
Fixes ([2337](https://github.com/bazelbuild/rules_python/issues/2337)).
8486
* (uv): Correct the sha256sum for the `uv` binary for aarch64-apple-darwin.
8587
Fixes ([2411](https://github.com/bazelbuild/rules_python/issues/2411)).
88+
* (binaries/tests) ({obj}`--bootstrap_impl=scipt`) Using `sys.executable` will
89+
use the same `sys.path` setup as the calling binary.
90+
([2169](https://github.com/bazelbuild/rules_python/issues/2169)).
8691

8792
{#v0-0-0-added}
8893
### Added
@@ -97,6 +102,9 @@ Other changes:
97102
for the latest toolchain versions for each minor Python version. You can control
98103
the toolchain selection by using the
99104
{bzl:obj}`//python/config_settings:py_linux_libc` build flag.
105+
* (providers) Added {obj}`py_runtime_info.site_init_template` and
106+
{obj}`PyRuntimeInfo.site_init_template` for specifying the template to use to
107+
initialize the interpreter via venv startup hooks.
100108

101109
{#v0-0-0-removed}
102110
### Removed

python/private/BUILD.bazel

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,14 @@ filegroup(
702702
visibility = ["//visibility:public"],
703703
)
704704

705+
filegroup(
706+
name = "site_init_template",
707+
srcs = ["site_init_template.py"],
708+
# Not actually public. Only public because it's an implicit dependency of
709+
# py_runtime.
710+
visibility = ["//visibility:public"],
711+
)
712+
705713
# NOTE: Windows builds don't use this bootstrap. Instead, a native Windows
706714
# program locates some Python exe and runs `python.exe foo.zip` which
707715
# runs the __main__.py in the zip file.

python/private/py_executable_bazel.bzl

Lines changed: 133 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ the `srcs` of Python targets as required.
8181
"_py_toolchain_type": attr.label(
8282
default = TARGET_TOOLCHAIN_TYPE,
8383
),
84+
"_python_version_flag": attr.label(
85+
default = "//python/config_settings:python_version",
86+
),
8487
"_windows_launcher_maker": attr.label(
8588
default = "@bazel_tools//tools/launcher:launcher_maker",
8689
cfg = "exec",
@@ -177,13 +180,22 @@ def _create_executable(
177180
else:
178181
base_executable_name = executable.basename
179182

183+
venv = None
184+
180185
# The check for stage2_bootstrap_template is to support legacy
181186
# BuiltinPyRuntimeInfo providers, which is likely to come from
182187
# @bazel_tools//tools/python:autodetecting_toolchain, the toolchain used
183188
# for workspace builds when no rules_python toolchain is configured.
184189
if (BootstrapImplFlag.get_value(ctx) == BootstrapImplFlag.SCRIPT and
185190
runtime_details.effective_runtime and
186191
hasattr(runtime_details.effective_runtime, "stage2_bootstrap_template")):
192+
venv = _create_venv(
193+
ctx,
194+
output_prefix = base_executable_name,
195+
imports = imports,
196+
runtime_details = runtime_details,
197+
)
198+
187199
stage2_bootstrap = _create_stage2_bootstrap(
188200
ctx,
189201
output_prefix = base_executable_name,
@@ -192,11 +204,12 @@ def _create_executable(
192204
imports = imports,
193205
runtime_details = runtime_details,
194206
)
195-
extra_runfiles = ctx.runfiles([stage2_bootstrap])
207+
extra_runfiles = ctx.runfiles([stage2_bootstrap] + venv.files_without_interpreter)
196208
zip_main = _create_zip_main(
197209
ctx,
198210
stage2_bootstrap = stage2_bootstrap,
199211
runtime_details = runtime_details,
212+
venv = venv,
200213
)
201214
else:
202215
stage2_bootstrap = None
@@ -272,6 +285,7 @@ def _create_executable(
272285
zip_file = zip_file,
273286
stage2_bootstrap = stage2_bootstrap,
274287
runtime_details = runtime_details,
288+
venv = venv,
275289
)
276290
elif bootstrap_output:
277291
_create_stage1_bootstrap(
@@ -282,6 +296,7 @@ def _create_executable(
282296
is_for_zip = False,
283297
imports = imports,
284298
main_py = main_py,
299+
venv = venv,
285300
)
286301
else:
287302
# Otherwise, this should be the Windows case of launcher + zip.
@@ -296,13 +311,20 @@ def _create_executable(
296311
build_zip_enabled = build_zip_enabled,
297312
))
298313

314+
# The interpreter is added this late in the process so that it isn't
315+
# added to the zipped files.
316+
if venv:
317+
extra_runfiles = extra_runfiles.merge(ctx.runfiles([venv.interpreter]))
299318
return create_executable_result_struct(
300319
extra_files_to_build = depset(extra_files_to_build),
301320
output_groups = {"python_zip_file": depset([zip_file])},
302321
extra_runfiles = extra_runfiles,
303322
)
304323

305-
def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details):
324+
def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details, venv):
325+
python_binary = _runfiles_root_path(ctx, venv.interpreter.short_path)
326+
python_binary_actual = _runfiles_root_path(ctx, venv.interpreter_actual_path)
327+
306328
# The location of this file doesn't really matter. It's added to
307329
# the zip file as the top-level __main__.py file and not included
308330
# elsewhere.
@@ -311,7 +333,8 @@ def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details):
311333
template = runtime_details.effective_runtime.zip_main_template,
312334
output = output,
313335
substitutions = {
314-
"%python_binary%": runtime_details.executable_interpreter_path,
336+
"%python_binary%": python_binary,
337+
"%python_binary_actual%": python_binary_actual,
315338
"%stage2_bootstrap%": "{}/{}".format(
316339
ctx.workspace_name,
317340
stage2_bootstrap.short_path,
@@ -321,6 +344,82 @@ def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details):
321344
)
322345
return output
323346

347+
# Create a venv the executable can use.
348+
# For venv details and the venv startup process, see:
349+
# * https://docs.python.org/3/library/venv.html
350+
# * https://snarky.ca/how-virtual-environments-work/
351+
# * https://github.com/python/cpython/blob/main/Modules/getpath.py
352+
# * https://github.com/python/cpython/blob/main/Lib/site.py
353+
def _create_venv(ctx, output_prefix, imports, runtime_details):
354+
venv = "_{}.venv".format(output_prefix.lstrip("_"))
355+
356+
# The pyvenv.cfg file must be present to trigger the venv site hooks.
357+
# Because it's paths are expected to be absolute paths, we can't reliably
358+
# put much in it. See https://github.com/python/cpython/issues/83650
359+
pyvenv_cfg = ctx.actions.declare_file("{}/pyvenv.cfg".format(venv))
360+
ctx.actions.write(pyvenv_cfg, "")
361+
362+
runtime = runtime_details.effective_runtime
363+
if runtime.interpreter:
364+
py_exe_basename = paths.basename(runtime.interpreter.short_path)
365+
366+
# Even though ctx.actions.symlink() is used, using
367+
# declare_symlink() is required to ensure that the resulting file
368+
# in runfiles is always a symlink. An RBE implementation, for example,
369+
# may choose to write what symlink() points to instead.
370+
interpreter = ctx.actions.declare_symlink("{}/bin/{}".format(venv, py_exe_basename))
371+
interpreter_actual_path = runtime.interpreter.short_path
372+
parent = "/".join([".."] * (interpreter_actual_path.count("/") + 1))
373+
rel_path = parent + "/" + interpreter_actual_path
374+
ctx.actions.symlink(output = interpreter, target_path = rel_path)
375+
else:
376+
py_exe_basename = paths.basename(runtime.interpreter_path)
377+
interpreter = ctx.actions.declare_symlink("{}/bin/{}".format(venv, py_exe_basename))
378+
ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path)
379+
interpreter_actual_path = runtime.interpreter_path
380+
381+
if runtime.interpreter_version_info:
382+
version = "{}.{}".format(
383+
runtime.interpreter_version_info.major,
384+
runtime.interpreter_version_info.minor,
385+
)
386+
else:
387+
version_flag = ctx.attr._python_version_flag[config_common.FeatureFlagInfo].value
388+
version_flag_parts = version_flag.split(".")[0:2]
389+
version = "{}.{}".format(*version_flag_parts)
390+
391+
# See site.py logic: free-threaded builds append "t" to the venv lib dir name
392+
if "t" in runtime.abi_flags:
393+
version += "t"
394+
395+
site_packages = "{}/lib/python{}/site-packages".format(venv, version)
396+
pth = ctx.actions.declare_file("{}/bazel.pth".format(site_packages))
397+
ctx.actions.write(pth, "import _bazel_site_init\n")
398+
399+
site_init = ctx.actions.declare_file("{}/_bazel_site_init.py".format(site_packages))
400+
computed_subs = ctx.actions.template_dict()
401+
computed_subs.add_joined("%imports%", imports, join_with = ":", map_each = _map_each_identity)
402+
ctx.actions.expand_template(
403+
template = runtime.site_init_template,
404+
output = site_init,
405+
substitutions = {
406+
"%import_all%": "True" if ctx.fragments.bazel_py.python_import_all_repositories else "False",
407+
"%site_init_runfiles_path%": "{}/{}".format(ctx.workspace_name, site_init.short_path),
408+
"%workspace_name%": ctx.workspace_name,
409+
},
410+
computed_substitutions = computed_subs,
411+
)
412+
413+
return struct(
414+
interpreter = interpreter,
415+
# Runfiles-relative path or absolute path
416+
interpreter_actual_path = interpreter_actual_path,
417+
files_without_interpreter = [pyvenv_cfg, pth, site_init],
418+
)
419+
420+
def _map_each_identity(v):
421+
return v
422+
324423
def _create_stage2_bootstrap(
325424
ctx,
326425
*,
@@ -363,6 +462,13 @@ def _create_stage2_bootstrap(
363462
)
364463
return output
365464

465+
def _runfiles_root_path(ctx, path):
466+
# The ../ comes from short_path for files in other repos.
467+
if path.startswith("../"):
468+
return path[3:]
469+
else:
470+
return "{}/{}".format(ctx.workspace_name, path)
471+
366472
def _create_stage1_bootstrap(
367473
ctx,
368474
*,
@@ -371,12 +477,24 @@ def _create_stage1_bootstrap(
371477
stage2_bootstrap = None,
372478
imports = None,
373479
is_for_zip,
374-
runtime_details):
480+
runtime_details,
481+
venv = None):
375482
runtime = runtime_details.effective_runtime
376483

484+
if venv:
485+
python_binary_path = _runfiles_root_path(ctx, venv.interpreter.short_path)
486+
else:
487+
python_binary_path = runtime_details.executable_interpreter_path
488+
489+
if is_for_zip and venv:
490+
python_binary_actual = _runfiles_root_path(ctx, venv.interpreter_actual_path)
491+
else:
492+
python_binary_actual = ""
493+
377494
subs = {
378495
"%is_zipfile%": "1" if is_for_zip else "0",
379-
"%python_binary%": runtime_details.executable_interpreter_path,
496+
"%python_binary%": python_binary_path,
497+
"%python_binary_actual%": python_binary_actual,
380498
"%target%": str(ctx.label),
381499
"%workspace_name%": ctx.workspace_name,
382500
}
@@ -447,6 +565,7 @@ def _create_windows_exe_launcher(
447565
)
448566

449567
def _create_zip_file(ctx, *, output, original_nonzip_executable, zip_main, runfiles):
568+
"""Create a Python zipapp (zip with __main__.py entry point)."""
450569
workspace_name = ctx.workspace_name
451570
legacy_external_runfiles = _py_builtins.get_legacy_external_runfiles(ctx)
452571

@@ -524,7 +643,14 @@ def _get_zip_runfiles_path(path, workspace_name, legacy_external_runfiles):
524643
zip_runfiles_path = paths.normalize("{}/{}".format(workspace_name, path))
525644
return "{}/{}".format(_ZIP_RUNFILES_DIRECTORY_NAME, zip_runfiles_path)
526645

527-
def _create_executable_zip_file(ctx, *, output, zip_file, stage2_bootstrap, runtime_details):
646+
def _create_executable_zip_file(
647+
ctx,
648+
*,
649+
output,
650+
zip_file,
651+
stage2_bootstrap,
652+
runtime_details,
653+
venv):
528654
prelude = ctx.actions.declare_file(
529655
"{}_zip_prelude.sh".format(output.basename),
530656
sibling = output,
@@ -536,6 +662,7 @@ def _create_executable_zip_file(ctx, *, output, zip_file, stage2_bootstrap, runt
536662
stage2_bootstrap = stage2_bootstrap,
537663
runtime_details = runtime_details,
538664
is_for_zip = True,
665+
venv = venv,
539666
)
540667
else:
541668
ctx.actions.write(prelude, "#!/usr/bin/env python3\n")

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy