# debhelper format -- lintian check script -*- perl -*- # Copyright (C) 1999 by Joey Hess # Copyright (C) 2016-2020 Chris Lamb # 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", [.]copyright, [.]changelog # and [.]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 =~ /((?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