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/loader.py CHANGED
@@ -1,23 +1,43 @@
1
+ from contextlib import contextmanager
2
+ import fasteners
3
+ import json
1
4
  import glob
2
5
  from importlib.machinery import SourceFileLoader
3
6
  import os
7
+ import platform
8
+ import subprocess
4
9
  import sys
5
10
  from types import ModuleType
6
11
 
7
12
  from jolt import inspection
8
- from jolt.tasks import attributes
9
- from jolt.tasks import Alias, Task, TaskGenerator, TaskRegistry, WorkspaceResource
10
- from jolt.error import raise_error_if, raise_task_error_if
13
+ from jolt.tasks import Task, TaskGenerator
14
+ from jolt.error import raise_error_if
11
15
  from jolt import common_pb2 as common_pb
12
16
  from jolt import config
13
17
  from jolt import filesystem as fs
14
18
  from jolt import log
15
19
  from jolt import utils
16
- from jolt.manifest import ManifestExtension
17
- from jolt.manifest import ManifestExtensionRegistry
20
+ from jolt.tools import Tools
18
21
 
19
22
 
20
23
  class Recipe(object):
24
+ """
25
+ Abstract representation a single recipe file.
26
+
27
+ Implementations of this class are responsible for reading the recipe source
28
+ from the filesystem. The recipe source is then parsed and the tasks are
29
+ extracted and made available for execution.
30
+
31
+ The format of the recipe source is implementation defined.
32
+ """
33
+
34
+ tasks = []
35
+ """
36
+ List of task classes defined in the recipe.
37
+
38
+ Available after the recipe has been loaded.
39
+ """
40
+
21
41
  def __init__(self, path, joltdir=None, project=None, source=None):
22
42
  self.path = path
23
43
  self.basepath = os.path.basename(path)
@@ -27,12 +47,14 @@ class Recipe(object):
27
47
  self.tasks = []
28
48
 
29
49
  def load(self):
50
+ """ Load the recipe source from the file system. """
30
51
  raise_error_if(self.source is not None, "recipe already loaded: {}", self.path)
31
52
 
32
53
  with open(self.path) as f:
33
54
  self.source = f.read()
34
55
 
35
56
  def save(self):
57
+ """ Save the recipe source to the file system. """
36
58
  raise_error_if(self.source is None, "recipe source unknown: {}", self.path)
37
59
 
38
60
  with open(self.path, "w") as f:
@@ -40,6 +62,8 @@ class Recipe(object):
40
62
 
41
63
 
42
64
  class NativeRecipe(Recipe):
65
+ """ Represents a Python recipe file (.jolt, .py). """
66
+
43
67
  @staticmethod
44
68
  def _is_abstract(cls):
45
69
  return cls.__dict__.get("abstract", False) or cls.__name__.startswith("_")
@@ -50,7 +74,14 @@ class NativeRecipe(Recipe):
50
74
  issubclass(cls, Task) and \
51
75
  not NativeRecipe._is_abstract(cls)
52
76
 
53
- def load(self):
77
+ def load(self, joltdir=None):
78
+ """
79
+ Load the recipe source from the file system.
80
+
81
+ Python classes defined in the recipe source are extracted and made available
82
+ as tasks for execution. Task classes must be subclasses of Task or TaskGenerator.
83
+
84
+ """
54
85
  super(NativeRecipe, self).load()
55
86
 
56
87
  name = utils.canonical(self.path)
@@ -64,7 +95,7 @@ class NativeRecipe(Recipe):
64
95
  generators = []
65
96
 
66
97
  for cls in classes[TaskGenerator]:
67
- cls.joltdir = os.path.normpath(self.joltdir or os.path.dirname(self.path))
98
+ cls.joltdir = os.path.normpath(joltdir or self.joltdir or os.path.dirname(self.path))
68
99
  generators.append(cls())
69
100
 
70
101
  for generator in generators:
@@ -73,7 +104,7 @@ class NativeRecipe(Recipe):
73
104
 
74
105
  for task in classes[Task]:
