jolt 0.9.76__py3-none-any.whl → 0.9.429__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of jolt might be problematic. Click here for more details.

Files changed (201) hide show
  1. jolt/__init__.py +88 -7
  2. jolt/__main__.py +9 -1
  3. jolt/bin/fstree-darwin-x86_64 +0 -0
  4. jolt/bin/fstree-linux-x86_64 +0 -0
  5. jolt/cache.py +839 -367
  6. jolt/chroot.py +156 -0
  7. jolt/cli.py +362 -143
  8. jolt/common_pb2.py +63 -0
  9. jolt/common_pb2_grpc.py +4 -0
  10. jolt/config.py +99 -42
  11. jolt/error.py +19 -4
  12. jolt/expires.py +2 -2
  13. jolt/filesystem.py +8 -6
  14. jolt/graph.py +705 -117
  15. jolt/hooks.py +63 -1
  16. jolt/influence.py +129 -6
  17. jolt/loader.py +369 -121
  18. jolt/log.py +225 -63
  19. jolt/manifest.py +28 -38
  20. jolt/options.py +35 -10
  21. jolt/pkgs/abseil.py +42 -0
  22. jolt/pkgs/asio.py +25 -0
  23. jolt/pkgs/autoconf.py +41 -0
  24. jolt/pkgs/automake.py +41 -0
  25. jolt/pkgs/b2.py +31 -0
  26. jolt/pkgs/boost.py +111 -0
  27. jolt/pkgs/boringssl.py +32 -0
  28. jolt/pkgs/busybox.py +39 -0
  29. jolt/pkgs/bzip2.py +43 -0
  30. jolt/pkgs/cares.py +29 -0
  31. jolt/pkgs/catch2.py +36 -0
  32. jolt/pkgs/cbindgen.py +17 -0
  33. jolt/pkgs/cista.py +19 -0
  34. jolt/pkgs/clang.py +44 -0
  35. jolt/pkgs/cli11.py +23 -0
  36. jolt/pkgs/cmake.py +48 -0
  37. jolt/pkgs/cpython.py +196 -0
  38. jolt/pkgs/crun.py +29 -0
  39. jolt/pkgs/curl.py +38 -0
  40. jolt/pkgs/dbus.py +18 -0
  41. jolt/pkgs/double_conversion.py +24 -0
  42. jolt/pkgs/fastfloat.py +21 -0
  43. jolt/pkgs/ffmpeg.py +28 -0
  44. jolt/pkgs/flatbuffers.py +29 -0
  45. jolt/pkgs/fmt.py +27 -0
  46. jolt/pkgs/fstree.py +20 -0
  47. jolt/pkgs/gflags.py +18 -0
  48. jolt/pkgs/glib.py +18 -0
  49. jolt/pkgs/glog.py +25 -0
  50. jolt/pkgs/glslang.py +21 -0
  51. jolt/pkgs/golang.py +16 -11
  52. jolt/pkgs/googlebenchmark.py +18 -0
  53. jolt/pkgs/googletest.py +46 -0
  54. jolt/pkgs/gperf.py +15 -0
  55. jolt/pkgs/grpc.py +73 -0
  56. jolt/pkgs/hdf5.py +19 -0
  57. jolt/pkgs/help2man.py +14 -0
  58. jolt/pkgs/inja.py +28 -0
  59. jolt/pkgs/jsoncpp.py +31 -0
  60. jolt/pkgs/libarchive.py +43 -0
  61. jolt/pkgs/libcap.py +44 -0
  62. jolt/pkgs/libdrm.py +44 -0
  63. jolt/pkgs/libedit.py +42 -0
  64. jolt/pkgs/libevent.py +31 -0
  65. jolt/pkgs/libexpat.py +27 -0
  66. jolt/pkgs/libfastjson.py +21 -0
  67. jolt/pkgs/libffi.py +16 -0
  68. jolt/pkgs/libglvnd.py +30 -0
  69. jolt/pkgs/libogg.py +28 -0
  70. jolt/pkgs/libpciaccess.py +18 -0
  71. jolt/pkgs/libseccomp.py +21 -0
  72. jolt/pkgs/libtirpc.py +24 -0
  73. jolt/pkgs/libtool.py +42 -0
  74. jolt/pkgs/libunwind.py +35 -0
  75. jolt/pkgs/libva.py +18 -0
  76. jolt/pkgs/libvorbis.py +33 -0
  77. jolt/pkgs/libxml2.py +35 -0
  78. jolt/pkgs/libxslt.py +17 -0
  79. jolt/pkgs/libyajl.py +16 -0
  80. jolt/pkgs/llvm.py +81 -0
  81. jolt/pkgs/lua.py +54 -0
  82. jolt/pkgs/lz4.py +26 -0
  83. jolt/pkgs/m4.py +14 -0
  84. jolt/pkgs/make.py +17 -0
  85. jolt/pkgs/mesa.py +81 -0
  86. jolt/pkgs/meson.py +17 -0
  87. jolt/pkgs/mstch.py +28 -0
  88. jolt/pkgs/mysql.py +60 -0
  89. jolt/pkgs/nasm.py +49 -0
  90. jolt/pkgs/ncurses.py +30 -0
  91. jolt/pkgs/ng_log.py +25 -0
  92. jolt/pkgs/ninja.py +45 -0
  93. jolt/pkgs/nlohmann_json.py +25 -0
  94. jolt/pkgs/nodejs.py +19 -11
  95. jolt/pkgs/opencv.py +24 -0
  96. jolt/pkgs/openjdk.py +26 -0
  97. jolt/pkgs/openssl.py +103 -0
  98. jolt/pkgs/paho.py +76 -0
  99. jolt/pkgs/patchelf.py +16 -0
  100. jolt/pkgs/perl.py +42 -0
  101. jolt/pkgs/pkgconfig.py +64 -0
  102. jolt/pkgs/poco.py +39 -0
  103. jolt/pkgs/protobuf.py +77 -0
  104. jolt/pkgs/pugixml.py +27 -0
  105. jolt/pkgs/python.py +19 -0
  106. jolt/pkgs/qt.py +35 -0
  107. jolt/pkgs/rapidjson.py +26 -0
  108. jolt/pkgs/rapidyaml.py +28 -0
  109. jolt/pkgs/re2.py +30 -0
  110. jolt/pkgs/re2c.py +17 -0
  111. jolt/pkgs/readline.py +15 -0
  112. jolt/pkgs/rust.py +41 -0
  113. jolt/pkgs/sdl.py +28 -0
  114. jolt/pkgs/simdjson.py +27 -0
  115. jolt/pkgs/soci.py +46 -0
  116. jolt/pkgs/spdlog.py +29 -0
  117. jolt/pkgs/spirv_llvm.py +21 -0
  118. jolt/pkgs/spirv_tools.py +24 -0
  119. jolt/pkgs/sqlite.py +83 -0
  120. jolt/pkgs/ssl.py +12 -0
  121. jolt/pkgs/texinfo.py +15 -0
  122. jolt/pkgs/tomlplusplus.py +22 -0
  123. jolt/pkgs/wayland.py +26 -0
  124. jolt/pkgs/x11.py +58 -0
  125. jolt/pkgs/xerces_c.py +20 -0
  126. jolt/pkgs/xorg.py +360 -0
  127. jolt/pkgs/xz.py +29 -0
  128. jolt/pkgs/yamlcpp.py +30 -0
  129. jolt/pkgs/zeromq.py +47 -0
  130. jolt/pkgs/zlib.py +69 -0
  131. jolt/pkgs/zstd.py +33 -0
  132. jolt/plugins/alias.py +3 -0
  133. jolt/plugins/allure.py +5 -2
  134. jolt/plugins/autotools.py +66 -0
  135. jolt/plugins/cache.py +133 -0
  136. jolt/plugins/cmake.py +74 -6
  137. jolt/plugins/conan.py +238 -0
  138. jolt/plugins/cxx.py +698 -0
  139. jolt/plugins/cxxinfo.py +7 -0
  140. jolt/plugins/dashboard.py +1 -1
  141. jolt/plugins/docker.py +91 -23
  142. jolt/plugins/email.py +5 -2
  143. jolt/plugins/email.xslt +144 -101
  144. jolt/plugins/environ.py +11 -0
  145. jolt/plugins/fetch.py +141 -0
  146. jolt/plugins/gdb.py +44 -21
  147. jolt/plugins/gerrit.py +1 -14
  148. jolt/plugins/git.py +316 -101
  149. jolt/plugins/googletest.py +522 -1
  150. jolt/plugins/http.py +36 -38
  151. jolt/plugins/libtool.py +63 -0
  152. jolt/plugins/linux.py +990 -0
  153. jolt/plugins/logstash.py +4 -4
  154. jolt/plugins/meson.py +61 -0
  155. jolt/plugins/ninja-compdb.py +107 -31
  156. jolt/plugins/ninja.py +929 -134
  157. jolt/plugins/paths.py +11 -1
  158. jolt/plugins/pkgconfig.py +219 -0
  159. jolt/plugins/podman.py +148 -91
  160. jolt/plugins/python.py +137 -0
  161. jolt/plugins/remote_execution/__init__.py +0 -0
  162. jolt/plugins/remote_execution/administration_pb2.py +46 -0
  163. jolt/plugins/remote_execution/administration_pb2_grpc.py +170 -0
  164. jolt/plugins/remote_execution/log_pb2.py +32 -0
  165. jolt/plugins/remote_execution/log_pb2_grpc.py +68 -0
  166. jolt/plugins/remote_execution/scheduler_pb2.py +41 -0
  167. jolt/plugins/remote_execution/scheduler_pb2_grpc.py +141 -0
  168. jolt/plugins/remote_execution/worker_pb2.py +38 -0
  169. jolt/plugins/remote_execution/worker_pb2_grpc.py +112 -0
  170. jolt/plugins/report.py +12 -2
  171. jolt/plugins/rust.py +25 -0
  172. jolt/plugins/scheduler.py +710 -0
  173. jolt/plugins/selfdeploy/setup.py +9 -4
  174. jolt/plugins/selfdeploy.py +138 -88
  175. jolt/plugins/strings.py +35 -22
  176. jolt/plugins/symlinks.py +26 -11
  177. jolt/plugins/telemetry.py +5 -2
  178. jolt/plugins/timeline.py +13 -3
  179. jolt/plugins/volume.py +46 -48
  180. jolt/scheduler.py +591 -191
  181. jolt/tasks.py +1783 -245
  182. jolt/templates/export.sh.template +12 -6
  183. jolt/templates/timeline.html.template +44 -47
  184. jolt/timer.py +22 -0
  185. jolt/tools.py +749 -302
  186. jolt/utils.py +245 -18
  187. jolt/version.py +1 -1
  188. jolt/version_utils.py +2 -2
  189. jolt/xmldom.py +12 -2
  190. {jolt-0.9.76.dist-info → jolt-0.9.429.dist-info}/METADATA +98 -38
  191. jolt-0.9.429.dist-info/RECORD +207 -0
  192. {jolt-0.9.76.dist-info → jolt-0.9.429.dist-info}/WHEEL +1 -1
  193. jolt/plugins/amqp.py +0 -834
  194. jolt/plugins/debian.py +0 -338
  195. jolt/plugins/ftp.py +0 -181
  196. jolt/plugins/ninja-cache.py +0 -64
  197. jolt/plugins/ninjacli.py +0 -271
  198. jolt/plugins/repo.py +0 -253
  199. jolt-0.9.76.dist-info/RECORD +0 -79
  200. {jolt-0.9.76.dist-info → jolt-0.9.429.dist-info}/entry_points.txt +0 -0
  201. {jolt-0.9.76.dist-info → jolt-0.9.429.dist-info}/top_level.txt +0 -0
