Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion bazel/dependency/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [],
Expand All @@ -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 = [],
Expand Down
81 changes: 53 additions & 28 deletions bazel/dependency/reachability.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
43 changes: 40 additions & 3 deletions bazel/dependency/test/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -308,6 +340,7 @@ dependency_reachability(
roots = [":core_root"],
excluded_edges = [],
excluded_patterns = [],
unrecorded_patterns = [],
)

sh_test(
Expand All @@ -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 = {
Expand Down
54 changes: 52 additions & 2 deletions bazel/dependency/test/exclusions_reachability_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}"

Expand All @@ -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}"

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bazel/dependency/test/transitive/ext/excluded/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ package(default_visibility = ["//visibility:public"])

filegroup(
name = "root",
srcs = ["@rules_pkg//pkg:filter_directory"],
srcs = ["@bazel_tools//src/conditions:linux"],
)