75
106
  task.name = task.name or task.__name__.lower()
76
- task.joltdir = os.path.normpath(self.joltdir or os.path.dirname(self.path))
107
+ task.joltdir = os.path.normpath(joltdir or self.joltdir or os.path.dirname(self.path))
77
108
  task.joltproject = self.project
78
109
  self.tasks.append(task)
79
110
 
@@ -81,25 +112,62 @@ class NativeRecipe(Recipe):
81
112
 
82
113
 
83
114
  class Loader(object):
84
- def recipes(self):
115
+ """
116
+ Base class for recipe loaders.
117
+
118
+ A Loader is responsible for finding recipes in the file system providing a list
119
+ of Recipe:s from which tasks can be loaded.
120
+ """
121
+
122
+ def recipes(self) -> list:
123
+ """ Return a list of Recipe:s from which tasks can be loaded. """
85
124
  pass
86
125
 
87
126
 
88
127
  class LoaderFactory(object):
128
+ """
129
+ A factory for creating Loader instances.
130
+
131
+ Factories are registered with the JoltLoader where it is used to create Loader instances.
132
+ """
89
133
  def create(self):
90
134
  raise NotImplementedError()
91
135
 
92
136
 
93
137
  class NativeLoader(Loader):
138
+ """ A loader for Python recipe files (.jolt, .py). """
139
+
94
140
  def __init__(self, searchpath):
95
- self._recipes = []
96
- self._find_files(searchpath)
141
+ """
142
+ Create a new NativeLoader instance.
143
+
144
+ Args:
145
+ searchpath (str): The path to search for recipe files. If the path is a file,
146
+ only that file will be loaded. If the path is a directory, all files with
147
+ the .jolt or .py extension will be loaded.
148
+
149
+ """
150
+
151
+ self._files = self._find_files(searchpath)
152
+ self._recipes = self._load_files(self._files) if self._files else []
97
153
 
98
154
  def _find_files(self, searchpath):
99
- files = glob.glob(fs.path.join(searchpath, "*.jolt"))
155
+ # If the searchpath is a file, load it directly
156
+ if fs.path.isdir(searchpath):
157
+ return glob.glob(fs.path.join(searchpath, "*.jolt"))
158
+
159
+ _, ext = fs.path.splitext(searchpath)
160
+ raise_error_if(not fs.path.exists(searchpath), "File does not exist: {}", searchpath)
161
+ raise_error_if(ext not in [".build", ".jolt", ".py"], "Invalid file extension: {}", ext)
162
+
163
+ return [searchpath]
164
+
165
+ def _load_files(self, files):
166
+ recipes = []
100
167
  for filepath in files:
101
168
  recipe = NativeRecipe(filepath)
102
- self._recipes.append(recipe)
169
+ recipes.append(recipe)
170
+ return recipes
103
171
 
104
172
  @property
105
173
  def recipes(self):
@@ -110,6 +178,7 @@ _loaders = []
110
178
 
111
179
 
112
180
  def register(factory):
181
+ """ Register a LoaderFactory with the JoltLoader. """
113
182
  raise_error_if(not issubclass(factory, LoaderFactory),
114
183
  "{} is not a LoaderFactory", factory.__name__)
115
184
  _loaders.append(factory)
@@ -117,64 +186,38 @@ def register(factory):
117
186
 
118
187
  @register
119
188
  class NativeLoaderFactory(LoaderFactory):
189
+ """ A factory for creating NativeLoader instances. """
120
190
  def create(self, searchpath):
121
191
  return NativeLoader(searchpath)
122
192
 
123
193
 
124
194
  @utils.Singleton
125
195
  class JoltLoader(object):
196
+ """
197
+ The JoltLoader is responsible for loading recipes from the file system.
198
+
199
+ The JoltLoader is a singleton and is used to load recipes from the file system.
200
+ The recipes are loaded from the workspace directory and any project directories
201
+ defined in the workspace. The recipes are then made available for execution.
202
+
203
+ """
204
+
126
205
  filename = "*.jolt"
