jolt 0.9.172__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 (185) 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 +596 -252
  6. jolt/chroot.py +36 -11
  7. jolt/cli.py +143 -130
  8. jolt/common_pb2.py +45 -45
  9. jolt/config.py +76 -40
  10. jolt/error.py +19 -4
  11. jolt/filesystem.py +2 -6
  12. jolt/graph.py +400 -82
  13. jolt/influence.py +110 -3
  14. jolt/loader.py +338 -174
  15. jolt/log.py +127 -31
  16. jolt/manifest.py +13 -46
  17. jolt/options.py +35 -11
  18. jolt/pkgs/abseil.py +42 -0
  19. jolt/pkgs/asio.py +25 -0
  20. jolt/pkgs/autoconf.py +41 -0
  21. jolt/pkgs/automake.py +41 -0
  22. jolt/pkgs/b2.py +31 -0
  23. jolt/pkgs/boost.py +111 -0
  24. jolt/pkgs/boringssl.py +32 -0
  25. jolt/pkgs/busybox.py +39 -0
  26. jolt/pkgs/bzip2.py +43 -0
  27. jolt/pkgs/cares.py +29 -0
  28. jolt/pkgs/catch2.py +36 -0
  29. jolt/pkgs/cbindgen.py +17 -0
  30. jolt/pkgs/cista.py +19 -0
  31. jolt/pkgs/clang.py +44 -0
  32. jolt/pkgs/cli11.py +24 -0
  33. jolt/pkgs/cmake.py +48 -0
  34. jolt/pkgs/cpython.py +196 -0
  35. jolt/pkgs/crun.py +29 -0
  36. jolt/pkgs/curl.py +38 -0
  37. jolt/pkgs/dbus.py +18 -0
  38. jolt/pkgs/double_conversion.py +24 -0
  39. jolt/pkgs/fastfloat.py +21 -0
  40. jolt/pkgs/ffmpeg.py +28 -0
  41. jolt/pkgs/flatbuffers.py +29 -0
  42. jolt/pkgs/fmt.py +27 -0
  43. jolt/pkgs/fstree.py +20 -0
  44. jolt/pkgs/gflags.py +18 -0
  45. jolt/pkgs/glib.py +18 -0
  46. jolt/pkgs/glog.py +25 -0
  47. jolt/pkgs/glslang.py +21 -0
  48. jolt/pkgs/golang.py +16 -11
  49. jolt/pkgs/googlebenchmark.py +18 -0
  50. jolt/pkgs/googletest.py +46 -0
  51. jolt/pkgs/gperf.py +15 -0
  52. jolt/pkgs/grpc.py +73 -0
  53. jolt/pkgs/hdf5.py +19 -0
  54. jolt/pkgs/help2man.py +14 -0
  55. jolt/pkgs/inja.py +28 -0
  56. jolt/pkgs/jsoncpp.py +31 -0
  57. jolt/pkgs/libarchive.py +43 -0
  58. jolt/pkgs/libcap.py +44 -0
  59. jolt/pkgs/libdrm.py +44 -0
  60. jolt/pkgs/libedit.py +42 -0
  61. jolt/pkgs/libevent.py +31 -0
  62. jolt/pkgs/libexpat.py +27 -0
  63. jolt/pkgs/libfastjson.py +21 -0
  64. jolt/pkgs/libffi.py +16 -0
  65. jolt/pkgs/libglvnd.py +30 -0
  66. jolt/pkgs/libogg.py +28 -0
  67. jolt/pkgs/libpciaccess.py +18 -0
  68. jolt/pkgs/libseccomp.py +21 -0
  69. jolt/pkgs/libtirpc.py +24 -0
  70. jolt/pkgs/libtool.py +42 -0
  71. jolt/pkgs/libunwind.py +35 -0
  72. jolt/pkgs/libva.py +18 -0
  73. jolt/pkgs/libvorbis.py +33 -0
  74. jolt/pkgs/libxml2.py +35 -0
  75. jolt/pkgs/libxslt.py +17 -0
  76. jolt/pkgs/libyajl.py +16 -0
  77. jolt/pkgs/llvm.py +81 -0
  78. jolt/pkgs/lua.py +54 -0
  79. jolt/pkgs/lz4.py +26 -0
  80. jolt/pkgs/m4.py +14 -0
  81. jolt/pkgs/make.py +17 -0
  82. jolt/pkgs/mesa.py +81 -0
  83. jolt/pkgs/meson.py +17 -0
  84. jolt/pkgs/mstch.py +28 -0
  85. jolt/pkgs/mysql.py +60 -0
  86. jolt/pkgs/nasm.py +49 -0
  87. jolt/pkgs/ncurses.py +30 -0
  88. jolt/pkgs/ng_log.py +25 -0
  89. jolt/pkgs/ninja.py +45 -0
  90. jolt/pkgs/nlohmann_json.py +25 -0
  91. jolt/pkgs/nodejs.py +19 -11
  92. jolt/pkgs/opencv.py +24 -0
  93. jolt/pkgs/openjdk.py +26 -0
  94. jolt/pkgs/openssl.py +103 -0
  95. jolt/pkgs/paho.py +76 -0
  96. jolt/pkgs/patchelf.py +16 -0
  97. jolt/pkgs/perl.py +42 -0
  98. jolt/pkgs/pkgconfig.py +64 -0
  99. jolt/pkgs/poco.py +39 -0
  100. jolt/pkgs/protobuf.py +77 -0
  101. jolt/pkgs/pugixml.py +27 -0
  102. jolt/pkgs/python.py +19 -0
  103. jolt/pkgs/qt.py +35 -0
  104. jolt/pkgs/rapidjson.py +26 -0
  105. jolt/pkgs/rapidyaml.py +28 -0
  106. jolt/pkgs/re2.py +30 -0
  107. jolt/pkgs/re2c.py +17 -0
  108. jolt/pkgs/readline.py +15 -0
  109. jolt/pkgs/rust.py +41 -0
  110. jolt/pkgs/sdl.py +28 -0
  111. jolt/pkgs/simdjson.py +27 -0
  112. jolt/pkgs/soci.py +46 -0
  113. jolt/pkgs/spdlog.py +29 -0
  114. jolt/pkgs/spirv_llvm.py +21 -0
  115. jolt/pkgs/spirv_tools.py +24 -0
  116. jolt/pkgs/sqlite.py +83 -0
  117. jolt/pkgs/ssl.py +12 -0
  118. jolt/pkgs/texinfo.py +15 -0
  119. jolt/pkgs/tomlplusplus.py +22 -0
  120. jolt/pkgs/wayland.py +26 -0
  121. jolt/pkgs/x11.py +58 -0
  122. jolt/pkgs/xerces_c.py +20 -0
  123. jolt/pkgs/xorg.py +360 -0
  124. jolt/pkgs/xz.py +29 -0
  125. jolt/pkgs/yamlcpp.py +30 -0
  126. jolt/pkgs/zeromq.py +47 -0
  127. jolt/pkgs/zlib.py +87 -0
  128. jolt/pkgs/zstd.py +33 -0
  129. jolt/plugins/alias.py +3 -0
  130. jolt/plugins/allure.py +2 -2
  131. jolt/plugins/autotools.py +66 -0
  132. jolt/plugins/cache.py +1 -1
  133. jolt/plugins/cmake.py +74 -6
  134. jolt/plugins/conan.py +238 -0
  135. jolt/plugins/cxxinfo.py +7 -0
  136. jolt/plugins/docker.py +76 -19
  137. jolt/plugins/email.xslt +141 -118
  138. jolt/plugins/environ.py +11 -0
  139. jolt/plugins/fetch.py +141 -0
  140. jolt/plugins/gdb.py +33 -14
  141. jolt/plugins/gerrit.py +0 -13
  142. jolt/plugins/git.py +248 -66
  143. jolt/plugins/googletest.py +1 -1
  144. jolt/plugins/http.py +1 -1
  145. jolt/plugins/libtool.py +63 -0
  146. jolt/plugins/linux.py +990 -0
  147. jolt/plugins/logstash.py +4 -4
  148. jolt/plugins/meson.py +61 -0
  149. jolt/plugins/ninja-compdb.py +96 -28
  150. jolt/plugins/ninja.py +424 -150
  151. jolt/plugins/paths.py +11 -1
  152. jolt/plugins/pkgconfig.py +219 -0
  153. jolt/plugins/podman.py +131 -87
  154. jolt/plugins/python.py +137 -0
  155. jolt/plugins/remote_execution/administration_pb2.py +27 -19
  156. jolt/plugins/remote_execution/log_pb2.py +12 -12
  157. jolt/plugins/remote_execution/scheduler_pb2.py +23 -23
  158. jolt/plugins/remote_execution/worker_pb2.py +19 -19
  159. jolt/plugins/report.py +7 -2
  160. jolt/plugins/rust.py +25 -0
  161. jolt/plugins/scheduler.py +135 -86
  162. jolt/plugins/selfdeploy/setup.py +6 -6
  163. jolt/plugins/selfdeploy.py +49 -31
  164. jolt/plugins/strings.py +35 -22
  165. jolt/plugins/symlinks.py +11 -4
  166. jolt/plugins/telemetry.py +1 -2
  167. jolt/plugins/timeline.py +13 -3
  168. jolt/scheduler.py +467 -165
  169. jolt/tasks.py +427 -111
  170. jolt/templates/timeline.html.template +44 -47
  171. jolt/timer.py +22 -0
  172. jolt/tools.py +527 -188
  173. jolt/utils.py +183 -3
  174. jolt/version.py +1 -1
  175. jolt/xmldom.py +12 -2
  176. {jolt-0.9.172.dist-info → jolt-0.9.435.dist-info}/METADATA +97 -41
  177. jolt-0.9.435.dist-info/RECORD +207 -0
  178. {jolt-0.9.172.dist-info → jolt-0.9.435.dist-info}/WHEEL +1 -1
  179. jolt/plugins/amqp.py +0 -855
  180. jolt/plugins/debian.py +0 -338
  181. jolt/plugins/repo.py +0 -253
  182. jolt/plugins/snap.py +0 -122
  183. jolt-0.9.172.dist-info/RECORD +0 -92
  184. {jolt-0.9.172.dist-info → jolt-0.9.435.dist-info}/entry_points.txt +0 -0
  185. {jolt-0.9.172.dist-info → jolt-0.9.435.dist-info}/top_level.txt +0 -0
