Skip to content

Commit d6e8338

Browse files
committed
rpm: add fileattr multifile generator
This allows dlopen notes to be turned into appropriate dependencies automatically. The dlopen_notes.attr file needs to be installed into %{_fileattrsdir}. By default, dependencies are generated for all files that have package notes. I think this is a reasonable default because it makes the whole feature easier to discover. In more realistic cases, esp. with multiple subpackages, it's likely that the packager may need to configure the distribution of dependencies between subpackages. One shortcoming of the scheme is that everything is per file, so it's not possible to say that dependencies generated from a feature should be assigned to a different subpackage. This is how the feature is designed in rpm. The opt-out mechanism is a bit clunky. The first option I considered was to tell the user to undefine %__dlopen_notes_requires/recommends/suggests, but that requires three lines of boilerplate. And might not be forwards-compatible if we add new features in the future. The second option would be to tell the user to define __dlopen_notes_requires/recommends/suggests_opts to %nil. But that has similar problems. I think it's nice to have an obvious oneliner to handle this. Unfortunately, when I tried to use %__dlopen_notes_requires %{?_dlopen_notes_generator:%{_dlopen_notes_generator} ...} %__dlopen_notes_recommends %{?_dlopen_notes_generator:%{_dlopen_notes_generator} ...} %__dlopen_notes_suggests %{?_dlopen_notes_generator:%{_dlopen_notes_generator} ...} in the .attr file, when the package has %undefine _dlopen_notes_generator, we still end up with the macro being expanded. Maybe I misunderstood the macro expansion logic. The approach with 'true' is clunky, but it works fine. Thanks to Neal Gompa for the suggestion to use this protocol. The new interface is new, independent of the existing options --feature, --rpm-recommends, --rpm-requires that were previously added to support rpms. Unfortunately, with the fileattr protocol, the old way to specify information is not useful. Instead of trying to shoehorn the new metadata into existing options, I think it's easier to add a new set with clear semantics.
1 parent 2ad3aca commit d6e8338

File tree

3 files changed

+150
-3
lines changed

3 files changed

