1#
2#  Copyright (C) 2018 Codethink Limited
3#
4#  This program is free software; you can redistribute it and/or
5#  modify it under the terms of the GNU Lesser General Public
6#  License as published by the Free Software Foundation; either
7#  version 2 of the License, or (at your option) any later version.
8#
9#  This library is distributed in the hope that it will be useful,
10#  but WITHOUT ANY WARRANTY; without even the implied warranty of
11#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the GNU
12#  Lesser General Public License for more details.
13#
14#  You should have received a copy of the GNU Lesser General Public
15#  License along with this library. If not, see <http://www.gnu.org/licenses/>.
16#
17#  Authors:
18#        Tristan Van Berkom <tristan.vanberkom@codethink.co.uk>
19#        Tiago Gomes <tiago.gomes@codethink.co.uk>
20
21from enum import Enum
22
23# Disable pylint warnings for whole file here:
24# pylint: disable=global-statement
25
26# The last raised exception, this is used in test cases only
27_last_exception = None
28_last_task_error_domain = None
29_last_task_error_reason = None
30
31
32# get_last_exception()
33#
34# Fetches the last exception from the main process
35#
36# Used by regression tests
37#
38def get_last_exception():
39    global _last_exception
40
41    le = _last_exception
42    _last_exception = None
43    return le
44
45
46# get_last_task_error()
47#
48# Fetches the last exception from a task
49#
50# Used by regression tests
51#
52def get_last_task_error():
53    global _last_task_error_domain
54    global _last_task_error_reason
55
56    d = _last_task_error_domain
57    r = _last_task_error_reason
58    _last_task_error_domain = _last_task_error_reason = None
59    return (d, r)
60
61
62# set_last_task_error()
63#
64# Sets the last exception of a task
65#
66# This is set by some internals to inform regression
67# tests about how things failed in a machine readable way
68#
69def set_last_task_error(domain, reason):
70    global _last_task_error_domain
71    global _last_task_error_reason
72
73    _last_task_error_domain = domain
74    _last_task_error_reason = reason
75
76
77class ErrorDomain(Enum):
78    PLUGIN = 1
79    LOAD = 2
80    IMPL = 3
81    PLATFORM = 4
82    SANDBOX = 5
83    ARTIFACT = 6
84    PIPELINE = 7
85    OSTREE = 8
86    UTIL = 9
87    PROG_NOT_FOUND = 12
88    SOURCE = 10
89    ELEMENT = 11
90    APP = 12
91    STREAM = 13
92
93
94# BstError is an internal base exception class for BuildSream
95# exceptions.
96#
97# The sole purpose of using the base class is to add additional
98# context to exceptions raised by plugins in child tasks, this
99# context can then be communicated back to the main process.
100#
101class BstError(Exception):
102
103    def __init__(self, message, *, detail=None, domain=None, reason=None, temporary=False):
104        global _last_exception
105
106        super().__init__(message)
107
108        # Additional error detail, these are used to construct detail
109        # portions of the logging messages when encountered.
110        #
111        self.detail = detail
112
113        # The build sandbox in which the error occurred, if the
114        # error occurred at element assembly time.
115        #
116        self.sandbox = None
117
118        # When this exception occurred during the handling of a job, indicate
119        # whether or not there is any point retrying the job.
120        #
121        self.temporary = temporary
122
123        # Error domain and reason
124        #
125        self.domain = domain
126        self.reason = reason
127
128        # Hold on to the last raised exception for testing purposes
129        _last_exception = self
130
131
132# PluginError
133#
134# Raised on plugin related errors.
135#
136# This exception is raised either by the plugin loading process,
137# or by the base :class:`.Plugin` element itself.
138#
139class PluginError(BstError):
140    def __init__(self, message, reason=None, temporary=False):
141        super().__init__(message, domain=ErrorDomain.PLUGIN, reason=reason, temporary=False)
142
143
144# LoadErrorReason
145#
146# Describes the reason why a :class:`.LoadError` was raised.
147#
148class LoadErrorReason(Enum):
149
150    # A file was not found.
151    MISSING_FILE = 1
152
153    # The parsed data was not valid YAML.
154    INVALID_YAML = 2
155
156    # Data was malformed, a value was not of the expected type, etc
157    INVALID_DATA = 3
158
159    # An error occurred during YAML dictionary composition.
160    #
161    # This can happen by overriding a value with a new differently typed
162    # value, or by overwriting some named value when that was not allowed.
163    ILLEGAL_COMPOSITE = 4
164
165    # An circular dependency chain was detected
166    CIRCULAR_DEPENDENCY = 5
167
168    # A variable could not be resolved. This can happen if your project
169    # has cyclic dependencies in variable declarations, or, when substituting
170    # a string which refers to an undefined variable.
171    UNRESOLVED_VARIABLE = 6
172
173    # BuildStream does not support the required project format version
174    UNSUPPORTED_PROJECT = 7
175
176    # Project requires a newer version of a plugin than the one which was loaded
177    UNSUPPORTED_PLUGIN = 8
178
179    # A conditional expression failed to resolve
180    EXPRESSION_FAILED = 9
181
182    # An assertion was intentionally encoded into project YAML
183    USER_ASSERTION = 10
184
185    # A list composition directive did not apply to any underlying list
186    TRAILING_LIST_DIRECTIVE = 11
187
188    # Conflicting junctions in subprojects
189    CONFLICTING_JUNCTION = 12
190
191    # Failure to load a project from a specified junction
192    INVALID_JUNCTION = 13
193
194    # Subproject needs to be fetched
195    SUBPROJECT_FETCH_NEEDED = 14
196
197    # Subproject has no ref
198    SUBPROJECT_INCONSISTENT = 15
199
200    # An invalid symbol name was encountered
201    INVALID_SYMBOL_NAME = 16
202
203    # A project.conf file was missing
204    MISSING_PROJECT_CONF = 17
205
206    # Try to load a directory not a yaml file
207    LOADING_DIRECTORY = 18
208
209    # A project path leads outside of the project directory
210    PROJ_PATH_INVALID = 19
211
212    # A project path points to a file of the not right kind (e.g. a
213    # socket)
214    PROJ_PATH_INVALID_KIND = 20
215
216    # A recursive include has been encountered.
217    RECURSIVE_INCLUDE = 21
218
219    # A recursive variable has been encountered
220    RECURSIVE_VARIABLE = 22
221
222    # An attempt so set the value of a protected variable
223    PROTECTED_VARIABLE_REDEFINED = 23
224
225
226# LoadError
227#
228# Raised while loading some YAML.
229#
230# Args:
231#    reason (LoadErrorReason): machine readable error reason
232#    message (str): human readable error explanation
233#
234# This exception is raised when loading or parsing YAML, or when
235# interpreting project YAML
236#
237class LoadError(BstError):
238    def __init__(self, reason, message, *, detail=None):
239        super().__init__(message, detail=detail, domain=ErrorDomain.LOAD, reason=reason)
240
241
242# ImplError
243#
244# Raised when a :class:`.Source` or :class:`.Element` plugin fails to
245# implement a mandatory method
246#
247class ImplError(BstError):
248    def __init__(self, message, reason=None):
249        super().__init__(message, domain=ErrorDomain.IMPL, reason=reason)
250
251
252# PlatformError
253#
254# Raised if the current platform is not supported.
255class PlatformError(BstError):
256    def __init__(self, message, reason=None):
257        super().__init__(message, domain=ErrorDomain.PLATFORM, reason=reason)
258
259
260# SandboxError
261#
262# Raised when errors are encountered by the sandbox implementation
263#
264class SandboxError(BstError):
265    def __init__(self, message, reason=None):
266        super().__init__(message, domain=ErrorDomain.SANDBOX, reason=reason)
267
268
269# ArtifactError
270#
271# Raised when errors are encountered in the artifact caches
272#
273class ArtifactError(BstError):
274    def __init__(self, message, *, detail=None, reason=None, temporary=False):
275        super().__init__(message, detail=detail, domain=ErrorDomain.ARTIFACT, reason=reason, temporary=True)
276
277
278# PipelineError
279#
280# Raised from pipeline operations
281#
282class PipelineError(BstError):
283
284    def __init__(self, message, *, detail=None, reason=None):
285        super().__init__(message, detail=detail, domain=ErrorDomain.PIPELINE, reason=reason)
286
287
288# StreamError
289#
290# Raised when a stream operation fails
291#
292class StreamError(BstError):
293
294    def __init__(self, message=None, *, detail=None, reason=None, terminated=False):
295
296        # The empty string should never appear to a user,
297        # this only allows us to treat this internal error as
298        # a BstError from the frontend.
299        if message is None:
300            message = ""
301
302        super().__init__(message, detail=detail, domain=ErrorDomain.STREAM, reason=reason)
303
304        self.terminated = terminated
305
306
307# AppError
308#
309# Raised from the frontend App directly
310#
311class AppError(BstError):
312    def __init__(self, message, detail=None, reason=None):
313        super().__init__(message, detail=detail, domain=ErrorDomain.APP, reason=reason)
314
315
316# SkipJob
317#
318# Raised from a child process within a job when the job should be
319# considered skipped by the parent process.
320#
321class SkipJob(Exception):
322    pass
323