Skip to content

Commit 7e8a02e

Browse files
committed
Add guardrail against Bazel legacy C++ feature regressions
Add no_legacy_features_guard_test, an analysis-driven test rule under tests/guardrails/ that resolves the active C++ toolchain and fails as a test if the explicit-feature contract regresses -- i.e. the no_legacy_features marker is no longer enabled or a Bazel implicit legacy feature (legacy_compile_flags, legacy_link_flags, sysroot) becomes active again. The verdict is deterministic and surfaced via the test's exit status, so it needs no target hardware and runs on any --config. Wire the new guardrail_tests suite into every platform CI workflow and document what it validates and how to interpret failures in tests/guardrails/README.md and docs/tests_and_validation.md. resolves #82
1 parent e403417 commit 7e8a02e

9 files changed

Lines changed: 286 additions & 2 deletions

File tree

.github/workflows/aarch64-ebclfsa.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,6 @@ jobs:
3939
- name: Bazel Build (basic)
4040
run: |
4141
bazel build --lockfile_mode=error --config=aarch64-linux-ebclfsa -- //:language_and_standards_tests //:feature_verification_tests
42+
- name: Guardrail (no legacy features)
43+
run: |
44+
bazel test --lockfile_mode=error --config=aarch64-linux-ebclfsa -- //:guardrail_tests

.github/workflows/aarch64-linux.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,6 @@ jobs:
3939
- name: Bazel Build (basic)
4040
run: |
4141
bazel build --config=aarch64-linux -- //:language_and_standards_tests //:feature_verification_tests
42+
- name: Guardrail (no legacy features)
43+
run: |
44+
bazel test --config=aarch64-linux -- //:guardrail_tests

.github/workflows/autosd.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,6 @@ jobs:
3939
- name: Bazel Build (basic)
4040
run: |
4141
bazel build --lockfile_mode=error --config=x86_64-linux-autosd10 -- //:language_and_standards_tests //:feature_verification_tests
42+
- name: Guardrail (no legacy features)
43+
run: |
44+
bazel test --lockfile_mode=error --config=x86_64-linux-autosd10 -- //:guardrail_tests

.github/workflows/x86_64-linux.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,12 @@ jobs:
3939
- name: Bazel Build (basic)
4040
run: |
4141
bazel build --lockfile_mode=error --config=x86_64-linux -- //:language_and_standards_tests //:feature_verification_tests
42+
- name: Guardrail (no legacy features)
43+
run: |
44+
bazel test --lockfile_mode=error --config=x86_64-linux -- //:guardrail_tests
4245
- name: Bazel Test
4346
run: |
44-
bazel test --config=x86_64-linux -- //:language_and_standards_tests //:feature_verification_tests
47+
bazel test --lockfile_mode=error --config=x86_64-linux -- //:language_and_standards_tests //:feature_verification_tests
4548
- name: Bazel Test (force_pic)
4649
run: |
47-
bazel test --config=x86_64-linux --force_pic -- //feature_verification/opt_in_features:force_pic_flags_test
50+
bazel test --lockfile_mode=error --config=x86_64-linux --force_pic -- //feature_verification/opt_in_features:force_pic_flags_test

docs/tests_and_validation.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ generated toolchains and platforms.
5050
Defines the `feature_verification_tests` and `language_and_standards_tests`
5151
test suites that aggregate the individual test targets.
5252

53+
`tests/guardrails/`
54+
55+
Holds the explicit-feature regression guardrail
56+
(`no_legacy_features_guard_test`), aggregated by the `guardrail_tests` suite in
57+
`tests/BUILD`. It fails as a test if the active toolchain regresses to relying
58+
on Bazel's implicit legacy C++ features (i.e. loses the `no_legacy_features`
59+
contract). See `tests/guardrails/README.md` for details and how to interpret
60+
failures.
61+
5362
`tests/feature_verification/`
5463

5564
C++ targets that verify toolchain features such as preprocessor defines,

tests/BUILD

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,14 @@ test_suite(
5252
"//feature_verification:whole_archive_test",
5353
],
5454
)
55+
56+
# ============================================================================
57+
# Test Suite for Explicit-Feature Regression Guardrails
58+
# ============================================================================
59+
60+
test_suite(
61+
name = "guardrail_tests",
62+
tests = [
63+
"//guardrails:no_legacy_features_guard_test",
64+
],
65+
)

tests/guardrails/BUILD

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# *******************************************************************************
2+
# Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
#
4+
# See the NOTICE file(s) distributed with this work for additional
5+
# information regarding copyright ownership.
6+
#
7+
# This program and the accompanying materials are made available under the
8+
# terms of the Apache License Version 2.0 which is available at
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# SPDX-License-Identifier: Apache-2.0
12+
# *******************************************************************************
13+
14+
"""Regression guardrail for the explicit-feature (`no_legacy_features`) model."""
15+
16+
load(":legacy_feature_guard.bzl", "no_legacy_features_guard_test")
17+
18+
# Validates the C++ toolchain resolved for the current --config. Run it under
19+
# each platform config (x86_64-linux, aarch64-linux, x86_64-qnx, ...) to cover
20+
# the whole toolchain matrix.
21+
no_legacy_features_guard_test(
22+
name = "no_legacy_features_guard_test",
23+
visibility = ["//visibility:public"],
24+
)

