summaryrefslogtreecommitdiffstats
path: root/third_party/python/taskcluster_taskgraph/taskgraph/transforms/from_deps.py
blob: 337d68e4ba10daef5ce7f811b7d57ff05e95f2c6 (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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

"""
Transforms used to create tasks based on the kind dependencies, filtering on
common attributes like the ``build-type``.

These transforms are useful when follow-up tasks are needed for some
indeterminate subset of existing tasks. For example, running a signing task
after each build task, whatever builds may exist.
"""
from copy import deepcopy
from textwrap import dedent

from voluptuous import Any, Extra, Optional, Required

from taskgraph.transforms.base import TransformSequence
from taskgraph.transforms.job import fetches_schema
from taskgraph.util.attributes import attrmatch
from taskgraph.util.dependencies import GROUP_BY_MAP, get_dependencies
from taskgraph.util.schema import Schema, validate_schema

FROM_DEPS_SCHEMA = Schema(
    {
        Required("from-deps"): {
            Optional(
                "kinds",
                description=dedent(
                    """
                Limit dependencies to specified kinds (defaults to all kinds in
                `kind-dependencies`).

                The first kind in the list is the "primary" kind. The
                dependency of this kind will be used to derive the label
                and copy attributes (if `copy-attributes` is True).
                """.lstrip()
                ),
            ): list,
            Optional(
                "set-name",
                description=dedent(
                    """
                When True, `from_deps` will derive a name for the generated
                tasks from the name of the primary dependency. Defaults to
                True.
                """.lstrip()
                ),
            ): bool,
            Optional(
                "with-attributes",
                description=dedent(
                    """
                Limit dependencies to tasks whose attributes match
                using :func:`~taskgraph.util.attributes.attrmatch`.
                """.lstrip()
                ),
            ): {str: Any(list, str)},
            Optional(
                "group-by",
                description=dedent(
                    """
                Group cross-kind dependencies using the given group-by
                function. One task will be created for each group. If not
                specified, the 'single' function will be used which creates
                a new task for each individual dependency.
                """.lstrip()
                ),
            ): Any(
                None,
                *GROUP_BY_MAP,
                {Any(*GROUP_BY_MAP): object},
            ),
            Optional(
                "copy-attributes",
                description=dedent(
                    """
                If True, copy attributes from the dependency matching the
                first kind in the `kinds` list (whether specified explicitly
                or taken from `kind-dependencies`).
                """.lstrip()
                ),
            ): bool,
            Optional(
                "unique-kinds",
                description=dedent(
                    """
                If true (the default), there must be only a single unique task
                for each kind in a dependency group. Setting this to false
                disables that requirement.
                """.lstrip()
                ),
            ): bool,
            Optional(
                "fetches",
                description=dedent(
                    """
                If present, a `fetches` entry will be added for each task
                dependency. Attributes of the upstream task may be used as
                substitution values in the `artifact` or `dest` values of the
                `fetches` entry.
                """.lstrip()
                ),
            ): {str: [fetches_schema]},
        },
        Extra: object,
    },
)
"""Schema for from_deps transforms."""

transforms = TransformSequence()
transforms.add_validate(FROM_DEPS_SCHEMA)


@transforms.add
def from_deps(config, tasks):
    for task in tasks:
        # Setup and error handling.
        from_deps = task.pop("from-deps")
        kind_deps = config.config.get("kind-dependencies", [])
        kinds = from_deps.get("kinds", kind_deps)

        invalid = set(kinds) - set(kind_deps)
        if invalid:
            invalid = "\n".join(sorted(invalid))
            raise Exception(
                dedent(
                    f"""
                    The `from-deps.kinds` key contains the following kinds
                    that are not defined in `kind-dependencies`:
                    {invalid}
                """.lstrip()
                )
            )

        if not kinds:
            raise Exception(
                dedent(
                    """
                The `from_deps` transforms require at least one kind defined
                in `kind-dependencies`!
                """.lstrip()
                )
            )

        # Resolve desired dependencies.
        with_attributes = from_deps.get("with-attributes")
        deps = [
            task
            for task in config.kind_dependencies_tasks.values()
            if task.kind in kinds
            if not with_attributes or attrmatch(task.attributes, **with_attributes)
        ]

        # Resolve groups.
        group_by = from_deps.get("group-by", "single")
        groups = set()

        if isinstance(group_by, dict):
            assert len(group_by) == 1
            group_by, arg = group_by.popitem()
            func = GROUP_BY_MAP[group_by]
            if func.schema:
                validate_schema(
                    func.schema, arg, f"Invalid group-by {group_by} argument"
                )
            groups = func(config, deps, arg)
        else:
            func = GROUP_BY_MAP[group_by]
            groups = func(config, deps)

        # Split the task, one per group.
        set_name = from_deps.get("set-name", True)
        copy_attributes = from_deps.get("copy-attributes", False)
        unique_kinds = from_deps.get("unique-kinds", True)
        fetches = from_deps.get("fetches", [])
        for group in groups:
            # Verify there is only one task per kind in each group.
            group_kinds = {t.kind for t in group}
            if unique_kinds and len(group_kinds) < len(group):
                raise Exception(
                    "The from_deps transforms only allow a single task per kind in a group!"
                )

            new_task = deepcopy(task)
            new_task.setdefault("dependencies", {})
            new_task["dependencies"].update(
                {dep.kind if unique_kinds else dep.label: dep.label for dep in group}
            )

            # Set name and copy attributes from the primary kind.
            for kind in kinds:
                if kind in group_kinds:
                    primary_kind = kind
                    break
            else:
                raise Exception("Could not detect primary kind!")

            new_task.setdefault("attributes", {})[
                "primary-kind-dependency"
            ] = primary_kind

            primary_dep = [dep for dep in group if dep.kind == primary_kind][0]

            if set_name:
                if primary_dep.label.startswith(primary_kind):
                    new_task["name"] = primary_dep.label[len(primary_kind) + 1 :]
                else:
                    new_task["name"] = primary_dep.label

            if copy_attributes:
                attrs = new_task.setdefault("attributes", {})
                new_task["attributes"] = primary_dep.attributes.copy()
                new_task["attributes"].update(attrs)

            if fetches:
                task_fetches = new_task.setdefault("fetches", {})

                for dep_task in get_dependencies(config, new_task):
                    # Nothing to do if this kind has no fetches listed
                    if dep_task.kind not in fetches:
                        continue

                    fetches_from_dep = []
                    for kind, kind_fetches in fetches.items():
                        if kind != dep_task.kind:
                            continue

                        for fetch in kind_fetches:
                            entry = fetch.copy()
                            entry["artifact"] = entry["artifact"].format(
                                **dep_task.attributes
                            )
                            if "dest" in entry:
                                entry["dest"] = entry["dest"].format(
                                    **dep_task.attributes
                                )
                            fetches_from_dep.append(entry)

                    task_fetches[dep_task.label] = fetches_from_dep

            yield new_task