jolt/cli.py CHANGED
@@ -1,10 +1,12 @@
1
1
  import atexit
2
2
  import click
3
+ import datetime
4
+ import os
5
+ import platform
3
6
  import subprocess
4
7
  import sys
5
8
  import uuid
6
9
  import webbrowser
7
- from os import _exit, environ, getcwd
8
10
 
9
11
  from jolt.tasks import Task, TaskRegistry, Parameter
10
12
  from jolt import scheduler
@@ -16,13 +18,12 @@ from jolt import log
16
18
  from jolt import __version__
17
19
  from jolt.log import logfile
18
20
  from jolt import config
19
- from jolt.loader import JoltLoader
21
+ from jolt.loader import JoltLoader, import_workspace
20
22
  from jolt import tools
21
23
  from jolt import utils
22
24
  from jolt.influence import HashInfluenceRegistry
23
25
  from jolt.options import JoltOptions
24
26
  from jolt import hooks
25
- from jolt.manifest import JoltManifest
26
27
  from jolt.error import JoltError
27
28
  from jolt.error import raise_error
28
29
  from jolt.error import raise_error_if
@@ -30,7 +31,7 @@ from jolt.error import raise_task_error_if
30
31
  from jolt.plugins import report
31
32
 
32
33
  debug_enabled = False
