jolt 0.9.328__tar.gz → 0.9.332__tar.gz

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 (97) hide show
  1. {jolt-0.9.328 → jolt-0.9.332}/PKG-INFO +10 -10
  2. {jolt-0.9.328 → jolt-0.9.332}/jolt/cache.py +26 -8
  3. {jolt-0.9.328 → jolt-0.9.332}/jolt/graph.py +163 -27
  4. {jolt-0.9.328 → jolt-0.9.332}/jolt/log.py +39 -10
  5. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/docker.py +8 -7
  6. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/podman.py +87 -36
  7. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/scheduler.py +7 -3
  8. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/selfdeploy.py +1 -1
  9. {jolt-0.9.328 → jolt-0.9.332}/jolt/scheduler.py +10 -15
  10. {jolt-0.9.328 → jolt-0.9.332}/jolt/tasks.py +21 -68
  11. {jolt-0.9.328 → jolt-0.9.332}/jolt/tools.py +29 -5
  12. jolt-0.9.332/jolt/version.py +1 -0
  13. {jolt-0.9.328 → jolt-0.9.332}/jolt.egg-info/PKG-INFO +10 -10
  14. {jolt-0.9.328 → jolt-0.9.332}/jolt.egg-info/requires.txt +9 -9
  15. jolt-0.9.328/jolt/version.py +0 -1
  16. {jolt-0.9.328 → jolt-0.9.332}/README.rst +0 -0
  17. {jolt-0.9.328 → jolt-0.9.332}/jolt/__init__.py +0 -0
  18. {jolt-0.9.328 → jolt-0.9.332}/jolt/__main__.py +0 -0
  19. {jolt-0.9.328 → jolt-0.9.332}/jolt/bin/fstree-darwin-x86_64 +0 -0
  20. {jolt-0.9.328 → jolt-0.9.332}/jolt/bin/fstree-linux-x86_64 +0 -0
  21. {jolt-0.9.328 → jolt-0.9.332}/jolt/chroot.py +0 -0
  22. {jolt-0.9.328 → jolt-0.9.332}/jolt/cli.py +0 -0
  23. {jolt-0.9.328 → jolt-0.9.332}/jolt/colors.py +0 -0
  24. {jolt-0.9.328 → jolt-0.9.332}/jolt/common_pb2.py +0 -0
  25. {jolt-0.9.328 → jolt-0.9.332}/jolt/common_pb2_grpc.py +0 -0
  26. {jolt-0.9.328 → jolt-0.9.332}/jolt/config.py +0 -0
  27. {jolt-0.9.328 → jolt-0.9.332}/jolt/error.py +0 -0
  28. {jolt-0.9.328 → jolt-0.9.332}/jolt/expires.py +0 -0
  29. {jolt-0.9.328 → jolt-0.9.332}/jolt/filesystem.py +0 -0
  30. {jolt-0.9.328 → jolt-0.9.332}/jolt/hooks.py +0 -0
  31. {jolt-0.9.328 → jolt-0.9.332}/jolt/influence.py +0 -0
  32. {jolt-0.9.328 → jolt-0.9.332}/jolt/inspection.py +0 -0
  33. {jolt-0.9.328 → jolt-0.9.332}/jolt/loader.py +0 -0
  34. {jolt-0.9.328 → jolt-0.9.332}/jolt/manifest.py +0 -0
  35. {jolt-0.9.328 → jolt-0.9.332}/jolt/options.py +0 -0
  36. {jolt-0.9.328 → jolt-0.9.332}/jolt/pkgs/__init__.py +0 -0
  37. {jolt-0.9.328 → jolt-0.9.332}/jolt/pkgs/golang.py +0 -0
  38. {jolt-0.9.328 → jolt-0.9.332}/jolt/pkgs/nodejs.py +0 -0
  39. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/__init__.py +0 -0
  40. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/alias.py +0 -0
  41. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/allure.py +0 -0
  42. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/autoweight.py +0 -0
  43. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/cache.py +0 -0
  44. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/cmake.py +0 -0
  45. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/conan.py +0 -0
  46. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/cxx.py +0 -0
  47. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/cxxinfo.py +0 -0
  48. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/dashboard.py +0 -0
  49. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/debian.py +0 -0
  50. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/email.py +0 -0
  51. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/email.xslt +0 -0
  52. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/environ.py +0 -0
  53. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/gdb.py +0 -0
  54. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/gerrit.py +0 -0
  55. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/git.py +0 -0
  56. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/golang.py +0 -0
  57. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/googletest.py +0 -0
  58. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/http.py +0 -0
  59. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/junit.py +0 -0
  60. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/logstash.py +0 -0
  61. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/ninja-compdb.py +0 -0
  62. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/ninja.py +0 -0
  63. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/nodejs.py +0 -0
  64. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/paths.py +0 -0
  65. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/python.py +0 -0
  66. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/remote_execution/__init__.py +0 -0
  67. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/remote_execution/administration_pb2.py +0 -0
  68. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/remote_execution/administration_pb2_grpc.py +0 -0
  69. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/remote_execution/log_pb2.py +0 -0
  70. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/remote_execution/log_pb2_grpc.py +0 -0
  71. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/remote_execution/scheduler_pb2.py +0 -0
  72. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/remote_execution/scheduler_pb2_grpc.py +0 -0
  73. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/remote_execution/worker_pb2.py +0 -0
  74. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/remote_execution/worker_pb2_grpc.py +0 -0
  75. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/repo.py +0 -0
  76. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/report.py +0 -0
  77. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/strings.py +0 -0
  78. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/symlinks.py +0 -0
  79. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/telemetry.py +0 -0
  80. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/timeline.py +0 -0
  81. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/volume.py +0 -0
  82. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/yaml-ninja.py +0 -0
  83. {jolt-0.9.328 → jolt-0.9.332}/jolt/plugins/yamltask.py +0 -0
  84. {jolt-0.9.328 → jolt-0.9.332}/jolt/templates/cxxexecutable.cmake.template +0 -0
  85. {jolt-0.9.328 → jolt-0.9.332}/jolt/templates/cxxlibrary.cmake.template +0 -0
  86. {jolt-0.9.328 → jolt-0.9.332}/jolt/templates/export.sh.template +0 -0
  87. {jolt-0.9.328 → jolt-0.9.332}/jolt/templates/timeline.html.template +0 -0
  88. {jolt-0.9.328 → jolt-0.9.332}/jolt/timer.py +0 -0
  89. {jolt-0.9.328 → jolt-0.9.332}/jolt/utils.py +0 -0
  90. {jolt-0.9.328 → jolt-0.9.332}/jolt/version_utils.py +0 -0
  91. {jolt-0.9.328 → jolt-0.9.332}/jolt/xmldom.py +0 -0
  92. {jolt-0.9.328 → jolt-0.9.332}/jolt.egg-info/SOURCES.txt +0 -0
  93. {jolt-0.9.328 → jolt-0.9.332}/jolt.egg-info/dependency_links.txt +0 -0
  94. {jolt-0.9.328 → jolt-0.9.332}/jolt.egg-info/entry_points.txt +0 -0
  95. {jolt-0.9.328 → jolt-0.9.332}/jolt.egg-info/top_level.txt +0 -0
  96. {jolt-0.9.328 → jolt-0.9.332}/setup.cfg +0 -0
  97. {jolt-0.9.328 → jolt-0.9.332}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: jolt
