Skip to content

Commit dc5d12d

Browse files
authored
Merge pull request #39 from keszybz/rpm-requires
Add generator for rpm Requires/Recommends
2 parents 6d1ca42 + c2d3548 commit dc5d12d

File tree

2 files changed

+156
-77
lines changed

2 files changed

+156
-77
lines changed

dlopen-notes.py

Lines changed: 132 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -24,50 +24,54 @@ def wrap(*args, **kwargs):
2424
return list(f(*args, **kwargs))
2525
return functools.update_wrapper(wrap, f)
2626

27-
@listify
28-
def read_dlopen_notes(filename):
29-
elffile = ELFFile(open(filename, 'rb'))
30-
31-
for section in elffile.iter_sections():
32-
if not isinstance(section, NoteSection) or section.name != '.note.dlopen':
33-
continue
34-
35-
for note in section.iter_notes():
36-
if note['n_type'] != 0x407c0c0a or note['n_name'] != 'FDO':
27+
class ELFFileReader:
28+
def __init__(self, filename):
29+
self.filename = filename
30+
self.elffile = ELFFile(open(filename, 'rb'))
31+
32+
@functools.cache
33+
@listify
34+
def notes(self):
35+
for section in self.elffile.iter_sections():
36+
if not isinstance(section, NoteSection) or section.name != '.note.dlopen':
3737
continue
38-
note_desc = note['n_desc']
39-
40-
try:
41-
# On older Python versions (e.g.: Ubuntu 22.04) we get a string, on
42-
# newer versions a bytestring
43-
if not isinstance(note_desc, str):
44-
text = note_desc.decode('utf-8').rstrip('\0')
45-
else:
46-
text = note_desc.rstrip('\0')
47-
except UnicodeDecodeError as e:
48-
raise ValueError(f'{filename}: Invalid UTF-8 in .note.dlopen n_desc') from e
49-
50-
try:
51-
j = json.loads(text)
52-
except json.JSONDecodeError as e:
53-
raise ValueError(f'{filename}: Invalid JSON in .note.dlopen note_desc') from e
5438

55-
if not isinstance(j, list):
56-
print(f'{filename}: ignoring .note.dlopen n_desc with JSON that is not a list',
57-
file=sys.stderr)
58-
continue
39+
for note in section.iter_notes():
40+
if note['n_type'] != 0x407c0c0a or note['n_name'] != 'FDO':
41+
continue
42+
note_desc = note['n_desc']
43+
44+
try:
45+
# On older Python versions (e.g.: Ubuntu 22.04) we get a string, on
46+
# newer versions a bytestring
47+
if not isinstance(note_desc, str):
48+
text = note_desc.decode('utf-8').rstrip('\0')
49+
else:
50+
text = note_desc.rstrip('\0')
51+
except UnicodeDecodeError as e:
52+
raise ValueError(f'{self.filename}: Invalid UTF-8 in .note.dlopen n_desc') from e
53+
54+
try:
55+
j = json.loads(text)
56+
except json.JSONDecodeError as e:
57+
raise ValueError(f'{self.filename}: Invalid JSON in .note.dlopen note_desc') from e
58+
59+
if not isinstance(j, list):
60+
print(f'{self.filename}: ignoring .note.dlopen n_desc with JSON that is not a list',
61+
file=sys.stderr)
62+
continue
5963

60-
yield from j
64+
yield from j
6165

6266
def dictify(f):
6367
def wrap(*args, **kwargs):
6468
return dict(f(*args, **kwargs))
6569
return functools.update_wrapper(wrap, f)
6670

6771
@dictify
68-
def group_by_soname(notes):
69-
for note in notes:
70-
for element in note:
72+
def group_by_soname(elffiles):
73+
for elffile in elffiles:
74+
for element in elffile.notes():
7175
priority = element.get('priority', 'recommended')
7276
for soname in element['soname']:
7377
yield soname, priority
@@ -80,7 +84,7 @@ class Priority(enum.Enum):
8084
def __lt__(self, other):
8185
return self.value < other.value
8286

