jolt 0.9.123__py3-none-any.whl → 0.9.435__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 (196) hide show
  1. jolt/__init__.py +80 -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 +832 -362
  6. jolt/chroot.py +156 -0
  7. jolt/cli.py +281 -162
  8. jolt/common_pb2.py +63 -0
  9. jolt/common_pb2_grpc.py +4 -0
  10. jolt/config.py +98 -41
  11. jolt/error.py +19 -4
  12. jolt/filesystem.py +2 -6
  13. jolt/graph.py +705 -117
  14. jolt/hooks.py +43 -0
  15. jolt/influence.py +122 -3
  16. jolt/loader.py +369 -121
  17. jolt/log.py +225 -63
  18. jolt/manifest.py +28 -38
  19. jolt/options.py +35 -10
  20. jolt/pkgs/abseil.py +42 -0
  21. jolt/pkgs/asio.py +25 -0
  22. jolt/pkgs/autoconf.py +41 -0
  23. jolt/pkgs/automake.py +41 -0
  24. jolt/pkgs/b2.py +31 -0
  25. jolt/pkgs/boost.py +111 -0
  26. jolt/pkgs/boringssl.py +32 -0
  27. jolt/pkgs/busybox.py +39 -0
  28. jolt/pkgs/bzip2.py +43 -0
  29. jolt/pkgs/cares.py +29 -0
  30. jolt/pkgs/catch2.py +36 -0
  31. jolt/pkgs/cbindgen.py +17 -0
  32. jolt/pkgs/cista.py +19 -0
  33. jolt/pkgs/clang.py +44 -0
  34. jolt/pkgs/cli11.py +24 -0
  35. jolt/pkgs/cmake.py +48 -0
  36. jolt/pkgs/cpython.py +196 -0
  37. jolt/pkgs/crun.py +29 -0
  38. jolt/pkgs/curl.py +38 -0
  39. jolt/pkgs/dbus.py +18 -0
  40. jolt/pkgs/double_conversion.py +24 -0
  41. jolt/pkgs/fastfloat.py +21 -0
  42. jolt/pkgs/ffmpeg.py +28 -0
  43. jolt/pkgs/flatbuffers.py +29 -0
  44. jolt/pkgs/fmt.py +27 -0
  45. jolt/pkgs/fstree.py +20 -0
  46. jolt/pkgs/gflags.py +18 -0
  47. jolt/pkgs/glib.py +18 -0
  48. jolt/pkgs/glog.py +25 -0
  49. jolt/pkgs/glslang.py +21 -0
  50. jolt/pkgs/golang.py +16 -11
  51. jolt/pkgs/googlebenchmark.py +18 -0
  52. jolt/pkgs/googletest.py +46 -0
  53. jolt/pkgs/gperf.py +15 -0
  54. jolt/pkgs/grpc.py +73 -0
  55. jolt/pkgs/hdf5.py +19 -0
  56. jolt/pkgs/help2man.py +14 -0
  57. jolt/pkgs/inja.py +28 -0
  58. jolt/pkgs/jsoncpp.py +31 -0
  59. jolt/pkgs/libarchive.py +43 -0
  60. jolt/pkgs/libcap.py +44 -0
  61. jolt/pkgs/libdrm.py +44 -0
  62. jolt/pkgs/libedit.py +42 -0
  63. jolt/pkgs/libevent.py +31 -0
  64. jolt/pkgs/libexpat.py +27 -0
  65. jolt/pkgs/libfastjson.py +21 -0
  66. jolt/pkgs/libffi.py +16 -0
  67. jolt/pkgs/libglvnd.py +30 -0
  68. jolt/pkgs/libogg.py +28 -0
  69. jolt/pkgs/libpciaccess.py +18 -0
  70. jolt/pkgs/libseccomp.py +21 -0
  71. jolt/pkgs/libtirpc.py +24 -0
  72. jolt/pkgs/libtool.py +42 -0
  73. jolt/pkgs/libunwind.py +35 -0
  74. jolt/pkgs/libva.py +18 -0
  75. jolt/pkgs/libvorbis.py +33 -0
  76. jolt/pkgs/libxml2.py +35 -0
  77. jolt/pkgs/libxslt.py +17 -0
  78. jolt/pkgs/libyajl.py +16 -0
  79. jolt/pkgs/llvm.py +81 -0
  80. jolt/pkgs/lua.py +54 -0
  81. jolt/pkgs/lz4.py +26 -0
  82. jolt/pkgs/m4.py +14 -0
  83. jolt/pkgs/make.py +17 -0
  84. jolt/pkgs/mesa.py +81 -0
  85. jolt/pkgs/meson.py +17 -0
  86. jolt/pkgs/mstch.py +28 -0
  87. jolt/pkgs/mysql.py +60 -0
  88. jolt/pkgs/nasm.py +49 -0
  89. jolt/pkgs/ncurses.py +30 -0
  90. jolt/pkgs/ng_log.py +25 -0
  91. jolt/pkgs/ninja.py +45 -0
  92. jolt/pkgs/nlohmann_json.py +25 -0
  93. jolt/pkgs/nodejs.py +19 -11
  94. jolt/pkgs/opencv.py +24 -0
  95. jolt/pkgs/openjdk.py +26 -0
  96. jolt/pkgs/openssl.py +103 -0
  97. jolt/pkgs/paho.py +76 -0
  98. jolt/pkgs/patchelf.py +16 -0
  99. jolt/pkgs/perl.py +42 -0
  100. jolt/pkgs/pkgconfig.py +64 -0
  101. jolt/pkgs/poco.py +39 -0
  102. jolt/pkgs/protobuf.py +77 -0
  103. jolt/pkgs/pugixml.py +27 -0
  104. jolt/pkgs/python.py +19 -0
  105. jolt/pkgs/qt.py +35 -0
  106. jolt/pkgs/rapidjson.py +26 -0
  107. jolt/pkgs/rapidyaml.py +28 -0
  108. jolt/pkgs/re2.py +30 -0
  109. jolt/pkgs/re2c.py +17 -0
  110. jolt/pkgs/readline.py +15 -0
  111. jolt/pkgs/rust.py +41 -0
  112. jolt/pkgs/sdl.py +28 -0
  113. jolt/pkgs/simdjson.py +27 -0
  114. jolt/pkgs/soci.py +46 -0
  115. jolt/pkgs/spdlog.py +29 -0
  116. jolt/pkgs/spirv_llvm.py +21 -0
  117. jolt/pkgs/spirv_tools.py +24 -0
  118. jolt/pkgs/sqlite.py +83 -0
  119. jolt/pkgs/ssl.py +12 -0
  120. jolt/pkgs/texinfo.py +15 -0
  121. jolt/pkgs/tomlplusplus.py +22 -0
  122. jolt/pkgs/wayland.py +26 -0
  123. jolt/pkgs/x11.py +58 -0
  124. jolt/pkgs/xerces_c.py +20 -0
  125. jolt/pkgs/xorg.py +360 -0
  126. jolt/pkgs/xz.py +29 -0
  127. jolt/pkgs/yamlcpp.py +30 -0
  128. jolt/pkgs/zeromq.py +47 -0
  129. jolt/pkgs/zlib.py +87 -0
  130. jolt/pkgs/zstd.py +33 -0
  131. jolt/plugins/alias.py +3 -0
  132. jolt/plugins/allure.py +5 -2
  133. jolt/plugins/autotools.py +66 -0
  134. jolt/plugins/cache.py +133 -0
  135. jolt/plugins/cmake.py +74 -6
  136. jolt/plugins/conan.py +238 -0
  137. jolt/plugins/cxx.py +698 -0
  138. jolt/plugins/cxxinfo.py +7 -0
  139. jolt/plugins/dashboard.py +1 -1
  140. jolt/plugins/docker.py +80 -23
  141. jolt/plugins/email.py +2 -2
  142. jolt/plugins/email.xslt +144 -101
  143. jolt/plugins/environ.py +11 -0
  144. jolt/plugins/fetch.py +141 -0
  145. jolt/plugins/gdb.py +39 -19
  146. jolt/plugins/gerrit.py +1 -14
  147. jolt/plugins/git.py +283 -85
  148. jolt/plugins/googletest.py +2 -1
  149. jolt/plugins/http.py +36 -38
  150. jolt/plugins/libtool.py +63 -0
  151. jolt/plugins/linux.py +990 -0
  152. jolt/plugins/logstash.py +4 -4
  153. jolt/plugins/meson.py +61 -0
  154. jolt/plugins/ninja-compdb.py +99 -30
  155. jolt/plugins/ninja.py +468 -166
  156. jolt/plugins/paths.py +11 -1
  157. jolt/plugins/pkgconfig.py +219 -0
  158. jolt/plugins/podman.py +136 -92
  159. jolt/plugins/python.py +137 -0
  160. jolt/plugins/remote_execution/__init__.py +0 -0
  161. jolt/plugins/remote_execution/administration_pb2.py +46 -0
  162. jolt/plugins/remote_execution/administration_pb2_grpc.py +170 -0
  163. jolt/plugins/remote_execution/log_pb2.py +32 -0
  164. jolt/plugins/remote_execution/log_pb2_grpc.py +68 -0
  165. jolt/plugins/remote_execution/scheduler_pb2.py +41 -0
  166. jolt/plugins/remote_execution/scheduler_pb2_grpc.py +141 -0
  167. jolt/plugins/remote_execution/worker_pb2.py +38 -0
  168. jolt/plugins/remote_execution/worker_pb2_grpc.py +112 -0
  169. jolt/plugins/report.py +12 -2
  170. jolt/plugins/rust.py +25 -0
  171. jolt/plugins/scheduler.py +710 -0
  172. jolt/plugins/selfdeploy/setup.py +8 -4
  173. jolt/plugins/selfdeploy.py +138 -88
  174. jolt/plugins/strings.py +35 -22
  175. jolt/plugins/symlinks.py +26 -11
  176. jolt/plugins/telemetry.py +5 -2
  177. jolt/plugins/timeline.py +13 -3
  178. jolt/plugins/volume.py +46 -48
  179. jolt/scheduler.py +589 -192
  180. jolt/tasks.py +625 -121
  181. jolt/templates/timeline.html.template +44 -47
  182. jolt/timer.py +22 -0
  183. jolt/tools.py +638 -282
  184. jolt/utils.py +211 -7
  185. jolt/version.py +1 -1
  186. jolt/xmldom.py +12 -2
  187. {jolt-0.9.123.dist-info → jolt-0.9.435.dist-info}/METADATA +97 -38
  188. jolt-0.9.435.dist-info/RECORD +207 -0
  189. {jolt-0.9.123.dist-info → jolt-0.9.435.dist-info}/WHEEL +1 -1
  190. jolt/plugins/amqp.py +0 -834
  191. jolt/plugins/debian.py +0 -338
  192. jolt/plugins/ftp.py +0 -181
  193. jolt/plugins/repo.py +0 -253
  194. jolt-0.9.123.dist-info/RECORD +0 -77
  195. {jolt-0.9.123.dist-info → jolt-0.9.435.dist-info}/entry_points.txt +0 -0
  196. {jolt-0.9.123.dist-info → jolt-0.9.435.dist-info}/top_level.txt +0 -0
