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
97 changes: 54 additions & 43 deletions debian/dh_dlopenlibdeps
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ on_pkgs_in_parallel {
my $tmp = tmpdir($package);
my $ext = pkgext($package);
my (@filelist);
my %required_packages;
my %recommended_packages;
my %suggested_packages;
my %required_deps;
my %recommended_deps;
my %suggested_deps;

# Generate a list of ELF binaries in the package, ignoring any we were told to exclude.
my $find_options='';
Expand All @@ -76,71 +76,82 @@ on_pkgs_in_parallel {
}

if (@filelist) {
my $required_sonames = '';
my $recommended_sonames = '';
my $suggested_sonames = '';
# Each line is a group of alternative sonames (preferred first)
# followed by the priority, to become a "pkg1 | pkg2" dependency.
my @groups;
my %all_sonames;

my $sonames = `dlopen-notes --sonames @filelist`;
foreach my $line (split(/\n/, $sonames)) {
my ($soname, $priority) = split(' ', $line, 2);

if ($priority eq 'required') {
$required_sonames .= " $soname";
} elsif ($priority eq 'recommended') {
$recommended_sonames .= " $soname";
} elsif ($priority eq 'suggested') {
$suggested_sonames .= " $soname";
} else {
warning("Unknown priority $priority for $soname");
my @fields = split(' ', $line);
next if @fields < 2;
my $priority = pop @fields;
if ($priority ne 'required' && $priority ne 'recommended' && $priority ne 'suggested') {
warning("Unknown priority $priority for @fields");
next;
}
push @groups, { priority => $priority, sonames => [@fields] };
$all_sonames{$_} = 1 foreach @fields;
}

if ($required_sonames) {
my $dpkg_query = `dpkg-query --search -- $required_sonames`;
# Map each soname to its package(s) with a single query, matching
# dpkg-query's substring search to preserve the grouping.
my %soname_packages;
if (%all_sonames) {
my @query = sort keys %all_sonames;
my $dpkg_query = `dpkg-query --search -- @query`;
foreach my $line (split(/\n/, $dpkg_query)) {
chomp $line;
if ($line =~ m/^local diversion |^diversion by/) {
next;
}
if ($line =~ m/^([-a-z0-9+.]+):/) {
$required_packages{$1} = 1;
# Format: "pkg[:arch][, pkg2...]: /path/to/file"
next unless $line =~ m/^(.*?): (\/.*)$/;
my ($pkgfield, $path) = ($1, $2);

my @pkgs;
foreach my $pkg (split(/,\s*/, $pkgfield)) {
$pkg =~ s/:.*//;
push @pkgs, $pkg if $pkg =~ m/^[-a-z0-9+.]+$/;
}
}
}
next unless @pkgs;

if ($recommended_sonames) {
my $dpkg_query = `dpkg-query --search -- $recommended_sonames`;
foreach my $line (split(/\n/, $dpkg_query)) {
chomp $line;
if ($line =~ m/^local diversion |^diversion by/) {
next;
}
if ($line =~ m/^([-a-z0-9+.]+):/) {
$recommended_packages{$1} = 1;
foreach my $soname (keys %all_sonames) {
if (index($path, $soname) >= 0) {
push @{$soname_packages{$soname}}, @pkgs;
}
}
}
}

if ($suggested_sonames) {
my $dpkg_query = `dpkg-query --search -- $suggested_sonames`;
foreach my $line (split(/\n/, $dpkg_query)) {
chomp $line;
if ($line =~ m/^local diversion |^diversion by/) {
next;
}
if ($line =~ m/^([-a-z0-9+.]+):/) {
$suggested_packages{$1} = 1;
# Build a "pkg1 | pkg2" alternative dependency for each group.
foreach my $group (@groups) {
my @alternatives;
my %seen;
foreach my $soname (@{$group->{sonames}}) {
foreach my $pkg (@{$soname_packages{$soname} // []}) {
push @alternatives, $pkg unless $seen{$pkg}++;
}
}
next unless @alternatives;

my $dep = join(" | ", @alternatives);
if ($group->{priority} eq 'required') {
$required_deps{$dep} = 1;
} elsif ($group->{priority} eq 'recommended') {
$recommended_deps{$dep} = 1;
} else {
$suggested_deps{$dep} = 1;
}
}
}

# Always write the substvars file, even if it's empty, so that the variables are defined and
# there are no warnings when using them in the control file.
open(SV, ">>debian/${ext}substvars") || error("open debian/${ext}substvars: $!");
print SV "dlopen:Depends=" . join(", ", sort keys %required_packages) . "\n";
print SV "dlopen:Recommends=" . join(", ", sort keys %recommended_packages) . "\n";
print SV "dlopen:Suggests=" . join(", ", sort keys %suggested_packages) . "\n";
print SV "dlopen:Depends=" . join(", ", sort keys %required_deps) . "\n";
print SV "dlopen:Recommends=" . join(", ", sort keys %recommended_deps) . "\n";
print SV "dlopen:Suggests=" . join(", ", sort keys %suggested_deps) . "\n";
close(SV);
}
};
Expand Down
35 changes: 21 additions & 14 deletions dlopen-notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,17 @@ def notes(self):

yield from j

def group_by_soname(elffiles):
sonames = {}
def group_alternatives(elffiles):
# A note's sonames are alternatives for one dependency (preferred first).
# Group them by tuple, merging identical groups at the highest priority.
groups = {}
for elffile in elffiles:
for element in elffile.notes():
priority = Priority[element.get('priority', 'recommended')]
for soname in element['soname']:
sonames[soname] = max(sonames.get(soname, priority), priority)
sonames = tuple(element['soname'])
groups[sonames] = max(groups.get(sonames, priority), priority)

return sonames
return groups

class Priority(enum.Enum):
suggested = 1
Expand Down Expand Up @@ -147,17 +149,23 @@ def filter_features(features, filter):
sys.exit('Some features not found:', ', '.join(missing))
return ans

def rpm_dependency(sonames, suffix):
# Alternative sonames become an rpm boolean "or" dependency.
deps = [f'{soname}{suffix}' for soname in sonames]
if len(deps) == 1:
return deps[0]
return '(' + ' or '.join(deps) + ')'

@listify
def generate_rpm(elffiles, stanza, filter):
# Produces output like:
# Requires: libqrencode.so.4()(64bit)
# Requires: libzstd.so.1()(64bit)
# Requires: (libcrypto.so.4()(64bit) or libcrypto.so.3()(64bit))
for elffile in elffiles:
suffix = '()(64bit)' if elffile.elffile.elfclass == 64 else ''
for note in elffile.notes():
if note['feature'] in filter or not filter:
soname = next(iter(note['soname'])) # we take the first — most recommended — soname
yield f"{stanza}: {soname}{suffix}"
yield f"{stanza}: {rpm_dependency(note['soname'], suffix)}"


def rpm_fileattr_generator(args):
Expand Down Expand Up @@ -198,8 +206,7 @@ def rpm_fileattr_generator(args):
print(f';{file}')
first = False

soname = next(iter(note['soname'])) # we take the first — most recommended — soname
print(f'{soname}{suffix}')
print(rpm_dependency(note['soname'], suffix))


@listify
Expand Down Expand Up @@ -238,7 +245,7 @@ def make_parser():
p.add_argument(
'-s', '--sonames',
action='store_true',
help='List all sonames and their priorities, one soname per line',
help='List sonames and their priorities, one group of alternatives per line',
)
p.add_argument(
'-f', '--features',
Expand Down Expand Up @@ -353,6 +360,6 @@ def parse_args():
print('\n'.join(lines))

if args.sonames:
sonames = group_by_soname(elffiles)
for soname in sorted(sonames.keys()):
print(f"{soname} {sonames[soname]}")
groups = group_alternatives(elffiles)
for sonames in sorted(groups.keys()):
print(f"{' '.join(sonames)} {groups[sonames]}")
51 changes: 37 additions & 14 deletions test/test.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
# SPDX-License-Identifier: CC0-1.0

from _notes import ELFFileReader, group_by_soname, generate_rpm, Priority
from _notes import ELFFileReader, group_alternatives, generate_rpm, rpm_dependency, Priority

def test_sonames():
expected = {
'libfido2.so.1': Priority.required,
'liblz4.so.1': Priority.recommended,
'libpcre2-8.so.0': Priority.suggested,
'libpcre2-8.so.1': Priority.suggested,
'libtss2-esys.so.0': Priority.recommended,
'libtss2-mu.so.0': Priority.recommended,
('libfido2.so.1',): Priority.required,
('liblz4.so.1',): Priority.recommended,
('libpcre2-8.so.0', 'libpcre2-8.so.1'): Priority.suggested,
('libtss2-esys.so.0',): Priority.recommended,
('libtss2-mu.so.0',): Priority.recommended,
}
notes = ELFFileReader('notes')
assert group_by_soname([notes]) == expected
assert group_alternatives([notes]) == expected

def test_requires():
notes = ELFFileReader('notes')

expected = {
32: [
'Suggests: libpcre2-8.so.0',
'Suggests: (libpcre2-8.so.0 or libpcre2-8.so.1)',
'Suggests: libtss2-mu.so.0',
'Suggests: libtss2-esys.so.0',
],
64: [
'Suggests: libpcre2-8.so.0()(64bit)',
'Suggests: (libpcre2-8.so.0()(64bit) or libpcre2-8.so.1()(64bit))',
'Suggests: libtss2-mu.so.0()(64bit)',
'Suggests: libtss2-esys.so.0()(64bit)',
],
Expand All @@ -34,6 +33,14 @@ def test_requires():
expect = expected[notes.elffile.elfclass]
assert sorted(lines) == sorted(expect)

def test_rpm_dependency():
# One soname stays plain; multiple become an rpm boolean "or" dependency.
assert rpm_dependency(['libfoo.so.1'], '') == 'libfoo.so.1'
assert rpm_dependency(['libfoo.so.1'], '()(64bit)') == 'libfoo.so.1()(64bit)'
assert rpm_dependency(['libfoo.so.2', 'libfoo.so.1'], '') == '(libfoo.so.2 or libfoo.so.1)'
assert rpm_dependency(['libfoo.so.2', 'libfoo.so.1'], '()(64bit)') == \
'(libfoo.so.2()(64bit) or libfoo.so.1()(64bit))'

class FakeReader:
def __init__(self, notes):
self._notes = notes
Expand All @@ -46,7 +53,23 @@ def test_sonames_highest_priority_stable():
b = FakeReader([{'soname': ['libcrypto.so.3'], 'priority': 'recommended'}])
c = FakeReader([{'soname': ['libcrypto.so.3'], 'priority': 'suggested'}])

expected = {'libcrypto.so.3': Priority.required}
assert group_by_soname([a, b, c]) == expected
assert group_by_soname([c, b, a]) == expected
assert group_by_soname([b, a, c]) == expected
expected = {('libcrypto.so.3',): Priority.required}
assert group_alternatives([a, b, c]) == expected
assert group_alternatives([c, b, a]) == expected
assert group_alternatives([b, a, c]) == expected

def test_alternatives_grouped():
# Sonames in one note are alternatives and stay grouped, preferred first.
a = FakeReader([{'soname': ['libcrypto.so.4', 'libcrypto.so.3'], 'priority': 'required'}])
expected = {('libcrypto.so.4', 'libcrypto.so.3'): Priority.required}
assert group_alternatives([a]) == expected

def test_alternatives_distinct_from_separate_notes():
# Separate notes are distinct dependencies, not alternatives.
grouped = FakeReader([{'soname': ['liba.so.1', 'libb.so.1'], 'priority': 'required'}])
separate = FakeReader([{'soname': ['liba.so.1'], 'priority': 'required'},
{'soname': ['libb.so.1'], 'priority': 'required'}])

assert group_alternatives([grouped]) == {('liba.so.1', 'libb.so.1'): Priority.required}
assert group_alternatives([separate]) == {('liba.so.1',): Priority.required,
('libb.so.1',): Priority.required}
Loading