# rules -- lintian check script -*- perl -*-

# Copyright (C) 2006 Russ Allbery <rra@debian.org>
# Copyright (C) 2005 René van Bevern <rvb@pro-linux.de>
#
# 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::rules;
use strict;
use Tags;
use Util;

# The allowed Python dependencies currently.  This is the list of alternatives
# that, either directly or through transitive dependencies that can be relied
# upon, ensure /usr/bin/python will exist for the use of dh_python.
our $PYTHON_DEPEND = 'python | python-dev | python-all | python-all-dev | '
    . join (' | ', map { "python$_ | python$_-dev" } qw(2.4 2.5));

# 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.
our @GLOBAL_CLEAN_DEPENDS =
    (
     [ ant => qr'^include\s*/usr/share/cdbs/1/rules/ant\.mk' ],
     [ cdbs => qr'^include\s+/usr/share/cdbs/' ],
     [ cdbs => qr'^include\s+/usr/share/R/debian/r-cran\.mk' ],
     [ dbs => qr'^include\s+/usr/share/dbs/' ],
     [ 'dh-make-php' => qr'^include\s+/usr/share/cdbs/1/class/pear\.mk' ],
     [ debhelper => qr'^include\s+/usr/share/cdbs/1/rules/debhelper\.mk' ],
     [ debhelper => qr'^include\s+/usr/share/R/debian/r-cran\.mk' ],
     [ dpatch => qr'^include\s+/usr/share/cdbs/1/rules/dpatch\.mk' ],
     [ 'gnome-pkg-tools' => qr'^include\s+/usr/share/gnome-pkg-tools/' ],
     [ quilt => qr'^include\s+/usr/share/cdbs/1/rules/patchsys-quilt\.mk' ],
     [ dpatch => qr'^include\s+/usr/share/dpatch/' ],
     [ 'mozilla-devscripts' => qr'^include\s+/usr/share/mozilla-devscripts/' ],
     [ quilt => qr'^include\s+/usr/share/quilt/' ],
     [ 'ruby-pkg-tools' => qr'^include\s+/usr/share/ruby-pkg-tools/1/class/' ],
     [ 'r-base-dev' => qr'^include\s+/usr/share/R/debian/r-cran\.mk' ],
     [ $PYTHON_DEPEND => qr'/usr/share/cdbs/1/class/python-distutils\.mk', 'missing-python-build-dependency' ],
    );

# These are similar, but the resulting dependency is only allowed, not
# required.
#
# The xsfclean rule is specific to the packages maintained by the X Strike
# Force, but there are enough of those to make the rule worthwhile.
my @GLOBAL_CLEAN_ALLOWED = (
	[ patchutils => qr'^include\s+/usr/share/cdbs/1/rules/dpatch\.mk' ],
	[ patchutils => qr'^include\s+/usr/share/cdbs/1/rules/patchsys-quilt\.mk' ],
	[ patchutils => qr'^include\s+/usr/share/cdbs/1/rules/simple-patchsys\.mk' ],
	[ 'python-central' => qr'^DEB_PYTHON_SYSTEM\s*:?=\s*pycentral' ],
	[ 'python-support' => qr'^DEB_PYTHON_SYSTEM\s*:?=\s*pysupport' ],
	[ 'python-setuptools' => qr'/usr/share/cdbs/1/class/python-distutils\.mk' ],
	[ quilt => qr'^clean:\s+xsfclean\b' ],
);

# 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 =
    (
     [ $PYTHON_DEPEND => qr'^\t\s*dh_python\s', 'missing-dh_python-build-dependency' ],
     [ 'python-central' => qr'^\t\s*dh_pycentral\s' ],
     [ 'python-support' => qr'^\t\s*dh_pysupport\s' ],
     [ 'python-central' => qr'^DEB_PYTHON_SYSTEM\s*:?=\s*pycentral' ],
     [ 'python-support' => qr'^DEB_PYTHON_SYSTEM\s*:?=\s*pysupport' ],
    );