jolt/tasks.py CHANGED
@@ -7,10 +7,13 @@ import fnmatch
7
7
  import functools
8
8
  import hashlib
9
9
  import platform
10
+ from threading import RLock
10
11
  import subprocess
11
12
  from os import environ
13
+ from os import sys as os_sys
12
14
  import sys
13
15
  import unittest as ut
16
+ from urllib.parse import urlparse
14
17
  import uuid
15
18
  import re
16
19
  import traceback
@@ -21,10 +24,11 @@ from jolt import utils
21
24
  from jolt.cache import ArtifactAttributeSetProvider
22
25
  from jolt.error import raise_error_if, raise_task_error, raise_task_error_if
23
26
  from jolt.error import raise_unreported_task_error_if
24
- from jolt.error import JoltError, JoltCommandError
27
+ from jolt.error import JoltError, JoltCommandError, LoggedJoltError
25
28
  from jolt.expires import Immediately
26
29
  from jolt.influence import FileInfluence, TaintInfluenceProvider
27
30
  from jolt.influence import TaskClassSourceInfluence
31
+ from jolt.influence import CallbackInfluence
28
32
  from jolt.influence import attribute as attribute_influence
29
33
  from jolt.influence import environ as environ_influence
30
34
  from jolt.influence import source as source_influence