jolt/tasks.py CHANGED
@@ -10,8 +10,10 @@ import platform
10
10
  from threading import RLock
11
11
  import subprocess
12
12
  from os import environ
13
+ from os import sys as os_sys
13
14
  import sys
14
15
  import unittest as ut
16
+ from urllib.parse import urlparse
15
17
  import uuid
16
18
  import re
17
19
  import traceback
@@ -20,12 +22,13 @@ from jolt import filesystem as fs
20
22
  from jolt import log
21
23
  from jolt import utils
22
24
  from jolt.cache import ArtifactAttributeSetProvider
23
- from jolt.error import raise_error, raise_error_if, raise_task_error, raise_task_error_if
25
+ from jolt.error import raise_error_if, raise_task_error, raise_task_error_if
24
26
  from jolt.error import raise_unreported_task_error_if
25
- from jolt.error import JoltError, JoltCommandError
27
+ from jolt.error import JoltError, JoltCommandError, LoggedJoltError
26
28
  from jolt.expires import Immediately
27
29
  from jolt.influence import FileInfluence, TaintInfluenceProvider
28
30
  from jolt.influence import TaskClassSourceInfluence
31
+ from jolt.influence import CallbackInfluence
29
32
  from jolt.influence import attribute as attribute_influence
30
33
  from jolt.influence import environ as environ_influence
