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.
Files changed (201) hide show
  1. jolt/__init__.py +88 -7
  2. jolt/__main__.py +9 -1
  3. jolt/bin/fstree-darwin-x86_64 +0 -0
  4. jolt/bin/fstree-linux-x86_64 +0 -0
  5. jolt/cache.py +839 -367
  6. jolt/chroot.py +156 -0
  7. jolt/cli.py +362 -143
  8. jolt/common_pb2.py +63 -0
  9. jolt/common_pb2_grpc.py +4 -0
  10. jolt/config.py +99 -42
  11. jolt/error.py +19 -4
  12. jolt/expires.py +2 -2
  13. jolt/filesystem.py +8 -6
  14. jolt/graph.py +705 -117
  15. jolt/hooks.py +63 -1
  16. jolt/influence.py +129 -6
  17. jolt/loader.py +369 -121
  18. jolt/log.py +225 -63
  19. jolt/manifest.py +28 -38
  20. jolt/options.py +35 -10
  21. jolt/pkgs/abseil.py +42 -0
  22. jolt/pkgs/asio.py +25 -0
  23. jolt/pkgs/autoconf.py +41 -0
  24. jolt/pkgs/automake.py +41 -0
  25. jolt/pkgs/b2.py +31 -0
  26. jolt/pkgs/boost.py +111 -0
  27. jolt/pkgs/boringssl.py +32 -0
  28. jolt/pkgs/busybox.py +39 -0
  29. jolt/pkgs/bzip2.py +43 -0
  30. jolt/pkgs/cares.py +29 -0
  31. jolt/pkgs/catch2.py +36 -0
  32. jolt/pkgs/cbindgen.py +17 -0
  33. jolt/pkgs/cista.py +19 -0
  34. jolt/pkgs/clang.py +44 -0
  35. jolt/pkgs/cli11.py +23 -0
  36. jolt/pkgs/cmake.py +48 -0
  37. jolt/pkgs/cpython.py +196 -0
  38. jolt/pkgs/crun.py +29 -0
  39. jolt/pkgs/curl.py +38 -0
  40. jolt/pkgs/dbus.py +18 -0
  41. jolt/pkgs/double_conversion.py +24 -0
  42. jolt/pkgs/fastfloat.py +21 -0
  43. jolt/pkgs/ffmpeg.py +28 -0
  44. jolt/pkgs/flatbuffers.py +29 -0
  45. jolt/pkgs/fmt.py +27 -0
  46. jolt/pkgs/fstree.py +20 -0
  47. jolt/pkgs/gflags.py +18 -0
  48. jolt/pkgs/glib.py +18 -0
  49. jolt/pkgs/glog.py +25 -0
  50. jolt/pkgs/glslang.py +21 -0
  51. jolt/pkgs/golang.py +16 -11
  52. jolt/pkgs/googlebenchmark.py +18 -0
  53. jolt/pkgs/googletest.py +46 -0
  54. jolt/pkgs/gperf.py +15 -0
  55. jolt/pkgs/grpc.py +73 -0
  56. jolt/pkgs/hdf5.py +19 -0
  57. jolt/pkgs/help2man.py +14 -0
  58. jolt/pkgs/inja.py +28 -0
  59. jolt/pkgs/jsoncpp.py +31 -0
  60. jolt/pkgs/libarchive.py +43 -0
  61. jolt/pkgs/libcap.py +44 -0
  62. jolt/pkgs/libdrm.py +44 -0
  63. jolt/pkgs/libedit.py +42 -0
  64. jolt/pkgs/libevent.py +31 -0
  65. jolt/pkgs/libexpat.py +27 -0
  66. jolt/pkgs/libfastjson.py +21 -0
  67. jolt/pkgs/libffi.py +16 -0
  68. jolt/pkgs/libglvnd.py +30 -0
  69. jolt/pkgs/libogg.py +28 -0
  70. jolt/pkgs/libpciaccess.py +18 -0
  71. jolt/pkgs/libseccomp.py +21 -0
  72. jolt/pkgs/libtirpc.py +24 -0
  73. jolt/pkgs/libtool.py +42 -0
  74. jolt/pkgs/libunwind.py +35 -0
  75. jolt/pkgs/libva.py +18 -0
  76. jolt/pkgs/libvorbis.py +33 -0
  77. jolt/pkgs/libxml2.py +35 -0
  78. jolt/pkgs/libxslt.py +17 -0
  79. jolt/pkgs/libyajl.py +16 -0
  80. jolt/pkgs/llvm.py +81 -0
  81. jolt/pkgs/lua.py +54 -0
  82. jolt/pkgs/lz4.py +26 -0
  83. jolt/pkgs/m4.py +14 -0
  84. jolt/pkgs/make.py +17 -0
  85. jolt/pkgs/mesa.py +81 -0
  86. jolt/pkgs/meson.py +17 -0
  87. jolt/pkgs/mstch.py +28 -0
  88. jolt/pkgs/mysql.py +60 -0
  89. jolt/pkgs/nasm.py +49 -0
  90. jolt/pkgs/ncurses.py +30 -0
  91. jolt/pkgs/ng_log.py +25 -0
  92. jolt/pkgs/ninja.py +45 -0
  93. jolt/pkgs/nlohmann_json.py +25 -0
  94. jolt/pkgs/nodejs.py +19 -11
  95. jolt/pkgs/opencv.py +24 -0
  96. jolt/pkgs/openjdk.py +26 -0
  97. jolt/pkgs/openssl.py +103 -0
  98. jolt/pkgs/paho.py +76 -0
  99. jolt/pkgs/patchelf.py +16 -0
  100. jolt/pkgs/perl.py +42 -0
  101. jolt/pkgs/pkgconfig.py +64 -0
  102. jolt/pkgs/poco.py +39 -0
  103. jolt/pkgs/protobuf.py +77 -0
  104. jolt/pkgs/pugixml.py +27 -0
  105. jolt/pkgs/python.py +19 -0
  106. jolt/pkgs/qt.py +35 -0
  107. jolt/pkgs/rapidjson.py +26 -0
  108. jolt/pkgs/rapidyaml.py +28 -0
  109. jolt/pkgs/re2.py +30 -0
  110. jolt/pkgs/re2c.py +17 -0
  111. jolt/pkgs/readline.py +15 -0
  112. jolt/pkgs/rust.py +41 -0
  113. jolt/pkgs/sdl.py +28 -0
  114. jolt/pkgs/simdjson.py +27 -0
  115. jolt/pkgs/soci.py +46 -0
  116. jolt/pkgs/spdlog.py +29 -0
  117. jolt/pkgs/spirv_llvm.py +21 -0
  118. jolt/pkgs/spirv_tools.py +24 -0
  119. jolt/pkgs/sqlite.py +83 -0
  120. jolt/pkgs/ssl.py +12 -0
  121. jolt/pkgs/texinfo.py +15 -0
  122. jolt/pkgs/tomlplusplus.py +22 -0
  123. jolt/pkgs/wayland.py +26 -0
  124. jolt/pkgs/x11.py +58 -0
  125. jolt/pkgs/xerces_c.py +20 -0
  126. jolt/pkgs/xorg.py +360 -0
  127. jolt/pkgs/xz.py +29 -0
  128. jolt/pkgs/yamlcpp.py +30 -0
  129. jolt/pkgs/zeromq.py +47 -0
  130. jolt/pkgs/zlib.py +69 -0
  131. jolt/pkgs/zstd.py +33 -0
  132. jolt/plugins/alias.py +3 -0
  133. jolt/plugins/allure.py +5 -2
  134. jolt/plugins/autotools.py +66 -0
  135. jolt/plugins/cache.py +133 -0
  136. jolt/plugins/cmake.py +74 -6
  137. jolt/plugins/conan.py +238 -0
  138. jolt/plugins/cxx.py +698 -0
  139. jolt/plugins/cxxinfo.py +7 -0
  140. jolt/plugins/dashboard.py +1 -1
  141. jolt/plugins/docker.py +91 -23
  142. jolt/plugins/email.py +5 -2
  143. jolt/plugins/email.xslt +144 -101
  144. jolt/plugins/environ.py +11 -0
  145. jolt/plugins/fetch.py +141 -0
  146. jolt/plugins/gdb.py +44 -21
  147. jolt/plugins/gerrit.py +1 -14
  148. jolt/plugins/git.py +316 -101
  149. jolt/plugins/googletest.py +522 -1
  150. jolt/plugins/http.py +36 -38
  151. jolt/plugins/libtool.py +63 -0
  152. jolt/plugins/linux.py +990 -0
  153. jolt/plugins/logstash.py +4 -4
  154. jolt/plugins/meson.py +61 -0
  155. jolt/plugins/ninja-compdb.py +107 -31
  156. jolt/plugins/ninja.py +929 -134
  157. jolt/plugins/paths.py +11 -1
  158. jolt/plugins/pkgconfig.py +219 -0
  159. jolt/plugins/podman.py +148 -91
  160. jolt/plugins/python.py +137 -0
  161. jolt/plugins/remote_execution/__init__.py +0 -0
  162. jolt/plugins/remote_execution/administration_pb2.py +46 -0
  163. jolt/plugins/remote_execution/administration_pb2_grpc.py +170 -0
  164. jolt/plugins/remote_execution/log_pb2.py +32 -0
  165. jolt/plugins/remote_execution/log_pb2_grpc.py +68 -0
  166. jolt/plugins/remote_execution/scheduler_pb2.py +41 -0
  167. jolt/plugins/remote_execution/scheduler_pb2_grpc.py +141 -0
  168. jolt/plugins/remote_execution/worker_pb2.py +38 -0
  169. jolt/plugins/remote_execution/worker_pb2_grpc.py +112 -0
  170. jolt/plugins/report.py +12 -2
  171. jolt/plugins/rust.py +25 -0
  172. jolt/plugins/scheduler.py +710 -0
  173. jolt/plugins/selfdeploy/setup.py +9 -4
  174. jolt/plugins/selfdeploy.py +138 -88
  175. jolt/plugins/strings.py +35 -22
  176. jolt/plugins/symlinks.py +26 -11
  177. jolt/plugins/telemetry.py +5 -2
  178. jolt/plugins/timeline.py +13 -3
  179. jolt/plugins/volume.py +46 -48
  180. jolt/scheduler.py +591 -191
  181. jolt/tasks.py +1783 -245
  182. jolt/templates/export.sh.template +12 -6
  183. jolt/templates/timeline.html.template +44 -47
  184. jolt/timer.py +22 -0
  185. jolt/tools.py +749 -302
  186. jolt/utils.py +245 -18
  187. jolt/version.py +1 -1
  188. jolt/version_utils.py +2 -2
  189. jolt/xmldom.py +12 -2
  190. {jolt-0.9.76.dist-info → jolt-0.9.429.dist-info}/METADATA +98 -38
  191. jolt-0.9.429.dist-info/RECORD +207 -0
  192. {jolt-0.9.76.dist-info → jolt-0.9.429.dist-info}/WHEEL +1 -1
  193. jolt/plugins/amqp.py +0 -834
  194. jolt/plugins/debian.py +0 -338
  195. jolt/plugins/ftp.py +0 -181
  196. jolt/plugins/ninja-cache.py +0 -64
  197. jolt/plugins/ninjacli.py +0 -271
  198. jolt/plugins/repo.py +0 -253
  199. jolt-0.9.76.dist-info/RECORD +0 -79
  200. {jolt-0.9.76.dist-info → jolt-0.9.429.dist-info}/entry_points.txt +0 -0
  201. {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 contextlib import contextmanager
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
- ValueError: If the parameter is assigned an illegal value.
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 "{} {}".format(self._help, values) if self._help else values
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 ValueError(value)
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
- ValueError: If the parameter is assigned an illegal value.
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 ValueError("immutable")
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 ``info`` command on the
356
+ This text is displayed when running the ``inspect`` command on the
320
357
  associated task.
321
358
 
322
359
  Raises:
323
- ValueError: If the parameter is assigned an illegal value.
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(BooleanParameter, self).__init__(
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
- ValueError: If the parameter is assigned an illegal value.
383
+ ParameterValueError: If the parameter is assigned an illegal value.
347
384
  """
348
385
  value = str(value).lower()
349
- super(BooleanParameter, self).set_value(value)
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 ``info`` command on the
674
+ This text is displayed when running the ``inspect`` command on the
446
675
  associated task.
447
676
 
448
677
  Raises:
449
- ValueError: If the parameter is assigned an illegal value.
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
- ValueError: If the parameter is assigned an illegal value.
690
+ ParameterValueError: If the parameter is assigned an illegal value.
462
691
  """
463
- value = str(value).split("+") if type(value) == str else 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 ValueError(item)
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) == str:
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
- self.instances[task.qualified_name] = task
589
- self.instances[full_name] = task
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 requires(attrib):
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
- Decorates a task with an alternative ``requires`` attribute.
962
+ name = utils.canonical(name)
669
963
 
670
- The new attribute will be concatenated with the regular
671
- ``requires`` attribute.
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
- attrib (str): Name of alternative attribute.
675
- Keywords are expanded.
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
- return utils.concat_attributes("requires", attrib)
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
- setattr(cls, alias, property(_get))
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
- class TaskBase(object):
805
- """ Task base class. """
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
- cacheable = True
808
- """ Whether the task produces an artifact or not. """
1396
+ Args:
1397
+ seconds (int): Timeout in seconds.
809
1398
 
810
- expires = Immediately()
811
- """An expiration strategy, defining when the artifact may be evicted from the cache.
1399
+ Example:
812
1400
 
813
- When the size of the artifact cache exceeds the configured limit
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
- By default, an artifact expires immediately and may be evicted at any time
819
- (in LRU order). An exception to this rule is if the artifact is required by
820
- a task in the active task set. For example, if a task A requires the output
821
- of task B, B will never be evicted by A while A is being executed.
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
- def __init__(self, parameters=None, manifest=None, **kwargs):
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 ValueError as e:
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.get_task()
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
- def _get_parameter_objects(self, unset=False):
1078
- return self._parameters
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
- def _get_parameters(self, unset=False):
1081
- return {
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
- def _get_explicitly_set_parameters(self):
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
- def __str__(self):
1095
- return str(self.name)
2274
+ .. code-block:: python
1096
2275
 
1097
- @property
1098
- def canonical_name(self):
1099
- return utils.canonical(self.name)
2276
+ executable = self.command(
2277
+ "g++ {inputs} -o {output}",
2278
+ inputs=["main.cpp"],
2279
+ outputs=["executable"])
1100
2280
 
1101
- @property
1102
- def qualified_name(self):
1103
- return utils.format_task_name(
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
- @property
1108
- def short_qualified_name(self):
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
- def expand(self, string_or_list, *args, **kwargs):
1114
- """ Expands keyword arguments/macros in a format string.
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
- See :func:`jolt.Tools.expand` for details.
1117
- """
2293
+ command = self.expand(command, inputs=inputfiles, outputs=outputs, **kwargs)
2294
+ subtask = CommandSubtask(self, command)
1118
2295
 
1119
- try:
1120
- kwargs["_instance"] = self
1121
- if type(string_or_list) == list:
1122
- return [utils.expand(string, *args, **kwargs) for string in string_or_list]
1123
- return utils.expand(string_or_list, *args, **kwargs)
1124
- except KeyError as e:
1125
- raise_task_error(self, "Invalid macro '{0}' encountered - forgot to set a parameter?", e)
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
- @property
1128
- def identity(self):
1129
- return self._identity
2304
+ if message:
2305
+ subtask.set_message(self.expand(message, inputs=inputfiles, outputs=outputs, **kwargs))
1130
2306
 
1131
- @identity.setter
1132
- def identity(self, identity):
1133
- self._identity = identity
2307
+ self._add_subtask(subtask)
1134
2308
 
1135
- def is_cacheable(self):
1136
- return self.cacheable
2309
+ return subtask
1137
2310
 
1138
- def is_runnable(self):
1139
- return True
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
- class Task(TaskBase):
1143
- #: Path to the .jolt file where the task was defined.
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
- abstract = True
1146
- """ An abstract task class indended to be subclassed.
2322
+ Returns:
2323
+ Subtask object.
1147
2324
 
1148
- Abstract tasks can't be executed and won't be listed.
1149
- """
2325
+ Example:
1150
2326
 
1151
- def __init__(self, parameters=None, **kwargs):
1152
- super(Task, self).__init__(parameters, **kwargs)
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
- Log information about the task.
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
- fmt = self.tools.expand(fmt, *args, **kwargs)
1159
- log.info(fmt, *args, **kwargs)
2346
+ Create a subtask that creates a directory.
1160
2347
 
1161
- def warning(self, fmt, *args, **kwargs):
1162
- """ Log a warning concerning the task """
1163
- fmt = self.tools.expand(fmt, *args, **kwargs)
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
- def error(self, fmt, *args, **kwargs):
1167
- """ Log an error concerning the task """
1168
- fmt = self.tools.expand(fmt, *args, **kwargs)
1169
- log.error(fmt, *args, **kwargs)
2352
+ Returns:
2353
+ Subtask object.
1170
2354
 
1171
- def clean(self, tools):
1172
- """
1173
- Cleans up resources and intermediate files created by the task.
2355
+ Example:
1174
2356
 
1175
- The method is invoked in response to the user running clean
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
- An implementation must not clean any local or remote artifact cache.
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 run(self, deps, tools):
2367
+ def mkdirname(self, path, *args, **kwargs):
1184
2368
  """
1185
- Performs the work of the task.
2369
+ Create a subtask that creates a parent directory.
1186
2370
 
1187
- Dependencies specified with "requires" are passed as the
1188
- deps dictionary. The tools argument provides a set of low
1189
- level tool functions that may be useful.
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
- .. code-block:: python
2375
+ Returns:
2376
+ Subtask object.
1192
2377
 
1193
- with tools.cwd("path/to/subdir"):
1194
- tools.run("make {target}")
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 publish(self, artifact, tools):
2390
+ def render(self, template, outputs, **kwargs):
1201
2391
  """
1202
- Publishes files produced by :func:`~run`.
2392
+ Create a subtask that renders a Jinja template string to file.
1203
2393
 
1204
- Files can be collected in to the artifact by calling
1205
- artifact.collect().
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
- Additional metadata can be provided, such as environment
1208
- variables that should be set whenever the task artifact is
1209
- consumed. Example:
2399
+ Returns:
2400
+ Subtask object.
1210
2401
 
1211
- .. code-block:: python
2402
+ Example:
1212
2403
 
1213
- # Append <artifact-path>/bin to the PATH
1214
- artifact.environ.PATH.append("bin")
2404
+ .. code-block:: python
1215
2405
 
1216
- # Pass an arbitrary string to a consumer
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
- Unpacks files published by publish() .
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
- The intention of this hook is to make necessary adjustments
1226
- to artifact files and directories once they have been downloaded
1227
- into the local cache on a different machine. For example,
1228
- paths may have to be adjusted or an installer executed.
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
- This hook is executed in the context of a consuming task.
1231
- """
1232
- raise NotImplementedError()
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
- Invoked to start a debug shell.
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
- The method prepares the environment with attributes exported by task requirement
1239
- artifacts. The shell is entered by passing the ``-g`` flag to the build command.
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
- with tools.environ(PS1="jolt$ ") as env:
1244
- from jolt import config
1245
- subprocess.call(config.get_shell().split(), env=env, cwd=tools._cwd)
2473
+ pass
1246
2474
 
1247
- @contextmanager
1248
- def report(self):
2475
+ def run(self, deps, tools):
1249
2476
  """
1250
- Provide error analysis for task.
2477
+ Executes subtasks defined in :func:`~generate`.
1251
2478
 
1252
- Intentionally undocumented. Use at own risk.
2479
+ This method should typically not be overridden in subclasses.
1253
2480
  """
1254
- yield ReportProxy(self, self._report)
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
- type,
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
- with self._task.tools.cwd(reldir):
1321
- try:
1322
- details = self._task.tools.read_file(error["file"])
1323
- details = details.splitlines()
1324
- details = str(error["line"]) + ": " + details[int(error["line"]) - 1]
1325
- except Exception:
1326
- details = ""
1327
- self.add_error(type, error.get("location", ""), message, details)
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("\"(.*?\", line [0-9]+, in .*?)\n", tb[1])
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 = str(exc)
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(Resource, self).__init__(*args, **kwargs)
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(WorkspaceResource, self).__init__(*args, **kwargs)
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 acquire_ws(self):
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) == tuple:
3131
+ if type(files) is tuple:
1593
3132
  artifact.collect(*files, symlinks=self.symlinks)
1594
- elif type(files) == dict:
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
- script = [line[4:] for line in script]
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) == tuple:
3226
+ if type(files) is tuple:
1687
3227
  artifact.collect(*files)
1688
- elif type(files) == dict:
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
- pattern = Parameter(required=False, help="Test-case filter wildcard.")
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) != list, "Test.parameterized() expects a list as argument")
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.pattern.is_unset() or fnmatch.fnmatch(test, str(self.pattern)):
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 ResourceAttributeSetProvider(ArtifactAttributeSetProvider):
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.get_task()
1982
- if isinstance(resource, Resource):
1983
- from inspect import signature
3521
+ resource = artifact.task
3522
+ node = artifact.get_node()
3523
+ if not node.is_workspace_resource():
3524
+ return
1984
3525
 
1985
- deps = resource._run_env
1986
- deps.__enter__()
1987
- sig = signature(resource.acquire)
1988
- try:
1989
- ba = sig.bind_partial(artifact=artifact, deps=deps, tools=resource.tools, owner=task)
1990
- acquire = resource.acquire
1991
- except Exception:
1992
- ba = sig.bind_partial(artifact, deps, resource.tools)
1993
- acquire = utils.deprecated(resource.acquire)
1994
- acquire(*ba.args, **ba.kwargs)
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.get_task()
1998
- if isinstance(resource, Resource):
1999
- from inspect import signature
3538
+ resource = artifact.task
3539
+ node = artifact.get_node()
3540
+ if not node.is_workspace_resource():
3541
+ return
2000
3542
 
2001
- deps = resource._run_env
2002
- sig = signature(resource.release)
2003
- try:
2004
- ba = sig.bind_partial(artifact=artifact, deps=deps, tools=resource.tools, owner=task)
2005
- release = resource.release
2006
- except Exception:
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)