@@ -133,7 +137,7 @@ class Parameter(object):
133
137
  self.name = name
134
138
 
135
139
  def __init__(self, default=None, values=None, required=True,
136
- const=False, influence=True, help=None):
140
+ const=False, influence=True, help=None, valuesfn=None):
137
141
  """
138
142
  Creates a new parameter.
139
143
 
@@ -141,6 +145,10 @@ class Parameter(object):
141
145
  default (str, optional): An optional default value.
142
146
  values (list, optional): A list of accepted values. An
143
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.
144
152
  required (boolean, optional): If required, the parameter must be assigned
145
153
  a value before the task can be executed. The default is ``True``.
146
154
  const (boolean, optional): If const is True, the parameter is immutable
@@ -162,6 +170,7 @@ class Parameter(object):
162
170
  self._default = default
163
171
  self._value = default
164
172
  self._accepted_values = values
173
+ self._accepted_values_fn = valuesfn
165
174
  self._required = required
166
175
  self._const = const
167
176
  self._influence = influence
@@ -188,7 +197,7 @@ class Parameter(object):
188
197
  def highlight(value):
189
198
  return colors.bright(value) if self._is_default(value) else colors.dim(value)
190
199
 
191
- 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 ""
192
201
 
193
202
  def __str__(self):
194
203
  """ Returns the parameter value as a string """
@@ -197,6 +206,8 @@ class Parameter(object):
197
206
  def _validate(self, value, what=None):
198
207
  if self._accepted_values is not None and value not in self._accepted_values:
199
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)
200
211
 
201
212
  def get_default(self):
202
213
  """ Get the default value of the parameter.
@@ -350,7 +361,7 @@ class BooleanParameter(Parameter):
350
361
 
351
362
  """
352
363
  default = str(default).lower() if default is not None else None
353
- super(BooleanParameter, self).__init__(
364
+ super().__init__(
354
365
  default,
355
366
  values=["false", "true", "0", "1", "no", "yes"],
356
367
  required=required,
@@ -372,7 +383,7 @@ class BooleanParameter(Parameter):
372
383
  ParameterValueError: If the parameter is assigned an illegal value.
373
384
  """
374
385
  value = str(value).lower()
375
- super(BooleanParameter, self).set_value(value)
386
+ super().set_value(value)
376
387
 
377
388
  @property
378
389
  def is_true(self):
@@ -434,7 +445,7 @@ class IntParameter(Parameter):
434
445
  """
435
446
 
436
447
  def __init__(self, default=None, min=None, max=None, values=None, required=True, const=False,
437
- influence=True, help=None):
448
+ influence=True, help=None, valuesfn=None):
438
449
  """
439
450
  Creates a new parameter.
440
451
 
@@ -482,7 +493,8 @@ class IntParameter(Parameter):
482
493
  required=required,
483
494
  const=const,
484
495
  influence=influence,
485
- help=help)
496
+ help=help,
497
+ valuesfn=valuesfn)
486
498
 
487
499
  def _validate(self, value, what=None):
488
500
  if self._min is not None and value < self._min:
@@ -686,6 +698,10 @@ class ListParameter(Parameter):
686
698
  for item in value:
687
699
  if item not in self._accepted_values:
688
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)
689
705
 
690
706
  def get_value(self):
691
707
  return "+".join(self._value)
@@ -767,6 +783,7 @@ class TaskRegistry(object):
767
783
  self.env = env
768
784
  self.tasks = {}
769
785
  self.instances = {}
786
+ self._workspace_resources = []
770
787
 
771
788
  @staticmethod
772
789
  def get(*args, **kwargs):
@@ -775,6 +792,21 @@ class TaskRegistry(object):
775
792
  return TaskRegistry._instance
776
793
 
777
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
+
778
810
  self.tasks[cls.name] = cls
779
811
 
780
812
  def add_task(self, task, extra_params):
@@ -783,13 +815,20 @@ class TaskRegistry(object):
783
815
  full_name = utils.format_task_name(name, params)
784
816
  self.instances[full_name] = task
785
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
+
786
825
  def get_task_class(self, name):
787
826
  return self.tasks.get(name)
788
827
 
789
828
  def get_task_classes(self):
790
829
  return list(self.tasks.values())
791
830
 
792
- def get_task(self, name, extra_params=None, manifest=None):
831
+ def get_task(self, name, extra_params=None, manifest=None, buildenv=None):
793
832
  name, params = utils.parse_task_name(name)
794
833
  params.update(extra_params or {})
795
834
  full_name = utils.format_task_name(name, params)
@@ -800,14 +839,19 @@ class TaskRegistry(object):
800
839
 
801
840
  cls = self.tasks.get(name)
802
841
  if cls:
803
- task = cls(parameters=params, manifest=manifest)
842
+ task = cls(parameters=params, manifest=manifest, buildenv=buildenv)
804
843
  task = self.instances.get(task.qualified_name, task)
805
- self.instances[task.qualified_name] = task
806
- 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
807
847
  return task
808
848
 
809
849
  raise_task_error_if(not task, full_name, "No such task")
810
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
+
811
855
  def set_default_parameters(self, task):
812
856
  name, params = utils.parse_task_name(task)
813
857
 
@@ -880,18 +924,195 @@ class TaskGenerator(object):
880
924
 
881
925
  class attributes:
882
926
  @staticmethod
883
- 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
+
884
961
  """
885
- Decorates a task with an alternative ``requires`` attribute.
962
+ name = utils.canonical(name)
886
963
 
887
- The new attribute will be concatenated with the regular
888
- ``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.
889
1015
 
890
1016
  Args:
891
- attrib (str): Name of alternative attribute.
892
- 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
893
1030
  """
894
- 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
895
1116
 
896
1117
  @staticmethod
