Skip to content

Commit b8e32c4

Browse files
authored
fix(system_python): write import paths to generated file instead of using PYTHONPATH (#3242)
This changes the system_python bootstrap to use a 2-stage process like the script bootstrap does. Among other things, this means the import paths are written to a generated file (`bazel_site_init.py`, same as boostrap=script) and sys.path setup is performed by the Python code in stage 2. Since the PYTHONPATH environment variable isn't used, this fixes the problem on Windows where the value is too long. This also better unifies the system_python and script based bootstraps because the same stage 2 code and bazel_site_init code is used. Along the way, several other improvements: * Fixes path ordering for system_python. The order now matches venv ordering (stdlib, binary paths, runtime site packages). * Makes the venv-based solution work when the site module is disabled (`-S`). * Makes `interpreter_args` attribute and `RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` env var work with system_python. * Makes `main_module` work with system_python. * Progress towards a supportable non-shell based bootstrap (a user requested this because their environment doesn't install any shells as a security precaution). Fixes #2652
1 parent 5cbb5b1 commit b8e32c4

File tree

8 files changed

+280
-405
lines changed

8 files changed

+280
-405
lines changed

CHANGELOG.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,30 @@ END_UNRELEASED_TEMPLATE
6262
{#v0-0-0-changed}
6363
### Changed
6464
* (deps) bumped rules_cc dependency to `0.1.5`.
65+
* (bootstrap) For {obj}`--bootstrap_impl=system_python`, `PYTHONPATH` is no
66+
longer used to add import paths. The sys.path order has changed from
67+
`[app paths, stdlib, runtime site-packages]` to `[stdlib, app paths, runtime
68+
site-packages]`.
69+
* (bootstrap) For {obj}`--bootstrap_impl=system_python`, the sys.path order has
70+
changed from `[app paths, stdlib, runtime site-packages]` to `[stdlib, app
71+
paths, runtime site-packages]`.
6572

6673
{#v0-0-0-fixed}
6774
### Fixed
6875
* (bootstrap) The stage1 bootstrap script now correctly handles nested `RUNFILES_DIR`
6976
environments, fixing issues where a `py_binary` calls another `py_binary`
7077
([#3187](https://github.com/bazel-contrib/rules_python/issues/3187)).
78+
* (bootstrap) For Windows, having many dependencies no longer results in max
79+
length errors due to too long environment variables.
80+
* (bootstrap) {obj}`--bootstrap_impl=script` now supports the `-S` interpreter
81+
setting.
7182

7283
{#v0-0-0-added}
7384
### Added
74-
* Nothing added.
85+
* (bootstrap) {obj}`--bootstrap_impl=system_python` now supports the
86+
{obj}`main_module` attribute.
87+
* (bootstrap) {obj}`--bootstrap_impl=system_python` now supports the
88+
{any}`RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` attribute.
7589

7690

7791
{#v1-6-0}

docs/environment-variables.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ The {bzl:obj}`interpreter_args` attribute.
2525
:::
2626

2727
:::{versionadded} 1.3.0
28+
:::
29+
:::{versionchanged} VERSION_NEXT_FEATURE
30+
Support added for {obj}`--bootstrap_impl=system_python`.
31+
:::
2832

2933
::::
3034

python/private/py_executable.bzl

Lines changed: 82 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ This is mutually exclusive with {obj}`main`.
140140
141141
:::{versionadded} 1.3.0
142142
:::
143+
:::{versionchanged} VERSION_NEXT_FEATURE
144+
Support added for {obj}`--bootstrap_impl=system_python`.
145+
:::
143146
""",
144147
),
145148
"pyc_collection": lambda: attrb.String(
@@ -332,9 +335,10 @@ def _create_executable(
332335
# BuiltinPyRuntimeInfo providers, which is likely to come from
333336
# @bazel_tools//tools/python:autodetecting_toolchain, the toolchain used
334337
# for workspace builds when no rules_python toolchain is configured.
335-
if (BootstrapImplFlag.get_value(ctx) == BootstrapImplFlag.SCRIPT and
338+
if (
336339
runtime_details.effective_runtime and
337-
hasattr(runtime_details.effective_runtime, "stage2_bootstrap_template")):
340+
hasattr(runtime_details.effective_runtime, "stage2_bootstrap_template")
341+
):
338342
venv = _create_venv(
339343
ctx,
340344
output_prefix = base_executable_name,
@@ -351,7 +355,11 @@ def _create_executable(
351355
runtime_details = runtime_details,
352356
venv = venv,
353357
)
354-
extra_runfiles = ctx.runfiles([stage2_bootstrap] + venv.files_without_interpreter)
358+
extra_runfiles = ctx.runfiles(
359+
[stage2_bootstrap] + (
360+
venv.files_without_interpreter if venv else []
361+
),
362+
)
355363
zip_main = _create_zip_main(
356364
ctx,
357365
stage2_bootstrap = stage2_bootstrap,
@@ -460,7 +468,7 @@ def _create_executable(
460468

461469
# The interpreter is added this late in the process so that it isn't
462470
# added to the zipped files.
463-
if venv:
471+
if venv and venv.interpreter:
464472
extra_runfiles = extra_runfiles.merge(ctx.runfiles([venv.interpreter]))
465473
return create_executable_result_struct(
466474
extra_files_to_build = depset(extra_files_to_build),
@@ -469,7 +477,10 @@ def _create_executable(
469477
)
470478

471479
def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details, venv):
472-
python_binary = runfiles_root_path(ctx, venv.interpreter.short_path)
480+
if venv.interpreter:
481+
python_binary = runfiles_root_path(ctx, venv.interpreter.short_path)
482+
else:
483+
python_binary = ""
473484
python_binary_actual = venv.interpreter_actual_path
474485

475486
# The location of this file doesn't really matter. It's added to
@@ -529,62 +540,66 @@ def relative_path(from_, to):
529540
# * https://github.com/python/cpython/blob/main/Modules/getpath.py
530541
# * https://github.com/python/cpython/blob/main/Lib/site.py
531542
def _create_venv(ctx, output_prefix, imports, runtime_details):
543+
create_full_venv = BootstrapImplFlag.get_value(ctx) == BootstrapImplFlag.SCRIPT
532544
venv = "_{}.venv".format(output_prefix.lstrip("_"))
533545

534-
# The pyvenv.cfg file must be present to trigger the venv site hooks.
535-
# Because it's paths are expected to be absolute paths, we can't reliably
536-
# put much in it. See https://github.com/python/cpython/issues/83650
537-
pyvenv_cfg = ctx.actions.declare_file("{}/pyvenv.cfg".format(venv))
538-
ctx.actions.write(pyvenv_cfg, "")
546+
if create_full_venv:
547+
# The pyvenv.cfg file must be present to trigger the venv site hooks.
548+
# Because it's paths are expected to be absolute paths, we can't reliably
549+
# put much in it. See https://github.com/python/cpython/issues/83650
550+
pyvenv_cfg = ctx.actions.declare_file("{}/pyvenv.cfg".format(venv))
551+
ctx.actions.write(pyvenv_cfg, "")
552+
else:
553+
pyvenv_cfg = None
539554

540555
runtime = runtime_details.effective_runtime
541556

542557
venvs_use_declare_symlink_enabled = (
543558
VenvsUseDeclareSymlinkFlag.get_value(ctx) == VenvsUseDeclareSymlinkFlag.YES
544559
)
545560
recreate_venv_at_runtime = False
546-
bin_dir = "{}/bin".format(venv)
547-
548-
if not venvs_use_declare_symlink_enabled or not runtime.supports_build_time_venv:
549-
recreate_venv_at_runtime = True
550-
if runtime.interpreter:
551-
interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path)
552-
else:
553-
interpreter_actual_path = runtime.interpreter_path
554561

555-
py_exe_basename = paths.basename(interpreter_actual_path)
562+
if runtime.interpreter:
563+
interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path)
564+
else:
565+
interpreter_actual_path = runtime.interpreter_path
556566

557-
# When the venv symlinks are disabled, the $venv/bin/python3 file isn't
558-
# needed or used at runtime. However, the zip code uses the interpreter
559-
# File object to figure out some paths.
560-
interpreter = ctx.actions.declare_file("{}/{}".format(bin_dir, py_exe_basename))
561-
ctx.actions.write(interpreter, "actual:{}".format(interpreter_actual_path))
567+
bin_dir = "{}/bin".format(venv)
562568

563-
elif runtime.interpreter:
569+
if create_full_venv:
564570
# Some wrappers around the interpreter (e.g. pyenv) use the program
565571
# name to decide what to do, so preserve the name.
566-
py_exe_basename = paths.basename(runtime.interpreter.short_path)
572+
py_exe_basename = paths.basename(interpreter_actual_path)
567573

568-
# Even though ctx.actions.symlink() is used, using
569-
# declare_symlink() is required to ensure that the resulting file
570-
# in runfiles is always a symlink. An RBE implementation, for example,
571-
# may choose to write what symlink() points to instead.
572-
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
574+
if not venvs_use_declare_symlink_enabled or not runtime.supports_build_time_venv:
575+
recreate_venv_at_runtime = True
573576

574-
interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path)
575-
rel_path = relative_path(
576-
# dirname is necessary because a relative symlink is relative to
577-
# the directory the symlink resides within.
578-
from_ = paths.dirname(runfiles_root_path(ctx, interpreter.short_path)),
579-
to = interpreter_actual_path,
580-
)
577+
# When the venv symlinks are disabled, the $venv/bin/python3 file isn't
578+
# needed or used at runtime. However, the zip code uses the interpreter
579+
# File object to figure out some paths.
580+
interpreter = ctx.actions.declare_file("{}/{}".format(bin_dir, py_exe_basename))
581+
ctx.actions.write(interpreter, "actual:{}".format(interpreter_actual_path))
581582

582-
ctx.actions.symlink(output = interpreter, target_path = rel_path)
583+
elif runtime.interpreter:
584+
# Even though ctx.actions.symlink() is used, using
585+
# declare_symlink() is required to ensure that the resulting file
586+
# in runfiles is always a symlink. An RBE implementation, for example,
587+
# may choose to write what symlink() points to instead.
588+
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
589+
590+
rel_path = relative_path(
591+
# dirname is necessary because a relative symlink is relative to
592+
# the directory the symlink resides within.
593+
from_ = paths.dirname(runfiles_root_path(ctx, interpreter.short_path)),
594+
to = interpreter_actual_path,
595+
)
596+
597+
ctx.actions.symlink(output = interpreter, target_path = rel_path)
598+
else:
599+
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
600+
ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path)
583601
else:
584-
py_exe_basename = paths.basename(runtime.interpreter_path)
585-
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
586-
ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path)
587-
interpreter_actual_path = runtime.interpreter_path
602+
interpreter = None
588603

589604
if runtime.interpreter_version_info:
590605
version = "{}.{}".format(
@@ -626,14 +641,29 @@ def _create_venv(ctx, output_prefix, imports, runtime_details):
626641
}
627642
venv_symlinks = _create_venv_symlinks(ctx, venv_dir_map)
628643

644+
files_without_interpreter = [pth, site_init] + venv_symlinks
645+
if pyvenv_cfg:
646+
files_without_interpreter.append(pyvenv_cfg)
647+
629648
return struct(
649+
# File or None; the `bin/python3` executable in the venv.
650+
# None if a full venv isn't created.
630651
interpreter = interpreter,
652+
# bool; True if the venv should be recreated at runtime
631653
recreate_venv_at_runtime = recreate_venv_at_runtime,
632654
# Runfiles root relative path or absolute path
633655
interpreter_actual_path = interpreter_actual_path,
634-
files_without_interpreter = [pyvenv_cfg, pth, site_init] + venv_symlinks,
656+
files_without_interpreter = files_without_interpreter,
635657
# string; venv-relative path to the site-packages directory.
636658
venv_site_packages = venv_site_packages,
659+
# string; runfiles-root relative path to venv root.
660+
venv_root = runfiles_root_path(
661+
ctx,
662+
paths.join(
663+
py_internal.get_label_repo_runfiles_path(ctx.label),
664+
venv,
665+
),
666+
),
637667
)
638668

639669
def _create_venv_symlinks(ctx, venv_dir_map):
@@ -746,7 +776,7 @@ def _create_stage2_bootstrap(
746776
main_py,
747777
imports,
748778
runtime_details,
749-
venv = None):
779+
venv):
750780
output = ctx.actions.declare_file(
751781
# Prepend with underscore to prevent pytest from trying to
752782
# process the bootstrap for files starting with `test_`
@@ -758,17 +788,10 @@ def _create_stage2_bootstrap(
758788
template = runtime.stage2_bootstrap_template
759789

760790
if main_py:
761-
main_py_path = "{}/{}".format(ctx.workspace_name, main_py.short_path)
791+
main_py_path = runfiles_root_path(ctx, main_py.short_path)
762792
else:
763793
main_py_path = ""
764794

765-
# The stage2 bootstrap uses the venv site-packages location to fix up issues
766-
# that occur when the toolchain doesn't support the build-time venv.
767-
if venv and not runtime.supports_build_time_venv:
768-
venv_rel_site_packages = venv.venv_site_packages
769-
else:
770-
venv_rel_site_packages = ""
771-
772795
ctx.actions.expand_template(
773796
template = template,
774797
output = output,
@@ -779,7 +802,8 @@ def _create_stage2_bootstrap(
779802
"%main%": main_py_path,
780803
"%main_module%": ctx.attr.main_module,
781804
"%target%": str(ctx.label),
782-
"%venv_rel_site_packages%": venv_rel_site_packages,
805+
"%venv_rel_site_packages%": venv.venv_site_packages,
806+
"%venv_root%": venv.venv_root,
783807
"%workspace_name%": ctx.workspace_name,
784808
},
785809
is_executable = True,
@@ -800,7 +824,10 @@ def _create_stage1_bootstrap(
800824
runtime = runtime_details.effective_runtime
801825

802826
if venv:
803-
python_binary_path = runfiles_root_path(ctx, venv.interpreter.short_path)
827+
if venv.interpreter:
828+
python_binary_path = runfiles_root_path(ctx, venv.interpreter.short_path)
829+
else:
830+
python_binary_path = ""
804831
else:
805832
python_binary_path = runtime_details.executable_interpreter_path
806833

0 commit comments

Comments
 (0)