diff --git a/bazel/dependency/BUILD b/bazel/dependency/BUILD index 1cf128bf89..814f8b239f 100644 --- a/bazel/dependency/BUILD +++ b/bazel/dependency/BUILD @@ -10,7 +10,7 @@ exports_files([ # Private build settings used by reachability.bzl to transport exclusion lists # through the configuration transition (including the exec-config transition). # These are not part of the public API; use dependency_reachability's -# excluded_edges / excluded_patterns attributes instead. +# excluded_edges / excluded_patterns / unrecorded_patterns attributes instead. string_list_flag( name = "_excluded_edges", build_setting_default = [], @@ -23,6 +23,12 @@ string_list_flag( visibility = ["//visibility:public"], ) +string_list_flag( + name = "_unrecorded_patterns", + build_setting_default = [], + visibility = ["//visibility:public"], +) + string_list_flag( name = "_attribution_patterns", build_setting_default = [], diff --git a/bazel/dependency/reachability.bzl b/bazel/dependency/reachability.bzl index afcc853c6c..5ea44761ff 100644 --- a/bazel/dependency/reachability.bzl +++ b/bazel/dependency/reachability.bzl @@ -165,23 +165,29 @@ recorded over all public attributes, including ones that carry build-time-only deps (e.g. `tools`-style attrs on custom rules); consumers that need to distinguish build-time from runtime paths should apply that policy themselves. -Exclusion settings (`excluded_edges`, `excluded_patterns`) are transported via -Starlark build settings rather than `--features`, so they survive Bazel's exec -configuration transition. They therefore apply uniformly across target and exec -configurations: edges reached through `cfg = "exec"` attributes are excluded -just as reliably as edges in the target configuration. The same transport is -used for `attribution_patterns`, so opted-in package attribution applies -uniformly across all analyzed configurations. All three settings are carried -through every branch of the split transition. - -Note on exclusion semantics: an excluded repository is neither recorded nor -descended into. Pruning descent means that repositories reachable *only through* -an excluded repository also disappear from the output, even if they do not -themselves match any exclusion pattern. This is a deliberate trade-off: it keeps -the walk bounded but means the output can depend on which repository sits first -on a path. The same pruning applies to `attributed_packages`: excluded -repositories are not attributed, and packages are not attributed to repositories -reachable only through an excluded repository. +Exclusion settings (`excluded_edges`, `excluded_patterns`, +`unrecorded_patterns`) are transported via Starlark build settings rather than +`--features`, so they survive Bazel's exec configuration transition. They +therefore apply uniformly across target and exec configurations: edges reached +through `cfg = "exec"` attributes are excluded just as reliably as edges in the +target configuration. The same transport is used for `attribution_patterns`, so +opted-in package attribution applies uniformly across all analyzed +configurations. All four settings are carried through every branch of the split +transition. + +Repository-pattern exclusion has two modes: + +- `excluded_patterns`: prune. Matching repositories are neither recorded nor + descended into, so repositories reachable *only through* them also disappear + from the output. +- `unrecorded_patterns`: record suppression only. Matching repositories are not + recorded (and are not attribution targets), but traversal continues through + them so repositories behind them still appear and are still attributed. + +When a repository matches both, `excluded_patterns` wins (pruning is stronger). +Use pruning to keep the walk bounded when the entire subtree is irrelevant; use +record suppression for boundary repositories that are uninteresting themselves +but fan out to dependencies that still need coverage. `attribution_patterns` is opt-in and defaults to empty. When left unset, the emitted JSON is byte-identical to the historical output and no attribution sets @@ -191,13 +197,14 @@ needs transitive main-repo package attribution. load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") -# Private build settings used to transport excluded_edges and excluded_patterns +# Private build settings used to transport exclusion/attribution settings # through the configuration transition. Build settings survive the exec # configuration transition (unlike --features, which Bazel resets to # --host_features when entering exec config). These settings are not intended # for direct use; they are an implementation detail of dependency_reachability. _EXCLUDED_EDGES_SETTING = "//dependency:_excluded_edges" _EXCLUDED_PATTERNS_SETTING = "//dependency:_excluded_patterns" +_UNRECORDED_PATTERNS_SETTING = "//dependency:_unrecorded_patterns" _ATTRIBUTION_PATTERNS_SETTING = "//dependency:_attribution_patterns" DependencyReachabilityInfo = provider( @@ -269,13 +276,7 @@ def _matches_repo_pattern(repo_name, pattern): return repo_name == pattern def _repo_is_excluded(repo_name, patterns): - """Return True if repo_name matches any exclusion pattern. - - An excluded repository is neither recorded as a dependency edge nor - descended into during traversal. Pruning descent means that repositories - reachable *only through* an excluded repository also disappear from the - output, even if they do not themselves match any exclusion pattern. - """ + """Return True if repo_name matches any repository pattern.""" for pattern in patterns: if _matches_repo_pattern(repo_name, pattern): return True @@ -358,6 +359,7 @@ def _reachability_aspect_impl(target, ctx): testonly = bool(getattr(ctx.rule.attr, "testonly", False)) excluded_edges = {edge: True for edge in ctx.attr._excluded_edges[BuildSettingInfo].value} excluded_patterns = ctx.attr._excluded_patterns[BuildSettingInfo].value + unrecorded_patterns = ctx.attr._unrecorded_patterns[BuildSettingInfo].value attribution_patterns = ctx.attr._attribution_patterns[BuildSettingInfo].value collect_attributions = bool(attribution_patterns) edges = [] @@ -395,9 +397,11 @@ def _reachability_aspect_impl(target, ctx): continue for dep in _attr_targets(ctx.rule.attr, attr_name): dep_repo = dep.label.repo_name - if dep_repo and _repo_is_excluded(dep_repo, excluded_patterns): + excluded = dep_repo and _repo_is_excluded(dep_repo, excluded_patterns) + if excluded: continue - if dep_repo and dep_repo != consumer_repo: + unrecorded = dep_repo and _repo_is_excluded(dep_repo, unrecorded_patterns) + if dep_repo and dep_repo != consumer_repo and not unrecorded: edges.append(struct( attr = attr_name, consumer = consumer, @@ -483,6 +487,10 @@ reachability_aspect = aspect( default = _EXCLUDED_PATTERNS_SETTING, providers = [BuildSettingInfo], ), + "_unrecorded_patterns": attr.label( + default = _UNRECORDED_PATTERNS_SETTING, + providers = [BuildSettingInfo], + ), "_attribution_patterns": attr.label( default = _ATTRIBUTION_PATTERNS_SETTING, providers = [BuildSettingInfo], @@ -648,6 +656,7 @@ def _dependency_reachability_transition(flags): # apply uniformly across all analyzed configurations. output[_EXCLUDED_EDGES_SETTING] = attr.excluded_edges output[_EXCLUDED_PATTERNS_SETTING] = attr.excluded_patterns + output[_UNRECORDED_PATTERNS_SETTING] = attr.unrecorded_patterns output[_ATTRIBUTION_PATTERNS_SETTING] = attr.attribution_patterns transitioned[config] = output return transitioned @@ -656,6 +665,7 @@ def _dependency_reachability_transition(flags): transition_outputs = flag_inputs + [ _EXCLUDED_EDGES_SETTING, _EXCLUDED_PATTERNS_SETTING, + _UNRECORDED_PATTERNS_SETTING, _ATTRIBUTION_PATTERNS_SETTING, ] return transition( @@ -720,7 +730,22 @@ def _dependency_reachability_rule(flags = []): "Canonical repository-name patterns excluded from " + "traversal. Supported forms are exact matches (`repo_name`) " + "and a single trailing `*` wildcard (`prefix*`) for prefix " + - "matching." + "matching. Matching repositories are neither recorded nor " + + "descended into (pruning). If a repository matches both " + + "excluded_patterns and unrecorded_patterns, pruning wins." + ), + ), + "unrecorded_patterns": attr.string_list( + default = [], + doc = ( + "Canonical repository-name patterns that suppress direct " + + "recording but preserve traversal. Supported forms are " + + "exact matches (`repo_name`) and a single trailing `*` " + + "wildcard (`prefix*`) for prefix matching. Matching " + + "repositories are not recorded as dependencies and are not " + + "attribution targets, but repositories reachable through " + + "them are still traversed and recorded unless excluded by " + + "excluded_patterns." ), ), "attribution_patterns": attr.string_list( diff --git a/bazel/dependency/test/BUILD b/bazel/dependency/test/BUILD index c3c5b22434..6684bd1344 100644 --- a/bazel/dependency/test/BUILD +++ b/bazel/dependency/test/BUILD @@ -167,7 +167,15 @@ dependency_reachability( testonly = True, roots = ["//dependency/test/transitive/ext/excluded:root"], attribution_patterns = ["//dependency/test/transitive/ext/..."], - excluded_patterns = ["rules_pkg*"], + excluded_patterns = ["bazel_tools"], +) + +dependency_reachability( + name = "reachability_attribution_unrecorded", + testonly = True, + roots = ["//dependency/test/transitive/ext/excluded:root"], + attribution_patterns = ["//dependency/test/transitive/ext/..."], + unrecorded_patterns = ["bazel_tools"], ) sh_test( @@ -280,7 +288,7 @@ dependency_reachability( filegroup( name = "rules_pkg_root", - srcs = ["@rules_pkg//pkg:filter_directory"], + srcs = ["@bazel_tools//src/conditions:linux"], ) dependency_reachability( @@ -293,7 +301,31 @@ dependency_reachability( name = "reachability_excluded_rules_pkg_transitive", testonly = True, roots = [":rules_pkg_root"], - excluded_patterns = ["rules_pkg*"], + excluded_patterns = ["bazel_tools"], +) + +dependency_reachability( + name = "reachability_unrecorded_rules_pkg_transitive", + testonly = True, + roots = [":rules_pkg_root"], + unrecorded_patterns = ["bazel_tools"], +) + +dependency_reachability( + name = "reachability_excluded_over_unrecorded_rules_pkg_transitive", + testonly = True, + roots = [":rules_pkg_root"], + excluded_patterns = ["bazel_tools"], + unrecorded_patterns = ["bazel_tools"], +) + +dependency_reachability( + name = "reachability_unrecorded_edge_and_pattern", + testonly = True, + roots = [":excluded_edge_root"], + excluded_edges = ["library"], + excluded_patterns = ["bazel_tools*"], + unrecorded_patterns = ["bazel_skylib*"], ) dependency_reachability( @@ -308,6 +340,7 @@ dependency_reachability( roots = [":core_root"], excluded_edges = [], excluded_patterns = [], + unrecorded_patterns = [], ) sh_test( @@ -320,10 +353,14 @@ sh_test( ":reachability_excluded_pattern", ":reachability_rules_pkg_transitive", ":reachability_excluded_rules_pkg_transitive", + ":reachability_unrecorded_rules_pkg_transitive", + ":reachability_excluded_over_unrecorded_rules_pkg_transitive", + ":reachability_unrecorded_edge_and_pattern", ":reachability_empty_exclusions", ":reachability_empty_exclusions_explicit", ":reachability_attribution_excluded_baseline", ":reachability_attribution_excluded", + ":reachability_attribution_unrecorded", "@jq_toolchains//:resolved_toolchain", ], env = { diff --git a/bazel/dependency/test/exclusions_reachability_test.sh b/bazel/dependency/test/exclusions_reachability_test.sh index fb29f46ae9..a2fb803df4 100755 --- a/bazel/dependency/test/exclusions_reachability_test.sh +++ b/bazel/dependency/test/exclusions_reachability_test.sh @@ -19,10 +19,14 @@ REACHABILITY_EDGE_AND_PATTERN_JSON="${RUNFILES_DIR}/dependency/test/reachability REACHABILITY_EXCLUDED_PATTERN_JSON="${RUNFILES_DIR}/dependency/test/reachability_excluded_pattern.json" REACHABILITY_RULES_PKG_TRANSITIVE_JSON="${RUNFILES_DIR}/dependency/test/reachability_rules_pkg_transitive.json" REACHABILITY_EXCLUDED_RULES_PKG_TRANSITIVE_JSON="${RUNFILES_DIR}/dependency/test/reachability_excluded_rules_pkg_transitive.json" +REACHABILITY_UNRECORDED_RULES_PKG_TRANSITIVE_JSON="${RUNFILES_DIR}/dependency/test/reachability_unrecorded_rules_pkg_transitive.json" +REACHABILITY_EXCLUDED_OVER_UNRECORDED_RULES_PKG_TRANSITIVE_JSON="${RUNFILES_DIR}/dependency/test/reachability_excluded_over_unrecorded_rules_pkg_transitive.json" +REACHABILITY_UNRECORDED_EDGE_AND_PATTERN_JSON="${RUNFILES_DIR}/dependency/test/reachability_unrecorded_edge_and_pattern.json" REACHABILITY_EMPTY_EXCLUSIONS_JSON="${RUNFILES_DIR}/dependency/test/reachability_empty_exclusions.json" REACHABILITY_EMPTY_EXCLUSIONS_EXPLICIT_JSON="${RUNFILES_DIR}/dependency/test/reachability_empty_exclusions_explicit.json" REACHABILITY_ATTRIBUTION_EXCLUDED_BASELINE_JSON="${RUNFILES_DIR}/dependency/test/reachability_attribution_excluded_baseline.json" REACHABILITY_ATTRIBUTION_EXCLUDED_JSON="${RUNFILES_DIR}/dependency/test/reachability_attribution_excluded.json" +REACHABILITY_ATTRIBUTION_UNRECORDED_JSON="${RUNFILES_DIR}/dependency/test/reachability_attribution_unrecorded.json" FAILED=0 @@ -71,7 +75,7 @@ check "excluded_patterns can remove all matching deps from one root" \ "${REACHABILITY_EXCLUDED_PATTERN_JSON}" check "excluded_patterns baseline includes the matched boundary repo" \ - '[.dependencies[] | .name] | any(. == "rules_pkg")' \ + '[.dependencies[] | .name] | any(. == "bazel_tools")' \ "true" \ "${REACHABILITY_RULES_PKG_TRANSITIVE_JSON}" @@ -80,13 +84,44 @@ check "excluded_patterns prunes matched repos transitively" \ "0" \ "${REACHABILITY_EXCLUDED_RULES_PKG_TRANSITIVE_JSON}" +check "unrecorded_patterns suppresses recording of the matched boundary repo" \ + '[.dependencies[] | .name] | any(. == "bazel_tools")' \ + "false" \ + "${REACHABILITY_UNRECORDED_RULES_PKG_TRANSITIVE_JSON}" + +check "unrecorded_patterns preserves descent behind the matched boundary repo" \ + '[.dependencies[] | .name] | length > 0' \ + "true" \ + "${REACHABILITY_UNRECORDED_RULES_PKG_TRANSITIVE_JSON}" + +BASELINE_MINUS_BOUNDARY="$("${JQ}" -r '[.dependencies[] | .name] | map(select(. != "bazel_tools")) | sort | join(",")' "${REACHABILITY_RULES_PKG_TRANSITIVE_JSON}")" +UNRECORDED_NAMES="$("${JQ}" -r '[.dependencies[] | .name] | sort | join(",")' "${REACHABILITY_UNRECORDED_RULES_PKG_TRANSITIVE_JSON}")" +if [ "${UNRECORDED_NAMES}" != "${BASELINE_MINUS_BOUNDARY}" ]; then + echo "FAIL: unrecorded_patterns keeps the baseline transitive set except the suppressed repo" >&2 + echo " baseline-minus-boundary: ${BASELINE_MINUS_BOUNDARY}" >&2 + echo " unrecorded: ${UNRECORDED_NAMES}" >&2 + FAILED=1 +else + echo "PASS: unrecorded_patterns keeps the baseline transitive set except the suppressed repo" +fi + check "combined excluded_edges and excluded_patterns apply together" \ '[.dependencies[] | .name] | length' \ "0" \ "${REACHABILITY_EDGE_AND_PATTERN_JSON}" +check "excluded_patterns wins over unrecorded_patterns when both match" \ + '[.dependencies[] | .name] | length' \ + "0" \ + "${REACHABILITY_EXCLUDED_OVER_UNRECORDED_RULES_PKG_TRANSITIVE_JSON}" + +check "unrecorded_patterns composes with excluded_edges and excluded_patterns" \ + '[.dependencies[] | .name] | length' \ + "0" \ + "${REACHABILITY_UNRECORDED_EDGE_AND_PATTERN_JSON}" + check "baseline attribution records the unexcluded repository" \ - '.dependencies | to_entries[] | select(.value.name == "rules_pkg") | .value.attributed_packages[0].package' \ + '.dependencies | to_entries[] | select(.value.name == "bazel_tools") | .value.attributed_packages[0].package' \ "//dependency/test/transitive/ext/excluded" \ "${REACHABILITY_ATTRIBUTION_EXCLUDED_BASELINE_JSON}" @@ -100,6 +135,21 @@ check "nothing is attributed through an excluded repository" \ "0" \ "${REACHABILITY_ATTRIBUTION_EXCLUDED_JSON}" +check "unrecorded repository is not attributed" \ + '[.dependencies[] | select(.name == "bazel_tools")] | length' \ + "0" \ + "${REACHABILITY_ATTRIBUTION_UNRECORDED_JSON}" + +check "attribution still passes through an unrecorded repository" \ + '[.dependencies[] | .attributed_packages[]?] | length > 0' \ + "true" \ + "${REACHABILITY_ATTRIBUTION_UNRECORDED_JSON}" + +check "repos behind an unrecorded repository keep their package attribution" \ + '.dependencies | to_entries[] | select(.value.name == "platforms") | .value.attributed_packages[0].package' \ + "//dependency/test/transitive/ext/excluded" \ + "${REACHABILITY_ATTRIBUTION_UNRECORDED_JSON}" + if ! cmp -s "${REACHABILITY_EMPTY_EXCLUSIONS_JSON}" "${REACHABILITY_EMPTY_EXCLUSIONS_EXPLICIT_JSON}"; then echo "FAIL: explicit empty exclusions must be byte-identical to defaults" >&2 FAILED=1 diff --git a/bazel/dependency/test/transitive/ext/excluded/BUILD b/bazel/dependency/test/transitive/ext/excluded/BUILD index 3505df1af3..737c4222fc 100644 --- a/bazel/dependency/test/transitive/ext/excluded/BUILD +++ b/bazel/dependency/test/transitive/ext/excluded/BUILD @@ -2,5 +2,5 @@ package(default_visibility = ["//visibility:public"]) filegroup( name = "root", - srcs = ["@rules_pkg//pkg:filter_directory"], + srcs = ["@bazel_tools//src/conditions:linux"], )