897
1118
  def attribute(alias, target, influence=True, default=False):
@@ -995,6 +1216,79 @@ class attributes:
995
1216
  return utils.concat_attributes("_publish_files", attrib)(cls)
996
1217
  return decorate
997
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
+
998
1292
  @staticmethod
999
1293
  def load(filepath):
1000
1294
  """
@@ -1066,6 +1360,20 @@ class attributes:
1066
1360
  return cls
1067
1361
  return _decorate
1068
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
+
1069
1377
  @staticmethod
1070
1378
  def system(cls):
1071
1379
  """
@@ -1077,6 +1385,40 @@ class attributes:
1077
1385
  cls.system = property(lambda t: t._system.value)
1078
1386
  return cls
1079
1387
 
1388
+ @staticmethod
1389
+ def timeout(seconds):
1390
+ """
1391
+ Decorator setting a timeout for a task.
1392
+
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.
1395
+
1396
+ Args:
1397
+ seconds (int): Timeout in seconds.
1398
+
1399
+ Example:
1400
+
1401
+ .. code-block:: python
1402
+
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
+
1080
1422
 
1081
1423
  class TaskBase(object):
1082
1424
  """ Task base class. """
@@ -1160,9 +1502,37 @@ class TaskBase(object):
1160
1502
  joltproject = None
1161
1503
  """ Name of project this task belongs to. """
1162
1504
 
1505
+ local = False
1506
+ """ A local task is only executed on the local node where the build is initiated. """
1507
+
1163
1508
  name = None
1164
1509
  """ Name of the task. Derived from class name if not set. """
1165
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
+
1166
1536
  requires = []
1167
1537
  """ List of dependencies to other tasks. """
1168
1538
 
@@ -1209,7 +1579,15 @@ class TaskBase(object):
1209
1579
  execution requests, the instance ID won't be.
1210
1580
  """
1211
1581
 
1212
- 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):
1213
1591
  self._identity = None
1214
1592
  self._report = _JoltTask()
1215
1593
  self.name = self.__class__.name
@@ -1230,6 +1608,7 @@ class TaskBase(object):
1230
1608
  self.selfsustained = utils.call_or_return(self, self.__class__._selfsustained)
1231
1609
  self.tools = Tools(self, self.joltdir)
1232
1610
  self._apply_manifest(manifest)
1611
+ self._apply_protobuf(buildenv)
1233
1612
  self.requires = self.expand(self.requires)
1234
1613
 
1235
1614
  def _apply_manifest(self, manifest):
@@ -1246,6 +1625,26 @@ class TaskBase(object):
1246
1625
  .format(attrib.name, self.qualified_name)
1247
1626
  export.assign(attrib.value)
1248
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
+
1249
1648
  def _influence(self):
1250
1649
  return utils.as_list(self.__class__.influence)
1251
1650
 
@@ -1305,6 +1704,9 @@ class TaskBase(object):
1305
1704
  "Required parameter '{0}' has not been set", key)
1306
1705
 
1307
1706
  def _verify_influence(self, deps, artifact, tools, sources=None):
1707
+ if "configurationrepository" in self.name:
1708
+ breakpoint()
1709
+
1308
1710
  # Verify that any transformed sources are influencing
1309
1711
  sources = set(map(tools.expand_path, sources or []))
1310
1712
 
@@ -1321,29 +1723,29 @@ class TaskBase(object):
1321
1723
  return not fs.is_relative_to(fname, rootpath)
1322
1724
  return _filter
1323
1725
 
1726
+ # Ignore any files in build directories
1727
+ sources = filter(_subpath_filter(tools.expand_path(tools.buildroot)), sources)
1728
+
1324
1729
  for _, dep in deps.items():
1325
- deptask = dep.get_task()
1730
+ deptask = dep.task
1326
1731
  if isinstance(deptask, FileInfluence):
1327
1732
  # Resource dependencies may cover the influence implicitly
1328
1733
  deppath = self.tools.expand_path(str(deptask.path))
1329
- sources = set(filter(lambda d: not deptask.is_influenced_by(self, d), sources))
1734
+ sources = filter(lambda d, dt=deptask: not dt.is_influenced_by(self, d), sources)
1330
1735
  else:
1331
1736
  # Ignore any files in artifacts
1332
1737
  deppath = self.tools.expand_path(dep.path)
1333
- sources = set(filter(_subpath_filter(deppath), sources))
1334
-
1335
- # Ignore any files in build directories
1336
- sources = filter(_subpath_filter(tools.expand_path(tools.buildroot)), sources)
1337
- sources = set(sources)
1738
+ sources = filter(_subpath_filter(deppath), sources)
1338
1739
 
1339
1740
  for ip in self.influence:
1340
1741
  if not isinstance(ip, FileInfluence):
1341
1742
  continue
1342
- ok = [source for source in sources if ip.is_influenced_by(self, source)]
1343
- sources.difference_update(ok)
1743
+ sources = {source for source in sources if not ip.is_influenced_by(self, source)}
1744
+
1344
1745
  for source in sources:
1345
1746
  log.warning("Missing influence: {} ({})", source, self.name)
1346
- raise_task_error_if(sources, self, "Task is missing source influence")
1747
+
1748
+ raise_task_error_if(set(sources), self, "Task is missing source influence")
1347
1749
 
1348
1750
  def _get_export_objects(self):
1349
1751
  return self._exports
@@ -1384,6 +1786,16 @@ class TaskBase(object):
1384
1786
  self.name,
1385
1787
  self._get_explicitly_set_parameters())
1386
1788
 
1789
+ @property
1790
+ def exported_name(self):
1791
+ if hasattr(self, "_exported_name"):
1792
+ return self._exported_name
1793
+ return self.short_qualified_name
1794
+
1795
+ @exported_name.setter
1796
+ def exported_name(self, name):
1797
+ self._exported_name = name
1798
+
1387
1799
  def expand(self, string_or_list, *args, **kwargs):
1388
1800
  """ Expands keyword arguments/macros in a format string.
1389
1801
 
@@ -1422,8 +1834,15 @@ class Task(TaskBase):
1422
1834
  Abstract tasks can't be executed and won't be listed.
1423
1835
  """
