From 295a47bc99a62129219c21615b5dd0802356e0fa Mon Sep 17 00:00:00 2001 From: Liam Merino <35940408+MerinoSheep@users.noreply.github.com> Date: Mon, 27 Jul 2026 04:34:53 +0000 Subject: [PATCH 1/5] fix(py): skip self-transition write for targets without an explicit Python pin _python_transition_impl always wrote a resolved Python version into PYTHON_VERSION_FLAG/_RPY_VERSION_FLAG, even for targets with no python_version attribute. Writing a resolved value into a flag that was previously unset/default forks a new configuration for the target's entire transitive closure -- including cc_library deps reached through Python C-extensions (e.g. pybind11 modules) -- duplicating every native compile action in that subtree. Pass the two flags through unchanged when attr.python_version is unset, so the transition is a true no-op for unpinned targets. Targets that do pin a version still fork exactly as before. The now-unused _python_version helper is removed. Validated against v1.10.0: duplicate -ST- config CppCompile actions eliminated entirely (target-config 962 -> 0, exec-config 584 -> 0) on a large cc-heavy target with pybind11/rclpy deps. --- py/private/transitions.bzl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/py/private/transitions.bzl b/py/private/transitions.bzl index c81f1dc38..bb2fd4a04 100644 --- a/py/private/transitions.bzl +++ b/py/private/transitions.bzl @@ -19,9 +19,6 @@ def _baseline(settings, flag, current): return current return baseline -def _python_version(settings): - return settings[PYTHON_VERSION_FLAG] or settings[_RPY_VERSION_FLAG] - def _python_transition_impl(settings, attr): acc = {} acc[_PYTHON_VERSION_BASELINE_FLAG] = _baseline( @@ -36,11 +33,14 @@ def _python_transition_impl(settings, attr): ) if attr.python_version: version = str(attr.python_version) + acc[PYTHON_VERSION_FLAG] = version + acc[_RPY_VERSION_FLAG] = version else: - version = _python_version(settings) - - acc[PYTHON_VERSION_FLAG] = version - acc[_RPY_VERSION_FLAG] = version + # No explicit pin: pass the flags through unchanged rather than + # writing a resolved value, which can flip them from unset/default + # and fork a new configuration for the whole transitive closure. + acc[PYTHON_VERSION_FLAG] = settings[PYTHON_VERSION_FLAG] + acc[_RPY_VERSION_FLAG] = settings[_RPY_VERSION_FLAG] # Set the dep_group transition. The attr is only present on `py_venv` # (rules without it propagate the inherited setting; `py_venv_exec` From b5c961c1100f64dc4abc9e239ca442199e9a5f13 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Tue, 11 Aug 2026 14:01:37 -0700 Subject: [PATCH 2/5] test(e2e): cover noop default transition --- e2e/cases/noop-default-transition/BUILD.bazel | 13 +++++++++++++ e2e/cases/noop-default-transition/main.py | 1 + e2e/cases/noop-default-transition/native_dep.cc | 3 +++ e2e/cases/noop-default-transition/test.sh | 12 ++++++++++++ 4 files changed, 29 insertions(+) create mode 100644 e2e/cases/noop-default-transition/BUILD.bazel create mode 100644 e2e/cases/noop-default-transition/main.py create mode 100644 e2e/cases/noop-default-transition/native_dep.cc create mode 100755 e2e/cases/noop-default-transition/test.sh diff --git a/e2e/cases/noop-default-transition/BUILD.bazel b/e2e/cases/noop-default-transition/BUILD.bazel new file mode 100644 index 000000000..efd923571 --- /dev/null +++ b/e2e/cases/noop-default-transition/BUILD.bazel @@ -0,0 +1,13 @@ +load("@aspect_rules_py//py:defs.bzl", "py_binary") + +cc_library( + name = "native_dep", + srcs = ["native_dep.cc"], +) + +py_binary( + name = "unversioned", + srcs = ["main.py"], + main = "main.py", + deps = [":native_dep"], +) diff --git a/e2e/cases/noop-default-transition/main.py b/e2e/cases/noop-default-transition/main.py new file mode 100644 index 000000000..c9f9b6de4 --- /dev/null +++ b/e2e/cases/noop-default-transition/main.py @@ -0,0 +1 @@ +print("ok") diff --git a/e2e/cases/noop-default-transition/native_dep.cc b/e2e/cases/noop-default-transition/native_dep.cc new file mode 100644 index 000000000..c21ce4721 --- /dev/null +++ b/e2e/cases/noop-default-transition/native_dep.cc @@ -0,0 +1,3 @@ +int native_dep() { + return 0; +} diff --git a/e2e/cases/noop-default-transition/test.sh b/e2e/cases/noop-default-transition/test.sh new file mode 100755 index 000000000..df36a0316 --- /dev/null +++ b/e2e/cases/noop-default-transition/test.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# An unpinned Python terminal must not put its native deps in a second target +# configuration. The native library is requested directly and through py_binary; +# both paths must share one CppCompile action. +set -euo pipefail + +cd "$(dirname "$0")/.." + +BAZEL="${BAZEL:-bazel}" +count="$($BAZEL aquery --lockfile_mode=off 'mnemonic(CppCompile, //noop-default-transition:native_dep + //noop-default-transition:unversioned)' | grep -c '^ Mnemonic: CppCompile$')" + +test "$count" -eq 1 From 0e2f6c51c881a35a1ba47b8f98f4bcf55f77e230 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Tue, 11 Aug 2026 14:09:39 -0700 Subject: [PATCH 3/5] fix(e2e): load cc library rule --- e2e/cases/noop-default-transition/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/cases/noop-default-transition/BUILD.bazel b/e2e/cases/noop-default-transition/BUILD.bazel index efd923571..1be1408df 100644 --- a/e2e/cases/noop-default-transition/BUILD.bazel +++ b/e2e/cases/noop-default-transition/BUILD.bazel @@ -1,4 +1,5 @@ load("@aspect_rules_py//py:defs.bzl", "py_binary") +load("@rules_cc//cc:cc_library.bzl", "cc_library") cc_library( name = "native_dep", From eacdf7135c6e0735b96a3fc0e5e7b2ca4e73a699 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Tue, 11 Aug 2026 15:16:25 -0700 Subject: [PATCH 4/5] test(py): cover unpinned transition no-op --- e2e/cases/noop-default-transition/BUILD.bazel | 14 ------ e2e/cases/noop-default-transition/main.py | 1 - .../noop-default-transition/native_dep.cc | 3 -- e2e/cases/noop-default-transition/test.sh | 12 ----- py/tests/reset-data-edges/BUILD.bazel | 16 ++++++- py/tests/reset-data-edges/tests.bzl | 46 +++++++++++++++++++ 6 files changed, 61 insertions(+), 31 deletions(-) delete mode 100644 e2e/cases/noop-default-transition/BUILD.bazel delete mode 100644 e2e/cases/noop-default-transition/main.py delete mode 100644 e2e/cases/noop-default-transition/native_dep.cc delete mode 100755 e2e/cases/noop-default-transition/test.sh diff --git a/e2e/cases/noop-default-transition/BUILD.bazel b/e2e/cases/noop-default-transition/BUILD.bazel deleted file mode 100644 index 1be1408df..000000000 --- a/e2e/cases/noop-default-transition/BUILD.bazel +++ /dev/null @@ -1,14 +0,0 @@ -load("@aspect_rules_py//py:defs.bzl", "py_binary") -load("@rules_cc//cc:cc_library.bzl", "cc_library") - -cc_library( - name = "native_dep", - srcs = ["native_dep.cc"], -) - -py_binary( - name = "unversioned", - srcs = ["main.py"], - main = "main.py", - deps = [":native_dep"], -) diff --git a/e2e/cases/noop-default-transition/main.py b/e2e/cases/noop-default-transition/main.py deleted file mode 100644 index c9f9b6de4..000000000 --- a/e2e/cases/noop-default-transition/main.py +++ /dev/null @@ -1 +0,0 @@ -print("ok") diff --git a/e2e/cases/noop-default-transition/native_dep.cc b/e2e/cases/noop-default-transition/native_dep.cc deleted file mode 100644 index c21ce4721..000000000 --- a/e2e/cases/noop-default-transition/native_dep.cc +++ /dev/null @@ -1,3 +0,0 @@ -int native_dep() { - return 0; -} diff --git a/e2e/cases/noop-default-transition/test.sh b/e2e/cases/noop-default-transition/test.sh deleted file mode 100755 index df36a0316..000000000 --- a/e2e/cases/noop-default-transition/test.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -# An unpinned Python terminal must not put its native deps in a second target -# configuration. The native library is requested directly and through py_binary; -# both paths must share one CppCompile action. -set -euo pipefail - -cd "$(dirname "$0")/.." - -BAZEL="${BAZEL:-bazel}" -count="$($BAZEL aquery --lockfile_mode=off 'mnemonic(CppCompile, //noop-default-transition:native_dep + //noop-default-transition:unversioned)' | grep -c '^ Mnemonic: CppCompile$')" - -test "$count" -eq 1 diff --git a/py/tests/reset-data-edges/BUILD.bazel b/py/tests/reset-data-edges/BUILD.bazel index 66176b2b5..033829a0f 100644 --- a/py/tests/reset-data-edges/BUILD.bazel +++ b/py/tests/reset-data-edges/BUILD.bazel @@ -1,5 +1,5 @@ load("//py:defs.bzl", "py_binary", "py_library", "py_venv") -load(":tests.bzl", "probe", "reset_data_edges_test_suite", "root", "terminal") +load(":tests.bzl", "probe", "reset_data_edges_test_suite", "root", "seeded_root", "terminal") probe(name = "probe") @@ -51,6 +51,12 @@ terminal( tags = ["manual"], ) +terminal( + name = "unversioned_terminal", + tags = ["manual"], + deps = [":probe"], +) + py_venv( name = "second", srcs = ["main.py"], @@ -75,4 +81,12 @@ root( ], ) +seeded_root( + name = "seeded_root", + deps = [ + ":probe", + ":unversioned_terminal", + ], +) + reset_data_edges_test_suite() diff --git a/py/tests/reset-data-edges/tests.bzl b/py/tests/reset-data-edges/tests.bzl index b24ada8c8..8f7f60e95 100644 --- a/py/tests/reset-data-edges/tests.bzl +++ b/py/tests/reset-data-edges/tests.bzl @@ -4,8 +4,11 @@ load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") load("//py/private:transitions.bzl", "python_transition", "reset_python_flags_transition") _DEP_GROUP_FLAG = "@aspect_rules_py//uv/private/constraints/dep_group:dep_group" +_DEP_GROUP_BASELINE_FLAG = "@aspect_rules_py//uv/private/constraints/dep_group:baseline" _PYTHON_VERSION_FLAG = "@aspect_rules_py//py/private/interpreter:python_version" +_PYTHON_VERSION_BASELINE_FLAG = "@aspect_rules_py//py/private/interpreter:baseline_python_version" _RPY_VERSION_FLAG = "@rules_python//python/config_settings:python_version" +_RPY_VERSION_BASELINE_FLAG = "@aspect_rules_py//py/private/interpreter:baseline_rules_python_version" _ProbeInfo = provider(fields = ["file"]) _ProbeFilesInfo = provider(fields = ["files"]) @@ -96,6 +99,45 @@ root = rule( }, ) +# Give both dependency paths the same transition scratch state while preserving +# the default/unset state of the version flags. An unpinned terminal used to +# turn those version flags into explicit resolved values, creating a second +# configured target for :probe. +def _seed_baselines_transition_impl(settings, _attr): + return { + _DEP_GROUP_BASELINE_FLAG: settings[_DEP_GROUP_FLAG], + _PYTHON_VERSION_BASELINE_FLAG: settings[_PYTHON_VERSION_FLAG], + _RPY_VERSION_BASELINE_FLAG: settings[_RPY_VERSION_FLAG], + } + +_seed_baselines_transition = transition( + implementation = _seed_baselines_transition_impl, + inputs = [ + _DEP_GROUP_FLAG, + _PYTHON_VERSION_FLAG, + _RPY_VERSION_FLAG, + ], + outputs = [ + _DEP_GROUP_BASELINE_FLAG, + _PYTHON_VERSION_BASELINE_FLAG, + _RPY_VERSION_BASELINE_FLAG, + ], +) + +seeded_root = rule( + implementation = _root_impl, + attrs = { + "deps": attr.label_list( + aspects = [_probe_aspect], + allow_empty = False, + ), + "_allowlist_function_transition": attr.label( + default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + ), + }, + cfg = _seed_baselines_transition, +) + def _reset_data_edges_test_impl(ctx): env = analysistest.begin(ctx) files = analysistest.target_under_test(env)[_ProbeFilesInfo].files.to_list() @@ -114,3 +156,7 @@ def reset_data_edges_test_suite(): name = "reset_data_edges_test", target_under_test = ":root", ) + _reset_data_edges_test( + name = "unversioned_terminal_noop_transition_test", + target_under_test = ":seeded_root", + ) From 9f59d85d89fd6d48411b1bfe867c55678924341e Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Tue, 11 Aug 2026 16:58:58 -0700 Subject: [PATCH 5/5] test(e2e): cover version flag precedence --- e2e/cases/python-version-flag/test.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/e2e/cases/python-version-flag/test.sh b/e2e/cases/python-version-flag/test.sh index c69b1cead..10143db02 100755 --- a/e2e/cases/python-version-flag/test.sh +++ b/e2e/cases/python-version-flag/test.sh @@ -36,3 +36,10 @@ for version in 3.9 3.10 3.11 3.12 3.13; do "--@rules_python//python/config_settings:python_version=${version}" \ -- //python-version-flag:version_check "${version}" done + +# The native flag takes precedence over the legacy fallback when both are set. +"$BAZEL" run \ + --lockfile_mode=off \ + --@aspect_rules_py//py:python_version=3.12 \ + --@rules_python//python/config_settings:python_version=3.11 \ + -- //python-version-flag:version_check 3.12