127
206
 
128
207
  def __init__(self):
208
+ self._lock = None
129
209
  self._recipes = []
130
210
  self._tasks = []
131
211
  self._path = None
132
- self._project_modules = {}
133
- self._project_recipes = {}
134
- self._project_resources = {}
135
-
136
- def _add_project_module(self, project, src):
137
- modules = self._project_modules.get(project, [])
138
- modules.append(src)
139
- self._project_modules[project] = modules
140
-
141
- def _get_project_modules(self, project):
142
- return self._project_modules.get(project, [])
143
-
144
- def _add_project_recipe(self, project, joltdir, src):
145
- recipes = self._project_recipes.get(project, [])
146
- recipes.append((joltdir, src))
147
- self._project_recipes[project] = recipes
148
-
149
- def _get_project_recipes(self, project):
150
- return self._project_recipes.get(project, [])
151
-
152
- def _add_project_resource(self, project, resource_name, resource_task):
153
- class ProjectResource(Alias):
154
- name = project + "/" + resource_name
155
- requires = [resource_task]
156
-
157
- self._tasks.append(ProjectResource)
158
- resources = self._project_resources.get(project, [])
159
- resources.append((resource_name, resource_task))
160
- self._project_resources[project] = resources
161
-
162
- def _get_project_resources(self, project):
163
- return self._project_resources.get(project, [])
164
-
165
- def _load_project_recipes(self):
166
- for project, recipes in self._project_recipes.items():
167
- resources = [resource for _, resource in self._get_project_resources(project)]
168
- for joltdir, src in recipes:
169
- joltdir = fs.path.join(self.joltdir, joltdir) if joltdir else self.joltdir
170
- recipe = NativeRecipe(fs.path.join(self._path, src), joltdir, project)
171
- recipe.load()
172
- for task in recipe.tasks:
173
- task._resources = resources
174
- attributes.requires("_resources")(task)
175
- self._tasks += recipe.tasks
212
+ self._build_path = None
213
+ self._workspace_name = None
214
+
215
+ def _get_first_recipe_path(self):
216
+ for recipe in self.recipes:
217
+ return recipe.path
218
+ return None
176
219
 
177
- def _find_joltdir(self, searchdir):
220
+ def _find_workspace_path(self, searchdir):
178
221
  for factory in _loaders:
179
222
  loader = factory().create(searchdir)
180
223
  if loader.recipes:
@@ -184,31 +227,63 @@ class JoltLoader(object):
184
227
  if searchdir == parentdir:
185
228
  return os.getcwd()
186
229
 
187
- return self._find_joltdir(parentdir)
230
+ return self._find_workspace_path(parentdir)
188
231
 
189
232
  def _get_searchpaths(self):
190
- return [self.joltdir]
233
+ return [self.workspace_path]
234
+
235
+ def load(self, registry=None):
236
+ """
237
+ Load all recipes from the workspace directory.
191
238
 
192
- def load(self, manifest=None):
193
- if not self.joltdir:
194
- self.set_joltdir(self._find_joltdir(os.getcwd()))
239
+ Optionally populate the task registry with the tasks found in the recipes.
195
240
 
196
- if not self.joltdir:
241
+ Returns:
242
+ List of Task classes found in the recipes.
243
+ """
244
+
245
+ if not self.workspace_path:
246
+ self.set_workspace_path(self._find_workspace_path(os.getcwd()))
247
+
248
+ if not self.workspace_path:
197
249
  return []
198
250
 
199
251
  for searchpath in self._get_searchpaths():
200
252
  for factory in _loaders:
201
253
  loader = factory().create(searchpath)
202
254
  for recipe in loader.recipes:
255
+ recipe.workspace_path = os.path.relpath(recipe.path, self.workspace_path)
203
256
  recipe.load()
204
257
  self._recipes.append(recipe)
205
258
  self._tasks += recipe.tasks
206
259
 