1424
1836
 
1837
+ unstable = False
1838
+ """
1839
+ An unstable task is allowed to fail without stopping or failing the entire build.
1840
+
1841
+ The unstable task is still reported as a failure at the end of the build.
1842
+ """
1843
+
1425
1844
  def __init__(self, parameters=None, **kwargs):
1426
- super(Task, self).__init__(parameters, **kwargs)
1845
+ super().__init__(parameters, **kwargs)
1427
1846
 
1428
1847
  def info(self, fmt, *args, **kwargs):
1429
1848
  """
@@ -1478,6 +1897,9 @@ class Task(TaskBase):
1478
1897
  as ``target`` above, are automatically expanded to their values.
1479
1898
  """
1480
1899
 
1900
+ def nopublish(self, artifact, tools):
1901
+ raise NotImplementedError()
1902
+
1481
1903
  def publish(self, artifact, tools):
1482
1904
  """
1483
1905
  Publishes files produced by :func:`~run`.
@@ -1512,7 +1934,7 @@ class Task(TaskBase):
1512
1934
  """
1513
1935
  raise NotImplementedError()
1514
1936
 
1515
- def shell(self, deps, tools):
1937
+ def debugshell(self, deps, tools):
1516
1938
  """
1517
1939
  Invoked to start a debug shell.
1518
1940
 
@@ -1543,7 +1965,7 @@ class SubTask(object):
1543
1965
  self._message = None
1544
1966
  self._outputs = []
1545
1967
  self._task = task
1546
- self._tools = Tools(task, task.joltdir)
1968
+ self._tools = copy.copy(task.tools)
1547
1969
 
1548
1970
  def __str__(self):
1549
1971
  if self.message:
@@ -1629,15 +2051,21 @@ class SubTask(object):
1629
2051
  self._influence.append(infl)
1630
2052
 
1631
2053
  def add_influence_file(self, path):
2054
+ path = self._tools.expand_path(path)
1632
2055
  self.add_influence(utils.filesha1(path))
1633
2056
 
1634
2057
  def add_influence_depfile(self, path):
1635
2058
  def depfile():
1636
2059
  result = ""
1637
- deps = self._tools.read_depfile(fs.path.join(self._task.joltdir, path))
1638
- for output in self.outputs:
1639
- for input in deps.get(output, []):
1640
- result += utils.filesha1(input)
2060
+ try:
2061
+ deps = self._tools.read_depfile(fs.path.join(self._task.joltdir, path))
2062
+ except OSError:
2063
+ return "N/A"
2064
+ with self._tools.cwd(self._task.joltdir):
2065
+ for output in self.outputs:
2066
+ for input in deps.get(output, []):
2067
+ input = self._tools.expand_path(input)
2068
+ result += utils.filesha1(input)
1641
2069
  return result
1642
2070
  self.add_influence(depfile)
1643
2071
 
@@ -1683,7 +2111,7 @@ class CommandSubtask(SubTask):
1683
2111
 
1684
2112
  def __str__(self):
1685
2113
  s = super().__str__()
1686
- return s if s is not None and not log.is_verbose() else self._command
2114
+ return s if s is not None and not log.is_verbose() else self._tools.expand(self._command)
1687
2115
 
1688
2116
  def run(self):
1689
2117
  self._tools.run(self._command)
@@ -1770,11 +2198,11 @@ class MultiTask(Task):
1770
2198
 
1771
2199
  def __init__(self, *args, **kwargs):
1772
2200
  super().__init__(*args, **kwargs)
1773
- self._subtasks = []
2201
+ self._subtasks = set()
1774
2202
  self._subtasks_by_output = {}
1775
2203
 
1776
2204
  def _add_subtask(self, subtask):
1777
- self._subtasks.append(subtask)
2205
+ self._subtasks.add(subtask)
1778
2206
 
1779
2207
  for output in subtask.outputs:
1780
2208
  if output in self._subtasks_by_output:
@@ -1799,19 +2227,22 @@ class MultiTask(Task):
1799
2227
  inputsubtask = Input(self, input)
1800
2228
  else:
1801
2229
  inputsubtask = input
1802
- self._subtasks.append(inputsubtask)
2230
+ self._subtasks.add(inputsubtask)
1803
2231
  self._subtasks_by_output[input] = inputsubtask
1804
2232
  return inputsubtask
1805
2233
 
1806
2234
  def _to_subtask_list(self, inputs):
1807
2235
  inputs = utils.as_list(inputs)
1808
- return [self._add_input(input) for input in inputs]
2236
+ return utils.unique_list([self._add_input(input) for input in inputs])
1809
2237
 
1810
2238
  def _to_output_files(self, subtasks):
1811
2239
  subtasks = utils.as_list(subtasks)
1812
2240
  outputs = []
1813
2241
  for subtask in subtasks:
1814
- outputs.extend(subtask.outputs)
2242
+ if isinstance(subtask, SubTask):
2243
+ outputs.extend(subtask.outputs)
2244
+ else:
2245
+ outputs.append(subtask)
1815
2246
  return outputs
1816
2247
 
1817
2248
  def _to_input_subtasks(self, inputs, **kwargs):
@@ -1851,12 +2282,11 @@ class MultiTask(Task):
1851
2282
  outputs=["executable"])
1852
2283
 
1853
2284
  """
1854
- inputs = self._to_input_subtasks(inputs, **kwargs)
2285
+ input_jobs = self._to_input_subtasks(inputs, **kwargs)
1855
2286
  inputfiles = self._to_output_files(inputs)
1856
2287
 
1857
2288
  outputs = utils.as_list(outputs)
1858
- outputs = [self.expand(output, **kwargs) for output in outputs]
1859
- outputs = [fs.path.relpath(output) for output in outputs]
2289
+ outputs = [self.tools.expand_relpath(output, self.joltdir, **kwargs) for output in outputs]
1860
2290
 
1861
2291
  dirs = set()
1862
2292
  if mkdir:
@@ -1868,7 +2298,7 @@ class MultiTask(Task):
1868
2298
 
1869
2299
  for dir in dirs:
1870
2300
  subtask.add_dependency(dir)
