jolt 0.9.76__py3-none-any.whl → 0.9.429__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jolt/__init__.py +88 -7
- jolt/__main__.py +9 -1
- jolt/bin/fstree-darwin-x86_64 +0 -0
- jolt/bin/fstree-linux-x86_64 +0 -0
- jolt/cache.py +839 -367
- jolt/chroot.py +156 -0
- jolt/cli.py +362 -143
- jolt/common_pb2.py +63 -0
- jolt/common_pb2_grpc.py +4 -0
- jolt/config.py +99 -42
- jolt/error.py +19 -4
- jolt/expires.py +2 -2
- jolt/filesystem.py +8 -6
- jolt/graph.py +705 -117
- jolt/hooks.py +63 -1
- jolt/influence.py +129 -6
- jolt/loader.py +369 -121
- jolt/log.py +225 -63
- jolt/manifest.py +28 -38
- jolt/options.py +35 -10
- jolt/pkgs/abseil.py +42 -0
- jolt/pkgs/asio.py +25 -0
- jolt/pkgs/autoconf.py +41 -0
- jolt/pkgs/automake.py +41 -0
- jolt/pkgs/b2.py +31 -0
- jolt/pkgs/boost.py +111 -0
- jolt/pkgs/boringssl.py +32 -0
- jolt/pkgs/busybox.py +39 -0
- jolt/pkgs/bzip2.py +43 -0
- jolt/pkgs/cares.py +29 -0
- jolt/pkgs/catch2.py +36 -0
- jolt/pkgs/cbindgen.py +17 -0
- jolt/pkgs/cista.py +19 -0
- jolt/pkgs/clang.py +44 -0
- jolt/pkgs/cli11.py +23 -0
- jolt/pkgs/cmake.py +48 -0
- jolt/pkgs/cpython.py +196 -0
- jolt/pkgs/crun.py +29 -0
- jolt/pkgs/curl.py +38 -0
- jolt/pkgs/dbus.py +18 -0
- jolt/pkgs/double_conversion.py +24 -0
- jolt/pkgs/fastfloat.py +21 -0
- jolt/pkgs/ffmpeg.py +28 -0
- jolt/pkgs/flatbuffers.py +29 -0
- jolt/pkgs/fmt.py +27 -0
- jolt/pkgs/fstree.py +20 -0
- jolt/pkgs/gflags.py +18 -0
- jolt/pkgs/glib.py +18 -0
- jolt/pkgs/glog.py +25 -0
- jolt/pkgs/glslang.py +21 -0
- jolt/pkgs/golang.py +16 -11
- jolt/pkgs/googlebenchmark.py +18 -0
- jolt/pkgs/googletest.py +46 -0
- jolt/pkgs/gperf.py +15 -0
- jolt/pkgs/grpc.py +73 -0
- jolt/pkgs/hdf5.py +19 -0
- jolt/pkgs/help2man.py +14 -0
- jolt/pkgs/inja.py +28 -0
- jolt/pkgs/jsoncpp.py +31 -0
- jolt/pkgs/libarchive.py +43 -0
- jolt/pkgs/libcap.py +44 -0
- jolt/pkgs/libdrm.py +44 -0
- jolt/pkgs/libedit.py +42 -0
- jolt/pkgs/libevent.py +31 -0
- jolt/pkgs/libexpat.py +27 -0
- jolt/pkgs/libfastjson.py +21 -0
- jolt/pkgs/libffi.py +16 -0
- jolt/pkgs/libglvnd.py +30 -0
- jolt/pkgs/libogg.py +28 -0
- jolt/pkgs/libpciaccess.py +18 -0
- jolt/pkgs/libseccomp.py +21 -0
- jolt/pkgs/libtirpc.py +24 -0
- jolt/pkgs/libtool.py +42 -0
- jolt/pkgs/libunwind.py +35 -0
- jolt/pkgs/libva.py +18 -0
- jolt/pkgs/libvorbis.py +33 -0
- jolt/pkgs/libxml2.py +35 -0
- jolt/pkgs/libxslt.py +17 -0
- jolt/pkgs/libyajl.py +16 -0
- jolt/pkgs/llvm.py +81 -0
- jolt/pkgs/lua.py +54 -0
- jolt/pkgs/lz4.py +26 -0
- jolt/pkgs/m4.py +14 -0
- jolt/pkgs/make.py +17 -0
- jolt/pkgs/mesa.py +81 -0
- jolt/pkgs/meson.py +17 -0
- jolt/pkgs/mstch.py +28 -0
- jolt/pkgs/mysql.py +60 -0
- jolt/pkgs/nasm.py +49 -0
- jolt/pkgs/ncurses.py +30 -0
- jolt/pkgs/ng_log.py +25 -0
- jolt/pkgs/ninja.py +45 -0
- jolt/pkgs/nlohmann_json.py +25 -0
- jolt/pkgs/nodejs.py +19 -11
- jolt/pkgs/opencv.py +24 -0
- jolt/pkgs/openjdk.py +26 -0
- jolt/pkgs/openssl.py +103 -0
- jolt/pkgs/paho.py +76 -0
- jolt/pkgs/patchelf.py +16 -0
- jolt/pkgs/perl.py +42 -0
- jolt/pkgs/pkgconfig.py +64 -0
- jolt/pkgs/poco.py +39 -0
- jolt/pkgs/protobuf.py +77 -0
- jolt/pkgs/pugixml.py +27 -0
- jolt/pkgs/python.py +19 -0
- jolt/pkgs/qt.py +35 -0
- jolt/pkgs/rapidjson.py +26 -0
- jolt/pkgs/rapidyaml.py +28 -0
- jolt/pkgs/re2.py +30 -0
- jolt/pkgs/re2c.py +17 -0
- jolt/pkgs/readline.py +15 -0
- jolt/pkgs/rust.py +41 -0
- jolt/pkgs/sdl.py +28 -0
- jolt/pkgs/simdjson.py +27 -0
- jolt/pkgs/soci.py +46 -0
- jolt/pkgs/spdlog.py +29 -0
- jolt/pkgs/spirv_llvm.py +21 -0
- jolt/pkgs/spirv_tools.py +24 -0
- jolt/pkgs/sqlite.py +83 -0
- jolt/pkgs/ssl.py +12 -0
- jolt/pkgs/texinfo.py +15 -0
- jolt/pkgs/tomlplusplus.py +22 -0
- jolt/pkgs/wayland.py +26 -0
- jolt/pkgs/x11.py +58 -0
- jolt/pkgs/xerces_c.py +20 -0
- jolt/pkgs/xorg.py +360 -0
- jolt/pkgs/xz.py +29 -0
- jolt/pkgs/yamlcpp.py +30 -0
- jolt/pkgs/zeromq.py +47 -0
- jolt/pkgs/zlib.py +69 -0
- jolt/pkgs/zstd.py +33 -0
- jolt/plugins/alias.py +3 -0
- jolt/plugins/allure.py +5 -2
- jolt/plugins/autotools.py +66 -0
- jolt/plugins/cache.py +133 -0
- jolt/plugins/cmake.py +74 -6
- jolt/plugins/conan.py +238 -0
- jolt/plugins/cxx.py +698 -0
- jolt/plugins/cxxinfo.py +7 -0
- jolt/plugins/dashboard.py +1 -1
- jolt/plugins/docker.py +91 -23
- jolt/plugins/email.py +5 -2
- jolt/plugins/email.xslt +144 -101
- jolt/plugins/environ.py +11 -0
- jolt/plugins/fetch.py +141 -0
- jolt/plugins/gdb.py +44 -21
- jolt/plugins/gerrit.py +1 -14
- jolt/plugins/git.py +316 -101
- jolt/plugins/googletest.py +522 -1
- jolt/plugins/http.py +36 -38
- jolt/plugins/libtool.py +63 -0
- jolt/plugins/linux.py +990 -0
- jolt/plugins/logstash.py +4 -4
- jolt/plugins/meson.py +61 -0
- jolt/plugins/ninja-compdb.py +107 -31
- jolt/plugins/ninja.py +929 -134
- jolt/plugins/paths.py +11 -1
- jolt/plugins/pkgconfig.py +219 -0
- jolt/plugins/podman.py +148 -91
- jolt/plugins/python.py +137 -0
- jolt/plugins/remote_execution/__init__.py +0 -0
- jolt/plugins/remote_execution/administration_pb2.py +46 -0
- jolt/plugins/remote_execution/administration_pb2_grpc.py +170 -0
- jolt/plugins/remote_execution/log_pb2.py +32 -0
- jolt/plugins/remote_execution/log_pb2_grpc.py +68 -0
- jolt/plugins/remote_execution/scheduler_pb2.py +41 -0
- jolt/plugins/remote_execution/scheduler_pb2_grpc.py +141 -0
- jolt/plugins/remote_execution/worker_pb2.py +38 -0
- jolt/plugins/remote_execution/worker_pb2_grpc.py +112 -0
- jolt/plugins/report.py +12 -2
- jolt/plugins/rust.py +25 -0
- jolt/plugins/scheduler.py +710 -0
- jolt/plugins/selfdeploy/setup.py +9 -4
- jolt/plugins/selfdeploy.py +138 -88
- jolt/plugins/strings.py +35 -22
- jolt/plugins/symlinks.py +26 -11
- jolt/plugins/telemetry.py +5 -2
- jolt/plugins/timeline.py +13 -3
- jolt/plugins/volume.py +46 -48
- jolt/scheduler.py +591 -191
- jolt/tasks.py +1783 -245
- jolt/templates/export.sh.template +12 -6
- jolt/templates/timeline.html.template +44 -47
- jolt/timer.py +22 -0
- jolt/tools.py +749 -302
- jolt/utils.py +245 -18
- jolt/version.py +1 -1
- jolt/version_utils.py +2 -2
- jolt/xmldom.py +12 -2
- {jolt-0.9.76.dist-info → jolt-0.9.429.dist-info}/METADATA +98 -38
- jolt-0.9.429.dist-info/RECORD +207 -0
- {jolt-0.9.76.dist-info → jolt-0.9.429.dist-info}/WHEEL +1 -1
- jolt/plugins/amqp.py +0 -834
- jolt/plugins/debian.py +0 -338
- jolt/plugins/ftp.py +0 -181
- jolt/plugins/ninja-cache.py +0 -64
- jolt/plugins/ninjacli.py +0 -271
- jolt/plugins/repo.py +0 -253
- jolt-0.9.76.dist-info/RECORD +0 -79
- {jolt-0.9.76.dist-info → jolt-0.9.429.dist-info}/entry_points.txt +0 -0
- {jolt-0.9.76.dist-info → jolt-0.9.429.dist-info}/top_level.txt +0 -0
jolt/tasks.py
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
from collections import OrderedDict
|
|
3
|
-
from
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
4
|
+
from contextlib import contextmanager, ExitStack
|
|
4
5
|
import copy
|
|
5
6
|
import fnmatch
|
|
6
7
|
import functools
|
|
8
|
+
import hashlib
|
|
7
9
|
import platform
|
|
10
|
+
from threading import RLock
|
|
8
11
|
import subprocess
|
|
9
12
|
from os import environ
|
|
13
|
+
from os import sys as os_sys
|
|
10
14
|
import sys
|
|
11
15
|
import unittest as ut
|
|
16
|
+
from urllib.parse import urlparse
|
|
12
17
|
import uuid
|
|
13
18
|
import re
|
|
14
19
|
import traceback
|
|
@@ -19,10 +24,11 @@ from jolt import utils
|
|
|
19
24
|
from jolt.cache import ArtifactAttributeSetProvider
|
|
20
25
|
from jolt.error import raise_error_if, raise_task_error, raise_task_error_if
|
|
21
26
|
from jolt.error import raise_unreported_task_error_if
|
|
22
|
-
from jolt.error import JoltError, JoltCommandError
|
|
27
|
+
from jolt.error import JoltError, JoltCommandError, LoggedJoltError
|
|
23
28
|
from jolt.expires import Immediately
|
|
24
29
|
from jolt.influence import FileInfluence, TaintInfluenceProvider
|
|
25
30
|
from jolt.influence import TaskClassSourceInfluence
|
|
31
|
+
from jolt.influence import CallbackInfluence
|
|
26
32
|
from jolt.influence import attribute as attribute_influence
|
|
27
33
|
from jolt.influence import environ as environ_influence
|
|
28
34
|
from jolt.influence import source as source_influence
|
|
@@ -94,6 +100,23 @@ class EnvironExport(Export):
|
|
|
94
100
|
self._task.tools.setenv(self._envname, value)
|
|
95
101
|
|
|
96
102
|
|
|
103
|
+
class ParameterValueError(JoltError):
|
|
104
|
+
""" Raised if an illegal value is assigned to a parameter """
|
|
105
|
+
def __init__(self, param, value, what=None, detail=None):
|
|
106
|
+
what = what + " " if what is not None else ""
|
|
107
|
+
detail = ", " + detail if detail is not None else ""
|
|
108
|
+
if hasattr(param, "name"):
|
|
109
|
+
super().__init__(f"Illegal {what}value '{value}' assigned to parameter '{param.name}'{detail}")
|
|
110
|
+
else:
|
|
111
|
+
super().__init__(f"Illegal {what}value '{value}' assigned to {type(param).__name__}{detail}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ParameterImmutableError(JoltError):
|
|
115
|
+
""" Raised if an immutable (const=True) parameter is reassigned """
|
|
116
|
+
def __init__(self, param):
|
|
117
|
+
super().__init__(f"Cannot reassign immutable parameter '{param.name}'")
|
|
118
|
+
|
|
119
|
+
|
|
97
120
|
class Parameter(object):
|
|
98
121
|
""" Generic task parameter type. """
|
|
99
122
|
|
|
@@ -111,9 +134,10 @@ class Parameter(object):
|
|
|
111
134
|
if "__param_list" not in owner.__dict__:
|
|
112
135
|
setattr(owner, "__param_list", {})
|
|
113
136
|
getattr(owner, "__param_list")[name] = self
|
|
137
|
+
self.name = name
|
|
114
138
|
|
|
115
139
|
def __init__(self, default=None, values=None, required=True,
|
|
116
|
-
const=False, influence=True, help=None):
|
|
140
|
+
const=False, influence=True, help=None, valuesfn=None):
|
|
117
141
|
"""
|
|
118
142
|
Creates a new parameter.
|
|
119
143
|
|
|
@@ -121,6 +145,10 @@ class Parameter(object):
|
|
|
121
145
|
default (str, optional): An optional default value.
|
|
122
146
|
values (list, optional): A list of accepted values. An
|
|
123
147
|
assertion is raised if an unlisted value is assigned to the parameter.
|
|
148
|
+
valuesfn (func, optional); A function that validates the assigned value.
|
|
149
|
+
If both values and valuesfn are specified, the values list is validated
|
|
150
|
+
first. The function is passed the assigned value and should return.
|
|
151
|
+
boolean value. If the function returns False, a ParameterValueError is raised.
|
|
124
152
|
required (boolean, optional): If required, the parameter must be assigned
|
|
125
153
|
a value before the task can be executed. The default is ``True``.
|
|
126
154
|
const (boolean, optional): If const is True, the parameter is immutable
|
|
@@ -135,42 +163,51 @@ class Parameter(object):
|
|
|
135
163
|
associated task.
|
|
136
164
|
|
|
137
165
|
Raises:
|
|
138
|
-
|
|
166
|
+
ParameterValueError: If the parameter is assigned an illegal value.
|
|
139
167
|
|
|
140
168
|
"""
|
|
141
169
|
|
|
142
170
|
self._default = default
|
|
143
171
|
self._value = default
|
|
144
172
|
self._accepted_values = values
|
|
173
|
+
self._accepted_values_fn = valuesfn
|
|
145
174
|
self._required = required
|
|
146
175
|
self._const = const
|
|
147
176
|
self._influence = influence
|
|
148
177
|
self._help = help
|
|
149
|
-
if default:
|
|
150
|
-
self._validate(default)
|
|
178
|
+
if default is not None:
|
|
179
|
+
self._validate(default, "default")
|
|
151
180
|
|
|
152
181
|
@property
|
|
153
182
|
def help(self):
|
|
154
183
|
values = self._help_values()
|
|
155
184
|
if values:
|
|
156
|
-
return "{} {}"
|
|
185
|
+
return f"{self._help} {values}"if self._help else values
|
|
186
|
+
elif self._default is not None:
|
|
187
|
+
return f"{self._help} [default: {self._help_default}]" if self._help else f"[default: {self._help_default}]"
|
|
157
188
|
return self._help or ""
|
|
158
189
|
|
|
190
|
+
@property
|
|
191
|
+
def _help_default(self):
|
|
192
|
+
return colors.bright(str(self._default))
|
|
193
|
+
|
|
159
194
|
def _help_values(self, accepted=None):
|
|
160
195
|
accepted = accepted or self._accepted_values or []
|
|
161
196
|
|
|
162
197
|
def highlight(value):
|
|
163
198
|
return colors.bright(value) if self._is_default(value) else colors.dim(value)
|
|
164
199
|
|
|
165
|
-
return "[{}]".format(", ".join([highlight(value) for value in accepted])) if accepted else ""
|
|
200
|
+
return "[{}]".format(", ".join([highlight(str(value)) for value in accepted])) if accepted else ""
|
|
166
201
|
|
|
167
202
|
def __str__(self):
|
|
168
203
|
""" Returns the parameter value as a string """
|
|
169
204
|
return str(self._value) if self._value is not None else ''
|
|
170
205
|
|
|
171
|
-
def _validate(self, value):
|
|
206
|
+
def _validate(self, value, what=None):
|
|
172
207
|
if self._accepted_values is not None and value not in self._accepted_values:
|
|
173
|
-
raise
|
|
208
|
+
raise ParameterValueError(self, value, what=what)
|
|
209
|
+
if self._accepted_values_fn is not None and not self._accepted_values_fn(value):
|
|
210
|
+
raise ParameterValueError(self, value, what=what)
|
|
174
211
|
|
|
175
212
|
def get_default(self):
|
|
176
213
|
""" Get the default value of the parameter.
|
|
@@ -275,11 +312,11 @@ class Parameter(object):
|
|
|
275
312
|
value (str): The new parameter value.
|
|
276
313
|
|
|
277
314
|
Raises:
|
|
278
|
-
|
|
315
|
+
ParameterValueError: If the parameter is assigned an illegal value.
|
|
279
316
|
"""
|
|
280
317
|
self._validate(value)
|
|
281
318
|
if self._const and value != self._default:
|
|
282
|
-
raise
|
|
319
|
+
raise ParameterImmutableError(self)
|
|
283
320
|
self._value = value
|
|
284
321
|
|
|
285
322
|
|
|
@@ -316,15 +353,15 @@ class BooleanParameter(Parameter):
|
|
|
316
353
|
will not influence the identity of the task artifact. The default is
|
|
317
354
|
True.
|
|
318
355
|
help (str, optional): Documentation for the parameter.
|
|
319
|
-
This text is displayed when running the ``
|
|
356
|
+
This text is displayed when running the ``inspect`` command on the
|
|
320
357
|
associated task.
|
|
321
358
|
|
|
322
359
|
Raises:
|
|
323
|
-
|
|
360
|
+
ParameterValueError: If the parameter is assigned an illegal value.
|
|
324
361
|
|
|
325
362
|
"""
|
|
326
363
|
default = str(default).lower() if default is not None else None
|
|
327
|
-
super(
|
|
364
|
+
super().__init__(
|
|
328
365
|
default,
|
|
329
366
|
values=["false", "true", "0", "1", "no", "yes"],
|
|
330
367
|
required=required,
|
|
@@ -343,10 +380,10 @@ class BooleanParameter(Parameter):
|
|
|
343
380
|
False, True, "false, and "true", 0 and 1, "no" and "yes".
|
|
344
381
|
|
|
345
382
|
Raises:
|
|
346
|
-
|
|
383
|
+
ParameterValueError: If the parameter is assigned an illegal value.
|
|
347
384
|
"""
|
|
348
385
|
value = str(value).lower()
|
|
349
|
-
super(
|
|
386
|
+
super().set_value(value)
|
|
350
387
|
|
|
351
388
|
@property
|
|
352
389
|
def is_true(self):
|
|
@@ -399,6 +436,198 @@ class BooleanParameter(Parameter):
|
|
|
399
436
|
return key[0] if self.is_true else key[1]
|
|
400
437
|
|
|
401
438
|
|
|
439
|
+
class IntParameter(Parameter):
|
|
440
|
+
"""
|
|
441
|
+
Integer task parameter.
|
|
442
|
+
|
|
443
|
+
Implements all regular unary and binary integer operators.
|
|
444
|
+
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
def __init__(self, default=None, min=None, max=None, values=None, required=True, const=False,
|
|
448
|
+
influence=True, help=None, valuesfn=None):
|
|
449
|
+
"""
|
|
450
|
+
Creates a new parameter.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
default (int, optional): An optional default integer value.
|
|
454
|
+
min (int, optional): Minimum allowed value.
|
|
455
|
+
max (int, optional): Maximum allowed value.
|
|
456
|
+
values (list, optional): A list of accepted values. An
|
|
457
|
+
assertion is raised if an unlisted value is assigned to the parameter.
|
|
458
|
+
required (boolean, optional): If required, the parameter must be assigned
|
|
459
|
+
a value before the task can be executed. The default is ``True``.
|
|
460
|
+
const (boolean, optional): If const is True, the parameter is immutable
|
|
461
|
+
and cannot be assigned a non-default value. This is useful in
|
|
462
|
+
a class hierarchy where a subclass may want to impose restrictions
|
|
463
|
+
on a parent class parameter. The default is ``False``.
|
|
464
|
+
influence (boolean, optional): If influence is False, the parameter value
|
|
465
|
+
will not influence the identity of the task artifact. The default is
|
|
466
|
+
True.
|
|
467
|
+
help (str, optional): Documentation for the parameter.
|
|
468
|
+
This text is displayed when running the ``inspect`` command on the
|
|
469
|
+
associated task.
|
|
470
|
+
|
|
471
|
+
Raises:
|
|
472
|
+
ParameterValueError: If the parameter is assigned an illegal value.
|
|
473
|
+
|
|
474
|
+
"""
|
|
475
|
+
try:
|
|
476
|
+
default = int(default) if default is not None else None
|
|
477
|
+
except ValueError:
|
|
478
|
+
raise ParameterValueError(self, default, what="default")
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
self._min = int(min) if min is not None else None
|
|
482
|
+
except ValueError:
|
|
483
|
+
raise ParameterValueError(self, min, what="minimum")
|
|
484
|
+
|
|
485
|
+
try:
|
|
486
|
+
self._max = int(max) if max is not None else None
|
|
487
|
+
except ValueError:
|
|
488
|
+
raise ParameterValueError(self, max, what="maximum")
|
|
489
|
+
|
|
490
|
+
super().__init__(
|
|
491
|
+
default,
|
|
492
|
+
values,
|
|
493
|
+
required=required,
|
|
494
|
+
const=const,
|
|
495
|
+
influence=influence,
|
|
496
|
+
help=help,
|
|
497
|
+
valuesfn=valuesfn)
|
|
498
|
+
|
|
499
|
+
def _validate(self, value, what=None):
|
|
500
|
+
if self._min is not None and value < self._min:
|
|
501
|
+
raise ParameterValueError(self, value, what=what, detail=f"less than minimum value '{self._min}'")
|
|
502
|
+
if self._max is not None and value > self._max:
|
|
503
|
+
raise ParameterValueError(self, value, what=what, detail=f"greater than maximum value '{self._max}'")
|
|
504
|
+
super()._validate(value, what)
|
|
505
|
+
|
|
506
|
+
def set_value(self, value):
|
|
507
|
+
""" Set the parameter value.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
value (boolean): The new parameter value. Accepted values are:
|
|
511
|
+
False, True, "false, and "true", 0 and 1, "no" and "yes".
|
|
512
|
+
|
|
513
|
+
Raises:
|
|
514
|
+
ParameterValueError: If the parameter is assigned an illegal value.
|
|
515
|
+
"""
|
|
516
|
+
try:
|
|
517
|
+
value = int(value)
|
|
518
|
+
except ValueError:
|
|
519
|
+
raise ParameterValueError(self, value)
|
|
520
|
+
super().set_value(value)
|
|
521
|
+
|
|
522
|
+
def __bool__(self):
|
|
523
|
+
""" Evaluates to False if the value is 0, True otherwise """
|
|
524
|
+
return self.get_value() != 0
|
|
525
|
+
|
|
526
|
+
def __int__(self):
|
|
527
|
+
""" Returns the integer parameter value """
|
|
528
|
+
return int(self.get_value())
|
|
529
|
+
|
|
530
|
+
def __lt__(self, other):
|
|
531
|
+
""" Less-than comparison with another integer value """
|
|
532
|
+
return int(self.get_value()) < int(other)
|
|
533
|
+
|
|
534
|
+
def __le__(self, other):
|
|
535
|
+
""" Less-or-equal comparison with another integer value """
|
|
536
|
+
return int(self.get_value()) <= int(other)
|
|
537
|
+
|
|
538
|
+
def __gt__(self, other):
|
|
539
|
+
""" Greater-than comparison with another integer value """
|
|
540
|
+
return int(self.get_value()) > int(other)
|
|
541
|
+
|
|
542
|
+
def __ge__(self, other):
|
|
543
|
+
""" Greater-or-equal comparison with another integer value """
|
|
544
|
+
return int(self.get_value()) >= int(other)
|
|
545
|
+
|
|
546
|
+
def __add__(self, other):
|
|
547
|
+
return int(self.get_value()).__add__(other)
|
|
548
|
+
|
|
549
|
+
def __sub__(self, other):
|
|
550
|
+
return int(self.get_value()).__sub__(other)
|
|
551
|
+
|
|
552
|
+
def __mul__(self, other):
|
|
553
|
+
return int(self.get_value()).__mul__(other)
|
|
554
|
+
|
|
555
|
+
def __truediv__(self, other):
|
|
556
|
+
return int(self.get_value()).__truediv__(other)
|
|
557
|
+
|
|
558
|
+
def __floordiv__(self, other):
|
|
559
|
+
return int(self.get_value()).__floordiv__(other)
|
|
560
|
+
|
|
561
|
+
def __mod__(self, other):
|
|
562
|
+
return int(self.get_value()).__mod__(other)
|
|
563
|
+
|
|
564
|
+
def __divmod__(self, other):
|
|
565
|
+
return int(self.get_value()).__divmod__(other)
|
|
566
|
+
|
|
567
|
+
def __pow__(self, other, modulo=None):
|
|
568
|
+
return int(self.get_value()).__pow__(other, modulo)
|
|
569
|
+
|
|
570
|
+
def __lshift__(self, other):
|
|
571
|
+
return int(self.get_value()).__lshift__(other)
|
|
572
|
+
|
|
573
|
+
def __rshift__(self, other):
|
|
574
|
+
return int(self.get_value()).__rshift__(other)
|
|
575
|
+
|
|
576
|
+
def __and__(self, other):
|
|
577
|
+
return int(self.get_value()).__and__(other)
|
|
578
|
+
|
|
579
|
+
def __xor__(self, other):
|
|
580
|
+
return int(self.get_value()).__xor__(other)
|
|
581
|
+
|
|
582
|
+
def __or__(self, other):
|
|
583
|
+
return int(self.get_value()).__or__(other)
|
|
584
|
+
|
|
585
|
+
def __radd__(self, other):
|
|
586
|
+
return int(self.get_value()).__radd__(other)
|
|
587
|
+
|
|
588
|
+
def __rsub__(self, other):
|
|
589
|
+
return int(self.get_value()).__rsub__(other)
|
|
590
|
+
|
|
591
|
+
def __rmul__(self, other):
|
|
592
|
+
return int(self.get_value()).__rmul__(other)
|
|
593
|
+
|
|
594
|
+
def __rtruediv__(self, other):
|
|
595
|
+
return int(self.get_value()).__rtruediv__(other)
|
|
596
|
+
|
|
597
|
+
def __rfloordiv__(self, other):
|
|
598
|
+
return int(self.get_value()).__rfloordiv__(other)
|
|
599
|
+
|
|
600
|
+
def __rmod__(self, other):
|
|
601
|
+
return int(self.get_value()).__rmod__(other)
|
|
602
|
+
|
|
603
|
+
def __rlshift__(self, other):
|
|
604
|
+
return int(self.get_value()).__rlshift__(other)
|
|
605
|
+
|
|
606
|
+
def __rrshift__(self, other):
|
|
607
|
+
return int(self.get_value()).__rrshift__(other)
|
|
608
|
+
|
|
609
|
+
def __rand__(self, other):
|
|
610
|
+
return int(self.get_value()).__rand__(other)
|
|
611
|
+
|
|
612
|
+
def __rxor__(self, other):
|
|
613
|
+
return int(self.get_value()).__rxor__(other)
|
|
614
|
+
|
|
615
|
+
def __ror__(self, other):
|
|
616
|
+
return int(self.get_value()).__ror__(other)
|
|
617
|
+
|
|
618
|
+
def __neg__(self):
|
|
619
|
+
return int(self.get_value()).__neg__()
|
|
620
|
+
|
|
621
|
+
def __pos__(self):
|
|
622
|
+
return int(self.get_value()).__pos__()
|
|
623
|
+
|
|
624
|
+
def __abs__(self):
|
|
625
|
+
return int(self.get_value()).__abs__()
|
|
626
|
+
|
|
627
|
+
def __invert__(self):
|
|
628
|
+
return int(self.get_value()).__invert__()
|
|
629
|
+
|
|
630
|
+
|
|
402
631
|
class ListParameter(Parameter):
|
|
403
632
|
""" List parameter type.
|
|
404
633
|
|
|
@@ -442,11 +671,11 @@ class ListParameter(Parameter):
|
|
|
442
671
|
will not influence the identity of the task artifact. The default is
|
|
443
672
|
True.
|
|
444
673
|
help (str, optional): Documentation for the parameter.
|
|
445
|
-
This text is displayed when running the ``
|
|
674
|
+
This text is displayed when running the ``inspect`` command on the
|
|
446
675
|
associated task.
|
|
447
676
|
|
|
448
677
|
Raises:
|
|
449
|
-
|
|
678
|
+
ParameterValueError: If the parameter is assigned an illegal value.
|
|
450
679
|
|
|
451
680
|
"""
|
|
452
681
|
super().__init__(*args, **kwargs)
|
|
@@ -458,17 +687,21 @@ class ListParameter(Parameter):
|
|
|
458
687
|
value (str): A list of accepted values, separated by '+'.
|
|
459
688
|
|
|
460
689
|
Raises:
|
|
461
|
-
|
|
690
|
+
ParameterValueError: If the parameter is assigned an illegal value.
|
|
462
691
|
"""
|
|
463
|
-
value = str(value).split("+") if type(value)
|
|
692
|
+
value = str(value).split("+") if type(value) is str else value
|
|
464
693
|
value.sort()
|
|
465
694
|
super().set_value(value)
|
|
466
695
|
|
|
467
|
-
def _validate(self, value):
|
|
696
|
+
def _validate(self, value, what=None):
|
|
468
697
|
if self._accepted_values is not None:
|
|
469
698
|
for item in value:
|
|
470
699
|
if item not in self._accepted_values:
|
|
471
|
-
raise
|
|
700
|
+
raise ParameterValueError(self, item, what=what)
|
|
701
|
+
if self._accepted_values_fn is not None:
|
|
702
|
+
for item in value:
|
|
703
|
+
if not self._accepted_values_fn(item):
|
|
704
|
+
raise ParameterValueError(self, item, what=what)
|
|
472
705
|
|
|
473
706
|
def get_value(self):
|
|
474
707
|
return "+".join(self._value)
|
|
@@ -531,7 +764,7 @@ class ListParameter(Parameter):
|
|
|
531
764
|
[INFO] optimization is disabled (example)
|
|
532
765
|
|
|
533
766
|
"""
|
|
534
|
-
if type(key)
|
|
767
|
+
if type(key) is str:
|
|
535
768
|
key = key.split(",")
|
|
536
769
|
if len(key) == 1:
|
|
537
770
|
true = key[0]
|
|
@@ -550,6 +783,7 @@ class TaskRegistry(object):
|
|
|
550
783
|
self.env = env
|
|
551
784
|
self.tasks = {}
|
|
552
785
|
self.instances = {}
|
|
786
|
+
self._workspace_resources = []
|
|
553
787
|
|
|
554
788
|
@staticmethod
|
|
555
789
|
def get(*args, **kwargs):
|
|
@@ -558,6 +792,21 @@ class TaskRegistry(object):
|
|
|
558
792
|
return TaskRegistry._instance
|
|
559
793
|
|
|
560
794
|
def add_task_class(self, cls):
|
|
795
|
+
"""
|
|
796
|
+
Add a task class to the registry.
|
|
797
|
+
|
|
798
|
+
The class is decorated to require workspace resources.
|
|
799
|
+
"""
|
|
800
|
+
|
|
801
|
+
registry = self
|
|
802
|
+
|
|
803
|
+
def _workspace_resources(self):
|
|
804
|
+
return registry._workspace_resources
|
|
805
|
+
|
|
806
|
+
if not issubclass(cls, WorkspaceResource):
|
|
807
|
+
cls = attributes.requires("_workspace_resources")(cls)
|
|
808
|
+
cls._workspace_resources = property(_workspace_resources)
|
|
809
|
+
|
|
561
810
|
self.tasks[cls.name] = cls
|
|
562
811
|
|
|
563
812
|
def add_task(self, task, extra_params):
|
|
@@ -566,13 +815,20 @@ class TaskRegistry(object):
|
|
|
566
815
|
full_name = utils.format_task_name(name, params)
|
|
567
816
|
self.instances[full_name] = task
|
|
568
817
|
|
|
818
|
+
def require_workspace_resource(self, taskname):
|
|
819
|
+
name, _ = utils.parse_task_name(taskname)
|
|
820
|
+
task = self.get_task_class(name)
|
|
821
|
+
raise_task_error_if(task is None, name, "Resource not found")
|
|
822
|
+
raise_task_error_if(not issubclass(task, WorkspaceResource), name, "Not a workspace resource")
|
|
823
|
+
self._workspace_resources.append(taskname)
|
|
824
|
+
|
|
569
825
|
def get_task_class(self, name):
|
|
570
826
|
return self.tasks.get(name)
|
|
571
827
|
|
|
572
828
|
def get_task_classes(self):
|
|
573
829
|
return list(self.tasks.values())
|
|
574
830
|
|
|
575
|
-
def get_task(self, name, extra_params=None, manifest=None):
|
|
831
|
+
def get_task(self, name, extra_params=None, manifest=None, buildenv=None):
|
|
576
832
|
name, params = utils.parse_task_name(name)
|
|
577
833
|
params.update(extra_params or {})
|
|
578
834
|
full_name = utils.format_task_name(name, params)
|
|
@@ -583,14 +839,19 @@ class TaskRegistry(object):
|
|
|
583
839
|
|
|
584
840
|
cls = self.tasks.get(name)
|
|
585
841
|
if cls:
|
|
586
|
-
task = cls(parameters=params, manifest=manifest)
|
|
842
|
+
task = cls(parameters=params, manifest=manifest, buildenv=buildenv)
|
|
587
843
|
task = self.instances.get(task.qualified_name, task)
|
|
588
|
-
|
|
589
|
-
|
|
844
|
+
if not isinstance(task, Resource) or isinstance(task, WorkspaceResource):
|
|
845
|
+
self.instances[task.qualified_name] = task
|
|
846
|
+
self.instances[full_name] = task
|
|
590
847
|
return task
|
|
591
848
|
|
|
592
849
|
raise_task_error_if(not task, full_name, "No such task")
|
|
593
850
|
|
|
851
|
+
def has_task(self, name):
|
|
852
|
+
name, params = utils.parse_task_name(name)
|
|
853
|
+
return self.tasks.get(name) is not None
|
|
854
|
+
|
|
594
855
|
def set_default_parameters(self, task):
|
|
595
856
|
name, params = utils.parse_task_name(task)
|
|
596
857
|
|
|
@@ -663,21 +924,198 @@ class TaskGenerator(object):
|
|
|
663
924
|
|
|
664
925
|
class attributes:
|
|
665
926
|
@staticmethod
|
|
666
|
-
def
|
|
927
|
+
def arch(cls):
|
|
928
|
+
""" Return the architecture name (x86_64, arm64, etc.). """
|
|
929
|
+
|
|
930
|
+
cls._arch = Export(lambda t: platform.machine().lower().replace("amd64", "x86_64"))
|
|
931
|
+
cls.arch = property(lambda t: t._arch.value)
|
|
932
|
+
return cls
|
|
933
|
+
|
|
934
|
+
@staticmethod
|
|
935
|
+
def artifact(name, session=False):
|
|
936
|
+
"""Decorator adding an additional artifact to a task.
|
|
937
|
+
|
|
938
|
+
Jolt calls the new publish method `publish_<name>` with the
|
|
939
|
+
new artifact as argument. Non-alphanumeric characters in the
|
|
940
|
+
name are replaced with underscores (_). The new artifact is
|
|
941
|
+
consumable from another task by using the string format
|
|
942
|
+
`<artifact-name>@<task-name>` to index dependencies. The
|
|
943
|
+
standard artifact is named `main`. See the example below.
|
|
944
|
+
|
|
945
|
+
If `session` is `True`, the new artifact will be a session
|
|
946
|
+
artifact that is only valid during a single Jolt invokation.
|
|
947
|
+
Session artifacts are published even if the task fails and may
|
|
948
|
+
be used to save logs and data for post-mortem analysis.
|
|
949
|
+
|
|
950
|
+
Args:
|
|
951
|
+
name (str): Name of artifact. Used as reference from
|
|
952
|
+
consuming tasks.
|
|
953
|
+
session (boolean, False): Session artifact.
|
|
954
|
+
|
|
955
|
+
Example:
|
|
956
|
+
|
|
957
|
+
.. literalinclude:: ../examples/artifacts/build.jolt
|
|
958
|
+
:language: python
|
|
959
|
+
:caption: examples/artifacts/build.jolt
|
|
960
|
+
|
|
667
961
|
"""
|
|
668
|
-
|
|
962
|
+
name = utils.canonical(name)
|
|
669
963
|
|
|
670
|
-
|
|
671
|
-
|
|
964
|
+
def decorate(cls):
|
|
965
|
+
_old_artifacts = cls._artifacts
|
|
966
|
+
|
|
967
|
+
@functools.wraps(cls._artifacts)
|
|
968
|
+
def _artifacts(self, cache, node):
|
|
969
|
+
artifacts = _old_artifacts(self, cache, node)
|
|
970
|
+
artifacts += [cache.get_artifact(node, name, session=session or isinstance(cls, Resource))]
|
|
971
|
+
return artifacts
|
|
972
|
+
|
|
973
|
+
cls._artifacts = _artifacts
|
|
974
|
+
return cls
|
|
975
|
+
|
|
976
|
+
return decorate
|
|
977
|
+
|
|
978
|
+
@staticmethod
|
|
979
|
+
def artifact_upload(uri, name="main", condition=None):
|
|
980
|
+
"""
|
|
981
|
+
Decorator to add uploading of an artifact to a server.
|
|
982
|
+
|
|
983
|
+
Upon successful completion of the task, the resulting
|
|
984
|
+
artifact uploaded to the specified URI. The URI should
|
|
985
|
+
be in the format `protocol://user:password@host/path`.
|
|
986
|
+
|
|
987
|
+
The following protocols are supported:
|
|
988
|
+
|
|
989
|
+
- `http://`
|
|
990
|
+
- `https://`
|
|
991
|
+
- `file://`
|
|
992
|
+
|
|
993
|
+
Local path are also supported in which case the artifact
|
|
994
|
+
is copied to the specified location.
|
|
995
|
+
|
|
996
|
+
If the path ends with a slash, the artifact is treated as
|
|
997
|
+
a directory and copied into the root of the directory.
|
|
998
|
+
|
|
999
|
+
If the path ends with a supported archive extension, the
|
|
1000
|
+
artifact is archived and optionally compressed before being
|
|
1001
|
+
copied. See :func:`jolt.Tools.archive` for supported archive
|
|
1002
|
+
and compression formats.
|
|
1003
|
+
|
|
1004
|
+
Usernames and passwords are optional. If omitted, the
|
|
1005
|
+
artifact is uploaded anonymously. Environment variables
|
|
1006
|
+
can be used to store sensitive information, such as
|
|
1007
|
+
passwords. Specify the environment variable name in the
|
|
1008
|
+
URI as `protocol://{environ[USER]}:{environ[PASS]}@host/path`.
|
|
1009
|
+
|
|
1010
|
+
The upload can be conditioned on the return value of a
|
|
1011
|
+
function. The function is passed the task instance as
|
|
1012
|
+
an argument and should return a boolean value. If the
|
|
1013
|
+
function returns ``False``, the upload is skipped. The value
|
|
1014
|
+
of the condition influences the hash of the task.
|
|
672
1015
|
|
|
673
1016
|
Args:
|
|
674
|
-
|
|
675
|
-
|
|
1017
|
+
condition (str): Condition function to evaluate before
|
|
1018
|
+
uploading the artifact. The function is passed the
|
|
1019
|
+
task instance as an argument and should return a
|
|
1020
|
+
boolean value. By default, the artifact is always
|
|
1021
|
+
uploaded.
|
|
1022
|
+
name (str): Name of the artifact to upload.
|
|
1023
|
+
uri (str): Destination URI for the artifact.
|
|
1024
|
+
|
|
1025
|
+
Example:
|
|
1026
|
+
|
|
1027
|
+
.. literalinclude:: ../examples/artifacts/upload.jolt
|
|
1028
|
+
:language: python
|
|
1029
|
+
:caption: examples/artifacts/upload.jolt
|
|
676
1030
|
"""
|
|
677
|
-
|
|
1031
|
+
|
|
1032
|
+
def decorate(cls):
|
|
1033
|
+
if name == "main":
|
|
1034
|
+
old_publish = cls.publish
|
|
1035
|
+
else:
|
|
1036
|
+
old_publish = getattr(cls, "publish_" + name, None)
|
|
1037
|
+
|
|
1038
|
+
raise_error_if(old_publish is None, f"Cannot upload artifact '{name}' from task '{cls.name or cls.__name__.lower()}', it does not exist")
|
|
1039
|
+
|
|
1040
|
+
def upload_file(self, tools, cwd, src, dst):
|
|
1041
|
+
with tools.cwd(cwd):
|
|
1042
|
+
tools.upload(src, dst + src)
|
|
1043
|
+
|
|
1044
|
+
def copy_file(self, tools, cwd, src, dst):
|
|
1045
|
+
with tools.cwd(cwd):
|
|
1046
|
+
tools.copy(src, dst + src)
|
|
1047
|
+
|
|
1048
|
+
def list_files(self, artifact, tools):
|
|
1049
|
+
with tools.cwd(artifact.path):
|
|
1050
|
+
for path in tools.glob("**"):
|
|
1051
|
+
# Skip directories
|
|
1052
|
+
if tools.isdir(path):
|
|
1053
|
+
continue
|
|
1054
|
+
yield artifact.path, path
|
|
1055
|
+
|
|
1056
|
+
def archive_files(self, artifact, tools, filename):
|
|
1057
|
+
out = tools.builddir(artifact.identity)
|
|
1058
|
+
with tools.cwd(out):
|
|
1059
|
+
tools.archive(artifact.path, filename)
|
|
1060
|
+
yield out, filename
|
|
1061
|
+
|
|
1062
|
+
@functools.wraps(cls.publish)
|
|
1063
|
+
def publish(self, artifact, tools):
|
|
1064
|
+
old_publish(self, artifact, tools)
|
|
1065
|
+
|
|
1066
|
+
if condition and not bool(condition(self)):
|
|
1067
|
+
return
|
|
1068
|
+
|
|
1069
|
+
# Expand keywords
|
|
1070
|
+
uri_exp = tools.expand(uri)
|
|
1071
|
+
|
|
1072
|
+
# Parse URI to determine protocol
|
|
1073
|
+
uri_parsed = urlparse(uri_exp)
|
|
1074
|
+
if uri_parsed.scheme in ["http", "https"]:
|
|
1075
|
+
action = upload_file
|
|
1076
|
+
elif uri_parsed.scheme in ["", "file"]:
|
|
1077
|
+
uri_exp = uri_parsed.path
|
|
1078
|
+
action = copy_file
|
|
1079
|
+
else:
|
|
1080
|
+
raise_task_error(self, f"Unsupported protocol '{uri_parsed.scheme}' in URI '{uri_exp}'")
|
|
1081
|
+
|
|
1082
|
+
if uri_exp.endswith("/"):
|
|
1083
|
+
generator = list_files
|
|
1084
|
+
else:
|
|
1085
|
+
generator = functools.partial(archive_files, filename=fs.path.basename(uri_parsed.path))
|
|
1086
|
+
uri_exp = fs.path.dirname(uri_exp) + "/"
|
|
1087
|
+
|
|
1088
|
+
if action is copy_file:
|
|
1089
|
+
uri_exp = tools.expand_path(uri_exp) + "/"
|
|
1090
|
+
|
|
1091
|
+
for cwd, file in generator(self, artifact, tools):
|
|
1092
|
+
action(self, tools, cwd, file, uri_exp)
|
|
1093
|
+
|
|
1094
|
+
if name == "main":
|
|
1095
|
+
setattr(cls, "publish", publish)
|
|
1096
|
+
else:
|
|
1097
|
+
setattr(cls, "publish_" + name, publish)
|
|
1098
|
+
|
|
1099
|
+
if condition:
|
|
1100
|
+
old_init = cls.__init__
|
|
1101
|
+
|
|
1102
|
+
@functools.wraps(cls.__init__)
|
|
1103
|
+
def new_init(self, *args, **kwargs):
|
|
1104
|
+
old_init(self, *args, **kwargs)
|
|
1105
|
+
|
|
1106
|
+
def make_bool(fn, *args, **kwargs):
|
|
1107
|
+
return bool(fn(*args, **kwargs))
|
|
1108
|
+
|
|
1109
|
+
self.influence.append(CallbackInfluence(f"Upload {name}", make_bool, condition, self))
|
|
1110
|
+
|
|
1111
|
+
cls.__init__ = new_init
|
|
1112
|
+
|
|
1113
|
+
return cls
|
|
1114
|
+
|
|
1115
|
+
return decorate
|
|
678
1116
|
|
|
679
1117
|
@staticmethod
|
|
680
|
-
def attribute(alias, target, influence=True):
|
|
1118
|
+
def attribute(alias, target, influence=True, default=False):
|
|
681
1119
|
"""
|
|
682
1120
|
Decorates a task with an alias for another attribute.
|
|
683
1121
|
|
|
@@ -687,12 +1125,21 @@ class attributes:
|
|
|
687
1125
|
Keywords are expanded.
|
|
688
1126
|
influence (boolean): Add value of target
|
|
689
1127
|
attribute as influence of the task.
|
|
1128
|
+
default (boolean): Return alias attribute if
|
|
1129
|
+
target attribute does not exist. Value is
|
|
1130
|
+
accessed through the alias attribute name
|
|
1131
|
+
with a leading underscore, e.g. '_alias'.
|
|
690
1132
|
|
|
691
1133
|
"""
|
|
692
1134
|
def _decorate(cls):
|
|
693
1135
|
def _get(self):
|
|
1136
|
+
if default:
|
|
1137
|
+
return getattr(self, self.expand(target), getattr(self, self.expand(alias)))
|
|
694
1138
|
return getattr(self, self.expand(target))
|
|
695
|
-
|
|
1139
|
+
if default:
|
|
1140
|
+
setattr(cls, "_" + alias, property(_get))
|
|
1141
|
+
else:
|
|
1142
|
+
setattr(cls, alias, property(_get))
|
|
696
1143
|
if influence:
|
|
697
1144
|
attribute_influence(target)(cls)
|
|
698
1145
|
return cls
|
|
@@ -722,6 +1169,127 @@ class attributes:
|
|
|
722
1169
|
return cls
|
|
723
1170
|
return _decorate
|
|
724
1171
|
|
|
1172
|
+
@staticmethod
|
|
1173
|
+
def publish_files(attrib):
|
|
1174
|
+
"""
|
|
1175
|
+
Decorator adding a list attribute where file publication can be specified.
|
|
1176
|
+
|
|
1177
|
+
Each item in the list is a set of arguments passed directly to
|
|
1178
|
+
:func:`jolt.Artifact.collect`. Tuples, dictionaries and strings are
|
|
1179
|
+
accepted.
|
|
1180
|
+
|
|
1181
|
+
Example:
|
|
1182
|
+
|
|
1183
|
+
.. code-block:: python
|
|
1184
|
+
|
|
1185
|
+
@jolt.attributes.publish_files("collect")
|
|
1186
|
+
class Example(Task):
|
|
1187
|
+
collect = [
|
|
1188
|
+
# Publish file.txt into artifact root
|
|
1189
|
+
"file.txt",
|
|
1190
|
+
|
|
1191
|
+
# Publish file.txt into artifact dir/ directory
|
|
1192
|
+
("file.txt", "dir/"),
|
|
1193
|
+
|
|
1194
|
+
# Publish files from dir/ into artifact root
|
|
1195
|
+
{"files": "*", "cwd": "dir"},
|
|
1196
|
+
]
|
|
1197
|
+
|
|
1198
|
+
"""
|
|
1199
|
+
|
|
1200
|
+
def decorate(cls):
|
|
1201
|
+
if not hasattr(cls, "__publish_files"):
|
|
1202
|
+
old_pub = cls.publish
|
|
1203
|
+
|
|
1204
|
+
def publish(self, artifact, tools):
|
|
1205
|
+
old_pub(self, artifact, tools)
|
|
1206
|
+
for args in getattr(self, "__publish_files")():
|
|
1207
|
+
if type(args) is tuple:
|
|
1208
|
+
artifact.collect(*args)
|
|
1209
|
+
elif type(args) is dict:
|
|
1210
|
+
artifact.collect(**args)
|
|
1211
|
+
else:
|
|
1212
|
+
artifact.collect(args)
|
|
1213
|
+
|
|
1214
|
+
cls.publish = publish
|
|
1215
|
+
|
|
1216
|
+
return utils.concat_attributes("_publish_files", attrib)(cls)
|
|
1217
|
+
return decorate
|
|
1218
|
+
|
|
1219
|
+
@staticmethod
|
|
1220
|
+
def common_metadata(aclocal=True, cmake=True, cxxinfo=True, path=True, pkgconfig=True):
|
|
1221
|
+
"""
|
|
1222
|
+
Decorator adding common metadata to published artifacts.
|
|
1223
|
+
|
|
1224
|
+
The decorator adds common environment variables and C/C++ build information
|
|
1225
|
+
to the published artifact:
|
|
1226
|
+
|
|
1227
|
+
- Adds `bin/` to `PATH` environment variable if it exists.
|
|
1228
|
+
- Adds `lib/` and `lib64/` to C++ library paths if they exist.
|
|
1229
|
+
- Adds `include/` to C++ include paths if it exists.
|
|
1230
|
+
- Adds `lib/pkgconfig/`, `lib64/pkgconfig/` and `share/pkgconfig/` to
|
|
1231
|
+
`PKG_CONFIG_PATH` environment variable if `.pc` files are found.
|
|
1232
|
+
The `prefix` variable in `.pc` files is relocated to allow
|
|
1233
|
+
installation in arbitrary locations.
|
|
1234
|
+
- Adds `lib/cmake/`, `lib64/cmake/` and `share/cmake/` to
|
|
1235
|
+
`CMAKE_PREFIX_PATH` environment variable if they exist.
|
|
1236
|
+
- Adds `share/aclocal/` to `ACLOCAL_PATH` environment variable if it exists.
|
|
1237
|
+
|
|
1238
|
+
"""
|
|
1239
|
+
def decorate(cls):
|
|
1240
|
+
_old_publish = cls.publish
|
|
1241
|
+
|
|
1242
|
+
@functools.wraps(cls.publish)
|
|
1243
|
+
def publish(self, artifact, tools):
|
|
1244
|
+
_old_publish(self, artifact, tools)
|
|
1245
|
+
|
|
1246
|
+
with tools.cwd(artifact.path):
|
|
1247
|
+
if path and tools.exists("bin"):
|
|
1248
|
+
artifact.environ.PATH.append("bin")
|
|
1249
|
+
|
|
1250
|
+
pcdirs = set()
|
|
1251
|
+
|
|
1252
|
+
for pcpath in tools.glob("lib/pkgconfig/*.pc") + tools.glob("lib64/pkgconfig/*.pc") + tools.glob("share/pkgconfig/*.pc"):
|
|
1253
|
+
pcdirs.add(fs.path.dirname(pcpath))
|
|
1254
|
+
tools.replace_in_file(pcpath, artifact.strings.install_prefix, "${{pcfiledir}}/../..")
|
|
1255
|
+
for pcpath in tools.glob("lib/*/pkgconfig/*.pc") + tools.glob("lib64/*/pkgconfig/*.pc"):
|
|
1256
|
+
pcdirs.add(fs.path.dirname(pcpath))
|
|
1257
|
+
tools.replace_in_file(pcpath, artifact.strings.install_prefix, "${{pcfiledir}}/../../..")
|
|
1258
|
+
|
|
1259
|
+
for pcdir in pcdirs:
|
|
1260
|
+
artifact.environ.PKG_CONFIG_PATH.append(pcdir)
|
|
1261
|
+
|
|
1262
|
+
if cmake and tools.exists("lib/cmake"):
|
|
1263
|
+
artifact.environ.CMAKE_PREFIX_PATH.append(".")
|
|
1264
|
+
if cmake and tools.exists("lib64/cmake"):
|
|
1265
|
+
artifact.environ.CMAKE_PREFIX_PATH.append(".")
|
|
1266
|
+
if cmake and tools.exists("share/cmake"):
|
|
1267
|
+
artifact.environ.CMAKE_PREFIX_PATH.append(".")
|
|
1268
|
+
|
|
1269
|
+
if aclocal and tools.exists("share/aclocal"):
|
|
1270
|
+
artifact.environ.ACLOCAL_PATH.append("share/aclocal")
|
|
1271
|
+
|
|
1272
|
+
cls.publish = publish
|
|
1273
|
+
|
|
1274
|
+
return cls
|
|
1275
|
+
|
|
1276
|
+
return decorate
|
|
1277
|
+
|
|
1278
|
+
@staticmethod
|
|
1279
|
+
def requires(attrib):
|
|
1280
|
+
"""
|
|
1281
|
+
Decorates a task with an alternative ``requires`` attribute.
|
|
1282
|
+
|
|
1283
|
+
The new attribute will be concatenated with the regular
|
|
1284
|
+
``requires`` attribute.
|
|
1285
|
+
|
|
1286
|
+
Args:
|
|
1287
|
+
attrib (str): Name of alternative attribute.
|
|
1288
|
+
Keywords are expanded.
|
|
1289
|
+
"""
|
|
1290
|
+
return utils.concat_attributes("requires", attrib)
|
|
1291
|
+
|
|
1292
|
+
@staticmethod
|
|
725
1293
|
def load(filepath):
|
|
726
1294
|
"""
|
|
727
1295
|
Decorator which loads task class attributes from a file.
|
|
@@ -762,6 +1330,9 @@ class attributes:
|
|
|
762
1330
|
old_init(self, *args, **kwargs)
|
|
763
1331
|
for key, val in eval(self.tools.read_file(filepath)).items():
|
|
764
1332
|
setattr(self, key, val)
|
|
1333
|
+
self.requires = utils.unique_list(
|
|
1334
|
+
utils.call_or_return_list(self, self.__class__._requires))
|
|
1335
|
+
self.requires = self.expand(self.requires)
|
|
765
1336
|
|
|
766
1337
|
cls.__init__ = new_init
|
|
767
1338
|
return cls
|
|
@@ -789,6 +1360,20 @@ class attributes:
|
|
|
789
1360
|
return cls
|
|
790
1361
|
return _decorate
|
|
791
1362
|
|
|
1363
|
+
@staticmethod
|
|
1364
|
+
def platform(attrib):
|
|
1365
|
+
"""
|
|
1366
|
+
Decorates a task with an alternative ``platform`` attribute.
|
|
1367
|
+
|
|
1368
|
+
The new attribute will be concatenated with the regular
|
|
1369
|
+
``platform`` attribute.
|
|
1370
|
+
|
|
1371
|
+
Args:
|
|
1372
|
+
attrib (str): Name of alternative attribute.
|
|
1373
|
+
Keywords are expanded.
|
|
1374
|
+
"""
|
|
1375
|
+
return utils.concat_attributes("platform", attrib)
|
|
1376
|
+
|
|
792
1377
|
@staticmethod
|
|
793
1378
|
def system(cls):
|
|
794
1379
|
"""
|
|
@@ -800,25 +1385,59 @@ class attributes:
|
|
|
800
1385
|
cls.system = property(lambda t: t._system.value)
|
|
801
1386
|
return cls
|
|
802
1387
|
|
|
1388
|
+
@staticmethod
|
|
1389
|
+
def timeout(seconds):
|
|
1390
|
+
"""
|
|
1391
|
+
Decorator setting a timeout for a task.
|
|
803
1392
|
|
|
804
|
-
|
|
805
|
-
|
|
1393
|
+
The timeout applies to the task's run method. A JoltTimeoutError
|
|
1394
|
+
is raised if the task does not complete within the specified time.
|
|
806
1395
|
|
|
807
|
-
|
|
808
|
-
|
|
1396
|
+
Args:
|
|
1397
|
+
seconds (int): Timeout in seconds.
|
|
809
1398
|
|
|
810
|
-
|
|
811
|
-
"""An expiration strategy, defining when the artifact may be evicted from the cache.
|
|
1399
|
+
Example:
|
|
812
1400
|
|
|
813
|
-
|
|
814
|
-
an attempt will be made to evict artifacts from the cache. The eviction
|
|
815
|
-
algorithm processes artifacts in least recently used (LRU) order until
|
|
816
|
-
an expired artifact is found.
|
|
1401
|
+
.. code-block:: python
|
|
817
1402
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1403
|
+
@attributes.timeout(5)
|
|
1404
|
+
class Example(Task):
|
|
1405
|
+
def run(self, deps, tools):
|
|
1406
|
+
time.sleep(10)
|
|
1407
|
+
|
|
1408
|
+
"""
|
|
1409
|
+
def decorate(cls):
|
|
1410
|
+
_old_run = cls.run
|
|
1411
|
+
|
|
1412
|
+
@functools.wraps(cls.run)
|
|
1413
|
+
def run(self, deps, tools):
|
|
1414
|
+
with tools.timeout(seconds):
|
|
1415
|
+
_old_run(self, deps, tools)
|
|
1416
|
+
|
|
1417
|
+
cls.run = run
|
|
1418
|
+
return cls
|
|
1419
|
+
|
|
1420
|
+
return decorate
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
class TaskBase(object):
|
|
1424
|
+
""" Task base class. """
|
|
1425
|
+
|
|
1426
|
+
cacheable = True
|
|
1427
|
+
""" Whether the task produces an artifact or not. """
|
|
1428
|
+
|
|
1429
|
+
expires = Immediately()
|
|
1430
|
+
"""An expiration strategy, defining when the artifact may be evicted from the cache.
|
|
1431
|
+
|
|
1432
|
+
When the size of the artifact cache exceeds the configured limit
|
|
1433
|
+
an attempt will be made to evict artifacts from the cache. The eviction
|
|
1434
|
+
algorithm processes artifacts in least recently used (LRU) order until
|
|
1435
|
+
an expired artifact is found.
|
|
1436
|
+
|
|
1437
|
+
By default, an artifact expires immediately and may be evicted at any time
|
|
1438
|
+
(in LRU order). An exception to this rule is if the artifact is required by
|
|
1439
|
+
a task in the active task set. For example, if a task A requires the output
|
|
1440
|
+
of task B, B will never be evicted by A while A is being executed.
|
|
822
1441
|
|
|
823
1442
|
There are several expiration strategies to choose from:
|
|
824
1443
|
|
|
@@ -883,9 +1502,37 @@ class TaskBase(object):
|
|
|
883
1502
|
joltproject = None
|
|
884
1503
|
""" Name of project this task belongs to. """
|
|
885
1504
|
|
|
1505
|
+
local = False
|
|
1506
|
+
""" A local task is only executed on the local node where the build is initiated. """
|
|
1507
|
+
|
|
886
1508
|
name = None
|
|
887
1509
|
""" Name of the task. Derived from class name if not set. """
|
|
888
1510
|
|
|
1511
|
+
platform = {}
|
|
1512
|
+
"""
|
|
1513
|
+
Dictionary of task platform requirements.
|
|
1514
|
+
|
|
1515
|
+
Platform requirements control where tasks are allowed to execute.
|
|
1516
|
+
Multiple requirement key/values may be specified in which case all
|
|
1517
|
+
must be fulfilled in order for the task to be schedulable on a node.
|
|
1518
|
+
|
|
1519
|
+
The following builtin requirement labels exist:
|
|
1520
|
+
|
|
1521
|
+
- node.arch ["arm", "amd64"]
|
|
1522
|
+
- node.os ["linux", "windows"]
|
|
1523
|
+
|
|
1524
|
+
Example:
|
|
1525
|
+
|
|
1526
|
+
.. code-block:: python
|
|
1527
|
+
|
|
1528
|
+
class Hello(Task):
|
|
1529
|
+
# This task must run on Linux.
|
|
1530
|
+
platform = {
|
|
1531
|
+
"node.os": "linux",
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
"""
|
|
1535
|
+
|
|
889
1536
|
requires = []
|
|
890
1537
|
""" List of dependencies to other tasks. """
|
|
891
1538
|
|
|
@@ -932,7 +1579,15 @@ class TaskBase(object):
|
|
|
932
1579
|
execution requests, the instance ID won't be.
|
|
933
1580
|
"""
|
|
934
1581
|
|
|
935
|
-
|
|
1582
|
+
@property
|
|
1583
|
+
def instance(self):
|
|
1584
|
+
return str(self._instance.value)
|
|
1585
|
+
|
|
1586
|
+
@instance.setter
|
|
1587
|
+
def instance(self, value):
|
|
1588
|
+
self._instance.assign(value)
|
|
1589
|
+
|
|
1590
|
+
def __init__(self, parameters=None, manifest=None, buildenv=None, **kwargs):
|
|
936
1591
|
self._identity = None
|
|
937
1592
|
self._report = _JoltTask()
|
|
938
1593
|
self.name = self.__class__.name
|
|
@@ -953,6 +1608,7 @@ class TaskBase(object):
|
|
|
953
1608
|
self.selfsustained = utils.call_or_return(self, self.__class__._selfsustained)
|
|
954
1609
|
self.tools = Tools(self, self.joltdir)
|
|
955
1610
|
self._apply_manifest(manifest)
|
|
1611
|
+
self._apply_protobuf(buildenv)
|
|
956
1612
|
self.requires = self.expand(self.requires)
|
|
957
1613
|
|
|
958
1614
|
def _apply_manifest(self, manifest):
|
|
@@ -969,6 +1625,26 @@ class TaskBase(object):
|
|
|
969
1625
|
.format(attrib.name, self.qualified_name)
|
|
970
1626
|
export.assign(attrib.value)
|
|
971
1627
|
|
|
1628
|
+
def _apply_protobuf(self, buildenv):
|
|
1629
|
+
if buildenv is None:
|
|
1630
|
+
return
|
|
1631
|
+
task = buildenv.tasks.get(self.exported_name)
|
|
1632
|
+
if not task:
|
|
1633
|
+
return
|
|
1634
|
+
if task.identity:
|
|
1635
|
+
self.identity = task.identity
|
|
1636
|
+
if task.taint:
|
|
1637
|
+
self.taint = task.taint
|
|
1638
|
+
for prop in task.properties:
|
|
1639
|
+
export = utils.getattr_safe(self, prop.key)
|
|
1640
|
+
assert isinstance(export, Export), \
|
|
1641
|
+
"'{0}' is not an exportable attribute of task '{1}'"\
|
|
1642
|
+
.format(prop.key, self.qualified_name)
|
|
1643
|
+
export.assign(prop.value)
|
|
1644
|
+
|
|
1645
|
+
def _artifacts(self, cache, node):
|
|
1646
|
+
return [cache.get_artifact(node, "main")]
|
|
1647
|
+
|
|
972
1648
|
def _influence(self):
|
|
973
1649
|
return utils.as_list(self.__class__.influence)
|
|
974
1650
|
|
|
@@ -1003,11 +1679,8 @@ class TaskBase(object):
|
|
|
1003
1679
|
if isinstance(param, Parameter):
|
|
1004
1680
|
try:
|
|
1005
1681
|
param.set_value(value)
|
|
1006
|
-
except
|
|
1007
|
-
raise_task_error(
|
|
1008
|
-
self,
|
|
1009
|
-
"Illegal value '{0}' assigned to parameter '{1}'",
|
|
1010
|
-
str(e), key)
|
|
1682
|
+
except (ParameterValueError, ParameterImmutableError) as e:
|
|
1683
|
+
raise_task_error(self, str(e))
|
|
1011
1684
|
continue
|
|
1012
1685
|
raise_task_error(self, "No such parameter '{0}'", key)
|
|
1013
1686
|
self._assert_required_parameters_assigned()
|
|
@@ -1048,7 +1721,7 @@ class TaskBase(object):
|
|
|
1048
1721
|
return _filter
|
|
1049
1722
|
|
|
1050
1723
|
for _, dep in deps.items():
|
|
1051
|
-
deptask = dep.
|
|
1724
|
+
deptask = dep.task
|
|
1052
1725
|
if isinstance(deptask, FileInfluence):
|
|
1053
1726
|
# Resource dependencies may cover the influence implicitly
|
|
1054
1727
|
deppath = self.tools.expand_path(str(deptask.path))
|
|
@@ -1071,201 +1744,976 @@ class TaskBase(object):
|
|
|
1071
1744
|
log.warning("Missing influence: {} ({})", source, self.name)
|
|
1072
1745
|
raise_task_error_if(sources, self, "Task is missing source influence")
|
|
1073
1746
|
|
|
1074
|
-
def _get_export_objects(self):
|
|
1075
|
-
return self._exports
|
|
1747
|
+
def _get_export_objects(self):
|
|
1748
|
+
return self._exports
|
|
1749
|
+
|
|
1750
|
+
def _get_parameter_objects(self, unset=False):
|
|
1751
|
+
return self._parameters
|
|
1752
|
+
|
|
1753
|
+
def _get_parameters(self, unset=False):
|
|
1754
|
+
return {
|
|
1755
|
+
key: param.get_value()
|
|
1756
|
+
for key, param in self._get_parameter_objects().items()
|
|
1757
|
+
if unset or not param.is_unset()
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
def _get_explicitly_set_parameters(self):
|
|
1761
|
+
return {
|
|
1762
|
+
key: param.get_value()
|
|
1763
|
+
for key, param in self._get_parameter_objects().items()
|
|
1764
|
+
if param.is_set()
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
def __str__(self):
|
|
1768
|
+
return str(self.name)
|
|
1769
|
+
|
|
1770
|
+
@property
|
|
1771
|
+
def canonical_name(self):
|
|
1772
|
+
return utils.canonical(self.name)
|
|
1773
|
+
|
|
1774
|
+
@property
|
|
1775
|
+
def qualified_name(self):
|
|
1776
|
+
return utils.format_task_name(
|
|
1777
|
+
self.name,
|
|
1778
|
+
self._get_parameters())
|
|
1779
|
+
|
|
1780
|
+
@property
|
|
1781
|
+
def short_qualified_name(self):
|
|
1782
|
+
return utils.format_task_name(
|
|
1783
|
+
self.name,
|
|
1784
|
+
self._get_explicitly_set_parameters())
|
|
1785
|
+
|
|
1786
|
+
@property
|
|
1787
|
+
def exported_name(self):
|
|
1788
|
+
if hasattr(self, "_exported_name"):
|
|
1789
|
+
return self._exported_name
|
|
1790
|
+
return self.short_qualified_name
|
|
1791
|
+
|
|
1792
|
+
@exported_name.setter
|
|
1793
|
+
def exported_name(self, name):
|
|
1794
|
+
self._exported_name = name
|
|
1795
|
+
|
|
1796
|
+
def expand(self, string_or_list, *args, **kwargs):
|
|
1797
|
+
""" Expands keyword arguments/macros in a format string.
|
|
1798
|
+
|
|
1799
|
+
See :func:`jolt.Tools.expand` for details.
|
|
1800
|
+
"""
|
|
1801
|
+
|
|
1802
|
+
try:
|
|
1803
|
+
kwargs["_instance"] = self
|
|
1804
|
+
if type(string_or_list) is list:
|
|
1805
|
+
return [utils.expand(string, *args, **kwargs) for string in string_or_list]
|
|
1806
|
+
return utils.expand(string_or_list, *args, **kwargs)
|
|
1807
|
+
except KeyError as e:
|
|
1808
|
+
raise_task_error(self, "Invalid macro '{0}' encountered - forgot to set a parameter?", e)
|
|
1809
|
+
|
|
1810
|
+
@property
|
|
1811
|
+
def identity(self):
|
|
1812
|
+
return self._identity
|
|
1813
|
+
|
|
1814
|
+
@identity.setter
|
|
1815
|
+
def identity(self, identity):
|
|
1816
|
+
self._identity = identity
|
|
1817
|
+
|
|
1818
|
+
def is_cacheable(self):
|
|
1819
|
+
return self.cacheable
|
|
1820
|
+
|
|
1821
|
+
def is_runnable(self):
|
|
1822
|
+
return True
|
|
1823
|
+
|
|
1824
|
+
|
|
1825
|
+
class Task(TaskBase):
|
|
1826
|
+
#: Path to the .jolt file where the task was defined.
|
|
1827
|
+
|
|
1828
|
+
abstract = True
|
|
1829
|
+
""" An abstract task class indended to be subclassed.
|
|
1830
|
+
|
|
1831
|
+
Abstract tasks can't be executed and won't be listed.
|
|
1832
|
+
"""
|
|
1833
|
+
|
|
1834
|
+
unstable = False
|
|
1835
|
+
"""
|
|
1836
|
+
An unstable task is allowed to fail without stopping or failing the entire build.
|
|
1837
|
+
|
|
1838
|
+
The unstable task is still reported as a failure at the end of the build.
|
|
1839
|
+
"""
|
|
1840
|
+
|
|
1841
|
+
def __init__(self, parameters=None, **kwargs):
|
|
1842
|
+
super().__init__(parameters, **kwargs)
|
|
1843
|
+
|
|
1844
|
+
def info(self, fmt, *args, **kwargs):
|
|
1845
|
+
"""
|
|
1846
|
+
Log information about the task.
|
|
1847
|
+
"""
|
|
1848
|
+
fmt = self.tools.expand(fmt, *args, **kwargs)
|
|
1849
|
+
log.info(fmt, *args, **kwargs)
|
|
1850
|
+
|
|
1851
|
+
def warning(self, fmt, *args, **kwargs):
|
|
1852
|
+
""" Log a warning concerning the task """
|
|
1853
|
+
fmt = self.tools.expand(fmt, *args, **kwargs)
|
|
1854
|
+
log.warning(fmt, *args, **kwargs)
|
|
1855
|
+
|
|
1856
|
+
def error(self, fmt, *args, **kwargs):
|
|
1857
|
+
""" Log an error concerning the task """
|
|
1858
|
+
fmt = self.tools.expand(fmt, *args, **kwargs)
|
|
1859
|
+
log.error(fmt, *args, **kwargs)
|
|
1860
|
+
|
|
1861
|
+
def verbose(self, fmt, *args, **kwargs):
|
|
1862
|
+
"""
|
|
1863
|
+
Log verbose information about the task.
|
|
1864
|
+
"""
|
|
1865
|
+
fmt = self.tools.expand(fmt, *args, **kwargs)
|
|
1866
|
+
log.verbose(fmt, *args, **kwargs)
|
|
1867
|
+
|
|
1868
|
+
def clean(self, tools):
|
|
1869
|
+
"""
|
|
1870
|
+
Cleans up resources and intermediate files created by the task.
|
|
1871
|
+
|
|
1872
|
+
The method is invoked in response to the user running clean
|
|
1873
|
+
on the command line. It should restore the environment to its
|
|
1874
|
+
original state. The next execution of the task should behave
|
|
1875
|
+
as if the task is executed for the first time.
|
|
1876
|
+
|
|
1877
|
+
An implementation must not clean any local or remote artifact cache.
|
|
1878
|
+
"""
|
|
1879
|
+
|
|
1880
|
+
def run(self, deps, tools):
|
|
1881
|
+
"""
|
|
1882
|
+
Performs the work of the task.
|
|
1883
|
+
|
|
1884
|
+
Dependencies specified with "requires" are passed as the
|
|
1885
|
+
deps dictionary. The tools argument provides a set of low
|
|
1886
|
+
level tool functions that may be useful.
|
|
1887
|
+
|
|
1888
|
+
.. code-block:: python
|
|
1889
|
+
|
|
1890
|
+
with tools.cwd("path/to/subdir"):
|
|
1891
|
+
tools.run("make {target}")
|
|
1892
|
+
|
|
1893
|
+
When using methods from the toolbox, task parameters, such
|
|
1894
|
+
as ``target`` above, are automatically expanded to their values.
|
|
1895
|
+
"""
|
|
1896
|
+
|
|
1897
|
+
def nopublish(self, artifact, tools):
|
|
1898
|
+
raise NotImplementedError()
|
|
1899
|
+
|
|
1900
|
+
def publish(self, artifact, tools):
|
|
1901
|
+
"""
|
|
1902
|
+
Publishes files produced by :func:`~run`.
|
|
1903
|
+
|
|
1904
|
+
Files can be collected in to the artifact by calling
|
|
1905
|
+
artifact.collect().
|
|
1906
|
+
|
|
1907
|
+
Additional metadata can be provided, such as environment
|
|
1908
|
+
variables that should be set whenever the task artifact is
|
|
1909
|
+
consumed. Example:
|
|
1910
|
+
|
|
1911
|
+
.. code-block:: python
|
|
1912
|
+
|
|
1913
|
+
# Append <artifact-path>/bin to the PATH
|
|
1914
|
+
artifact.environ.PATH.append("bin")
|
|
1915
|
+
|
|
1916
|
+
# Pass an arbitrary string to a consumer
|
|
1917
|
+
artifact.strings.foo = "bar"
|
|
1918
|
+
|
|
1919
|
+
"""
|
|
1920
|
+
|
|
1921
|
+
def unpack(self, artifact, tools):
|
|
1922
|
+
"""
|
|
1923
|
+
Unpacks files published by publish() .
|
|
1924
|
+
|
|
1925
|
+
The intention of this hook is to make necessary adjustments
|
|
1926
|
+
to artifact files and directories once they have been downloaded
|
|
1927
|
+
into the local cache on a different machine. For example,
|
|
1928
|
+
paths may have to be adjusted or an installer executed.
|
|
1929
|
+
|
|
1930
|
+
This hook is executed in the context of a consuming task.
|
|
1931
|
+
"""
|
|
1932
|
+
raise NotImplementedError()
|
|
1933
|
+
|
|
1934
|
+
def debugshell(self, deps, tools):
|
|
1935
|
+
"""
|
|
1936
|
+
Invoked to start a debug shell.
|
|
1937
|
+
|
|
1938
|
+
The method prepares the environment with attributes exported by task requirement
|
|
1939
|
+
artifacts. The shell is entered by passing the ``-g`` flag to the build command.
|
|
1940
|
+
|
|
1941
|
+
Task execution resumes normally when exiting the shell.
|
|
1942
|
+
"""
|
|
1943
|
+
with tools.environ(PS1="jolt$ ") as env:
|
|
1944
|
+
from jolt import config
|
|
1945
|
+
subprocess.call(config.get_shell().split(), env=env, cwd=tools._cwd)
|
|
1946
|
+
|
|
1947
|
+
@contextmanager
|
|
1948
|
+
def report(self):
|
|
1949
|
+
"""
|
|
1950
|
+
Provide error analysis for task.
|
|
1951
|
+
|
|
1952
|
+
Intentionally undocumented. Use at own risk.
|
|
1953
|
+
"""
|
|
1954
|
+
yield ReportProxy(self, self._report)
|
|
1955
|
+
|
|
1956
|
+
|
|
1957
|
+
class SubTask(object):
|
|
1958
|
+
def __init__(self, task):
|
|
1959
|
+
self._deps = []
|
|
1960
|
+
self._identity = []
|
|
1961
|
+
self._influence = []
|
|
1962
|
+
self._message = None
|
|
1963
|
+
self._outputs = []
|
|
1964
|
+
self._task = task
|
|
1965
|
+
self._tools = copy.copy(task.tools)
|
|
1966
|
+
|
|
1967
|
+
def __str__(self):
|
|
1968
|
+
if self.message:
|
|
1969
|
+
return self.message
|
|
1970
|
+
if self._outputs:
|
|
1971
|
+
return " ".join(self._outputs)
|
|
1972
|
+
return None
|
|
1973
|
+
|
|
1974
|
+
@property
|
|
1975
|
+
def dependencies(self):
|
|
1976
|
+
return self._deps
|
|
1977
|
+
|
|
1978
|
+
@functools.cached_property
|
|
1979
|
+
def identity(self):
|
|
1980
|
+
sha = hashlib.sha1()
|
|
1981
|
+
for ident in self._identity:
|
|
1982
|
+
sha.update(ident.encode())
|
|
1983
|
+
for output in self._outputs:
|
|
1984
|
+
sha.update(output.encode())
|
|
1985
|
+
if self.message:
|
|
1986
|
+
sha.update(self.message.encode())
|
|
1987
|
+
return sha.hexdigest()
|
|
1988
|
+
|
|
1989
|
+
@functools.cached_property
|
|
1990
|
+
def influence(self):
|
|
1991
|
+
sha = hashlib.sha1()
|
|
1992
|
+
for infl in self._influence:
|
|
1993
|
+
if callable(infl):
|
|
1994
|
+
sha.update(infl().encode())
|
|
1995
|
+
else:
|
|
1996
|
+
sha.update(infl.encode())
|
|
1997
|
+
for dep in self._deps:
|
|
1998
|
+
sha.update(dep.influence.encode())
|
|
1999
|
+
return sha.hexdigest()
|
|
2000
|
+
|
|
2001
|
+
@functools.cached_property
|
|
2002
|
+
def is_outdated(self):
|
|
2003
|
+
try:
|
|
2004
|
+
with self._tools.cwd(self._tools.builddir("subtasks", incremental=True)):
|
|
2005
|
+
if self.influence != self._tools.read_file(self.identity):
|
|
2006
|
+
return True
|
|
2007
|
+
for output in self.outputs:
|
|
2008
|
+
output = fs.as_canonpath(output)
|
|
2009
|
+
assert self._tools.read_file(output + ".identity") == self.identity
|
|
2010
|
+
|
|
2011
|
+
# FIXME: Check hash content of output file
|
|
2012
|
+
for output in self.outputs:
|
|
2013
|
+
if not fs.path.exists(fs.path.join(self._task.joltdir, output)):
|
|
2014
|
+
return True
|
|
2015
|
+
for dep in self.dependencies:
|
|
2016
|
+
if dep.is_outdated:
|
|
2017
|
+
return True
|
|
2018
|
+
return False
|
|
2019
|
+
except Exception:
|
|
2020
|
+
return True
|
|
2021
|
+
|
|
2022
|
+
def set_uptodate(self):
|
|
2023
|
+
try:
|
|
2024
|
+
del self.influence
|
|
2025
|
+
del self.is_outdated
|
|
2026
|
+
except AttributeError:
|
|
2027
|
+
pass
|
|
2028
|
+
with self._tools.cwd(self._tools.builddir("subtasks", incremental=True)):
|
|
2029
|
+
self._tools.write_file(self.identity, self.influence)
|
|
2030
|
+
|
|
2031
|
+
for output in self.outputs:
|
|
2032
|
+
output = fs.as_canonpath(output)
|
|
2033
|
+
self._tools.mkdirname(output)
|
|
2034
|
+
self._tools.write_file(output + ".identity", self.identity)
|
|
2035
|
+
|
|
2036
|
+
def run(self):
|
|
2037
|
+
pass
|
|
2038
|
+
|
|
2039
|
+
def add_dependency(self, dep):
|
|
2040
|
+
deps = utils.as_list(dep)
|
|
2041
|
+
subtasks = [self._task._add_input(dep) for dep in deps]
|
|
2042
|
+
self._deps.extend(subtasks)
|
|
2043
|
+
|
|
2044
|
+
def add_identity(self, infl):
|
|
2045
|
+
self._identity.append(infl)
|
|
2046
|
+
|
|
2047
|
+
def add_influence(self, infl):
|
|
2048
|
+
self._influence.append(infl)
|
|
2049
|
+
|
|
2050
|
+
def add_influence_file(self, path):
|
|
2051
|
+
path = self._tools.expand_path(path)
|
|
2052
|
+
self.add_influence(utils.filesha1(path))
|
|
2053
|
+
|
|
2054
|
+
def add_influence_depfile(self, path):
|
|
2055
|
+
def depfile():
|
|
2056
|
+
result = ""
|
|
2057
|
+
try:
|
|
2058
|
+
deps = self._tools.read_depfile(fs.path.join(self._task.joltdir, path))
|
|
2059
|
+
except OSError:
|
|
2060
|
+
return "N/A"
|
|
2061
|
+
with self._tools.cwd(self._task.joltdir):
|
|
2062
|
+
for output in self.outputs:
|
|
2063
|
+
for input in deps.get(output, []):
|
|
2064
|
+
input = self._tools.expand_path(input)
|
|
2065
|
+
result += utils.filesha1(input)
|
|
2066
|
+
return result
|
|
2067
|
+
self.add_influence(depfile)
|
|
2068
|
+
|
|
2069
|
+
def add_output(self, output):
|
|
2070
|
+
self.add_identity(output)
|
|
2071
|
+
if type(output) is list:
|
|
2072
|
+
self._outputs.extend(output)
|
|
2073
|
+
else:
|
|
2074
|
+
self._outputs.append(output)
|
|
2075
|
+
|
|
2076
|
+
@property
|
|
2077
|
+
def message(self):
|
|
2078
|
+
return self._message
|
|
2079
|
+
|
|
2080
|
+
def set_message(self, message):
|
|
2081
|
+
self._message = message
|
|
2082
|
+
|
|
2083
|
+
@property
|
|
2084
|
+
def outputs(self):
|
|
2085
|
+
return self._outputs
|
|
2086
|
+
|
|
2087
|
+
|
|
2088
|
+
class Input(SubTask):
|
|
2089
|
+
def __init__(self, task, input):
|
|
2090
|
+
super().__init__(task)
|
|
2091
|
+
self._outputs = [input]
|
|
2092
|
+
self.add_influence_file(input)
|
|
2093
|
+
self.set_uptodate()
|
|
2094
|
+
|
|
2095
|
+
@property
|
|
2096
|
+
def message(self):
|
|
2097
|
+
return f"[IN] {self._outputs[0]}"
|
|
2098
|
+
|
|
2099
|
+
def run(self):
|
|
2100
|
+
raise_task_error(self._task, "Input file '{}' does not exist", self._outputs[0])
|
|
2101
|
+
|
|
2102
|
+
|
|
2103
|
+
class CommandSubtask(SubTask):
|
|
2104
|
+
def __init__(self, task, command):
|
|
2105
|
+
super().__init__(task)
|
|
2106
|
+
self.add_identity(command)
|
|
2107
|
+
self._command = command
|
|
2108
|
+
|
|
2109
|
+
def __str__(self):
|
|
2110
|
+
s = super().__str__()
|
|
2111
|
+
return s if s is not None and not log.is_verbose() else self._tools.expand(self._command)
|
|
2112
|
+
|
|
2113
|
+
def run(self):
|
|
2114
|
+
self._tools.run(self._command)
|
|
2115
|
+
|
|
2116
|
+
|
|
2117
|
+
class FunctionSubtask(SubTask):
|
|
2118
|
+
def __init__(self, task, fn):
|
|
2119
|
+
super().__init__(task)
|
|
2120
|
+
self.add_identity(fn.__name__)
|
|
2121
|
+
self.fn = fn
|
|
2122
|
+
|
|
2123
|
+
def run(self):
|
|
2124
|
+
self.fn(self)
|
|
2125
|
+
|
|
2126
|
+
|
|
2127
|
+
class RenderSubtask(SubTask):
|
|
2128
|
+
def __init__(self, task, template, **kwargs):
|
|
2129
|
+
super().__init__(task)
|
|
2130
|
+
self._data = self._tools.render(template, **kwargs)
|
|
2131
|
+
self.add_identity(template)
|
|
2132
|
+
self.add_influence(self._data)
|
|
2133
|
+
|
|
2134
|
+
def run(self):
|
|
2135
|
+
for output in self.outputs:
|
|
2136
|
+
self._tools.write_file(output, self._data, expand=False)
|
|
2137
|
+
|
|
2138
|
+
|
|
2139
|
+
class FileRenderSubtask(SubTask):
|
|
2140
|
+
def __init__(self, task, path, **kwargs):
|
|
2141
|
+
super().__init__(task)
|
|
2142
|
+
self.add_identity(path)
|
|
2143
|
+
self._path = path
|
|
2144
|
+
|
|
2145
|
+
def run(self):
|
|
2146
|
+
data = self._tools.render_file(self._path)
|
|
2147
|
+
for output in self.outputs:
|
|
2148
|
+
self._tools.write_file(output, data, expand=False)
|
|
2149
|
+
|
|
2150
|
+
|
|
2151
|
+
class MultiTask(Task):
|
|
2152
|
+
"""
|
|
2153
|
+
A task with subtasks that are executed in parallel with intermediate caching.
|
|
2154
|
+
|
|
2155
|
+
A MultiTask is useful for tasks with many subtasks that benefit from intermediate
|
|
2156
|
+
caching, such as compilation tasks where multiple source files are compiled into
|
|
2157
|
+
object files and then either linked into an executable or archived into a library.
|
|
2158
|
+
|
|
2159
|
+
Subtasks are executed in parallel and their output is cached locally in a build
|
|
2160
|
+
directory. The output is not automatically shared with other Jolt clients, only
|
|
2161
|
+
the files published by the MultiTask is shared. A subtask is only re-executed
|
|
2162
|
+
if the influence one of its dependencies change.
|
|
2163
|
+
|
|
2164
|
+
Subtasks are defined in the MultiTask method generate() and they can be either
|
|
2165
|
+
a shell command or a python function. Helper methods in the class allow
|
|
2166
|
+
implementors to define outputs and inter-subtask dependencies.
|
|
2167
|
+
|
|
2168
|
+
Example:
|
|
2169
|
+
|
|
2170
|
+
.. code-block:: python
|
|
2171
|
+
|
|
2172
|
+
flags = ["-DDEBUG"]
|
|
2173
|
+
|
|
2174
|
+
def generate(self, deps, tools):
|
|
2175
|
+
sources = ["a.cpp", "b.cpp", "c.cpp"]
|
|
2176
|
+
objects = []
|
|
2177
|
+
|
|
2178
|
+
# Create compilation subtasks for each source file
|
|
2179
|
+
for source in sources:
|
|
2180
|
+
object = self.command(
|
|
2181
|
+
"g++ {flags} -c {inputs} -o {outputs} ",
|
|
2182
|
+
inputs=[source],
|
|
2183
|
+
outputs=[source +".o"])
|
|
2184
|
+
objects.append(object)
|
|
2185
|
+
|
|
2186
|
+
# Create linker subtask
|
|
2187
|
+
executable = self.command(
|
|
2188
|
+
"g++ {inputs} -o {output}",
|
|
2189
|
+
inputs=objects,
|
|
2190
|
+
outputs=["executable"])
|
|
2191
|
+
|
|
2192
|
+
"""
|
|
2193
|
+
|
|
2194
|
+
abstract = True
|
|
2195
|
+
|
|
2196
|
+
def __init__(self, *args, **kwargs):
|
|
2197
|
+
super().__init__(*args, **kwargs)
|
|
2198
|
+
self._subtasks = set()
|
|
2199
|
+
self._subtasks_by_output = {}
|
|
2200
|
+
|
|
2201
|
+
def _add_subtask(self, subtask):
|
|
2202
|
+
self._subtasks.add(subtask)
|
|
2203
|
+
|
|
2204
|
+
for output in subtask.outputs:
|
|
2205
|
+
if output in self._subtasks_by_output:
|
|
2206
|
+
othersubtask = self._subtasks_by_output
|
|
2207
|
+
raise_task_error(
|
|
2208
|
+
self,
|
|
2209
|
+
"'{}' is generated by both '{}' and '{}'",
|
|
2210
|
+
output, subtask.command, othersubtask.command)
|
|
2211
|
+
self._subtasks_by_output[output] = subtask
|
|
2212
|
+
|
|
2213
|
+
return subtask
|
|
2214
|
+
|
|
2215
|
+
def _find_subtask(self, output):
|
|
2216
|
+
return self._subtasks_by_output.get(output)
|
|
2217
|
+
|
|
2218
|
+
def _add_input(self, input):
|
|
2219
|
+
raise_task_error_if(not input, self, "Input is None")
|
|
2220
|
+
if not isinstance(input, SubTask):
|
|
2221
|
+
inputsubtask = self._find_subtask(input)
|
|
2222
|
+
if inputsubtask:
|
|
2223
|
+
return inputsubtask
|
|
2224
|
+
inputsubtask = Input(self, input)
|
|
2225
|
+
else:
|
|
2226
|
+
inputsubtask = input
|
|
2227
|
+
self._subtasks.add(inputsubtask)
|
|
2228
|
+
self._subtasks_by_output[input] = inputsubtask
|
|
2229
|
+
return inputsubtask
|
|
2230
|
+
|
|
2231
|
+
def _to_subtask_list(self, inputs):
|
|
2232
|
+
inputs = utils.as_list(inputs)
|
|
2233
|
+
return utils.unique_list([self._add_input(input) for input in inputs])
|
|
2234
|
+
|
|
2235
|
+
def _to_output_files(self, subtasks):
|
|
2236
|
+
subtasks = utils.as_list(subtasks)
|
|
2237
|
+
outputs = []
|
|
2238
|
+
for subtask in subtasks:
|
|
2239
|
+
if isinstance(subtask, SubTask):
|
|
2240
|
+
outputs.extend(subtask.outputs)
|
|
2241
|
+
else:
|
|
2242
|
+
outputs.append(subtask)
|
|
2243
|
+
return outputs
|
|
2244
|
+
|
|
2245
|
+
def _to_input_subtasks(self, inputs, **kwargs):
|
|
2246
|
+
inputs = utils.as_list(inputs)
|
|
2247
|
+
subtasks = []
|
|
2248
|
+
for input in inputs:
|
|
2249
|
+
input = self.expand(input, **kwargs)
|
|
2250
|
+
subtasks.append(self._add_input(input))
|
|
2251
|
+
return subtasks
|
|
2252
|
+
|
|
2253
|
+
def command(self, command, inputs=None, outputs=None, message=None, mkdir=True, **kwargs):
|
|
2254
|
+
"""
|
|
2255
|
+
Create shell command subtask.
|
|
2256
|
+
|
|
2257
|
+
The subtask executes the specified command. String format specifiers may be used and
|
|
2258
|
+
are resolved primarily by kwargs and secondarily by task attributes.
|
|
1076
2259
|
|
|
1077
|
-
|
|
1078
|
-
|
|
2260
|
+
Args:
|
|
2261
|
+
inputs (str, list): files or subtasks that the command depends on.
|
|
2262
|
+
outputs (str, list): list of files that the subtasks produces.
|
|
2263
|
+
message (str): custom message that the subtask will print when executed.
|
|
2264
|
+
mkdir (boolean): automatically create directories for outputs. If False,
|
|
2265
|
+
the caller must ensure that the directories exist before the the subtask
|
|
2266
|
+
is executed.
|
|
2267
|
+
kwargs: additional keyword values used to format the command line string.
|
|
1079
2268
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
key: param.get_value()
|
|
1083
|
-
for key, param in self._get_parameter_objects().items()
|
|
1084
|
-
if unset or not param.is_unset()
|
|
1085
|
-
}
|
|
2269
|
+
Returns:
|
|
2270
|
+
Subtask object.
|
|
1086
2271
|
|
|
1087
|
-
|
|
1088
|
-
return {
|
|
1089
|
-
key: param.get_value()
|
|
1090
|
-
for key, param in self._get_parameter_objects().items()
|
|
1091
|
-
if param.is_set()
|
|
1092
|
-
}
|
|
2272
|
+
Example:
|
|
1093
2273
|
|
|
1094
|
-
|
|
1095
|
-
return str(self.name)
|
|
2274
|
+
.. code-block:: python
|
|
1096
2275
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
2276
|
+
executable = self.command(
|
|
2277
|
+
"g++ {inputs} -o {output}",
|
|
2278
|
+
inputs=["main.cpp"],
|
|
2279
|
+
outputs=["executable"])
|
|
1100
2280
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
self.name,
|
|
1105
|
-
self._get_parameters())
|
|
2281
|
+
"""
|
|
2282
|
+
input_jobs = self._to_input_subtasks(inputs, **kwargs)
|
|
2283
|
+
inputfiles = self._to_output_files(inputs)
|
|
1106
2284
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
return utils.format_task_name(
|
|
1110
|
-
self.name,
|
|
1111
|
-
self._get_explicitly_set_parameters())
|
|
2285
|
+
outputs = utils.as_list(outputs)
|
|
2286
|
+
outputs = [self.tools.expand_relpath(output, self.joltdir, **kwargs) for output in outputs]
|
|
1112
2287
|
|
|
1113
|
-
|
|
1114
|
-
|
|
2288
|
+
dirs = set()
|
|
2289
|
+
if mkdir:
|
|
2290
|
+
for output in outputs:
|
|
2291
|
+
dirs.add(self.mkdirname(output, inputs=inputfiles, outputs=outputs, **kwargs))
|
|
1115
2292
|
|
|
1116
|
-
|
|
1117
|
-
|
|
2293
|
+
command = self.expand(command, inputs=inputfiles, outputs=outputs, **kwargs)
|
|
2294
|
+
subtask = CommandSubtask(self, command)
|
|
1118
2295
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
2296
|
+
for dir in dirs:
|
|
2297
|
+
subtask.add_dependency(dir)
|
|
2298
|
+
for input in input_jobs:
|
|
2299
|
+
subtask.add_dependency(input)
|
|
2300
|
+
for output in outputs:
|
|
2301
|
+
output = self.expand(output, inputs=inputfiles, outputs=outputs, **kwargs)
|
|
2302
|
+
subtask.add_output(output)
|
|
1126
2303
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
return self._identity
|
|
2304
|
+
if message:
|
|
2305
|
+
subtask.set_message(self.expand(message, inputs=inputfiles, outputs=outputs, **kwargs))
|
|
1130
2306
|
|
|
1131
|
-
|
|
1132
|
-
def identity(self, identity):
|
|
1133
|
-
self._identity = identity
|
|
2307
|
+
self._add_subtask(subtask)
|
|
1134
2308
|
|
|
1135
|
-
|
|
1136
|
-
return self.cacheable
|
|
2309
|
+
return subtask
|
|
1137
2310
|
|
|
1138
|
-
def
|
|
1139
|
-
|
|
2311
|
+
def call(self, fn, outputs, **kwargs):
|
|
2312
|
+
"""
|
|
2313
|
+
Create a Python function call subtask.
|
|
1140
2314
|
|
|
2315
|
+
The subtask executes the specified Python function, passing the subtask as argument.
|
|
1141
2316
|
|
|
1142
|
-
|
|
1143
|
-
|
|
2317
|
+
Args:
|
|
2318
|
+
fn (func): Python function to execute.
|
|
2319
|
+
outputs (str, list): list of files that the subtasks produces.
|
|
2320
|
+
kwargs: additional keyword values used to format the output file paths.
|
|
1144
2321
|
|
|
1145
|
-
|
|
1146
|
-
|
|
2322
|
+
Returns:
|
|
2323
|
+
Subtask object.
|
|
1147
2324
|
|
|
1148
|
-
|
|
1149
|
-
"""
|
|
2325
|
+
Example:
|
|
1150
2326
|
|
|
1151
|
-
|
|
1152
|
-
|
|
2327
|
+
.. code-block:: python
|
|
2328
|
+
|
|
2329
|
+
def mkdir(subtask):
|
|
2330
|
+
for output in subtask.outputs:
|
|
2331
|
+
self.tools.mkdir(output)
|
|
2332
|
+
|
|
2333
|
+
dirtask = self.call(mkdir, outputs=["newly/created/directory"])
|
|
1153
2334
|
|
|
1154
|
-
def info(self, fmt, *args, **kwargs):
|
|
1155
2335
|
"""
|
|
1156
|
-
|
|
2336
|
+
subtask = FunctionSubtask(self, fn)
|
|
2337
|
+
outputs = utils.as_list(outputs)
|
|
2338
|
+
for output in outputs:
|
|
2339
|
+
output = self.tools.expand_relpath(output, self.joltdir, outputs=outputs, **kwargs)
|
|
2340
|
+
subtask.add_output(output)
|
|
2341
|
+
self._add_subtask(subtask)
|
|
2342
|
+
return subtask
|
|
2343
|
+
|
|
2344
|
+
def mkdir(self, path, *args, **kwargs):
|
|
1157
2345
|
"""
|
|
1158
|
-
|
|
1159
|
-
log.info(fmt, *args, **kwargs)
|
|
2346
|
+
Create a subtask that creates a directory.
|
|
1160
2347
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
log.warning(fmt, *args, **kwargs)
|
|
2348
|
+
Args:
|
|
2349
|
+
path (str): Path to directory.
|
|
2350
|
+
kwargs: additional keyword values used to format the directory path string.
|
|
1165
2351
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
fmt = self.tools.expand(fmt, *args, **kwargs)
|
|
1169
|
-
log.error(fmt, *args, **kwargs)
|
|
2352
|
+
Returns:
|
|
2353
|
+
Subtask object.
|
|
1170
2354
|
|
|
1171
|
-
|
|
1172
|
-
"""
|
|
1173
|
-
Cleans up resources and intermediate files created by the task.
|
|
2355
|
+
Example:
|
|
1174
2356
|
|
|
1175
|
-
|
|
1176
|
-
on the command line. It should restore the environment to its
|
|
1177
|
-
original state. The next execution of the task should behave
|
|
1178
|
-
as if the task is executed for the first time.
|
|
2357
|
+
.. code-block:: python
|
|
1179
2358
|
|
|
1180
|
-
|
|
2359
|
+
dirtask = self.mkdir("{outdir}/directory", outdir=tools.builddir())
|
|
1181
2360
|
"""
|
|
2361
|
+
path = self.expand(path, *args, **kwargs)
|
|
2362
|
+
subtask = self._find_subtask(path)
|
|
2363
|
+
if not subtask:
|
|
2364
|
+
subtask = self.call(lambda subtask: self.tools.mkdir(path), [path])
|
|
2365
|
+
return subtask
|
|
1182
2366
|
|
|
1183
|
-
def
|
|
2367
|
+
def mkdirname(self, path, *args, **kwargs):
|
|
1184
2368
|
"""
|
|
1185
|
-
|
|
2369
|
+
Create a subtask that creates a parent directory.
|
|
1186
2370
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
2371
|
+
Args:
|
|
2372
|
+
path (str): Path for which the parent directory shall be created.
|
|
2373
|
+
kwargs: additional keyword values used to format the directory path string.
|
|
1190
2374
|
|
|
1191
|
-
|
|
2375
|
+
Returns:
|
|
2376
|
+
Subtask object.
|
|
1192
2377
|
|
|
1193
|
-
|
|
1194
|
-
|
|
2378
|
+
Example:
|
|
2379
|
+
|
|
2380
|
+
.. code-block:: python
|
|
2381
|
+
|
|
2382
|
+
# Creates {outdir}/directory
|
|
2383
|
+
dirtask = self.mkdir("{outdir}/directory/object.o", outdir=tools.builddir())
|
|
1195
2384
|
|
|
1196
|
-
When using methods from the toolbox, task parameters, such
|
|
1197
|
-
as ``target`` above, are automatically expanded to their values.
|
|
1198
2385
|
"""
|
|
2386
|
+
path = self.expand(path, *args, **kwargs)
|
|
2387
|
+
path = fs.path.dirname(path)
|
|
2388
|
+
return self.mkdir(path, *args, **kwargs)
|
|
1199
2389
|
|
|
1200
|
-
def
|
|
2390
|
+
def render(self, template, outputs, **kwargs):
|
|
1201
2391
|
"""
|
|
1202
|
-
|
|
2392
|
+
Create a subtask that renders a Jinja template string to file.
|
|
1203
2393
|
|
|
1204
|
-
|
|
1205
|
-
|
|
2394
|
+
Args:
|
|
2395
|
+
template (str): Jinja template string.
|
|
2396
|
+
outputs (str, list): list of files that the subtasks produces.
|
|
2397
|
+
kwargs: additional keyword values used to render the template and output file paths.
|
|
1206
2398
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
consumed. Example:
|
|
2399
|
+
Returns:
|
|
2400
|
+
Subtask object.
|
|
1210
2401
|
|
|
1211
|
-
|
|
2402
|
+
Example:
|
|
1212
2403
|
|
|
1213
|
-
|
|
1214
|
-
artifact.environ.PATH.append("bin")
|
|
2404
|
+
.. code-block:: python
|
|
1215
2405
|
|
|
1216
|
-
|
|
1217
|
-
artifact.strings.foo = "bar"
|
|
2406
|
+
# Creates file.list with two lines containing "a.o" and "b.o"
|
|
1218
2407
|
|
|
1219
|
-
|
|
2408
|
+
template_task = self.render(
|
|
2409
|
+
"{% for line in lines %}{{ line }}\\n{% endfor %}",
|
|
2410
|
+
outputs=["file.list"],
|
|
2411
|
+
lines=["a.o", "b.o"])
|
|
1220
2412
|
|
|
1221
|
-
def unpack(self, artifact, tools):
|
|
1222
2413
|
"""
|
|
1223
|
-
|
|
2414
|
+
outputs = utils.as_list(outputs)
|
|
2415
|
+
subtask = RenderSubtask(self, template, **kwargs)
|
|
2416
|
+
for output in outputs:
|
|
2417
|
+
output = self.tools.expand_relpath(output, self.joltdir, outputs=outputs, **kwargs)
|
|
2418
|
+
subtask.add_output(output)
|
|
2419
|
+
self._add_subtask(subtask)
|
|
2420
|
+
return subtask
|
|
2421
|
+
|
|
2422
|
+
def render_file(self, template, outputs, **kwargs):
|
|
2423
|
+
"""
|
|
2424
|
+
Create a subtask that renders a Jinja template file to file.
|
|
1224
2425
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
2426
|
+
Args:
|
|
2427
|
+
template (str): Jinja template file path.
|
|
2428
|
+
outputs (str, list): list of files that the subtasks produces.
|
|
2429
|
+
kwargs: additional keyword values used to format the output file paths.
|
|
1229
2430
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
2431
|
+
Returns:
|
|
2432
|
+
Subtask object.
|
|
2433
|
+
|
|
2434
|
+
Example:
|
|
2435
|
+
|
|
2436
|
+
.. code-block:: python
|
|
2437
|
+
|
|
2438
|
+
# Render file.list.template into file.list
|
|
2439
|
+
template_task = self.render_file("file.list.template", outputs=["file.list"])
|
|
1233
2440
|
|
|
1234
|
-
def shell(self, deps, tools):
|
|
1235
2441
|
"""
|
|
1236
|
-
|
|
2442
|
+
template = self._to_subtask_list(template)
|
|
2443
|
+
templatefiles = self._to_output_files(template)
|
|
2444
|
+
raise_task_error_if(len(templatefiles) > 1, "Can only render one template at a time")
|
|
2445
|
+
|
|
2446
|
+
outputs = utils.as_list(outputs)
|
|
2447
|
+
subtask = FileRenderSubtask(self, templatefiles[0], **kwargs)
|
|
2448
|
+
for output in outputs:
|
|
2449
|
+
output = self.tools.expand_relpath(output, outputs=outputs, **kwargs)
|
|
2450
|
+
subtask.add_output(output)
|
|
2451
|
+
for input in templatefiles:
|
|
2452
|
+
subtask.add_dependency(input)
|
|
2453
|
+
self._add_subtask(subtask)
|
|
2454
|
+
return subtask
|
|
2455
|
+
|
|
2456
|
+
def generate(self, deps, tools):
|
|
2457
|
+
"""
|
|
2458
|
+
Called to generate subtasks.
|
|
1237
2459
|
|
|
1238
|
-
|
|
1239
|
-
|
|
2460
|
+
An implementer can override this method in order to create subtasks
|
|
2461
|
+
that will later be executed during the :func:`~run` stage of the task.
|
|
2462
|
+
|
|
2463
|
+
Subtasks can be defined using either of these helper methods:
|
|
2464
|
+
|
|
2465
|
+
- :func:`~call`
|
|
2466
|
+
- :func:`~command`
|
|
2467
|
+
- :func:`~mkdir`
|
|
2468
|
+
- :func:`~mkdirname`
|
|
2469
|
+
- :func:`~render`
|
|
2470
|
+
- :func:`~render_file`
|
|
1240
2471
|
|
|
1241
|
-
Task execution resumes normally when exiting the shell.
|
|
1242
2472
|
"""
|
|
1243
|
-
|
|
1244
|
-
from jolt import config
|
|
1245
|
-
subprocess.call(config.get_shell().split(), env=env, cwd=tools._cwd)
|
|
2473
|
+
pass
|
|
1246
2474
|
|
|
1247
|
-
|
|
1248
|
-
def report(self):
|
|
2475
|
+
def run(self, deps, tools):
|
|
1249
2476
|
"""
|
|
1250
|
-
|
|
2477
|
+
Executes subtasks defined in :func:`~generate`.
|
|
1251
2478
|
|
|
1252
|
-
|
|
2479
|
+
This method should typically not be overridden in subclasses.
|
|
1253
2480
|
"""
|
|
1254
|
-
|
|
2481
|
+
self.generate(deps, tools)
|
|
2482
|
+
|
|
2483
|
+
log.debug("About to start executing these subtasks:")
|
|
2484
|
+
for subtask in self._subtasks:
|
|
2485
|
+
for subtaskout in subtask.outputs:
|
|
2486
|
+
if not subtask.dependencies:
|
|
2487
|
+
log.debug(" {}", subtaskout)
|
|
2488
|
+
for dep in subtask.dependencies:
|
|
2489
|
+
for depout in dep.outputs:
|
|
2490
|
+
log.debug(" {}: {}", subtaskout, depout)
|
|
2491
|
+
|
|
2492
|
+
subtasks = {}
|
|
2493
|
+
deps = {}
|
|
2494
|
+
|
|
2495
|
+
# Build graph of inverse dependencies
|
|
2496
|
+
for subtask in self._subtasks:
|
|
2497
|
+
if subtask not in subtasks:
|
|
2498
|
+
subtasks[subtask] = []
|
|
2499
|
+
if subtask not in deps:
|
|
2500
|
+
deps[subtask] = []
|
|
2501
|
+
for dep in subtask.dependencies:
|
|
2502
|
+
if dep not in deps:
|
|
2503
|
+
deps[dep] = []
|
|
2504
|
+
deps[dep].append(subtask)
|
|
2505
|
+
subtasks[subtask].append(dep)
|
|
2506
|
+
|
|
2507
|
+
# Prune up-to-date subtasks
|
|
2508
|
+
for subtask in list(filter(lambda subtask: not subtask.is_outdated, subtasks.keys())):
|
|
2509
|
+
log.debug("Pruning {}", subtask)
|
|
2510
|
+
del subtasks[subtask]
|
|
2511
|
+
for dep in deps[subtask]:
|
|
2512
|
+
try:
|
|
2513
|
+
subtasks[dep].remove(subtask)
|
|
2514
|
+
except KeyError:
|
|
2515
|
+
pass
|
|
2516
|
+
|
|
2517
|
+
self.subtaskindex = 0
|
|
2518
|
+
self.subtaskcount = len(subtasks)
|
|
2519
|
+
|
|
2520
|
+
lock = RLock()
|
|
2521
|
+
|
|
2522
|
+
with ThreadPoolExecutor(max_workers=tools.cpu_count()) as pool:
|
|
2523
|
+
futures = {}
|
|
2524
|
+
|
|
2525
|
+
while subtasks or futures:
|
|
2526
|
+
completed = []
|
|
2527
|
+
candidates = [subtask for subtask, deps in subtasks.items() if not deps]
|
|
2528
|
+
|
|
2529
|
+
if not candidates and not futures:
|
|
2530
|
+
break
|
|
2531
|
+
|
|
2532
|
+
for subtask in candidates:
|
|
2533
|
+
del subtasks[subtask]
|
|
2534
|
+
if subtask.is_outdated:
|
|
2535
|
+
def runner(subtask):
|
|
2536
|
+
with lock:
|
|
2537
|
+
self.subtaskindex += 1
|
|
2538
|
+
log.info("[{}/{}] {}", self.subtaskindex, self.subtaskcount, str(subtask))
|
|
2539
|
+
subtask.run()
|
|
2540
|
+
futures[pool.submit(functools.partial(runner, subtask))] = subtask
|
|
2541
|
+
else:
|
|
2542
|
+
completed.append(subtask)
|
|
2543
|
+
|
|
2544
|
+
for future in as_completed(futures.keys()):
|
|
2545
|
+
subtask = futures[future]
|
|
2546
|
+
del futures[future]
|
|
2547
|
+
completed.append(subtask)
|
|
2548
|
+
|
|
2549
|
+
try:
|
|
2550
|
+
future.result()
|
|
2551
|
+
subtask.set_uptodate()
|
|
2552
|
+
except Exception as e:
|
|
2553
|
+
for future in futures:
|
|
2554
|
+
future.cancel()
|
|
2555
|
+
raise e
|
|
2556
|
+
break
|
|
2557
|
+
|
|
2558
|
+
for subtask in completed:
|
|
2559
|
+
for dep in deps[subtask]:
|
|
2560
|
+
subtasks[dep].remove(subtask)
|
|
2561
|
+
|
|
2562
|
+
if subtasks:
|
|
2563
|
+
log.debug("These remaining subtasks could not be started due to unresolved dependencies")
|
|
2564
|
+
for subtask in subtasks:
|
|
2565
|
+
log.debug(" {}", str(subtask))
|
|
2566
|
+
for dep in subtask.dependencies:
|
|
2567
|
+
log.debug(" - {}", str(dep))
|
|
2568
|
+
|
|
2569
|
+
raise_task_error_if(subtasks, self, "Subtasks with unresolved dependencies could not be executed")
|
|
2570
|
+
|
|
2571
|
+
def inputs(self, jobs):
|
|
2572
|
+
return self._to_subtask_list(jobs)
|
|
2573
|
+
|
|
2574
|
+
def outputs(self, jobs):
|
|
2575
|
+
jobs = utils.as_list(jobs)
|
|
2576
|
+
return [output for job in jobs for output in job.outputs]
|
|
2577
|
+
|
|
2578
|
+
|
|
2579
|
+
class Runner(Task):
|
|
2580
|
+
"""
|
|
2581
|
+
A Runner task executes applications packaged by other tasks.
|
|
2582
|
+
|
|
2583
|
+
It is typically used to run test applications compiled and linked
|
|
2584
|
+
by other tasks. The Runner finds the executable through the artifact
|
|
2585
|
+
metadata string ``artifact.strings.executable`` which must be exported
|
|
2586
|
+
by the consumed task artifact.
|
|
2587
|
+
|
|
2588
|
+
Example:
|
|
2589
|
+
|
|
2590
|
+
.. code-block:: python
|
|
2591
|
+
|
|
2592
|
+
from jolt import Runner, Task
|
|
2593
|
+
|
|
2594
|
+
class Exe(Task):
|
|
2595
|
+
\"\"\" Publish a script printing 'Hello world' to stdout \"\"\"
|
|
2596
|
+
def publish(self, artifact, tools):
|
|
2597
|
+
with tools.cwd(tools.builddir()):
|
|
2598
|
+
# Create Hello world script
|
|
2599
|
+
tools.write_file("hello.sh", "#!/bin/sh\\necho Hello world")
|
|
2600
|
+
|
|
2601
|
+
# Make it executable
|
|
2602
|
+
tools.chmod("hello.sh", 0o555)
|
|
2603
|
+
|
|
2604
|
+
# Publish script in artifact
|
|
2605
|
+
artifact.collect("hello.sh")
|
|
2606
|
+
|
|
2607
|
+
# Inform consuming Runner task about executable's name
|
|
2608
|
+
artifact.strings.executable = "hello.sh"
|
|
2609
|
+
|
|
2610
|
+
class Run(Runner):
|
|
2611
|
+
\"\"\" Runs the 'Hello world' script \"\"\"
|
|
2612
|
+
requires = ["exe"]
|
|
2613
|
+
|
|
2614
|
+
The Ninja CXXExecutable task class automatically sets the required artifact metadata.
|
|
2615
|
+
|
|
2616
|
+
Example:
|
|
2617
|
+
|
|
2618
|
+
.. code-block:: python
|
|
2619
|
+
|
|
2620
|
+
from jolt import Task
|
|
2621
|
+
from jolt.plugins.ninja import CXXExecutable
|
|
2622
|
+
|
|
2623
|
+
class Exe(CXXExecutable):
|
|
2624
|
+
\"\"\" Compiles and links the test application \"\"\"
|
|
2625
|
+
sources = ["test.cpp"]
|
|
2626
|
+
|
|
2627
|
+
class Run(Runner):
|
|
2628
|
+
\"\"\" Runs the test application \"\"\"
|
|
2629
|
+
requires = ["exe"]
|
|
2630
|
+
|
|
2631
|
+
"""
|
|
2632
|
+
|
|
2633
|
+
abstract = True
|
|
2634
|
+
|
|
2635
|
+
args = []
|
|
2636
|
+
"""
|
|
2637
|
+
List of arguments to pass to executables.
|
|
2638
|
+
|
|
2639
|
+
The arguments are passed the same way to all executables if there
|
|
2640
|
+
are multiple task requirements.
|
|
2641
|
+
"""
|
|
2642
|
+
|
|
2643
|
+
requires = []
|
|
2644
|
+
""" List of tasks packaging executables to run. """
|
|
2645
|
+
|
|
2646
|
+
shell = True
|
|
2647
|
+
""" Launch the executables through a shell. """
|
|
2648
|
+
|
|
2649
|
+
timeout = None
|
|
2650
|
+
""" Time after which the executable will be terminated """
|
|
2651
|
+
|
|
2652
|
+
def run(self, deps, tools):
|
|
2653
|
+
args = tools.expand(self.args)
|
|
2654
|
+
timeout = int(self.timeout) if self.timeout is not None else None
|
|
2655
|
+
found = False
|
|
2656
|
+
|
|
2657
|
+
for task, artifact in deps.items():
|
|
2658
|
+
if not artifact.task.is_cacheable():
|
|
2659
|
+
continue
|
|
2660
|
+
if artifact.strings.executable is None:
|
|
2661
|
+
self.verbose("No executable found in task artifact for '{}'", task)
|
|
2662
|
+
continue
|
|
2663
|
+
with tools.cwd(artifact.path):
|
|
2664
|
+
found = True
|
|
2665
|
+
exe = tools.expand_path(str(artifact.strings.executable))
|
|
2666
|
+
exe = [exe] + args
|
|
2667
|
+
exe = " ".join(exe) if self.shell else exe
|
|
2668
|
+
tools.run(exe, shell=bool(self.shell), timeout=timeout)
|
|
2669
|
+
|
|
2670
|
+
raise_task_error_if(
|
|
2671
|
+
not found, self,
|
|
2672
|
+
"No executable found in any requirement artifact")
|
|
1255
2673
|
|
|
1256
2674
|
|
|
1257
2675
|
class ErrorProxy(object):
|
|
1258
2676
|
def __init__(self, error):
|
|
1259
2677
|
self._error = error
|
|
1260
2678
|
|
|
2679
|
+
@property
|
|
2680
|
+
def type(self):
|
|
2681
|
+
return self._error.type
|
|
2682
|
+
|
|
2683
|
+
@type.setter
|
|
2684
|
+
def type(self, value):
|
|
2685
|
+
self._error.type = value
|
|
2686
|
+
|
|
2687
|
+
@property
|
|
2688
|
+
def details(self):
|
|
2689
|
+
return self._error.details
|
|
2690
|
+
|
|
2691
|
+
@property
|
|
2692
|
+
def location(self):
|
|
2693
|
+
return self._error.location
|
|
2694
|
+
|
|
2695
|
+
@property
|
|
2696
|
+
def message(self):
|
|
2697
|
+
return self._error.message
|
|
2698
|
+
|
|
1261
2699
|
|
|
1262
2700
|
class ReportProxy(object):
|
|
1263
2701
|
def __init__(self, task, report):
|
|
2702
|
+
from jolt import config
|
|
1264
2703
|
self._task = task
|
|
1265
2704
|
self._report = report
|
|
2705
|
+
self._max_errors = config.getint("jolt", "task_max_errors", 100)
|
|
1266
2706
|
|
|
1267
2707
|
def add_error(self, type, location, message, details=""):
|
|
1268
2708
|
""" Add an error to the build report. """
|
|
2709
|
+
if len(self.errors) >= self._max_errors:
|
|
2710
|
+
if not hasattr(self._report, "truncated"):
|
|
2711
|
+
error = self._report.create_error()
|
|
2712
|
+
error.type = "Error"
|
|
2713
|
+
error.message = "Too many errors, list truncated"
|
|
2714
|
+
self._report.truncated = True
|
|
2715
|
+
return None
|
|
2716
|
+
|
|
1269
2717
|
error = self._report.create_error()
|
|
1270
2718
|
error.type = type
|
|
1271
2719
|
error.location = location
|
|
@@ -1284,13 +2732,10 @@ class ReportProxy(object):
|
|
|
1284
2732
|
- details - futher error details
|
|
1285
2733
|
|
|
1286
2734
|
"""
|
|
1287
|
-
for match in re.finditer(regex, logbuf):
|
|
2735
|
+
for match in re.finditer(regex, logbuf, re.MULTILINE):
|
|
1288
2736
|
error = match.groupdict()
|
|
1289
|
-
self.add_error(
|
|
1290
|
-
|
|
1291
|
-
error.get("location", ""),
|
|
1292
|
-
error.get("message", ""),
|
|
1293
|
-
error.get("details", ""))
|
|
2737
|
+
if not self.add_error(type, error.get("location", ""), error.get("message", ""), error.get("details", "")):
|
|
2738
|
+
break
|
|
1294
2739
|
|
|
1295
2740
|
def add_regex_errors_with_file(self, type, regex, logbuf, reldir, filterfn=lambda n: True):
|
|
1296
2741
|
"""
|
|
@@ -1306,25 +2751,34 @@ class ReportProxy(object):
|
|
|
1306
2751
|
In case file is a relative path, reldir is the working directory.
|
|
1307
2752
|
"""
|
|
1308
2753
|
errors_by_location = OrderedDict()
|
|
1309
|
-
for match in re.finditer(regex, logbuf):
|
|
2754
|
+
for match in re.finditer(regex, logbuf, re.MULTILINE):
|
|
1310
2755
|
error = match.groupdict()
|
|
1311
2756
|
if not filterfn(error):
|
|
1312
2757
|
continue
|
|
1313
2758
|
if error["location"] not in errors_by_location:
|
|
1314
|
-
errors_by_location[error["location"]] = (error, [error["message"]])
|
|
2759
|
+
errors_by_location[error["location"]] = (error, [error["message"]], error.get("details", ""))
|
|
1315
2760
|
else:
|
|
1316
2761
|
errors_by_location[error["location"]][1].append(error["message"])
|
|
1317
2762
|
|
|
1318
|
-
for error, msgs in errors_by_location.values():
|
|
2763
|
+
for error, msgs, details in errors_by_location.values():
|
|
1319
2764
|
message = "\n".join(utils.unique_list(msgs))
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
2765
|
+
if not details:
|
|
2766
|
+
with self._task.tools.cwd(self._task.tools.wsroot):
|
|
2767
|
+
try:
|
|
2768
|
+
details = self._task.tools.read_file(error["file"])
|
|
2769
|
+
details = details.splitlines()
|
|
2770
|
+
details = str(error["line"]) + ": " + details[int(error["line"]) - 1]
|
|
2771
|
+
except Exception:
|
|
2772
|
+
details = ""
|
|
2773
|
+
|
|
2774
|
+
location = error.get("location", "")
|
|
2775
|
+
if location:
|
|
2776
|
+
with self._task.tools.cwd(self._task.tools.wsroot):
|
|
2777
|
+
location = self._task.tools.expand_path(location)
|
|
2778
|
+
location = self._task.tools.expand_relpath(location, self._task.tools.wsroot)
|
|
2779
|
+
|
|
2780
|
+
if not self.add_error(type, location, message, details):
|
|
2781
|
+
break
|
|
1328
2782
|
|
|
1329
2783
|
def add_exception(self, exc, errtype=None, location=None):
|
|
1330
2784
|
"""
|
|
@@ -1338,13 +2792,14 @@ class ReportProxy(object):
|
|
|
1338
2792
|
|
|
1339
2793
|
"""
|
|
1340
2794
|
tb = traceback.format_exception(type(exc), value=exc, tb=exc.__traceback__)
|
|
2795
|
+
|
|
1341
2796
|
installdir = fs.path.dirname(__file__)
|
|
1342
2797
|
if any(map(lambda frame: installdir not in frame, tb[1:-1])):
|
|
1343
2798
|
while len(tb) > 2 and installdir in tb[1]:
|
|
1344
2799
|
del tb[1]
|
|
1345
|
-
loc = re.findall("\"
|
|
2800
|
+
loc = re.findall("(\".*?\", line [0-9]+, in .*?)\n", tb[1])
|
|
1346
2801
|
location = location or (loc[0] if loc and len(loc) > 0 else "")
|
|
1347
|
-
message =
|
|
2802
|
+
message = log.format_exception_msg(exc)
|
|
1348
2803
|
if isinstance(exc, JoltCommandError):
|
|
1349
2804
|
details = "\n".join(exc.stderr)
|
|
1350
2805
|
elif isinstance(exc, JoltError):
|
|
@@ -1362,10 +2817,26 @@ class ReportProxy(object):
|
|
|
1362
2817
|
def errors(self):
|
|
1363
2818
|
return [ErrorProxy(error) for error in self._report.errors]
|
|
1364
2819
|
|
|
2820
|
+
@errors.setter
|
|
2821
|
+
def errors(self, errlist):
|
|
2822
|
+
assert all(isinstance(err, ErrorProxy) for err in errlist), "Invalid error list"
|
|
2823
|
+
self._report.clear_errors()
|
|
2824
|
+
for err in errlist:
|
|
2825
|
+
self.add_error(err.type, err.location, err.message, err.details)
|
|
2826
|
+
|
|
1365
2827
|
@property
|
|
1366
2828
|
def manifest(self):
|
|
1367
2829
|
return self._report
|
|
1368
2830
|
|
|
2831
|
+
def raise_for_status(self, log_details=False, log_error=False):
|
|
2832
|
+
for error in self.errors:
|
|
2833
|
+
if log_error:
|
|
2834
|
+
log.error("{}: {}", error.type, error.message, context=self._task.identity[:7])
|
|
2835
|
+
if log_details:
|
|
2836
|
+
for line in error.details.splitlines():
|
|
2837
|
+
log.transfer(line, context=self._task.identity[:7])
|
|
2838
|
+
raise LoggedJoltError(JoltError(f"{error.type}: {error.message}"))
|
|
2839
|
+
|
|
1369
2840
|
|
|
1370
2841
|
class Resource(Task):
|
|
1371
2842
|
"""
|
|
@@ -1379,13 +2850,17 @@ class Resource(Task):
|
|
|
1379
2850
|
|
|
1380
2851
|
"""
|
|
1381
2852
|
|
|
1382
|
-
cacheable = False
|
|
1383
|
-
|
|
1384
2853
|
abstract = True
|
|
1385
2854
|
""" An abstract resource class indended to be subclassed. """
|
|
1386
2855
|
|
|
2856
|
+
release_on_error = False
|
|
2857
|
+
""" Call release if an exception occurs during acquire. """
|
|
2858
|
+
|
|
1387
2859
|
def __init__(self, *args, **kwargs):
|
|
1388
|
-
super(
|
|
2860
|
+
super().__init__(*args, **kwargs)
|
|
2861
|
+
|
|
2862
|
+
def _artifacts(self, cache, node):
|
|
2863
|
+
return [cache.get_artifact(node, "main", session=True)]
|
|
1389
2864
|
|
|
1390
2865
|
def is_runnable(self):
|
|
1391
2866
|
return False
|
|
@@ -1446,17 +2921,25 @@ class WorkspaceResource(Resource):
|
|
|
1446
2921
|
"""
|
|
1447
2922
|
|
|
1448
2923
|
def __init__(self, *args, **kwargs):
|
|
1449
|
-
super(
|
|
2924
|
+
super().__init__(*args, **kwargs)
|
|
1450
2925
|
raise_task_error_if(len(self.requires) > 0, self,
|
|
1451
2926
|
"Workspace resource is not allowed to have requirements")
|
|
1452
2927
|
|
|
1453
|
-
def acquire(self, **kwargs):
|
|
2928
|
+
def acquire(self, *args, **kwargs):
|
|
1454
2929
|
return self.acquire_ws()
|
|
1455
2930
|
|
|
1456
|
-
def release(self, **kwargs):
|
|
2931
|
+
def release(self, *args, **kwargs):
|
|
1457
2932
|
return self.release_ws()
|
|
1458
2933
|
|
|
1459
|
-
def
|
|
2934
|
+
def prepare_ws_for(self, task):
|
|
2935
|
+
""" Called to prepare the workspace for a task.
|
|
2936
|
+
|
|
2937
|
+
An implementor overrides this method in a subclass. The method
|
|
2938
|
+
is called before the task influence is calculated and the workspace
|
|
2939
|
+
resource is acquired.
|
|
2940
|
+
"""
|
|
2941
|
+
|
|
2942
|
+
def acquire_ws(self, force=False):
|
|
1460
2943
|
""" Called to acquire the resource.
|
|
1461
2944
|
|
|
1462
2945
|
An implementor overrides this method in a subclass. The acquired
|
|
@@ -1471,6 +2954,61 @@ class WorkspaceResource(Resource):
|
|
|
1471
2954
|
"""
|
|
1472
2955
|
|
|
1473
2956
|
|
|
2957
|
+
@attributes.requires("_image")
|
|
2958
|
+
class Chroot(Resource):
|
|
2959
|
+
"""
|
|
2960
|
+
Resource to use task artifact or directory path as chroot in consumers.
|
|
2961
|
+
|
|
2962
|
+
Example:
|
|
2963
|
+
|
|
2964
|
+
.. code-block:: python
|
|
2965
|
+
|
|
2966
|
+
from jolt import Chroot, Task
|
|
2967
|
+
from jolt.plugins.podman import ContainerImage
|
|
2968
|
+
|
|
2969
|
+
class SdkImage(ContainerImage):
|
|
2970
|
+
dockerfile = \"\"\"
|
|
2971
|
+
FROM debian:sid-slim
|
|
2972
|
+
ARG DEBIAN_FRONTEND=noninteractive
|
|
2973
|
+
RUN apt-get update && apt-get install -y --no-install-recommends gcc g++ && apt-get clean
|
|
2974
|
+
\"\"\"
|
|
2975
|
+
output = "directory"
|
|
2976
|
+
|
|
2977
|
+
class Sdk(Chroot):
|
|
2978
|
+
chroot = "sdkimage"
|
|
2979
|
+
|
|
2980
|
+
class Compile(Task):
|
|
2981
|
+
requires = ["sdk"]
|
|
2982
|
+
|
|
2983
|
+
def run(self, deps, tools):
|
|
2984
|
+
tools.run("gcc -v")
|
|
2985
|
+
|
|
2986
|
+
"""
|
|
2987
|
+
abstract = True
|
|
2988
|
+
|
|
2989
|
+
chroot = None
|
|
2990
|
+
""" Task name or directory path to use as chroot """
|
|
2991
|
+
|
|
2992
|
+
@property
|
|
2993
|
+
def _image(self):
|
|
2994
|
+
registry = TaskRegistry.get()
|
|
2995
|
+
if registry.get_task_class(self.expand(self.chroot)):
|
|
2996
|
+
return [self.chroot]
|
|
2997
|
+
return []
|
|
2998
|
+
|
|
2999
|
+
def acquire(self, artifact, deps, tools, owner):
|
|
3000
|
+
try:
|
|
3001
|
+
rootfs = deps[self.chroot]
|
|
3002
|
+
except Exception:
|
|
3003
|
+
rootfs = tools.expand(self.image)
|
|
3004
|
+
self._context_stack = ExitStack()
|
|
3005
|
+
self._context_stack.enter_context(
|
|
3006
|
+
owner.tools.chroot(rootfs))
|
|
3007
|
+
|
|
3008
|
+
def release(self, artifact, deps, tools, owner):
|
|
3009
|
+
self._context_stack.close()
|
|
3010
|
+
|
|
3011
|
+
|
|
1474
3012
|
class Alias(Task):
|
|
1475
3013
|
"""
|
|
1476
3014
|
An alias task.
|
|
@@ -1505,6 +3043,7 @@ class Download(Task):
|
|
|
1505
3043
|
Once downloaded, archives are extracted and all of their files are published.
|
|
1506
3044
|
If the file is not an archive it is published as is. Recognized archive extensions are:
|
|
1507
3045
|
|
|
3046
|
+
- .7z
|
|
1508
3047
|
- .tar
|
|
1509
3048
|
- .tar.bz2
|
|
1510
3049
|
- .tar.gz
|
|
@@ -1567,7 +3106,7 @@ class Download(Task):
|
|
|
1567
3106
|
return fs.posixpath.basename(url.path) or "file"
|
|
1568
3107
|
|
|
1569
3108
|
def run(self, deps, tools):
|
|
1570
|
-
supported_formats = [".tar", ".tar.bz2", ".tar.gz", ".tar.xz", ".tgz", ".zip"]
|
|
3109
|
+
supported_formats = [".7z", ".tar", ".tar.bz2", ".tar.gz", ".tar.xz", ".tgz", ".zip"]
|
|
1571
3110
|
|
|
1572
3111
|
raise_task_error_if(not self.url, self, "No URL(s) specified")
|
|
1573
3112
|
|
|
@@ -1589,9 +3128,9 @@ class Download(Task):
|
|
|
1589
3128
|
def publish(self, artifact, tools):
|
|
1590
3129
|
with tools.cwd(self._extractdir):
|
|
1591
3130
|
for files in self.collect:
|
|
1592
|
-
if type(files)
|
|
3131
|
+
if type(files) is tuple:
|
|
1593
3132
|
artifact.collect(*files, symlinks=self.symlinks)
|
|
1594
|
-
elif type(files)
|
|
3133
|
+
elif type(files) is dict:
|
|
1595
3134
|
artifact.collect(**files, symlinks=self.symlinks)
|
|
1596
3135
|
else:
|
|
1597
3136
|
artifact.collect(files, symlinks=self.symlinks)
|
|
@@ -1664,7 +3203,8 @@ class Script(Task):
|
|
|
1664
3203
|
doc = self.__doc__.split("---", 1)
|
|
1665
3204
|
script = doc[1] if len(doc) > 1 else doc[0]
|
|
1666
3205
|
script = script.splitlines()
|
|
1667
|
-
|
|
3206
|
+
if os_sys.version_info < (3, 13):
|
|
3207
|
+
script = [line[4:] for line in script]
|
|
1668
3208
|
script = "\n".join(script)
|
|
1669
3209
|
script = script.lstrip()
|
|
1670
3210
|
if not script.startswith("#!"):
|
|
@@ -1683,9 +3223,9 @@ class Script(Task):
|
|
|
1683
3223
|
def publish(self, artifact, tools):
|
|
1684
3224
|
with tools.cwd(self.builddir):
|
|
1685
3225
|
for files in self.collect:
|
|
1686
|
-
if type(files)
|
|
3226
|
+
if type(files) is tuple:
|
|
1687
3227
|
artifact.collect(*files)
|
|
1688
|
-
elif type(files)
|
|
3228
|
+
elif type(files) is dict:
|
|
1689
3229
|
artifact.collect(**files)
|
|
1690
3230
|
else:
|
|
1691
3231
|
artifact.collect(files)
|
|
@@ -1791,7 +3331,7 @@ class Test(Task):
|
|
|
1791
3331
|
Abstract test tasks can't be executed and won't be listed.
|
|
1792
3332
|
"""
|
|
1793
3333
|
|
|
1794
|
-
|
|
3334
|
+
filter = Parameter(required=False, help="Test-case filter wildcard.")
|
|
1795
3335
|
|
|
1796
3336
|
def __init__(self, *args, **kwargs):
|
|
1797
3337
|
super().__init__(*args, **kwargs)
|
|
@@ -1818,7 +3358,7 @@ class Test(Task):
|
|
|
1818
3358
|
self.assertEqual(factor1*factor2, product)
|
|
1819
3359
|
|
|
1820
3360
|
"""
|
|
1821
|
-
raise_error_if(type(args)
|
|
3361
|
+
raise_error_if(type(args) is not list, "Test.parameterized() expects a list as argument")
|
|
1822
3362
|
|
|
1823
3363
|
class partialmethod(functools.partialmethod):
|
|
1824
3364
|
def __init__(self, index, func, *args):
|
|
@@ -1946,7 +3486,7 @@ class Test(Task):
|
|
|
1946
3486
|
def run(self, deps, tools):
|
|
1947
3487
|
testsuite = ut.TestSuite()
|
|
1948
3488
|
for test in self._get_test_names():
|
|
1949
|
-
if self.
|
|
3489
|
+
if self.filter.is_unset() or fnmatch.fnmatch(test, str(self.filter)):
|
|
1950
3490
|
testfunc = getattr(self, test)
|
|
1951
3491
|
if not testfunc:
|
|
1952
3492
|
continue
|
|
@@ -1967,7 +3507,7 @@ class Test(Task):
|
|
|
1967
3507
|
|
|
1968
3508
|
|
|
1969
3509
|
@ArtifactAttributeSetProvider.Register
|
|
1970
|
-
class
|
|
3510
|
+
class WorkspaceResourceAttributeSetProvider(ArtifactAttributeSetProvider):
|
|
1971
3511
|
def create(self, artifact):
|
|
1972
3512
|
pass
|
|
1973
3513
|
|
|
@@ -1978,33 +3518,31 @@ class ResourceAttributeSetProvider(ArtifactAttributeSetProvider):
|
|
|
1978
3518
|
pass
|
|
1979
3519
|
|
|
1980
3520
|
def apply(self, task, artifact):
|
|
1981
|
-
resource = artifact.
|
|
1982
|
-
|
|
1983
|
-
|
|
3521
|
+
resource = artifact.task
|
|
3522
|
+
node = artifact.get_node()
|
|
3523
|
+
if not node.is_workspace_resource():
|
|
3524
|
+
return
|
|
1984
3525
|
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
3526
|
+
resource.deps = node.cache.get_context(node)
|
|
3527
|
+
resource.deps.__enter__()
|
|
3528
|
+
|
|
3529
|
+
try:
|
|
3530
|
+
resource.acquire(artifact=artifact, deps=resource.deps, tools=resource.tools, owner=task)
|
|
3531
|
+
except (KeyboardInterrupt, Exception) as e:
|
|
3532
|
+
if resource.release_on_error:
|
|
3533
|
+
with utils.ignore_exception():
|
|
3534
|
+
self.unapply(task, artifact)
|
|
3535
|
+
raise e
|
|
1995
3536
|
|
|
1996
3537
|
def unapply(self, task, artifact):
|
|
1997
|
-
resource = artifact.
|
|
1998
|
-
|
|
1999
|
-
|
|
3538
|
+
resource = artifact.task
|
|
3539
|
+
node = artifact.get_node()
|
|
3540
|
+
if not node.is_workspace_resource():
|
|
3541
|
+
return
|
|
2000
3542
|
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
ba = sig.bind_partial(artifact, deps, resource.tools)
|
|
2008
|
-
release = utils.deprecated(resource.release)
|
|
2009
|
-
release(*ba.args, **ba.kwargs)
|
|
2010
|
-
deps.__exit__(None, None, None)
|
|
3543
|
+
try:
|
|
3544
|
+
resource.release(artifact=artifact, deps=resource.deps, tools=resource.tools, owner=task)
|
|
3545
|
+
except Exception as e:
|
|
3546
|
+
raise e
|
|
3547
|
+
|
|
3548
|
+
resource.deps.__exit__(None, None, None)
|