31
34
  from jolt.influence import source as source_influence
@@ -134,7 +137,7 @@ class Parameter(object):
134
137
  self.name = name
135
138
 
136
139
  def __init__(self, default=None, values=None, required=True,
137
- const=False, influence=True, help=None):
140
+ const=False, influence=True, help=None, valuesfn=None):
138
141
  """
139
142
  Creates a new parameter.
140
143
 
@@ -142,6 +145,10 @@ class Parameter(object):
142
145
  default (str, optional): An optional default value.
143
146
  values (list, optional): A list of accepted values. An
144
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.
145
152
  required (boolean, optional): If required, the parameter must be assigned
146
153
  a value before the task can be executed. The default is ``True``.
147
154
  const (boolean, optional): If const is True, the parameter is immutable
@@ -163,6 +170,7 @@ class Parameter(object):
163
170
  self._default = default
164
171
  self._value = default
165
172
  self._accepted_values = values
173
+ self._accepted_values_fn = valuesfn
166
174
  self._required = required
167
175
  self._const = const
168
176
  self._influence = influence
@@ -189,7 +197,7 @@ class Parameter(object):
189
197
  def highlight(value):
190
198
  return colors.bright(value) if self._is_default(value) else colors.dim(value)
191
199
 
192
- 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 ""
193
201
 
194
202
  def __str__(self):
195
203
  """ Returns the parameter value as a string """
@@ -198,6 +206,8 @@ class Parameter(object):
198
206
  def _validate(self, value, what=None):
199
207
  if self._accepted_values is not None and value not in self._accepted_values:
200
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)
201
211
 
202
212
  def get_default(self):
203
213
  """ Get the default value of the parameter.
