lockss-turtles 0.5.0.dev3__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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- # Copyright (c) 2000-2023, Board of Trustees of Leland Stanford Jr. University
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
- import importlib.resources
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
- from lockss.turtles.plugin import Plugin
39
- import lockss.turtles.resources
40
- from lockss.turtles.util import _load_and_validate, _path
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
- def get_name(self):
98
- return self._parsed['name']
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 has_plugin(self, plugin_id):
101
- raise NotImplementedError('has_plugin')
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 make_plugin(self, plugin_id):
104
- raise NotImplementedError('make_plugin')
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
- class AntPluginSet(PluginSet):
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
- TYPE = 'ant'
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
- DEFAULT_MAIN = 'plugins/src'
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
- DEFAULT_TEST = 'plugins/test/src'
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
- # Returns (jar_path, plugin)
121
- def build_plugin(self, plugin_id, keystore_path, keystore_alias, keystore_password=None):
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 get_main(self):
131
- return self._parsed.get('main', AntPluginSet.DEFAULT_MAIN)
132
-
133
- def get_main_path(self):
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
- def get_test_path(self):
146
- return self.get_root_path().joinpath(self.get_test())
147
-
148
- def has_plugin(self, plugin_id):
149
- return self._plugin_path(plugin_id).is_file()
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 make_plugin(self, plugin_id):
152
- return Plugin.from_path(self._plugin_path(plugin_id))
420
+ def _big_build(self) -> None:
421
+ """
422
+ Runs ``ant load-plugins`` if the ``_built`` flag is False.
153
423
 
154
- def _big_build(self):
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.get_root_path(), check=True, stdout=sys.stdout, stderr=sys.stderr)
429
+ shell=True, cwd=self.get_root(), check=True, stdout=sys.stdout, stderr=sys.stderr)
159
430
  self._built = True
160
431
 
161
- # Returns (jar_path, plugin)
162
- def _little_build(self, plugin_id, keystore_path, keystore_alias, keystore_password=None):
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 = list()
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.get_root_path().joinpath('plugins/jars', f'{plugin_id}.jar')
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.get_root_path(), check=True, stdout=sys.stdout, stderr=sys.stderr)
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 is not None:
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.get_root_path(), check=True, stdout=sys.stdout, stderr=sys.stderr)
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 (jar_path, plugin)
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 _plugin_path(self, plugin_id):
204
- return Path(self.get_main_path()).joinpath(Plugin.id_to_file(plugin_id))
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
- def _sanitize(self, called_process_error):
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 = 0
209
- for i in range(len(cmd)):
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
- class MavenPluginSet(PluginSet):
217
-
218
- TYPE = 'maven'
219
-
220
- DEFAULT_MAIN = 'src/main/java'
221
-
222
- DEFAULT_TEST = 'src/test/java'
223
-
224
- def __init__(self, parsed, path):
225
- super().__init__(parsed)
226
- self._built = False
227
- self._root = path.parent
228
-
229
- # Returns (jar_path, plugin)
230
- def build_plugin(self, plugin_id, keystore_path, keystore_alias, keystore_password=None):
231
- self._big_build(keystore_path, keystore_alias, keystore_password=keystore_password)
232
- return self._little_build(plugin_id)
233
-
234
- def get_main(self):
235
- return self._parsed.get('main', MavenPluginSet.DEFAULT_MAIN)
236
-
237
- def get_main_path(self):
238
- return self.get_root_path().joinpath(self.get_main())
239
-
240
- def get_root(self):
241
- return self._root
242
-
243
- def get_root_path(self):
244
- return Path(self.get_root()).expanduser().resolve()
245
-
246
- def get_test(self):
247
- return self._parsed.get('test', MavenPluginSet.DEFAULT_TEST)
248
-
249
- def get_test_path(self):
250
- return self.get_root_path().joinpath(self.get_test())
251
-
252
- def has_plugin(self, plugin_id):
253
- return self._plugin_path(plugin_id).is_file()
254
-
255
- def make_plugin(self, plugin_id):
256
- return Plugin.from_path(self._plugin_path(plugin_id))
257
-
258
- def _big_build(self, keystore_path, keystore_alias, keystore_password=None):
259
- if not self._built:
260
- # Do build
261
- cmd = ['mvn', 'package',
262
- f'-Dkeystore.file={keystore_path!s}',
263
- f'-Dkeystore.alias={keystore_alias}',
264
- f'-Dkeystore.password={keystore_password}']
265
- try:
266
- subprocess.run(cmd, cwd=self.get_root_path(), check=True, stdout=sys.stdout, stderr=sys.stderr)
267
- except subprocess.CalledProcessError as cpe:
268
- raise self._sanitize(cpe)
269
- self._built = True
270
-
271
- # Returns (jar_path, plugin)
272
- def _little_build(self, plugin_id):
273
- jar_path = Path(self.get_root_path(), 'target', 'pluginjars', f'{plugin_id}.jar')
274
- if not jar_path.is_file():
275
- raise Exception(f'{plugin_id}: built JAR not found: {jar_path!s}')
276
- return (jar_path, Plugin.from_jar(jar_path))
277
-
278
- def _plugin_path(self, plugin_id):
279
- return Path(self.get_main_path()).joinpath(Plugin.id_to_file(plugin_id))
280
-
281
- def _sanitize(self, called_process_error):
282
- cmd = called_process_error.cmd[:]
283
- i = 0
284
- for i in range(len(cmd)):
285
- if cmd[i].startswith('-Dkeystore.password='):
286
- cmd[i] = '-Dkeystore.password=<password>'
287
- called_process_error.cmd = ' '.join([shlex.quote(c) for c in cmd])
288
- return called_process_error
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)