1871
- for input in inputs:
2301
+ for input in input_jobs:
1872
2302
  subtask.add_dependency(input)
1873
2303
  for output in outputs:
1874
2304
  output = self.expand(output, inputs=inputfiles, outputs=outputs, **kwargs)
@@ -2051,7 +2481,6 @@ class MultiTask(Task):
2051
2481
 
2052
2482
  This method should typically not be overridden in subclasses.
2053
2483
  """
2054
-
2055
2484
  self.generate(deps, tools)
2056
2485
 
2057
2486
  log.debug("About to start executing these subtasks:")
@@ -2070,6 +2499,7 @@ class MultiTask(Task):
2070
2499
  for subtask in self._subtasks:
2071
2500
  if subtask not in subtasks:
2072
2501
  subtasks[subtask] = []
2502
+ if subtask not in deps:
2073
2503
  deps[subtask] = []
2074
2504
  for dep in subtask.dependencies:
2075
2505
  if dep not in deps:
@@ -2079,12 +2509,18 @@ class MultiTask(Task):
2079
2509
 
2080
2510
  # Prune up-to-date subtasks
2081
2511
  for subtask in list(filter(lambda subtask: not subtask.is_outdated, subtasks.keys())):
2512
+ log.debug("Pruning {}", subtask)
2082
2513
  del subtasks[subtask]
2083
2514
  for dep in deps[subtask]:
2084
- subtasks[dep].remove(subtask)
2515
+ try:
2516
+ subtasks[dep].remove(subtask)
2517
+ except KeyError:
2518
+ pass
2519
+
2520
+ self.subtaskindex = 0
2521
+ self.subtaskcount = len(subtasks)
2085
2522
 
2086
- subtaskindex = 0
2087
- subtaskcount = len(subtasks)
2523
+ lock = RLock()
2088
2524
 
2089
2525
  with ThreadPoolExecutor(max_workers=tools.cpu_count()) as pool:
2090
2526
  futures = {}
@@ -2097,11 +2533,14 @@ class MultiTask(Task):
2097
2533
  break
2098
2534
 
2099
2535
  for subtask in candidates:
2100
- subtaskindex += 1
2101
2536
  del subtasks[subtask]
2102
2537
  if subtask.is_outdated:
2103
- log.info("[{}/{}] {}", subtaskindex, subtaskcount, str(subtask))
2104
- futures[pool.submit(subtask.run)] = subtask
2538
+ def runner(subtask):
2539
+ with lock:
2540
+ self.subtaskindex += 1
2541
+ log.info("[{}/{}] {}", self.subtaskindex, self.subtaskcount, str(subtask))
2542
+ subtask.run()
2543
+ futures[pool.submit(functools.partial(runner, subtask))] = subtask
2105
2544
  else:
2106
2545
  completed.append(subtask)
2107
2546
 
@@ -2132,6 +2571,13 @@ class MultiTask(Task):
2132
2571
 
2133
2572
  raise_task_error_if(subtasks, self, "Subtasks with unresolved dependencies could not be executed")
2134
2573
 
2574
+ def inputs(self, jobs):
2575
+ return self._to_subtask_list(jobs)
2576
+
2577
+ def outputs(self, jobs):
2578
+ jobs = utils.as_list(jobs)
2579
+ return [output for job in jobs for output in job.outputs]
2580
+
2135
2581
 
2136
2582
  class Runner(Task):
2137
2583
  """
@@ -2212,9 +2658,9 @@ class Runner(Task):
2212
2658
  found = False
2213
2659
 
2214
2660
  for task, artifact in deps.items():
2215
- if not artifact.get_task().is_cacheable():
2661
+ if not artifact.task.is_cacheable():
2216
2662
  continue
2217
- if artifact.strings.executable.get_value() is None:
2663
+ if artifact.strings.executable is None:
2218
2664
  self.verbose("No executable found in task artifact for '{}'", task)
2219
2665
  continue
2220
2666
  with tools.cwd(artifact.path):
@@ -2233,6 +2679,22 @@ class ErrorProxy(object):
2233
2679
  def __init__(self, error):
2234
2680
  self._error = error
2235
2681
 
2682
+ @property
2683
+ def type(self):
2684
+ return self._error.type
2685
+
2686
+ @type.setter
2687
+ def type(self, value):
2688
+ self._error.type = value
2689
+
2690
+ @property
2691
+ def details(self):
2692
+ return self._error.details
2693
+
2694
+ @property
2695
+ def location(self):
2696
+ return self._error.location
2697
+
2236
2698
  @property
2237
2699
  def message(self):
2238
2700
  return self._error.message
@@ -2240,11 +2702,21 @@ class ErrorProxy(object):
2240
2702
 
2241
2703
  class ReportProxy(object):
2242
2704
  def __init__(self, task, report):
2705
+ from jolt import config
2243
2706
  self._task = task
2244
2707
  self._report = report
2708
+ self._max_errors = config.getint("jolt", "task_max_errors", 100)
2245
2709
 
2246
2710
  def add_error(self, type, location, message, details=""):
2247
2711
  """ Add an error to the build report. """
2712
+ if len(self.errors) >= self._max_errors:
2713
+ if not hasattr(self._report, "truncated"):
2714
+ error = self._report.create_error()
2715
+ error.type = "Error"
2716
+ error.message = "Too many errors, list truncated"
2717
+ self._report.truncated = True
2718
+ return None
2719
+
2248
2720
  error = self._report.create_error()
2249
2721
  error.type = type
2250
2722
  error.location = location
@@ -2265,11 +2737,8 @@ class ReportProxy(object):
2265
2737
  """
2266
2738
  for match in re.finditer(regex, logbuf, re.MULTILINE):
2267
2739
  error = match.groupdict()
2268
- self.add_error(
2269
- type,
2270
- error.get("location", ""),
2271
- error.get("message", ""),
2272
- error.get("details", ""))
2740
+ if not self.add_error(type, error.get("location", ""), error.get("message", ""), error.get("details", "")):
2741
+ break
2273
2742
 
2274
2743
  def add_regex_errors_with_file(self, type, regex, logbuf, reldir, filterfn=lambda n: True):
2275
2744
  """