@@ -435,7 +445,7 @@ class IntParameter(Parameter):
435
445
  """
436
446
 
437
447
  def __init__(self, default=None, min=None, max=None, values=None, required=True, const=False,
438
- influence=True, help=None):
448
+ influence=True, help=None, valuesfn=None):
439
449
  """
440
450
  Creates a new parameter.
441
451
 
@@ -483,7 +493,8 @@ class IntParameter(Parameter):
483
493
  required=required,
484
494
  const=const,
485
495
  influence=influence,
486
- help=help)
496
+ help=help,
497
+ valuesfn=valuesfn)
487
498
 
488
499
  def _validate(self, value, what=None):
489
500
  if self._min is not None and value < self._min:
@@ -687,6 +698,10 @@ class ListParameter(Parameter):
687
698
  for item in value:
688
699
  if item not in self._accepted_values:
689
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)
690
705
 
691
706
  def get_value(self):
692
707
  return "+".join(self._value)
@@ -768,6 +783,7 @@ class TaskRegistry(object):
768
783
  self.env = env
769
784
  self.tasks = {}
770
785
  self.instances = {}
786
+ self._workspace_resources = []
771
787
 
772
788
  @staticmethod
773
789
  def get(*args, **kwargs):
@@ -776,6 +792,21 @@ class TaskRegistry(object):
776
792
  return TaskRegistry._instance
777
793
 
778
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
+
779
810
  self.tasks[cls.name] = cls
780
811
 
781
812
  def add_task(self, task, extra_params):
@@ -784,6 +815,13 @@ class TaskRegistry(object):
784
815
  full_name = utils.format_task_name(name, params)
785
816
  self.instances[full_name] = task
786
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
+
787
825
  def get_task_class(self, name):
788
826
  return self.tasks.get(name)
789
827
 
@@ -803,12 +841,17 @@ class TaskRegistry(object):
803
841
  if cls:
804
842
  task = cls(parameters=params, manifest=manifest, buildenv=buildenv)
805
843
  task = self.instances.get(task.qualified_name, task)
806
- self.instances[task.qualified_name] = task
807
- 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
808
847
  return task
809
848
 
810
849
  raise_task_error_if(not task, full_name, "No such task")
811
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
+
812
855
  def set_default_parameters(self, task):
813
856
  name, params = utils.parse_task_name(task)
814
857
 
@@ -880,6 +923,14 @@ class TaskGenerator(object):
880
923
 
881
924
 
882
925
  class attributes:
926
+ @staticmethod
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
+
883
934
  @staticmethod
884
935
  def artifact(name, session=False):
885
936
  """Decorator adding an additional artifact to a task.
@@ -916,7 +967,7 @@ class attributes:
916
967
  @functools.wraps(cls._artifacts)
917
968
  def _artifacts(self, cache, node):
918
969
  artifacts = _old_artifacts(self, cache, node)
919
- artifacts += [cache.get_artifact(node, name, session=session)]
970
+ artifacts += [cache.get_artifact(node, name, session=session or isinstance(cls, Resource))]
920
971
  return artifacts
921
972
 
922
973
  cls._artifacts = _artifacts
@@ -924,6 +975,145 @@ class attributes:
924
975
 
925
976
  return decorate
926
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.
1015
+
1016
+ Args:
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
1030
+ """
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
1116
+
927
1117
  @staticmethod
928
1118
  def attribute(alias, target, influence=True, default=False):
929
1119
  """
@@ -1026,6 +1216,65 @@ class attributes:
1026
1216
  return utils.concat_attributes("_publish_files", attrib)(cls)
1027
1217
  return decorate
1028
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
+
1029
1278
  @staticmethod
1030
1279
  def requires(attrib):
1031
1280
  """
