Skip to content

Commit 70f97d8

Browse files
committed
feat: basic build data with stamping
1 parent f7656d1 commit 70f97d8

13 files changed

Lines changed: 245 additions & 80 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ END_UNRELEASED_TEMPLATE
5959
{#v0-0-0-changed}
6060
### Changed
6161
* (binaries/tests) The `PYTHONBREAKPOINT` environment variable is automatically inherited
62+
* (binaries/tests) The {obj}`stamp` attribute now transitions the Bazel builtin
63+
{obj}`--stamp` flag.
6264

6365
{#v0-0-0-fixed}
6466
### Fixed
@@ -68,6 +70,9 @@ END_UNRELEASED_TEMPLATE
6870
### Added
6971
* (binaries/tests) {obj}`--debugger`: allows specifying an extra dependency
7072
to add to binaries/tests for custom debuggers.
73+
* (binaries/tests) Build information is now included in binaries and tests.
74+
Use the `bazel_binary_info` module to access it. The {flag}`--stamp` flag will
75+
add {flag}`--workspace_status` information.
7176

7277
{#v1-8-0}
7378
## [1.8.0] - 2025-12-19

python/private/BUILD.bazel

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ load(":print_toolchain_checksums.bzl", "print_toolchains_checksums")
2121
load(":py_exec_tools_toolchain.bzl", "current_interpreter_executable")
2222
load(":sentinel.bzl", "sentinel")
2323
load(":stamp.bzl", "stamp_build_setting")
24+
load(":uncachable_version_file.bzl", "define_uncachable_version_file")
2425

2526
package(
2627
default_visibility = ["//:__subpackages__"],
@@ -97,6 +98,14 @@ bzl_library(
9798
],
9899
)
99100

101+
filegroup(
102+
name = "build_data_writer",
103+
srcs = select({
104+
"@platforms//os:windows": ["build_data_writer.ps1"],
105+
"//conditions:default": ["build_data_writer.sh"],
106+
}),
107+
)
108+
100109
bzl_library(
101110
name = "builders_bzl",
102111
srcs = ["builders.bzl"],
@@ -681,6 +690,10 @@ bzl_library(
681690
],
682691
)
683692

693+
define_uncachable_version_file(
694+
name = "uncachable_version_file",
695+
)
696+
684697
bzl_library(
685698
name = "version_bzl",
686699
srcs = ["version.bzl"],

python/private/attributes.bzl

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,8 +450,20 @@ Whether to encode build information into the binary. Possible values:
450450
451451
Stamped binaries are not rebuilt unless their dependencies change.
452452
453-
WARNING: Stamping can harm build performance by reducing cache hits and should
453+
Stamped build information can accessed using the `bazel_binary_info` module.
454+
See the [Accessing build information docs] for more information.
455+
456+
:::{warning}
457+
Stamping can harm build performance by reducing cache hits and should
454458
be avoided if possible.
459+
460+
In addition, this transitions the {obj}`--stamp` flag, which can additional
461+
config state overhead.
462+
:::
463+
464+
:::{note}
465+
Stamping of build data output is always disabled for the exec config.
466+
:::
455467
""",
456468
default = -1,
457469
),
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
$OutputPath = $env:OUTPUT
2+
3+
Add-Content -Path $OutputPath -Value "TARGET $env:TARGET"
4+
Add-Content -Path $OutputPath -Value "CONFIG_ID $env:CONFIG_ID"
5+
Add-Content -Path $OutputPath -Value "CONFIG_MODE $env:CONFIG_MODE"
6+
Add-Content -Path $OutputPath -Value "STAMPED $env:STAMPED"
7+
8+
$VersionFilePath = $env:VERSION_FILE
9+
if (-not [string]::IsNullOrEmpty($VersionFilePath)) {
10+
Get-Content -Path $VersionFilePath | Add-Content -Path $OutputPath
11+
}
12+
13+
$InfoFilePath = $env:INFO_FILE
14+
if (-not [string]::IsNullOrEmpty($InfoFilePath)) {
15+
Get-Content -Path $InfoFilePath | Add-Content -Path $OutputPath
16+
}
17+
18+
exit 0
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/sh
2+
3+
echo "TARGET $TARGET" >> $OUTPUT
4+
echo "CONFIG_ID $CONFIG_ID" >> $OUTPUT
5+
echo "CONFIG_MODE $CONFIG_MODE" >> $OUTPUT
6+
echo "STAMPED $STAMPED" >> $OUTPUT
7+
if [ -n "$VERSION_FILE" ]; then
8+
cat "$VERSION_FILE" >> "$OUTPUT"
9+
fi
10+
if [ -n "$INFO_FILE" ]; then
11+
cat "$INFO_FILE" >> "$OUTPUT"
12+
fi
13+
exit 0

python/private/common.bzl

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,34 +43,25 @@ PYTHON_FILE_EXTENSIONS = [
4343

4444
def create_binary_semantics_struct(
4545
*,
46-
get_central_uncachable_version_file,
4746
get_native_deps_dso_name,
48-
should_build_native_deps_dso,
49-
should_include_build_data):
47+
should_build_native_deps_dso):
5048
"""Helper to ensure a semantics struct has all necessary fields.
5149
5250
Call this instead of a raw call to `struct(...)`; it'll help ensure all
5351
the necessary functions are being correctly provided.
5452
5553
Args:
56-
get_central_uncachable_version_file: Callable that returns an optional
57-
Artifact; this artifact is special: it is never cached and is a copy
58-
of `ctx.version_file`; see py_builtins.copy_without_caching
5954
get_native_deps_dso_name: Callable that returns a string, which is the
6055
basename (with extension) of the native deps DSO library.
6156
should_build_native_deps_dso: Callable that returns bool; True if
6257
building a native deps DSO is supported, False if not.
63-
should_include_build_data: Callable that returns bool; True if
64-
build data should be generated, False if not.
6558
Returns:
6659
A "BinarySemantics" struct.
6760
"""
6861
return struct(
6962
# keep-sorted
70-
get_central_uncachable_version_file = get_central_uncachable_version_file,
7163
get_native_deps_dso_name = get_native_deps_dso_name,
7264
should_build_native_deps_dso = should_build_native_deps_dso,
73-
should_include_build_data = should_include_build_data,
7465
)
7566

7667
def create_cc_details_struct(

python/private/py_executable.bzl

Lines changed: 58 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,11 @@ accepting arbitrary Python versions.
206206
allow_single_file = True,
207207
default = "@bazel_tools//tools/python:python_bootstrap_template.txt",
208208
),
209+
"_build_data_writer": lambda: attrb.Label(
210+
default = "//python/private:build_data_writer",
211+
executable = True,
212+
cfg = "exec",
213+
),
209214
"_debugger_flag": lambda: attrb.Label(
210215
default = "//python/private:debugger_if_target_config",
211216
providers = [PyInfo],
@@ -226,6 +231,10 @@ accepting arbitrary Python versions.
226231
"_python_version_flag": lambda: attrb.Label(
227232
default = labels.PYTHON_VERSION,
228233
),
234+
"_uncachable_version_file": lambda: attrb.Label(
235+
default = "//python/private:uncachable_version_file",
236+
allow_files = True,
237+
),
229238
"_venvs_use_declare_symlink_flag": lambda: attrb.Label(
230239
default = labels.VENVS_USE_DECLARE_SYMLINK,
231240
providers = [BuildSettingInfo],
@@ -271,10 +280,8 @@ def py_executable_impl(ctx, *, is_test, inherited_environment):
271280
def create_binary_semantics():
272281
return create_binary_semantics_struct(
273282
# keep-sorted start
274-
get_central_uncachable_version_file = lambda ctx: None,
275283
get_native_deps_dso_name = _get_native_deps_dso_name,
276284
should_build_native_deps_dso = lambda ctx: False,
277-
should_include_build_data = lambda ctx: False,
278285
# keep-sorted end
279286
)
280287

@@ -336,6 +343,7 @@ def _create_executable(
336343
imports = imports,
337344
runtime_details = runtime_details,
338345
venv = venv,
346+
build_data_file = runfiles_details.build_data_file,
339347
)
340348
extra_runfiles = ctx.runfiles(
341349
[stage2_bootstrap] + (
@@ -648,6 +656,7 @@ def _create_stage2_bootstrap(
648656
main_py,
649657
imports,
650658
runtime_details,
659+
build_data_file,
651660
venv):
652661
output = ctx.actions.declare_file(
653662
# Prepend with underscore to prevent pytest from trying to
@@ -668,6 +677,7 @@ def _create_stage2_bootstrap(
668677
template = template,
669678
output = output,
670679
substitutions = {
680+
"%build_data_file%": runfiles_root_path(ctx, build_data_file.short_path),
671681
"%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime),
672682
"%import_all%": "True" if read_possibly_native_flag(ctx, "python_import_all_repositories") else "False",
673683
"%imports%": ":".join(imports.to_list()),
@@ -1241,8 +1251,7 @@ def _get_base_runfiles_for_binary(
12411251
required_pyc_files,
12421252
implicit_pyc_files,
12431253
implicit_pyc_source_files,
1244-
extra_common_runfiles,
1245-
semantics):
1254+
extra_common_runfiles):
12461255
"""Returns the set of runfiles necessary prior to executable creation.
12471256
12481257
NOTE: The term "common runfiles" refers to the runfiles that are common to
@@ -1264,7 +1273,6 @@ def _get_base_runfiles_for_binary(
12641273
files that are used when the implicit pyc files are not.
12651274
extra_common_runfiles: List of runfiles; additional runfiles that
12661275
will be added to the common runfiles.
1267-
semantics: A `BinarySemantics` struct; see `create_binary_semantics_struct`.
12681276
12691277
Returns:
12701278
struct with attributes:
@@ -1305,6 +1313,9 @@ def _get_base_runfiles_for_binary(
13051313
common_runfiles.add_targets(extra_deps)
13061314
common_runfiles.add(extra_common_runfiles)
13071315

1316+
build_data_file = _write_build_data(ctx)
1317+
common_runfiles.add(build_data_file)
1318+
13081319
common_runfiles = common_runfiles.build(ctx)
13091320

13101321
if _should_create_init_files(ctx):
@@ -1313,25 +1324,10 @@ def _get_base_runfiles_for_binary(
13131324
runfiles = common_runfiles,
13141325
)
13151326

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

1320-
# Don't include build_data.txt in data runfiles. This allows binaries to
1321-
# contain other binaries while still using the same fixed location symlink
1322-
# for the build_data.txt file. Really, the fixed location symlink should be
1323-
# removed and another way found to locate the underlying build data file.
13241329
data_runfiles = runfiles_with_exe
1325-
1326-
if is_stamping_enabled(ctx) and semantics.should_include_build_data(ctx):
1327-
build_data_file, build_data_runfiles = _create_runfiles_with_build_data(
1328-
ctx,
1329-
semantics.get_central_uncachable_version_file(ctx),
1330-
)
1331-
default_runfiles = runfiles_with_exe.merge(build_data_runfiles)
1332-
else:
1333-
build_data_file = None
1334-
default_runfiles = runfiles_with_exe
1330+
default_runfiles = runfiles_with_exe
13351331

13361332
return struct(
13371333
runfiles_without_exe = common_runfiles,
@@ -1340,31 +1336,18 @@ def _get_base_runfiles_for_binary(
13401336
data_runfiles = data_runfiles,
13411337
)
13421338

1343-
def _create_runfiles_with_build_data(
1344-
ctx,
1345-
central_uncachable_version_file):
1346-
build_data_file = _write_build_data(
1347-
ctx,
1348-
central_uncachable_version_file,
1349-
)
1350-
build_data_runfiles = ctx.runfiles(files = [
1351-
build_data_file,
1352-
])
1353-
return build_data_file, build_data_runfiles
1354-
1355-
def _write_build_data(ctx, central_uncachable_version_file):
1356-
# TODO: Remove this logic when a central file is always available
1357-
if not central_uncachable_version_file:
1358-
version_file = ctx.actions.declare_file(ctx.label.name + "-uncachable_version_file.txt")
1359-
_py_builtins.copy_without_caching(
1360-
ctx = ctx,
1361-
read_from = ctx.version_file,
1362-
write_to = version_file,
1363-
)
1339+
def _write_build_data(ctx):
1340+
inputs = []
1341+
if is_stamping_enabled(ctx):
1342+
# NOTE: ctx.info_file is undocumented; see
1343+
# https://github.com/bazelbuild/bazel/issues/9363
1344+
info_file = ctx.info_file
1345+
version_file = ctx.files._uncachable_version_file[0]
1346+
inputs = [info_file, version_file]
13641347
else:
1365-
version_file = central_uncachable_version_file
1366-
1367-
direct_inputs = [ctx.info_file, version_file]
1348+
inputs = []
1349+
info_file = None
1350+
version_file = None
13681351

13691352
# A "constant metadata" file is basically a special file that doesn't
13701353
# support change detection logic and reports that it is unchanged. i.e., it
@@ -1397,19 +1380,23 @@ def _write_build_data(ctx, central_uncachable_version_file):
13971380
)
13981381

13991382
ctx.actions.run(
1400-
executable = ctx.executable._build_data_gen,
1383+
executable = ctx.executable._build_data_writer,
14011384
env = {
1402-
# NOTE: ctx.info_file is undocumented; see
1403-
# https://github.com/bazelbuild/bazel/issues/9363
1404-
"INFO_FILE": ctx.info_file.path,
1385+
# Include config id so binaries can e.g. cache content based on how
1386+
# they were built.
1387+
"CONFIG_ID": ctx.configuration.short_id,
1388+
# Include config mode so that binaries can detect if they're
1389+
# being used as a build tool or not, allowing for runtime optimizations.
1390+
"CONFIG_MODE": "EXEC" if _is_tool_config(ctx) else "TARGET",
1391+
"INFO_FILE": info_file.path if info_file else "",
14051392
"OUTPUT": build_data.path,
1406-
"PLATFORM": cc_helper.find_cpp_toolchain(ctx).toolchain_id,
1393+
# Include this so it's explicit, otherwise, one has to detect
1394+
# this by looking for the absense of info_file keys.
1395+
"STAMPED": "TRUE" if is_stamping_enabled(ctx) else "FALSE",
14071396
"TARGET": str(ctx.label),
1408-
"VERSION_FILE": version_file.path,
1397+
"VERSION_FILE": version_file.path if version_file else "",
14091398
},
1410-
inputs = depset(
1411-
direct = direct_inputs,
1412-
),
1399+
inputs = depset(direct = inputs),
14131400
outputs = [build_data],
14141401
mnemonic = "PyWriteBuildData",
14151402
progress_message = "Generating %{label} build_data.txt",
@@ -1607,6 +1594,9 @@ def is_stamping_enabled(ctx):
16071594
Returns:
16081595
bool; True if stamping is enabled, False if not.
16091596
"""
1597+
1598+
# Always ignore stamping for exec config. This mitigates stamping
1599+
# invalidating build action caching.
16101600
if _is_tool_config(ctx):
16111601
return False
16121602

@@ -1616,8 +1606,9 @@ def is_stamping_enabled(ctx):
16161606
elif stamp == 0:
16171607
return False
16181608
elif stamp == -1:
1619-
# NOTE: Undocumented API; private to builtins
1620-
return ctx.configuration.stamp_binaries
1609+
# NOTE: ctx.configuration.stamp_binaries() exposes this, but that's
1610+
# a private API. To workaround, it'd been eposed via py_internal.
1611+
return py_internal.stamp_binaries(ctx)
16211612
else:
16221613
fail("Unsupported `stamp` value: {}".format(stamp))
16231614

@@ -1770,6 +1761,9 @@ def _transition_executable_impl(settings, attr):
17701761

17711762
if attr.python_version and attr.python_version not in ("PY2", "PY3"):
17721763
settings[labels.PYTHON_VERSION] = attr.python_version
1764+
1765+
if attr.stamp != -1:
1766+
settings["//command_line_option:stamp"] = str(attr.stamp)
17731767
return settings
17741768

17751769
def create_executable_rule(*, attrs, **kwargs):
@@ -1820,8 +1814,14 @@ def create_executable_rule_builder(implementation, **kwargs):
18201814
] + ([ruleb.ToolchainType(_LAUNCHER_MAKER_TOOLCHAIN_TYPE)] if rp_config.bazel_9_or_later else []),
18211815
cfg = dict(
18221816
implementation = _transition_executable_impl,
1823-
inputs = TRANSITION_LABELS + [labels.PYTHON_VERSION],
1824-
outputs = TRANSITION_LABELS + [labels.PYTHON_VERSION],
1817+
inputs = TRANSITION_LABELS + [
1818+
labels.PYTHON_VERSION,
1819+
"//command_line_option:stamp",
1820+
],
1821+
outputs = TRANSITION_LABELS + [
1822+
labels.PYTHON_VERSION,
1823+
"//command_line_option:stamp",
1824+
],
18251825
),
18261826
**kwargs
18271827
)

0 commit comments

Comments
 (0)