207
- self._load_project_recipes()
260
+ # Create workspace lock on the first loaded recipe
261
+ if not self._lock:
262
+ path = self._get_first_recipe_path()
263
+ if path:
264
+ self._lock = fasteners.InterProcessLock(path)
265
+
266
+ # Add tasks to the registry if provided
267
+ if registry is not None:
268
+ for task in self._tasks:
269
+ registry.add_task_class(task)
208
270
 
209
271
  return self._tasks
210
272
 
273
+ def load_file(self, path, joltdir=None):
274
+ """ Load a single recipe file. """
275
+
276
+ for factory in _loaders:
277
+ loader = factory().create(path)
278
+ for recipe in loader.recipes:
279
+ joltdir = fs.path.join(self.joltdir, joltdir) if joltdir else self.joltdir
280
+ recipe.load(joltdir=joltdir)
281
+ self._recipes.append(recipe)
282
+ self._tasks += recipe.tasks
283
+
211
284
  def load_plugin(self, filepath):
285
+ """ Load a single plugin file. """
286
+
212
287
  plugin, ext = os.path.splitext(fs.path.basename(filepath))
213
288
  loader = SourceFileLoader("jolt.plugins." + plugin, filepath)
214
289
  module = ModuleType(loader.name)
@@ -217,6 +292,15 @@ class JoltLoader(object):
217
292
  sys.modules[loader.name] = module
218
293
 
219
294
  def load_plugins(self):
295
+ """
296
+ Load all configured plugins.
297
+
298
+ Plugins are loaded from the plugin path configured in the Jolt configuration file
299
+ or from the default plugin path in the Jolt package.
300
+
301
+ If a plugin is already loaded, it will not be loaded again. If a plugin is not found
302
+ in the plugin path, it will not be loaded.
303
+ """
220
304
  searchpath = config.get("jolt", "pluginpath")
221
305
  searchpath = searchpath.split(":") if searchpath else []
222
306
  searchpath.append(fs.path.join(fs.path.dirname(__file__), "plugins"))
@@ -243,150 +327,230 @@ class JoltLoader(object):
243
327
 
244
328
  @property
245
329
  def tasks(self):
330
+ """ Returns a list of all loaded tasks. """
246
331
  return self._tasks
247
332
 
248
333
  @property
249
334
  def joltdir(self):
335
+ """ Returns the path to the workspace. """
250
336
  return self._path
251
337
 
252
- def set_joltdir(self, value):
253
- if not self._path or len(value) < len(self._path):
254
- self._path = os.path.normpath(value) if value is not None else None
255
-
338
+ @property
339
+ def workspace_name(self):
340
+ """ Returns the name of the workspace. """
341
+ return self._workspace_name or os.path.basename(self.workspace_path)
256
342
 
257
- class RecipeExtension(ManifestExtension):
258
- def export_manifest(self, manifest, tasks):
259
- loader = JoltLoader.get()
343
+ def set_workspace_name(self, name):
344
+ self._workspace_name = name
260
345
 
261
- for recipe in loader.recipes:
262
- manifest_recipe = manifest.create_recipe()
263
- manifest_recipe.path = recipe.basepath
264
- manifest_recipe.source = recipe.source
346
+ @property
347
+ def workspace_path(self):
348
+ """ Returns the path to the workspace. """
349
+ return self._path
265
350
 
266
- projects = set([task.task.joltproject for task in tasks])
267
- for project in filter(lambda x: x is not None, projects):
268
- manifest_project = manifest.create_project()
269
- manifest_project.name = project
351
+ @contextmanager
352
+ @utils.locked
353
+ def workspace_lock(self):
354
+ if not self._lock:
355
+ yield
356
+ with self._lock:
357
+ yield
270
358
 
271
- for name, resource_task in loader._get_project_resources(project):
272
- resource = manifest_project.create_resource()
273
- resource.name = name
274
- resource.text = resource_task
359
+ def set_workspace_path(self, path):
360
+ if not self._path or len(path) < len(self._path):
361
+ self._path = os.path.normpath(path) if path is not None else None
275
362
 
