modpkgbuild 0.1.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.
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: modpkgbuild
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Creates a new PKGBUILD by modifying another
|
|
5
|
+
Author-email: Russell Wright Helder <rwh@russellhelder.net>
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
14
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
15
|
+
Classifier: Topic :: System :: Archiving :: Packaging
|
|
16
|
+
Classifier: Topic :: System :: Software Distribution
|
|
17
|
+
Classifier: Topic :: Text Processing :: Filters
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: pyyaml
|
|
20
|
+
Project-URL: Homepage, https://github.com/rhelder/modpkgbuild
|
|
21
|
+
Project-URL: Issues, https://github.com/rhelder/modpkgbuild/issues
|
|
22
|
+
Import-Name: modpkgbuild
|
|
23
|
+
|
|
24
|
+
# modpkgbuild
|
|
25
|
+
|
|
26
|
+
## Description
|
|
27
|
+
|
|
28
|
+
Creates one PKGBUILD from another PKGBUILD by applying a series of changes
|
|
29
|
+
specified in a YAML file. Designed to automatically generate alternative
|
|
30
|
+
versions of Arch Linux packages (e.g., enabling SELinux support, applying
|
|
31
|
+
security-related patches, etc.). This makes it possible to e.g. implement a
|
|
32
|
+
CI/CD pipeline that checks for updates to the relevant Arch package,
|
|
33
|
+
automatically generates an alternative PKGBUILD when there has been an update,
|
|
34
|
+
builds the alternative package in a clean container, and deploys it to a
|
|
35
|
+
custom Pacman repo.
|
|
36
|
+
|
|
37
|
+
The PKGBUILD is written to the current working directory. A diff between the
|
|
38
|
+
old PKGBUILD and the new PKGBUILD is written to stderr on success.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
This script is available on PyPI as `modpkgbuild`:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
pip install modpkgbuild
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## modpkgbuild.yml
|
|
49
|
+
|
|
50
|
+
`modpkgbuild` must be run in the same directory as a `modpkgbuild.yml`. The
|
|
51
|
+
`modpkgbuild.yml` must contain the following two fields:
|
|
52
|
+
|
|
53
|
+
| Key | Value |
|
|
54
|
+
| ------------ | -------------------------------
|
|
55
|
+
| repo | The URL to the git repository that contains the PKGBUILD you want to modify |
|
|
56
|
+
| replacements | A list of replacement objects |
|
|
57
|
+
|
|
58
|
+
A replacement object must contain three fields:
|
|
59
|
+
|
|
60
|
+
| Key | Value |
|
|
61
|
+
| ----------- | ---------------------------------------------------------
|
|
62
|
+
| description | A description of the purpose of the replacement |
|
|
63
|
+
| old | A literal string you want to replace (may be multiline) |
|
|
64
|
+
| new | A literal string you want to replace `old` with (may be multiline) |
|
|
65
|
+
|
|
66
|
+
Or, if the optional field `regex` is set to true, the fields mean the
|
|
67
|
+
following:
|
|
68
|
+
|
|
69
|
+
| Key | Value |
|
|
70
|
+
| ----------- | ---------------------------------------------------------
|
|
71
|
+
| description | A description of the purpose of the replacement |
|
|
72
|
+
| old | A regular expression matching text you want to replace (may be multiline, and may contain captures) |
|
|
73
|
+
| new | A replacement string (may be multiline, may contain backreferences) |
|
|
74
|
+
|
|
75
|
+
All replacements must be specified **in order**. Any failure to match is
|
|
76
|
+
considered an error. `modpkgbuild` is intentionally conservative in these ways
|
|
77
|
+
so that it can be relied on in an automated packaging pipeline to not produce
|
|
78
|
+
an incorrect PKGBUILD without an error.
|
|
79
|
+
|
|
80
|
+
Accordingly, the user should use literal strings in `modpkgbuild.yml` whenever
|
|
81
|
+
possible. Setting `regex` to true is to be discouraged unless there is a good
|
|
82
|
+
reason – for example, you may need to capture part of the match and reference
|
|
83
|
+
it in the replacement text.
|
|
84
|
+
|
|
85
|
+
## Project Status
|
|
86
|
+
|
|
87
|
+
This package should be considered to be alpha software. Until `modpkgbuild`
|
|
88
|
+
has been more extensively tested, the user should manually review generated
|
|
89
|
+
PKGBUILDs before building or deploying packages.
|
|
90
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
modpkgbuild.py,sha256=sKTwCw2012XkQe0TzPT4I7SnhtGn55BlMGWjkwBzNek,8955
|
|
2
|
+
modpkgbuild-0.1.0.dist-info/entry_points.txt,sha256=ie8ZfD45Kz0Q8HuYHpJVlyU89sEl0XtZn_HAJrAsFzA,48
|
|
3
|
+
modpkgbuild-0.1.0.dist-info/licenses/LICENSE,sha256=EQJaGGeBeSD0jhbWKGQp1bCxUr5M5Ty_FlNpaf_RIDw,1078
|
|
4
|
+
modpkgbuild-0.1.0.dist-info/WHEEL,sha256=rLJE8OLPBZbz7tqxs1yD-wA-ADa1Ec5PuYU-Y_PNWDI,81
|
|
5
|
+
modpkgbuild-0.1.0.dist-info/METADATA,sha256=Gr0dm5NwO8rOav5TmNe0AVKeYWbCOSSi9oSSlufXptU,3823
|
|
6
|
+
modpkgbuild-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Russell Wright Helder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
modpkgbuild.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Creates one PKGBUILD from another by applying changes specified in a YAML file.
|
|
2
|
+
|
|
3
|
+
Designed to automatically generate alternative versions of Arch Linux packages
|
|
4
|
+
(e.g., enabling SELinux support, applying security-related patches, etc.).
|
|
5
|
+
Must be run in the same directory as a modpkgbuild.yml. The PKGBUILD is
|
|
6
|
+
written to the current working directory. A diff between the old PKGBUILD and
|
|
7
|
+
the new PKGBUILD is written to stderr on success.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import difflib
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from yaml import CSafeLoader as SafeLoader
|
|
22
|
+
except ImportError:
|
|
23
|
+
from yaml import SafeLoader
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ReplacementError(Exception):
|
|
27
|
+
"""An error in applying a replacement."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SpecError(Exception):
|
|
31
|
+
"""An error in modpkgbuild.yml."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def write_new(new: str):
|
|
35
|
+
"""Writes the new PKGBUILD to `./PKGBUILD`.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
new (string):
|
|
39
|
+
The content of the new PKGBUILD.
|
|
40
|
+
"""
|
|
41
|
+
tmpdir = tempfile.TemporaryDirectory(dir='.')
|
|
42
|
+
pkgbuild_path = Path(tmpdir.name) / 'PKGBUILD'
|
|
43
|
+
|
|
44
|
+
pkgbuild_path.write_text(new)
|
|
45
|
+
|
|
46
|
+
target_path = Path('PKGBUILD')
|
|
47
|
+
|
|
48
|
+
if target_path.is_file():
|
|
49
|
+
message('warning: PKGBUILD exists. Saving as PKGBUILD.orig...')
|
|
50
|
+
|
|
51
|
+
orig_path = Path('PKGBUILD.orig')
|
|
52
|
+
|
|
53
|
+
if orig_path.is_file():
|
|
54
|
+
raise FileExistsError('PKGBUILD.orig also exists. Aborting.')
|
|
55
|
+
else:
|
|
56
|
+
target_path.rename(orig_path)
|
|
57
|
+
|
|
58
|
+
pkgbuild_path.rename(target_path)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def gen_new(old: str, spec: dict) -> str:
|
|
62
|
+
"""Applies the changes specified in `modpkgbuild.yml`.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
old (string):
|
|
66
|
+
The content of the upstream PKGBUILD.
|
|
67
|
+
spec (dictionary):
|
|
68
|
+
The data loaded from modpkgbuild.yml.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
(string) The content of the new PKGBUILD.
|
|
72
|
+
"""
|
|
73
|
+
new = []
|
|
74
|
+
pos = 0
|
|
75
|
+
|
|
76
|
+
for r in spec['replacements']:
|
|
77
|
+
# Save the position of the end of the last match in the old PKGBUILD
|
|
78
|
+
prev_pos = pos
|
|
79
|
+
|
|
80
|
+
if not r.get('regex', False):
|
|
81
|
+
# Search for the next match in the old PKGBUILD, starting at the
|
|
82
|
+
# end of the last match, so that the changes specified in
|
|
83
|
+
# modpkgbuild.yml are applied in order. Advance the position
|
|
84
|
+
# tracker to the position of the beginning of the next match. It
|
|
85
|
+
# is an error if a match cannot be found.
|
|
86
|
+
try:
|
|
87
|
+
pos += old[prev_pos:].index(r['old'])
|
|
88
|
+
except ValueError:
|
|
89
|
+
raise ReplacementError(
|
|
90
|
+
f"substring '{r['old']}' "
|
|
91
|
+
f"in replacement '{r['description']}' not found"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# The text between the end of the last match and the start of the
|
|
95
|
+
# next match are the same in the old PKGBUILD and in the new
|
|
96
|
+
# PKGBUILD. Append it to the new PKGBUILD.
|
|
97
|
+
new += old[prev_pos:pos]
|
|
98
|
+
# At this position, append the replacement text to the new
|
|
99
|
+
# PKGBUILD
|
|
100
|
+
new += r['new']
|
|
101
|
+
# Advance the position tracker, which tracks our position in the
|
|
102
|
+
# old PKGBUILD, by the length of the matched and the replaced
|
|
103
|
+
# text
|
|
104
|
+
pos += len(r['old'])
|
|
105
|
+
else:
|
|
106
|
+
try:
|
|
107
|
+
pattern = re.compile(r['old'])
|
|
108
|
+
except re.PatternError as e:
|
|
109
|
+
raise SpecError(
|
|
110
|
+
f"invalid regex in replacement '{r['description']}': {e}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Search for the next match in the old PKGBUILD, starting at the
|
|
114
|
+
# end of the last match, so that the changes specified in
|
|
115
|
+
# modpkgbuild.yml are applied in order
|
|
116
|
+
match = re.search(pattern, old[prev_pos:])
|
|
117
|
+
|
|
118
|
+
# It is an error if a match cannot be found
|
|
119
|
+
if not match:
|
|
120
|
+
raise ReplacementError(
|
|
121
|
+
f"pattern '{pattern.pattern}' "
|
|
122
|
+
f"in replacement '{r['description']}' not matched"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Advance the position tracker to the position of the beginning of
|
|
126
|
+
# the next match
|
|
127
|
+
pos += match.start()
|
|
128
|
+
# The text between the end of the last match and the start of the
|
|
129
|
+
# next match are the same in the old PKGBUILD and in the new
|
|
130
|
+
# PKGBUILD. Append it to the new PKGBUILD.
|
|
131
|
+
new += old[prev_pos:pos]
|
|
132
|
+
# At this position, append the replacement text to the new
|
|
133
|
+
# PKGBUILD
|
|
134
|
+
new += match.expand(r['new'])
|
|
135
|
+
# Advance the position tracker, which tracks our position in the
|
|
136
|
+
# old PKGBUILD, by the length of the matched and the replaced
|
|
137
|
+
# text
|
|
138
|
+
pos += len(match[0])
|
|
139
|
+
|
|
140
|
+
# Append any remaining text in the old PKGBUILD to the new PKGBUILD
|
|
141
|
+
new += old[pos:]
|
|
142
|
+
return ''.join(new)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def read_old(repo: str) -> str:
|
|
146
|
+
"""Clones the upstream git repo and reads its PKGBUILD.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
repo (string):
|
|
150
|
+
The URL to the upstream git repository.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
(string) The content of the upstream PKGBUILD.
|
|
154
|
+
"""
|
|
155
|
+
tmpdir = tempfile.TemporaryDirectory()
|
|
156
|
+
|
|
157
|
+
subprocess.run(
|
|
158
|
+
['git', 'clone', repo, tmpdir.name],
|
|
159
|
+
check=True,
|
|
160
|
+
capture_output=True,
|
|
161
|
+
text=True,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
with open(Path(tmpdir.name) / 'PKGBUILD') as f:
|
|
165
|
+
return f.read()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def parse_spec() -> dict:
|
|
169
|
+
"""Loads and validates modpkgbuild.yml.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
(dictionary) The data loaded from modpkgbuild.yml.
|
|
173
|
+
"""
|
|
174
|
+
spec = load_spec()
|
|
175
|
+
|
|
176
|
+
validate_spec(spec)
|
|
177
|
+
return spec
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def load_spec() -> dict:
|
|
181
|
+
"""Loads modpkgbuild.yml.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
(dictionary) The data loaded from modpkgbuild.yml.
|
|
185
|
+
"""
|
|
186
|
+
with open('./modpkgbuild.yml') as f:
|
|
187
|
+
spec = yaml.load(f.read(), Loader=SafeLoader)
|
|
188
|
+
|
|
189
|
+
return spec
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def validate_spec(spec: dict):
|
|
193
|
+
"""Validates the data loaded from modpkgbuild.yml.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
spec (dictionary):
|
|
197
|
+
The data loaded from modpkgbuild.yml.
|
|
198
|
+
"""
|
|
199
|
+
if not isinstance(spec, dict):
|
|
200
|
+
raise SpecError('expected a YAML mapping')
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
if not isinstance(spec['repo'], str):
|
|
204
|
+
raise SpecError("'repo' must be a string")
|
|
205
|
+
elif not isinstance(spec['replacements'], list):
|
|
206
|
+
raise SpecError("'replacements' must be a list")
|
|
207
|
+
except KeyError as e:
|
|
208
|
+
raise SpecError(f'missing required field {e}')
|
|
209
|
+
|
|
210
|
+
for i, r in enumerate(spec['replacements']):
|
|
211
|
+
try:
|
|
212
|
+
if not isinstance(r['description'], str):
|
|
213
|
+
raise SpecError(
|
|
214
|
+
f"'description' in replacement {i + 1} must be a string"
|
|
215
|
+
)
|
|
216
|
+
elif not isinstance(r['old'], str):
|
|
217
|
+
raise SpecError(
|
|
218
|
+
f"'old' in replacement '{r['description']}' must be a string"
|
|
219
|
+
)
|
|
220
|
+
elif not isinstance(r['new'], str):
|
|
221
|
+
raise SpecError(
|
|
222
|
+
f"'new' in replacement '{r['description']}' must be a string"
|
|
223
|
+
)
|
|
224
|
+
except KeyError as e:
|
|
225
|
+
raise SpecError(
|
|
226
|
+
'missing required field {} in replacement {}'.format(
|
|
227
|
+
e,
|
|
228
|
+
f"'{r['description']}'" if r.get('description') else i + 1,
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
raise
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def parse_args():
|
|
235
|
+
"""Prints help message if necessary."""
|
|
236
|
+
parser = argparse.ArgumentParser(
|
|
237
|
+
description="""
|
|
238
|
+
Create one PKGBUILD from another by applying changes specified in a YAML file.
|
|
239
|
+
""",
|
|
240
|
+
epilog="""
|
|
241
|
+
Must be run in the same directory as a modpkgbuild.yml.
|
|
242
|
+
The PKGBUILD is written to the current working directory.
|
|
243
|
+
Prints a diff to stderr on success.
|
|
244
|
+
""",
|
|
245
|
+
)
|
|
246
|
+
return parser.parse_args()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def message(*args: str, **kwargs):
|
|
250
|
+
"""Prints messages to stderr."""
|
|
251
|
+
print('modpkgbuild.py:', *args, **kwargs, file=sys.stderr)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def main():
|
|
255
|
+
parse_args()
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
spec = parse_spec()
|
|
259
|
+
old = read_old(spec['repo'])
|
|
260
|
+
new = gen_new(old, spec)
|
|
261
|
+
write_new(new)
|
|
262
|
+
except OSError as e:
|
|
263
|
+
message(
|
|
264
|
+
f' {e.filename}:' if e.filename else ' ',
|
|
265
|
+
e.strerror or str(e),
|
|
266
|
+
sep='',
|
|
267
|
+
)
|
|
268
|
+
return 1
|
|
269
|
+
except yaml.YAMLError as e:
|
|
270
|
+
message('failed to parse modpkgbuild.yml:', str(e))
|
|
271
|
+
return 2
|
|
272
|
+
except SpecError as e:
|
|
273
|
+
message('invalid modpkgbuild.yml:', str(e))
|
|
274
|
+
return 2
|
|
275
|
+
except subprocess.CalledProcessError as e:
|
|
276
|
+
message(e.stderr)
|
|
277
|
+
return e.returncode
|
|
278
|
+
except ReplacementError as e:
|
|
279
|
+
message(str(e))
|
|
280
|
+
return 1
|
|
281
|
+
|
|
282
|
+
message(
|
|
283
|
+
'new PKGBUILD successfully written to ./PKGBUILD,',
|
|
284
|
+
'with the following changes from the old PKGBUILD:',
|
|
285
|
+
)
|
|
286
|
+
sys.stderr.writelines(
|
|
287
|
+
difflib.unified_diff(
|
|
288
|
+
old.splitlines(keepends=True),
|
|
289
|
+
new.splitlines(keepends=True),
|
|
290
|
+
fromfile=f'{spec["repo"]}:PKGBUILD',
|
|
291
|
+
tofile='PKGBUILD',
|
|
292
|
+
)
|
|
293
|
+
)
|