3
- Version: 0.9.328
3
+ Version: 0.9.332
4
4
  Summary: A task executor
5
5
  Home-page: https://github.com/srand/jolt
6
6
  Author: Robert Andersson
@@ -27,33 +27,33 @@ Requires-Dist: backports.tarfile==1.2.0
27
27
  Requires-Dist: bz2file==0.98
28
28
  Requires-Dist: certifi==2024.8.30
29
29
  Requires-Dist: cffi==1.17.1
30
- Requires-Dist: charset-normalizer==3.3.2
30
+ Requires-Dist: charset-normalizer==3.4.0
31
31
  Requires-Dist: click==8.1.7
32
32
  Requires-Dist: colorama==0.4.6
33
33
  Requires-Dist: fasteners==0.19
34
- Requires-Dist: grpcio==1.66.1
34
+ Requires-Dist: grpcio==1.68.0
35
35
  Requires-Dist: idna==3.10
36
36
  Requires-Dist: importlib-metadata==8.5.0
37
37
  Requires-Dist: importlib_metadata
38
38
  Requires-Dist: jaraco.classes==3.4.0
39
39
  Requires-Dist: jaraco.context==6.0.1
40
- Requires-Dist: jaraco.functools==4.0.2
40
+ Requires-Dist: jaraco.functools==4.1.0
41
41
  Requires-Dist: jeepney==0.8.0
