diff --git a/debian/dh_dlopenlibdeps b/debian/dh_dlopenlibdeps index 1a6bf29..5487f50 100755 --- a/debian/dh_dlopenlibdeps +++ b/debian/dh_dlopenlibdeps @@ -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=''; @@ -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); } }; diff --git a/dlopen-notes.py b/dlopen-notes.py index b7e24be..27d55f6 100755 --- a/dlopen-notes.py +++ b/dlopen-notes.py @@ -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 @@ -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): @@ -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 @@ -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', @@ -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]}") diff --git a/test/test.py b/test/test.py index fcb1c7b..442d16e 100644 --- a/test/test.py +++ b/test/test.py @@ -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)', ], @@ -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 @@ -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}