# 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 => qr'^\t\s*(\S+=\S+\s+)*ant\s' ],
     [ debhelper => qr'^\t\s*dh_.+' ],
     [ dpatch => qr'^\t\s*(\S+=\S+\s+)*dpatch\s' ],
     [ "po-debconf" => qr'^\t\s*debconf-updatepo\s' ],
     [ $PYTHON_DEPEND => qr'^\t\s*python\s', 'missing-python-build-dependency' ],
     [ $PYTHON_DEPEND => qr'\ssetup\.py\b', 'missing-python-build-dependency' ],
     [ 'quilt (>= 0.46-7~)' => qr'^\t\s*dh_quilt_.+' ],
     [ quilt => qr'^\t\s*(\S+=\S+\s+)*quilt\s' ],
     [ yada => qr'^\t\s*yada\s' ],
    );

# Similar, but the resulting dependency is only allowed, not required.  We
# permit a versioned dependency on perl-base because that used to be the
# standard suggested dependency.  No package should be depending on just
# perl-base, since it's Priority: required.
my @RULE_CLEAN_ALLOWED =
    (
     [ patch => q'^\t\s*(?:perl debian/)?yada\s+unpatch' ],
     [ 'perl | perl-base (>= 5.6.0-16)' => qr'(^\t|\|\|)\s*(perl|\$\(PERL\))\s' ],
     [ 'perl-modules (>= 5.10) | libmodule-build-perl' => qr'(^\t|\|\|)\s*(perl|\$\(PERL\))\s+Build\b' ],
     [ 'python-setuptools' => qr'\ssetup\.py\b' ],
    );

# A simple list of regular expressions which, if they match anywhere in
# debian/rules, indicate the requirements for debian/rules clean are complex
# enough that we can't know what packages are permitted in Build-Depends and
# should bypass the build-depends-without-arch-dep check completely.
my @GLOBAL_CLEAN_BYPASS =
    (
     qr'^include\s*/usr/share/cdbs/1/class/ant\.mk',
     qr'^\s+(\S+=\S+\s+)*dh\s+'
    );

# The following targets are required per Policy.
my %required = map { $_ => 1 }
    qw(build binary binary-arch binary-indep clean);

# 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 run {

my $pkg = shift;
my $type = shift;
my $info = shift;

# 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.  Warn if it's a symlink (dpkg-source does as well) and skip
# all the tests if we then can't read it.
if (-l "debfiles/rules") {
    tag "debian-rules-is-symlink", "";
    return 0 unless -f "debfiles/rules";
}

#get architecture field:
unless (-d "fields") {
    fail("directory in lintian laboratory for $type package $pkg missing: fields");
}

my $architecture = $info->field('architecture') || '';

open(RULES, '<', 'debfiles/rules') or fail("Failed opening rules: $!");

# 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.
my $start = <RULES>;
tag "debian-rules-not-a-makefile", ""
    unless $start =~ m%^\#!\s*/usr/bin/make\s+-[re]?f[re]?\s*$%;

