1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
"""Rules for dependency checking."""
# DepsInfo provides a list of dependencies found when building a target.
DepsInfo = provider(
"lists dependencies encountered while building",
fields = {
"nodes": "a dict from targets to a list of their dependencies",
},
)
def _deps_check_impl(target, ctx):
# Check the target's dependencies and add any of our own deps.
deps = []
for dep in ctx.rule.attr.deps:
deps.append(dep)
nodes = {}
if len(deps) != 0:
nodes[target] = deps
# Keep and propagate each dep's providers.
for dep in ctx.rule.attr.deps:
nodes.update(dep[DepsInfo].nodes)
return [DepsInfo(nodes = nodes)]
_deps_check = aspect(
implementation = _deps_check_impl,
attr_aspects = ["deps"],
)
def _is_allowed(target, allowlist, prefixes):
# Check for allowed prefixes.
for prefix in prefixes:
workspace, pfx = prefix.split("//", 1)
if len(workspace) > 0 and workspace[0] == "@":
workspace = workspace[1:]
if target.workspace_name == workspace and target.package.startswith(pfx):
return True
# Check the allowlist.
for allowed in allowlist:
if target == allowed.label:
return True
return False
def _deps_test_impl(ctx):
nodes = {}
for target in ctx.attr.targets:
for (node_target, node_deps) in target[DepsInfo].nodes.items():
# Ignore any disallowed targets. This generates more useful error
# messages. Consider the case where A dependes on B and B depends
# on C, and both B and C are disallowed. Avoid emitting an error
# that B depends on C, when the real issue is that A depends on B.
if not _is_allowed(node_target.label, ctx.attr.allowed, ctx.attr.allowed_prefixes) and node_target.label != target.label:
continue
bad_deps = []
for dep in node_deps:
if not _is_allowed(dep.label, ctx.attr.allowed, ctx.attr.allowed_prefixes):
bad_deps.append(dep)
if len(bad_deps) > 0:
nodes[node_target] = bad_deps
# If there aren't any violations, write a passing test.
if len(nodes) == 0:
ctx.actions.write(
output = ctx.outputs.executable,
content = "#!/bin/bash\n\nexit 0\n",
)
return []
# If we're here, we've found at least one violation.
script_lines = [
"#!/bin/bash",
"echo Invalid dependencies found. If you\\'re sure you want to add dependencies,",
"echo modify this target.",
"echo",
]
# List the violations.
for target, deps in nodes.items():
script_lines.append(
'echo "{target} depends on:"'.format(target = target.label),
)
for dep in deps:
script_lines.append('echo "\t{dep}"'.format(dep = dep.label))
# The test must fail.
script_lines.append("exit 1\n")
ctx.actions.write(
output = ctx.outputs.executable,
content = "\n".join(script_lines),
)
return []
# Checks that targets only depend on an allowlist of other targets. Targets can
# be specified directly, or prefixes can be used to allow entire packages or
# directory trees.
#
# This recursively checks the "deps" attribute of each target, dependencies
# expressed other ways are not checked. For example, protobuf targets pull in
# protobuf code, but aren't analyzed by deps_test.
deps_test = rule(
implementation = _deps_test_impl,
attrs = {
"targets": attr.label_list(
doc = "The targets to check the transitive dependencies of.",
aspects = [_deps_check],
),
"allowed": attr.label_list(
doc = "The allowed dependency targets.",
),
"allowed_prefixes": attr.string_list(
doc = "Any packages beginning with these prefixes are allowed.",
),
},
test = True,
)
|