42
42
  Requires-Dist: jinja2==3.1.4
43
- Requires-Dist: keyring==25.4.1
43
+ Requires-Dist: keyring==25.5.0
44
44
  Requires-Dist: keyrings.alt==5.0.2
45
45
  Requires-Dist: lxml==5.3.0
46
46
  Requires-Dist: more-itertools==10.5.0
47
47
  Requires-Dist: multi_key_dict
48
- Requires-Dist: ninja==1.11.1.1
49
- Requires-Dist: protobuf==5.28.2
50
- Requires-Dist: psutil==6.0.0
48
+ Requires-Dist: ninja==1.11.1.2
49
+ Requires-Dist: protobuf==5.29.0
50
+ Requires-Dist: psutil==6.1.0
51
51
  Requires-Dist: pycparser==2.22
52
52
  Requires-Dist: pygit2==1.15.1
53
53
  Requires-Dist: requests==2.32.3
54
- Requires-Dist: tqdm==4.66.5
54
+ Requires-Dist: tqdm==4.67.1
55
55
  Requires-Dist: urllib3==1.26.20
56
- Requires-Dist: zipp==3.20.2
56
+ Requires-Dist: zipp==3.21.0
57
57
  Requires-Dist: zstandard==0.23.0
58
58
  Provides-Extra: allure
59
59
  Requires-Dist: allure-python-commons; extra == "allure"
@@ -879,6 +879,28 @@ class Artifact(object):
879
879
  return self._node.task
880
880
 
881
881
 
882
+ class ArtifactToolsProxy(object):
883
+ def __init__(self, artifact, tools):
884
+ self._artifact = artifact
885
+ self._tools = tools
886
+
887
+ def __getattr__(self, name):
888
+ if name == "tools":
889
+ return self._tools
890
+ if name == "_artifact":
891
+ return self._artifact
892
+ attr = getattr(self._artifact.__class__, name, None)
893
+ if attr is not None and callable(attr):
894
+ return attr.__get__(self, ArtifactToolsProxy)
895
+ return getattr(self._artifact, name)
896
+
897
+ def __setattr__(self, name, value):
898
+ if name == "_artifact" or name == "_tools":
899
+ super(ArtifactToolsProxy, self).__setattr__(name, value)
900
+ else:
901
+ setattr(self._artifact, name, value)
902
+
903
+
882
904
  class Context(object):