tests/guardrails/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Explicit-Feature Guardrail
2+
3+
This directory contains an automated guardrail that protects the
4+
**explicit-feature model** established by the toolchain migration. Both the
5+
Linux and QNX toolchains enable the `no_legacy_features` marker so that Bazel
6+
injects **none** of its implicit legacy C/C++ features — every flag comes from a
7+
feature this repository defines explicitly (see
8+
[docs/features.md](../../docs/features.md) and
9+
[docs/migration_guide.md](../../docs/migration_guide.md)).
10+
11+
The guardrail fails the test if that contract regresses.
12+
13+
## What it validates
14+
15+
`no_legacy_features_guard_test` (defined in
16+
[`legacy_feature_guard.bzl`](legacy_feature_guard.bzl)) resolves the C++
17+
toolchain configured for the current `--config` and checks that:
18+
19+
1. the `no_legacy_features` marker is still **enabled**, and
20+
2. none of Bazel's implicit legacy features have become active again. It probes
21+
a set of legacy feature names that this repository intentionally never
22+
defines (`legacy_compile_flags`, `legacy_link_flags`, `sysroot`); if any is
23+
enabled, Bazel has re-injected its legacy feature set.
24+
25+
The verdict is deterministic (derived from the resolved toolchain
26+
configuration) and is surfaced as the **test's exit status**, so it needs no
27+
target hardware and runs for whichever toolchain the active `--config` selects.
28+
29+
## How to run it
30+
31+
```bash
32+
cd tests
33+
34+
# Any toolchain in the matrix — run it as a test. The test binary is a plain
35+
# host-runnable script, so cross-target configs work without target hardware.
36+
bazel test --config=x86_64-linux //:guardrail_tests
37+
bazel test --config=aarch64-linux //:guardrail_tests
38+
bazel test --config=x86_64-qnx //:guardrail_tests
39+
```
40+
41+
CI runs the guardrail for every platform config covered by the GitHub Actions
42+
workflows under `.github/workflows/`.
43+
44+
## Interpreting a failure
45+
46+
A regression is reported as a normal **test failure**; the test log names the
47+
exact problem, for example:
48+
49+
```
50+
EXPLICIT-FEATURE GUARDRAIL FAILED
51+
Problems detected:
52+
- the `no_legacy_features` marker is NOT enabled in the resolved toolchain
53+
configuration; Bazel will inject its implicit legacy C++ features.
54+
```
55+
or
56+
57+
```
58+
Problems detected:
59+
- Bazel legacy feature(s) are enabled again: sysroot.
60+
```
61+
62+
To fix it:
63+
64+
- Re-add / re-enable the `no_legacy_features` feature (`enabled = True`) in the
65+
toolchain config templates under `templates/linux/` and `templates/qnx/`, and
66+
keep it in each template's `features` list.
67+
- Provide any behavior you need through an **explicit** feature rather than
68+
relying on a Bazel legacy default. See
69+
[docs/migration_guide.md](../../docs/migration_guide.md) for the supported
70+
extension mechanisms.
71+
72+
## Maintenance
73+
74+
- The list of probed legacy feature names lives in the
75+
`_DEFAULT_FORBIDDEN_LEGACY_FEATURES` constant in
76+
[`legacy_feature_guard.bzl`](legacy_feature_guard.bzl). Only add names that
77+
Bazel injects as legacy features **and** that this repository does not define
78+
itself, otherwise the probe would collide with a legitimate explicit feature.
79+
- The guardrail is intentionally narrow: it checks the explicit-feature contract
80+
only and does not replace the functional feature-verification tests under
81+
`tests/feature_verification/`.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# *******************************************************************************
2+
# Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
#
4+
# See the NOTICE file(s) distributed with this work for additional
5+
# information regarding copyright ownership.
6+
#
7+
# This program and the accompanying materials are made available under the
8+
# terms of the Apache License Version 2.0 which is available at
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# SPDX-License-Identifier: Apache-2.0
12+
# *******************************************************************************
13+
14+
"""Guardrail protecting the explicit-feature (`no_legacy_features`) contract.
15+
16+
The Linux and QNX toolchains deliberately enable the `no_legacy_features`
17+
marker so that Bazel injects none of its implicit legacy C++ features. This
18+
rule resolves the *active* C++ toolchain and reports a **test failure** if that
19+
contract regresses, either because:
20+
21+
* the `no_legacy_features` marker is no longer enabled, or
22+
* one of Bazel's implicit legacy features (which this repository never defines)
23+
has become active again.
24+
25+
The contract is evaluated deterministically from the resolved toolchain
26+
configuration and surfaced as the test's exit status, so it needs no target
27+
hardware and is exercised with `bazel test`.
28+
"""
29+
30+
load("@rules_cc//cc:defs.bzl", "cc_common")
31+
load("@rules_cc//cc:find_cc_toolchain.bzl", "find_cpp_toolchain", "use_cc_toolchain")
32+
33+
# The explicit-feature marker that must stay enabled on every toolchain.
34+
_CONTRACT_MARKER = "no_legacy_features"
35+
36+
# Bazel auto-injects these "legacy" features only when a cc_toolchain does not
37+
# declare `no_legacy_features`. This repository intentionally defines none of
38+
# them, so if any becomes enabled the toolchain has regressed to relying on
39+
# Bazel's legacy C++ feature behavior. Names here are chosen to NOT collide
40+
# with the toolchain's own explicit feature names.
41+
_DEFAULT_FORBIDDEN_LEGACY_FEATURES = [
42+
"legacy_compile_flags",
43+
"legacy_link_flags",
44+
"sysroot",
45+
]
46+
47+
def _impl(ctx):
48+
cc_toolchain = find_cpp_toolchain(ctx)
49+
feature_configuration = cc_common.configure_features(
50+
ctx = ctx,
51+
cc_toolchain = cc_toolchain,
52+
requested_features = ctx.features,
53+
unsupported_features = ctx.disabled_features,
54+
)
55+
56+
marker_enabled = cc_common.is_enabled(
57+
feature_configuration = feature_configuration,
58+
feature_name = _CONTRACT_MARKER,
59+
)
60+
61+
leaked = [
62+
name
63+
for name in ctx.attr.forbidden_legacy_features
64+
if cc_common.is_enabled(
65+
feature_configuration = feature_configuration,
66+
feature_name = name,
67+
)
68+
]
69+
70+
problems = []
71+
if not marker_enabled:
72+
problems.append(
73+
"the `{}` marker is NOT enabled in the resolved toolchain configuration; ".format(_CONTRACT_MARKER) +
74+
"Bazel will inject its implicit legacy C++ features.",
75+
)
76+
if leaked:
77+
problems.append(
78+
"Bazel legacy feature(s) are enabled again: {}.".format(", ".join(leaked)),
79+
)
80+
81+
script = ctx.actions.declare_file(ctx.label.name + ".sh")
82+
83+
if problems:
84+
message = [
85+
"=======================================================================",
86+
"EXPLICIT-FEATURE GUARDRAIL FAILED",
87+
"=======================================================================",
88+
"Target: {}".format(str(ctx.label)),
89+
"",
90+
"Problems detected:",
91+
]
92+
message.extend([" - " + p for p in problems])
93+
message.extend([
94+
"",
95+
"Expected contract (see docs/migration_guide.md):",
96+
" * The cc_toolchain keeps the `{}` feature (enabled = True) in its".format(_CONTRACT_MARKER),
97+
" `features` list.",
98+
" * None of Bazel's implicit legacy C++ features are active.",
99+
"",
100+
"How to fix:",
101+
" * Re-add / re-enable `{}` in the toolchain config templates".format(_CONTRACT_MARKER),
102+
" under templates/linux/ and templates/qnx/.",
103+
" * Provide any needed behavior through an explicit feature instead of",
104+
" relying on Bazel legacy defaults.",
105+
" * See tests/guardrails/README.md for details.",
106+
"=======================================================================",
107+
])
108+
109+
# Surface the verdict as a test failure (exit 1) rather than a build
110+
# failure. The heredoc delimiter is single-quoted so the message is
111+
# emitted verbatim (no shell expansion of backticks/braces).
112+
content = (
113+
"#!/usr/bin/env bash\n" +
114+
"cat >&2 <<'GUARDRAIL_EOF'\n" +
115+
"\n".join(message) + "\n" +
116+
"GUARDRAIL_EOF\n" +
117+
"exit 1\n"
118+
)
119+
else:
120+
content = (
121+
"#!/usr/bin/env bash\n" +
122+
"echo 'GUARDRAIL OK: {} enabled; no Bazel legacy features leaked ({}).'\n".format(
123+
_CONTRACT_MARKER,
124+
str(ctx.label),
125+
) +
126+
"exit 0\n"
127+
)
128+
129+
ctx.actions.write(output = script, content = content, is_executable = True)
130+
return DefaultInfo(executable = script)
131+
132+
no_legacy_features_guard_test = rule(
133+
implementation = _impl,
134+
doc = "Fails if the active C++ toolchain regresses to Bazel legacy feature behavior.",
135+
attrs = {
136+
"forbidden_legacy_features": attr.string_list(
137+
default = _DEFAULT_FORBIDDEN_LEGACY_FEATURES,
138+
doc = "Bazel legacy feature names that must never be enabled.",
139+
),
140+
"_cc_toolchain": attr.label(
141+
default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
142+
),
143+
},
144+
toolchains = use_cc_toolchain(),
145+
fragments = ["cpp"],
146+
test = True,
147+
)

0 commit comments

Comments
 (0)