Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ END_UNRELEASED_TEMPLATE
{#v0-0-0-changed}
### Changed
* (binaries/tests) The `PYTHONBREAKPOINT` environment variable is automatically inherited
* (binaries/tests) The {obj}`stamp` attribute now transitions the Bazel builtin
{obj}`--stamp` flag.

{#v0-0-0-fixed}
### Fixed
Expand All @@ -68,6 +70,9 @@ END_UNRELEASED_TEMPLATE
### Added
* (binaries/tests) {obj}`--debugger`: allows specifying an extra dependency
to add to binaries/tests for custom debuggers.
* (binaries/tests) Build information is now included in binaries and tests.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also tell, that if users want, the could, for example set the __version__ or other standard Python metadata from the bazel_binary_info module. What do you think?

Use the `bazel_binary_info` module to access it. The {flag}`--stamp` flag will
add {flag}`--workspace_status` information.

{#v1-8-0}
## [1.8.0] - 2025-12-19
Expand Down
13 changes: 13 additions & 0 deletions python/private/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ load(":print_toolchain_checksums.bzl", "print_toolchains_checksums")
load(":py_exec_tools_toolchain.bzl", "current_interpreter_executable")
load(":sentinel.bzl", "sentinel")
load(":stamp.bzl", "stamp_build_setting")
load(":uncachable_version_file.bzl", "define_uncachable_version_file")

package(
default_visibility = ["//:__subpackages__"],
Expand Down Expand Up @@ -97,6 +98,14 @@ bzl_library(
],
)

filegroup(
name = "build_data_writer",
srcs = select({
"@platforms//os:windows": ["build_data_writer.ps1"],
"//conditions:default": ["build_data_writer.sh"],
}),
)

bzl_library(
name = "builders_bzl",
srcs = ["builders.bzl"],
Expand Down Expand Up @@ -681,6 +690,10 @@ bzl_library(
],
)

define_uncachable_version_file(
name = "uncachable_version_file",
)

bzl_library(
name = "version_bzl",
srcs = ["version.bzl"],
Expand Down
14 changes: 13 additions & 1 deletion python/private/attributes.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -450,8 +450,20 @@ Whether to encode build information into the binary. Possible values:
Stamped binaries are not rebuilt unless their dependencies change.
WARNING: Stamping can harm build performance by reducing cache hits and should
Stamped build information can accessed using the `bazel_binary_info` module.
See the [Accessing build information docs] for more information.
:::{warning}
Stamping can harm build performance by reducing cache hits and should
be avoided if possible.
In addition, this transitions the {obj}`--stamp` flag, which can additional
config state overhead.
:::
:::{note}
Stamping of build data output is always disabled for the exec config.
:::
""",
default = -1,
),
Expand Down
18 changes: 18 additions & 0 deletions python/private/build_data_writer.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
$OutputPath = $env:OUTPUT

Add-Content -Path $OutputPath -Value "TARGET $env:TARGET"
Add-Content -Path $OutputPath -Value "CONFIG_ID $env:CONFIG_ID"
Add-Content -Path $OutputPath -Value "CONFIG_MODE $env:CONFIG_MODE"
Add-Content -Path $OutputPath -Value "STAMPED $env:STAMPED"

$VersionFilePath = $env:VERSION_FILE
if (-not [string]::IsNullOrEmpty($VersionFilePath)) {
Get-Content -Path $VersionFilePath | Add-Content -Path $OutputPath
}

$InfoFilePath = $env:INFO_FILE
if (-not [string]::IsNullOrEmpty($InfoFilePath)) {
Get-Content -Path $InfoFilePath | Add-Content -Path $OutputPath
}

exit 0
13 changes: 13 additions & 0 deletions python/private/build_data_writer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh

echo "TARGET $TARGET" >> $OUTPUT
echo "CONFIG_ID $CONFIG_ID" >> $OUTPUT
echo "CONFIG_MODE $CONFIG_MODE" >> $OUTPUT
echo "STAMPED $STAMPED" >> $OUTPUT
if [ -n "$VERSION_FILE" ]; then
cat "$VERSION_FILE" >> "$OUTPUT"
fi
if [ -n "$INFO_FILE" ]; then
cat "$INFO_FILE" >> "$OUTPUT"
fi
exit 0
11 changes: 1 addition & 10 deletions python/private/common.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -43,34 +43,25 @@ PYTHON_FILE_EXTENSIONS = [

def create_binary_semantics_struct(
*,
get_central_uncachable_version_file,
get_native_deps_dso_name,
should_build_native_deps_dso,
should_include_build_data):
should_build_native_deps_dso):
"""Helper to ensure a semantics struct has all necessary fields.

Call this instead of a raw call to `struct(...)`; it'll help ensure all
the necessary functions are being correctly provided.

Args:
get_central_uncachable_version_file: Callable that returns an optional
Artifact; this artifact is special: it is never cached and is a copy
of `ctx.version_file`; see py_builtins.copy_without_caching
get_native_deps_dso_name: Callable that returns a string, which is the
basename (with extension) of the native deps DSO library.
should_build_native_deps_dso: Callable that returns bool; True if
building a native deps DSO is supported, False if not.
should_include_build_data: Callable that returns bool; True if
build data should be generated, False if not.
Returns:
A "BinarySemantics" struct.
"""
return struct(
# keep-sorted
get_central_uncachable_version_file = get_central_uncachable_version_file,
get_native_deps_dso_name = get_native_deps_dso_name,
should_build_native_deps_dso = should_build_native_deps_dso,
should_include_build_data = should_include_build_data,
)

def create_cc_details_struct(
Expand Down
118 changes: 58 additions & 60 deletions python/private/py_executable.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ accepting arbitrary Python versions.
allow_single_file = True,
default = "@bazel_tools//tools/python:python_bootstrap_template.txt",
),
"_build_data_writer": lambda: attrb.Label(
default = "//python/private:build_data_writer",
executable = True,
cfg = "exec",
),
"_debugger_flag": lambda: attrb.Label(
default = "//python/private:debugger_if_target_config",
providers = [PyInfo],
Expand All @@ -226,6 +231,10 @@ accepting arbitrary Python versions.
"_python_version_flag": lambda: attrb.Label(
default = labels.PYTHON_VERSION,
),
"_uncachable_version_file": lambda: attrb.Label(
default = "//python/private:uncachable_version_file",
allow_files = True,
),
"_venvs_use_declare_symlink_flag": lambda: attrb.Label(
default = labels.VENVS_USE_DECLARE_SYMLINK,
providers = [BuildSettingInfo],
Expand Down Expand Up @@ -271,10 +280,8 @@ def py_executable_impl(ctx, *, is_test, inherited_environment):
def create_binary_semantics():
return create_binary_semantics_struct(
# keep-sorted start
get_central_uncachable_version_file = lambda ctx: None,
get_native_deps_dso_name = _get_native_deps_dso_name,
should_build_native_deps_dso = lambda ctx: False,
should_include_build_data = lambda ctx: False,
# keep-sorted end
)

Expand Down Expand Up @@ -336,6 +343,7 @@ def _create_executable(
imports = imports,
runtime_details = runtime_details,
venv = venv,
build_data_file = runfiles_details.build_data_file,
)
extra_runfiles = ctx.runfiles(
[stage2_bootstrap] + (
Expand Down Expand Up @@ -648,6 +656,7 @@ def _create_stage2_bootstrap(
main_py,
imports,
runtime_details,
build_data_file,
venv):
output = ctx.actions.declare_file(
# Prepend with underscore to prevent pytest from trying to
Expand All @@ -668,6 +677,7 @@ def _create_stage2_bootstrap(
template = template,
output = output,
substitutions = {
"%build_data_file%": runfiles_root_path(ctx, build_data_file.short_path),
"%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime),
"%import_all%": "True" if read_possibly_native_flag(ctx, "python_import_all_repositories") else "False",
"%imports%": ":".join(imports.to_list()),
Expand Down Expand Up @@ -1052,7 +1062,6 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
cc_details.extra_runfiles,
native_deps_details.runfiles,
],
semantics = semantics,
)
exec_result = _create_executable(
ctx,
Expand Down Expand Up @@ -1241,8 +1250,7 @@ def _get_base_runfiles_for_binary(
required_pyc_files,
implicit_pyc_files,
implicit_pyc_source_files,
extra_common_runfiles,
semantics):
extra_common_runfiles):
"""Returns the set of runfiles necessary prior to executable creation.

NOTE: The term "common runfiles" refers to the runfiles that are common to
Expand All @@ -1264,7 +1272,6 @@ def _get_base_runfiles_for_binary(
files that are used when the implicit pyc files are not.
extra_common_runfiles: List of runfiles; additional runfiles that
will be added to the common runfiles.
semantics: A `BinarySemantics` struct; see `create_binary_semantics_struct`.

Returns:
struct with attributes:
Expand Down Expand Up @@ -1305,6 +1312,9 @@ def _get_base_runfiles_for_binary(
common_runfiles.add_targets(extra_deps)
common_runfiles.add(extra_common_runfiles)

build_data_file = _write_build_data(ctx)
common_runfiles.add(build_data_file)

common_runfiles = common_runfiles.build(ctx)

if _should_create_init_files(ctx):
Expand All @@ -1313,25 +1323,10 @@ def _get_base_runfiles_for_binary(
runfiles = common_runfiles,
)

# Don't include build_data.txt in the non-exe runfiles. The build data
# may contain program-specific content (e.g. target name).
runfiles_with_exe = common_runfiles.merge(ctx.runfiles([executable]))

# Don't include build_data.txt in data runfiles. This allows binaries to
# contain other binaries while still using the same fixed location symlink
# for the build_data.txt file. Really, the fixed location symlink should be
# removed and another way found to locate the underlying build data file.
data_runfiles = runfiles_with_exe

if is_stamping_enabled(ctx) and semantics.should_include_build_data(ctx):
build_data_file, build_data_runfiles = _create_runfiles_with_build_data(
ctx,
semantics.get_central_uncachable_version_file(ctx),
)
default_runfiles = runfiles_with_exe.merge(build_data_runfiles)
else:
build_data_file = None
default_runfiles = runfiles_with_exe
default_runfiles = runfiles_with_exe

return struct(
runfiles_without_exe = common_runfiles,
Expand All @@ -1340,31 +1335,17 @@ def _get_base_runfiles_for_binary(
data_runfiles = data_runfiles,
)

def _create_runfiles_with_build_data(
ctx,
central_uncachable_version_file):
build_data_file = _write_build_data(
ctx,
central_uncachable_version_file,
)
build_data_runfiles = ctx.runfiles(files = [
build_data_file,
])
return build_data_file, build_data_runfiles

def _write_build_data(ctx, central_uncachable_version_file):
# TODO: Remove this logic when a central file is always available
if not central_uncachable_version_file:
version_file = ctx.actions.declare_file(ctx.label.name + "-uncachable_version_file.txt")
_py_builtins.copy_without_caching(
ctx = ctx,
read_from = ctx.version_file,
write_to = version_file,
)
def _write_build_data(ctx):
if is_stamping_enabled(ctx):
# NOTE: ctx.info_file is undocumented; see
# https://github.com/bazelbuild/bazel/issues/9363
info_file = ctx.info_file
version_file = ctx.files._uncachable_version_file[0]
inputs = [info_file, version_file]
else:
version_file = central_uncachable_version_file

direct_inputs = [ctx.info_file, version_file]
inputs = []
info_file = None
version_file = None

# A "constant metadata" file is basically a special file that doesn't
# support change detection logic and reports that it is unchanged. i.e., it
Expand Down Expand Up @@ -1397,22 +1378,26 @@ def _write_build_data(ctx, central_uncachable_version_file):
)

ctx.actions.run(
executable = ctx.executable._build_data_gen,
executable = ctx.executable._build_data_writer,
env = {
# NOTE: ctx.info_file is undocumented; see
# https://github.com/bazelbuild/bazel/issues/9363
"INFO_FILE": ctx.info_file.path,
# Include config id so binaries can e.g. cache content based on how
# they were built.
"CONFIG_ID": ctx.configuration.short_id,
# Include config mode so that binaries can detect if they're
# being used as a build tool or not, allowing for runtime optimizations.
"CONFIG_MODE": "EXEC" if _is_tool_config(ctx) else "TARGET",
"INFO_FILE": info_file.path if info_file else "",
"OUTPUT": build_data.path,
"PLATFORM": cc_helper.find_cpp_toolchain(ctx).toolchain_id,
# Include this so it's explicit, otherwise, one has to detect
# this by looking for the absense of info_file keys.
"STAMPED": "TRUE" if is_stamping_enabled(ctx) else "FALSE",
"TARGET": str(ctx.label),
"VERSION_FILE": version_file.path,
"VERSION_FILE": version_file.path if version_file else "",
},
inputs = depset(
direct = direct_inputs,
),
inputs = depset(direct = inputs),
outputs = [build_data],
mnemonic = "PyWriteBuildData",
progress_message = "Generating %{label} build_data.txt",
progress_message = "Reticulating %{label} build data",
)
return build_data

Expand Down Expand Up @@ -1607,6 +1592,9 @@ def is_stamping_enabled(ctx):
Returns:
bool; True if stamping is enabled, False if not.
"""

# Always ignore stamping for exec config. This mitigates stamping
# invalidating build action caching.
if _is_tool_config(ctx):
return False

Expand All @@ -1616,8 +1604,9 @@ def is_stamping_enabled(ctx):
elif stamp == 0:
return False
elif stamp == -1:
# NOTE: Undocumented API; private to builtins
return ctx.configuration.stamp_binaries
# NOTE: ctx.configuration.stamp_binaries() exposes this, but that's
# a private API. To workaround, it'd been eposed via py_internal.
return py_internal.stamp_binaries(ctx)
else:
fail("Unsupported `stamp` value: {}".format(stamp))

Expand Down Expand Up @@ -1770,6 +1759,9 @@ def _transition_executable_impl(settings, attr):

if attr.python_version and attr.python_version not in ("PY2", "PY3"):
settings[labels.PYTHON_VERSION] = attr.python_version

if attr.stamp != -1:
settings["//command_line_option:stamp"] = str(attr.stamp)
return settings

def create_executable_rule(*, attrs, **kwargs):
Expand Down Expand Up @@ -1820,8 +1812,14 @@ def create_executable_rule_builder(implementation, **kwargs):
] + ([ruleb.ToolchainType(_LAUNCHER_MAKER_TOOLCHAIN_TYPE)] if rp_config.bazel_9_or_later else []),
cfg = dict(
implementation = _transition_executable_impl,
inputs = TRANSITION_LABELS + [labels.PYTHON_VERSION],
outputs = TRANSITION_LABELS + [labels.PYTHON_VERSION],
inputs = TRANSITION_LABELS + [
labels.PYTHON_VERSION,
"//command_line_option:stamp",
],
outputs = TRANSITION_LABELS + [
labels.PYTHON_VERSION,
"//command_line_option:stamp",
],
),
**kwargs
)
Expand Down
Loading