@@ -1112,7 +1361,7 @@ class attributes:
1112
1361
  return _decorate
1113
1362
 
1114
1363
  @staticmethod
1115
- def platform(cls, attrib):
1364
+ def platform(attrib):
1116
1365
  """
1117
1366
  Decorates a task with an alternative ``platform`` attribute.
1118
1367
 
@@ -1136,6 +1385,40 @@ class attributes:
1136
1385
  cls.system = property(lambda t: t._system.value)
1137
1386
  return cls
1138
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
+
1139
1422
 
1140
1423
  class TaskBase(object):
1141
1424
  """ Task base class. """
@@ -1219,6 +1502,9 @@ class TaskBase(object):
1219
1502
  joltproject = None
1220
1503
  """ Name of project this task belongs to. """
1221
1504
 
1505
+ local = False
1506
+ """ A local task is only executed on the local node where the build is initiated. """
1507
+
1222
1508
  name = None
1223
1509
  """ Name of the task. Derived from class name if not set. """
1224
1510
 
@@ -1293,6 +1579,14 @@ class TaskBase(object):
1293
1579
  execution requests, the instance ID won't be.
1294
1580
  """
1295
1581
 
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
+
1296
1590
  def __init__(self, parameters=None, manifest=None, buildenv=None, **kwargs):
1297
1591
  self._identity = None
1298
1592
  self._report = _JoltTask()
@@ -1334,7 +1628,7 @@ class TaskBase(object):
1334
1628
  def _apply_protobuf(self, buildenv):
1335
1629
  if buildenv is None:
1336
1630
  return
1337
- task = buildenv.tasks.get(self.short_qualified_name)
1631
+ task = buildenv.tasks.get(self.exported_name)
1338
1632
  if not task:
1339
1633
  return
1340
1634
  if task.identity:
@@ -1410,6 +1704,9 @@ class TaskBase(object):
1410
1704
  "Required parameter '{0}' has not been set", key)
1411
1705
 
1412
1706
  def _verify_influence(self, deps, artifact, tools, sources=None):
1707
+ if "configurationrepository" in self.name:
1708
+ breakpoint()
1709
+
1413
1710
  # Verify that any transformed sources are influencing
1414
1711
  sources = set(map(tools.expand_path, sources or []))
1415
1712
 
@@ -1426,29 +1723,29 @@ class TaskBase(object):
1426
1723
  return not fs.is_relative_to(fname, rootpath)
1427
1724
  return _filter
1428
1725
 
1726
+ # Ignore any files in build directories
1727
+ sources = filter(_subpath_filter(tools.expand_path(tools.buildroot)), sources)
1728
+
1429
1729
  for _, dep in deps.items():
1430
1730
  deptask = dep.task
1431
1731
  if isinstance(deptask, FileInfluence):
1432
1732
  # Resource dependencies may cover the influence implicitly
1433
1733
  deppath = self.tools.expand_path(str(deptask.path))
1434
- 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)
1435
1735
  else:
1436
1736
  # Ignore any files in artifacts
1437
1737
  deppath = self.tools.expand_path(dep.path)
1438
- sources = set(filter(_subpath_filter(deppath), sources))
1439
-
1440
- # Ignore any files in build directories
1441
- sources = filter(_subpath_filter(tools.expand_path(tools.buildroot)), sources)
1442
- sources = set(sources)
1738
+ sources = filter(_subpath_filter(deppath), sources)
1443
1739
 
1444
1740
  for ip in self.influence:
1445
1741
  if not isinstance(ip, FileInfluence):
1446
1742
  continue
1447
- ok = [source for source in sources if ip.is_influenced_by(self, source)]
1448
- sources.difference_update(ok)
1743
+ sources = {source for source in sources if not ip.is_influenced_by(self, source)}
1744
+
1449
1745
  for source in sources:
1450
1746
  log.warning("Missing influence: {} ({})", source, self.name)
1451
- 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")
1452
1749
 
1453
1750
  def _get_export_objects(self):
1454
1751
  return self._exports
@@ -1489,6 +1786,16 @@ class TaskBase(object):
1489
1786
  self.name,
1490
1787
  self._get_explicitly_set_parameters())
1491
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
+
1492
1799
  def expand(self, string_or_list, *args, **kwargs):