276
- for joltdir, src in loader._get_project_recipes(project):
277
- recipe = manifest_project.create_recipe()
278
- recipe.src = src
279
- if joltdir:
280
- recipe.joltdir = joltdir
363
+ @property
364
+ def build_path(self):
365
+ """ Returns the path to the build directory. """
366
+ return self._build_path or os.path.join(self.workspace_path, "build")
281
367
 
282
- for path in loader._get_project_modules(project):
283
- module = manifest_project.create_module()
284
- module.path = path
368
+ @property
369
+ def build_path_rel(self):
370
+ """" Returns the path to the build directory relative to the workspace. """
371
+ return os.path.relpath(self.build_path, self.workspace_path)
285
372
 
286
- def import_manifest(self, manifest):
287
- loader = JoltLoader.get()
288
- loader.set_joltdir(manifest.joltdir)
373
+ def set_build_path(self, path):
374
+ self._build_path = os.path.normpath(os.path.join(self.workspace_path, path))
375
+ log.debug("Jolt build path: {}", self._build_path)
289
376
 
290
- for recipe in manifest.recipes:
291
- recipe = Recipe(recipe.path, source=recipe.source)
292
- recipe.save()
293
377
 
294
- for project in manifest.projects:
295
- for recipe in project.recipes:
296
- loader._add_project_recipe(project.name, recipe.joltdir, recipe.src)
378
+ def get_workspacedir():
379
+ workspacedir = JoltLoader.get().workspace_path
380
+ assert workspacedir is not None, "No workspace present"
381
+ return workspacedir
297
382
 
298
- for resource in project.resources:
299
- loader._add_project_resource(project.name, resource.name, resource.text)
300
383
 
301
- # Acquire resource immediately
302
- task = TaskRegistry.get().get_task(resource.text, manifest=manifest)
303
- raise_task_error_if(not isinstance(task, WorkspaceResource), task,
304
- "only workspace resources are allowed in manifest")
305
- task.acquire_ws()
384
+ @contextmanager
385
+ def workspace_lock():
386
+ with JoltLoader.get().workspace_lock():
387
+ yield
306
388
 
307
- for module in project.modules:
308
- loader._add_project_module(project.name, module.src)
309
- sys.path.append(fs.path.join(manifest.joltdir, module.src))
310
389
 
311
- def import_protobuf(self, buildenv):
312
- loader = JoltLoader.get()
313
- loader.set_joltdir(os.getcwd())
390
+ def workspace_locked(func):
391
+ def wrapper(*args, **kwargs):
392
+ with workspace_lock():
393
+ return func(*args, **kwargs)
394
+ return wrapper
314
395
 
315
- # Write .jolt files into workspace
316
- for file in buildenv.workspace.files:
317
- if file.content:
318
- with open(file.path, "w") as f:
319
- f.write(file.content)
320
396
 
321
- for project in buildenv.workspace.projects:
322
- for recipe in project.recipes:
323
- loader._add_project_recipe(project.name, recipe.workdir, recipe.path)
397
+ def import_workspace(buildenv: common_pb.BuildEnvironment):
398
+ """
399
+ Import workspace from a BuildEnvironment protobuf message.
324
400
 
325
- for resource in project.resources:
326
- loader._add_project_resource(project.name, resource.alias, resource.name)
401
+ This function will create files, recipes, resources and modules in the workspace
402
+ based on the information in the BuildEnvironment message.
327
403
 
328
- # Acquire resource immediately
329
- task = TaskRegistry.get().get_task(resource.name, buildenv=buildenv)
330
- raise_task_error_if(
331
- not isinstance(task, WorkspaceResource), task,
332
- "only workspace resources are allowed in manifest")
333
- task.acquire_ws()
404
+ The workspace tree is not pulled and checked out here. This is done by the
405
+ worker before it starts the executor.
334
406
 
335
- for path in project.paths:
336
- loader._add_project_module(project.name, path.path)
337
- sys.path.append(fs.path.join(loader.joltdir, path.path))
407
+ """
408
+ loader = JoltLoader.get()
409
+ loader.set_workspace_path(os.getcwd())
410
+ loader.set_workspace_name(buildenv.workspace.name)
411
+ if buildenv.workspace.builddir:
412
+ loader.set_build_path(buildenv.workspace.builddir)
338
413
 