@@ -2290,20 +2759,29 @@ class ReportProxy(object):
2290
2759
  if not filterfn(error):
2291
2760
  continue
2292
2761
  if error["location"] not in errors_by_location:
2293
- errors_by_location[error["location"]] = (error, [error["message"]])
2762
+ errors_by_location[error["location"]] = (error, [error["message"]], error.get("details", ""))
2294
2763
  else:
2295
2764
  errors_by_location[error["location"]][1].append(error["message"])
2296
2765
 
2297
- for error, msgs in errors_by_location.values():
2766
+ for error, msgs, details in errors_by_location.values():
2298
2767
  message = "\n".join(utils.unique_list(msgs))
2299
- with self._task.tools.cwd(reldir):
2300
- try:
2301
- details = self._task.tools.read_file(error["file"])
2302
- details = details.splitlines()
2303
- details = str(error["line"]) + ": " + details[int(error["line"]) - 1]
2304
- except Exception:
2305
- details = ""
2306
- self.add_error(type, error.get("location", ""), message, details)
2768
+ if not details:
2769
+ with self._task.tools.cwd(self._task.tools.wsroot):
2770
+ try:
2771
+ details = self._task.tools.read_file(error["file"])
2772
+ details = details.splitlines()
2773
+ details = str(error["line"]) + ": " + details[int(error["line"]) - 1]
2774
+ except Exception:
2775
+ details = ""
2776
+
2777
+ location = error.get("location", "")
2778
+ if location:
2779
+ with self._task.tools.cwd(self._task.tools.wsroot):
2780
+ location = self._task.tools.expand_path(location)
2781
+ location = self._task.tools.expand_relpath(location, self._task.tools.wsroot)
2782
+
2783
+ if not self.add_error(type, location, message, details):
2784
+ break
2307
2785
 
2308
2786
  def add_exception(self, exc, errtype=None, location=None):
2309
2787
  """
@@ -2317,13 +2795,14 @@ class ReportProxy(object):
2317
2795
 
2318
2796
  """
2319
2797
  tb = traceback.format_exception(type(exc), value=exc, tb=exc.__traceback__)
2798
+
2320
2799
  installdir = fs.path.dirname(__file__)
2321
2800
  if any(map(lambda frame: installdir not in frame, tb[1:-1])):
2322
2801
  while len(tb) > 2 and installdir in tb[1]:
2323
2802
  del tb[1]
2324
- loc = re.findall("\"(.*?\", line [0-9]+, in .*?)\n", tb[1])
2803
+ loc = re.findall("(\".*?\", line [0-9]+, in .*?)\n", tb[1])
2325
2804
  location = location or (loc[0] if loc and len(loc) > 0 else "")
2326
- message = str(exc)
2805
+ message = log.format_exception_msg(exc)
2327
2806
  if isinstance(exc, JoltCommandError):
2328
2807
  details = "\n".join(exc.stderr)
2329
2808
  elif isinstance(exc, JoltError):
@@ -2341,10 +2820,26 @@ class ReportProxy(object):
2341
2820
  def errors(self):
2342
2821
  return [ErrorProxy(error) for error in self._report.errors]
2343
2822
 
2823
+ @errors.setter
2824
+ def errors(self, errlist):
2825
+ assert all(isinstance(err, ErrorProxy) for err in errlist), "Invalid error list"
2826
+ self._report.clear_errors()
2827
+ for err in errlist:
2828
+ self.add_error(err.type, err.location, err.message, err.details)
2829
+
2344
2830
  @property
2345
2831
  def manifest(self):
2346
2832
  return self._report
2347
2833
 
2834
+ def raise_for_status(self, log_details=False, log_error=False):
2835
+ for error in self.errors:
2836
+ if log_error:
2837
+ log.error("{}: {}", error.type, error.message, context=self._task.identity[:7])
2838
+ if log_details:
2839
+ for line in error.details.splitlines():
2840
+ log.transfer(line, context=self._task.identity[:7])
2841
+ raise LoggedJoltError(JoltError(f"{error.type}: {error.message}"))
2842
+
2348
2843
 
2349
2844
  class Resource(Task):
2350
2845
  """
@@ -2358,13 +2853,17 @@ class Resource(Task):
2358
2853
 
2359
2854
  """
2360
2855
 
2361
- cacheable = False
2362
-
2363
2856
  abstract = True
2364
2857
  """ An abstract resource class indended to be subclassed. """
2365
2858
 
2859
+ release_on_error = False
2860
+ """ Call release if an exception occurs during acquire. """
2861
+
2366
2862
  def __init__(self, *args, **kwargs):
2367
- super(Resource, self).__init__(*args, **kwargs)
2863
+ super().__init__(*args, **kwargs)
2864
+
2865
+ def _artifacts(self, cache, node):
2866
+ return [cache.get_artifact(node, "main", session=True)]
2368
2867
 
2369
2868
  def is_runnable(self):
2370
2869
  return False
@@ -2425,17 +2924,25 @@ class WorkspaceResource(Resource):
2425
2924
  """
2426
2925
 
2427
2926
  def __init__(self, *args, **kwargs):
2428
- super(WorkspaceResource, self).__init__(*args, **kwargs)
2927
+ super().__init__(*args, **kwargs)
2429
2928
  raise_task_error_if(len(self.requires) > 0, self,
2430
2929
  "Workspace resource is not allowed to have requirements")
2431
2930
 
2432
- def acquire(self, **kwargs):
2931
+ def acquire(self, *args, **kwargs):
2433
2932
  return self.acquire_ws()
2434
2933
 
2435
- def release(self, **kwargs):
2934
+ def release(self, *args, **kwargs):
2436
2935
  return self.release_ws()
2437
2936
 
2438
- def acquire_ws(self):
2937
+ def prepare_ws_for(self, task):
2938
+ """ Called to prepare the workspace for a task.
2939
+
2940
+ An implementor overrides this method in a subclass. The method
2941
+ is called before the task influence is calculated and the workspace
2942
+ resource is acquired.
2943
+ """
2944
+
2945
+ def acquire_ws(self, force=False):
2439
2946
  """ Called to acquire the resource.
