Skip to content

Commit

Permalink
wip: make sys.executable work with script bootstrap
Browse files Browse the repository at this point in the history
  • Loading branch information
rickeylev committed Nov 14, 2024
1 parent 273cbd1 commit 0940884
Show file tree
Hide file tree
Showing 15 changed files with 627 additions and 93 deletions.
17 changes: 14 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,23 @@ Unreleased changes template.

{#v0-0-0-changed}
### Changed
* Nothing yet.
* (binaries/tests) **deprecated** In stage 2 bootstraps, expansion of the
following substrings is deprecated and will be removed in a subsequent
release: `%imports%`, `%import_all%`, `%workspace_name%`,
* (binaries/tests) For {obj}`--bootstrap_impl=script`, an empty,
binary-specific, virtual env is used to customize sys.path initialization.

{#v0-0-0-fixed}
### Fixed
* Nothing yet.
* (binaries/tests) ({obj}`--bootstrap_impl=scipt`) Using `sys.executable` will
use the same `sys.path` setup as the calling binary.
([XXX](XXX)).

{#v0-0-0-added}
### Added
* Nothing yet.
* (providers) Added {obj}`py_runtime_info.site_init_template` and
{obj}`PyRuntimeInfo.site_init_template` for specifying the template to use to
initialize the interpreter via venv startup hooks.

{#v0-0-0-removed}
### Removed
Expand Down Expand Up @@ -99,6 +107,9 @@ Unreleased changes template.
* (precompiling) Skip precompiling (instead of erroring) if the legacy
`@bazel_tools//tools/python:autodetecting_toolchain` is being used
([#2364](https://github.com/bazelbuild/rules_python/issues/2364)).
* (bzlmod) Generate `config_setting` values for all available toolchains instead
of only the registered toolchains, which restores the previous behaviour that
`bzlmod` users would have observed.

{#v0-39-0-added}
### Added
Expand Down
8 changes: 8 additions & 0 deletions python/private/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,14 @@ filegroup(
visibility = ["//visibility:public"],
)

filegroup(
name = "site_init_template",
srcs = ["site_init_template.py"],
# Not actually public. Only public because it's an implicit dependency of
# py_runtime.
visibility = ["//visibility:public"],
)

# NOTE: Windows builds don't use this bootstrap. Instead, a native Windows
# program locates some Python exe and runs `python.exe foo.zip` which
# runs the __main__.py in the zip file.
Expand Down
125 changes: 118 additions & 7 deletions python/private/py_executable_bazel.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ the `srcs` of Python targets as required.
"_py_toolchain_type": attr.label(
default = TARGET_TOOLCHAIN_TYPE,
),
"_python_version_flag": attr.label(
default = "//python/config_settings:python_version",
),
"_windows_launcher_maker": attr.label(
default = "@bazel_tools//tools/launcher:launcher_maker",
cfg = "exec",
Expand Down Expand Up @@ -177,13 +180,22 @@ def _create_executable(
else:
base_executable_name = executable.basename

venv = None

# The check for stage2_bootstrap_template is to support legacy
# BuiltinPyRuntimeInfo providers, which is likely to come from
# @bazel_tools//tools/python:autodetecting_toolchain, the toolchain used
# for workspace builds when no rules_python toolchain is configured.
if (BootstrapImplFlag.get_value(ctx) == BootstrapImplFlag.SCRIPT and
runtime_details.effective_runtime and
hasattr(runtime_details.effective_runtime, "stage2_bootstrap_template")):
venv = _create_venv(
ctx,
output_prefix = base_executable_name,
imports = imports,
runtime_details = runtime_details,
)

stage2_bootstrap = _create_stage2_bootstrap(
ctx,
output_prefix = base_executable_name,
Expand All @@ -192,11 +204,12 @@ def _create_executable(
imports = imports,
runtime_details = runtime_details,
)
extra_runfiles = ctx.runfiles([stage2_bootstrap])
extra_runfiles = ctx.runfiles([stage2_bootstrap] + venv.files_without_interpreter)
zip_main = _create_zip_main(
ctx,
stage2_bootstrap = stage2_bootstrap,
runtime_details = runtime_details,
venv = venv,
)
else:
stage2_bootstrap = None
Expand Down Expand Up @@ -272,6 +285,7 @@ def _create_executable(
zip_file = zip_file,
stage2_bootstrap = stage2_bootstrap,
runtime_details = runtime_details,
venv = venv,
)
elif bootstrap_output:
_create_stage1_bootstrap(
Expand All @@ -282,6 +296,7 @@ def _create_executable(
is_for_zip = False,
imports = imports,
main_py = main_py,
venv = venv,
)
else:
# Otherwise, this should be the Windows case of launcher + zip.
Expand All @@ -295,14 +310,18 @@ def _create_executable(
is_windows = is_windows,
build_zip_enabled = build_zip_enabled,
))

if venv:
extra_runfiles = extra_runfiles.merge(ctx.runfiles([venv.interpreter]))
return create_executable_result_struct(
extra_files_to_build = depset(extra_files_to_build),
output_groups = {"python_zip_file": depset([zip_file])},
extra_runfiles = extra_runfiles,
)

def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details):
def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details, venv):
python_binary = _runfiles_root_path(ctx, venv.interpreter.short_path)
python_binary_actual = _runfiles_root_path(ctx, venv.interpreter_actual_path)

# The location of this file doesn't really matter. It's added to
# the zip file as the top-level __main__.py file and not included
# elsewhere.
Expand All @@ -311,7 +330,8 @@ def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details):
template = runtime_details.effective_runtime.zip_main_template,
output = output,
substitutions = {
"%python_binary%": runtime_details.executable_interpreter_path,
"%python_binary%": python_binary,
"%python_binary_actual": python_binary_actual,
"%stage2_bootstrap%": "{}/{}".format(
ctx.workspace_name,
stage2_bootstrap.short_path,
Expand All @@ -321,6 +341,69 @@ def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details):
)
return output

def _create_venv(ctx, output_prefix, imports, runtime_details):
venv = "_{}.venv".format(output_prefix.lstrip("_"))

# The pyvenv.cfg file must be present to trigger the venv site hooks.
# Because it's paths are expected to be absolute paths, we can't reliably
# put much in it. See https://github.com/python/cpython/issues/83650
pyvenv_cfg = ctx.actions.declare_file("{}/pyvenv.cfg".format(venv))
ctx.actions.write(pyvenv_cfg, "")

runtime = runtime_details.effective_runtime
if runtime.interpreter:
py_exe_basename = paths.basename(runtime.interpreter.short_path)
interpreter = ctx.actions.declare_file("{}/bin/{}".format(venv, py_exe_basename))
ctx.actions.symlink(output = interpreter, target_file = runtime.interpreter)
interpreter_actual_path = runtime.interpreter.short_path
else:
py_exe_basename = paths.basename(runtime.interpreter_path)
interpreter = ctx.actions.declare_symlink("{}/bin/{}".format(venv, py_exe_basename))
ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path)
interpreter_actual_path = runtime.interpreter_path

if runtime.interpreter_version_info:
version = "{}.{}".format(
runtime.interpreter_version_info.major,
runtime.interpreter_version_info.minor,
)
else:
version_flag = ctx.attr._python_version_flag[config_common.FeatureFlagInfo].value
version_flag_parts = version_flag.split(".")[0:2]
version = "{}.{}".format(*version_flag_parts)

# See site.py logic: free-threaded builds append "t" to the venv lib dir name
if "t" in runtime.abi_flags:
version += "t"

site_packages = "{}/lib/python{}/site-packages".format(venv, version)
pth = ctx.actions.declare_file("{}/bazel.pth".format(site_packages))
ctx.actions.write(pth, "import _bazel_site_init\n")

site_init = ctx.actions.declare_file("{}/_bazel_site_init.py".format(site_packages))
computed_subs = ctx.actions.template_dict()
computed_subs.add_joined("%imports%", imports, join_with = ":", map_each = _map_each_identity)
ctx.actions.expand_template(
template = runtime.site_init_template,
output = site_init,
substitutions = {
"%import_all%": "True" if ctx.fragments.bazel_py.python_import_all_repositories else "False",
"%site_init_runfiles_path%": "{}/{}".format(ctx.workspace_name, site_init.short_path),
"%workspace_name%": ctx.workspace_name,
},
computed_substitutions = computed_subs,
)

return struct(
interpreter = interpreter,
# Runfiles-relative path or absolute path
interpreter_actual_path = interpreter_actual_path,
files_without_interpreter = [pyvenv_cfg, pth, site_init],
)

def _map_each_identity(v):
return v

def _create_stage2_bootstrap(
ctx,
*,
Expand Down Expand Up @@ -363,6 +446,13 @@ def _create_stage2_bootstrap(
)
return output

def _runfiles_root_path(ctx, path):
# The ../ comes from short_path for files in other repos.
if path.startswith("../"):
return path[3:]
else:
return "{}/{}".format(ctx.workspace_name, path)

def _create_stage1_bootstrap(
ctx,
*,
Expand All @@ -371,12 +461,24 @@ def _create_stage1_bootstrap(
stage2_bootstrap = None,
imports = None,
is_for_zip,
runtime_details):
runtime_details,
venv = None):
runtime = runtime_details.effective_runtime

if venv:
python_binary_path = _runfiles_root_path(ctx, venv.interpreter.short_path)
else:
python_binary_path = runtime_details.executable_interpreter_path

if is_for_zip and venv:
python_binary_actual = _runfiles_root_path(ctx, venv.interpreter_actual_path)
else:
python_binary_actual = ""

subs = {
"%is_zipfile%": "1" if is_for_zip else "0",
"%python_binary%": runtime_details.executable_interpreter_path,
"%python_binary%": python_binary_path,
"%python_binary_actual": python_binary_actual,
"%target%": str(ctx.label),
"%workspace_name%": ctx.workspace_name,
}
Expand Down Expand Up @@ -447,6 +549,7 @@ def _create_windows_exe_launcher(
)

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

Expand Down Expand Up @@ -524,7 +627,14 @@ def _get_zip_runfiles_path(path, workspace_name, legacy_external_runfiles):
zip_runfiles_path = paths.normalize("{}/{}".format(workspace_name, path))
return "{}/{}".format(_ZIP_RUNFILES_DIRECTORY_NAME, zip_runfiles_path)

def _create_executable_zip_file(ctx, *, output, zip_file, stage2_bootstrap, runtime_details):
def _create_executable_zip_file(
ctx,
*,
output,
zip_file,
stage2_bootstrap,
runtime_details,
venv):
prelude = ctx.actions.declare_file(
"{}_zip_prelude.sh".format(output.basename),
sibling = output,
Expand All @@ -536,6 +646,7 @@ def _create_executable_zip_file(ctx, *, output, zip_file, stage2_bootstrap, runt
stage2_bootstrap = stage2_bootstrap,
runtime_details = runtime_details,
is_for_zip = True,
venv = venv,
)
else:
ctx.actions.write(prelude, "#!/usr/bin/env python3\n")
Expand Down
29 changes: 27 additions & 2 deletions python/private/py_runtime_info.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ def _PyRuntimeInfo_init(
interpreter_version_info = None,
stage2_bootstrap_template = None,
zip_main_template = None,
abi_flags = ""):
abi_flags = "",
site_init_template = None):
if (interpreter_path and interpreter) or (not interpreter_path and not interpreter):
fail("exactly one of interpreter or interpreter_path must be specified")

Expand Down Expand Up @@ -117,6 +118,7 @@ def _PyRuntimeInfo_init(
"interpreter_version_info": interpreter_version_info_struct_from_dict(interpreter_version_info),
"pyc_tag": pyc_tag,
"python_version": python_version,
"site_init_template": site_init_template,
"stage2_bootstrap_template": stage2_bootstrap_template,
"stub_shebang": stub_shebang,
"zip_main_template": zip_main_template,
Expand Down Expand Up @@ -160,7 +162,8 @@ is expected to behave and the substutitions performed.
`%target%`, `%workspace_name`, `%coverage_tool%`, `%import_all%`, `%imports%`,
`%main%`, `%shebang%`
* `--bootstrap_impl=script` substititions: `%is_zipfile%`, `%python_binary%`,
`%target%`, `%workspace_name`, `%shebang%, `%stage2_bootstrap%`
`%python_binary_actual%`, `%target%`, `%workspace_name`,
`%shebang%`, `%stage2_bootstrap%`
Substitution definitions:
Expand All @@ -172,6 +175,19 @@ Substitution definitions:
* An absolute path to a system interpreter (e.g. begins with `/`).
* A runfiles-relative path to an interpreter (e.g. `somerepo/bin/python3`)
* A program to search for on PATH, i.e. a word without spaces, e.g. `python3`.
When `--bootstrap_impl=script` is used, this is always a runfiles-relative
path to a venv-based interpreter executable.
* `%python_binary_actual%`: The path to the interpreter that
`%python_binary%` invokes. There are three types of paths:
* An absolute path to a system interpreter (e.g. begins with `/`).
* A runfiles-relative path to an interpreter (e.g. `somerepo/bin/python3`)
* A program to search for on PATH, i.e. a word without spaces, e.g. `python3`.
Only set for zip builds with `--bootstrap_impl=script`; other builds will use
an empty string.
* `%workspace_name%`: The name of the workspace the target belongs to.
* `%is_zipfile%`: The string `1` if this template is prepended to a zipfile to
create a self-executable zip file. The string `0` otherwise.
Expand Down Expand Up @@ -250,6 +266,15 @@ correctly.
Indicates whether this runtime uses Python major version 2 or 3. Valid values
are (only) `"PY2"` and `"PY3"`.
""",
"site_init_template": """
:type: File
The template to use for the binary-specific site-init hook run by the
interpreter at startup.
:::{versionadded} VERSION_NEXT_FEATURE
:::
""",
"stage2_bootstrap_template": """
:type: File
Expand Down
12 changes: 12 additions & 0 deletions python/private/py_runtime_rule.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ def _py_runtime_impl(ctx):
stage2_bootstrap_template = ctx.file.stage2_bootstrap_template,
zip_main_template = ctx.file.zip_main_template,
abi_flags = abi_flags,
site_init_template = ctx.file.site_init_template,
))

if not IS_BAZEL_7_OR_HIGHER:
Expand Down Expand Up @@ -316,6 +317,17 @@ However, in the future this attribute will be mandatory and have no default
value.
""",
),
"site_init_template": attr.label(
allow_single_file = True,
default = "//python/private:site_init_template",
doc = """
The template to use for the binary-specific site-init hook run by the
interpreter at startup.
:::{versionadded} VERSION_NEXT_FEATURE
:::
""",
),
"stage2_bootstrap_template": attr.label(
default = "//python/private:stage2_bootstrap_template",
allow_single_file = True,
Expand Down
Loading

0 comments on commit 0940884

Please sign in to comment.