summaryrefslogtreecommitdiff
path: root/src/buildstream/_loader/loadcontext.py
blob: a9eb4a7b701d5b2c7bb3c3cb942332379e74fd44 (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
#
#  Copyright (C) 2020 Codethink Limited
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 2 of the License, or (at your option) any later version.
#
#  This library is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the GNU
#  Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public
#  License along with this library. If not, see <http://www.gnu.org/licenses/>.
#
#  Authors:
#        Tristan Van Berkom <tristan.vanberkom@codethink.co.uk>

from .._exceptions import LoadError
from ..exceptions import LoadErrorReason
from ..types import _ProjectInformation


# ProjectLoaders()
#
# An object representing all of the loaders for a given project.
#
class ProjectLoaders:
    def __init__(self, project_name):

        # The project name
        self._name = project_name

        # A list of all loaded loaders for this project
        self._collect = []

    # register_loader()
    #
    # Register a Loader for this project
    #
    # Args:
    #    loader (Loader): The loader to register
    #
    def register_loader(self, loader):
        assert loader.project.name == self._name

        self._collect.append(loader)

    # assert_loaders():
    #
    # Asserts the validity of loaders for this project
    #
    # Raises:
    #    (LoadError): In case there is a CONFLICTING_JUNCTION error
    #
    def assert_loaders(self):
        duplicates = {}
        internal = {}
        primary = []

        for loader in self._collect:
            duplicating, internalizing = self._search_project_relationships(loader)
            if duplicating:
                duplicates[loader] = duplicating
            if internalizing:
                internal[loader] = internalizing

            if not (duplicating or internalizing):
                primary.append(loader)

        if len(primary) > 1:
            self._raise_conflict(duplicates, internal)

        elif primary and duplicates:
            self._raise_conflict(duplicates, internal)

    # loaded_projects()
    #
    # A generator which yeilds all of the instances
    # of this loaded project.
    #
    # Yields:
    #    (_ProjectInformation): A descriptive project information object
    #
    def loaded_projects(self):
        for loader in self._collect:
            duplicating, internalizing = self._search_project_relationships(loader)
            yield _ProjectInformation(
                loader.project, loader.provenance_node, [str(l) for l in duplicating], [str(l) for l in internalizing]
            )

    # _search_project_relationships()
    #
    # Searches this loader's ancestry for projects which mark this
    # loader as internal or duplicate
    #
    # Args:
    #    loader (Loader): The loader to search for duplicate markers of
    #
    # Returns:
    #    (list): A list of Loader objects who's project has marked
    #            this junction as a duplicate
    #    (list): A list of Loader objects who's project has marked
    #            this junction as internal
    #
    def _search_project_relationships(self, loader):
        duplicates = []
        internal = []
        for parent in loader.ancestors():
            if parent.project.junction_is_duplicated(self._name, loader):
                duplicates.append(parent)
            if parent.project.junction_is_internal(loader):
                internal.append(parent)
        return duplicates, internal

    # _raise_conflict()
    #
    # Raises the LoadError indicating there was a conflict, this
    # will list all of the instances in which the project has
    # been loaded as the LoadError detail string
    #
    # Args:
    #    duplicates (dict): A table of duplicating Loaders, indexed
    #                       by duplicated Loader
    #    internals (dict): A table of Loaders which mark a loader as internal,
    #                      indexed by internal Loader
    #
    # Raises:
    #    (LoadError): In case there is a CONFLICTING_JUNCTION error
    #
    def _raise_conflict(self, duplicates, internals):
        explanation = (
            "Internal projects do not cause any conflicts. Conflicts can also be avoided\n"
            + "by marking every instance of the project as a duplicate."
        )
        lines = [self._loader_description(loader, duplicates, internals) for loader in self._collect]
        detail = "{}\n{}".format("\n".join(lines), explanation)

        raise LoadError(
            "Project '{}' was loaded in multiple contexts".format(self._name),
            LoadErrorReason.CONFLICTING_JUNCTION,
            detail=detail,
        )

    # _loader_description()
    #
    # Args:
    #    loader (Loader): The loader to describe
    #    duplicates (dict): A table of duplicating Loaders, indexed
    #                       by duplicated Loader
    #    internals (dict): A table of Loaders which mark a loader as internal,
    #                      indexed by internal Loader
    #
    # Returns:
    #    (str): A string representing how this loader was loaded
    #
    def _loader_description(self, loader, duplicates, internals):

        line = "{}\n".format(loader)

        # Mention projects which have marked this project as a duplicate
        duplicating = duplicates.get(loader)
        if duplicating:
            for dup in duplicating:
                line += "  Duplicated by: {}\n".format(dup)

        # Mention projects which have marked this project as internal
        internalizing = internals.get(loader)
        if internalizing:
            for internal in internalizing:
                line += "  Internal to: {}\n".format(internal)

        return line


# LoaderContext()
#
# An object to keep track of overall context during the load process.
#
# Args:
#    context (Context): The invocation context
#
class LoadContext:
    def __init__(self, context):

        # Keep track of global context required throughout the recursive load
        self.context = context
        self.rewritable = False
        self.fetch_subprojects = None
        self.task = None

        # A table of all Loaders, indexed by project name
        self._loaders = {}

    # set_rewritable()
    #
    # Sets whether the projects are to be loaded in a rewritable fashion,
    # this is used for tracking and is slightly more expensive in load time.
    #
    # Args:
    #   task (Task): The task to report progress on
    #
    def set_rewritable(self, rewritable):
        self.rewritable = rewritable

    # set_task()
    #
    # Sets the task for progress reporting.
    #
    # Args:
    #   task (Task): The task to report progress on
    #
    def set_task(self, task):
        self.task = task

    # set_fetch_subprojects()
    #
    # Sets the task for progress reporting.
    #
    # Args:
    #   task (callable): The callable for loading subprojects
    #
    def set_fetch_subprojects(self, fetch_subprojects):
        self.fetch_subprojects = fetch_subprojects

    # assert_loaders()
    #
    # Asserts that there are no conflicting projects loaded.
    #
    # Raises:
    #    (LoadError): A CONFLICTING_JUNCTION LoadError in the case of a conflict
    #
    def assert_loaders(self):
        for _, loaders in self._loaders.items():
            loaders.assert_loaders()

    # register_loader()
    #
    # Registers a new loader in the load context, possibly
    # raising an error in the case of a conflict.
    #
    # This must be called after a recursive load process has completed,
    # and after the pipeline is resolved (which is to say that all related
    # Plugin derived objects have been instantiated).
    #
    # Args:
    #    loader (Loader): The Loader object to register into context
    #
    def register_loader(self, loader):
        project = loader.project

        try:
            project_loaders = self._loaders[project.name]
        except KeyError:
            project_loaders = ProjectLoaders(project.name)
            self._loaders[project.name] = project_loaders

        project_loaders.register_loader(loader)

    # loaded_projects()
    #
    # A generator which yeilds all of the loaded projects
    #
    # Yields:
    #    (_ProjectInformation): A descriptive project information object
    #
    def loaded_projects(self):
        for _, project_loaders in self._loaders.items():
            yield from project_loaders.loaded_projects()