# Holds which dependencies are required.  The keys in %needed and
# %needed_clean are the dependencies; the values are the tags to use or the
# empty string to use the default tag.
my (%needed, %needed_clean, %allowed_clean, $bypass_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.
my $includes = 0;
my %seen;
local $_;
my @arch_rules = (qr/^clean$/, qr/^binary-arch$/, qr/^build-arch$/);
my @current_targets;
my %rules_per_target;
my %debhelper_group;
my $maybe_skipping;
while (<RULES>) {
    next if /^\s*\#/;
    $includes = 1 if m/^ *[s-]?include\s+/;

    # 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.
    if (/^\s*(export\s+)?DH_COMPAT\s*:?=/ && keys(%seen) == 0) {
        tag "debian-rules-sets-DH_COMPAT", "line $.";
    }

    # Check for problems that can occur anywhere in debian/rules.
    if (/\$[\(\{]PWD[\)\}]/) {
        tag "debian-rules-uses-pwd", "line $.";
    }
    if (/^\t\s*-(?:\$[\(\{]MAKE[\}\)]|make)\s.*(?:dist)?clean/ ||
	/^\t\s*(?:\$[\(\{]MAKE[\}\)]|make)\s(?:.*\s)?-\w*i.*(?:dist)?clean/) {
        tag "debian-rules-ignores-make-clean-error", "line $.";
    }
    if (/\$[\(\{]DEB_BUILD_OPTS[\)\}]/) {
        tag "debian-rules-uses-DEB_BUILD_OPTS", "line $.";
    }

    # Keep track of whether this portion of debian/rules may be optional
    if (/^ifn?(eq|def)\s/) {
        $maybe_skipping++;
    } elsif (/^endif\s/) {
        $maybe_skipping--;
    }

    # Check for strings anywhere in debian/rules that have implications for
    # our dependencies.
    for my $rule (@GLOBAL_CLEAN_DEPENDS) {
        if (/$rule->[1]/) {
            if ($maybe_skipping) {
                $allowed_clean{$rule->[0]} = 1;
            } else {
                $needed_clean{$rule->[0]}
                    = $rule->[2] || $needed_clean{$rule->[0]} || '';
            }
        }
    }
    for my $rule (@GLOBAL_CLEAN_ALLOWED) {
        if (/$rule->[1]/) {
            $allowed_clean{$rule->[0]} = 1;
        }
    }
    for my $rule (@GLOBAL_CLEAN_BYPASS) {
        if (/$rule/) {
            $bypass_needed_clean = 1;
        }
    }
    for my $rule (@GLOBAL_DEPENDS) {
        if (/$rule->[1]/ && !$maybe_skipping) {
            $needed{$rule->[0]} = $rule->[2] || $needed{$rule->[0]} || '';
        }
    }

    # 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 (/^(?:[^:]+\s)?\.PHONY(?:\s[^:]+)?:(.+)/) {
        my @targets = split (' ', $1);
        for (@targets) {
            $seen{$_}++ if $required{$_};
        }
    }

    if (!/^ifn?(eq|def)\s/ && /^([^\s:][^:]*):+(.*)/) {
	@current_targets = split (' ', $1);
        my @depends = map {
            $_ = quotemeta $_;
            s/\\\$\\\([^\):]+\\:([^=]+)\\=([^\)]+)\1\\\)/$2.*/g;
            qr/^$_$/;
        } split (' ', $2);
	for my $target (@current_targets) {
            if ($target =~ /%/) {
                my $pattern = quotemeta $target;
                $pattern =~ s/\\%/.*/g;
                for my $required (keys %required) {
                    $seen{$required}++ if $required =~ m/$pattern/;
                }
            } else {
                $seen{$target}++ if $required{$target};
            }
            if (grep { $target =~ /$_/ } @arch_rules) {
                push (@arch_rules, @depends);
            }
	}
        undef %debhelper_group;
    } elsif (/^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 (m/^\s+[^\#]/) {
            my $arch = 0;
            for my $target (@current_targets) {
                $rules_per_target{$target} ||= [];
                push @{$rules_per_target{$target}}, $_;
                $arch = 1 if (grep { $target =~ /$_/ } @arch_rules);
            }
            if ($arch) {
                for my $rule (@RULE_CLEAN_DEPENDS) {
                    if (/$rule->[1]/) {
                        if ($maybe_skipping) {
                            $allowed_clean{$rule->[0]} = 1;
                        } else {
                            $needed_clean{$rule->[0]}
                                = $rule->[2] || $needed_clean{$rule->[0]} || '';
                        }
                    }
                }
                for my $rule (@RULE_CLEAN_ALLOWED) {
                    if (/$rule->[1]/) {
                        $allowed_clean{$rule->[0]} = 1;
                    }
                }
	    }
            if (m/^\s+(dh_\S+)\b/ and $debhelper_order{$1}) {
                my $command = $1;
                my ($package) = /\s(?:-p|--package=)(\S+)/;
                $package ||= '';
                my $group = $debhelper_order{$command};
                $debhelper_group{$package} ||= 0;
                if ($group < $debhelper_group{$package}) {
                    tag "debian-rules-calls-debhelper-in-odd-order",
                        $command, "(line $.)";
                } else {
                    $debhelper_group{$package} = $group;
                }
            }
	}
    }
}
close RULES;

