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 )