From 2ad3aca11e60a9f50574a65df2232dfb58597f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Mon, 12 Jan 2026 13:57:06 +0100 Subject: [PATCH 1/3] dlopen-notes: group two helper functions together --- dlopen-notes.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dlopen-notes.py b/dlopen-notes.py index e55800c..f103611 100755 --- a/dlopen-notes.py +++ b/dlopen-notes.py @@ -19,6 +19,11 @@ except ImportError: print_json = print +def dictify(f): + def wrap(*args, **kwargs): + return dict(f(*args, **kwargs)) + return functools.update_wrapper(wrap, f) + def listify(f): def wrap(*args, **kwargs): return list(f(*args, **kwargs)) @@ -63,11 +68,6 @@ def notes(self): yield from j -def dictify(f): - def wrap(*args, **kwargs): - return dict(f(*args, **kwargs)) - return functools.update_wrapper(wrap, f) - @dictify def group_by_soname(elffiles): for elffile in elffiles: From d6e833810909a4278f59c19dbc748424ca5e153f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 13 Jan 2026 14:14:22 +0100 Subject: [PATCH 2/3] 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. --- README.md | 13 ++++++ dlopen-notes.py | 96 +++++++++++++++++++++++++++++++++++++++++-- rpm/dlopen_notes.attr | 44 ++++++++++++++++++++ 3 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 rpm/dlopen_notes.attr diff --git a/README.md b/README.md index 8b27e19..f7fa518 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,19 @@ $ dlopen-notes /usr/lib64/systemd/libsystemd-shared-257.so ... ``` +### Using the rpm fileattr generator + +The tool that processes package notes can be hooked into the rpm build process +to automatically generate virtual `Requires`, `Recommends`, and `Suggests` dependencies. + +The rpm file attribute mechanism is described in +[rpm-dependency-generators.7](https://rpm-software-management.github.io/rpm/man/rpm-dependency-generators.7). + +This tool implements the 'multifile' protocol: +it reads the list of files on stdin and outputs a list of virtual dependencies. + +See the `rpm/dlopen_notes.attr` file for invocation details and options. + ## Requirements * binutils (>= 2.39) * mold (>= 1.3.0) diff --git a/dlopen-notes.py b/dlopen-notes.py index f103611..4fa30d8 100755 --- a/dlopen-notes.py +++ b/dlopen-notes.py @@ -7,6 +7,7 @@ import argparse import enum +import fnmatch import functools import json import sys @@ -84,6 +85,16 @@ class Priority(enum.Enum): def __lt__(self, other): return self.value < other.value + def rpm_name(self): + if self == self.__class__.suggested: + return 'Suggests' + if self == self.__class__.recommended: + return 'Recommends' + if self == self.__class__.required: + return 'Requires' + raise ValueError + + def group_by_feature(elffiles): features = {} @@ -143,6 +154,52 @@ def generate_rpm(elffiles, stanza, filter): soname = next(iter(note['soname'])) # we take the first — most recommended — soname yield f"{stanza}: {soname}{suffix}" +def rpm_fileattr_generator(args): + if args.rpm_features is not None: + if not any(fnmatch.fnmatch(args.subpackage, pattern[0]) + for pattern in args.rpm_features): + # Current subpackage is not listed, nothing to do. + # Consume all input as required by the protocol. + sys.stdin.read() + return + + for file in sys.stdin: + file = file.strip() + if not file: + continue # ignore empty lines + + elffile = ELFFileReader(file) + suffix = '()(64bit)' if elffile.elffile.elfclass == 64 else '' + + first = True + + for note in elffile.notes(): + # Feature name is optional. Allow this to be matched + # by the empty string ('') or a wildcard glob ('*'). + feature = note.get('feature', '') + + if args.rpm_features is not None: + for package_pattern,feature_pattern in args.rpm_features: + if (fnmatch.fnmatch(args.subpackage, package_pattern) and + fnmatch.fnmatch(feature, feature_pattern)): + break + else: + # not matched + continue + else: + # if no mapping, print all features at the suggested level + level = Priority[note.get('priority', 'recommended')].rpm_name() + if level != args.rpm_fileattr: + continue + + if first: + print(f';{file}') + first = False + + soname = next(iter(note['soname'])) # we take the first — most recommended — soname + print(f'{soname}{suffix}') + + def make_parser(): p = argparse.ArgumentParser( description=__doc__, @@ -187,10 +244,28 @@ def make_parser(): metavar='FEATURE1,FEATURE2', help='Generate rpm Recommends for listed features', ) + p.add_argument( + '--rpm-fileattr', + metavar='TYPE', + help='Run as rpm fileattr generator for TYPE dependencies', + ) + p.add_argument( + '--subpackage', + metavar='NAME', + default='', + help='Current subpackage NAME', + ) + p.add_argument( + '--rpm-features', + metavar='SUBPACKAGE:FEATURE,SUBPACKAGE:FEATURE', + type=lambda s: [x.split(':', maxsplit=1) for x in s.split(',')], + action='extend', + help='Specify subpackage:feature mapping', + ) p.add_argument( 'filenames', - nargs='+', - metavar='filename', + nargs='*', + metavar='FILENAME', help='Library file to extract notes from', ) p.add_argument( @@ -207,15 +282,30 @@ def parse_args(): and not args.sonames and args.features is None and args.rpm_requires is None - and args.rpm_recommends is None): + and args.rpm_recommends is None + and args.rpm_fileattr is None): # Make --raw the default if no action is specified. args.raw = True + if args.rpm_fileattr is not None: + if (args.filenames + or args.raw + or args.features is not None + or args.rpm_requires + or args.rpm_recommends): + raise ValueError('--rpm-generate cannot be combined with most options') + + if args.rpm_fileattr is None and not args.filenames: + raise ValueError('At least one positional FILENAME parameter is required') + return args if __name__ == '__main__': args = parse_args() + if args.rpm_fileattr is not None: + sys.exit(rpm_fileattr_generator(args)) + elffiles = [ELFFileReader(filename) for filename in args.filenames] features = group_by_feature(elffiles) diff --git a/rpm/dlopen_notes.attr b/rpm/dlopen_notes.attr new file mode 100644 index 0000000..abe3e85 --- /dev/null +++ b/rpm/dlopen_notes.attr @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: MIT-0 +# +# This file is part of the package-notes package. +# +# +# The spec file for a package can specify which features are listed +# and at which level, using +# '--rpm-features=SUBPACKAGE1:FEATURE1,SUBPACKAGE2:FEATURE2' option in +# the '__dlopen_notes_TYPE_opts' macro, where TYPE is one of +# 'requires', 'recommends', or 'suggests'. The macro should be declared +# in the spec file using: +# %define __dlopen_notes_TYPE_opts SUBPACKAGE:FEATURE… +# e.g. +# %define __dlopen_notes_recommends_opts *:zstd +# +# The option accepts multiple comma-separated pairs, and can also be +# specified multiple times. Both the subpackage name and feature name +# can be a glob. If configuration is omitted, the priority recommended +# in the notes is used. +# +# The '--subpackage=SUBPACKAGE' option (inserted below) tells the generator +# which subpackage is being processed. +# +# For example, for a package using compression libraries, we can say +# that the 'package-libs' subpackage shall carry 'Requires' on all the +# libraries needed for the 'zstd' feature, all subpackages shall carry +# 'Recommends' on all the libraries needed for the 'gzip' feature, and +# the 'package' subpackage shall carry 'Suggests' for any feature +# matching 'lzma' or 'bzip*'. +# +# %define __dlopen_notes_requires_opts --rpm-features=package-libs:zstd +# %define __dlopen_notes_recommends_opts --rpm-features=*:gzip +# %define __dlopen_notes_suggests_opts --rpm-features=package:lzma,package:bzip* +# +# To opt out, undefine the %_dlopen_notes_generator macro: +# %undefine _dlopen_notes_generator + +%_dlopen_notes_generator %{_bindir}/dlopen-notes + +%__dlopen_notes_requires %{!?_dlopen_notes_generator:true }%{_dlopen_notes_generator} --subpackage='%{name}' --rpm-fileattr=Requires +%__dlopen_notes_recommends %{!?_dlopen_notes_generator:true }%{_dlopen_notes_generator} --subpackage='%{name}' --rpm-fileattr=Recommends +%__dlopen_notes_suggests %{!?_dlopen_notes_generator:true }%{_dlopen_notes_generator} --subpackage='%{name}' --rpm-fileattr=Suggests +%__dlopen_notes_protocol multifile +%__dlopen_notes_magic ^.*ELF (32|64)-bit.*$ From 9f6142b43b730d2ff51cf2b355050548f5f8fc9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 13 Jan 2026 09:53:07 +0100 Subject: [PATCH 3/3] fakelib: add test for the new fileattr plugin This is a package that "builds" by copying two files: a systemd library that is known to use dlopen notes and another glibc library that doesn't have them. It can be built with 'rpmbuild' or 'fedpkg local'. For example: (cd fakelib && fedpkg local && echo 'Requires:' && rpm -qpv --requires x86_64/fakelib-0-1.fc44.x86_64.rpm && echo 'Recommends:' && rpm -qpv --recommends x86_64/fakelib-0-1.fc44.x86_64.rpm && echo 'Suggests:' && rpm -qpv --suggests x86_64/fakelib-0-1.fc44.x86_64.rpm) --- fakelib/fakelib.spec | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 fakelib/fakelib.spec diff --git a/fakelib/fakelib.spec b/fakelib/fakelib.spec new file mode 100644 index 0000000..40714cb --- /dev/null +++ b/fakelib/fakelib.spec @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: MIT-0 +# +# This file is part of the package-notes package. + +Name: fakelib +Version: 0 +Release: %autorelease +Summary: %{name} + +License: None + +%define __dlopen_notes_requires_opts --rpm-features=fakelib:gcrypt,fakelib:lz4 +%define __dlopen_notes_recommends_opts --rpm-features=*:zstd +%define __dlopen_notes_suggests_opts --rpm-features=fakelib:lzm[abc] + +#undefine _dlopen_notes_generator + +%description +%{summary}. + +%prep + +%build + +%install +install -Dt %{buildroot}/usr/lib64/ /usr/lib64/libsystemd.so.0 +ln -s libsystemd.so.0 %{buildroot}/usr/lib64/libsystemd.so +install -Dt %{buildroot}/usr/lib64/ /usr/lib64/libmvec.so.1 + +%files +/usr/lib64/libsystemd.so.0 +/usr/lib64/libsystemd.so +/usr/lib64/libmvec.so.1 + +%changelog +%autochangelog