883
905
  """
884
906
  Execution context and dependency wrapper.
@@ -904,18 +926,14 @@ class Context(object):
904
926
  for dep in reversed(self._node.children):
905
927
  for artifact in dep.artifacts:
906
928
  # Create clone with tools from this task
907
- artifact = self._cache.get_artifact(
908
- dep,
909
- name=artifact.name,
910
- session=artifact.is_session(),
911
- tools=self._node.tools,
912
- )
929
+ artifact = ArtifactToolsProxy(artifact, self._node.tools)
913
930
 
914
931
  # Don't include session artifacts that don't exist,
915
932
  # i.e. where no build has taken place due to presence
916
933
  # of the persistent artifacts.
917
- if artifact.is_session() and not self._cache.is_available_locally(artifact):
918
- continue
934
+ if not dep.is_resource():
935
+ if artifact.is_session() and not self._cache.is_available_locally(artifact):
936
+ continue
919
937
 
920
938
  self._cache.unpack(artifact)
921
939
 
@@ -1,4 +1,4 @@
1
- from contextlib import contextmanager, ExitStack
1
+ from contextlib import contextmanager, ExitStack, nullcontext
2
2
  import copy
3
3
  import hashlib
4
4
  from os import getenv
@@ -144,7 +144,11 @@ class TaskProxy(object):
144
144
 
145
145
  @property
146
146
  def instance(self):
147
- return self.task._instance.value
147
+ return self.task.instance
148
+
149
+ @instance.setter
150
+ def instance(self, value):
151
+ self.task.instance = value
148
152
 
149
153
  @property
150
154
  def is_unstable(self):
@@ -183,7 +187,7 @@ class TaskProxy(object):
183
187
  return len(self.ancestors) > 0
184
188
 
185
189
  def has_artifact(self):
186
- return self.is_cacheable() and not self.is_resource() and not self.is_alias()
190
+ return self.is_cacheable() and not self.is_alias()
187
191
 
188
192
  def has_extensions(self):
189
193
  return len(self.extensions) > 0
@@ -212,12 +216,14 @@ class TaskProxy(object):
212
216
  def is_alias(self):
213
217
  return isinstance(self.task, Alias)
214
218
 
215
- def is_available_locally(self, extensions=True):
219
+ def is_available_locally(self, extensions=True, persistent_only=True):
216
220
  dep_artifacts = []
217
221
  if extensions:
218
222
  for dep in self.extensions:
219
223
  dep_artifacts += dep.artifacts
220
- artifacts = filter(lambda a: not a.is_session(), self._artifacts + dep_artifacts)
224
+ artifacts = self._artifacts + dep_artifacts
225
+ if persistent_only:
226
+ artifacts = filter(lambda a: not a.is_session(), artifacts)
221
227
  return all(map(self.cache.is_available_locally, artifacts))
222
228
 
223
229
  def is_available_remotely(self, extensions=True, cache=True):
@@ -250,6 +256,12 @@ class TaskProxy(object):
250
256
  def is_goal(self, with_extensions=True):
251
257
  return self._goal or (with_extensions and any([e.is_goal() for e in self.extensions]))
252
258
 
259
+ def is_local(self):
260
+ if self.is_extension():
261
+ return self.get_extended_task().is_local()
262
+ tasks = [self.task] + [e.task for e in self.extensions]
263
+ return any([task.local for task in tasks])
264
+
253
265
  def in_progress(self):
254
266
  return self._in_progress
255
267
 
@@ -316,7 +328,7 @@ class TaskProxy(object):
316
328
  self._download = False
317
329
 
318
330
  def download(self, force=False, session_only=False, persistent_only=False):
319
- if not self.is_downloadable():
331
+ if not force and not self.is_downloadable():
320
332
  return True
321
333
  artifacts = self._artifacts
322
334
  if session_only:
@@ -420,12 +432,13 @@ class TaskProxy(object):
420
432
  self._finalized = True
421
433
  self.identity
422
434
 
423
- self._artifacts.extend(self.task._artifacts(self.cache, self))
424
-
425
435
  hooks.task_created(self)
426
436
 
427
437
  return self.identity
428
438
 
439
+ def finalize_artifacts(self):
440
+ self._artifacts.extend(self.task._artifacts(self.cache, self))
441
+
429
442
  def taint(self, salt=None):
430
443
  self.task.taint = self.task.taint or salt or uuid.uuid4()
431
444
  if salt is None:
@@ -570,21 +583,20 @@ class TaskProxy(object):
570
583
  for child in self.children:
571
584
  if not child.has_artifact():
572
585
  continue
586
+
587
+ if child.is_resource() and child.is_local() and child.options.worker:
588
+ return raise_task_error_if(
589
+ not child.download(force=True),
590
+ child, "Failed to download task artifact")
591
+
573
592
  raise_task_error_if(
574
593
  not child.is_completed() and child.is_unstable,
575
594
  self, "Task depends on failed task '{}'", child.short_qualified_name)
576
595
  if not child.is_available_locally(extensions=False):
577
596
  raise_task_error_if(
578
- not child.download(persistent_only=True),
597
+ not child.download(force=True),
579
598
  child, "Failed to download task artifact")
580
599
 
581
- def _run_prepare_resources(self, cache, force_upload=False, force_build=False):
582
- from jolt.scheduler import ExecutorRegistry, JoltEnvironment
583
-
584
- for child in filter(lambda task: task.is_resource() and not task.is_completed(), reversed(self.children)):
585
- executor = ExecutorRegistry.get().create_local(child)
586
- executor.run(JoltEnvironment(cache=cache))
587
-
588
600
  def partially_available_locally(self):
589
601
  availability = map(lambda a: self.cache.is_available_locally(a), self.artifacts)
590
602
  return any(availability) and not all(availability)
@@ -607,16 +619,126 @@ class TaskProxy(object):
607
619
  arch != platform_arch,
608
620
  self, f"Task is not runnable on current platform (wants node.arch={arch})")
609
621
 
610
- def run(self, cache, force_upload=False, force_build=False):
622
+ def run_acquire(self, artifact, owner, log_prefix=False):
623
+ """
624
+ Acquires a resource and publishes its artifact.
625
+
626
+ The artifact is published to the cache even if the acquisition fails.
627
+ """
628
+ try:
629
+ if not isinstance(self.task, WorkspaceResource):
630
+ ts = utils.duration()
631
+ log.info(colors.blue("Resource acquisition started ({} for {})"), self.short_qualified_name, owner.short_qualified_name)
632
+
633
+ try:
634
+ with log.thread_prefix(owner.identity[:8]) if log_prefix else nullcontext():
635
+ acquire = getattr(self.task, "acquire_" + artifact.name) if artifact.name != "main" else self.task.acquire
636
+ acquire(artifact, self.deps, self.tools, owner.task)
637
+ finally:
638
+ # Always commit the resource session artifact to the cache, even if the acquisition failed.
639
+ self.cache.commit(artifact)
640
+
641
+ if not isinstance(self.task, WorkspaceResource):
642
+ log.info(colors.green("Resource acquisition finished after {} ({} for {})"), ts, self.short_qualified_name, owner.short_qualified_name)
643
+
644
+ except (KeyboardInterrupt, Exception) as e:
645
+ if not isinstance(self.task, WorkspaceResource):
646
+ log.error(colors.red("Resource acquisition failed after {} ({} for {})"), ts, self.short_qualified_name, owner.short_qualified_name)
647
+ if self.task.release_on_error:
648
+ with utils.ignore_exception():
649
+ self.run_release(artifact, owner)
650
+ raise e
651
+
652
+ def run_release(self, artifact, owner, log_prefix=False):
653
+ """
654
+ Releases a resource.
655
+ """
656
+ try:
657
+ if not isinstance(self.task, WorkspaceResource):
658
+ ts = utils.duration()
659
+ log.info(colors.blue("Resource release started ({} for {})"), self.short_qualified_name, owner.short_qualified_name)
660
+
661
+ with log.thread_prefix(owner.identity[:8]) if log_prefix else nullcontext():
662
+ release = getattr(self.task, "release_" + artifact.name) if artifact.name != "main" else self.task.release
663
+ release(artifact, self.deps, self.tools, owner.task)
664
+
665
+ if not isinstance(self.task, WorkspaceResource):
666
+ log.info(colors.green("Resource release finished after {} ({} for {})"), ts, self.short_qualified_name, owner.short_qualified_name)
667
+
668
+ except (KeyboardInterrupt, Exception) as e:
669
+ if not isinstance(self.task, WorkspaceResource):
670
+ log.error(colors.red("Resource release failed after {} ({} for {})"), ts, self.short_qualified_name, owner.short_qualified_name)
671
+ raise e
672
+
673
+ @contextmanager
674
+ def run_resources(self):
675
+ """
676
+ Acquires and releases resources for the task.
677
+
678
+ The method is called by executors before invoking the task proxy's run() method.
679
+ Resource dependencies are acquired and released in reverse order. If an acquisition fails,
680
+ already acquired resources are released in reverse order and the exception is propagated
681
+ to the caller.
682
+
683
+ Resource artifacts are always published and uploaded if the acquisition has been started,
684
+ even if the acquisition fails. That way, a failed acquisition can be debugged.
685
+ """
686
+
687
+ # Log messages are prefixed with task identity if resources are acquired in parallel
688
+ log_prefix = False
689
+
690
+ # Collect list of resource dependencies
691
+ resource_deps = [child for child in self.children if child.is_resource()]
692
+
693
+ if self.options.worker:
694
+ # Exclude local resources when running as worker. They are already acquired by the client.
695
+ resource_deps = [child for child in resource_deps if not child.is_local()]
696
+ elif self.options.network and not self.is_local():
697
+ # Exclude non-local resources in the client when running a network build.
698
+ # They are acquired by the remote worker.
699
+ resource_deps = [child for child in resource_deps if child.is_local()]
700
+ log_prefix = True
701
+
702
+ exitstack = ExitStack()
703
+ acquired = []
704
+ try:
705
+ # Acquire resource dependencies in reverse order.
706
+ for resource in reversed(resource_deps):
707
+ # Download resource dependencies if not already done.
708
+ resource._run_download_dependencies(self.cache, force_upload=False, force_build=False)
709
+
710
+ with resource.lock_artifacts(discard=False):
711
+ resource.deps = self.cache.get_context(resource)
712
+ exitstack.enter_context(resource.deps)
713
+
714
+ # Just like tasks, a resource may have multiple artifacts. Run acquire for each artifact.
715
+ for artifact in resource.artifacts:
716
+ try:
717
+ resource.run_acquire(artifact, self, log_prefix=log_prefix)
718
+ acquired.append(resource)
719
+ finally:
720
+ # Always upload the artifact session artifact to the cache, even if the acquisition failed.
721
+ if not resource.is_workspace_resource():
722
+ resource.upload(locked=False, session_only=True, artifacts=[artifact])
723
+
724
+ yield
725
+
726
+ finally:
727
+ for resource in reversed(acquired):
728
+ for artifact in resource.artifacts:
729
+ resource.run_release(artifact, self, log_prefix=log_prefix)
730
+ exitstack.close()
731
+
732
+ def run(self, env, force_upload=False, force_build=False):
733
+ cache = env.cache
734
+ queue = env.queue
735
+
611
736
  with self.tools:
612
737
  available_locally = available_remotely = False
613
738
 
614
739
  # Download dependency artifacts if not already done
615
740
  self._run_download_dependencies(cache, force_upload, force_build)
616
741
 
617
- # Prepare resources if not already done. They are not acquired yet.
618
- self._run_prepare_resources(cache, force_upload, force_build)
619
-
620
742
  # Check if task artifact is available locally or remotely,
621
743
  # either skip execution or download it if necessary.
622
744
  if not force_build:
@@ -689,10 +811,14 @@ class TaskProxy(object):
689
811
  raise e
690
812
 
691
813
  except Exception as e:
692
- self.failed_execution()
814
+ self.failed_execution(interrupt=queue.is_aborted() if queue else False)
815
+
693
816
  with utils.ignore_exception():
694
817
  exitstack.close()
695
818
 
819
+ if queue is not None and queue.is_aborted():
820
+ raise KeyboardInterrupt()
821
+
696
822
  if cli.debug_enabled:
697
823
  import pdb
698
824
  extype, value, tb = sys.exc_info()
@@ -734,7 +860,7 @@ class TaskProxy(object):
734
860
 
735
861
  for extension in self.extensions:
736
862
  with hooks.task_run(extension):
737
- extension.run(cache, force_upload, force_build)
863
+ extension.run(env, force_upload, force_build)
738
864
 
739
865
  def publish(self, context, artifact, buildlog=None):
740
866
  hooks.task_prepublish(self, artifact, self.tools)
@@ -929,7 +1055,7 @@ class GraphBuilder(object):
929
1055
  self.progress = progress
930
1056
  self.options = options or JoltOptions()
931
1057
 
932
- def _get_node(self, progress, name):
1058
+ def _get_node(self, progress, name, parent=None):
933
1059
  name = utils.stable_task_name(name)
934
1060
  node = self.nodes.get(name)
935
1061
  if not node:
@@ -938,8 +1064,12 @@ class GraphBuilder(object):
938
1064
  if node is not None:
939
1065
  return node
940
1066
  node = TaskProxy(task, self.graph, self.cache, self.options)
941
- self.nodes[node.short_qualified_name] = node
942
- self.nodes[node.qualified_name] = node
1067
+ if not node.is_resource():
1068
+ self.nodes[node.short_qualified_name] = node
1069
+ self.nodes[node.qualified_name] = node
1070
+ elif parent:
1071
+ # A resource inherits its instance uuid from the consuming task
1072
+ node.instance = parent.instance
943
1073
  if self.options.salt:
944
1074
  node.taint(self.options.salt)
945
1075
  self._build_node(progress, node)
@@ -950,7 +1080,7 @@ class GraphBuilder(object):
950
1080
  self.graph.add_node(node)
951
1081
 
952
1082
  if node.task.extends:
953
- extended_node = self._get_node(progress, node.task.extends)
1083
+ extended_node = self._get_node(progress, node.task.extends, parent=node)
954
1084
  self.graph.add_edges_from([(node, extended_node)])
955
1085
  node.set_extended_task(extended_node)
956
1086
  extended_node.add_extension(node)
@@ -961,7 +1091,8 @@ class GraphBuilder(object):
961
1091
 
962
1092
  for requirement in node.task.requires:
963
1093
  alias, artifact, task, name = utils.parse_aliased_task_name(requirement)
964
- child = self._get_node(progress, utils.format_task_name(task, name))
1094
+ child = self._get_node(progress, utils.format_task_name(task, name), parent=node)
1095
+
965
1096
  # Create direct edges from alias parents to alias children
966
1097
  if child.is_alias():
967
1098
  for child_child in child.children:
@@ -997,6 +1128,11 @@ class GraphBuilder(object):
997
1128
  node.finalize(self.graph, self.manifest)
998
1129
  p.update(1)
999
1130
 
1131
+ # Create artifacts in forward order so that parent identities are available
1132
+ # when creating resource artifacts that depend on them.
1133
+ for node in topological_nodes:
1134
+ node.finalize_artifacts()
1135
+
1000
1136
  max_time = 0
1001
1137
  min_time = 0
1002
1138
  for node in topological_nodes:
@@ -143,6 +143,25 @@ class TqdmStream(object):
143
143
  getattr(self.stream, 'flush', lambda: None)()
144
144
 
145
145
 
146
+ class ThreadPrefix(logging.LoggerAdapter):
147
+ thread_prefix = {}
148
+
149
+ def process(self, msg, kwargs):
150
+ tid = threading.current_thread()
151
+ if tid in self.thread_prefix:
152
+ msg = f"[{self.thread_prefix[tid]}] {msg}"
153
+ return msg, kwargs
154
+
155
+ def set_thread_prefix(self, prefix):
156
+ tid = threading.current_thread()
157
+ self.thread_prefix[tid] = prefix
158
+
159
+ def clear_thread_prefix(self):
160
+ tid = threading.current_thread()
161
+ if tid in self.thread_prefix:
162
+ del self.thread_prefix[tid]
163
+
164
+
146
165
  # silence root logger
147
166
  _root = logging.getLogger()
148
167
  _root.setLevel(logging.CRITICAL)
@@ -175,6 +194,7 @@ _stderr.addFilter(Filter(lambda r: r.levelno >= ERROR or r.levelno == EXCEPTION)
175
194
 
176
195
  _logger.addHandler(_stdout)
177
196
  _logger.addHandler(_stderr)
197
+ _logger_frontend = ThreadPrefix(_logger, {})
178
198
 
179
199
  _file_formatter = Formatter('{asctime} [{levelname:>7}] {message}')
180
200
 
@@ -212,35 +232,35 @@ def log(level, message, created=None, context=None, prefix=False):
212
232
 
213
233
 
214
234
  def info(fmt, *args, **kwargs):
215
- _logger.log(INFO, fmt, *args, **kwargs)
235
+ _logger_frontend.log(INFO, fmt, *args, **kwargs)
216
236
 
217
237
 
218
238
  def warning(fmt, *args, **kwargs):
219
- _logger.log(WARNING, fmt, *args, **kwargs)
239
+ _logger_frontend.log(WARNING, fmt, *args, **kwargs)
220
240
 
221
241
 
222
242
  def verbose(fmt, *args, **kwargs):
223
- _logger.log(VERBOSE, fmt, *args, **kwargs)
243
+ _logger_frontend.log(VERBOSE, fmt, *args, **kwargs)
224
244
 
225
245
 
226
246
  def debug(fmt, *args, **kwargs):
227
- _logger.log(DEBUG, fmt, *args, **kwargs)
247
+ _logger_frontend.log(DEBUG, fmt, *args, **kwargs)
228
248
 
229
249
 
230
250
  def error(fmt, *args, **kwargs):
231
- _logger.log(ERROR, fmt, *args, **kwargs)
251
+ _logger_frontend.log(ERROR, fmt, *args, **kwargs)
232
252
 
233
253
 
234
254
  def stdout(line, **kwargs):
235
255
  line = line.replace("{", "{{")
236
256
  line = line.replace("}", "}}")
237
- _logger.log(STDOUT, line, extra=kwargs)
257
+ _logger_frontend.log(STDOUT, line, extra=kwargs)
238
258
 
239
259
 
240
260
  def stderr(line, **kwargs):
241
261
  line = line.replace("{", "{{")
242
262
  line = line.replace("}", "}}")
243
- _logger.log(STDERR, line, extra=kwargs)
263
+ _logger_frontend.log(STDERR, line, extra=kwargs)
244
264
 
245
265
 
246
266
  def format_exception_msg(exc):
@@ -273,7 +293,7 @@ def format_exception_msg(exc):
273
293
  def exception(exc=None, error=True):
274
294
  if exc:
275
295
  if error:
276
- _logger.log(ERROR, format_exception_msg(exc))
296
+ _logger_frontend.log(ERROR, format_exception_msg(exc))
277
297
 
278
298
  tb = traceback.format_exception(type(exc), value=exc, tb=exc.__traceback__)
279
299
  installdir = fs.path.dirname(__file__)
@@ -288,7 +308,7 @@ def exception(exc=None, error=True):
288
308
  line = line.replace("{", "{{")
289
309
  line = line.replace("}", "}}")
290
310
  line = line.strip()
291
- _logger.log(EXCEPTION, line)
311
+ _logger_frontend.log(EXCEPTION, line)
292
312
 
293
313
 
294
314
  def transfer(line, context):
@@ -308,7 +328,7 @@ def transfer(line, context):
308
328
  elif line.startswith("[ EXCEPT]"):
309
329
  outline1 = outline1.replace("{", "{{")
310
330
  outline1 = outline1.replace("}", "}}")
311
- _logger.log(EXCEPTION, outline1)
331
+ _logger_frontend.log(EXCEPTION, outline1)
312
332
  elif line.startswith("[ STDERR]"):
313
333
  stderr(outline1, prefix=True)
314
334
  elif line.startswith("[ STDOUT]"):
@@ -491,6 +511,15 @@ def map_thread(thread_from, thread_to):
491
511
  _thread_map.unmap(tid)
492
512
 
493
513
 
514
+ @contextmanager
515
+ def thread_prefix(prefix):
516
+ try:
517
+ _logger_frontend.set_thread_prefix(prefix)
518
+ yield
519
+ finally:
520
+ _logger_frontend.clear_thread_prefix()
521
+
522
+
494
523
  class _LogStream(object):
495
524
  def __init__(self):
496
525
  self.buf = ""
@@ -25,8 +25,6 @@ class DockerListVariable(ArtifactListAttribute):
25
25
 
26
26
  class DockerLoadListVariable(DockerListVariable):
27
27
  def apply(self, task, artifact):
28
- if isinstance(task, Resource):
29
- return
30
28
  for image in self.items():
31
29
  task.tools.run(
32
30
  "docker load -i {}",
@@ -35,16 +33,12 @@ class DockerLoadListVariable(DockerListVariable):
35
33
 
36
34
  class DockerPullListVariable(DockerListVariable):
37
35
  def apply(self, task, artifact):
38
- if isinstance(task, Resource):
39
- return
40
36
  for image in self.items():
41
37
  task.tools.run("docker pull {}", image, output_on_error=True)
42
38
 
43
39
 
44
40
  class DockerRmiListVariable(DockerListVariable):
45
41
  def unapply(self, task, artifact):
46
- if isinstance(task, Resource):
47
- return
48
42
  for image in self.items():
49
43
  task.tools.run("docker rmi -f {}", image, output_on_error=True)
50
44
 
@@ -499,6 +493,9 @@ class DockerImage(Task):
499
493
  """