1493
1800
  """ Expands keyword arguments/macros in a format string.
1494
1801
 
@@ -2353,7 +2660,7 @@ class Runner(Task):
2353
2660
  for task, artifact in deps.items():
2354
2661
  if not artifact.task.is_cacheable():
2355
2662
  continue
2356
- if artifact.strings.executable.get_value() is None:
2663
+ if artifact.strings.executable is None:
2357
2664
  self.verbose("No executable found in task artifact for '{}'", task)
2358
2665
  continue
2359
2666
  with tools.cwd(artifact.path):
@@ -2376,6 +2683,10 @@ class ErrorProxy(object):
2376
2683
  def type(self):
2377
2684
  return self._error.type
2378
2685
 
2686
+ @type.setter
2687
+ def type(self, value):
2688
+ self._error.type = value
2689
+
2379
2690
  @property
2380
2691
  def details(self):
2381
2692
  return self._error.details
@@ -2391,11 +2702,21 @@ class ErrorProxy(object):
2391
2702
 
2392
2703
  class ReportProxy(object):
2393
2704
  def __init__(self, task, report):
2705
+ from jolt import config
2394
2706
  self._task = task
2395
2707
  self._report = report
2708
+ self._max_errors = config.getint("jolt", "task_max_errors", 100)
2396
2709
 
2397
2710
  def add_error(self, type, location, message, details=""):
2398
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
+
2399
2720
  error = self._report.create_error()
2400
2721
  error.type = type
2401
2722
  error.location = location
@@ -2416,11 +2737,8 @@ class ReportProxy(object):
2416
2737
  """
2417
2738
  for match in re.finditer(regex, logbuf, re.MULTILINE):
2418
2739
  error = match.groupdict()
2419
- self.add_error(
2420
- type,
2421
- error.get("location", ""),
2422
- error.get("message", ""),
2423
- error.get("details", ""))
2740
+ if not self.add_error(type, error.get("location", ""), error.get("message", ""), error.get("details", "")):
2741
+ break
2424
2742
 
2425
2743
  def add_regex_errors_with_file(self, type, regex, logbuf, reldir, filterfn=lambda n: True):
2426
2744
  """
@@ -2441,25 +2759,29 @@ class ReportProxy(object):
2441
2759
  if not filterfn(error):
2442
2760
  continue
2443
2761
  if error["location"] not in errors_by_location:
2444
- errors_by_location[error["location"]] = (error, [error["message"]])
2762
+ errors_by_location[error["location"]] = (error, [error["message"]], error.get("details", ""))
2445
2763
  else:
2446
2764
  errors_by_location[error["location"]][1].append(error["message"])
2447
2765
 
2448
- for error, msgs in errors_by_location.values():
2766
+ for error, msgs, details in errors_by_location.values():
2449
2767
  message = "\n".join(utils.unique_list(msgs))
2450
- with self._task.tools.cwd(reldir):
2451
- try:
2452
- details = self._task.tools.read_file(error["file"])
2453
- details = details.splitlines()
2454
- details = str(error["line"]) + ": " + details[int(error["line"]) - 1]
2455
- except Exception:
2456
- 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 = ""
2457
2776
 
2458
2777
  location = error.get("location", "")
2459
2778
  if location:
2460
- location = self._task.tools.expand_relpath(location, self._task.joltdir)
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)
2461
2782
 
2462
- self.add_error(type, location, message, details)
2783
+ if not self.add_error(type, location, message, details):
2784
+ break
2463
2785
 
2464
2786
  def add_exception(self, exc, errtype=None, location=None):
2465
2787
  """
@@ -2498,10 +2820,26 @@ class ReportProxy(object):
2498
2820
  def errors(self):
2499
2821
  return [ErrorProxy(error) for error in self._report.errors]
2500
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
+
2501
2830
  @property
2502
2831
  def manifest(self):
2503
2832
  return self._report
2504
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
+
2505
2843
 
2506
2844
  class Resource(Task):
2507
2845
  """
@@ -2515,14 +2853,18 @@ class Resource(Task):
2515
2853
 
2516
2854
  """
2517
2855
 
2518
- cacheable = False
2519
-
2520
2856
  abstract = True
2521
2857
  """ An abstract resource class indended to be subclassed. """
2522
2858
 
2859
+ release_on_error = False
2860
+ """ Call release if an exception occurs during acquire. """
2861
+
2523
2862
  def __init__(self, *args, **kwargs):