33
- workdir = getcwd()
34
+ workdir = os.getcwd()
34
35
 
35
36
 
36
37
  class ArgRequiredUnless(click.Argument):
@@ -54,10 +55,12 @@ class PluginGroup(click.Group):
54
55
 
55
56
  if cmd_name in ["export", "inspect"]:
56
57
  log.set_level(log.SILENCE)
57
- elif ctx.params.get("verbose", False):
58
- log.set_level(log.VERBOSE)
59
- elif ctx.params.get("extra_verbose", False):
58
+ elif ctx.params.get("verbose") >= 3:
59
+ log.set_level(log.EXCEPTION)
60
+ elif ctx.params.get("verbose") >= 2:
60
61
  log.set_level(log.DEBUG)
62
+ elif ctx.params.get("verbose") >= 1:
63
+ log.set_level(log.VERBOSE)
61
64
 
62
65
  config_files = ctx.params.get("config_file") or []
63
66
  for config_file in config_files:
@@ -72,10 +75,12 @@ class PluginGroup(click.Group):
72
75
 
73
76
  @click.group(cls=PluginGroup, invoke_without_command=True)
74
77
  @click.version_option(__version__)
75
- @click.option("-v", "--verbose", is_flag=True, help="Verbose output.")
76
- @click.option("-vv", "--extra-verbose", is_flag=True, help="Extra verbose output.")
78
+ @click.option("-v", "--verbose", count=True, help="Verbose output (repeat to raise verbosity).")
77
79
  @click.option("-c", "--config", "config_file", multiple=True, type=str,
78
80
  help="Load a configuration file or set a configuration key.")
81
+ @click.option("-C", "--chdir", type=str,
82
+ help="Change working directory before executing command.")
83
+ @click.option("--interpreter", "machine_interface", type=str, help="Used for debugging.", hidden=True)
79
84
  @click.option("-d", "--debugger", is_flag=True,
80
85
  help="Attach debugger on exception.")
81
86
  @click.option("-p", "--profile", is_flag=True, hidden=True,
@@ -86,6 +91,7 @@ class PluginGroup(click.Group):
86
91
  help="Add salt as task influence.")
87
92
  @click.option("-g", "--debug", is_flag=True, default=False, hidden=True,
88
93
  help="Start debug shell before executing task.")
94
+ @click.option("-m", "--mute", is_flag=True, help="Display task log only if it fails.")
89
95
  @click.option("-n", "--network", is_flag=True, default=False, hidden=True,
90
96
  help="Build on network.")
91
97
  @click.option("-l", "--local", is_flag=True, default=False, hidden=True,
@@ -96,8 +102,8 @@ class PluginGroup(click.Group):
96
102
  help="Number of tasks allowed to execute in parallel (1). ")
97
103
  @click.option("-h", "--help", is_flag=True, help="Show this message and exit.")
98
104
  @click.pass_context