unless ($includes) {
    # Make sure all the required rules were seen.
    for my $target (sort keys %required) {
        tag "debian-rules-missing-required-target", $target
            unless $seen{$target};
    }
}

# 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;
    foreach (@{$rules_per_target{'binary-arch'}}) {
        # dh binary-arch is actually a no-op if there is no
        # Architecture: any package in the control file
        unless (m/^\s*dh\s+(?:binary-arch|\$\@)/) {
            $nonempty = 1;
        }
    }
    tag "binary-arch-rules-but-pkg-is-arch-indep" if $nonempty;
}

# Make sure that all the required build dependencies are there.	 Don't
# issue missing-build-dependency errors for debhelper, since there's
# another test that does that and it would just be a duplicate.
my $build_regular = $info->relation('build-depends');
my $build_indep   = $info->relation('build-depends-indep');
for my $package (keys %needed_clean) {
    my $tag = $needed_clean{$package} || 'missing-build-dependency';
    unless ($build_regular->implies($package)) {
        if ($build_indep->implies($package)) {
            tag "clean-should-be-satisfied-by-build-depends", $package;
        } else {
            if ($tag eq 'missing-build-dependency') {
                tag $tag, $package if $package ne 'debhelper';
            } else {
                tag $tag;
            }
        }
    }
}
my $noarch = $info->relation_noarch('build-depends-all');
for my $package (keys %needed) {
    my $tag = $needed{$package} || 'missing-build-dependency';

    # dh_python deactivates itself if the new Python build policy is enabled.
    if ($tag eq 'missing-dh_python-build-dependency') {
        next if -f 'debfiles/pycomat';
        next if defined $info->field('python-version');
    }
    unless ($noarch->implies($package)) {
        if ($tag eq 'missing-build-dependency') {
            tag $tag, $package;
        } else {
            tag $tag;
        }
    }
}

# This check is a bit tricky.  We want to allow in Build-Depends a dependency
# with any version, since reporting this tag over version mismatches would be
# confusing and quite likely wrong.  The approach taken is to strip the
# version information off all dependencies allowed in Build-Depends, strip the
# version information off of the dependencies in Build-Depends, and then allow
# any dependency in Build-Depends that's implied by the dependencies we
# require or allow there.
#
# We also have to map | to , when building the list of allowed packages so
# that the implications will work properly.
#
# This is confusing.  There should be a better way to do this.
my $arch_dep_packages = 0;
for my $binpkg (keys %{ $info->binaries } ) {
    my $arch = $info->binary_field($binpkg, 'architecture');
    $arch_dep_packages++ unless ($arch eq 'all');
}
if (defined $info->field('build-depends') and $arch_dep_packages == 0
    and not $bypass_needed_clean) {
    my $build_depends = $info->field('build-depends');
    my @packages = split /\s*,\s*/, $build_depends;
    my @allowed = map {
        s/\([^\)]+\)//g;
        s/\|/,/g;
        $_
    } keys (%needed_clean), keys (%allowed_clean);
    my $dep = Lintian::Relation->new_noarch(join(',', @allowed));
    for my $pkg (@packages) {
        my $name = $pkg;
        $name =~ s/[\[\(][^\)\]]+[\)\]]//g;
        $name =~ s/\s+$//;
        $name =~ s/\s+/ /g;
        unless ($dep->implies($name)) {
            tag "build-depends-without-arch-dep", $name;
        }
    }
}

}

1;

# Local Variables:
# indent-tabs-mode: nil
# cperl-indent-level: 4
# End:
# vim: syntax=perl sw=4 sts=4 ts=4 et shiftround