+150
-3
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ $ dlopen-notes /usr/lib64/systemd/libsystemd-shared-257.so
9797
...
9898
```
9999

100+
### Using the rpm fileattr generator
101+
102+
The tool that processes package notes can be hooked into the rpm build process
103+
to automatically generate virtual `Requires`, `Recommends`, and `Suggests` dependencies.
104+
105+
The rpm file attribute mechanism is described in
106+
[rpm-dependency-generators.7](https://rpm-software-management.github.io/rpm/man/rpm-dependency-generators.7).
107+
108+
This tool implements the 'multifile' protocol:
109+
it reads the list of files on stdin and outputs a list of virtual dependencies.
110+
111+
See the `rpm/dlopen_notes.attr` file for invocation details and options.
112+
100113
## Requirements
101114
* binutils (>= 2.39)
102115
* mold (>= 1.3.0)

dlopen-notes.py

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import argparse
99
import enum
10+
import fnmatch
1011
import functools
1112
import json
1213
import sys
@@ -84,6 +85,16 @@ class Priority(enum.Enum):
8485
def __lt__(self, other):
8586
return self.value < other.value
8687

88+
def rpm_name(self):
89+
if self == self.__class__.suggested:
90+
return 'Suggests'
91+
if self == self.__class__.recommended:
92+
return 'Recommends'
93+
if self == self.__class__.required:
94+
return 'Requires'
95+
raise ValueError
96+
97+
8798
def group_by_feature(elffiles):
8899
features = {}
89100

@@ -143,6 +154,52 @@ def generate_rpm(elffiles, stanza, filter):
143154
soname = next(iter(note['soname'])) # we take the first — most recommended — soname
144155
yield f"{stanza}: {soname}{suffix}"
145156

157+
def rpm_fileattr_generator(args):
158+
if args.rpm_features is not None:
159+
if not any(fnmatch.fnmatch(args.subpackage, pattern[0])
160+
for pattern in args.rpm_features):
161+
# Current subpackage is not listed, nothing to do.
162+
# Consume all input as required by the protocol.
163+
sys.stdin.read()
164+
return
165+
166+
for file in sys.stdin:
167+
file = file.strip()
168+
if not file:
169+
continue # ignore empty lines
170+
171+
elffile = ELFFileReader(file)
172+
suffix = '()(64bit)' if elffile.elffile.elfclass == 64 else ''
173+
174+
first = True
175+
176+
for note in elffile.notes():
177+
# Feature name is optional. Allow this to be matched
178+
# by the empty string ('') or a wildcard glob ('*').
179+
feature = note.get('feature', '')
180+
181+
if args.rpm_features is not None:
182+
for package_pattern,feature_pattern in args.rpm_features:
183+
if (fnmatch.fnmatch(args.subpackage, package_pattern) and
184+
fnmatch.fnmatch(feature, feature_pattern)):
185+
break
186+
else:
187+
# not matched
188+
continue
189+
else:
190+
# if no mapping, print all features at the suggested level
191+
level = Priority[note.get('priority', 'recommended')].rpm_name()
192+
if level != args.rpm_fileattr:
193+
continue
194+
195+
if first:
196+
print(f';{file}')
197+
first = False
198+
199+
soname = next(iter(note['soname'])) # we take the first — most recommended — soname
200+
print(f'{soname}{suffix}')
201+
202+
146203
def make_parser():
147204
p = argparse.ArgumentParser(
148205
description=__doc__,
@@ -187,10 +244,28 @@ def make_parser():
187244
metavar='FEATURE1,FEATURE2',
188245
help='Generate rpm Recommends for listed features',
189246
)
247+
p.add_argument(
248+
'--rpm-fileattr',
249+
metavar='TYPE',
250+
help='Run as rpm fileattr generator for TYPE dependencies',
251+
)
252+
p.add_argument(
253+
'--subpackage',
254+
metavar='NAME',
255+
default='',
256+
help='Current subpackage NAME',
257+
)
258+
p.add_argument(
259+
'--rpm-features',
260+
metavar='SUBPACKAGE:FEATURE,SUBPACKAGE:FEATURE',
261+
type=lambda s: [x.split(':', maxsplit=1) for x in s.split(',')],
262+
action='extend',
263+
help='Specify subpackage:feature mapping',
264+
)
190265
p.add_argument(
191266
'filenames',
192-
nargs='+',
193-
metavar='filename',
267+
nargs='*',
268+
metavar='FILENAME',
194269
help='Library file to extract notes from',
195270
)
196271
p.add_argument(
@@ -207,15 +282,30 @@ def parse_args():
207282
and not args.sonames
208283
and args.features is None
209284
and args.rpm_requires is None
210-
and args.rpm_recommends is None):
285+
and args.rpm_recommends is None
286+
and args.rpm_fileattr is None):
211287
# Make --raw the default if no action is specified.
212288
args.raw = True
213289

290+
if args.rpm_fileattr is not None:
291+
if (args.filenames
292+
or args.raw
293+
or args.features is not None
294+
or args.rpm_requires
295+
or args.rpm_recommends):
296+
raise ValueError('--rpm-generate cannot be combined with most options')
297+
298+
if args.rpm_fileattr is None and not args.filenames:
299+
raise ValueError('At least one positional FILENAME parameter is required')
300+
214301
return args
215302

216303
if __name__ == '__main__':
217304
args = parse_args()
218305

306+
if args.rpm_fileattr is not None:
307+
sys.exit(rpm_fileattr_generator(args))
308+
219309
elffiles = [ELFFileReader(filename) for filename in args.filenames]
220310
features = group_by_feature(elffiles)
221311

rpm/dlopen_notes.attr

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# SPDX-License-Identifier: MIT-0
2+
#
3+
# This file is part of the package-notes package.
4+
#
5+
#
6+
# The spec file for a package can specify which features are listed
7+
# and at which level, using
8+
# '--rpm-features=SUBPACKAGE1:FEATURE1,SUBPACKAGE2:FEATURE2' option in
9+
# the '__dlopen_notes_TYPE_opts' macro, where TYPE is one of
10+
# 'requires', 'recommends', or 'suggests'. The macro should be declared
11+
# in the spec file using:
12+
# %define __dlopen_notes_TYPE_opts SUBPACKAGE:FEATURE…
13+
# e.g.
14+
# %define __dlopen_notes_recommends_opts *:zstd
15+
#
16+
# The option accepts multiple comma-separated pairs, and can also be
17+
# specified multiple times. Both the subpackage name and feature name
18+
# can be a glob. If configuration is omitted, the priority recommended
19+
# in the notes is used.
20+
#
21+
# The '--subpackage=SUBPACKAGE' option (inserted below) tells the generator
22+
# which subpackage is being processed.
23+
#
24+
# For example, for a package using compression libraries, we can say
25+
# that the 'package-libs' subpackage shall carry 'Requires' on all the
26+
# libraries needed for the 'zstd' feature, all subpackages shall carry
27+
# 'Recommends' on all the libraries needed for the 'gzip' feature, and
28+
# the 'package' subpackage shall carry 'Suggests' for any feature
29+
# matching 'lzma' or 'bzip*'.
30+
#
31+
# %define __dlopen_notes_requires_opts --rpm-features=package-libs:zstd
32+
# %define __dlopen_notes_recommends_opts --rpm-features=*:gzip
33+
# %define __dlopen_notes_suggests_opts --rpm-features=package:lzma,package:bzip*
34+
#
35+
# To opt out, undefine the %_dlopen_notes_generator macro:
36+
# %undefine _dlopen_notes_generator
37+
38+
%_dlopen_notes_generator %{_bindir}/dlopen-notes
39+
40+
%__dlopen_notes_requires %{!?_dlopen_notes_generator:true }%{_dlopen_notes_generator} --subpackage='%{name}' --rpm-fileattr=Requires
41+
%__dlopen_notes_recommends %{!?_dlopen_notes_generator:true }%{_dlopen_notes_generator} --subpackage='%{name}' --rpm-fileattr=Recommends
42+
%__dlopen_notes_suggests %{!?_dlopen_notes_generator:true }%{_dlopen_notes_generator} --subpackage='%{name}' --rpm-fileattr=Suggests
43+
%__dlopen_notes_protocol multifile
44+
%__dlopen_notes_magic ^.*ELF (32|64)-bit.*$

0 commit comments

Comments
 (0)