2440
2947
 
2441
2948
  An implementor overrides this method in a subclass. The acquired
@@ -2539,6 +3046,7 @@ class Download(Task):
2539
3046
  Once downloaded, archives are extracted and all of their files are published.
2540
3047
  If the file is not an archive it is published as is. Recognized archive extensions are:
2541
3048
 
3049
+ - .7z
2542
3050
  - .tar
2543
3051
  - .tar.bz2
2544
3052
  - .tar.gz
@@ -2601,7 +3109,7 @@ class Download(Task):
2601
3109
  return fs.posixpath.basename(url.path) or "file"
2602
3110
 
2603
3111
  def run(self, deps, tools):
2604
- supported_formats = [".tar", ".tar.bz2", ".tar.gz", ".tar.xz", ".tgz", ".zip"]
3112
+ supported_formats = [".7z", ".tar", ".tar.bz2", ".tar.gz", ".tar.xz", ".tgz", ".zip"]
2605
3113
 
2606
3114
  raise_task_error_if(not self.url, self, "No URL(s) specified")
2607
3115
 
@@ -2698,7 +3206,8 @@ class Script(Task):
2698
3206
  doc = self.__doc__.split("---", 1)
2699
3207
  script = doc[1] if len(doc) > 1 else doc[0]
2700
3208
  script = script.splitlines()
2701
- script = [line[4:] for line in script]
3209
+ if os_sys.version_info < (3, 13):
3210
+ script = [line[4:] for line in script]
2702
3211
  script = "\n".join(script)
2703
3212
  script = script.lstrip()
2704
3213
  if not script.startswith("#!"):
@@ -2825,7 +3334,7 @@ class Test(Task):
2825
3334
  Abstract test tasks can't be executed and won't be listed.
2826
3335
  """
2827
3336
 
2828
- pattern = Parameter(required=False, help="Test-case filter wildcard.")
3337
+ filter = Parameter(required=False, help="Test-case filter wildcard.")
2829
3338
 
2830
3339
  def __init__(self, *args, **kwargs):
2831
3340
  super().__init__(*args, **kwargs)
@@ -2854,24 +3363,21 @@ class Test(Task):
2854
3363
  """
2855
3364
  raise_error_if(type(args) is not list, "Test.parameterized() expects a list as argument")
2856
3365
 
2857
- class partialmethod(functools.partialmethod):
2858
- def __init__(self, index, func, *args):
2859
- super().__init__(func, *args)
2860
- self.__index = index
2861
-
2862
- def __get__(self, obj, cls=None):
2863
- retval = super().__get__(obj, cls)
2864
- retval.__name__ = f"{self.func.__name__}[{self.__index}]"
2865
- retval.__doc__ = self.func.__doc__
2866
- return retval
3366
+ def make_method(index, func, *args):
3367
+ def testmethod(self):
3368
+ return func(self, *args)
3369
+ testmethod.__name__ = f"{func.__name__}[{index}]"
3370
+ testmethod.__doc__ = func.__doc__
3371
+ return testmethod
2867
3372
 
2868
3373
  def decorate(method):
2869
3374
  frame = sys._getframe().f_back.f_locals
2870
3375
  for index, arg in enumerate(args):
2871
- testmethod = partialmethod(index, method, *utils.as_list(arg))
3376
+ testmethod = make_method(index, method, *utils.as_list(arg))
2872
3377
  name = f"{method.__name__}[{index}]"
2873
3378
  frame[name] = testmethod
2874
3379
  return None
3380
+
2875
3381
  return decorate
2876
3382
 
2877
3383
  def setup(self, deps, tools):
@@ -2980,7 +3486,7 @@ class Test(Task):
2980
3486
  def run(self, deps, tools):
2981
3487
  testsuite = ut.TestSuite()
2982
3488
  for test in self._get_test_names():
2983
- 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)):
2984
3490
  testfunc = getattr(self, test)
2985
3491
  if not testfunc:
2986
3492
  continue
@@ -3001,7 +3507,7 @@ class Test(Task):
3001
3507
 
3002
3508
 
3003
3509
  @ArtifactAttributeSetProvider.Register
3004
- class ResourceAttributeSetProvider(ArtifactAttributeSetProvider):
3510
+ class WorkspaceResourceAttributeSetProvider(ArtifactAttributeSetProvider):
3005
3511
  def create(self, artifact):
3006
3512
  pass
3007
3513
 
@@ -3012,33 +3518,31 @@ class ResourceAttributeSetProvider(ArtifactAttributeSetProvider):
3012
3518
  pass
3013
3519
 
3014
3520
  def apply(self, task, artifact):
3015
- resource = artifact.get_task()
3016
- if isinstance(resource, Resource):
3017
- from inspect import signature
3521
+ resource = artifact.task
3522
+ node = artifact.get_node()
3523
+ if not node.is_workspace_resource():
3524
+ return
3018
3525
 
3019
- deps = resource._run_env
3020
- deps.__enter__()
3021
- sig = signature(resource.acquire)
3022
- try:
3023
- ba = sig.bind_partial(artifact=artifact, deps=deps, tools=resource.tools, owner=task)
3024
- acquire = resource.acquire
3025
- except Exception:
3026
- ba = sig.bind_partial(artifact, deps, resource.tools)
3027
- acquire = utils.deprecated(resource.acquire)
3028
- 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
3029
3536
 
3030
3537
  def unapply(self, task, artifact):
3031
- resource = artifact.get_task()
3032
- if isinstance(resource, Resource):
3033
- from inspect import signature
3538
+ resource = artifact.task
3539
+ node = artifact.get_node()
3540
+ if not node.is_workspace_resource():
3541
+ return
3034
3542
 
3035
- deps = resource._run_env
3036
- sig = signature(resource.release)
3037
- try:
3038
- ba = sig.bind_partial(artifact=artifact, deps=deps, tools=resource.tools, owner=task)
3039
- release = resource.release
3040
- except Exception:
3041
- ba = sig.bind_partial(artifact, deps, resource.tools)
3042
- release = utils.deprecated(resource.release)
3043
- release(*ba.args, **ba.kwargs)
3044
- 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)