2524
2863
  super().__init__(*args, **kwargs)
2525
2864
 
2865
+ def _artifacts(self, cache, node):
2866
+ return [cache.get_artifact(node, "main", session=True)]
2867
+
2526
2868
  def is_runnable(self):
2527
2869
  return False
2528
2870
 
@@ -2586,13 +2928,21 @@ class WorkspaceResource(Resource):
2586
2928
  raise_task_error_if(len(self.requires) > 0, self,
2587
2929
  "Workspace resource is not allowed to have requirements")
2588
2930
 
2589
- def acquire(self, **kwargs):
2931
+ def acquire(self, *args, **kwargs):
2590
2932
  return self.acquire_ws()
2591
2933
 
2592
- def release(self, **kwargs):
2934
+ def release(self, *args, **kwargs):
2593
2935
  return self.release_ws()
2594
2936
 
2595
- 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):
2596
2946
  """ Called to acquire the resource.
2597
2947
 
2598
2948
  An implementor overrides this method in a subclass. The acquired
@@ -2696,6 +3046,7 @@ class Download(Task):
2696
3046
  Once downloaded, archives are extracted and all of their files are published.
2697
3047
  If the file is not an archive it is published as is. Recognized archive extensions are:
2698
3048
 
3049
+ - .7z
2699
3050
  - .tar
2700
3051
  - .tar.bz2
2701
3052
  - .tar.gz
@@ -2758,7 +3109,7 @@ class Download(Task):
2758
3109
  return fs.posixpath.basename(url.path) or "file"
2759
3110
 
2760
3111
  def run(self, deps, tools):
2761
- supported_formats = [".tar", ".tar.bz2", ".tar.gz", ".tar.xz", ".tgz", ".zip"]
3112
+ supported_formats = [".7z", ".tar", ".tar.bz2", ".tar.gz", ".tar.xz", ".tgz", ".zip"]
2762
3113
 
2763
3114
  raise_task_error_if(not self.url, self, "No URL(s) specified")
2764
3115
 
@@ -2855,7 +3206,8 @@ class Script(Task):
2855
3206
  doc = self.__doc__.split("---", 1)
2856
3207
  script = doc[1] if len(doc) > 1 else doc[0]
2857
3208
  script = script.splitlines()
2858
- script = [line[4:] for line in script]
3209
+ if os_sys.version_info < (3, 13):
3210
+ script = [line[4:] for line in script]
2859
3211
  script = "\n".join(script)
2860
3212
  script = script.lstrip()
2861
3213
  if not script.startswith("#!"):
@@ -2982,7 +3334,7 @@ class Test(Task):
2982
3334
  Abstract test tasks can't be executed and won't be listed.
2983
3335
  """
2984
3336
 
2985
- pattern = Parameter(required=False, help="Test-case filter wildcard.")
3337
+ filter = Parameter(required=False, help="Test-case filter wildcard.")
2986
3338
 
2987
3339
  def __init__(self, *args, **kwargs):
2988
3340
  super().__init__(*args, **kwargs)