99
- def cli(ctx, verbose, extra_verbose, config_file, debugger, profile,
100
- force, salt, debug, network, local, keep_going, jobs, help):
105
+ def cli(ctx, verbose, config_file, debugger, profile,
106
+ force, salt, debug, mute, network, local, keep_going, jobs, help, machine_interface, chdir):
101
107
  """
102
108
  A task execution tool.
103
109
 
@@ -121,11 +127,24 @@ def cli(ctx, verbose, extra_verbose, config_file, debugger, profile,
121
127
  global debug_enabled
122
128
  debug_enabled = debugger
123
129
 
130
+ if machine_interface:
131
+ log.enable_gdb()
132
+
133
+ if ctx.invoked_subcommand not in ["log", "report"]:
134
+ log.start_file_log()
135
+
136
+ if chdir:
137
+ global workdir
138
+ workdir = chdir
139
+ os.chdir(workdir)
140
+
141
+ log.verbose("Jolt version: {}", __version__)
124
142
  log.verbose("Jolt command: {}", " ".join([fs.path.basename(sys.argv[0])] + sys.argv[1:]))
125
- log.verbose("Jolt host: {}", environ.get("HOSTNAME", "localhost"))
143
+ log.verbose("Jolt host: {}", os.environ.get("HOSTNAME", "localhost"))
126
144
  log.verbose("Jolt install path: {}", fs.path.dirname(__file__))
145
+ log.verbose("Jolt workdir: {}", workdir)
127
146
 
128
- if ctx.invoked_subcommand in ["config"]:
147
+ if ctx.invoked_subcommand in ["config", "executor", "log"]:
129
148
  # Don't attempt to load any task recipes as they might require
130
149
  # plugins that are not yet configured.
131
150
  return
@@ -137,27 +156,13 @@ def cli(ctx, verbose, extra_verbose, config_file, debugger, profile,
137
156
  print(ctx.get_help())
138
157
  sys.exit(0)
139
158
 
140
- manifest = JoltManifest()
141
- utils.call_and_catch(manifest.parse)
142
- manifest.process_import()
143
- ctx.obj["manifest"] = manifest
144
-
145
- if manifest.version:
146
- from jolt.version_utils import requirement, version
147
- req = requirement(manifest.version)
148
- ver = version(__version__)
149
- raise_error_if(not req.satisfied(ver),
150
- "This project requires Jolt version {} (running {})",
151
- req, __version__)
152
-
159
+ registry = TaskRegistry.get()
153
160
  loader = JoltLoader.get()
154
- tasks = loader.load()
155
- for cls in tasks:
156
- TaskRegistry.get().add_task_class(cls)
161
+ loader.load(registry)
157
162
 
158
163
  if ctx.invoked_subcommand in ["build", "clean"] and loader.joltdir:
159
164
  ctx.obj["workspace_lock"] = utils.LockFile(
160
- fs.path.join(loader.joltdir, "build"),
165
+ loader.build_path,
161
166
  log.info, "Workspace is locked by another process, please wait...")
162
167
  atexit.register(ctx.obj["workspace_lock"].close)
163
168
 
@@ -166,8 +171,8 @@ def cli(ctx, verbose, extra_verbose, config_file, debugger, profile,
166
171
  if ctx.invoked_subcommand is None:
167
172
  task = config.get("jolt", "default", "default")
168
173
  taskname, _ = utils.parse_task_name(task)
169
- if TaskRegistry.get().get_task_class(taskname) is not None:
170
- ctx.invoke(build, task=[task], force=force, salt=salt, debug=debug,
174
+ if registry.get_task_class(taskname) is not None:
175
+ ctx.invoke(build, task=[task], force=force, salt=salt, debug=debug, mute=mute,
171
176
  network=network, local=local, keep_going=keep_going, jobs=jobs)
172
177
  else:
173
178
  print(cli.get_help(ctx))
@@ -175,11 +180,8 @@ def cli(ctx, verbose, extra_verbose, config_file, debugger, profile,
175
180
 
176
181
 
177
182
  def _autocomplete_tasks(ctx, args, incomplete):
178
- manifest = JoltManifest()
179
- utils.call_and_catch(manifest.parse)
180
- manifest.process_import()
181
-
182
- tasks = JoltLoader.get().load()
183
+ loader = JoltLoader.get()
184
+ tasks = loader.load()
183
185
  tasks = [task.name for task in tasks if task.name.startswith(incomplete or '')]
184
186
  return sorted(tasks)
185
187
 
@@ -197,10 +199,14 @@ def _autocomplete_tasks(ctx, args, incomplete):
197
199
  @click.option("-l", "--local", is_flag=True, default=False, help="Disable remote cache access.")
198
200
  @click.option("-n", "--network", is_flag=True, default=False, help="Distribute tasks to network workers.")
199
201
  @click.option("-s", "--salt", type=str, help="Add salt as hash influence for all tasks in dependency tree.", metavar="SALT")
202
+ @click.option("-m", "--mute", is_flag=True, help="Display task log only if it fails.")
203
+ @click.option("-v", "--verbose", count=True, help="Verbose output (repeat to raise verbosity).")
200
204
  @click.option("--result", type=click.Path(), hidden=True,
201
205
  help="Write result manifest to this file.")
202
206
  @click.option("--no-download", is_flag=True, default=False,
203
- help="Don't download artifacts from remote storage")
207
+ help="Don't download any artifacts from remote storage")
208
+ @click.option("--no-download-persistent", is_flag=True, default=False,
209
+ help="Don't download persistent artifacts from remote storage (only session artifacts)")
204
210
  @click.option("--no-upload", is_flag=True, default=False,
205
211
  help="Don't upload artifacts to remote storage")
206
212
  @click.option("--download", is_flag=True, default=False,
@@ -211,11 +217,13 @@ def _autocomplete_tasks(ctx, args, incomplete):
211
217
  help="Don't prune cached artifacts from the build graph. This option can be used to populate the local cache with remotely cached dependency artifacts.")
212
218
  @click.option("--worker", is_flag=True, default=False,
213
219
  help="Run with the worker build strategy", hidden=True)
220
+ @click.option("--environ", type=click.Path(), help="Import build environment from protobuf", hidden=True)
214
221
  @click.pass_context
215
222
  @hooks.cli_build
216
223
  def build(ctx, task, network, keep_going, default, local,
217
- no_download, no_upload, download, upload, worker, force,
218
- salt, copy, debug, result, jobs, no_prune):
224
+ no_download, no_download_persistent, no_upload, download, upload, worker, force,
225
+ salt, copy, debug, result, jobs, no_prune, verbose,
226
+ mute, environ):
219
227
  """
220
228
  Build task artifact.
221
229
 
