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 e55800c..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 @@ -19,6 +20,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 +69,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: @@ -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/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 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.*$