@@ -3011,24 +3363,21 @@ class Test(Task):
3011
3363
  """
3012
3364
  raise_error_if(type(args) is not list, "Test.parameterized() expects a list as argument")
3013
3365
 
3014
- class partialmethod(functools.partialmethod):
3015
- def __init__(self, index, func, *args):
3016
- super().__init__(func, *args)
3017
- self.__index = index
3018
-
3019
- def __get__(self, obj, cls=None):
3020
- retval = super().__get__(obj, cls)
3021
- retval.__name__ = f"{self.func.__name__}[{self.__index}]"
3022
- retval.__doc__ = self.func.__doc__
3023
- 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
3024
3372
 
3025
3373
  def decorate(method):
3026
3374
  frame = sys._getframe().f_back.f_locals
3027
3375
  for index, arg in enumerate(args):
3028
- testmethod = partialmethod(index, method, *utils.as_list(arg))
3376
+ testmethod = make_method(index, method, *utils.as_list(arg))
3029
3377
  name = f"{method.__name__}[{index}]"
3030
3378
  frame[name] = testmethod
3031
3379
  return None
3380
+
3032
3381
  return decorate
3033
3382
 
3034
3383
  def setup(self, deps, tools):
@@ -3137,7 +3486,7 @@ class Test(Task):
3137
3486
  def run(self, deps, tools):
3138
3487
  testsuite = ut.TestSuite()
3139
3488
  for test in self._get_test_names():
3140
- 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)):
3141
3490
  testfunc = getattr(self, test)
3142
3491
  if not testfunc:
3143
3492
  continue
@@ -3158,7 +3507,7 @@ class Test(Task):
3158
3507
 
3159
3508
 
3160
3509
  @ArtifactAttributeSetProvider.Register
3161
- class ResourceAttributeSetProvider(ArtifactAttributeSetProvider):
3510
+ class WorkspaceResourceAttributeSetProvider(ArtifactAttributeSetProvider):
3162
3511
  def create(self, artifact):
3163
3512
  pass
3164
3513
 
@@ -3170,63 +3519,30 @@ class ResourceAttributeSetProvider(ArtifactAttributeSetProvider):
3170
3519
 
3171
3520
  def apply(self, task, artifact):
3172
3521
  resource = artifact.task
3173
- if isinstance(resource, Resource):
3174
- from inspect import signature
3175
-
3176
- if not hasattr(resource, "_run_env"):
3177
- raise_error("Internal scheduling error, resource has not been prepared: {}", task.short_qualified_name)
3522
+ node = artifact.get_node()
3523
+ if not node.is_workspace_resource():
3524
+ return
3178
3525
 
3179
- deps = resource._run_env
3180
- deps.__enter__()
3181
- sig = signature(resource.acquire)
3182
- try:
3183
- ba = sig.bind_partial(artifact=artifact, deps=deps, tools=resource.tools, owner=task)
3184
- acquire = resource.acquire
3185
- except Exception:
3186
- ba = sig.bind_partial(artifact, deps, resource.tools)
3187
- acquire = utils.deprecated(resource.acquire)
3526
+ resource.deps = node.cache.get_context(node)
3527
+ resource.deps.__enter__()
3188
3528
 
3189
- try:
3190
- if not isinstance(resource, WorkspaceResource):
3191
- ts = utils.duration()
3192
- log.info(colors.blue("Resource acquisition started ({})"),
3193
- resource.short_qualified_name)
3194
- acquire(*ba.args, **ba.kwargs)
3195
- if not isinstance(resource, WorkspaceResource):
3196
- log.info(colors.green("Resource acquisition finished after {} ({})"),
3197
- ts, resource.short_qualified_name)
3198
- except Exception as e:
3199
- if not isinstance(resource, WorkspaceResource):
3200
- log.error("Resource acquisition failed after {} ({})",
3201
- ts, resource.short_qualified_name)
3202
- raise e
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
3203
3536
 
3204
3537
  def unapply(self, task, artifact):
3205
3538
  resource = artifact.task
3206
- if isinstance(resource, Resource):
3207
- from inspect import signature
3539
+ node = artifact.get_node()
3540
+ if not node.is_workspace_resource():
3541
+ return
3208
3542
 
3209
- deps = resource._run_env
3210
- sig = signature(resource.release)
3211
- try:
3212
- ba = sig.bind_partial(artifact=artifact, deps=deps, tools=resource.tools, owner=task)
3213
- release = resource.release
3214
- except Exception:
3215
- ba = sig.bind_partial(artifact, deps, resource.tools)
3216
- release = utils.deprecated(resource.release)
3543
+ try:
3544
+ resource.release(artifact=artifact, deps=resource.deps, tools=resource.tools, owner=task)
3545
+ except Exception as e:
3546
+ raise e
3217
3547
 
3218
- try:
3219
- if not isinstance(resource, WorkspaceResource):
3220
- ts = utils.duration()
3221
- log.info(colors.blue("Resource release started ({})"),
3222
- resource.short_qualified_name)
3223
- release(*ba.args, **ba.kwargs)
3224
- if not isinstance(resource, WorkspaceResource):
3225
- log.info(colors.green("Resource release finished after {} ({})"),
3226
- ts, resource.short_qualified_name)
3227
- except Exception as e:
3228
- if not isinstance(resource, WorkspaceResource):
3229
- log.error("Resource release failed after {} ({})",
3230
- ts, resource.short_qualified_name)
3231
- raise e
3232
- deps.__exit__(None, None, None)
3548
+ resource.deps.__exit__(None, None, None)