@@ -247,6 +255,7 @@ def build(ctx, task, network, keep_going, default, local,
247
255
  are removed before execution starts.
248
256
 
249
257
  """
258
+
250
259
  raise_error_if(network and local,
251
260
  "The -n and -l flags are mutually exclusive")
252
261
 
@@ -259,8 +268,14 @@ def build(ctx, task, network, keep_going, default, local,
259
268
  raise_error_if(no_upload and upload,
260
269
  "The --upload and --no-upload flags are mutually exclusive")
261
270
 
262
- duration = utils.duration()
271
+ if verbose >= 3:
272
+ log.set_level(log.EXCEPTION)
273
+ elif verbose >= 2:
274
+ log.set_level(log.DEBUG)
275
+ elif verbose >= 1:
276
+ log.set_level(log.VERBOSE)
263
277
 
278
+ ts_start = utils.duration()
264
279
  task = list(task)
265
280
  task = [utils.stable_task_name(t) for t in task]
266
281
 
@@ -270,6 +285,7 @@ def build(ctx, task, network, keep_going, default, local,
270
285
  else:
271
286
  _download = config.getboolean("jolt", "download", True)
272
287
  _upload = config.getboolean("jolt", "upload", True)
288
+ _download_session = _download
273
289
 
274
290
  if local:
275
291
  _download = False
@@ -277,23 +293,60 @@ def build(ctx, task, network, keep_going, default, local,
277
293
  else:
278
294
  if no_download:
279
295
  _download = False
296
+ _download_session = False
297
+ if no_download_persistent:
298
+ _download = False
280
299
  if no_upload:
281
300
  _upload = False
282
301
  if download:
283
302
  _download = True
303
+ _download_session = True
284
304
  if upload:
285
305
  _upload = True
286
306
 
287
- options = JoltOptions(network=network,
288
- local=local,
289
- download=_download,
290
- upload=_upload,
291
- keep_going=keep_going,
292
- default=default,
293
- worker=worker,
294
- debug=debug,
295
- salt=salt,
296
- jobs=jobs)
307
+ if keep_going:
308
+ config.set_keep_going(True)
309
+
310
+ # Import build environment from protobuf if provided
311
+ buildenv = None
312
+ if environ:
313
+ with open(environ, "rb") as f:
314
+ from jolt import common_pb2 as common_pb
315
+ buildenv = common_pb.BuildEnvironment()
316
+ try:
317
+ buildenv.ParseFromString(f.read())
318
+ except Exception as e:
319
+ raise_error("Failed to parse build environment protobuf: {}", e)
320
+
321
+ # Import log level
322
+ log.set_level_pb(buildenv.loglevel)
323
+
324
+ # Import workspace
325
+ import_workspace(buildenv)
326
+
327
+ # Import configuration snippet
328
+ config.import_config(buildenv.config)
329
+
330
+ # Import configuration parameters (-c params.key)
331
+ config.import_params({param.key: param.value for param in buildenv.parameters})
332
+
333
+ # Import default parameters (-d taskname:param=value)
334
+ default = utils.as_list(default)
335
+ default += buildenv.task_default_parameters
336
+
337
+ options = JoltOptions(
338
+ network=network,
339
+ local=local,
340
+ download=_download,
341
+ download_session=_download_session,
342
+ upload=_upload,
343
+ keep_going=keep_going,
344
+ default=default,
345
+ worker=worker,
346
+ debug=debug,
347
+ salt=salt,
348
+ jobs=jobs,
349
+ mute=mute)
297
350
 
298
351
  acache = cache.ArtifactCache.get(options)
299
352
 
@@ -315,29 +368,29 @@ def build(ctx, task, network, keep_going, default, local,
315
368
  for params in default:
316
369
  registry.set_default_parameters(params)
317
370
 
318
- manifest = ctx.obj["manifest"]
371
+ log.info("Started: {}", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
319
372
 
320
- for mb in manifest.builds:
321
- for mt in mb.tasks:
322
- task.append(mt.name)
323
- for mt in mb.defaults:
324
- registry.set_default_parameters(mt.name)
373
+ gb = graph.GraphBuilder(registry, acache, options, progress=True, buildenv=buildenv)
374
+ dag = gb.build(task)
325
375
 
376
+ # If asked to force rebuild, taint all goal tasks
326
377
  if force:
327
- for goal in task:
328
- registry.get_task(goal, manifest=manifest).taint = uuid.uuid4()
378
+ for goal in dag.goals:
379
+ goal.get_extended_task().taint()
329
380
 
330
- gb = graph.GraphBuilder(registry, manifest, options, progress=True)
331
- dag = gb.build(task)
381
+ # Collect information about artifact presence before starting prune or build
382
+ acache.precheck(dag.persistent_artifacts, remote=not local)
332
383
 
384
+ # Prune the graph to remove tasks that are already available locally or remotely
333
385
  if not no_prune:
334
- gp = graph.GraphPruner(strategy)
386
+ gp = graph.GraphPruner(acache, strategy)
335
387
  dag = gp.prune(dag)
336
388
 
337
389
  goal_tasks = dag.goals
338
390
  goal_task_duration = 0
339
391
 
340
- queue = scheduler.TaskQueue(strategy)
392
+ session = executors.create_session(dag) if options.network else {}
393
+ queue = scheduler.TaskQueue()
341
394
 
342
395
  try:
343
396
  if not dag.has_tasks():
@@ -351,16 +404,20 @@ def build(ctx, task, network, keep_going, default, local,
351
404
  debug=debug)
352
405
 
353
406
  with progress:
354
- while dag.has_tasks():
407
+ in_progress = set()
408
+
409
+ while dag.has_tasks() or not queue.empty():
355
410
  # Find all tasks ready to be executed
356
- leafs = dag.select(lambda graph, task: task.is_ready())
411
+ leafs = dag.select(lambda graph, task: task.is_ready() and task not in in_progress)
357
412
 
358
413
  # Order the tasks by their weights to improve build times
359
414
  leafs.sort(key=lambda x: x.weight)
360
415
 
361
416
  while leafs:
362
417
  task = leafs.pop()
363
- queue.submit(acache, task)
418
+ executor = strategy.create_executor(session, task)
419
+ queue.submit(executor)
420
+ in_progress.add(task)
364
421
 
365
422
  task, error = queue.wait()
366
423
 
@@ -370,6 +427,7 @@ def build(ctx, task, network, keep_going, default, local,
370
427
  elif task.is_goal() and task.duration_running:
371
428
  goal_task_duration += task.duration_running.seconds
372
429
 
430
+ # Unpack tasks with overridden unpack() method
373
431
  if not task.is_resource():
374
432
  if no_prune and task.task.unpack.__func__ is not Task.unpack:
375
433
  with acache.get_context(task):
@@ -377,35 +435,50 @@ def build(ctx, task, network, keep_going, default, local,
377
435
 
378
436
  progress.update(1)
379
437
 
438
+ if no_prune and task.is_workspace_resource():
439
+ task.task.acquire_ws(force=True)
440
+
380
441
  if not keep_going and error is not None:
381
442
  queue.abort()
443
+ executors.shutdown()
444
+ task.raise_for_status()
382
445
  raise error
383
446
 
384
- if dag.failed:
447
+ if dag.failed or dag.unstable:
385
448
  log.error("List of failed tasks")
386
- for failed in dag.failed:
449
+ for failed in dag.failed + dag.unstable:
387
450
  log.error("- {}", failed.log_name.strip("()"))
451
+
452
+ for failed_task in dag.failed:
453
+ failed_task.raise_for_status()
454
+ if dag.failed:
388
455
  raise_error("No more tasks could be executed")
389
456
 
390
- for goal in goal_tasks:
391
- if acache.is_available_locally(goal):
392
- with acache.get_artifact(goal) as artifact:
393
- log.info("Location: {0}", artifact.path)
394
- if copy:
395
- artifact.copy("*", utils.as_dirpath(fs.path.join(workdir, click.format_filename(copy))), symlinks=True)
396
457
  except KeyboardInterrupt:
397
458
  print()
398
459
  log.warning("Interrupted by user")
399
460
  try:
400
461
  queue.abort()
462
+ executors.shutdown()
401
463
  sys.exit(1)
402
464
  except KeyboardInterrupt:
403
465
  print()
404
466
  log.warning("Interrupted again, exiting")
405
- _exit(1)
467
+ os._exit(1)
406
468
  finally:
469
+ queue.shutdown()
470
+
471
+ for task in goal_tasks:
472
+ for artifact in task.artifacts:
473
+ if acache.is_available_locally(artifact):
474
+ log.info("Location: {0}", artifact.path)
475
+ if copy:
476
+ dst = utils.as_dirpath(fs.path.join(workdir, click.format_filename(copy)))
477
+ artifact.copy("*", dst, symlinks=True)
478
+
479
+ log.info("Ended: {}", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
407
480
  log.info("Total execution time: {0} {1}",
408
- str(duration),
481
+ str(ts_start),
409
482
  str(queue.duration_acc) if network else '')
410
483
  if result:
411
484
  with report.update() as manifest:
@@ -441,7 +514,7 @@ def clean(ctx, task, deps, expired):
441
514
  if task:
442
515
  task = [utils.stable_task_name(t) for t in task]
443
516
  registry = TaskRegistry.get()
444
- dag = graph.GraphBuilder(registry, ctx.obj["manifest"]).build(task)
517
+ dag = graph.GraphBuilder(registry, acache).build(task)
445
518
  if deps:
446
519
  tasks = dag.tasks
447
520
  else:
@@ -576,20 +649,20 @@ def _config(ctx, list, delete, global_, user, key, value):
576
649
 
577
650
  @cli.command()
578
651
  @click.argument("task", type=str, nargs=-1, required=False, shell_complete=_autocomplete_tasks)
579
- @click.option("-r", "--reverse", type=str, help="Display consumers of REVERSE if TASK is executed.")
580
652
  @click.option("-c", "--cached", "show_cache", is_flag=True, help="Highlight cache presence with colors.")
653
+ @click.option("-r", "--reverse", type=str, help="Display consumers of REVERSE if TASK is executed.")
654
+ @click.option("-p", "--prune", "prune", is_flag=True, help="Do not repeat tasks. An already visited task's name is printed inside [square brackets] and its deps are omitted.")
581
655
  @click.pass_context
582
- def display(ctx, task, reverse=None, show_cache=False):
656
+ def display(ctx, task, reverse=None, show_cache=False, prune=False):
583
657
  """
584
658
  Display a task and its dependencies visually.
585
659
 
586
660
  """
587
661
  registry = TaskRegistry.get()
588
- gb = graph.GraphBuilder(registry, ctx.obj["manifest"])
589
- dag = gb.build(task, influence=show_cache)
590
-
591
662
  options = JoltOptions()
592
663
  acache = cache.ArtifactCache.get(options)
664
+ gb = graph.GraphBuilder(registry, acache)
665
+ dag = gb.build(task, influence=show_cache)
593
666
 
594
667
  if reverse:
595
668
  def iterator(task):
@@ -604,6 +677,8 @@ def display(ctx, task, reverse=None, show_cache=False):
604
677
  tasklist = dag.requested_goals
605
678
 
606
679
  if dag.has_tasks():
680
+ processed = set()
681
+
607
682
  def _display(task, indent=0, last=None):
608
683
  header = ""
609
684
  if indent > 0:
@@ -624,10 +699,22 @@ def display(ctx, task, reverse=None, show_cache=False):
624
699
  else:
625
700
  colorize = colors.green
626
701
 
627
- print(header + colorize(task.short_qualified_name))
702
+ if prune and task.short_qualified_name in processed:
703
+ def prune_marker(n):
704
+ return "[" + n + "]"
705
+ else:
706
+ def prune_marker(n):
707
+ return n
708
+
709
+ print(header + colorize(prune_marker(task.short_qualified_name)))
628
710
  children = iterator(task)
629
- for i in range(0, len(children)):
630
- _display(children[i], indent + 1, last=(last or []) + [i + 1 != len(children)])
711
+ if not prune or task.short_qualified_name not in processed:
712
+ for i in range(0, len(children)):
713
+ _display(children[i], indent + 1, last=(last or []) + [i + 1 != len(children)])
714
+
715
+ if prune:
716
+ processed.add(task.short_qualified_name)
717
+
631
718
  for task in tasklist:
632
719
  _display(task)
633
720
  else:
@@ -639,58 +726,103 @@ def docs():
639
726
  """
640
727
  Opens the Jolt documentation in the default webbrowser.
641
728
  """
642
- webbrowser.open(config.get("jolt", "docs", "http://jolt.readthedocs.io/"))
729
+ url = config.get("jolt", "docs", "http://jolt.readthedocs.io/")
730
+ success = False
731
+ try:
732
+ success = webbrowser.open(url)
733
+ except Exception:
734
+ pass
735
+ if not success:
736
+ print(f"Failed to open web browser. Visit {url} manually.")
643
737
 
644
738
 
645
- @cli.command(hidden=True)
646
- @click.argument("task", type=str, nargs=-1, required=True)
647
- @click.option("-r", "--remove", is_flag=True, help="Remove tasks from existing manifest.")
648
- @click.option("-d", "--default", type=str, multiple=True, help="Override default parameter values.")
649
- @click.option("-o", "--output", type=str, default="default.joltxmanifest", help="Manifest filename.")
739
+ @cli.command()
740
+ @click.argument("task", type=str, nargs=-1, required=True, shell_complete=_autocomplete_tasks)
741
+ @click.option("-c", "--copy", type=click.Path(),
742
+ help="Copy artifact content to directory PATH.")
743
+ @click.option("-ca", "--copy-all", type=click.Path(), help="Copy artifacts, including dependency artifacts, to directory PATH. Implies --deps.")
744
+ @click.option("-d", "--deps", is_flag=True, help="Download dependencies.")
650
745
  @click.pass_context
651
- def freeze(ctx, task, default, output, remove):
746
+ @hooks.cli_download
747
+ def download(ctx, task, deps, copy, copy_all):
652
748
  """
653
- Freeze the identity of a task.
749
+ Download task artifacts from remote caches.
750
+
751
+ No attempt to build the artifact is made if it is not present
752
+ in configured remote caches.
654
753
 
655
- <WIP>
754
+ Task dependencies may optionally be downloaded by passing the -d option.
656
755
  """
657
- manifest = ctx.obj["manifest"]
658
756
 
659
- options = JoltOptions(default=default)
757
+ raise_error_if(copy and copy_all, "--copy and --copy-all are mutually exclusive")
758
+ if copy_all:
759
+ deps = True
760
+
761
+ options = JoltOptions()
660
762
  acache = cache.ArtifactCache.get(options)
661
- scheduler.ExecutorRegistry.get(options)
763
+ hooks.TaskHookRegistry.get(options)
764
+ executors = scheduler.ExecutorRegistry.get(options)
662
765
  registry = TaskRegistry.get()
766
+ strategy = scheduler.DownloadStrategy(executors, acache)
767
+ queue = scheduler.TaskQueue()
768
+ gb = graph.GraphBuilder(registry, acache, options, progress=True)
769
+ dag = gb.build(task)
663
770
 
664
- for params in default:
665
- registry.set_default_parameters(params)
771
+ if not deps:
772
+ for task in dag.tasks:
773
+ if not task.is_goal():
774
+ task.pruned()
666
775
 
667
- gb = graph.GraphBuilder(registry, manifest)
668
- dag = gb.build(task)
776
+ all_tasks = dag.tasks
777
+ goal_tasks = dag.goals
669
778
 
670
- available_in_cache = [
671
- (t.is_available_locally(acache) or (
672
- t.is_available_remotely(acache) and acache.download_enabled()), t)
673
- for t in dag.tasks if t.is_cacheable()]
779
+ try:
780
+ with log.progress("Progress", dag.number_of_tasks(), " tasks", estimates=False, debug=False) as p:
781
+ in_progress = set()
674
782
 
675
- for available, task in available_in_cache:
676
- raise_task_error_if(
677
- not remove and not available, task,
678
- "Task artifact is not available in any cache, build it first")
783
+ while dag.has_tasks() or not queue.empty():
784
+ leafs = dag.select(lambda graph, task: task.is_ready() and task not in in_progress)
679
785
 
680
- for task in dag.tasks:
681
- if task.is_resource() or not task.is_cacheable():
682
- continue
683
- manifest_task = manifest.find_task(task)
684
- if remove and manifest_task:
685
- manifest.remove_task(manifest_task)
686
- continue
687
- if not remove:
688
- if not manifest_task:
689
- manifest_task = manifest.create_task()
690
- manifest_task.name = task.qualified_name
691
- manifest_task.identity = task.identity
786
+ while leafs:
787
+ task = leafs.pop()
788
+ executor = strategy.create_executor({}, task)
789
+ queue.submit(executor)
790
+ in_progress.add(task)
692
791
 
693
- manifest.write(fs.path.join(JoltLoader.get().joltdir, output))
792
+ task, error = queue.wait()
793
+ p.update(1)
794
+
795
+ copy_tasks = goal_tasks if not copy_all else all_tasks
796
+ for goal in copy_tasks:
797
+ if goal.is_available_locally():
798
+ for artifact in goal.artifacts:
799
+ if copy:
800
+ log.info("Copying: {0}", artifact.path)
801
+ artifact.copy("*", utils.as_dirpath(fs.path.join(workdir, click.format_filename(copy))), symlinks=True)
802
+ elif copy_all:
803
+ log.info("Copying: {0}", artifact.path)
804
+ artifact.copy("*", utils.as_dirpath(fs.path.join(workdir, click.format_filename(copy_all))), symlinks=True)
805
+ elif goal.is_goal():
806
+ log.info("Location: {0}", artifact.path)
807
+
808
+ except KeyboardInterrupt:
809
+ print()
810
+ log.warning("Interrupted by user")
811
+ try:
812
+ queue.abort()
813
+ executors.shutdown()
814
+ sys.exit(1)
815
+ except KeyboardInterrupt:
816
+ print()
817
+ log.warning("Interrupted again, exiting")
818
+ os._exit(1)
819
+
820
+ except Exception as e:
821
+ log.set_interactive(True)
822
+ raise e
823
+
824
+ finally:
825
+ queue.shutdown()
694
826
 
695
827
 
696
828
  @cli.command(name="list")
@@ -713,6 +845,8 @@ def _list(ctx, task=None, all=False, reverse=None):
713
845
 
714
846
  raise_error_if(not task and reverse, "TASK required with --reverse")
715
847
 
848
+ options = JoltOptions()
849
+ acache = cache.ArtifactCache.get(options)
716
850
  registry = TaskRegistry.get()
717
851
 
718
852
  if not task:
@@ -726,7 +860,7 @@ def _list(ctx, task=None, all=False, reverse=None):
726
860
  reverse = [utils.stable_task_name(t) for t in utils.as_list(reverse or [])]
727
861
 
728
862
  try:
729
- dag = graph.GraphBuilder(registry, ctx.obj["manifest"]).build(task, influence=False)
863
+ dag = graph.GraphBuilder(registry, acache).build(task, influence=False)
730
864
  except JoltError as e:
731
865
  raise e
732
866
  except Exception:
@@ -760,16 +894,21 @@ def _log(follow, delete):
760
894
  Display the Jolt log file.
761
895
 
762
896
  """
897
+ if not log.logfiles:
898
+ print("No logs exist")
899
+ return
900
+
763
901
  if follow:
764
- subprocess.call("tail -f {0}".format(logfile), shell=True)
902
+ subprocess.call("tail -f {0}".format(log.logfiles[-1]), shell=True)
765
903
  elif delete:
766
- fs.unlink(logfile)
904
+ for file in log.logfiles:
905
+ fs.unlink(file)
767
906
  else:
768
907
  t = tools.Tools()
769
- configured_pager = config.get("jolt", "pager", environ.get("PAGER", None))
908
+ configured_pager = config.get("jolt", "pager", os.environ.get("PAGER", None))
770
909
  for pager in [configured_pager, "less", "more", "cat"]:
771
910
  if pager and t.which(pager):
772
- return subprocess.call("{1} {0}".format(logfile, pager), shell=True)
911
+ return subprocess.call("{1} {0}".format(log.logfiles[-1], pager), shell=True)
773
912
  print(t.read_file(logfile))
774
913
 
775
914
 
@@ -821,9 +960,8 @@ def inspect(ctx, task, influence=False, artifact=False, salt=None):
821
960
 
822
961
  print()
823
962
  print(" Requirements")
824
- manifest = ctx.obj["manifest"]
825
963
  try:
826
- task = task_registry.get_task(task_name, manifest=manifest)
964
+ task = task_registry.get_task(task_name)
827
965
  for req in sorted(utils.as_list(utils.call_or_return(task, task.requires))):
828
966
  print(" {0}".format(task.tools.expand(req)))
829
967
  if not task.requires:
@@ -843,8 +981,9 @@ def inspect(ctx, task, influence=False, artifact=False, salt=None):
843
981
  task.taint = salt
844
982
 
845
983
  if artifact:
984
+ options = JoltOptions(salt=salt)
846
985
  acache = cache.ArtifactCache.get()
847
- builder = graph.GraphBuilder(task_registry, manifest)
986
+ builder = graph.GraphBuilder(task_registry, acache, options)
848
987
  dag = builder.build([task.qualified_name])
849
988
  tasks = dag.select(lambda graph, node: node.task is task)
850
989
  assert len(tasks) == 1, "graph produced multiple tasks, one expected"
@@ -853,14 +992,14 @@ def inspect(ctx, task, influence=False, artifact=False, salt=None):
853
992
 
854
993
  print(" Cache")
855
994
  print(" Identity {0}".format(proxy.identity))
856
- if acache.is_available_locally(proxy):
857
- with acache.get_artifact(proxy) as artifact:
995
+ if proxy.is_available_locally():
996
+ for artifact in filter(lambda a: not a.is_session(), proxy.artifacts):
858
997
  print(" Location {0}".format(artifact.path))
859
998
  print(" Local True ({0})".format(
860
- utils.as_human_size(acache.get_artifact(proxy).get_size())))
999
+ utils.as_human_size(sum([artifact.get_size() for artifact in proxy.artifacts]))))
861
1000
  else:
