lockss-turtles 0.5.0.dev4__py3-none-any.whl → 0.6.0__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.
- lockss/turtles/__init__.py +10 -30
- lockss/turtles/__main__.py +7 -3
- lockss/turtles/app.py +520 -109
- lockss/turtles/cli.py +540 -333
- lockss/turtles/plugin.py +207 -50
- lockss/turtles/plugin_registry.py +617 -189
- lockss/turtles/plugin_set.py +534 -187
- lockss/turtles/plugin_signing_credentials.py +84 -0
- lockss/turtles/util.py +70 -21
- {lockss_turtles-0.5.0.dev4.dist-info → lockss_turtles-0.6.0.dist-info}/LICENSE +1 -1
- lockss_turtles-0.6.0.dist-info/METADATA +64 -0
- lockss_turtles-0.6.0.dist-info/RECORD +18 -0
- {lockss_turtles-0.5.0.dev4.dist-info → lockss_turtles-0.6.0.dist-info}/WHEEL +1 -1
- unittest/lockss/turtles/__init__.py +106 -0
- unittest/lockss/turtles/test_plugin_registry.py +417 -0
- unittest/lockss/turtles/test_plugin_set.py +274 -0
- unittest/lockss/turtles/test_plugin_signing_credentials.py +102 -0
- CHANGELOG.rst +0 -113
- lockss/turtles/resources/__init__.py +0 -29
- lockss/turtles/resources/plugin-registry-catalog-schema.json +0 -27
- lockss/turtles/resources/plugin-registry-schema.json +0 -115
- lockss/turtles/resources/plugin-set-catalog-schema.json +0 -27
- lockss/turtles/resources/plugin-set-schema.json +0 -92
- lockss/turtles/resources/plugin-signing-credentials-schema.json +0 -27
- lockss_turtles-0.5.0.dev4.dist-info/METADATA +0 -1041
- lockss_turtles-0.5.0.dev4.dist-info/RECORD +0 -20
- {lockss_turtles-0.5.0.dev4.dist-info → lockss_turtles-0.6.0.dist-info}/entry_points.txt +0 -0
lockss/turtles/plugin_set.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
# Copyright (c) 2000-
|
|
3
|
+
# Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
|
|
4
4
|
#
|
|
5
5
|
# Redistribution and use in source and binary forms, with or without
|
|
6
6
|
# modification, are permitted provided that the following conditions are met:
|
|
@@ -28,97 +28,376 @@
|
|
|
28
28
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
29
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
"""
|
|
32
|
+
Module to represent plugin sets and plugin set catalogs.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# Remove in Python 3.14; see https://stackoverflow.com/a/33533514
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from abc import ABC, abstractmethod
|
|
32
39
|
import os
|
|
33
40
|
from pathlib import Path
|
|
34
41
|
import shlex
|
|
35
42
|
import subprocess
|
|
36
43
|
import sys
|
|
44
|
+
from typing import Annotated, Any, Callable, ClassVar, Literal, Optional, Union
|
|
45
|
+
|
|
46
|
+
from pydantic import BaseModel, Field
|
|
47
|
+
|
|
48
|
+
from .plugin import Plugin, PluginIdentifier
|
|
49
|
+
from .util import BaseModelWithRoot
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
#: A type alias for the plugin set catalog kind.
|
|
53
|
+
PluginSetCatalogKind = Literal['PluginSetCatalog']
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PluginSetCatalog(BaseModelWithRoot):
|
|
57
|
+
"""
|
|
58
|
+
A Pydantic model (``lockss.turtles.util.BaseModelWithRoot``) to represent a
|
|
59
|
+
plugin set catalog.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
#: This object's kind.
|
|
63
|
+
kind: PluginSetCatalogKind = Field(title='Kind',
|
|
64
|
+
description="This object's kind")
|
|
65
|
+
|
|
66
|
+
#: A non-empty list of plugin set files.
|
|
67
|
+
plugin_set_files: list[str] = Field(alias='plugin-set-files',
|
|
68
|
+
min_length=1,
|
|
69
|
+
title='Plugin Set Files',
|
|
70
|
+
description="A non-empty list of plugin set files")
|
|
71
|
+
|
|
72
|
+
def get_plugin_set_files(self) -> list[Path]:
|
|
73
|
+
"""
|
|
74
|
+
Return this plugin set catalog's list of plugin set definition file
|
|
75
|
+
paths (relative to the root if not absolute).
|
|
76
|
+
|
|
77
|
+
:return: A list of plugin set definition file paths.
|
|
78
|
+
:rtype: list[Path]
|
|
79
|
+
"""
|
|
80
|
+
return [self.get_root().joinpath(p) for p in self.plugin_set_files]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
#: A type alias for the plugin set builder type.
|
|
84
|
+
PluginSetBuilderType = Literal['ant', 'maven']
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class BasePluginSetBuilder(BaseModelWithRoot, ABC):
|
|
88
|
+
"""
|
|
89
|
+
An abstract Pydantic model (``lockss.turtles.util.BaseModelWithRoot``) to
|
|
90
|
+
represent a plugin set builder, with concrete implementations
|
|
91
|
+
``MavenPluginSetBuilder`` and ``AntPluginSetBuilder``.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
#: Pydantic definition of the ``type`` field.
|
|
95
|
+
TYPE_FIELD: ClassVar[dict[str, str]] = dict(title='Plugin Builder Type',
|
|
96
|
+
description='A plugin builder type')
|
|
97
|
+
|
|
98
|
+
#: Pydantic definition of the ``main`` field.
|
|
99
|
+
MAIN_FIELD: ClassVar[dict[str, str]] = dict(title='Main Code Path',
|
|
100
|
+
description="The path to the plugins' source code, relative to the root of the project")
|
|
101
|
+
|
|
102
|
+
#: Pydantic definition of the ``test`` field.
|
|
103
|
+
TEST_FIELD: ClassVar[dict[str, str]] = dict(title='Test Code Path',
|
|
104
|
+
description="The path to the plugins' unit tests, relative to the root of the project")
|
|
105
|
+
|
|
106
|
+
@abstractmethod
|
|
107
|
+
def build_plugin(self,
|
|
108
|
+
plugin_id: PluginIdentifier,
|
|
109
|
+
keystore_path: Path,
|
|
110
|
+
keystore_alias: str,
|
|
111
|
+
keystore_password=None) -> tuple[Path, Plugin]:
|
|
112
|
+
"""
|
|
113
|
+
Builds the given plugin, using the given plugin signing credentials.
|
|
114
|
+
|
|
115
|
+
:param plugin_id: A plugin identifier.
|
|
116
|
+
:type plugin_id: PluginIdentifier
|
|
117
|
+
:param keystore_path: The path to the plugin signing keystore.
|
|
118
|
+
:type keystore_path: Path
|
|
119
|
+
:param keystore_alias: The signing alias to use from the plugin signing
|
|
120
|
+
keystore.
|
|
121
|
+
:type keystore_alias: str
|
|
122
|
+
:param keystore_password: The signing password.
|
|
123
|
+
:type keystore_password: Any
|
|
124
|
+
:return: A tuple of the plugin JAR path and the corresponding ``Plugin``
|
|
125
|
+
object.
|
|
126
|
+
:rtype: tuple[Path, Plugin]
|
|
127
|
+
"""
|
|
128
|
+
pass # FIXME: typing of keystore_password
|
|
129
|
+
|
|
130
|
+
def get_main(self) -> Path:
|
|
131
|
+
"""
|
|
132
|
+
Returns this plugin set builder's main code path (relative to the root
|
|
133
|
+
if not absolute).
|
|
134
|
+
|
|
135
|
+
:return: This plugin set's main code path.
|
|
136
|
+
:rtype: Path
|
|
137
|
+
:raises ValueError: If this object is not properly initialized.
|
|
138
|
+
"""
|
|
139
|
+
return self.get_root().joinpath(self._get_main())
|
|
140
|
+
|
|
141
|
+
def get_test(self) -> Path:
|
|
142
|
+
"""
|
|
143
|
+
Returns this plugin set builder's unit test path (relative to the root
|
|
144
|
+
if not absolute).
|
|
145
|
+
|
|
146
|
+
:return: This plugin set's unit test path.
|
|
147
|
+
:rtype: Path
|
|
148
|
+
:raises ValueError: If this object is not properly initialized.
|
|
149
|
+
"""
|
|
150
|
+
return self.get_root().joinpath(self._get_test())
|
|
151
|
+
|
|
152
|
+
def get_type(self) -> PluginSetBuilderType:
|
|
153
|
+
"""
|
|
154
|
+
Returns this plugin set builder's type.
|
|
155
|
+
|
|
156
|
+
:return: This plugin set builder's type.
|
|
157
|
+
:rtype: PluginSetBuilderType
|
|
158
|
+
"""
|
|
159
|
+
return getattr(self, 'type')
|
|
160
|
+
|
|
161
|
+
def has_plugin(self,
|
|
162
|
+
plugin_id: PluginIdentifier) -> bool:
|
|
163
|
+
"""
|
|
164
|
+
Determines if the given plugin identifier represents a plugin that is
|
|
165
|
+
present in the plugin set.
|
|
166
|
+
|
|
167
|
+
:param plugin_id: A plugin identifier.
|
|
168
|
+
:type plugin_id: PluginIdentifier
|
|
169
|
+
:return: Whether the plugin is present in the plugin set.
|
|
170
|
+
:rtype: bool
|
|
171
|
+
"""
|
|
172
|
+
return self._plugin_path(plugin_id).is_file()
|
|
37
173
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class PluginSetCatalog(object):
|
|
44
|
-
|
|
45
|
-
PLUGIN_SET_CATALOG_SCHEMA = 'plugin-set-catalog-schema.json'
|
|
46
|
-
|
|
47
|
-
@staticmethod
|
|
48
|
-
def from_path(plugin_set_catalog_path):
|
|
49
|
-
plugin_set_catalog_path = _path(plugin_set_catalog_path)
|
|
50
|
-
with importlib.resources.path(lockss.turtles.resources, PluginSetCatalog.PLUGIN_SET_CATALOG_SCHEMA) as plugin_set_catalog_schema_path:
|
|
51
|
-
parsed = _load_and_validate(plugin_set_catalog_schema_path, plugin_set_catalog_path)
|
|
52
|
-
return PluginSetCatalog(parsed)
|
|
53
|
-
|
|
54
|
-
def __init__(self, parsed):
|
|
55
|
-
super().__init__()
|
|
56
|
-
self._parsed = parsed
|
|
57
|
-
|
|
58
|
-
def get_plugin_set_files(self):
|
|
59
|
-
return self._parsed['plugin-set-files']
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class PluginSet(object):
|
|
63
|
-
|
|
64
|
-
PLUGIN_SET_SCHEMA = 'plugin-set-schema.json'
|
|
65
|
-
|
|
66
|
-
@staticmethod
|
|
67
|
-
def from_path(plugin_set_file_path):
|
|
68
|
-
plugin_set_file_path = _path(plugin_set_file_path)
|
|
69
|
-
with importlib.resources.path(lockss.turtles.resources, PluginSet.PLUGIN_SET_SCHEMA) as plugin_set_schema_path:
|
|
70
|
-
lst = _load_and_validate(plugin_set_schema_path, plugin_set_file_path, multiple=True)
|
|
71
|
-
return [PluginSet._from_obj(parsed, plugin_set_file_path) for parsed in lst]
|
|
72
|
-
|
|
73
|
-
@staticmethod
|
|
74
|
-
def _from_obj(parsed, plugin_set_file_path):
|
|
75
|
-
typ = parsed['builder']['type']
|
|
76
|
-
if typ == AntPluginSet.TYPE:
|
|
77
|
-
return AntPluginSet(parsed, plugin_set_file_path)
|
|
78
|
-
elif typ == MavenPluginSet.TYPE:
|
|
79
|
-
return MavenPluginSet(parsed, plugin_set_file_path)
|
|
80
|
-
else:
|
|
81
|
-
raise Exception(f'{plugin_set_file_path!s}: unknown builder type: {typ}')
|
|
82
|
-
|
|
83
|
-
def __init__(self, parsed):
|
|
84
|
-
super().__init__()
|
|
85
|
-
self._parsed = parsed
|
|
86
|
-
|
|
87
|
-
# Returns (jar_path, plugin)
|
|
88
|
-
def build_plugin(self, plugin_id, keystore_path, keystore_alias, keystore_password=None):
|
|
89
|
-
raise NotImplementedError('build_plugin')
|
|
90
|
-
|
|
91
|
-
def get_builder_type(self):
|
|
92
|
-
return self._parsed['builder']['type']
|
|
93
|
-
|
|
94
|
-
def get_id(self):
|
|
95
|
-
return self._parsed['id']
|
|
174
|
+
def make_plugin(self,
|
|
175
|
+
plugin_id: PluginIdentifier) -> Plugin:
|
|
176
|
+
"""
|
|
177
|
+
Makes a ``Plugin`` object from the given plugin identifier.
|
|
96
178
|
|
|
97
|
-
|
|
98
|
-
|
|
179
|
+
:param plugin_id: A plugin identifier.
|
|
180
|
+
:type plugin_id: PluginIdentifier
|
|
181
|
+
:return: The corresponding ``Plugin`` object.
|
|
182
|
+
:rtype: Plugin
|
|
183
|
+
"""
|
|
184
|
+
return Plugin.from_path(self._plugin_path(plugin_id))
|
|
99
185
|
|
|
100
|
-
def
|
|
101
|
-
|
|
186
|
+
def _get_main(self) -> str:
|
|
187
|
+
"""
|
|
188
|
+
Returns the concrete implementation's ``main`` field.
|
|
189
|
+
|
|
190
|
+
:return: The ``main`` field.
|
|
191
|
+
:rtype: str
|
|
192
|
+
"""
|
|
193
|
+
return getattr(self, 'main')
|
|
194
|
+
|
|
195
|
+
def _get_test(self) -> str:
|
|
196
|
+
"""
|
|
197
|
+
Returns the concrete implementation's ``test`` field.
|
|
198
|
+
|
|
199
|
+
:return: The ``test`` field.
|
|
200
|
+
:rtype: str
|
|
201
|
+
"""
|
|
202
|
+
return getattr(self, 'test')
|
|
203
|
+
|
|
204
|
+
def _plugin_path(self,
|
|
205
|
+
plugin_id: PluginIdentifier) -> Path:
|
|
206
|
+
"""
|
|
207
|
+
Returns the path of the plugin file for the given plugin identifier
|
|
208
|
+
relative to the plugin set's main code path.
|
|
209
|
+
|
|
210
|
+
:param plugin_id: A plugin identifier.
|
|
211
|
+
:type plugin_id: PluginIdentifier
|
|
212
|
+
:return: The plugin file.
|
|
213
|
+
:rtype: Path
|
|
214
|
+
"""
|
|
215
|
+
return self.get_main().joinpath(Plugin.id_to_file(plugin_id))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class MavenPluginSetBuilder(BasePluginSetBuilder):
|
|
219
|
+
"""
|
|
220
|
+
A plugin set builder that uses `Java <https://www.oracle.com/java/>`_
|
|
221
|
+
Development Kit (JDK) 17 and `Apache Maven <https://maven.apache.org/>`_,
|
|
222
|
+
with the parent POM
|
|
223
|
+
`org.lockss:lockss-plugins-parent-pom <https://central.sonatype.com/artifact/org.lockss/lockss-plugins-parent-pom>`_.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
#: The default value for ``main``.
|
|
227
|
+
DEFAULT_MAIN: ClassVar[str] = 'src/main/java'
|
|
228
|
+
|
|
229
|
+
#: The default value for ``test``.
|
|
230
|
+
DEFAULT_TEST: ClassVar[str] = 'src/test/java'
|
|
231
|
+
|
|
232
|
+
#: This Plugin set builder's type.
|
|
233
|
+
type: Literal['maven'] = Field(**BasePluginSetBuilder.TYPE_FIELD)
|
|
234
|
+
|
|
235
|
+
#: This plugin set builder's main code path.
|
|
236
|
+
main: str = Field(DEFAULT_MAIN,
|
|
237
|
+
**BasePluginSetBuilder.MAIN_FIELD)
|
|
238
|
+
|
|
239
|
+
#: This plugin set builder's unit test path.
|
|
240
|
+
test: str = Field(DEFAULT_TEST,
|
|
241
|
+
**BasePluginSetBuilder.TEST_FIELD)
|
|
242
|
+
|
|
243
|
+
#: An internal flag to remember if a build has occurred.
|
|
244
|
+
_built: bool
|
|
245
|
+
|
|
246
|
+
def build_plugin(self,
|
|
247
|
+
plugin_id: PluginIdentifier,
|
|
248
|
+
keystore_path: Path,
|
|
249
|
+
keystore_alias: str,
|
|
250
|
+
keystore_password: Optional[Callable[[], str]]=None) -> tuple[Path, Plugin]:
|
|
251
|
+
"""
|
|
252
|
+
Builds the given plugin with the supplied plugin signing credentials.
|
|
253
|
+
|
|
254
|
+
:param plugin_id: A plugin identifier.
|
|
255
|
+
:type plugin_id: PluginIdentifier
|
|
256
|
+
:param keystore_path: The path to the plugin signing keystore.
|
|
257
|
+
:type keystore_path: Path
|
|
258
|
+
:param keystore_alias: The alias to use in the plugin signing keystore.
|
|
259
|
+
:type keystore_alias: str
|
|
260
|
+
:param keystore_password: The plugin signing password.
|
|
261
|
+
:type keystore_password:
|
|
262
|
+
:return: A tuple of the built and signed JAR file and a Plugin object
|
|
263
|
+
instantiated from it.
|
|
264
|
+
:rtype: tuple[Path, Plugin]
|
|
265
|
+
:raises subprocess.CalledProcessError: If a subprocess fails.
|
|
266
|
+
:raises FileNotFoundError: If the expected built and signed plugin JAR
|
|
267
|
+
path is unexpectedly not found despite the
|
|
268
|
+
build.
|
|
269
|
+
"""
|
|
270
|
+
self._big_build(keystore_path, keystore_alias, keystore_password=keystore_password)
|
|
271
|
+
return self._little_build(plugin_id)
|
|
102
272
|
|
|
103
|
-
def
|
|
104
|
-
|
|
273
|
+
def model_post_init(self,
|
|
274
|
+
context: Any) -> None:
|
|
275
|
+
"""
|
|
276
|
+
Pydantic post-initialization method to initialize the ``_built`` flag.
|
|
105
277
|
|
|
278
|
+
:param context: The Pydantic context.
|
|
279
|
+
:type context: Any
|
|
280
|
+
"""
|
|
281
|
+
super().model_post_init(context)
|
|
282
|
+
self._built = False
|
|
106
283
|
|
|
107
|
-
|
|
284
|
+
def _big_build(self,
|
|
285
|
+
keystore_path: Path,
|
|
286
|
+
keystore_alias: str,
|
|
287
|
+
keystore_password: Optional[Callable[[], str]]=None) -> None:
|
|
288
|
+
"""
|
|
289
|
+
Runs ``mvn package`` on the project if the ``_built`` flag is False.
|
|
290
|
+
|
|
291
|
+
:param keystore_path: The path to the plugin signing keystore.
|
|
292
|
+
:type keystore_path: Path
|
|
293
|
+
:param keystore_alias: The alias to use in the plugin signing keystore.
|
|
294
|
+
:type keystore_alias: str
|
|
295
|
+
:param keystore_password: The plugin signing password.
|
|
296
|
+
:type keystore_password: Optional[Callable[[], str]]
|
|
297
|
+
:raises subprocess.CalledProcessError: If a subprocess fails.
|
|
298
|
+
"""
|
|
299
|
+
if not self._built:
|
|
300
|
+
# Do build
|
|
301
|
+
cmd = ['mvn', 'package',
|
|
302
|
+
f'-Dkeystore.file={keystore_path!s}',
|
|
303
|
+
f'-Dkeystore.alias={keystore_alias}']
|
|
304
|
+
if keystore_password:
|
|
305
|
+
cmd.append(f'-Dkeystore.password={keystore_password()}')
|
|
306
|
+
try:
|
|
307
|
+
subprocess.run(cmd, cwd=self.get_root(), check=True, stdout=sys.stdout, stderr=sys.stderr)
|
|
308
|
+
except subprocess.CalledProcessError as cpe:
|
|
309
|
+
raise self._sanitize(cpe)
|
|
310
|
+
self._built = True
|
|
108
311
|
|
|
109
|
-
|
|
312
|
+
def _little_build(self,
|
|
313
|
+
plugin_id: PluginIdentifier) -> tuple[Path, Plugin]:
|
|
314
|
+
"""
|
|
315
|
+
In the Maven implementation, essentially a no-op (keeping for parallel
|
|
316
|
+
structure with the legacy Ant plugin set builder).
|
|
317
|
+
|
|
318
|
+
:param plugin_id: A plugin identifier.
|
|
319
|
+
:type plugin_id: PluginIdentifier
|
|
320
|
+
:return: A tuple of the built and signed JAR file and a Plugin object
|
|
321
|
+
instantiated from it.
|
|
322
|
+
:rtype: tuple[Path, Plugin]
|
|
323
|
+
:raises FileNotFoundError: If the expected built and signed plugin JAR
|
|
324
|
+
path is unexpectedly not found despite the
|
|
325
|
+
build.
|
|
326
|
+
"""
|
|
327
|
+
jar_path = self.get_root().joinpath('target', 'pluginjars', f'{plugin_id}.jar')
|
|
328
|
+
if not jar_path.is_file():
|
|
329
|
+
raise FileNotFoundError(str(jar_path))
|
|
330
|
+
return jar_path, Plugin.from_jar(jar_path)
|
|
110
331
|
|
|
111
|
-
|
|
332
|
+
def _sanitize(self,
|
|
333
|
+
called_process_error: subprocess.CalledProcessError) -> subprocess.CalledProcessError:
|
|
334
|
+
"""
|
|
335
|
+
Alters the ``-Dkeystore.password=`` portion of a called process error.
|
|
112
336
|
|
|
113
|
-
|
|
337
|
+
:param called_process_error: A called process error.
|
|
338
|
+
:return: The same called process error, with ``-Dkeystore.password=``
|
|
339
|
+
altered in ``cmd``.
|
|
340
|
+
"""
|
|
341
|
+
cmd = called_process_error.cmd[:]
|
|
342
|
+
for i in range(len(cmd)):
|
|
343
|
+
if cmd[i].startswith('-Dkeystore.password='):
|
|
344
|
+
cmd[i] = '-Dkeystore.password=<password>'
|
|
345
|
+
called_process_error.cmd = ' '.join([shlex.quote(c) for c in cmd])
|
|
346
|
+
return called_process_error
|
|
114
347
|
|
|
115
|
-
def __init__(self, parsed, path):
|
|
116
|
-
super().__init__(parsed)
|
|
117
|
-
self._built = False
|
|
118
|
-
self._root = path.parent
|
|
119
348
|
|
|
120
|
-
|
|
121
|
-
|
|
349
|
+
class AntPluginSetBuilder(BasePluginSetBuilder):
|
|
350
|
+
"""
|
|
351
|
+
A plugin set builder that uses `Java <https://www.oracle.com/java/>`_
|
|
352
|
+
Development Kit (JDK) 8 and `Apache Ant <https://ant.apache.org/>`_,
|
|
353
|
+
with the legacy LOCKSS 1.x build system.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
#: Default value for the ``main`` field.
|
|
357
|
+
DEFAULT_MAIN: ClassVar[str] = 'plugins/src'
|
|
358
|
+
|
|
359
|
+
#: Default value for the ``test`` field.
|
|
360
|
+
DEFAULT_TEST: ClassVar[str] = 'plugins/test/src'
|
|
361
|
+
|
|
362
|
+
#: This plugin set builder's type.
|
|
363
|
+
type: Literal['ant'] = Field(**BasePluginSetBuilder.TYPE_FIELD)
|
|
364
|
+
|
|
365
|
+
#: This plugin set builder's main code path.
|
|
366
|
+
main: str = Field(DEFAULT_MAIN,
|
|
367
|
+
**BasePluginSetBuilder.MAIN_FIELD)
|
|
368
|
+
|
|
369
|
+
#: This plugin set builder's unit test path.
|
|
370
|
+
test: str = Field(DEFAULT_TEST,
|
|
371
|
+
**BasePluginSetBuilder.TEST_FIELD)
|
|
372
|
+
|
|
373
|
+
#: An internal flag to remember if a build has occurred.
|
|
374
|
+
_built: bool
|
|
375
|
+
|
|
376
|
+
def build_plugin(self,
|
|
377
|
+
plugin_id: PluginIdentifier,
|
|
378
|
+
keystore_path: Path,
|
|
379
|
+
keystore_alias: str,
|
|
380
|
+
keystore_password: Optional[Callable[[], str]]=None) -> tuple[Path, Plugin]:
|
|
381
|
+
"""
|
|
382
|
+
Builds the given plugin with the supplied plugin signing credentials.
|
|
383
|
+
|
|
384
|
+
:param plugin_id: A plugin identifier.
|
|
385
|
+
:type plugin_id: PluginIdentifier
|
|
386
|
+
:param keystore_path: The path to the plugin signing keystore.
|
|
387
|
+
:type keystore_path: Path
|
|
388
|
+
:param keystore_alias: The alias to use in the plugin signing keystore.
|
|
389
|
+
:type keystore_alias: str
|
|
390
|
+
:param keystore_password: The plugin signing password.
|
|
391
|
+
:type keystore_password: Optional[Callable[[], str]]
|
|
392
|
+
:return: A tuple of the built and signed JAR file and a Plugin object
|
|
393
|
+
instantiated from it.
|
|
394
|
+
:rtype: tuple[Path, Plugin]
|
|
395
|
+
:raises Exception: If ``JAVA_HOME`` is not set in the environment.
|
|
396
|
+
:raises subprocess.CalledProcessError: If a subprocess fails.
|
|
397
|
+
:raises FileNotFoundError: If the expected built and signed plugin JAR
|
|
398
|
+
path is unexpectedly not found despite the
|
|
399
|
+
build.
|
|
400
|
+
"""
|
|
122
401
|
# Prerequisites
|
|
123
402
|
if 'JAVA_HOME' not in os.environ:
|
|
124
403
|
raise Exception('error: JAVA_HOME must be set in your environment')
|
|
@@ -127,43 +406,57 @@ class AntPluginSet(PluginSet):
|
|
|
127
406
|
# Little build
|
|
128
407
|
return self._little_build(plugin_id, keystore_path, keystore_alias, keystore_password=keystore_password)
|
|
129
408
|
|
|
130
|
-
def
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return self.get_root_path().joinpath(self.get_main())
|
|
135
|
-
|
|
136
|
-
def get_root(self):
|
|
137
|
-
return self._root
|
|
138
|
-
|
|
139
|
-
def get_root_path(self):
|
|
140
|
-
return Path(self.get_root()).expanduser().resolve()
|
|
141
|
-
|
|
142
|
-
def get_test(self):
|
|
143
|
-
return self._parsed.get('test', AntPluginSet.DEFAULT_TEST)
|
|
409
|
+
def model_post_init(self,
|
|
410
|
+
context: Any) -> None:
|
|
411
|
+
"""
|
|
412
|
+
Pydantic post-initialization method to initialize the ``_built`` flag.
|
|
144
413
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
414
|
+
:param context: The Pydantic context.
|
|
415
|
+
:type context: Any
|
|
416
|
+
"""
|
|
417
|
+
super().model_post_init(context)
|
|
418
|
+
self._built = False
|
|
150
419
|
|
|
151
|
-
def
|
|
152
|
-
|
|
420
|
+
def _big_build(self) -> None:
|
|
421
|
+
"""
|
|
422
|
+
Runs ``ant load-plugins`` if the ``_built`` flag is False.
|
|
153
423
|
|
|
154
|
-
|
|
424
|
+
:raises subprocess.CalledProcessError: If a subprocess fails.
|
|
425
|
+
"""
|
|
155
426
|
if not self._built:
|
|
156
427
|
# Do build
|
|
157
428
|
subprocess.run('ant load-plugins',
|
|
158
|
-
shell=True, cwd=self.
|
|
429
|
+
shell=True, cwd=self.get_root(), check=True, stdout=sys.stdout, stderr=sys.stderr)
|
|
159
430
|
self._built = True
|
|
160
431
|
|
|
161
|
-
|
|
162
|
-
|
|
432
|
+
def _little_build(self,
|
|
433
|
+
plugin_id: PluginIdentifier,
|
|
434
|
+
keystore_path: Path,
|
|
435
|
+
keystore_alias: str,
|
|
436
|
+
keystore_password: Optional[Callable[[], str]]=None) -> tuple[Path, Plugin]:
|
|
437
|
+
"""
|
|
438
|
+
Performs the "little build" of the given plugin.
|
|
439
|
+
|
|
440
|
+
:param plugin_id: A plugin identifier.
|
|
441
|
+
:type plugin_id: PluginIdentifier
|
|
442
|
+
:param keystore_path: The path to the plugin signing keystore.
|
|
443
|
+
:type keystore_path: Path
|
|
444
|
+
:param keystore_alias: The alias to use in the plugin signing keystore.
|
|
445
|
+
:type keystore_alias: str
|
|
446
|
+
:param keystore_password: The plugin signing password.
|
|
447
|
+
:type keystore_password: Optional[Callable[[], str]]
|
|
448
|
+
:return: A tuple of the built and signed JAR file and a Plugin object
|
|
449
|
+
instantiated from it.
|
|
450
|
+
:rtype: tuple[Path, Plugin]
|
|
451
|
+
:raises subprocess.CalledProcessError: If a subprocess fails.
|
|
452
|
+
:raises FileNotFoundError: If the expected built and signed plugin JAR
|
|
453
|
+
path is unexpectedly not found despite the
|
|
454
|
+
build.
|
|
455
|
+
"""
|
|
163
456
|
orig_plugin = None
|
|
164
457
|
cur_id = plugin_id
|
|
165
458
|
# Get all directories for jarplugin -d
|
|
166
|
-
dirs =
|
|
459
|
+
dirs = []
|
|
167
460
|
while cur_id is not None:
|
|
168
461
|
cur_plugin = self.make_plugin(cur_id)
|
|
169
462
|
orig_plugin = orig_plugin or cur_plugin
|
|
@@ -177,112 +470,166 @@ class AntPluginSet(PluginSet):
|
|
|
177
470
|
cur_id = cur_plugin.get_parent_identifier()
|
|
178
471
|
# Invoke jarplugin
|
|
179
472
|
jar_fstr = Plugin.id_to_file(plugin_id)
|
|
180
|
-
jar_path = self.
|
|
473
|
+
jar_path = self.get_root().joinpath('plugins/jars', f'{plugin_id}.jar')
|
|
181
474
|
jar_path.parent.mkdir(parents=True, exist_ok=True)
|
|
182
475
|
cmd = ['test/scripts/jarplugin',
|
|
183
476
|
'-j', str(jar_path),
|
|
184
477
|
'-p', str(jar_fstr)]
|
|
185
478
|
for d in dirs:
|
|
186
479
|
cmd.extend(['-d', d])
|
|
187
|
-
subprocess.run(cmd, cwd=self.
|
|
480
|
+
subprocess.run(cmd, cwd=self.get_root(), check=True, stdout=sys.stdout, stderr=sys.stderr)
|
|
188
481
|
# Invoke signplugin
|
|
189
482
|
cmd = ['test/scripts/signplugin',
|
|
190
483
|
'--jar', str(jar_path),
|
|
191
484
|
'--alias', keystore_alias,
|
|
192
485
|
'--keystore', str(keystore_path)]
|
|
193
|
-
if keystore_password
|
|
194
|
-
cmd.extend(['--password', keystore_password])
|
|
486
|
+
if keystore_password:
|
|
487
|
+
cmd.extend(['--password', keystore_password()])
|
|
195
488
|
try:
|
|
196
|
-
subprocess.run(cmd, cwd=self.
|
|
489
|
+
subprocess.run(cmd, cwd=self.get_root(), check=True, stdout=sys.stdout, stderr=sys.stderr)
|
|
197
490
|
except subprocess.CalledProcessError as cpe:
|
|
198
491
|
raise self._sanitize(cpe)
|
|
199
492
|
if not jar_path.is_file():
|
|
200
493
|
raise FileNotFoundError(str(jar_path))
|
|
201
|
-
return
|
|
494
|
+
return jar_path, orig_plugin
|
|
495
|
+
|
|
496
|
+
# def _plugin_path(self, plugin_id: PluginIdentifier) -> Path:
|
|
497
|
+
# return self.get_main().joinpath(Plugin.id_to_file(plugin_id))
|
|
202
498
|
|
|
203
|
-
def
|
|
204
|
-
|
|
499
|
+
def _sanitize(self,
|
|
500
|
+
called_process_error: subprocess.CalledProcessError) -> subprocess.CalledProcessError:
|
|
501
|
+
"""
|
|
502
|
+
Alters the value of ``--password`` in a called process error.
|
|
205
503
|
|
|
206
|
-
|
|
504
|
+
:param called_process_error: A called process error.
|
|
505
|
+
:return: The same called process error, with the value of ``--password``
|
|
506
|
+
altered in ``cmd``.
|
|
507
|
+
"""
|
|
207
508
|
cmd = called_process_error.cmd[:]
|
|
208
|
-
i
|
|
209
|
-
|
|
210
|
-
if i > 1 and cmd[i - 1] == '--password':
|
|
509
|
+
for i in range(1, len(cmd)):
|
|
510
|
+
if cmd[i - 1] == '--password':
|
|
211
511
|
cmd[i] = '<password>'
|
|
212
512
|
called_process_error.cmd = ' '.join([shlex.quote(c) for c in cmd])
|
|
213
513
|
return called_process_error
|
|
214
514
|
|
|
215
515
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
def
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return
|
|
280
|
-
|
|
281
|
-
def
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return
|
|
516
|
+
#: A type alias for plugin set builders, which is the union of
|
|
517
|
+
#: ``MavenPluginSetBuilder`` and ``AntPluginSetBuilder`` using ``type`` as the
|
|
518
|
+
#: discriminator field.
|
|
519
|
+
PluginSetBuilder = Annotated[Union[MavenPluginSetBuilder, AntPluginSetBuilder], Field(discriminator='type')]
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
#: A type alias for the plugin set kind.
|
|
523
|
+
PluginSetKind = Literal['PluginSet']
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
#: A type alias for plugin set identifiers.
|
|
527
|
+
PluginSetIdentifier = str
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
class PluginSet(BaseModel):
|
|
531
|
+
"""
|
|
532
|
+
A Pydantic model for a plugin set.
|
|
533
|
+
"""
|
|
534
|
+
|
|
535
|
+
#: This object's kind.
|
|
536
|
+
kind: PluginSetKind = Field(title='Kind',
|
|
537
|
+
description="This object's kind")
|
|
538
|
+
|
|
539
|
+
#: This plugin set's identifier.
|
|
540
|
+
id: PluginSetIdentifier = Field(title='Plugin Set Identifier',
|
|
541
|
+
description='An identifier for the plugin set')
|
|
542
|
+
|
|
543
|
+
#: This plugin set's name.
|
|
544
|
+
name: str = Field(title='Plugin Set Name',
|
|
545
|
+
description='A name for the plugin set')
|
|
546
|
+
|
|
547
|
+
#: This plugin set's builder.
|
|
548
|
+
builder: PluginSetBuilder = Field(title='Plugin Set Builder',
|
|
549
|
+
description='A builder for the plugin set')
|
|
550
|
+
|
|
551
|
+
def build_plugin(self,
|
|
552
|
+
plugin_id: PluginIdentifier,
|
|
553
|
+
keystore_path: Path,
|
|
554
|
+
keystore_alias: str,
|
|
555
|
+
keystore_password: Optional[Callable[[], str]]=None) -> tuple[Path, Plugin]:
|
|
556
|
+
"""
|
|
557
|
+
Builds the given plugin with the supplied plugin signing credentials.
|
|
558
|
+
|
|
559
|
+
:param plugin_id: A plugin identifier.
|
|
560
|
+
:type plugin_id: PluginIdentifier
|
|
561
|
+
:param keystore_path: The path to the plugin signing keystore.
|
|
562
|
+
:type keystore_path: Path
|
|
563
|
+
:param keystore_alias: The alias to use in the plugin signing keystore.
|
|
564
|
+
:type keystore_alias: str
|
|
565
|
+
:param keystore_password: The plugin signing password.
|
|
566
|
+
:type keystore_password: Optional[Callable[[], str]]
|
|
567
|
+
:return: A tuple of the built and signed JAR file and a Plugin object
|
|
568
|
+
instantiated from it.
|
|
569
|
+
"""
|
|
570
|
+
return self.builder.build_plugin(plugin_id, keystore_path, keystore_alias, keystore_password)
|
|
571
|
+
|
|
572
|
+
def get_builder(self) -> PluginSetBuilder:
|
|
573
|
+
"""
|
|
574
|
+
Returns this plugin set's builder.
|
|
575
|
+
|
|
576
|
+
:return: This plugin set's builder.
|
|
577
|
+
:rtype: PluginSetBuilder
|
|
578
|
+
"""
|
|
579
|
+
return self.builder
|
|
580
|
+
|
|
581
|
+
def get_id(self) -> PluginSetIdentifier:
|
|
582
|
+
"""
|
|
583
|
+
Returns this plugin set's identifier.
|
|
584
|
+
|
|
585
|
+
:return: This plugin set's identifier.
|
|
586
|
+
:rtype: PluginSetIdentifier
|
|
587
|
+
"""
|
|
588
|
+
return self.id
|
|
589
|
+
|
|
590
|
+
def get_name(self) -> str:
|
|
591
|
+
"""
|
|
592
|
+
Returns this plugin set's name.
|
|
593
|
+
|
|
594
|
+
:return: This plugin set's name.
|
|
595
|
+
:rtype: str
|
|
596
|
+
"""
|
|
597
|
+
return self.name
|
|
598
|
+
|
|
599
|
+
def has_plugin(self,
|
|
600
|
+
plugin_id: PluginIdentifier) -> bool:
|
|
601
|
+
"""
|
|
602
|
+
Determines if the given plugin identifier represents a plugin that is
|
|
603
|
+
present in the plugin set.
|
|
604
|
+
|
|
605
|
+
:param plugin_id: A plugin identifier.
|
|
606
|
+
:type plugin_id: PluginIdentifier
|
|
607
|
+
:return: Whether the plugin is present in the plugin set.
|
|
608
|
+
:rtype: bool
|
|
609
|
+
"""
|
|
610
|
+
return self.get_builder().has_plugin(plugin_id)
|
|
611
|
+
|
|
612
|
+
def initialize(self,
|
|
613
|
+
root: Path) -> PluginSet:
|
|
614
|
+
"""
|
|
615
|
+
Mandatory initialization of the builder.
|
|
616
|
+
|
|
617
|
+
:param root: This plugin set's root path.
|
|
618
|
+
:type root: Path
|
|
619
|
+
:return: This plugin set, for chaining.
|
|
620
|
+
:rtype: PluginSet
|
|
621
|
+
"""
|
|
622
|
+
self.get_builder().initialize(root)
|
|
623
|
+
return self
|
|
624
|
+
|
|
625
|
+
def make_plugin(self,
|
|
626
|
+
plugin_id: PluginIdentifier) -> Plugin:
|
|
627
|
+
"""
|
|
628
|
+
Makes a ``Plugin`` object from the given plugin identifier.
|
|
629
|
+
|
|
630
|
+
:param plugin_id: A plugin identifier.
|
|
631
|
+
:type plugin_id: PluginIdentifier
|
|
632
|
+
:return: The corresponding ``Plugin`` object.
|
|
633
|
+
:rtype: Plugin
|
|
634
|
+
"""
|
|
635
|
+
return self.get_builder().make_plugin(plugin_id)
|