summaryrefslogtreecommitdiff
path: root/rtemsspec/packagebuild.py
blob: 57c3b334b6868670bc946ea58ac6e48ed00fb492 (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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# SPDX-License-Identifier: BSD-2-Clause
""" This module provides the basic support to build a QDP. """

# Copyright (C) 2021 EDISOFT (https://www.edisoft.pt/)
# Copyright (C) 2020, 2023 embedded brains GmbH & Co. KG
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import copy
import itertools
import logging
import re
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type

from rtemsspec.items import data_digest, Item, ItemCache, \
    ItemGetValueContext, ItemGetValue, ItemMapper, is_enabled, \
    Link, link_is_enabled
from rtemsspec.sphinxcontent import SphinxMapper

_SINGLE_SUBSTITUTION = re.compile(
    r"^\${[a-zA-Z0-9._/-]+(:[a-zA-Z0-9._/-]+)?(:[^${}]*)?}$")


def _get_input_links(item: Item) -> Iterator[Link]:
    yield from itertools.chain(item.links_to_parents("input"),
                               item.links_to_children("input-to"))


def build_item_input(item: Item, name: str) -> Item:
    """ Returns the first input item with the name.  """
    for link in _get_input_links(item):
        if link["name"] == name:
            return link.item
    raise KeyError


def _get_spec(ctx: ItemGetValueContext) -> Any:
    return ctx.item.spec


class BuildItemMapper(SphinxMapper):
    """
    The build item mapper provides a method to get a link to the primary
    documentation place of the item.
    """

    def __init__(self, item: Item, recursive: bool = False):
        super().__init__(item, recursive)
        for type_name in item.cache.items_by_type:
            self.add_get_value(f"{type_name}:/spec", _get_spec)

    def get_link(self, _item: Item) -> str:
        """ Returns a link to the primary documentation place of the item. """
        raise NotImplementedError


class BuildItem():
    """ This is the base class for build steps. """

    # pylint: disable=too-many-public-methods
    @classmethod
    def prepare_factory(cls, _factory: "BuildItemFactory",
                        _type_name: str) -> None:
        """ Prepares the build item factory for the type. """

    def __init__(self,
                 director: "PackageBuildDirector",
                 item: Item,
                 mapper: Optional[BuildItemMapper] = None):
        if mapper is None:
            mapper = BuildItemMapper(item, recursive=True)
        self.director = director
        self.item = item
        self.mapper = mapper
        director.factory.add_get_values_to_mapper(self.mapper)
        self._did_run = False

    def __contains__(self, key: str) -> bool:
        return key in self.item

    def __getitem__(self, key: str) -> Any:
        return self.substitute(self.item[key])

    def __setitem__(self, key: str, value: Any) -> None:
        self.item[key] = value

    @property
    def uid(self) -> str:
        """ Returns the UID of the build item. """
        return self.item.uid

    @property
    def variant(self) -> "BuildItem":
        """ Returns the variant build item. """
        return self.director["/qdp/variant"]

    @property
    def enabled_set(self) -> List["str"]:
        """ Is the enabled set of the variant item. """
        return self.director["/qdp/variant"]["enabled"]

    @property
    def enabled(self) -> bool:
        """
        Is true, if the build item is enabled using the enabled set of the
        variant item, otherwise false.
        """
        return is_enabled(self.enabled_set, self["enabled-by"])

    def build(self, force: bool) -> None:
        """ Runs the build if necessary. """
        self._did_run = False
        self.mapper.item = self.item
        logging.info("%s: check if build is necessary", self.uid)
        necessary = self.is_build_necessary()
        if necessary:
            logging.info("%s: build is necessary", self.uid)
        if force:
            logging.info("%s: build is forced", self.uid)
        if force or necessary:
            logging.info("%s: discard outputs", self.uid)
            self.discard_outputs()
            logging.info("%s: run", self.uid)
            self.run()
            self._did_run = True
            logging.info("%s: refresh outputs", self.uid)
            self.refresh_outputs()
            logging.info("%s: refresh input links", self.uid)
            self.refresh_input_links()
            logging.info("%s: refresh", self.uid)
            self.refresh()
            logging.info("%s: commit", self.uid)
            self.commit("Finish build step")
            logging.info("%s: finished", self.uid)
        else:
            logging.info("%s: build is unnecessary", self.uid)

    def has_changed(self, link: Link) -> bool:
        """
        Returns true, if the build item state changed with respect to the state
        of the link, otherwise false.
        """
        return self._did_run or link["hash"] is None or self.digest != link[
            "hash"]

    def is_build_necessary(self) -> bool:
        """ Returns true, if the build is necessary, otherwise false. """
        necessary = False
        for link in itertools.chain(
                self.item.links_to_parents("input",
                                           is_link_enabled=link_is_enabled),
                self.item.links_to_children("input-to",
                                            is_link_enabled=link_is_enabled)):
            if not link.item.is_enabled(self.enabled_set):
                logging.info("%s: input is disabled: %s", self.uid,
                             link.item.uid)
                continue
            build_item = self.director[link.item.uid]
            if link["hash"] is None:
                logging.info("%s: input is new: %s", self.uid, build_item.uid)
            if build_item.has_changed(link):
                logging.info("%s: input has changed: %s", self.uid,
                             build_item.uid)
                necessary = True
            else:
                logging.info("%s: input has not changed: %s", self.uid,
                             build_item.uid)
        return necessary

    def discard(self) -> None:
        """ Discards the data associated with the build item.  """

    def discard_outputs(self) -> None:
        """ Discards all outputs of the build item.  """
        for item in self.item.parents("output",
                                      is_link_enabled=link_is_enabled):
            if not item.is_enabled(self.enabled_set):
                logging.info("%s: output is disabled: %s", self.uid, item.uid)
                continue
            self.director[item.uid].discard()

    def clear(self) -> None:
        """ Clears the state of the build item.  """

    def refresh(self) -> None:
        """ Refreshes the build item state.  """

    def commit(self, _reason: str) -> None:
        """ Commits the build item state.  """
        for item in self.item.children("input-to"):
            item.save()
        self.item.save()

    @property
    def digest(self) -> str:
        """ Is the hash of the build item. """
        data = self.item.data
        data["links"] = copy.deepcopy(data["links"])
        for link in data["links"]:
            if link["role"] == "input-to":
                link["hash"] = None
        return data_digest(data)

    def refresh_link(self, link: Link) -> None:
        """ Refreshes the link to reflect the state of the build item. """
        link["hash"] = self.digest

    def refresh_outputs(self) -> None:
        """ Refreshes all outputs of the build item.  """
        for item in self.item.parents("output"):
            self.director[item.uid].refresh()

    def refresh_input_links(self) -> None:
        """ Refreshes all input links of the build item.  """
        for link in _get_input_links(self.item):
            self.director[link.item.uid].refresh_link(link)

    def run(self):
        """ Runs the build item tasks. """

    def substitute(self, data: Any, item: Optional[Item] = None) -> Any:
        """
        Recursively substitutes the data using the item mapper of the build
        step.
        """
        if item is None:
            item = self.item
        if isinstance(data, str):
            return self.mapper.substitute(data, item)
        if isinstance(data, list):
            new_list: List[Any] = []
            for element in data:
                if isinstance(element, str):
                    match = _SINGLE_SUBSTITUTION.search(element)
                    if match:
                        new_item, _, new_element = self.mapper.map(
                            element[2:-1], item)
                        if isinstance(new_element, list):
                            new_list.extend(
                                self.mapper.substitute(new_element_2, new_item)
                                for new_element_2 in new_element)
                        else:
                            new_list.append(
                                self.mapper.substitute(new_element, new_item))
                    else:
                        new_list.append(self.mapper.substitute(element, item))
                else:
                    new_list.append(self.substitute(element))
            return new_list
        if isinstance(data, dict):
            new_dict: Dict[Any, Any] = {}
            for key, value in data.items():
                new_dict[key] = self.substitute(value)
            return new_dict
        return data

    def input(self, name: str) -> "BuildItem":
        """
        Returns the first directory state dependency and the expected hash
        associated with the name.
        """
        for link in _get_input_links(self.item):
            if link["name"] == name:
                return self.director[link.item.uid]
        raise KeyError

    def inputs(self, name: Optional[str] = None) -> Iterator["BuildItem"]:
        """ Yields the inputs associated with the name. """
        for link in _get_input_links(self.item):
            if name is None or link["name"] == name:
                yield self.director[link.item.uid]

    def input_links(self, name: Optional[str] = None) -> Iterator[Link]:
        """ Yields the inputs associated with the name. """
        for link in _get_input_links(self.item):
            if name is None or link["name"] == name:
                yield link

    def output(self, name: str) -> "BuildItem":
        """
        Returns the first directory state production associated with the
        name.
        """
        for link in self.item.links_to_parents(
                "output", is_link_enabled=link_is_enabled):
            if link["name"] == name:
                if link.item.is_enabled(self.enabled_set):
                    return self.director[link.item.uid]
                logging.info("%s: output is disabled: %s", self.uid,
                             link.item.uid)
                raise ValueError
        raise KeyError


def _get_dash(ctx: ItemGetValueContext) -> str:
    return f"-{ctx.value}" if ctx.value else ""


def _get_slash(ctx: ItemGetValueContext) -> str:
    return f"/{ctx.value}" if ctx.value else ""


class PackageVariant(BuildItem):
    """ This is the class represents a package variant. """

    @classmethod
    def prepare_factory(cls, factory: "BuildItemFactory",
                        type_name: str) -> None:
        BuildItem.prepare_factory(factory, type_name)
        factory.add_get_value(f"{type_name}:/config/dash", _get_dash)
        factory.add_get_value(f"{type_name}:/config/slash", _get_slash)
        factory.add_get_value(f"{type_name}:/params/bsp-extra/dash", _get_dash)
        factory.add_get_value(f"{type_name}:/params/bsp-extra/slash",
                              _get_slash)
        factory.add_get_value(f"{type_name}:/params/bsp-qual-only/dash",
                              _get_dash)
        factory.add_get_value(f"{type_name}:/params/bsp-qual-only/slash",
                              _get_slash)


class BuildItemFactory:
    """
    The build item factory can create build items for registered build item
    types.
    """

    def __init__(self) -> None:
        """ Initializes the dictionary of build steps """
        self._build_step_map: Dict[str, Type[BuildItem]] = {}
        self._get_values: List[Tuple[str, ItemGetValue]] = []

    def add_constructor(self, type_name: str, cls: Type[BuildItem]):
        """ Associates the build item constructor with the type name. """
        self._build_step_map[type_name] = cls
        cls.prepare_factory(self, type_name)

    def create(self, director: "PackageBuildDirector",
               item: Item) -> BuildItem:
        """
        Creates a build item for the item.

        The new build item will be assocated with the build director.
        """
        return self._build_step_map.get(item.type, BuildItem)(director, item)

    def add_get_value(self, type_path_key: str,
                      get_value: ItemGetValue) -> None:
        """ Adds a get value method for the type key path. """
        self._get_values.append((type_path_key, get_value))

    def add_get_values_to_mapper(self, mapper: ItemMapper) -> None:
        """ Adds add registered get value methods to the mapper. """
        for type_path_key, get_value in self._get_values:
            mapper.add_get_value(type_path_key, get_value)


class PackageBuildDirector:
    """
    The package build director contains the package build state and runs the
    build.
    """

    # pylint: disable=too-few-public-methods
    def __init__(self, item_cache: ItemCache, factory: BuildItemFactory):
        self._item_cache = item_cache
        self.factory = factory
        self._build_items: Dict[str, BuildItem] = {}

    def __getitem__(self, uid: str) -> BuildItem:
        item = self._build_items.get(uid, None)
        if item is not None:
            return item
        logging.info("%s: create build item", uid)
        item = self.factory.create(self, self._item_cache[uid])
        self._build_items[uid] = item
        return item

    def clear(self) -> None:
        """ Clears the build items of the director.  """
        self._build_items.clear()

    def build_package(self, only: Optional[List[str]],
                      force: Optional[List[str]]):
        """ Builds the package """
        if force is None:
            force = []
        build_steps = self._item_cache["/qdp/variant"].parent("package-build")
        enabled_set = self["/qdp/variant"]["enabled"]
        logging.info("%s: build the package", build_steps.uid)
        for step in build_steps.parents("build-step",
                                        is_link_enabled=link_is_enabled):
            if not step.is_enabled(enabled_set):
                logging.info("%s: is disabled", step.uid)
                continue
            builder = self[step.uid]
            if only is not None and step.uid not in only:
                logging.info("%s: build is skipped", step.uid)
                continue
            builder.build(step.uid in force)
        logging.info("%s: finished building package", build_steps.uid)