diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:42:30 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:42:30 +0000 |
commit | 75808db17caf8b960b351e3408e74142f4c85aac (patch) | |
tree | 7989e9c09a4240248bf4658a22208a0a52d991c4 /lib/Lintian/Check/Debhelper.pm | |
parent | Initial commit. (diff) | |
download | lintian-upstream.tar.xz lintian-upstream.zip |
Adding upstream version 2.117.0.upstream/2.117.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lib/Lintian/Check/Debhelper.pm')
-rw-r--r-- | lib/Lintian/Check/Debhelper.pm | 1088 |
1 files changed, 1088 insertions, 0 deletions
diff --git a/lib/Lintian/Check/Debhelper.pm b/lib/Lintian/Check/Debhelper.pm new file mode 100644 index 0000000..b2cee04 --- /dev/null +++ b/lib/Lintian/Check/Debhelper.pm @@ -0,0 +1,1088 @@ +# debhelper format -- lintian check script -*- perl -*- + +# Copyright (C) 1999 by Joey Hess +# Copyright (C) 2016-2020 Chris Lamb <lamby@debian.org> +# Copyright (C) 2021 Felix Lechner +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, you can find it on the World Wide +# Web at https://www.gnu.org/copyleft/gpl.html, or write to the Free +# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, +# MA 02110-1301, USA. + +package Lintian::Check::Debhelper; + +use v5.20; +use warnings; +use utf8; + +use Const::Fast; +use List::Compare; +use List::SomeUtils qw(any firstval); +use List::UtilsBy qw(min_by); +use Text::LevenshteinXS qw(distance); +use Unicode::UTF8 qw(encode_utf8); + +use Lintian::Relation; + +const my $EMPTY => q{}; +const my $SPACE => q{ }; +const my $DOLLAR => q{$}; +const my $UNDERSCORE => q{_}; +const my $HORIZONTAL_BAR => q{|}; + +const my $ARROW => q{=>}; + +# If there is no debian/compat file present but cdbs is being used, cdbs will +# create one automatically. Currently it always uses compatibility level 5. +# It may be better to look at what version of cdbs the package depends on and +# from that derive the compatibility level.... +const my $CDBS_COMPAT => 5; + +# minimum versions for features +const my $BRACE_EXPANSION => 5; +const my $USES_EXECUTABLE_FILES => 9; +const my $DH_PARALLEL_NOT_NEEDED => 10; +const my $REQUIRES_AUTOTOOLS => 10; +const my $USES_AUTORECONF => 10; +const my $INVOKES_SYSTEMD => 10; +const my $BETTER_SYSTEMD_INTEGRATION => 11; +const my $VERSIONED_PREREQUISITE_AVAILABLE => 11; + +const my $LEVENSHTEIN_TOLERANCE => 3; +const my $MANY_OVERRIDES => 20; + +use Moo; +use namespace::clean; + +with 'Lintian::Check'; + +my $MISC_DEPENDS = Lintian::Relation->new->load($DOLLAR . '{misc:Depends}'); + +# Manually maintained list of dh_commands that requires a versioned +# dependency *AND* are not provided by debhelper. Commands provided +# by debhelper is handled in checks/debhelper. +# +# This overrules any thing listed in dh_commands (which is auto-generated). + +my %DH_COMMAND_MANUAL_PREREQUISITES = ( + dh_apache2 => 'dh-apache2:any | apache2-dev:any', + dh_autoreconf_clean => +'dh-autoreconf:any | debhelper:any (>= 9.20160403~) | debhelper-compat:any', + dh_autoreconf => +'dh-autoreconf:any | debhelper:any (>= 9.20160403~) | debhelper-compat:any', + dh_dkms => 'dh-dkms:any | dh-sequence-dkms:any', + dh_girepository => 'gobject-introspection:any | dh-sequence-gir:any', + dh_gnome => 'gnome-pkg-tools:any | dh-sequence-gnome:any', + dh_gnome_clean => 'gnome-pkg-tools:any | dh-sequence-gnome:any', + dh_lv2config => 'lv2core:any', + dh_make_pgxs => 'postgresql-server-dev-all:any | postgresql-all:any', + dh_nativejava => 'gcj-native-helper:any | default-jdk-builddep:any', + dh_pgxs_test => 'postgresql-server-dev-all:any | postgresql-all:any', + dh_python2 => 'dh-python:any | dh-sequence-python2:any', + dh_python3 => + 'dh-python:any | dh-sequence-python3:any | pybuild-plugin-pyproject:any', + dh_sphinxdoc => +'sphinx:any | python-sphinx:any | python3-sphinx:any | dh-sequence-sphinxdoc:any', + dh_xine => 'libxine-dev:any | libxine2-dev:any' +); + +# Manually maintained list of dependencies needed for dh addons. This overrides +# information from data/common/dh_addons (the latter file is automatically +# generated). +my %DH_ADDON_MANUAL_PREREQUISITES = ( + ada_library => 'dh-ada-library:any | dh-sequence-ada-library:any', + apache2 => 'dh-apache2:any | apache2-dev:any', + autoreconf => +'dh-autoreconf:any | debhelper:any (>= 9.20160403~) | debhelper-compat:any', + cli => 'cli-common-dev:any | dh-sequence-cli:any', + dwz => 'debhelper:any | debhelper-compat:any | dh-sequence-dwz:any', + installinitramfs => +'debhelper:any | debhelper-compat:any | dh-sequence-installinitramfs:any', + gnome => 'gnome-pkg-tools:any | dh-sequence-gnome:any', + lv2config => 'lv2core:any', + nodejs => 'pkg-js-tools:any | dh-sequence-nodejs:any', + perl_dbi => 'libdbi-perl:any | dh-sequence-perl-dbi:any', + perl_imager => 'libimager-perl:any | dh-sequence-perl-imager:any', + pgxs => 'postgresql-server-dev-all:any | postgresql-all:any', + pgxs_loop => 'postgresql-server-dev-all:any | postgresql-all:any', + pypy => 'dh-python:any | dh-sequence-pypy:any', + python2 => 'python2:any | python2-dev:any | dh-sequence-python2:any', + python3 => +'python3:any | python3-all:any | python3-dev:any | python3-all-dev:any | dh-sequence-python3:any', + scour => 'scour:any | python-scour:any | dh-sequence-scour:any', + sphinxdoc => +'sphinx:any | python-sphinx:any | python3-sphinx:any | dh-sequence-sphinxdoc:any', + systemd => +'debhelper:any (>= 9.20160709~) | debhelper-compat:any | dh-sequence-systemd:any | dh-systemd:any', + vim_addon => 'dh-vim-addon:any | dh-sequence-vim-addon:any', +); + +sub visit_patched_files { + my ($self, $item) = @_; + + return + unless $item->dirname eq 'debian/'; + + return + if !$item->is_symlink && !$item->is_file; + + if ( $item->basename eq 'control' + || $item->basename =~ m/^(?:.*\.)?(?:copyright|changelog|NEWS)$/) { + + # Handle "control", [<pkg>.]copyright, [<pkg>.]changelog + # and [<pkg>.]NEWS + + # The permissions of symlinks are not really defined, so resolve + # $item to ensure we are not dealing with a symlink. + my $actual = $item->resolve_path; + + $self->pointed_hint('package-file-is-executable', $item->pointer) + if $actual && $actual->is_executable; + + return; + } + + return; +} + +sub source { + my ($self) = @_; + + my @MAINT_COMMANDS = @{$self->data->debhelper_commands->maint_commands}; + + my $FILENAME_CONFIGS= $self->data->load('debhelper/filename-config-files'); + + my $DEBHELPER_LEVELS = $self->data->debhelper_levels; + my $DH_ADDONS = $self->data->debhelper_addons; + my $DH_COMMANDS_DEPENDS= $self->data->debhelper_commands; + + my @KNOWN_DH_COMMANDS; + for my $command ($DH_COMMANDS_DEPENDS->all) { + for my $focus ($EMPTY, qw(-arch -indep)) { + for my $timing (qw(override execute_before execute_after)) { + + push(@KNOWN_DH_COMMANDS, + $timing . $UNDERSCORE . $command . $focus); + } + } + } + + my $debhelper_level; + my $dh_compat_variable; + my $maybe_skipping; + + my $uses_debhelper = 0; + my $uses_dh_exec = 0; + my $uses_autotools_dev_dh = 0; + + my $includes_cdbs = 0; + my $modifies_scripts = 0; + + my $seen_any_dh_command = 0; + my $seen_dh_sequencer = 0; + my $seen_dh_dynamic = 0; + my $seen_dh_systemd = 0; + my $seen_dh_parallel = 0; + my $seen_dh_clean_k = 0; + + my %command_by_prerequisite; + my %addon_by_prerequisite; + my %overrides; + + my $droot = $self->processable->patched->resolve_path('debian/'); + + my $drules; + $drules = $droot->child('rules') if $droot; + + return + unless $drules && $drules->is_open_ok; + + open(my $rules_fd, '<', $drules->unpacked_path) + or die encode_utf8('Cannot open ' . $drules->unpacked_path); + + my $command_prefix_pattern = qr/\s+[@+-]?(?:\S+=\S+\s+)*/; + + my $build_prerequisites_norestriction + = $self->processable->relation_norestriction('Build-Depends-All'); + my $build_prerequisites= $self->processable->relation('Build-Depends-All'); + + my %seen = ( + 'python2' => 0, + 'python3' => 0, + 'runit' => 0, + 'sphinxdoc' => 0, + ); + + for (qw(python2 python3)) { + + $seen{$_} = 1 + if $build_prerequisites_norestriction->satisfies( + "dh-sequence-$_:any"); + } + + my %build_systems; + + my $position = 1; + while (my $line = <$rules_fd>) { + + my $pointer = $drules->pointer($position); + + while ($line =~ s/\\$// && defined(my $cont = <$rules_fd>)) { + $line .= $cont; + } + + if ($line =~ /^ifn?(?:eq|def)\s/) { + $maybe_skipping++; + + } elsif ($line =~ /^endif\s/) { + $maybe_skipping--; + } + + next + if $line =~ /^\s*\#/; + + if ($line =~ /^$command_prefix_pattern(dh_(?!autoreconf)\S+)/) { + + my $dh_command = $1; + + $build_systems{'debhelper'} = 1 + unless exists $build_systems{'dh'}; + + $self->pointed_hint('dh_installmanpages-is-obsolete',$pointer) + if $dh_command eq 'dh_installmanpages'; + + if ( $dh_command eq 'dh_autotools-dev_restoreconfig' + || $dh_command eq 'dh_autotools-dev_updateconfig') { + + $self->pointed_hint( + 'debhelper-tools-from-autotools-dev-are-deprecated', + $pointer, $dh_command); + $uses_autotools_dev_dh = 1; + } + + # Record if we've seen specific helpers, special-casing + # "dh_python" as Python 2.x. + $seen{'python2'} = 1 if $dh_command eq 'dh_python2'; + for my $k (keys %seen) { + $seen{$k} = 1 if $dh_command eq "dh_$k"; + } + + $seen_dh_clean_k = 1 + if $dh_command eq 'dh_clean' + && $line =~ /\s+\-k(?:\s+.*)?$/s; + + # if command is passed -n, it does not modify the scripts + $modifies_scripts = 1 + if (any { $dh_command eq $_ } @MAINT_COMMANDS) + && $line !~ /\s+\-n\s+/; + + # If debhelper commands are wrapped in make conditionals, assume the + # maintainer knows what they're doing and don't check build + # dependencies. + unless ($maybe_skipping) { + + if (exists $DH_COMMAND_MANUAL_PREREQUISITES{$dh_command}) { + my $prerequisite + = $DH_COMMAND_MANUAL_PREREQUISITES{$dh_command}; + $command_by_prerequisite{$prerequisite} = $dh_command; + + } elsif ($DH_COMMANDS_DEPENDS->installed_by($dh_command)) { + + my @broadened = map { "$_:any" } + $DH_COMMANDS_DEPENDS->installed_by($dh_command); + my $prerequisite + = join($SPACE . $HORIZONTAL_BAR . $SPACE,@broadened); + $command_by_prerequisite{$prerequisite} = $dh_command; + } + } + + $seen_any_dh_command = 1; + $uses_debhelper = 1; + + } elsif ($line =~ m{^(?:$command_prefix_pattern)dh\s+}) { + + $build_systems{'dh'} = 1; + delete($build_systems{'debhelper'}); + + $seen_dh_sequencer = 1; + $seen_any_dh_command = 1; + + $seen_dh_dynamic = 1 + if $line =~ /\$[({]\w/; + + $seen_dh_parallel = $position + if $line =~ /--parallel/; + + $uses_debhelper = 1; + $modifies_scripts = 1; + + while ($line =~ /\s--with(?:=|\s+)(['"]?)(\S+)\1/g) { + + my $addon_list = $2; + + for my $addon (split(/,/, $addon_list)) { + + my $orig_addon = $addon; + + $addon =~ y,-,_,; + + my @broadened + = map { "$_:any" } $DH_ADDONS->installed_by($addon); + my $prerequisite = $DH_ADDON_MANUAL_PREREQUISITES{$addon} + || join($SPACE . $HORIZONTAL_BAR . $SPACE,@broadened); + + if ($addon eq 'autotools_dev') { + + $self->pointed_hint( +'debhelper-tools-from-autotools-dev-are-deprecated', + $pointer,"dh ... --with $orig_addon" + ); + $uses_autotools_dev_dh = 1; + } + + $seen_dh_systemd = $position + if $addon eq 'systemd'; + + $self->pointed_hint( + 'dh-quilt-addon-but-quilt-source-format', + $pointer,"dh ... --with $orig_addon") + if $addon eq 'quilt' + && $self->processable->fields->value('Format') eq + '3.0 (quilt)'; + + $addon_by_prerequisite{$prerequisite} = $addon + if defined $prerequisite; + + for my $k (keys %seen) { + $seen{$k} = 1 + if $addon eq $k; + } + } + } + + } elsif ($line =~ m{^include\s+/usr/share/cdbs/1/rules/debhelper.mk} + || $line =~ m{^include\s+/usr/share/R/debian/r-cran.mk}) { + + $build_systems{'cdbs-with-debhelper.mk'} = 1; + delete($build_systems{'cdbs-without-debhelper.mk'}); + + $seen_any_dh_command = 1; + $uses_debhelper = 1; + $modifies_scripts = 1; + $includes_cdbs = 1; + + # CDBS sets DH_COMPAT but doesn't export it. + $dh_compat_variable = $CDBS_COMPAT; + + } elsif ($line =~ /^\s*export\s+DH_COMPAT\s*:?=\s*([^\s]+)/) { + $debhelper_level = $1; + + } elsif ($line =~ /^\s*export\s+DH_COMPAT/) { + $debhelper_level = $dh_compat_variable + if $dh_compat_variable; + + } elsif ($line =~ /^\s*DH_COMPAT\s*:?=\s*([^\s]+)/) { + $dh_compat_variable = $1; + + # one can export and then set the value: + $debhelper_level = $1 + if $debhelper_level; + + } elsif ( + $line =~ /^[^:]*(override|execute_(?:after|before))\s+(dh_[^:]*):/) + { + $self->pointed_hint('typo-in-debhelper-override-target', + $pointer, "$1 $2",$ARROW, "$1_$2"); + + } elsif ($line =~ /^([^:]*_dh_[^:]*):/) { + + my $alltargets = $1; + # can be multiple targets per rule. + my @targets = split(/\s+/, $alltargets); + my @dh_targets = grep { /_dh_/ } @targets; + + # If maintainer is using wildcards, it's unlikely to be a typo. + my @no_wildcards = grep { !/%/ } @dh_targets; + + my $lc = List::Compare->new(\@no_wildcards, \@KNOWN_DH_COMMANDS); + my @unknown = $lc->get_Lonly; + + for my $target (@unknown) { + + my %distance + = map { $_ => distance($target, $_) } @KNOWN_DH_COMMANDS; + my @near = grep { $distance{$_} < $LEVENSHTEIN_TOLERANCE } + keys %distance; + my $nearest = min_by { $distance{$_} } @near; + + $self->pointed_hint('typo-in-debhelper-override-target', + $pointer, $target, $ARROW, $nearest) + if length $nearest; + } + + for my $target (@no_wildcards) { + + next + unless $target + =~ /^(override|execute_(?:before|after))_dh_([^\s]+?)(-arch|-indep|)$/; + + my $timing = $1; + my $command = $2; + my $focus = $3; + my $dh_command = "dh_$command"; + + $overrides{$dh_command} = [$position, $focus]; + $uses_debhelper = 1; + + next + if $DH_COMMANDS_DEPENDS->installed_by($dh_command); + + # Unknown command, so check for likely misspellings + my $missingauto = firstval { "dh_auto_$command" eq $_ } + $DH_COMMANDS_DEPENDS->all; + + $self->pointed_hint( + 'typo-in-debhelper-override-target',$pointer, + $timing . $UNDERSCORE . $dh_command,$ARROW, + $timing . $UNDERSCORE . $missingauto, + )if length $missingauto; + } + + } elsif ($line =~ m{^include\s+/usr/share/cdbs/}) { + + $includes_cdbs = 1; + + $build_systems{'cdbs-without-debhelper.mk'} = 1 + unless exists $build_systems{'cdbs-with-debhelper.mk'}; + + } elsif ( + $line =~m{ + ^include \s+ + /usr/share/(?: + dh-php/pkg-pecl\.mk + |blends-dev/rules + ) + }xsm + ) { + # All of these indirectly use dh. + $seen_any_dh_command = 1; + $build_systems{'dh'} = 1; + delete($build_systems{'debhelper'}); + + } elsif ( + $line =~m{ + ^include \s+ + /usr/share/pkg-kde-tools/qt-kde-team/\d+/debian-qt-kde\.mk + }xsm + ) { + + $includes_cdbs = 1; + $build_systems{'dhmk'} = 1; + delete($build_systems{'debhelper'}); + } + + } continue { + ++$position; + } + + close $rules_fd; + + # Variables could contain any add-ons; assume we have seen them all + %seen = map { $_ => 1 } keys %seen + if $seen_dh_dynamic; + + # Okay - d/rules does not include any file in /usr/share/cdbs/ + $self->pointed_hint('unused-build-dependency-on-cdbs', $drules->pointer) + if $build_prerequisites->satisfies('cdbs:any') + && !$includes_cdbs; + + if (%build_systems) { + + my @systems = sort keys %build_systems; + $self->pointed_hint('debian-build-system', $drules->pointer, + join(', ', @systems)); + + } else { + $self->pointed_hint('debian-build-system', $drules->pointer, 'other'); + } + + unless ($seen_any_dh_command || $includes_cdbs) { + + $self->pointed_hint('package-does-not-use-debhelper-or-cdbs', + $drules->pointer); + return; + } + + my @installable_names= $self->processable->debian_control->installables; + + for my $installable_name (@installable_names) { + + next + if $self->processable->debian_control->installable_package_type( + $installable_name) ne 'deb'; + + my $strong + = $self->processable->binary_relation($installable_name, 'strong'); + my $all= $self->processable->binary_relation($installable_name, 'all'); + + $self->hint('debhelper-but-no-misc-depends', $installable_name) + unless $all->satisfies($MISC_DEPENDS); + + $self->hint('weak-dependency-on-misc-depends', $installable_name) + if $all->satisfies($MISC_DEPENDS) + && !$strong->satisfies($MISC_DEPENDS); + } + + for my $installable ($self->group->get_installables) { + + next + if $installable->type eq 'udeb'; + + my $breaks + = $self->processable->binary_relation($installable->name, 'Breaks'); + my $strong + = $self->processable->binary_relation($installable->name, 'strong'); + + $self->pointed_hint('package-uses-dh-runit-but-lacks-breaks-substvar', + $drules->pointer,$installable->name) + if $seen{'runit'} + && $strong->satisfies('runit:any') + && (any { m{^ etc/sv/ }msx } @{$installable->installed->sorted_list}) + && !$breaks->satisfies($DOLLAR . '{runit:Breaks}'); + } + + my $virtual_compat; + + $build_prerequisites->visit( + sub { + return 0 + unless + m{^ debhelper-compat (?: : \S+ )? \s+ [(]= \s+ (\d+) [)] $}x; + + $virtual_compat = $1; + + return 1; + }, + Lintian::Relation::VISIT_PRED_FULL + | Lintian::Relation::VISIT_STOP_FIRST_MATCH + ); + + my $control_item=$self->processable->debian_control->item; + + $self->pointed_hint('debhelper-compat-virtual-relation', + $control_item->pointer, $virtual_compat) + if length $virtual_compat; + + # gives precedence to virtual compat + $debhelper_level = $virtual_compat + if length $virtual_compat; + + my $compat_file = $droot->child('compat'); + + $self->hint('debhelper-compat-file-is-missing') + unless ($compat_file && $compat_file->is_open_ok) + || $virtual_compat; + + my $from_compat_file = $self->check_compat_file; + + if (length $debhelper_level && length $from_compat_file) { + + $self->pointed_hint( + 'declares-possibly-conflicting-debhelper-compat-versions', + $compat_file->pointer,$from_compat_file,'vs elsewhere', + $debhelper_level); + } + + # this is not just to fill in the gap, but because debhelper + # prefers DH_COMPAT over debian/compat + $debhelper_level ||= $from_compat_file; + + $self->hint('debhelper-compat-level', $debhelper_level) + if length $debhelper_level; + + $debhelper_level ||= 1; + + $self->hint('package-uses-deprecated-debhelper-compat-version', + $debhelper_level) + if $debhelper_level < $DEBHELPER_LEVELS->value('deprecated'); + + $self->hint('package-uses-old-debhelper-compat-version', $debhelper_level) + if $debhelper_level >= $DEBHELPER_LEVELS->value('deprecated') + && $debhelper_level < $DEBHELPER_LEVELS->value('recommended'); + + $self->hint('package-uses-experimental-debhelper-compat-version', + $debhelper_level) + if $debhelper_level >= $DEBHELPER_LEVELS->value('experimental'); + + $self->pointed_hint('dh-clean-k-is-deprecated', $drules->pointer) + if $seen_dh_clean_k; + + for my $suffix (qw(enable start)) { + + my ($stored_position, $focus) + = @{$overrides{"dh_systemd_$suffix"} // []}; + + $self->pointed_hint( + 'debian-rules-uses-deprecated-systemd-override', + $drules->pointer($stored_position), + "override_dh_systemd_$suffix$focus" + ) + if $stored_position + && $debhelper_level >= $BETTER_SYSTEMD_INTEGRATION; + } + + my $num_overrides = scalar(keys %overrides); + + $self->hint('excessive-debhelper-overrides', $num_overrides) + if $num_overrides >= $MANY_OVERRIDES; + + $self->pointed_hint( + 'debian-rules-uses-unnecessary-dh-argument', + $drules->pointer($seen_dh_parallel), + "$debhelper_level >= $DH_PARALLEL_NOT_NEEDED", + 'dh ... --parallel' + )if $seen_dh_parallel && $debhelper_level >= $DH_PARALLEL_NOT_NEEDED; + + $self->pointed_hint( + 'debian-rules-uses-unnecessary-dh-argument', + $drules->pointer($seen_dh_systemd), + "$debhelper_level >= $INVOKES_SYSTEMD", + 'dh ... --with=systemd' + )if $seen_dh_systemd && $debhelper_level >= $INVOKES_SYSTEMD; + + for my $item ($droot->children) { + + next + if !$item->is_symlink && !$item->is_file; + + next + if $item->name eq $drules->name; + + if ($item->basename =~ m/^(?:(.*)\.)?(?:post|pre)(?:inst|rm)$/) { + + next + unless $modifies_scripts; + + # They need to have #DEBHELPER# in their scripts. Search + # for scripts that look like maintainer scripts and make + # sure the token is there. + my $installable_name = $1 || $EMPTY; + my $seentag = 0; + + $seentag = 1 + if $item->decoded_utf8 =~ /\#DEBHELPER\#/; + + if (!$seentag) { + + my $single_pkg = $EMPTY; + $single_pkg + = $self->processable->debian_control + ->installable_package_type($installable_names[0]) + if scalar @installable_names == 1; + + my $installable_type + = $self->processable->debian_control + ->installable_package_type($installable_name); + + my $is_udeb = 0; + + $is_udeb = 1 + if $installable_name && $installable_type eq 'udeb'; + + $is_udeb = 1 + if !$installable_name && $single_pkg eq 'udeb'; + + $self->pointed_hint('maintainer-script-lacks-debhelper-token', + $item->pointer) + unless $is_udeb; + } + + next; + } + + my $category = $item->basename; + $category =~ s/^.+\.//; + + next + unless length $category; + + # Check whether this is a debhelper config file that takes + # a list of filenames. + if ($FILENAME_CONFIGS->recognizes($category)) { + + # The permissions of symlinks are not really defined, so resolve + # $item to ensure we are not dealing with a symlink. + my $actual = $item->resolve_path; + next + unless defined $actual; + + $self->check_for_brace_expansion($item, $debhelper_level); + + # debhelper only use executable files in compat 9 + $self->pointed_hint('package-file-is-executable', $item->pointer) + if $actual->is_executable + && $debhelper_level < $USES_EXECUTABLE_FILES; + + if ($debhelper_level >= $USES_EXECUTABLE_FILES) { + + $self->pointed_hint( + 'executable-debhelper-file-without-being-executable', + $item->pointer) + if $actual->is_executable + && !length $actual->hashbang; + + # Only /usr/bin/dh-exec is allowed, even if + # /usr/lib/dh-exec/dh-exec-subst works too. + $self->pointed_hint('dh-exec-private-helper', $item->pointer) + if $actual->is_executable + && $actual->hashbang =~ m{^/usr/lib/dh-exec/}; + + # Do not make assumptions about the contents of an + # executable debhelper file, unless it's a dh-exec + # script. + if ($actual->hashbang =~ /dh-exec/) { + + $uses_dh_exec = 1; + $self->check_dh_exec($item, $category); + } + } + } + } + + $self->pointed_hint('package-uses-debhelper-but-lacks-build-depends', + $drules->pointer) + if $uses_debhelper + && !$build_prerequisites->satisfies('debhelper:any') + && !$build_prerequisites->satisfies('debhelper-compat:any'); + + $self->pointed_hint('package-uses-dh-exec-but-lacks-build-depends', + $drules->pointer) + if $uses_dh_exec + && !$build_prerequisites->satisfies('dh-exec:any'); + + for my $prerequisite (keys %command_by_prerequisite) { + + my $command = $command_by_prerequisite{$prerequisite}; + + # handled above + next + if $prerequisite eq 'debhelper:any'; + + next + if $debhelper_level >= $REQUIRES_AUTOTOOLS + && (any { $_ eq $prerequisite } + qw(autotools-dev:any dh-strip-nondeterminism:any)); + + $self->pointed_hint('missing-build-dependency-for-dh_-command', + $drules->pointer,$command, "(does not satisfy $prerequisite)") + unless $build_prerequisites_norestriction->satisfies($prerequisite); + } + + for my $prerequisite (keys %addon_by_prerequisite) { + + my $addon = $addon_by_prerequisite{$prerequisite}; + + next + if $debhelper_level >= $REQUIRES_AUTOTOOLS + && $addon eq 'autoreconf'; + + $self->pointed_hint('missing-build-dependency-for-dh-addon', + $drules->pointer,$addon, "(does not satisfy $prerequisite)") + unless ( + $build_prerequisites_norestriction->satisfies($prerequisite)); + + # As a special case, the python3 addon needs a dependency on + # dh-python unless the -dev packages are used. + my $python_source + = 'dh-python:any | dh-sequence-python3:any | pybuild-plugin-pyproject:any'; + + $self->pointed_hint('missing-build-dependency-for-dh-addon', + $drules->pointer,$addon, "(does not satisfy $python_source)") + if $addon eq 'python3' + && $build_prerequisites_norestriction->satisfies($prerequisite) + && !$build_prerequisites_norestriction->satisfies( + 'python3-dev:any | python3-all-dev:any') + && !$build_prerequisites_norestriction->satisfies($python_source); + } + + $self->hint('no-versioned-debhelper-prerequisite', $debhelper_level) + unless $build_prerequisites->satisfies( + "debhelper:any (>= $debhelper_level~)") + || $build_prerequisites->satisfies( + "debhelper-compat:any (= $debhelper_level)"); + + if ($debhelper_level >= $USES_AUTORECONF) { + for my $autotools_source (qw(dh-autoreconf:any autotools-dev:any)) { + + next + if $autotools_source eq 'autotools-dev:any' + && $uses_autotools_dev_dh; + + $self->hint('useless-autoreconf-build-depends', + "(does not need to satisfy $autotools_source)") + if $build_prerequisites->satisfies($autotools_source); + } + } + + if ($seen_dh_sequencer && !$seen{'python2'}) { + + my %python_depends; + + for my $installable_name (@installable_names) { + + $python_depends{$installable_name} = 1 + if $self->processable->binary_relation($installable_name,'all') + ->satisfies($DOLLAR . '{python:Depends}'); + } + + $self->hint('python-depends-but-no-python-helper', + (sort keys %python_depends)) + if %python_depends; + } + + if ($seen_dh_sequencer && !$seen{'python3'}) { + + my %python3_depends; + + for my $installable_name (@installable_names) { + + $python3_depends{$installable_name} = 1 + if $self->processable->binary_relation($installable_name,'all') + ->satisfies($DOLLAR . '{python3:Depends}'); + } + + $self->hint('python3-depends-but-no-python3-helper', + (sort keys %python3_depends)) + if %python3_depends; + } + + if ($seen{'sphinxdoc'} && !$seen_dh_dynamic) { + + my $seen_sphinxdoc = 0; + + for my $installable_name (@installable_names) { + $seen_sphinxdoc = 1 + if $self->processable->binary_relation($installable_name,'all') + ->satisfies($DOLLAR . '{sphinxdoc:Depends}'); + } + + $self->pointed_hint('sphinxdoc-but-no-sphinxdoc-depends', + $drules->pointer) + unless $seen_sphinxdoc; + } + + return; +} + +sub check_for_brace_expansion { + my ($self, $item, $debhelper_level) = @_; + + return + unless $item->is_open_ok; + + open(my $fd, '<', $item->unpacked_path) + or die encode_utf8('Cannot open ' . $item->unpacked_path); + + my $position = 1; + while (my $line = <$fd>) { + + next + if $line =~ /^\s*$/; + + next + if $line =~ /^\#/ + && $debhelper_level >= $BRACE_EXPANSION; + + if ($line =~ /((?<!\\)\{(?:[^\s\\\}]*?,)+[^\\\}\s,]*,*\})/){ + my $expansion = $1; + + my $pointer = $item->pointer($position); + + $self->pointed_hint('brace-expansion-in-debhelper-config-file', + $pointer, $expansion); + + last; + } + + } continue { + ++$position; + } + + close $fd; + + return; +} + +sub check_compat_file { + my ($self) = @_; + + # Check the compat file. Do this separately from looping over all + # of the other files since we use the compat value when checking + # for brace expansion. + + my $compat_file + = $self->processable->patched->resolve_path('debian/compat'); + + # missing file is dealt with elsewhere + return $EMPTY + unless $compat_file && $compat_file->is_open_ok; + + my $debhelper_level; + + open(my $fd, '<', $compat_file->unpacked_path) + or die encode_utf8('Cannot open ' . $compat_file->unpacked_path); + + my $position = 1; + while (my $line = <$fd>) { + + if ($position == 1) { + + $debhelper_level = $line; + next; + } + + my $pointer = $compat_file->pointer($position); + + $self->pointed_hint('debhelper-compat-file-contains-multiple-levels', + $pointer) + if $line =~ /^\d/; + + } continue { + ++$position; + } + + close $fd; + + # trim both ends + $debhelper_level =~ s/^\s+|\s+$//g; + + if (!length $debhelper_level) { + + $self->pointed_hint('debhelper-compat-file-is-empty', + $compat_file->pointer); + return $EMPTY; + } + + my $DEBHELPER_LEVELS = $self->data->debhelper_levels; + + # Recommend people use debhelper-compat (introduced in debhelper + # 11.1.5~alpha1) over debian/compat, except for experimental/beta + # versions. + $self->pointed_hint('uses-debhelper-compat-file', $compat_file->pointer) + if $debhelper_level >= $VERSIONED_PREREQUISITE_AVAILABLE + && $debhelper_level < $DEBHELPER_LEVELS->value('experimental'); + + return $debhelper_level; +} + +sub check_dh_exec { + my ($self, $item, $category) = @_; + + return + unless $item->is_open_ok; + + my $dhe_subst = 0; + my $dhe_install = 0; + my $dhe_filter = 0; + + open(my $fd, '<', $item->unpacked_path) + or die encode_utf8('Cannot open ' . $item->unpacked_path); + + my $position = 1; + while (my $line = <$fd>) { + + chomp $line; + + my $pointer = $item->pointer($position); + + if ($line =~ /\$\{([^\}]+)\}/) { + + my $sv = $1; + $dhe_subst = 1; + + if ( + $sv !~ m{ \A + DEB_(?:BUILD|HOST)_(?: + ARCH (?: _OS|_CPU|_BITS|_ENDIAN )? + |GNU_ (?:CPU|SYSTEM|TYPE)|MULTIARCH + ) \Z}xsm + ) { + $self->pointed_hint('dh-exec-subst-unknown-variable', + $pointer, $sv); + } + } + + $dhe_install = 1 + if $line =~ /[ \t]=>[ \t]/; + + $dhe_filter = 1 + if $line =~ /\[[^\]]+\]/; + + $dhe_filter = 1 + if $line =~ /<[^>]+>/; + + if ( $line =~ /^usr\/lib\/\$\{([^\}]+)\}\/?$/ + || $line + =~ /^usr\/lib\/\$\{([^\}]+)\}\/?\s+\/usr\/lib\/\$\{([^\}]+)\}\/?$/ + || $line =~ /^usr\/lib\/\$\{([^\}]+)\}[^\s]+$/) { + + my $sv = $1; + my $dv = $2; + my $dhe_useless = 0; + + if ( + $sv =~ m{ \A + DEB_(?:BUILD|HOST)_(?: + ARCH (?: _OS|_CPU|_BITS|_ENDIAN )? + |GNU_ (?:CPU|SYSTEM|TYPE)|MULTIARCH + ) \Z}xsm + ) { + if (defined($dv)) { + $dhe_useless = ($sv eq $dv); + } else { + $dhe_useless = 1; + } + } + + $self->pointed_hint('dh-exec-useless-usage', $pointer, $line) + if $dhe_useless && $item =~ /debian\/.*(install|manpages)/; + } + + } continue { + ++$position; + } + + close $fd; + + $self->pointed_hint('dh-exec-script-without-dh-exec-features', + $item->pointer) + if !$dhe_subst + && !$dhe_install + && !$dhe_filter; + + $self->pointed_hint('dh-exec-install-not-allowed-here', $item->pointer) + if $dhe_install + && $category ne 'install' + && $category ne 'manpages'; + + return; +} + +1; + +# Local Variables: +# indent-tabs-mode: nil +# cperl-indent-level: 4 +# End: +# vim: syntax=perl sw=4 sts=4 sr et |