# debian/rules -- lintian check script -*- perl -*- # Copyright (C) 2006 Russ Allbery # Copyright (C) 2005 Rene van Bevern # Copyright (C) 2019-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. package Lintian::Check::Debian::Rules; use v5.20; use warnings; use utf8; use Carp qw(croak); use Const::Fast; use List::Compare; use List::SomeUtils qw(any none uniq); use Unicode::UTF8 qw(encode_utf8); use Moo; use namespace::clean; with 'Lintian::Check'; const my $EMPTY => q{}; const my $SPACE => q{ }; const my $PERCENT => q{%}; my @py3versions = qw(3.4 3.5 3.6 3.7); my $PYTHON_DEPEND= 'python2:any | python2-dev:any'; my $PYTHON3_DEPEND = 'python3:any | python3-dev:any | python3-all:any | python3-all-dev:any'; my $PYTHON2X_DEPEND = 'python2.7:any | python2.7-dev:any'; my $PYTHON3X_DEPEND = join(' | ',map { "python${_}:any | python${_}-dev:any" } @py3versions); my $ANYPYTHON_DEPEND = "$PYTHON_DEPEND | $PYTHON2X_DEPEND | $PYTHON3_DEPEND | $PYTHON3X_DEPEND"; my $PYTHON3_ALL_DEPEND = 'python3-all:any | python3-all-dev:any | python3-all-dbg:any'; my %TAG_FOR_POLICY_TARGET = ( build => 'debian-rules-missing-required-target', binary => 'debian-rules-missing-required-target', 'binary-arch' => 'debian-rules-missing-required-target', 'binary-indep' => 'debian-rules-missing-required-target', clean => 'debian-rules-missing-required-target', 'build-arch' => 'debian-rules-missing-required-target', 'build-indep' => 'debian-rules-missing-required-target' ); # Rules about required debhelper command ordering. Each command is put into a # class and the tag is issued if they're called in the wrong order for the # classes. Unknown commands won't trigger this flag. my %debhelper_order = ( dh_makeshlibs => 1, dh_shlibdeps => 2, dh_installdeb => 2, dh_gencontrol => 2, dh_builddeb => 3 ); sub source { my ($self) = @_; my $debian_dir = $self->processable->patched->resolve_path('debian'); my $rules; $rules = $debian_dir->child('rules') if defined $debian_dir; return unless defined $rules; # Policy could be read as allowing debian/rules to be a symlink to # some other file, and in a native Debian package it could be a # symlink to a file that we didn't unpack. $self->pointed_hint('debian-rules-is-symlink', $rules->pointer) if $rules->is_symlink; # dereference symbolic links $rules = $rules->follow; return unless defined $rules; $self->pointed_hint('debian-rules-not-executable', $rules->pointer) unless $rules->is_executable; my $KNOWN_MAKEFILES= $self->data->load('rules/known-makefiles', '\|\|'); my $DEPRECATED_MAKEFILES= $self->data->load('rules/deprecated-makefiles'); my $architecture = $self->processable->fields->value('Architecture'); # If the version field is missing, we assume a neutral non-native one. my $version = $self->processable->fields->value('Version') || '0-1'; # Check for required #!/usr/bin/make -f opening line. Allow -r or -e; a # strict reading of Policy doesn't allow either, but they seem harmless. $self->pointed_hint('debian-rules-not-a-makefile', $rules->pointer) unless $rules->hashbang =~ m{^/usr/bin/make\s+-[re]?f[re]?$}; # Certain build tools must be listed in Build-Depends even if there are no # arch-specific packages because they're required in order to run the clean # rule. (See Policy 7.6.) The following is a list of package dependencies; # regular expressions that, if they match anywhere in the debian/rules file, # say that this package is allowed (and required) in Build-Depends; and # optional tags to use for reporting the problem if some information other # than the default is required. my %GLOBAL_CLEAN_DEPENDS = ( 'ant:any' => [qr{^include\s*/usr/share/cdbs/1/rules/ant\.mk}], 'cdbs:any' => [ qr{^include\s+/usr/share/cdbs/}, qr{^include\s+/usr/share/R/debian/r-cran\.mk} ], 'dbs:any' => [qr{^include\s+/usr/share/dbs/}], 'dh-make-php:any' => [qr{^include\s+/usr/share/cdbs/1/class/pear\.mk}], 'debhelper:any | debhelper-compat:any' =>[ qr{^include\s+/usr/share/cdbs/1/rules/debhelper\.mk}, qr{^include\s+/usr/share/R/debian/r-cran\.mk} ], 'dpatch:any' => [ qr{^include\s+/usr/share/dpatch/}, qr{^include\s+/usr/share/cdbs/1/rules/dpatch\.mk} ], 'gnome-pkg-tools:any | dh-sequence-gnome:any' => [qr{^include\s+/usr/share/gnome-pkg-tools/}], 'quilt:any' => [ qr{^include\s+/usr/share/quilt/}, qr{^include\s+/usr/share/cdbs/1/rules/patchsys-quilt\.mk} ], 'mozilla-devscripts:any' => [qr{^include\s+/usr/share/mozilla-devscripts/}], 'ruby-pkg-tools:any' => [qr{^include\s+/usr/share/ruby-pkg-tools/1/class/}], 'r-base-dev:any' => [qr{^include\s+/usr/share/R/debian/r-cran\.mk}], $ANYPYTHON_DEPEND =>[qr{/usr/share/cdbs/1/class/python-distutils\.mk}], ); # A list of packages; regular expressions that, if they match anywhere in the # debian/rules file, this package must be listed in either Build-Depends or # Build-Depends-Indep as appropriate; and optional tags as above. my %GLOBAL_DEPENDS = ( 'dh-ocaml:any, ocaml-nox:any | ocaml:any' => [qr/^\t\s*dh_ocaml(?:init|doc)\s/], 'debhelper:any | debhelper-compat:any | dh-autoreconf:any' => [qr/^\t\s*dh_autoreconf(?:_clean)?\s/], ); # Similarly, this list of packages, regexes, and optional tags say that if the # regex matches in one of clean, build-arch, binary-arch, or a rule they # depend on, this package is allowed (and required) in Build-Depends. my %RULE_CLEAN_DEPENDS =( 'ant:any' => [qr/^\t\s*(\S+=\S+\s+)*ant\s/], 'debhelper:any | debhelper-compat:any' => [qr/^\t\s*dh_(?!autoreconf).+/], 'dh-ocaml:any, ocaml-nox:any | ocaml:any' =>[qr/^\t\s*dh_ocamlinit\s/], 'dpatch:any' => [qr/^\t\s*(\S+=\S+\s+)*dpatch\s/], 'po-debconf:any' => [qr/^\t\s*debconf-updatepo\s/], $PYTHON_DEPEND => [qr/^\t\s*python\s/], $PYTHON3_DEPEND => [qr/^\t\s*python3\s/], $ANYPYTHON_DEPEND => [qr/\ssetup\.py\b/], 'quilt:any' => [qr/^\t\s*(\S+=\S+\s+)*quilt\s/], ); my $build_all = $self->processable->relation('Build-Depends-All'); my $build_all_norestriction = $self->processable->relation_norestriction('Build-Depends-All'); my $build_regular = $self->processable->relation('Build-Depends'); my $build_indep = $self->processable->relation('Build-Depends-Indep'); # no need to look for items we have delete %GLOBAL_DEPENDS{$_} for grep { $build_regular->satisfies($_) } keys %GLOBAL_DEPENDS; delete %GLOBAL_CLEAN_DEPENDS{$_} for grep { $build_regular->satisfies($_) } keys %GLOBAL_CLEAN_DEPENDS; delete %RULE_CLEAN_DEPENDS{$_} for grep { $build_regular->satisfies($_) } keys %RULE_CLEAN_DEPENDS; my @needed; my @needed_clean; # Scan debian/rules. We would really like to let make do this for # us, but unfortunately there doesn't seem to be a way to get make # to syntax-check and analyze a makefile without running at least # $(shell) commands. # # We skip some of the rule analysis if debian/rules includes any # other files, since to chase all includes we'd have to have all # of its build dependencies installed. local $_ = undef; my @arch_rules = map { qr/^$_$/ } qw(clean binary-arch build-arch); my @indep_rules = qw(build build-indep binary-indep); my @current_targets; my %rules_per_target; my %debhelper_group; my %seen; my %overridden; my $maybe_skipping; my @conditionals; my %variables; my $includes = 0; my $contents = $rules->decoded_utf8; return unless length $contents; my @lines = split(/\n/, $contents); my $continued = $EMPTY; my $position = 1; for my $line (@lines) { my $pointer = $rules->pointer($position); $self->pointed_hint('debian-rules-is-dh_make-template', $pointer) if $line =~ m/dh_make generated override targets/; next if $line =~ /^\s*\#/; if (length $continued) { $line = $continued . $line; $continued = $EMPTY; } if ($line =~ s/\\$//) { $continued = $line; next; } if ($line =~ /^\s*[s-]?include\s+(\S++)/){ my $makefile = $1; my $targets = $KNOWN_MAKEFILES->value($makefile); if (defined $targets){ for my $target (split /\s*+,\s*+/, $targets){ $seen{$target}++ if exists $TAG_FOR_POLICY_TARGET{$target}; } } else { $includes = 1; } $self->pointed_hint('debian-rules-uses-deprecated-makefile', $pointer, $makefile) if $DEPRECATED_MAKEFILES->recognizes($makefile); } # problems occurring only outside targets unless (%seen) { # Check for DH_COMPAT settings outside of any rule, which are now # deprecated. It's a bit easier structurally to do this here than in # debhelper. $self->pointed_hint('debian-rules-sets-DH_COMPAT', $pointer) if $line =~ /^\s*(?:export\s+)?DH_COMPAT\s*:?=/; $self->pointed_hint('debian-rules-sets-DEB_BUILD_OPTIONS',$pointer) if $line =~ /^\s*(?:export\s+)?DEB_BUILD_OPTIONS\s*:?=/; if ( $line =~m{^ \s*(?:export\s+)? (DEB_(?:HOST|BUILD|TARGET)_(?:ARCH|MULTIARCH|GNU)[A-Z_]*)\s*:?= }x ) { my $variable = $1; $self->pointed_hint( 'debian-rules-sets-dpkg-architecture-variable', $pointer, $variable); } } if ( $line =~ /^\t\s*-(?:\$[\(\{]MAKE[\}\)]|make)\s.*(?:dist)?clean/s || $line =~ /^\t\s*(?:\$[\(\{]MAKE[\}\)]|make)\s(?:.*\s)?-(\w*)i.*(?:dist)?clean/s ) { my $flags = $1 // $EMPTY; # Ignore "-C" (#671537) $self->pointed_hint('debian-rules-ignores-make-clean-error', $pointer) unless $flags =~ /^C/; } if ($line =~ m{dh_strip\b.*(--(?:ddeb|dbgsym)-migration=(?:'[^']*'|\S*))}) { my $context = $1; $self->pointed_hint('debug-symbol-migration-possibly-complete', $pointer, $context); } $self->pointed_hint('debian-rules-passes-version-info-to-dh_shlibdeps', $pointer) if $line =~ m{dh_shlibdeps\b.*(?:--version-info|-V)\b}; $self->pointed_hint('debian-rules-updates-control-automatically', $pointer) if $line =~ m{^\s*DEB_AUTO_UPDATE_DEBIAN_CONTROL\s*=\s*yes}; $self->pointed_hint('debian-rules-uses-deb-build-opts', $pointer) if $line =~ m{\$[\(\{]DEB_BUILD_OPTS[\)\}]}; if ($line =~ m{^\s*DH_EXTRA_ADDONS\s*=\s*(.*)$}) { my $context = $1; $self->pointed_hint('debian-rules-should-not-use-DH_EXTRA_ADDONS', $pointer, $context); } $self->pointed_hint('debian-rules-uses-wrong-environment-variable', $pointer) if $line =~ m{\bDEB_[^_ \t]+FLAGS_(?:SET|APPEND)\b}; $self->pointed_hint('debian-rules-calls-pwd', $pointer) if $line =~ m{\$[\(\{]PWD[\)\}]}; $self->pointed_hint( 'debian-rules-should-not-use-sanitize-all-buildflag',$pointer) if $line =~ m{^\s*(?:export\s+)?DEB_BUILD_MAINT_OPTIONS\s*:?=.*\bsanitize=\+all\b}; $self->pointed_hint('debian-rules-uses-special-shell-variable', $pointer) if $line =~ m{\$[\(\{]_[\)\}]}; if ($line =~ m{(dh_builddeb\b.*--.*-[zZS].*)$}) { my $context = $1; $self->pointed_hint('custom-compression-in-debian-rules', $pointer, $context); } if ($line =~ m{(py3versions\s+([\w\-\s]*--installed|-\w*i\w*))}) { my $context = $1; $self->pointed_hint('debian-rules-uses-installed-python-versions', $pointer, $context); } $self->pointed_hint('debian-rules-uses-as-needed-linker-flag',$pointer) if $line =~ /--as-needed/ && $line !~ /--no-as-needed/; if ($line =~ /(py3versions\s+([\w\-\s]*--supported|-\w*s\w*))/) { my $context = $1; $self->pointed_hint( 'debian-rules-uses-supported-python-versions-without-python-all-build-depends', $pointer, $context )unless $build_all_norestriction->satisfies($PYTHON3_ALL_DEPEND); } # General assignment - save the variable if ($line =~ /^\s*(?:\S+\s+)*?(\S+)\s*[:\?\+]?=\s*(.*+)?$/s) { # This is far too simple from a theoretical PoV, but should do # rather well. my ($var, $value) = ($1, $2); $variables{$var} = $value; $self->pointed_hint('unnecessary-source-date-epoch-assignment', $pointer) if $var eq 'SOURCE_DATE_EPOCH' && !$build_all->satisfies( 'dpkg-dev:any (>= 1.18.8) | debhelper:any (>= 10.10)'); } # Keep track of whether this portion of debian/rules may be optional if ($line =~ /^ifn?(?:eq|def)\s(.*)/) { push(@conditionals, $1); $maybe_skipping++; } elsif ($line =~ /^endif\s/) { $maybe_skipping--; } unless ($maybe_skipping) { for my $prerequisite (keys %GLOBAL_DEPENDS) { my @patterns = @{ $GLOBAL_DEPENDS{$prerequisite} }; push(@needed, $prerequisite) if any { $line =~ $_ } @patterns; } for my $prerequisite (keys %GLOBAL_CLEAN_DEPENDS) { my @patterns = @{ $GLOBAL_CLEAN_DEPENDS{$prerequisite} }; if (any { $line =~ $_ } @patterns) { push(@needed, $prerequisite); push(@needed_clean, $prerequisite); } } } # Listing a rule as a dependency of .PHONY is sufficient to make it # present for the purposes of GNU make and therefore the Policy # requirement. if ($line =~ /^(?:[^:]+\s)?\.PHONY(?:\s[^:]+)?:(.+)/s) { my @targets = split($SPACE, $1); for my $target (@targets) { # Is it $(VAR) ? if ($target =~ /^\$[\(\{]([^\)\}]++)[\)\}]$/) { my $name = $1; my $val = $variables{$name}; if ($val) { # we think we know what it will expand to - note # we ought to "delay" it was a "=" variable rather # than ":=" or "+=". # discards empty elements at end, effectively trimming right for (split(/\s+/, $val)) { $seen{$target}++ if exists $TAG_FOR_POLICY_TARGET{$target}; } last; } # We don't know, so just mark the target as seen. } $seen{$target}++ if exists $TAG_FOR_POLICY_TARGET{$target}; } #.PHONY implies the rest will not match next; } if ( !$includes && $line =~ /dpkg-parsechangelog.*(?:Source|Version|Date|Timestamp)/s) { $self->pointed_hint('debian-rules-parses-dpkg-parsechangelog', $pointer); } if ($line !~ /^ifn?(?:eq|def)\s/ && $line =~ /^([^\s:][^:]*):+(.*)/s) { my ($target_names, $target_dependencies) = ($1, $2); @current_targets = split $SPACE, $target_names; my @quoted = map { quotemeta } split($SPACE, $target_dependencies); s/\\\$\\\([^\):]+\\:([^=]+)\\=([^\)]+)\1\\\)/$2.*/g for @quoted; my @depends = map { qr/^$_$/ } @quoted; for my $target (@current_targets) { $overridden{$1} = $position if $target =~ m/override_(.+)/; if ($target =~ /%/) { my $pattern = quotemeta $target; $pattern =~ s/\\%/.*/g; for my $rulebypolicy (keys %TAG_FOR_POLICY_TARGET) { $seen{$rulebypolicy}++ if $rulebypolicy =~ m/$pattern/; } } else { # Is it $(VAR) ? if ($target =~ m/^\$[\(\{]([^\)\}]++)[\)\}]$/) { my $name = $1; my $val = $variables{$name}; if ($val) { # we think we know what it will expand to - note # we ought to "delay" it was a "=" variable rather # than ":=" or "+=". local $_ = undef; # discards empty elements at end, effectively trimming right for (split(/\s+/, $val)) { $seen{$_}++ if exists $TAG_FOR_POLICY_TARGET{$_}; } last; } # We don't know, so just mark the target as seen. } $seen{$target}++ if exists $TAG_FOR_POLICY_TARGET{$target}; } if (any { $target =~ /$_/ } @arch_rules) { push(@arch_rules, @depends); } } undef %debhelper_group; } elsif ($line =~ /^define /) { # We don't want to think the body of the define is part of # the previous rule or we'll get false positives on tags # like binary-arch-rules-but-pkg-is-arch-indep. Treat a # define as the end of the current rule, although that # isn't very accurate either. @current_targets = (); } else { # If we have non-empty, non-comment lines, store them for # all current targets and check whether debhelper programs # are called in a reasonable order. if ($line =~ /^\s+[^\#]/) { my ($arch, $indep) = (0, 0); for my $target (@current_targets) { $rules_per_target{$target} ||= []; push(@{$rules_per_target{$target}}, $line); $arch = 1 if any { $target =~ /$_/ } @arch_rules; $indep = 1 if any { $target eq $_ } @indep_rules; $indep = 1 if $target eq $PERCENT; $indep = 1 if $target =~ /^override_/; } if (!$maybe_skipping && ($arch || $indep)) { for my $prerequisite (keys %RULE_CLEAN_DEPENDS) { my @patterns = @{ $RULE_CLEAN_DEPENDS{$prerequisite} }; if (any { $line =~ $_ } @patterns) { push(@needed, $prerequisite); push(@needed_clean, $prerequisite) if $arch; } } } if ($line =~ /^\s+(dh_\S+)\b/ && $debhelper_order{$1}) { my $command = $1; my ($package) = ($line =~ /\s(?:-p|--package=)(\S+)/); $package ||= $EMPTY; my $group = $debhelper_order{$command}; $debhelper_group{$package} ||= 0; if ($group < $debhelper_group{$package}) { $self->pointed_hint( 'debian-rules-calls-debhelper-in-odd-order', $pointer, $command); } else { $debhelper_group{$package} = $group; } } } } } continue { ++$position; } my @missing_targets; @missing_targets = grep { !$seen{$_} } keys %TAG_FOR_POLICY_TARGET unless $includes; $self->pointed_hint($TAG_FOR_POLICY_TARGET{$_}, $rules->pointer, $_) for @missing_targets; # Make sure we have no content for binary-arch if we are arch-indep: $rules_per_target{'binary-arch'} ||= []; if ($architecture eq 'all' && scalar @{$rules_per_target{'binary-arch'}}) { my $nonempty = 0; for my $rule (@{$rules_per_target{'binary-arch'}}) { # dh binary-arch is actually a no-op if there is no # Architecture: any package in the control file $nonempty = 1 unless $rule =~ /^\s*dh\s+(?:binary-arch|\$\@)/; } $self->pointed_hint('binary-arch-rules-but-pkg-is-arch-indep', $rules->pointer) if $nonempty; } for my $cmd (qw(dh_clean dh_fixperms)) { for my $suffix ($EMPTY, '-indep', '-arch') { my $memorized_position = $overridden{"$cmd$suffix"}; next unless defined $memorized_position; $self->pointed_hint( "override_$cmd-does-not-call-$cmd", $rules->pointer($memorized_position) ) if none { m/^\t\s*-?($cmd\b|\$\(overridden_command\))/ } @{$rules_per_target{"override_$cmd$suffix"}}; } } if (my $memorized_position = $overridden{'dh_auto_test'}) { my @rules = grep { !m{^\t\s*[\:\[]} && !m{^\s*$} && !m{\bdh_auto_test\b} && ! m{^\t\s*[-@]?(?:(?:/usr)?/bin/)?(?:cp|chmod|echo|ln|mv|mkdir|rm|test|true)} } @{$rules_per_target{'override_dh_auto_test'}}; $self->pointed_hint( 'override_dh_auto_test-does-not-check-DEB_BUILD_OPTIONS', $rules->pointer($memorized_position)) if @rules and none { m/(DEB_BUILD_OPTIONS|nocheck)/ } @conditionals; } $self->pointed_hint( 'debian-rules-contains-unnecessary-get-orig-source-target', $rules->pointer) if any { m/^\s+uscan\b/ } @{$rules_per_target{'get-orig-source'}}; my @clean_in_indep = grep { $build_indep->satisfies($_) } uniq @needed_clean; $self->pointed_hint( 'missing-build-depends-for-clean-target-in-debian-rules', $rules->pointer, "(does not satisfy $_)") for @clean_in_indep; # another check complains when debhelper is missing from d/rules my $combined_lc = List::Compare->new(\@needed, ['debhelper:any']); my @still_missing = grep { !$build_all_norestriction->satisfies($_) } $combined_lc->get_Lonly; $self->pointed_hint('rules-require-build-prerequisite', $rules->pointer, "(does not satisfy $_)") for @still_missing; $self->pointed_hint('debian-rules-should-not-set-CFLAGS-from-noopt', $rules->pointer) if $contents =~ m{^ ifn?eq \s+ [(] , \$ [(] findstring \s+ noopt , \$ [(] DEB_BUILD_OPTIONS [)] [)] [)] \n+ \t+ CFLAGS \s+ \+ = \s+ -O[02] \n+ else \n+ \t+ CFLAGS \s+ \+ = \s+ -O[02] \n+ endif $}xsm; return; } 1; # Local Variables: # indent-tabs-mode: nil # cperl-indent-level: 4 # End: # vim: syntax=perl sw=4 sts=4 sr et