summaryrefslogtreecommitdiffhomepage
path: root/tools/deps.bzl
blob: 91442617c7bc5af6f45d7145e039c79bdb8f461d (plain)
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,
)