414
+ # Write .jolt files into workspace
415
+ for file in buildenv.workspace.files:
416
+ if file.content:
417
+ with open(file.path, "w") as f:
418
+ f.write(file.content)
339
419
 
340
- ManifestExtensionRegistry.add(RecipeExtension())
341
420
 
421
+ @workspace_locked
422
+ def export_workspace(tasks=None) -> common_pb.Workspace:
423
+ """
424
+ Export workspace to a Workspace protobuf message.
342
425
 
343
- def get_workspacedir():
344
- workspacedir = JoltLoader.get().joltdir
345
- assert workspacedir is not None, "No workspace present"
346
- return workspacedir
426
+ This function will create a Workspace protobuf message containing all the
427
+ recipes, resources and modules in the workspace. If tasks is provided, only
428
+ the projects associated with the tasks will be exported. Otherwise, all
429
+ projects will be exported.
347
430
 
431
+ If the workspace is configured to use a remote cache, the workspace will be
432
+ pushed to the remote cache using the fstree tool. The tree hash of the
433
+ workspace will be included in the returned Workspace message.
348
434
 
349
- def export_workspace(tasks=None):
435
+ """
350
436
  loader = JoltLoader.get()
437
+ tools = Tools()
438
+ tree = None
439
+
440
+ fstree_enabled = config.getboolean("jolt", "fstree", True)
441
+ if fstree_enabled:
442
+ fstree = tools.which("fstree")
443
+ if not fstree:
444
+ host = platform.system().lower()
445
+ arch = platform.machine().lower()
446
+ fstree = os.path.join(os.path.dirname(__file__), "bin", f"fstree-{host}-{arch}")
447
+ if not os.path.exists(fstree):
448
+ fstree_enabled = False
449
+ log.warning("fstree executable not found, will not push workspace to remote cache")
450
+
451
+ cache_grpc_uri = config.geturi("cache", "grpc_uri")
452
+ if fstree_enabled:
453
+ if not cache_grpc_uri:
454
+ fstree_enabled = False
455
+ log.warning("No cache gRPC URI configured, will not push workspace to remote cache")
456
+ else:
457
+ raise_error_if(cache_grpc_uri.scheme not in ["tcp"], "Invalid scheme in cache gRPC URI config: {}", cache_grpc_uri.scheme)
458
+ raise_error_if(not cache_grpc_uri.netloc, "Invalid network address in cache gRPC URI config: {}", cache_grpc_uri.netloc)
459
+
460
+ # Push workspace to remote cache if possible
461
+ if fstree_enabled and fstree and cache_grpc_uri:
462
+ with tools.cwd(loader.workspace_path):
463
+ cwd = tools.getcwd()
464
+ cachedir = config.get_cachedir()
465
+ indexhash = utils.sha1(cwd)
466
+ indexfile = tools.expand_path(
467
+ "{cachedir}/indexes/{}/{}",
468
+ indexhash[:2], indexhash[2:],
469
+ cwd=fs.posixpath.abspath(cwd),
470
+ cachedir=cachedir)
471
+
472
+ if not os.path.exists(indexfile):
473
+ process = None
474
+ try:
475
+ with log.progress("Indexing workspace for the first time", count=None, unit="objects", estimates=False) as progress:
476
+ process = subprocess.Popen(
477
+ [fstree, "write-tree", "--json", "--cache", cachedir, "--ignore", ".joltignore", "--index", indexfile, "--remote", cache_grpc_uri.geturl(), "--threads", str(tools.thread_count())],
478
+ stdout=subprocess.DEVNULL,
479
+ stderr=subprocess.PIPE,
480
+ cwd=tools.getcwd())
481
+
482
+ for line in iter(process.stderr.readline, b''):
483
+ try:
484
+ event = json.loads(line.decode())
485
+ except json.JSONDecodeError as exc:
486
+ log.error("Failed to decode fstree event: {}", line.decode())
487
+ raise exc
488
+
489
+ if event.get("type") in ["cache::add"]:
490
+ progress.update(1)
491
+
492
+ except Exception as exc:
493
+ if process:
494
+ process.terminate()
495
+ raise exc
496
+ finally:
497
+ if process:
498
+ process.wait()
499
+ process.stderr.close()
500
+ raise_error_if(process.returncode != 0, "Failed to index workspace")
501
+
502
+ process = None
503
+ try:
504
+ with log.progress("Pushing workspace to remote cache", count=None, unit="objects", estimates=False) as progress:
505
+ process = subprocess.Popen(
506
+ [fstree, "write-tree-push", "--json", "--cache", cachedir, "--ignore", ".joltignore", "--index", indexfile, "--remote", cache_grpc_uri.geturl(), "--threads", str(tools.thread_count())],
507
+ stdout=subprocess.DEVNULL,
508
+ stderr=subprocess.PIPE,
509
+ cwd=tools.getcwd())
510
+
511
+ for line in iter(process.stderr.readline, b''):
512
+ try:
513
+ event = json.loads(line.decode())
514
+ except json.JSONDecodeError as exc:
515
+ log.error("Failed to decode fstree event: {}", line.decode())
516
+ raise exc
517
+
518
+ if event.get("type") in ["cache::push"]:
519
+ tree = event.get("path")
520
+ log.info("Workspace tree: {} ({} objects)", tree, event.get("value"))
521
+
522
+ if event.get("type") in ["cache::remote_missing_object", "cache::remote_missing_tree"]:
523
+ progress.refresh()
524
+
525
+ if event.get("type") in ["cache::push_object", "cache::push_tree"]:
526
+ progress.update(1)
527
+
528
+ except Exception as exc:
529
+ if process:
530
+ process.terminate()
531
+ raise exc
532
+
533
+ finally:
534
+ if process:
535
+ process.wait()
536
+ process.stderr.close()
537
+ raise_error_if(process.returncode != 0, "Failed to push workspace to remote cache")
538
+
351
539
  workspace = common_pb.Workspace(
540
+ builddir=loader.build_path_rel,
352
541
  cachedir=config.get_cachedir(),
353
- rootdir=loader.joltdir,
542
+ rootdir=loader.workspace_path,
543
+ name=loader.workspace_name,
544
+ tree=tree,
354
545
  )
