@@ -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
6266def 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+
121146def 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
150203def 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():
159216if __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 ]} " )
0 commit comments