862
1001
  print(" Local False")
863
- print(" Remote {0}".format(acache.is_available_remotely(proxy)))
1002
+ print(" Remote {0}".format(proxy.is_available_remotely(cache=False)))
864
1003
  print()
865
1004
 
866
1005
  if influence:
@@ -908,10 +1047,10 @@ def _export(ctx, task):
908
1047
  executors = scheduler.ExecutorRegistry.get()
909
1048
  strategy = scheduler.LocalStrategy(executors, acache)
910
1049
 
911
- dag = graph.GraphBuilder(registry, ctx.obj["manifest"])
1050
+ dag = graph.GraphBuilder(registry, acache)
912
1051
  dag = dag.build(task)
913
1052
 
914
- gp = graph.GraphPruner(strategy)
1053
+ gp = graph.GraphPruner(acache, strategy)
915
1054
  dag = gp.prune(dag)
916
1055
 
917
1056
  class Export(object):
@@ -936,17 +1075,97 @@ def _export(ctx, task):
936
1075
  context = Context(tasks)
937
1076
 
938
1077
  for task in context.tasks:
939
- artifact = acache.get_artifact(task)
940
- raise_task_error_if(
941
- artifact.is_temporary(), task,
942
- "Task artifact not found in local cache, build it first")
1078
+ for artifact in task.artifacts:
1079
+ raise_task_error_if(
1080
+ not task.is_resource() and artifact.is_temporary(), task,
1081
+ "Task artifact not found in local cache, build it first")
943
1082
 