355
546
 
356
- for recipe in loader.recipes:
357
- workspace.files.append(
358
- common_pb.File(
359
- path=recipe.basepath,
360
- content=recipe.source,
547
+ if not fstree_enabled:
548
+ for recipe in loader.recipes:
549
+ workspace.files.append(
550
+ common_pb.File(
551
+ path=recipe.workspace_path,
552
+ content=recipe.source,
553
+ )
361
554
  )
362
- )
363
-
364
- if tasks is None:
365
- projects = loader._project_recipes.keys()
366
- else:
367
- projects = set([task.task.joltproject for task in tasks])
368
-
369
- for project in filter(lambda x: x is not None, projects):
370
- pb_project = common_pb.Project()
371
- pb_project.name = project
372
-
373
- for name, resource_task in loader._get_project_resources(project):
374
- resource = common_pb.Project.Resource()
375
- resource.alias = name
376
- resource.name = resource_task
377
- pb_project.resources.append(resource)
378
-
379
- for joltdir, src in loader._get_project_recipes(project):
380
- recipe = common_pb.Project.Recipe()
381
- recipe.path = src
382
- if joltdir:
383
- recipe.workdir = joltdir
384
- pb_project.recipes.append(recipe)
385
-
386
- for path in loader._get_project_modules(project):
387
- syspath = common_pb.Project.SystemPath(path=path)
388
- pb_project.paths.append(syspath)
389
-
390
- workspace.projects.append(pb_project)
391
555
 
392
556
  return workspace