83-
def group_by_feature(filenames, notes):
87+
def group_by_feature(elffiles):
8488
features = {}
8589

8690
# We expect each note to be in the format:
@@ -92,8 +96,8 @@ def group_by_feature(filenames, notes):
9296
# "soname": ["..."],
9397
# }
9498
# ]
95-
for filename, note_group in zip(filenames, notes):
96-
for note in note_group:
99+
for elffiles in elffiles:
100+
for note in elffiles.notes():
97101
prio = Priority[note.get('priority', 'recommened')]
98102
feature_name = note['feature']
99103

@@ -108,7 +112,7 @@ def group_by_feature(filenames, notes):
108112
else:
109113
# Merge
110114
if feature['description'] != note.get('description', ''):
111-
print(f"{filename}: feature {note['feature']!r} found with different description, ignoring",
115+
print(f"{note.filename}: feature {note['feature']!r} found with different description, ignoring",
112116
file=sys.stderr)
113117

114118
for soname in note['soname']:
@@ -118,39 +122,92 @@ def group_by_feature(filenames, notes):
118122

119123
return features
120124

125+
def filter_features(features, filter):
126+
if filter is None:
127+
return None
128+
ans = { name:feature for name,feature in features.items()
129+
if name in filter or not filter }
130+
if missing := set(filter) - set(ans):
131+
sys.exit('Some features not found:', ', '.join(missing))
132+
return ans
133+
134+
@listify
135+
def generate_rpm(elffiles, stanza, filter):
136+
# Produces output like:
137+
# Requires: libqrencode.so.4()(64bit)
138+
# Requires: libzstd.so.1()(64bit)
139+
for elffile in elffiles:
140+
suffix = '()(64bit)' if elffile.elffile.elfclass == 64 else ''
141+
for note in elffile.notes():
142+
if note['feature'] in filter or not filter:
143+
soname = next(iter(note['soname'])) # we take the first — most recommended — soname
144+
yield f"{stanza}: {soname}{suffix}"
145+
121146
def make_parser():
122147
p = argparse.ArgumentParser(
123148
description=__doc__,
124149
allow_abbrev=False,
125150
add_help=False,
126151
epilog='If no option is specifed, --raw is the default.',
127152
)
128-
p.add_argument('-r', '--raw',
129-
action='store_true',
130-
help='Show the original JSON extracted from input files')
131-
p.add_argument('-s', '--sonames',
132-
action='store_true',
133-
help='List all sonames and their priorities, one soname per line')
134-
p.add_argument('-f', '--features',
135-
nargs='?',
136-
const=[],
137-
type=lambda s: s.split(','),
138-
action='extend',
139-
metavar='FEATURE1,FEATURE2',
140-
help='Describe features, can be specified multiple times')
141-
p.add_argument('filenames',
142-
nargs='+',
143-
metavar='filename',
144-
help='Library file to extract notes from')
145-
p.add_argument('-h', '--help',
146-
action='help',
147-
help='Show this help message and exit')
153+
p.add_argument(
154+
'-r', '--raw',
155+
action='store_true',
156+
help='Show the original JSON extracted from input files',
157+
)
158+
p.add_argument(
159+
'-s', '--sonames',
160+
action='store_true',
161+
help='List all sonames and their priorities, one soname per line',
162+
)
163+
p.add_argument(
164+
'-f', '--features',
165+
nargs='?',
166+
const=[],
167+
type=lambda s: s.split(','),
168+
action='extend',
169+
metavar='FEATURE1,FEATURE2',
170+
help='Describe features, can be specified multiple times',
171+
)
172+
p.add_argument(
173+
'--rpm-requires',
174+
nargs='?',
175+
const=[],
176+
type=lambda s: s.split(','),
177+
action='extend',
178+
metavar='FEATURE1,FEATURE2',
179+
help='Generate rpm Requires for listed features',
180+
)
181+
p.add_argument(
182+
'--rpm-recommends',
183+
nargs='?',
184+
const=[],
185+
type=lambda s: s.split(','),
186+
action='extend',
187+
metavar='FEATURE1,FEATURE2',
188+
help='Generate rpm Recommends for listed features',
189+
)
190+
p.add_argument(
191+
'filenames',
192+
nargs='+',
193+
metavar='filename',
194+
help='Library file to extract notes from',
195+
)
196+
p.add_argument(
197+
'-h', '--help',
198+
action='help',
199+
help='Show this help message and exit',
200+
)
148201
return p
149202

150203
def parse_args():
151204
args = make_parser().parse_args()
152205

153-
if not args.raw and args.features is None and not args.sonames:
206+
if (not args.raw
207+
and not args.sonames
208+
and args.features is None
209+
and args.rpm_requires is None
210+
and args.rpm_recommends is None):
154211
# Make --raw the default if no action is specified.
155212
args.raw = True
156213

@@ -159,27 +216,29 @@ def parse_args():
159216
if __name__ == '__main__':
160217
args = parse_args()
161218

162-
notes = [read_dlopen_notes(filename) for filename in args.filenames]
219+
elffiles = [ELFFileReader(filename) for filename in args.filenames]
220+
features = group_by_feature(elffiles)
163221

164222
if args.raw:
165-
for filename, note in zip(args.filenames, notes):
166-
print(f'# {filename}')
167-
print_json(json.dumps(note, indent=2))
168-
169-
if args.features is not None:
170-
features = group_by_feature(args.filenames, notes)
171-
172-
toprint = {name:feature for name,feature in features.items()
173-
if name in args.features or not args.features}
174-
if len(toprint) < len(args.features):
175-
sys.exit('Some features were not found')
223+
for elffile in elffiles:
224+
print(f'# {elffile.filename}')
225+
print_json(json.dumps(elffile.notes(), indent=2))
176226

227+
if features_to_print := filter_features(features, args.features):
177228
print('# grouped by feature')
178-
print_json(json.dumps(toprint,
229+
print_json(json.dumps(features_to_print,
179230
indent=2,
180231
default=lambda prio: prio.name))
181232

233+
if args.rpm_requires is not None:
234+
lines = generate_rpm(elffiles, 'Requires', args.rpm_requires)
235+
print('\n'.join(lines))
236+
237+
if args.rpm_recommends is not None:
238+
lines = generate_rpm(elffiles, 'Recommends', args.rpm_recommends)
239+
print('\n'.join(lines))
240+
182241
if args.sonames:
183-
sonames = group_by_soname(notes)
242+
sonames = group_by_soname(elffiles)
184243
for soname in sorted(sonames.keys()):
185244
print(f"{soname} {sonames[soname]}")

test/test.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# SPDX-License-Identifier: CC0-1.0
22

3-
from _notes import read_dlopen_notes, group_by_soname
3+
from _notes import ELFFileReader, group_by_soname, generate_rpm
44

5-
def test_notes():
5+
def test_sonames():
66
expected = {
77
'libfido2.so.1': 'required',
88
'liblz4.so.1': 'recommended',
@@ -11,5 +11,25 @@ def test_notes():
1111
'libtss2-esys.so.0': 'recommended',
1212
'libtss2-mu.so.0': 'recommended',
1313
}
14-
notes = [read_dlopen_notes('notes')]
15-
assert group_by_soname(notes) == expected
14+
notes = ELFFileReader('notes')
15+
assert group_by_soname([notes]) == expected
16+
17+
def test_requires():
18+
notes = ELFFileReader('notes')
19+
20+
expected = {
21+
32: [
22+
'Suggests: libpcre2-8.so.0',
23+
'Suggests: libtss2-mu.so.0',
24+
'Suggests: libtss2-esys.so.0',
25+
],
26+
64: [
27+
'Suggests: libpcre2-8.so.0()(64bit)',
28+
'Suggests: libtss2-mu.so.0()(64bit)',
29+
'Suggests: libtss2-esys.so.0()(64bit)',
30+
],
31+
}
32+
33+
lines = generate_rpm([notes], 'Suggests', ('pcre2', 'tpm'))
34+
expect = expected[notes.elffile.elfclass]
35+
assert lines == expect

0 commit comments

Comments
 (0)