944
- visitor = Export()
945
- cache.visit_artifact(task, artifact, visitor)
946
- context.add_export(task, visitor)
1083
+ visitor = Export()
1084
+ cache.visit_artifact(task, artifact, visitor)
1085
+ context.add_export(task, visitor)
947
1086
 
948
1087
  script = utils.render(
949
1088
  "export.sh.template",
950
1089
  ctx=context)
951
1090
 
952
1091
  print(script)
1092
+
1093
+
1094
+ @cli.command(name="report")
1095
+ @click.pass_context
1096
+ @hooks.cli_report
1097
+ def _report(ctx):
1098
+ """Create and publish a report with system information and logs.
1099
+
1100
+ The purpose of the report command is to aid troubleshooting by
1101
+ providing users a method to quickly generate and share a report
1102
+ with information about the environment and previous activity.
1103
+
1104
+ The report is uploaded to remote caches if configured, otherwise
1105
+ a report archive is created in the current working directory.
1106
+ """
1107
+
1108
+ options = JoltOptions()
1109
+ acache = cache.ArtifactCache.get(options)
1110
+
1111
+ with tools.Tools() as t:
1112
+
1113
+ artifact = cache.Artifact(
1114
+ acache,
1115
+ None,
1116
+ identity=str(uuid.uuid4()),
1117
+ name="jolt",
1118
+ session=True,
1119
+ tools=t)
1120
+
1121
+ with t.tmpdir("report") as tmp, t.cwd(tmp):
1122
+ log.info("Collecting environment")
1123
+ env = ""
1124
+ for key, val in os.environ.items():
1125
+ env += f"{key} = {val}\n"
1126
+ t.write_file("environ.txt", env, expand=False)
1127
+
1128
+ log.info("Collecting configuration")
1129
+ config.save(tmp)
1130
+ artifact.collect("*.conf", "configs/")
1131
+
1132
+ log.info("Collecting platform information")
1133
+ uname = platform.uname()
1134
+ libc = " ".join(platform.libc_ver())
1135
+ pyver = platform.python_version()
1136
+ pyimpl = platform.python_implementation()
1137
+ pfm = f"""System: {uname.system}
1138
+ Node: {uname.node}
1139
+ Release: {uname.release}
1140
+ Version: {uname.version}
1141
+ Machine: {uname.machine}
1142
+ Libc: {libc}
1143
+ Python: {pyimpl} {pyver}
1144
+ """
1145
+ t.write_file("platform.txt", pfm, expand=False)
1146
+
1147
+ def collect_command(what, cmd):
1148
+ try:
1149
+ log.info("Collecting {}", what)
1150
+ t.run(cmd, output=False)
1151
+ except Exception:
1152
+ pass
1153
+
1154
+ collect_command("pip modules", "pip list > packages-pip.txt")
1155
+ if uname.system == "Linux":
1156
+ collect_command("apt packages", "apt list --installed > packages-apt.txt")
1157
+ artifact.collect("/etc/os-release", "os-release.txt")
1158
+ artifact.collect("*.txt")
1159
+
1160
+ with t.cwd(log.logpath):
1161
+ artifact.collect("*.log", "logs/")
1162
+
1163
+ acache.commit(artifact)
1164
+ if acache.upload_enabled():
1165
+ assert acache.upload(artifact)
1166
+ url = acache.location(artifact)
1167
+ else:
1168
+ url = f"{artifact.identity}.tgz"
1169
+ t.archive(artifact.path, url)
1170
+
1171
+ log.info("Location: {}", url)