500
494
  abstract = True
501
495
 
496
+ annotations = []
497
+ """ A list of image annotations """
498
+
502
499
  autoload = True
503
500
  """
504
501
  Automatically load image file into local registry when the artifact is
@@ -585,6 +582,10 @@ class DockerImage(Task):
585
582
  with _Tarfile.open(layerpath, 'r') as tar:
586
583
  tar.extractall(targetpath)
587
584
 
585
+ @property
586
+ def _annotations(self):
587
+ return " ".join([utils.option("--annotation ", self.tools.expand(an)) for an in self.annotations])
588
+
588
589
  @property
589
590
  def _buildargs(self):
590
591
  return " ".join([utils.option("--build-arg ", self.tools.expand(ba)) for ba in self.buildargs])
@@ -626,7 +627,7 @@ class DockerImage(Task):
626
627
  tools.expand_relpath(context))
627
628
 
628
629
  with tools.cwd(context):
629
- tools.run("docker build {_platform} . -f {} {_buildargs} {_labels} {_tags} {pull}{squash}",
630
+ tools.run("docker build {_platform} . -f {} {_annotations} {_buildargs} {_labels} {_tags} {pull}{squash}",
630
631
  utils.quote(dockerfile), pull=pull, squash=squash)
631
632
 
632
633
  try: