diff --git a/.github/workflows/aarch64-ebclfsa.yml b/.github/workflows/aarch64-ebclfsa.yml index cf8caed..1fff5aa 100644 --- a/.github/workflows/aarch64-ebclfsa.yml +++ b/.github/workflows/aarch64-ebclfsa.yml @@ -39,3 +39,6 @@ jobs: - name: Bazel Build (basic) run: | bazel build --lockfile_mode=error --config=aarch64-linux-ebclfsa -- //:language_and_standards_tests //:feature_verification_tests + - name: Guardrail (no legacy features) + run: | + bazel test --lockfile_mode=error --config=aarch64-linux-ebclfsa -- //:guardrail_tests diff --git a/.github/workflows/aarch64-linux.yml b/.github/workflows/aarch64-linux.yml index 5c4029b..e6afa5f 100644 --- a/.github/workflows/aarch64-linux.yml +++ b/.github/workflows/aarch64-linux.yml @@ -39,3 +39,6 @@ jobs: - name: Bazel Build (basic) run: | bazel build --config=aarch64-linux -- //:language_and_standards_tests //:feature_verification_tests + - name: Guardrail (no legacy features) + run: | + bazel test --config=aarch64-linux -- //:guardrail_tests diff --git a/.github/workflows/autosd.yml b/.github/workflows/autosd.yml index 74b7ba7..8977d51 100644 --- a/.github/workflows/autosd.yml +++ b/.github/workflows/autosd.yml @@ -39,3 +39,6 @@ jobs: - name: Bazel Build (basic) run: | bazel build --lockfile_mode=error --config=x86_64-linux-autosd10 -- //:language_and_standards_tests //:feature_verification_tests + - name: Guardrail (no legacy features) + run: | + bazel test --lockfile_mode=error --config=x86_64-linux-autosd10 -- //:guardrail_tests diff --git a/.github/workflows/x86_64-linux.yml b/.github/workflows/x86_64-linux.yml index aa466f9..98c8969 100644 --- a/.github/workflows/x86_64-linux.yml +++ b/.github/workflows/x86_64-linux.yml @@ -39,9 +39,12 @@ jobs: - name: Bazel Build (basic) run: | bazel build --lockfile_mode=error --config=x86_64-linux -- //:language_and_standards_tests //:feature_verification_tests + - name: Guardrail (no legacy features) + run: | + bazel test --lockfile_mode=error --config=x86_64-linux -- //:guardrail_tests - name: Bazel Test run: | - bazel test --config=x86_64-linux -- //:language_and_standards_tests //:feature_verification_tests + bazel test --lockfile_mode=error --config=x86_64-linux -- //:language_and_standards_tests //:feature_verification_tests - name: Bazel Test (force_pic) run: | - bazel test --config=x86_64-linux --force_pic -- //feature_verification/opt_in_features:force_pic_flags_test + bazel test --lockfile_mode=error --config=x86_64-linux --force_pic -- //feature_verification/opt_in_features:force_pic_flags_test diff --git a/docs/tests_and_validation.md b/docs/tests_and_validation.md index eaaecc2..3ddf21e 100644 --- a/docs/tests_and_validation.md +++ b/docs/tests_and_validation.md @@ -50,6 +50,15 @@ generated toolchains and platforms. Defines the `feature_verification_tests` and `language_and_standards_tests` test suites that aggregate the individual test targets. +`tests/guardrails/` + +Holds the explicit-feature regression guardrail +(`no_legacy_features_guard_test`), aggregated by the `guardrail_tests` suite in +`tests/BUILD`. It fails as a test if the active toolchain regresses to relying +on Bazel's implicit legacy C++ features (i.e. loses the `no_legacy_features` +contract). See `tests/guardrails/README.md` for details and how to interpret +failures. + `tests/feature_verification/` C++ targets that verify toolchain features such as preprocessor defines, diff --git a/tests/BUILD b/tests/BUILD index 0274cfa..9f18fb2 100644 --- a/tests/BUILD +++ b/tests/BUILD @@ -52,3 +52,14 @@ test_suite( "//feature_verification:whole_archive_test", ], ) + +# ============================================================================ +# Test Suite for Explicit-Feature Regression Guardrails +# ============================================================================ + +test_suite( + name = "guardrail_tests", + tests = [ + "//guardrails:no_legacy_features_guard_test", + ], +) diff --git a/tests/guardrails/BUILD b/tests/guardrails/BUILD new file mode 100644 index 0000000..a81eb99 --- /dev/null +++ b/tests/guardrails/BUILD @@ -0,0 +1,24 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +"""Regression guardrail for the explicit-feature (`no_legacy_features`) model.""" + +load(":legacy_feature_guard.bzl", "no_legacy_features_guard_test") + +# Validates the C++ toolchain resolved for the current --config. Run it under +# each platform config (x86_64-linux, aarch64-linux, x86_64-qnx, ...) to cover +# the whole toolchain matrix. +no_legacy_features_guard_test( + name = "no_legacy_features_guard_test", + visibility = ["//visibility:public"], +) diff --git a/tests/guardrails/README.md b/tests/guardrails/README.md new file mode 100644 index 0000000..f93f0b5 --- /dev/null +++ b/tests/guardrails/README.md @@ -0,0 +1,81 @@ +# Explicit-Feature Guardrail + +This directory contains an automated guardrail that protects the +**explicit-feature model** established by the toolchain migration. Both the +Linux and QNX toolchains enable the `no_legacy_features` marker so that Bazel +injects **none** of its implicit legacy C/C++ features — every flag comes from a +feature this repository defines explicitly (see +[docs/features.md](../../docs/features.md) and +[docs/migration_guide.md](../../docs/migration_guide.md)). + +The guardrail fails the test if that contract regresses. + +## What it validates + +`no_legacy_features_guard_test` (defined in +[`legacy_feature_guard.bzl`](legacy_feature_guard.bzl)) resolves the C++ +toolchain configured for the current `--config` and checks that: + +1. the `no_legacy_features` marker is still **enabled**, and +2. none of Bazel's implicit legacy features have become active again. It probes + a set of legacy feature names that this repository intentionally never + defines (`legacy_compile_flags`, `legacy_link_flags`, `sysroot`); if any is + enabled, Bazel has re-injected its legacy feature set. + +The verdict is deterministic (derived from the resolved toolchain +configuration) and is surfaced as the **test's exit status**, so it needs no +target hardware and runs for whichever toolchain the active `--config` selects. + +## How to run it + +```bash +cd tests + +# Any toolchain in the matrix — run it as a test. The test binary is a plain +# host-runnable script, so cross-target configs work without target hardware. +bazel test --config=x86_64-linux //:guardrail_tests +bazel test --config=aarch64-linux //:guardrail_tests +bazel test --config=x86_64-qnx //:guardrail_tests +``` + + CI runs the guardrail for every platform config covered by the GitHub Actions + workflows under `.github/workflows/`. + +## Interpreting a failure + +A regression is reported as a normal **test failure**; the test log names the +exact problem, for example: + +``` +EXPLICIT-FEATURE GUARDRAIL FAILED +Problems detected: + - the `no_legacy_features` marker is NOT enabled in the resolved toolchain + configuration; Bazel will inject its implicit legacy C++ features. +``` +or + +``` +Problems detected: + - Bazel legacy feature(s) are enabled again: sysroot. +``` + +To fix it: + +- Re-add / re-enable the `no_legacy_features` feature (`enabled = True`) in the + toolchain config templates under `templates/linux/` and `templates/qnx/`, and + keep it in each template's `features` list. +- Provide any behavior you need through an **explicit** feature rather than + relying on a Bazel legacy default. See + [docs/migration_guide.md](../../docs/migration_guide.md) for the supported + extension mechanisms. + +## Maintenance + +- The list of probed legacy feature names lives in the + `_DEFAULT_FORBIDDEN_LEGACY_FEATURES` constant in + [`legacy_feature_guard.bzl`](legacy_feature_guard.bzl). Only add names that + Bazel injects as legacy features **and** that this repository does not define + itself, otherwise the probe would collide with a legitimate explicit feature. +- The guardrail is intentionally narrow: it checks the explicit-feature contract + only and does not replace the functional feature-verification tests under + `tests/feature_verification/`. diff --git a/tests/guardrails/legacy_feature_guard.bzl b/tests/guardrails/legacy_feature_guard.bzl new file mode 100644 index 0000000..5e7f22b --- /dev/null +++ b/tests/guardrails/legacy_feature_guard.bzl @@ -0,0 +1,147 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +"""Guardrail protecting the explicit-feature (`no_legacy_features`) contract. + +The Linux and QNX toolchains deliberately enable the `no_legacy_features` +marker so that Bazel injects none of its implicit legacy C++ features. This +rule resolves the *active* C++ toolchain and reports a **test failure** if that +contract regresses, either because: + +* the `no_legacy_features` marker is no longer enabled, or +* one of Bazel's implicit legacy features (which this repository never defines) + has become active again. + +The contract is evaluated deterministically from the resolved toolchain +configuration and surfaced as the test's exit status, so it needs no target +hardware and is exercised with `bazel test`. +""" + +load("@rules_cc//cc:defs.bzl", "cc_common") +load("@rules_cc//cc:find_cc_toolchain.bzl", "find_cpp_toolchain", "use_cc_toolchain") + +# The explicit-feature marker that must stay enabled on every toolchain. +_CONTRACT_MARKER = "no_legacy_features" + +# Bazel auto-injects these "legacy" features only when a cc_toolchain does not +# declare `no_legacy_features`. This repository intentionally defines none of +# them, so if any becomes enabled the toolchain has regressed to relying on +# Bazel's legacy C++ feature behavior. Names here are chosen to NOT collide +# with the toolchain's own explicit feature names. +_DEFAULT_FORBIDDEN_LEGACY_FEATURES = [ + "legacy_compile_flags", + "legacy_link_flags", + "sysroot", +] + +def _impl(ctx): + cc_toolchain = find_cpp_toolchain(ctx) + feature_configuration = cc_common.configure_features( + ctx = ctx, + cc_toolchain = cc_toolchain, + requested_features = ctx.features, + unsupported_features = ctx.disabled_features, + ) + + marker_enabled = cc_common.is_enabled( + feature_configuration = feature_configuration, + feature_name = _CONTRACT_MARKER, + ) + + leaked = [ + name + for name in ctx.attr.forbidden_legacy_features + if cc_common.is_enabled( + feature_configuration = feature_configuration, + feature_name = name, + ) + ] + + problems = [] + if not marker_enabled: + problems.append( + "the `{}` marker is NOT enabled in the resolved toolchain configuration; ".format(_CONTRACT_MARKER) + + "Bazel will inject its implicit legacy C++ features.", + ) + if leaked: + problems.append( + "Bazel legacy feature(s) are enabled again: {}.".format(", ".join(leaked)), + ) + + script = ctx.actions.declare_file(ctx.label.name + ".sh") + + if problems: + message = [ + "=======================================================================", + "EXPLICIT-FEATURE GUARDRAIL FAILED", + "=======================================================================", + "Target: {}".format(str(ctx.label)), + "", + "Problems detected:", + ] + message.extend([" - " + p for p in problems]) + message.extend([ + "", + "Expected contract (see docs/migration_guide.md):", + " * The cc_toolchain keeps the `{}` feature (enabled = True) in its".format(_CONTRACT_MARKER), + " `features` list.", + " * None of Bazel's implicit legacy C++ features are active.", + "", + "How to fix:", + " * Re-add / re-enable `{}` in the toolchain config templates".format(_CONTRACT_MARKER), + " under templates/linux/ and templates/qnx/.", + " * Provide any needed behavior through an explicit feature instead of", + " relying on Bazel legacy defaults.", + " * See tests/guardrails/README.md for details.", + "=======================================================================", + ]) + + # Surface the verdict as a test failure (exit 1) rather than a build + # failure. The heredoc delimiter is single-quoted so the message is + # emitted verbatim (no shell expansion of backticks/braces). + content = ( + "#!/usr/bin/env bash\n" + + "cat >&2 <<'GUARDRAIL_EOF'\n" + + "\n".join(message) + "\n" + + "GUARDRAIL_EOF\n" + + "exit 1\n" + ) + else: + content = ( + "#!/usr/bin/env bash\n" + + "echo 'GUARDRAIL OK: {} enabled; no Bazel legacy features leaked ({}).'\n".format( + _CONTRACT_MARKER, + str(ctx.label), + ) + + "exit 0\n" + ) + + ctx.actions.write(output = script, content = content, is_executable = True) + return DefaultInfo(executable = script) + +no_legacy_features_guard_test = rule( + implementation = _impl, + doc = "Fails if the active C++ toolchain regresses to Bazel legacy feature behavior.", + attrs = { + "forbidden_legacy_features": attr.string_list( + default = _DEFAULT_FORBIDDEN_LEGACY_FEATURES, + doc = "Bazel legacy feature names that must never be enabled.", + ), + "_cc_toolchain": attr.label( + default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), + ), + }, + toolchains = use_cc_toolchain(), + fragments = ["cpp"], + test = True, +)