michael@428: ##
michael@428: ## lint-spec.pl -- OpenPKG *.spec File Checker
michael@428: ## Copyright (c) 2000-2012 OpenPKG GmbH
michael@428: ##
michael@428: ## This software is property of the OpenPKG GmbH, DE MUC HRB 160208.
michael@428: ## All rights reserved. Licenses which grant limited permission to use,
michael@428: ## copy, modify and distribute this software are available from the
michael@428: ## OpenPKG GmbH.
michael@428: ##
michael@428: ## THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
michael@428: ## WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
michael@428: ## MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
michael@428: ## IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR
michael@428: ## CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
michael@428: ## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
michael@428: ## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
michael@428: ## USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
michael@428: ## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
michael@428: ## OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
michael@428: ## OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
michael@428: ## SUCH DAMAGE.
michael@428: ##
michael@428:
michael@428: # Perl run-time requirement
michael@428: require 5;
michael@428: BEGIN {
michael@428: eval "use Getopt::Long; use IO;";
michael@428: if ($@) {
michael@428: print STDERR
michael@428: "lint-spec: ERROR: This command requires a full-size Perl installation!\n" .
michael@428: "lint-spec: HINT: Install OpenPKG \"perl\" package to use this command.\n";
michael@428: exit(1);
michael@428: }
michael@428: }
michael@428:
michael@428: # OpenPKG instance prefix
michael@428: my $my_prefix = $ENV{'OPENPKG_PREFIX'};
michael@428: delete $ENV{'OPENPKG_PREFIX'};
michael@428:
michael@428: # program information
michael@428: my $progname = "lint-spec";
michael@428: my $progvers = "1.0.0";
michael@428:
michael@428: # parameters (defaults)
michael@428: my $version = 0;
michael@428: my $verbose = 0;
michael@428: my $help = 0;
michael@428: my $check = 'all';
michael@428: my $tmpdir = ($ENV{TMPDIR} || $ENV{TEMPDIR} || "/tmp") . "/$progname";
michael@428: my $rpm = "$my_prefix/bin/openpkg rpm";
michael@428:
michael@428: # exception handling support
michael@428: $SIG{__DIE__} = sub {
michael@428: my ($err) = @_;
michael@428: $err =~ s|\s+at\s+.*||s if (not $verbose);
michael@428: print STDERR "$progname:ERROR: $err ". ($! ? "($!)" : "") . "\n";
michael@428: exit(1);
michael@428: };
michael@428:
michael@428: # command line parsing
michael@428: Getopt::Long::Configure("bundling");
michael@428: my $result = GetOptions(
michael@428: 'V|version' => \$version,
michael@428: 'v|verbose' => \$verbose,
michael@428: 'h|help' => \$help,
michael@428: 'c|check=s' => \$check,
michael@428: 't|tmpdir=s' => \$tmpdir,
michael@428: 'r|rpm=s' => \$rpm,
michael@428: ) || die "option parsing failed";
michael@428: if ($help) {
michael@428: print "Usage: $progname [options] [SPECFILE ...]\n" .
michael@428: "Available options:\n" .
michael@428: " -v,--verbose enable verbose run-time mode\n" .
michael@428: " -h,--help print out this usage page\n" .
michael@428: " -c,--check=CHECKS select checks to perform (default='all')\n" .
michael@428: " -r,--rpm=FILE filesystem path to RPM program\n" .
michael@428: " -t,--tmpdir=PATH filesystem path to temporary directory\n" .
michael@428: " -V,--version print program version\n";
michael@428: exit(0);
michael@428: }
michael@428: if ($version) {
michael@428: print "OpenPKG $progname $progvers\n";
michael@428: exit(0);
michael@428: }
michael@428:
michael@428: # verbose message printing
michael@428: sub msg_verbose {
michael@428: my ($msg) = @_;
michael@428: print STDERR "$msg\n" if ($verbose);
michael@428: }
michael@428:
michael@428: # warning message printing
michael@428: sub msg_warning {
michael@428: my ($msg) = @_;
michael@428: print STDERR "$progname:WARNING: $msg\n";
michael@428: }
michael@428:
michael@428: # error message printing
michael@428: sub msg_error {
michael@428: my ($msg) = @_;
michael@428: print STDERR "$progname:ERROR: $msg\n";
michael@428: }
michael@428:
michael@428: # determine check list
michael@428: my @check_list = (qw(
michael@428: blank
michael@428: comment
michael@428: license
michael@428: header
michael@428: section
michael@428: preproc
michael@428: script
michael@428: global
michael@428: sources
michael@428: digest
michael@428: ));
michael@428: my @checks = ();
michael@428: if ($check eq 'all') {
michael@428: @checks = @check_list;
michael@428: }
michael@428: else {
michael@428: foreach my $c (split(/,/, $check)) {
michael@428: if (not grep($c, @check_list)) {
michael@428: die "invalid check \"$c\"";
michael@428: }
michael@428: push(@checks, $c);
michael@428: }
michael@428: }
michael@428:
michael@428: # global return code
michael@428: $main::GRC = 0;
michael@428:
michael@428: # iterate over all .spec files
michael@428: foreach my $filename (@ARGV) {
michael@428: my $io = new IO::File "<$filename"
michael@428: or die "unable to open file \"$filename\" for reading";
michael@428: my $spec; { local $/ = undef; $spec = <$io>; }
michael@428: $io->close;
michael@428: foreach my $check (@checks) {
michael@428: eval "\&check_$check(\$filename, \$spec);";
michael@428: }
michael@428: }
michael@428:
michael@428: # die gracefully
michael@428: exit($main::GRC);
michael@428:
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428: ## COMMON SUBROUTINES
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428:
michael@428: sub lines {
michael@428: my ($txt) = @_;
michael@428: my $l = 0;
michael@428: $txt =~ s|\n|$l++, ''|sge;
michael@428: return $l;
michael@428: }
michael@428:
michael@428: sub lint_message {
michael@428: my ($type, $file, $done, $this, $msg) = @_;
michael@428: if (defined($done) and defined($this)) {
michael@428: my $start = &lines($done) + 1;
michael@428: my $end = $start + &lines($this);
michael@428: my $pos = $start;
michael@428: $pos .= "-". $end if ($end > $start);
michael@428: printf("%s:%s: %s:%s: %s\n", $progname, $type, $file, $pos, $msg);
michael@428: }
michael@428: else {
michael@428: printf("%s:%s: %s: %s\n", $progname, $type, $file, $msg);
michael@428: }
michael@428: }
michael@428:
michael@428: sub lint_warning {
michael@428: my ($file, $done, $this, $msg) = @_;
michael@428: &lint_message("WARNING", $file, $done, $this, $msg);
michael@428: $main::GRC = 1 if ($main::GRC < 1);
michael@428: }
michael@428:
michael@428: sub lint_error {
michael@428: my ($file, $done, $this, $msg) = @_;
michael@428: &lint_message("ERROR", $file, $done, $this, $msg);
michael@428: $main::GRC = 2 if ($main::GRC < 2);
michael@428: }
michael@428:
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428: ## CHECK "blank": whitespace and blank lines
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428:
michael@428: sub check_blank {
michael@428: my ($file, $spec) = @_;
michael@428:
michael@428: # check for CR-LF combination
michael@428: my $done = ''; my $this = ''; my $todo = $spec;
michael@428: while ($todo =~ m/\r\n/s) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: &lint_warning($file, $done, $this, "carriage-return (CR, 0x0d) line-feed (NL, 0x0a) combination (expected just line-feed)");
michael@428: $done .= $this;
michael@428: }
michael@428:
michael@428: # check for multiple blank lines
michael@428: $done = ''; $this = ''; $todo = $spec;
michael@428: while ($todo =~ m/(\r?\n[ \t]*){3,}/s) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: &lint_warning($file, $done, $this, "multiple subsequent blank lines (expected single blank line)");
michael@428: $done .= $this;
michael@428: }
michael@428:
michael@428: # check for trailing whitespaces
michael@428: $done = ''; $this = ''; $todo = $spec;
michael@428: while ($todo =~ m/[ \t]+\r?\n/s) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: if ($done eq '' or $done =~ m|\n$|s) {
michael@428: &lint_warning($file, $done, $this, "whitespace on empty line (expected none)");
michael@428: }
michael@428: else {
michael@428: &lint_warning($file, $done, $this, "trailing whitespace (expected none)");
michael@428: }
michael@428: $done .= $this;
michael@428: }
michael@428:
michael@428: # check for bogus line continuations
michael@428: $done = ''; $this = ''; $todo = $spec;
michael@428: while ($todo =~ m/\\[ \t]*\r?\n(?=[ \t]*\r?\n)/s) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: &lint_warning($file, $done, $this, "bogus line continuation for following empty line (expect no line continuation)");
michael@428: $done .= $this;
michael@428: }
michael@428:
michael@428: # check for leading whitespaces before line continuations
michael@428: $done = ''; $this = ''; $todo = $spec;
michael@428: while ($todo =~ m/[ \t]{2,}\\[ \t]*\r?\n/s) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: &lint_warning($file, $done, $this, "multiple leading whitespace before line continuation (expected just a single space)");
michael@428: $done .= $this;
michael@428: }
michael@428:
michael@428: # check for leading tabs
michael@428: $done = ''; $this = ''; $todo = $spec;
michael@428: while ($todo =~ m/^ *\t+ *[^ \t]/m) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: &lint_warning($file, $done, $this, "leading tabs (expected spaces)");
michael@428: $done .= $this;
michael@428: }
michael@428:
michael@428: # check for mandatory/wished trailing blank line
michael@428: if ($spec !~ m|\n\n$|) {
michael@428: &lint_warning($file, $spec, "", "mandatory/wished trailing blank line missing (expected one)");
michael@428: }
michael@428: }
michael@428:
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428: ## CHECK "comment": sharp-comments
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428:
michael@428: sub check_comment {
michael@428: my ($file, $spec) = @_;
michael@428:
michael@428: # check for comment indentation and contents
michael@428: my $done = ''; my $this = ''; my $todo = $spec;
michael@428: while ($todo =~ m/^([ \t]*)(#+)([ \t]*)(.*?)$/m) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: my ($lead, $sharp, $pad, $text) = ($1, $2, $3, $4);
michael@428: if (length($lead) % 2 != 0) {
michael@428: &lint_warning($file, $done, $this, "incorrect comment indentation (expected a multiple of 2 spaces)");
michael@428: }
michael@428: if (length($lead) > 1 && length($sharp) > 1) {
michael@428: &lint_warning($file, $done, $this, "indented comment has introduced with multiple sharps (expected single sharp character)");
michael@428: }
michael@428: if (length($pad.$text) > 0 && length($sharp.$pad) % 4 != 0) {
michael@428: &lint_warning($file, $done, $this, "incorrect comment text padding (expected a multiple of 4 sharps or spaces)");
michael@428: }
michael@428: if (length($pad) == 0 && length($text) > 0) {
michael@428: &lint_warning($file, $done, $this, "missing leading space before comment text (expected padding spaces)");
michael@428: }
michael@428: if (length($pad) > 0 && length($text) == 0) {
michael@428: &lint_warning($file, $done, $this, "empty comment text (expected a reasonable text)");
michael@428: }
michael@428: $done .= $this;
michael@428: }
michael@428:
michael@428: # check for comment contents only
michael@428: if ($file !~ m|openpkg\.spec$|) {
michael@428: $done = ''; $this = ''; $todo = $spec;
michael@428: while ($todo =~ m/\n\n[ \t]*#[ \t]+([^\n]+)(?!\n([ \t]*#))/s) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: my $text = $1;
michael@428: if (length($text) > 0 and $text =~ m|^[A-Z][^A-Z]|) {
michael@428: &lint_warning($file, $done, $this, "comment text starts with upper-case letter (expected lower-case letter)");
michael@428: }
michael@428: $done .= $this;
michael@428: }
michael@428: }
michael@428: }
michael@428:
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428: ## CHECK "license": license header
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428:
michael@428: sub check_license {
michael@428: my ($file, $spec) = @_;
michael@428:
michael@428: my $name = "[a-z][a-z0-9-]*";
michael@428: if ($file =~ m|^.*/([a-z][a-z0-9-]*)\.spec$|) {
michael@428: $name = $1;
michael@428: }
michael@428: elsif ($file =~ m|^([a-z][a-z0-9-]*)\.spec$|) {
michael@428: $name = $1;
michael@428: }
michael@428: my $re = "";
michael@428: $re .= "##\\n";
michael@428: $re .= "## $name\\.spec -- OpenPKG RPM Package Specification\\n";
michael@428: $re .= "## Copyright \\(c\\) 200[0-9]-2012 OpenPKG Foundation e\.V\. \\n";
michael@428: $re .= "##\\n";
michael@428: $re .= "## Permission to use, copy, modify, and distribute this software for\\n";
michael@428: $re .= "## any purpose with or without fee is hereby granted, provided that\\n";
michael@428: $re .= "## the above copyright notice and this permission notice appear in all\\n";
michael@428: $re .= "## copies\\.\\n";
michael@428: $re .= "##\\n";
michael@428: $re .= "## THIS SOFTWARE IS PROVIDED \\`\\`AS IS'' AND ANY EXPRESSED OR IMPLIED\\n";
michael@428: $re .= "## WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\\n";
michael@428: $re .= "## MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED\\.\\n";
michael@428: $re .= "## IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR\\n";
michael@428: $re .= "## CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\\n";
michael@428: $re .= "## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES \\(INCLUDING, BUT NOT\\n";
michael@428: $re .= "## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF\\n";
michael@428: $re .= "## USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION\\) HOWEVER CAUSED AND\\n";
michael@428: $re .= "## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\\n";
michael@428: $re .= "## OR TORT \\(INCLUDING NEGLIGENCE OR OTHERWISE\\) ARISING IN ANY WAY OUT\\n";
michael@428: $re .= "## OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF\\n";
michael@428: $re .= "## SUCH DAMAGE\\.\\n";
michael@428: $re .= "##\\n";
michael@428: if ($name ne "openpkg" and $spec !~ m|^$re|os) {
michael@428: &lint_warning($file, "", "", "invalid license header");
michael@428: }
michael@428: }
michael@428:
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428: ## CHECK "header": RPM headers
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428:
michael@428: sub check_header {
michael@428: my ($file, $spec) = @_;
michael@428:
michael@428: my @headers = (qw(
michael@428: m:Name:^[a-z][a-z0-9-]*$
michael@428: m:Summary:^[A-Z]\S*(\s+([A-Z]\S*|of|for|from|in|at|on|\(\S+\)))*
michael@428: m:URL:^((https?|ftp)://.+|-)$
michael@428: m:Vendor:.+
michael@428: m:Packager:^(OpenPKG\sFoundation\se\.V\.|OpenPKG\sGmbH)$
michael@428: m:Distribution:^(OpenPKG|OpenPKG\sCommunity|OpenPKG\sEnterprise)$
michael@428: m:Class:^(BOOT|CORE|BASE|PLUS|EVAL|JUNK|PRIV)$
michael@428: m:Group:^[A-Z][a-zA-Z0-9]+$
michael@428: m:License:.+
michael@428: m:Version:^[^-]+$
michael@428: m:Release:^(E?([1-9]\.)?20[0-9][0-9](0[1-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[01])|E?[1-9]\.[0-9]\.\d+|%\{[^\}]+\})$
michael@428: o:Source\d+:^((https?|ftp)://.+|[^/]+)$
michael@428: o:Patch\d+:^((https?|ftp)://.+|[^/]+)$
michael@428: o:Prefix:^%{l_prefix}$
michael@428: o:BuildRoot:^%{l_buildroot}$
michael@428: m:BuildPreReq:^(((,\s+)?digest\\(sha1:\%\{(SOURCE|PATCH)\d+\}\\)\s+=\s+[0-9a-fA-F]+)+|(OpenPKG,\sopenpkg\s>=\s\S+)?((,\s)?([a-z][a-z0-9-]*(\:\:with_[a-z][a-z0-9_]+)?|[A-Z][A-Z0-9-]*)(\s(>=?|==?|<=?|!=)\s\S+)?)+)$
michael@428: m:PreReq:^(OpenPKG,\sopenpkg\s>=\s\S+)?((,\s)?([a-z][a-z0-9-]*(\:\:with_[a-z][a-z0-9_]+)?|[A-Z][A-Z0-9-]*)(\s(>=?|==?|<=?|!=)\s\S+)?)+$
michael@428: o:AutoReq:^no$
michael@428: o:AutoReqProv:^no$
michael@428: o:Provides:^((,\s)?([a-z][a-z0-9-]*(\:\:with_[a-z][a-z0-9_]+)?|[A-Z][A-Z0-9-]*)(\s==?\s\S+)?)+$
michael@428: o:Conflicts:^((,\s)?([a-z][a-z0-9-]*(\:\:with_[a-z][a-z0-9_]+)?|[A-Z][A-Z0-9-]*)(\s(>=?|==?|<=?|!=)\s\S+)?)+$
michael@428: ));
michael@428: my @headers_def = (qw(
michael@428: Summary:^Badly\sPackaged\sProgram$
michael@428: Vendor:^John\sDoe$
michael@428: Group:^Unknown$
michael@428: License:^DoePL$
michael@428: ));
michael@428: my @headers_mult = (qw(
michael@428: BuildPreReq PreReq Provides Conflicts
michael@428: ));
michael@428:
michael@428: my @seen = ();
michael@428: my %count = ();
michael@428: my $done = ''; my $this = ''; my $todo = $spec;
michael@428: while ($todo =~ m/^(\S+):([ \t]*)(.*?)$/m) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: my ($header, $pad, $value) = ($1, $2, $3);
michael@428:
michael@428: # check for layouting
michael@428: if (length($value) == 0) {
michael@428: &lint_error($file, $done, $this, "empty RPM header value");
michael@428: }
michael@428: if (length($header.":".$pad) != 14) {
michael@428: &lint_warning($file, $done, $this, "invalid RPM header name/value padding (expected value at column 15)");
michael@428: }
michael@428:
michael@428: # check for valid header name and value
michael@428: if (not grep { $header =~ m|^$_$|s } map { m/^[^:]:([^:]+):/, $1 } @headers) {
michael@428: &lint_error($file, $done, $this, "invalid RPM header name \"$header\"");
michael@428: }
michael@428: else {
michael@428: my $hn = quotemeta((grep { $header =~ m|^$_$|s } map { m/^[^:]+:([^:]+):/, $1 } @headers)[0]);
michael@428: my $re = (map { m/^[^:]+:${hn}:(.+)$/s } @headers)[0];
michael@428: my $re_match = $re;
michael@428: $re_match =~ s|^\(|(?:|sg;
michael@428: $re_match =~ s|([^\\])\(|\1(?:|sg;
michael@428: if ( not ($file =~ m|openpkg\.spec$| and $header eq 'Provides' and $value eq '%{l_prefix}')
michael@428: and $value !~ m|${re_match}|s) {
michael@428: &lint_warning($file, $done, $this, "RPM header \"$header\": " .
michael@428: "invalid value \"$value\" (expected to match \"$re\")");
michael@428: }
michael@428: my $re_def = (map { m/^${hn}:(.+)$/s } @headers_def)[0];
michael@428: if (defined($re_def)) {
michael@428: my $re_def_match = $re_def;
michael@428: $re_def_match =~ s|^\(|(?:|sg;
michael@428: $re_def_match =~ s|([^\\])\(|\1(?:|sg;
michael@428: if ($value =~ m|${re_def_match}|s) {
michael@428: &lint_warning($file, $done, $this, "RPM header \"$header\": " .
michael@428: "default value \"$value\" (expected to match \"$re\", except for this)");
michael@428: }
michael@428: }
michael@428: if ($header =~ m/^Vendor/ and $value =~ m/et\sal/ and $value !~ m/et\sal\./) {
michael@428: &lint_warning($file, $done, $this, "RPM header \"$header\": " .
michael@428: "has value \"$value\" (expected \"et al.\" with dot)");
michael@428: }
michael@428: }
michael@428: push(@seen, $header);
michael@428: $count{$header}++;
michael@428: $done .= $this;
michael@428: }
michael@428:
michael@428: # check for existence of mandatory headers
michael@428: foreach my $header (@headers) {
michael@428: my ($type, $name, $regex) = split(/:/, $header, 3);
michael@428: if ($type eq 'm') {
michael@428: if ($file =~ m|openpkg\.spec$| and
michael@428: ($name eq "BuildPreReq" or $name eq "PreReq")) {
michael@428: # the bootstrap package is an obvious exception
michael@428: next;
michael@428: }
michael@428: if (not grep(/^$name$/, @seen)) {
michael@428: &lint_warning($file, undef, undef, "mandatory RPM header \"$name\" not found");
michael@428: }
michael@428: }
michael@428: }
michael@428:
michael@428: # check for multiple occurrence headers
michael@428: foreach my $seen (@seen) {
michael@428: if ($count{$seen} > 1 and not (grep { $_ eq $seen } @headers_mult)) {
michael@428: &lint_error($file, undef, undef, "RPM header \"$seen\" occurs multiple times (expected just once)");
michael@428: }
michael@428: }
michael@428: }
michael@428:
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428: ## CHECK "section": RPM sections
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428:
michael@428: sub check_section {
michael@428: my ($file, $spec) = @_;
michael@428:
michael@428: my $require = qq{
michael@428: (%define,)*
michael@428: Name:,
michael@428: Summary:,
michael@428: URL:,
michael@428: Vendor:,
michael@428: Packager:,
michael@428: Distribution:,
michael@428: Class:,
michael@428: Group:,
michael@428: License:,
michael@428: Version:,
michael@428: Release:,
michael@428: (%option,)*
michael@428: (%define,|%undefine,)*
michael@428: (Source\\d+:,)*
michael@428: (Patch\\d+:,)*
michael@428: (%NoSource,)*
michael@428: (%NoPatch,)*
michael@428: (Prefix:,)?
michael@428: (BuildRoot:,)?
michael@428: ((BuildPreReq:,)
michael@428: |(PreReq:,))*
michael@428: (AutoReq:,)?
michael@428: (AutoReqProv:,)?
michael@428: (Provides:,)*
michael@428: (Conflicts:,)*
michael@428: %description,
michael@428: (%track,)?
michael@428: %prep,
michael@428: %build,
michael@428: %install,
michael@428: (%check,)?
michael@428: %files,
michael@428: %clean,
michael@428: (%pre,)?
michael@428: (%post,)?
michael@428: (%preun,)?
michael@428: (%postun,)?
michael@428: (%trigger,)?
michael@428: (%triggerin,)?
michael@428: (%triggerun,)?
michael@428: (%triggerpostun,)?
michael@428: (%verifyscript,)?
michael@428: };
michael@428:
michael@428: # check for order of headers
michael@428: my $sections = "";
michael@428: my $done = ''; my $this = ''; my $todo = $spec;
michael@428: while ($todo =~ m/^(\S+:|%\S+).*$/m) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: my $section = $1;
michael@428: next if ($section =~ m/^%(if|else|endif)/);
michael@428: $sections .= "$section,";
michael@428: $done .= $this;
michael@428: }
michael@428: my $regex = $require;
michael@428: $regex =~ s|\s+||sg;
michael@428: if ($sections !~ m/^$regex$/s) {
michael@428: $regex =~ s|,| |sg;
michael@428: &lint_error($file, undef, undef, "invalid RPM section order: $sections (expected \"$regex\")");
michael@428: }
michael@428: }
michael@428:
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428: ## CHECK "preproc": RPM macro pre-processor
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428:
michael@428: sub check_preproc {
michael@428: my ($file, $spec) = @_;
michael@428:
michael@428: my $done = ''; my $this = ''; my $todo = $spec;
michael@428: while ($todo =~ m/^(%(?:if|else|endif|define|undefine|option))(.*)$/m) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: my ($cmd, $args) = ($1, $2);
michael@428: if ($cmd eq '%if') {
michael@428: # FIXME: either try to really parse the boolean expression
michael@428: # FIXME: or at least try to guess its correct syntax
michael@428: }
michael@428: elsif ($cmd eq '%else' or $cmd eq '%endif') {
michael@428: if (length($args) > 0) {
michael@428: &lint_warning($file, $done, $this, "garbage after pre-processor directive " .
michael@428: "\"$cmd\" (expected no arguments)");
michael@428: }
michael@428: }
michael@428: elsif ($cmd eq '%undefine') {
michael@428: if ($args =~ m|^\s+(\S+)\s*$|) {
michael@428: my $var = $1;
michael@428: if ($var !~ m/^(V|with)_[a-z][a-zA-Z0-9_]*$/) {
michael@428: &lint_warning($file, $done, $this, "unusually named macro: \"$var\" " .
michael@428: "(expected \"(V|with)_[a-z][a-zA-Z0-9_]*\")");
michael@428: next;
michael@428: }
michael@428: }
michael@428: else {
michael@428: &lint_error($file, $done, $this, "invalid number of arguments to pre-processor " .
michael@428: "directive \"$cmd\" (expected exactly 1 argument)");
michael@428: }
michael@428: }
michael@428: elsif ($cmd eq '%define' or $cmd eq '%option') {
michael@428: if ($args =~ m|^\s+(\S+)\s+(.*)$|) {
michael@428: my ($var, $val) = ($1, $2);
michael@428: if ($var !~ m/^(V|with)_[a-z][a-zA-Z0-9_]*$/) {
michael@428: &lint_warning($file, $done, $this, "unusually named macro: \"$var\" " .
michael@428: "(expected \"(V|with)_[a-z][a-zA-Z0-9_]*\")");
michael@428: next;
michael@428: }
michael@428: if (length($val) == 0) {
michael@428: &lint_error($file, $done, $this, "empty macro value");
michael@428: }
michael@428: }
michael@428: else {
michael@428: &lint_error($file, $done, $this, "invalid number of arguments to pre-processor " .
michael@428: "directive \"$cmd\" (expected exactly 2 arguments)");
michael@428: }
michael@428: }
michael@428: $done .= $this;
michael@428: }
michael@428:
michael@428: # check correct if/endif nesting
michael@428: my @stack = ();
michael@428: $done = ''; $this = ''; $todo = $spec;
michael@428: while ($todo =~ m/^(%(?:if|else|endif|define|undefine|option)).*$/m) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: my $directive = $1;
michael@428: if ($directive eq '%if') {
michael@428: push(@stack, &lines($done . $this));
michael@428: }
michael@428: elsif ($directive eq '%endif') {
michael@428: if (@stack == 0) {
michael@428: &lint_error($file, $done, $this, "found \%endif without corresponding opening \%if");
michael@428: last;
michael@428: }
michael@428: pop(@stack);
michael@428: }
michael@428: $done .= $this;
michael@428: }
michael@428: my $line;
michael@428: while (defined($line = pop(@stack))) {
michael@428: &lint_error($file, undef, undef, "\%if at line $line never closed by \%endif");
michael@428: }
michael@428:
michael@428: # check for indented preprocessor constructs
michael@428: $done = ''; $this = ''; $todo = $spec;
michael@428: while ($todo =~ m/^[ \t]+(%(?:if|else|endif|define|undefine|option)).*$/m) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: my $directive = $1;
michael@428: &lint_error($file, $done, $this, "found indented \"$directive\" preprocessor directive (expected no indentation)");
michael@428: $done .= $this;
michael@428: }
michael@428: }
michael@428:
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428: ## CHECK "script": shell scripts
michael@428: ## _________________________________________________________________
michael@428: ##
michael@428:
michael@428: sub check_script {
michael@428: my ($file, $spec) = @_;
michael@428:
michael@428: my $done = ''; my $this = ''; my $todo = $spec;
michael@428: while ($todo =~ m/(\%(?:description|prep|build|install|check|files|clean|pre|post|preun|postun|trigger|triggerin|triggerun|triggerpostun|verifyscript))([^\n]*)\n(.*?\n)(?=\%(?:description|prep|build|install|check|files|clean|pre|post|preun|postun|trigger|triggerin|triggerun|triggerpostun|verifyscript)|$)/s) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: my ($section, $args, $script) = ($1, $2, $3);
michael@428:
michael@428: # perform checks for a single script section
michael@428: &check_script_section($file, $done, $this, $section, $args, $script);
michael@428:
michael@428: $done .= $this;
michael@428: }
michael@428: }
michael@428:
michael@428: sub check_script_section {
michael@428: my ($file, $outer_done, $outer_this, $section, $args, $script) = @_;
michael@428:
michael@428: # skip plain-text/non-scripting section %description
michael@428: return if ($section eq '%description');
michael@428:
michael@428: # remove comment contents
michael@428: $outer_this =~ s|^[ \t]*#[^\n]*||mg;
michael@428:
michael@428: # check shell redirections
michael@428: my $done = $outer_done; my $this = ''; my $todo = $outer_this;
michael@428: while ( $todo =~ m/[ \t]+(\d+)?[><][ \t]+\S+/s
michael@428: or $todo =~ m/[ \t]+[><](\&\d+)?[ \t]+\S+/s) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: &lint_warning($file, $done, $this, "section $section: whitespace after shell redirection (expected none)");
michael@428: $done .= $this;
michael@428: }
michael@428:
michael@428: # utility function: extract a single shell command
michael@428: sub command_extract {
michael@428: my ($script) = @_;
michael@428: my $cmd = '';
michael@428: while ($script ne '') {
michael@428: $script =~ s/^([ \t]*'[^']*')/ $cmd .= $1, ''/se && next;
michael@428: $script =~ s/^([ \t]*"[^"]*")/ $cmd .= $1, ''/se && next;
michael@428: $script =~ s/^([ \t]*[^ \t;\)\\\r\n]+)/$cmd .= $1, ''/se && next;
michael@428: $script =~ s/^([ \t]*\\[ \t]*\r?\n)/ $cmd .= $1, ''/se && next;
michael@428: last;
michael@428: }
michael@428: return ($cmd, $script);
michael@428: }
michael@428:
michael@428: # utility function: join a multi-line command
michael@428: sub multiline_join {
michael@428: my ($cmd) = @_;
michael@428: $cmd =~ s/([ \t]*\\[ \t]*\r?\n[ \t]*)/ /sg;
michael@428: return $cmd;
michael@428: }
michael@428:
michael@428: # utility function: split command into arguments
michael@428: sub shell_tokenize {
michael@428: my ($cmd) = @_;
michael@428: my @cmd = ();
michael@428: while ($cmd ne '') {
michael@428: $cmd =~ s/^\s*('[^']*')/push(@cmd, $1), ''/se && next;
michael@428: $cmd =~ s/^\s*("[^"]*")/push(@cmd, $1), ''/se && next;
michael@428: $cmd =~ s/^\s*(\S+)/ push(@cmd, $1), ''/se && next;
michael@428: $cmd =~ s/^\s*$/ ''/se && last;
michael@428: }
michael@428: return @cmd;
michael@428: }
michael@428:
michael@428: # check filesystem path style
michael@428: $done = $outer_done; $this = ''; $todo = $outer_this;
michael@428: while ($todo =~ m/\%\{l_shtool\}\s+(\w+)\s+/s) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: ($this, $todo) = &command_extract($this . $todo);
michael@428:
michael@428: # check for shtool options with no space before argument
michael@428: my $subthis = $this;
michael@428: $subthis =~ s/\%{[A-Za-z][A-Za-z0-9_]*}//sg;
michael@428: $subthis =~ s/\%{[A-Za-z][A-Za-z0-9_]*(?:\s+[^}]+?)?}//sg;
michael@428: # remove content of quoted arguments as they might contain unrelated things to catch
michael@428: $subthis =~ s/'[^']*'/ARG/sg;
michael@428: $subthis =~ s/"[^"]*"/ARG/sg;
michael@428: $subthis =~ s/`[^`]*`/ARG/sg;
michael@428: # remove us completely if line ends with a quote as this indicates we are a quoted argument to ourselfs
michael@428: $subthis =~ s/[^']*'$//s;
michael@428: $subthis =~ s/[^"]*"$//s;
michael@428: $subthis =~ s/[^`]*`$//s;
michael@428: $subthis =~ s/[^']*' \\\n//s;
michael@428: $subthis =~ s/[^"]*" \\\n//s;
michael@428: $subthis =~ s/[^`]*` \\\n//s;
michael@428: # catch command termination by semicolon, pipe, or, and;
michael@428: $subthis =~ s/[;|&].*$//s;
michael@428: if ($subthis =~ m/\s-[a-zA-Z]\S/) {
michael@428: &lint_warning($file, $done, $this, "found use of shtool option with space omitted before argument");
michael@428: }
michael@428:
michael@428: # openpkg-rc is special because does bootstrap things
michael@428: last if ($file =~ m|openpkg-rc\.spec$|);
michael@428:
michael@428: my @cmd = &shell_tokenize(&multiline_join($this));
michael@428: if ($cmd[1] eq 'mkdir') {
michael@428: # join flags with their arguments
michael@428: for (my $i = 2; $i <= $#cmd; $i++) {
michael@428: if ($cmd[$i] eq '-m') {
michael@428: splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]);
michael@428: }
michael@428: }
michael@428: # check paths
michael@428: for (my $i = $#cmd; $i > 1 and $cmd[$i] !~ m|^-| and $cmd[$i] !~ m|^\d+$|; $i--) {
michael@428: if ($cmd[$i] =~ m|/$|) {
michael@428: &lint_warning($file, $done, $this, "section $section: superfluous trailing slash on " .
michael@428: "created path in \"shtool mkdir\" command (expected none)");
michael@428: }
michael@428: }
michael@428: }
michael@428: elsif ($cmd[1] eq 'install') {
michael@428: # join flags with their arguments
michael@428: for (my $i = 2; $i <= $#cmd; $i++) {
michael@428: if ($cmd[$i] =~ m/-(e|m|o|g)$/) {
michael@428: splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]);
michael@428: }
michael@428: elsif ($cmd[$i] eq '%{SOURCE') {
michael@428: splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]);
michael@428: }
michael@428: elsif ($cmd[$i] eq '%{l_value') {
michael@428: while ($i < $#cmd and $cmd[$i+1] !~ m|\}$|s) {
michael@428: splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]);
michael@428: }
michael@428: splice(@cmd, $i, 2);
michael@428: $i--;
michael@428: }
michael@428: }
michael@428:
michael@428: # determine last path argument
michael@428: my $i = $#cmd; $i-- while ($i > 1 and $cmd[$i] !~ m|^-| and $cmd[$i] !~ m|^\d+$|);
michael@428:
michael@428: # check paths
michael@428: if (($#cmd - $i) > 2 and $cmd[-1] !~ m|/$|) {
michael@428: # simple case: multiple sources require target to be a directory
michael@428: &lint_warning($file, $done, $this, "section $section: missing trailing slash on " .
michael@428: "destination path in \"shtool install\" command (expected one) 1");
michael@428: }
michael@428: elsif (($#cmd - $i) == 2 and $cmd[-1] !~ m|/$|) {
michael@428: # complex case: single source, so we can check only known destination paths
michael@428: if ( $cmd[-1] =~ m/\%\{l_prefix\}\/(bin|cgi|include|info|lib|pub|sbin)$/
michael@428: or $cmd[-1] =~ m/\%\{l_prefix\}\/(etc|libexec|man|share|var)(\/[^\/]+)?$/) {
michael@428: &lint_warning($file, $done, $this, "section $section: missing trailing slash on " .
michael@428: "destination path in \"shtool install\" command (expected one) 2");
michael@428: }
michael@428: }
michael@428: else {
michael@428: # special case: if any path contains '*', then globbing might occur
michael@428: for (my $i = 2; $i <= $#cmd; $i++) {
michael@428: if ($cmd[$i] =~ m/\*/ and $cmd[-1] !~ m|/$|) {
michael@428: &lint_warning($file, $done, $this, "section $section: missing trailing slash on " .
michael@428: "destination path in \"shtool install\" command (expected one) 3");
michael@428: last;
michael@428: }
michael@428: }
michael@428: }
michael@428: }
michael@428: $done .= $this;
michael@428: }
michael@428:
michael@428: # check for redundant 'export PATH' statements
michael@428: #$done = $outer_done; $this = ''; $todo = $outer_this;
michael@428: #while ($todo =~ m/\bexport[ \t]+([a-zA-Z_][a-zA-Z0-9_]*[ \t]+)*PATH\b/s) {
michael@428: # $done .= $`; $this = $&; $todo = $';
michael@428: # &lint_warning($file, $done, $this, "section $section: redundant \"export PATH\" statement (expected none)");
michael@428: # $done .= $this;
michael@428: #}
michael@428:
michael@428: # check for obsolete cpp build flags
michael@428: $done = $outer_done; $this = ''; $todo = $outer_this;
michael@428: while ($todo =~ m/-I\%\{l_prefix\}\/include(\/([^ \t"';]+?))?[ \t"';]/s) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: my ($subdir) = $2;
michael@428: &lint_warning($file, $done, $this, "section $section: hard-coded C pre-processor path option " .
michael@428: "(use \"%{l_cppflags".($subdir ? " $subdir" : "")."}\" instead)");
michael@428: $done .= $this;
michael@428: }
michael@428:
michael@428: # check for obsolete ld build flags
michael@428: $done = $outer_done; $this = ''; $todo = $outer_this;
michael@428: while ($todo =~ m/-L\%\{l_prefix\}\/lib(\/([^ \t"';]+?))?[ \t"';]/s) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: my ($subdir) = $2;
michael@428: &lint_warning($file, $done, $this, "section $section: hard-coded linker path option " .
michael@428: "(use \"%{l_ldflags".($subdir ? " $subdir" : "")."}\" instead)");
michael@428: $done .= $this;
michael@428: }
michael@428:
michael@428: # check for "raw" tool usage
michael@428: if ($section ne '%description' and $file !~ m|openpkg\.spec$|) {
michael@428: foreach my $token (&shell_tokenize($outer_this)) {
michael@428: if ($token =~ m/^(rpmtool|shtool|curl|bash|gzip|bzip2|tar|cc|cxx|make|patch)$/s) {
michael@428: &lint_warning($file, undef, undef, "section $section: raw usage of standard tool \"$token\"" .
michael@428: " (use \"%{l_${token}}\" instead)");
michael@428: }
michael@428: }
michael@428: }
michael@428:
michael@428: # check for setup and patch macro usage
michael@428: if ($section eq '%prep') {
michael@428: $done = $outer_done; $this = ''; $todo = $outer_this;
michael@428: my @tokens = &shell_tokenize($outer_this);
michael@428: while ($todo =~ m/([^\n]*)\n/s) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: if ($this =~ m/^ *%setup.* .*$/) {
michael@428: &lint_warning($file, undef, undef, "section $section: multiple spaces in \"\%setup\" macro" .
michael@428: " (reduce to single space)");
michael@428: }
michael@428: if ($this =~ m/^ *%setup.*-[ab]$/ and grep(/^\%setup+$/, @tokens) == 1) {
michael@428: &lint_warning($file, undef, undef, "section $section: -[ab] option superflous for single \"\%setup\" macro" .
michael@428: " (remove it)");
michael@428: }
michael@428: if ($this =~ m/^ *%patch.* .*$/) {
michael@428: &lint_warning($file, undef, undef, "section $section: multiple spaces in \"\%patch\" macro" .
michael@428: " (reduce to single space)");
michael@428: }
michael@428: if ($this =~ m/^ *%patch.*-p +[01]/) {
michael@428: &lint_warning($file, undef, undef, "section $section: syntactically wrong space in \"\%patch\" macro" .
michael@428: " (use -pN)");
michael@428: }
michael@428: if ($this =~ m/^ *%patch.*-P 0\s*$/ and grep(/^\%patch+$/, @tokens) == 1) {
michael@428: &lint_warning($file, undef, undef, "section $section: syntactically wrong space in \"\%patch\" macro" .
michael@428: " (omit -P 0 for single patch)");
michael@428: }
michael@428: if ($this =~ m/^ *%patch.*-P [^0-9]/ and grep(/^\%patch+$/, @tokens) > 1) {
michael@428: &lint_warning($file, undef, undef, "section $section: syntactically wrong space in \"\%patch\" macro" .
michael@428: " (use -P for multiple patches)");
michael@428: }
michael@428: $done .= $this;
michael@428: }
michael@428: if (grep(/^\%setup\d+$/, @tokens)) {
michael@428: &lint_warning($file, undef, undef, "section $section: numbered \"\%setup\" macro" .
michael@428: " (do not use %setup directly followed by a number, replace with %setup ... -[ab])");
michael@428: }
michael@428: if (grep(/^\%patch\d+$/, @tokens)) {
michael@428: &lint_warning($file, undef, undef, "section $section: numbered \"\%patch\" macro" .
michael@428: " (do not use %patch directly followed by a number, replace with %patch ... -P)");
michael@428: }
michael@428: }
michael@428:
michael@428: # check for hard-coded prefix in %pre[un],%post[un] scripts
michael@428: if ($section =~ m/^\%(preun|postun|pre|post)$/s and $file !~ m|openpkg\.spec$|) {
michael@428: $done = $outer_done; $this = ''; $todo = $outer_this;
michael@428: while ($todo =~ m/\%\{l_prefix\}/s) {
michael@428: $done .= $`; $this = $&; $todo = $';
michael@428: &lint_warning($file, $done, $this, "section $section: hard-coded prefix (\%{l_prefix}) found " .
michael@428: "(use \"\$RPM_INSTALL_PREFIX\" to allow relocation)");
michael@428: $done .= $this;
michael@428: }
michael@428: }
michael@428:
michael@428: # check for sub-shell parenthesis style
michael@428: # (this is such complicated because the Bourne Shell has a
michael@428: # construct "case in )