Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
106 changes: 98 additions & 8 deletions dlopen-notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import argparse
import enum
import fnmatch
import functools
import json
import sys
Expand All @@ -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))
Expand Down Expand Up @@ -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:
Expand All @@ -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 = {}

Expand Down Expand Up @@ -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__,
Expand Down Expand Up @@ -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(
Expand All @@ -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)

Expand Down
36 changes: 36 additions & 0 deletions fakelib/fakelib.spec
Original file line number Diff line number Diff line change
@@ -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
44 changes: 44 additions & 0 deletions rpm/dlopen_notes.attr
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +16 to +19

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that the opts macros are not needed by default? It's a little unclear to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Rewording are welcome, but the last sentence way trying to convey that.

If the package has a file with dlopen notes and does nothing more, then it ends up with deps generated from the notes. The "priority" field, or the "recommends" fallback is used to pick Requires/Recommends/Suggests.

#
# 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.*$