openpkg/lint-spec.pl

Mon, 28 Jan 2013 17:37:18 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Mon, 28 Jan 2013 17:37:18 +0100
changeset 758
a2c6460cfb16
permissions
-rw-r--r--

Correct socket error reporting improvement with IPv6 portable code,
after helpful recommendation by Saúl Ibarra Corretgé on OSips devlist.

     1 ##
     2 ##  lint-spec.pl -- OpenPKG *.spec File Checker
     3 ##  Copyright (c) 2000-2012 OpenPKG GmbH <http://openpkg.com/>
     4 ##
     5 ##  This software is property of the OpenPKG GmbH, DE MUC HRB 160208.
     6 ##  All rights reserved. Licenses which grant limited permission to use,
     7 ##  copy, modify and distribute this software are available from the
     8 ##  OpenPKG GmbH.
     9 ##
    10 ##  THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
    11 ##  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
    12 ##  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
    13 ##  IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR
    14 ##  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    15 ##  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
    16 ##  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
    17 ##  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
    18 ##  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    19 ##  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
    20 ##  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
    21 ##  SUCH DAMAGE.
    22 ##
    24 #   Perl run-time requirement
    25 require 5;
    26 BEGIN {
    27     eval "use Getopt::Long; use IO;";
    28     if ($@) {
    29         print STDERR
    30             "lint-spec: ERROR: This command requires a full-size Perl installation!\n" .
    31             "lint-spec: HINT:  Install OpenPKG \"perl\" package to use this command.\n";
    32         exit(1);
    33     }
    34 }
    36 #   OpenPKG instance prefix
    37 my $my_prefix = $ENV{'OPENPKG_PREFIX'};
    38 delete $ENV{'OPENPKG_PREFIX'};
    40 #   program information
    41 my $progname = "lint-spec";
    42 my $progvers = "1.0.0";
    44 #   parameters (defaults)
    45 my $version = 0;
    46 my $verbose = 0;
    47 my $help    = 0;
    48 my $check   = 'all';
    49 my $tmpdir  = ($ENV{TMPDIR} || $ENV{TEMPDIR} || "/tmp") . "/$progname";
    50 my $rpm     = "$my_prefix/bin/openpkg rpm";
    52 #   exception handling support
    53 $SIG{__DIE__} = sub {
    54     my ($err) = @_;
    55     $err =~ s|\s+at\s+.*||s if (not $verbose);
    56     print STDERR "$progname:ERROR: $err ". ($! ? "($!)" : "") . "\n";
    57     exit(1);
    58 };
    60 #   command line parsing
    61 Getopt::Long::Configure("bundling");
    62 my $result = GetOptions(
    63     'V|version'     => \$version,
    64     'v|verbose'     => \$verbose,
    65     'h|help'        => \$help,
    66     'c|check=s'     => \$check,
    67     't|tmpdir=s'    => \$tmpdir,
    68     'r|rpm=s'       => \$rpm,
    69 ) || die "option parsing failed";
    70 if ($help) {
    71     print "Usage: $progname [options] [SPECFILE ...]\n" .
    72           "Available options:\n" .
    73           " -v,--verbose       enable verbose run-time mode\n" .
    74           " -h,--help          print out this usage page\n" .
    75           " -c,--check=CHECKS  select checks to perform (default='all')\n" .
    76           " -r,--rpm=FILE      filesystem path to RPM program\n" .
    77           " -t,--tmpdir=PATH   filesystem path to temporary directory\n" .
    78           " -V,--version       print program version\n";
    79     exit(0);
    80 }
    81 if ($version) {
    82     print "OpenPKG $progname $progvers\n";
    83     exit(0);
    84 }
    86 #   verbose message printing
    87 sub msg_verbose {
    88     my ($msg) = @_;
    89     print STDERR "$msg\n" if ($verbose);
    90 }
    92 #   warning message printing
    93 sub msg_warning {
    94     my ($msg) = @_;
    95     print STDERR "$progname:WARNING: $msg\n";
    96 }
    98 #   error message printing
    99 sub msg_error {
   100     my ($msg) = @_;
   101     print STDERR "$progname:ERROR: $msg\n";
   102 }
   104 #   determine check list
   105 my @check_list = (qw(
   106     blank
   107     comment
   108     license
   109     header
   110     section
   111     preproc
   112     script
   113     global
   114     sources
   115     digest
   116 ));
   117 my @checks = ();
   118 if ($check eq 'all') {
   119     @checks = @check_list;
   120 }
   121 else {
   122     foreach my $c (split(/,/, $check)) {
   123         if (not grep($c, @check_list)) {
   124             die "invalid check \"$c\"";
   125         }
   126         push(@checks, $c);
   127     }
   128 }
   130 #   global return code
   131 $main::GRC = 0;
   133 #   iterate over all .spec files
   134 foreach my $filename (@ARGV) {
   135     my $io = new IO::File "<$filename"
   136         or die "unable to open file \"$filename\" for reading";
   137     my $spec; { local $/ = undef; $spec = <$io>; }
   138     $io->close;
   139     foreach my $check (@checks) {
   140         eval "\&check_$check(\$filename, \$spec);";
   141     }
   142 }
   144 #   die gracefully
   145 exit($main::GRC);
   147 ##  _________________________________________________________________
   148 ##
   149 ##  COMMON SUBROUTINES
   150 ##  _________________________________________________________________
   151 ##
   153 sub lines {
   154     my ($txt) = @_;
   155     my $l = 0;
   156     $txt =~ s|\n|$l++, ''|sge;
   157     return $l;
   158 }
   160 sub lint_message {
   161     my ($type, $file, $done, $this, $msg) = @_;
   162     if (defined($done) and defined($this)) {
   163         my $start = &lines($done) + 1;
   164         my $end = $start + &lines($this);
   165         my $pos = $start;
   166         $pos .= "-". $end if ($end > $start);
   167         printf("%s:%s: %s:%s: %s\n", $progname, $type, $file, $pos, $msg);
   168     }
   169     else {
   170         printf("%s:%s: %s: %s\n", $progname, $type, $file, $msg);
   171     }
   172 }
   174 sub lint_warning {
   175     my ($file, $done, $this, $msg) = @_;
   176     &lint_message("WARNING", $file, $done, $this, $msg);
   177     $main::GRC = 1 if ($main::GRC < 1);
   178 }
   180 sub lint_error {
   181     my ($file, $done, $this, $msg) = @_;
   182     &lint_message("ERROR", $file, $done, $this, $msg);
   183     $main::GRC = 2 if ($main::GRC < 2);
   184 }
   186 ##  _________________________________________________________________
   187 ##
   188 ##  CHECK "blank": whitespace and blank lines
   189 ##  _________________________________________________________________
   190 ##
   192 sub check_blank {
   193     my ($file, $spec) = @_;
   195     #   check for CR-LF combination
   196     my $done = ''; my $this = ''; my $todo = $spec;
   197     while ($todo =~ m/\r\n/s) {
   198         $done .= $`; $this = $&; $todo = $';
   199         &lint_warning($file, $done, $this, "carriage-return (CR, 0x0d) line-feed (NL, 0x0a) combination (expected just line-feed)");
   200         $done .= $this;
   201     }
   203     #   check for multiple blank lines
   204     $done = ''; $this = ''; $todo = $spec;
   205     while ($todo =~ m/(\r?\n[ \t]*){3,}/s) {
   206         $done .= $`; $this = $&; $todo = $';
   207         &lint_warning($file, $done, $this, "multiple subsequent blank lines (expected single blank line)");
   208         $done .= $this;
   209     }
   211     #   check for trailing whitespaces
   212     $done = ''; $this = ''; $todo = $spec;
   213     while ($todo =~ m/[ \t]+\r?\n/s) {
   214         $done .= $`; $this = $&; $todo = $';
   215         if ($done eq '' or $done =~ m|\n$|s) {
   216             &lint_warning($file, $done, $this, "whitespace on empty line (expected none)");
   217         }
   218         else {
   219             &lint_warning($file, $done, $this, "trailing whitespace (expected none)");
   220         }
   221         $done .= $this;
   222     }
   224     #   check for bogus line continuations
   225     $done = ''; $this = ''; $todo = $spec;
   226     while ($todo =~ m/\\[ \t]*\r?\n(?=[ \t]*\r?\n)/s) {
   227         $done .= $`; $this = $&; $todo = $';
   228         &lint_warning($file, $done, $this, "bogus line continuation for following empty line (expect no line continuation)");
   229         $done .= $this;
   230     }
   232     #   check for leading whitespaces before line continuations
   233     $done = ''; $this = ''; $todo = $spec;
   234     while ($todo =~ m/[ \t]{2,}\\[ \t]*\r?\n/s) {
   235         $done .= $`; $this = $&; $todo = $';
   236         &lint_warning($file, $done, $this, "multiple leading whitespace before line continuation (expected just a single space)");
   237         $done .= $this;
   238     }
   240     #   check for leading tabs
   241     $done = ''; $this = ''; $todo = $spec;
   242     while ($todo =~ m/^ *\t+ *[^ \t]/m) {
   243         $done .= $`; $this = $&; $todo = $';
   244         &lint_warning($file, $done, $this, "leading tabs (expected spaces)");
   245         $done .= $this;
   246     }
   248     #   check for mandatory/wished trailing blank line
   249     if ($spec !~ m|\n\n$|) {
   250         &lint_warning($file, $spec, "", "mandatory/wished trailing blank line missing (expected one)");
   251     }
   252 }
   254 ##  _________________________________________________________________
   255 ##
   256 ##  CHECK "comment": sharp-comments
   257 ##  _________________________________________________________________
   258 ##
   260 sub check_comment {
   261     my ($file, $spec) = @_;
   263     #   check for comment indentation and contents
   264     my $done = ''; my $this = ''; my $todo = $spec;
   265     while ($todo =~ m/^([ \t]*)(#+)([ \t]*)(.*?)$/m) {
   266         $done .= $`; $this = $&; $todo = $';
   267         my ($lead, $sharp, $pad, $text) = ($1, $2, $3, $4);
   268         if (length($lead) % 2 != 0) {
   269             &lint_warning($file, $done, $this, "incorrect comment indentation (expected a multiple of 2 spaces)");
   270         }
   271         if (length($lead) > 1 && length($sharp) > 1) {
   272             &lint_warning($file, $done, $this, "indented comment has introduced with multiple sharps (expected single sharp character)");
   273         }
   274         if (length($pad.$text) > 0 && length($sharp.$pad) % 4 != 0) {
   275             &lint_warning($file, $done, $this, "incorrect comment text padding (expected a multiple of 4 sharps or spaces)");
   276         }
   277         if (length($pad) == 0 && length($text) > 0) {
   278             &lint_warning($file, $done, $this, "missing leading space before comment text (expected padding spaces)");
   279         }
   280         if (length($pad) > 0 && length($text) == 0) {
   281             &lint_warning($file, $done, $this, "empty comment text (expected a reasonable text)");
   282         }
   283         $done .= $this;
   284     }
   286     #   check for comment contents only
   287     if ($file !~ m|openpkg\.spec$|) {
   288         $done = ''; $this = ''; $todo = $spec;
   289         while ($todo =~ m/\n\n[ \t]*#[ \t]+([^\n]+)(?!\n([ \t]*#))/s) {
   290             $done .= $`; $this = $&; $todo = $';
   291             my $text = $1;
   292             if (length($text) > 0 and $text =~ m|^[A-Z][^A-Z]|) {
   293                 &lint_warning($file, $done, $this, "comment text starts with upper-case letter (expected lower-case letter)");
   294             }
   295             $done .= $this;
   296         }
   297     }
   298 }
   300 ##  _________________________________________________________________
   301 ##
   302 ##  CHECK "license": license header
   303 ##  _________________________________________________________________
   304 ##
   306 sub check_license {
   307     my ($file, $spec) = @_;
   309     my $name = "[a-z][a-z0-9-]*";
   310     if ($file =~ m|^.*/([a-z][a-z0-9-]*)\.spec$|) {
   311         $name = $1;
   312     }
   313     elsif ($file =~ m|^([a-z][a-z0-9-]*)\.spec$|) {
   314         $name = $1;
   315     }
   316     my $re = "";
   317     $re .= "##\\n";
   318     $re .= "##  $name\\.spec -- OpenPKG RPM Package Specification\\n";
   319     $re .= "##  Copyright \\(c\\) 200[0-9]-2012 OpenPKG Foundation e\.V\. <http://openpkg\\.net/>\\n";
   320     $re .= "##\\n";
   321     $re .= "##  Permission to use, copy, modify, and distribute this software for\\n";
   322     $re .= "##  any purpose with or without fee is hereby granted, provided that\\n";
   323     $re .= "##  the above copyright notice and this permission notice appear in all\\n";
   324     $re .= "##  copies\\.\\n";
   325     $re .= "##\\n";
   326     $re .= "##  THIS SOFTWARE IS PROVIDED \\`\\`AS IS'' AND ANY EXPRESSED OR IMPLIED\\n";
   327     $re .= "##  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\\n";
   328     $re .= "##  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED\\.\\n";
   329     $re .= "##  IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR\\n";
   330     $re .= "##  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\\n";
   331     $re .= "##  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES \\(INCLUDING, BUT NOT\\n";
   332     $re .= "##  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF\\n";
   333     $re .= "##  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION\\) HOWEVER CAUSED AND\\n";
   334     $re .= "##  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\\n";
   335     $re .= "##  OR TORT \\(INCLUDING NEGLIGENCE OR OTHERWISE\\) ARISING IN ANY WAY OUT\\n";
   336     $re .= "##  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF\\n";
   337     $re .= "##  SUCH DAMAGE\\.\\n";
   338     $re .= "##\\n";
   339     if ($name ne "openpkg" and $spec !~ m|^$re|os) {
   340         &lint_warning($file, "", "", "invalid license header");
   341     }
   342 }
   344 ##  _________________________________________________________________
   345 ##
   346 ##  CHECK "header": RPM headers
   347 ##  _________________________________________________________________
   348 ##
   350 sub check_header {
   351     my ($file, $spec) = @_;
   353     my @headers = (qw(
   354         m:Name:^[a-z][a-z0-9-]*$
   355         m:Summary:^[A-Z]\S*(\s+([A-Z]\S*|of|for|from|in|at|on|\(\S+\)))*
   356         m:URL:^((https?|ftp)://.+|-)$
   357         m:Vendor:.+
   358         m:Packager:^(OpenPKG\sFoundation\se\.V\.|OpenPKG\sGmbH)$
   359         m:Distribution:^(OpenPKG|OpenPKG\sCommunity|OpenPKG\sEnterprise)$
   360         m:Class:^(BOOT|CORE|BASE|PLUS|EVAL|JUNK|PRIV)$
   361         m:Group:^[A-Z][a-zA-Z0-9]+$
   362         m:License:.+
   363         m:Version:^[^-]+$
   364         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+|%\{[^\}]+\})$
   365         o:Source\d+:^((https?|ftp)://.+|[^/]+)$
   366         o:Patch\d+:^((https?|ftp)://.+|[^/]+)$
   367         o:Prefix:^%{l_prefix}$
   368         o:BuildRoot:^%{l_buildroot}$
   369         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+)?)+)$
   370         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+)?)+$
   371         o:AutoReq:^no$
   372         o:AutoReqProv:^no$
   373         o:Provides:^((,\s)?([a-z][a-z0-9-]*(\:\:with_[a-z][a-z0-9_]+)?|[A-Z][A-Z0-9-]*)(\s==?\s\S+)?)+$
   374         o:Conflicts:^((,\s)?([a-z][a-z0-9-]*(\:\:with_[a-z][a-z0-9_]+)?|[A-Z][A-Z0-9-]*)(\s(>=?|==?|<=?|!=)\s\S+)?)+$
   375     ));
   376     my @headers_def = (qw(
   377         Summary:^Badly\sPackaged\sProgram$
   378         Vendor:^John\sDoe$
   379         Group:^Unknown$
   380         License:^DoePL$
   381     ));
   382     my @headers_mult = (qw(
   383         BuildPreReq PreReq Provides Conflicts
   384     ));
   386     my @seen = ();
   387     my %count = ();
   388     my $done = ''; my $this = ''; my $todo = $spec;
   389     while ($todo =~ m/^(\S+):([ \t]*)(.*?)$/m) {
   390         $done .= $`; $this = $&; $todo = $';
   391         my ($header, $pad, $value) = ($1, $2, $3);
   393         #   check for layouting
   394         if (length($value) == 0) {
   395             &lint_error($file, $done, $this, "empty RPM header value");
   396         }
   397         if (length($header.":".$pad) != 14) {
   398             &lint_warning($file, $done, $this, "invalid RPM header name/value padding (expected value at column 15)");
   399         }
   401         #   check for valid header name and value
   402         if (not grep { $header =~ m|^$_$|s } map { m/^[^:]:([^:]+):/, $1 } @headers) {
   403             &lint_error($file, $done, $this, "invalid RPM header name \"$header\"");
   404         }
   405         else {
   406             my $hn = quotemeta((grep { $header =~ m|^$_$|s } map { m/^[^:]+:([^:]+):/, $1 } @headers)[0]);
   407             my $re = (map { m/^[^:]+:${hn}:(.+)$/s } @headers)[0];
   408             my $re_match = $re;
   409             $re_match =~ s|^\(|(?:|sg;
   410             $re_match =~ s|([^\\])\(|\1(?:|sg;
   411             if (    not ($file =~ m|openpkg\.spec$| and $header eq 'Provides' and $value eq '%{l_prefix}')
   412                 and $value !~ m|${re_match}|s) {
   413                 &lint_warning($file, $done, $this, "RPM header \"$header\": " .
   414                               "invalid value \"$value\" (expected to match \"$re\")");
   415             }
   416             my $re_def = (map { m/^${hn}:(.+)$/s } @headers_def)[0];
   417             if (defined($re_def)) {
   418                 my $re_def_match = $re_def;
   419                 $re_def_match =~ s|^\(|(?:|sg;
   420                 $re_def_match =~ s|([^\\])\(|\1(?:|sg;
   421                 if ($value =~ m|${re_def_match}|s) {
   422                     &lint_warning($file, $done, $this, "RPM header \"$header\": " .
   423                                   "default value \"$value\" (expected to match \"$re\", except for this)");
   424                 }
   425             }
   426             if ($header =~ m/^Vendor/ and $value =~ m/et\sal/ and $value !~ m/et\sal\./) {
   427                 &lint_warning($file, $done, $this, "RPM header \"$header\": " .
   428                               "has value \"$value\" (expected \"et al.\" with dot)");
   429             }
   430         }
   431         push(@seen, $header);
   432         $count{$header}++;
   433         $done .= $this;
   434     }
   436     #   check for existence of mandatory headers
   437     foreach my $header (@headers) {
   438         my ($type, $name, $regex) = split(/:/, $header, 3);
   439         if ($type eq 'm') {
   440             if ($file =~ m|openpkg\.spec$| and
   441                 ($name eq "BuildPreReq" or $name eq "PreReq")) {
   442                 #   the bootstrap package is an obvious exception
   443                 next;
   444             }
   445             if (not grep(/^$name$/, @seen)) {
   446                 &lint_warning($file, undef, undef, "mandatory RPM header \"$name\" not found");
   447             }
   448         }
   449     }
   451     #   check for multiple occurrence headers
   452     foreach my $seen (@seen) {
   453         if ($count{$seen} > 1 and not (grep { $_ eq $seen } @headers_mult)) {
   454             &lint_error($file, undef, undef, "RPM header \"$seen\" occurs multiple times (expected just once)");
   455         }
   456     }
   457 }
   459 ##  _________________________________________________________________
   460 ##
   461 ##  CHECK "section": RPM sections
   462 ##  _________________________________________________________________
   463 ##
   465 sub check_section {
   466     my ($file, $spec) = @_;
   468     my $require = qq{
   469         (%define,)*
   470         Name:,
   471         Summary:,
   472         URL:,
   473         Vendor:,
   474         Packager:,
   475         Distribution:,
   476         Class:,
   477         Group:,
   478         License:,
   479         Version:,
   480         Release:,
   481         (%option,)*
   482         (%define,|%undefine,)*
   483         (Source\\d+:,)*
   484         (Patch\\d+:,)*
   485         (%NoSource,)*
   486         (%NoPatch,)*
   487         (Prefix:,)?
   488         (BuildRoot:,)?
   489         ((BuildPreReq:,)
   490          |(PreReq:,))*
   491         (AutoReq:,)?
   492         (AutoReqProv:,)?
   493         (Provides:,)*
   494         (Conflicts:,)*
   495         %description,
   496         (%track,)?
   497         %prep,
   498         %build,
   499         %install,
   500         (%check,)?
   501         %files,
   502         %clean,
   503         (%pre,)?
   504         (%post,)?
   505         (%preun,)?
   506         (%postun,)?
   507         (%trigger,)?
   508         (%triggerin,)?
   509         (%triggerun,)?
   510         (%triggerpostun,)?
   511         (%verifyscript,)?
   512     };
   514     #   check for order of headers
   515     my $sections = "";
   516     my $done = ''; my $this = ''; my $todo = $spec;
   517     while ($todo =~ m/^(\S+:|%\S+).*$/m) {
   518         $done .= $`; $this = $&; $todo = $';
   519         my $section = $1;
   520         next if ($section =~ m/^%(if|else|endif)/);
   521         $sections .= "$section,";
   522         $done .= $this;
   523     }
   524     my $regex = $require;
   525     $regex =~ s|\s+||sg;
   526     if ($sections !~ m/^$regex$/s) {
   527         $regex =~ s|,| |sg;
   528         &lint_error($file, undef, undef, "invalid RPM section order: $sections (expected \"$regex\")");
   529     }
   530 }
   532 ##  _________________________________________________________________
   533 ##
   534 ##  CHECK "preproc": RPM macro pre-processor
   535 ##  _________________________________________________________________
   536 ##
   538 sub check_preproc {
   539     my ($file, $spec) = @_;
   541     my $done = ''; my $this = ''; my $todo = $spec;
   542     while ($todo =~ m/^(%(?:if|else|endif|define|undefine|option))(.*)$/m) {
   543         $done .= $`; $this = $&; $todo = $';
   544         my ($cmd, $args) = ($1, $2);
   545         if ($cmd eq '%if') {
   546             #   FIXME: either try to really parse the boolean expression
   547             #   FIXME: or at least try to guess its correct syntax
   548         }
   549         elsif ($cmd eq '%else' or $cmd eq '%endif') {
   550             if (length($args) > 0) {
   551                 &lint_warning($file, $done, $this, "garbage after pre-processor directive " .
   552                               "\"$cmd\" (expected no arguments)");
   553             }
   554         }
   555         elsif ($cmd eq '%undefine') {
   556             if ($args =~ m|^\s+(\S+)\s*$|) {
   557                 my $var = $1;
   558                 if ($var !~ m/^(V|with)_[a-z][a-zA-Z0-9_]*$/) {
   559                     &lint_warning($file, $done, $this, "unusually named macro: \"$var\" " .
   560                                   "(expected \"(V|with)_[a-z][a-zA-Z0-9_]*\")");
   561                     next;
   562                 }
   563             }
   564             else {
   565                 &lint_error($file, $done, $this, "invalid number of arguments to pre-processor " .
   566                             "directive \"$cmd\" (expected exactly 1 argument)");
   567             }
   568         }
   569         elsif ($cmd eq '%define' or $cmd eq '%option') {
   570             if ($args =~ m|^\s+(\S+)\s+(.*)$|) {
   571                 my ($var, $val) = ($1, $2);
   572                 if ($var !~ m/^(V|with)_[a-z][a-zA-Z0-9_]*$/) {
   573                     &lint_warning($file, $done, $this, "unusually named macro: \"$var\" " .
   574                                   "(expected \"(V|with)_[a-z][a-zA-Z0-9_]*\")");
   575                     next;
   576                 }
   577                 if (length($val) == 0) {
   578                     &lint_error($file, $done, $this, "empty macro value");
   579                 }
   580             }
   581             else {
   582                 &lint_error($file, $done, $this, "invalid number of arguments to pre-processor " .
   583                             "directive \"$cmd\" (expected exactly 2 arguments)");
   584             }
   585         }
   586         $done .= $this;
   587     }
   589     #   check correct if/endif nesting
   590     my @stack = ();
   591     $done = ''; $this = ''; $todo = $spec;
   592     while ($todo =~ m/^(%(?:if|else|endif|define|undefine|option)).*$/m) {
   593         $done .= $`; $this = $&; $todo = $';
   594         my $directive = $1;
   595         if ($directive eq '%if') {
   596             push(@stack, &lines($done . $this));
   597         }
   598         elsif ($directive eq '%endif') {
   599             if (@stack == 0) {
   600                 &lint_error($file, $done, $this, "found \%endif without corresponding opening \%if");
   601                 last;
   602             }
   603             pop(@stack);
   604         }
   605         $done .= $this;
   606     }
   607     my $line;
   608     while (defined($line = pop(@stack))) {
   609         &lint_error($file, undef, undef, "\%if at line $line never closed by \%endif");
   610     }
   612     #   check for indented preprocessor constructs
   613     $done = ''; $this = ''; $todo = $spec;
   614     while ($todo =~ m/^[ \t]+(%(?:if|else|endif|define|undefine|option)).*$/m) {
   615         $done .= $`; $this = $&; $todo = $';
   616         my $directive = $1;
   617         &lint_error($file, $done, $this, "found indented \"$directive\" preprocessor directive (expected no indentation)");
   618         $done .= $this;
   619     }
   620 }
   622 ##  _________________________________________________________________
   623 ##
   624 ##  CHECK "script": shell scripts
   625 ##  _________________________________________________________________
   626 ##
   628 sub check_script {
   629     my ($file, $spec) = @_;
   631     my $done = ''; my $this = ''; my $todo = $spec;
   632     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) {
   633         $done .= $`; $this = $&; $todo = $';
   634         my ($section, $args, $script) = ($1, $2, $3);
   636         #   perform checks for a single script section
   637         &check_script_section($file, $done, $this, $section, $args, $script);
   639         $done .= $this;
   640     }
   641 }
   643 sub check_script_section {
   644     my ($file, $outer_done, $outer_this, $section, $args, $script) = @_;
   646     #   skip plain-text/non-scripting section %description
   647     return if ($section eq '%description');
   649     #   remove comment contents
   650     $outer_this =~ s|^[ \t]*#[^\n]*||mg;
   652     #   check shell redirections
   653     my $done = $outer_done; my $this = ''; my $todo = $outer_this;
   654     while (   $todo =~ m/[ \t]+(\d+)?[><][ \t]+\S+/s
   655            or $todo =~ m/[ \t]+[><](\&\d+)?[ \t]+\S+/s) {
   656         $done .= $`; $this = $&; $todo = $';
   657         &lint_warning($file, $done, $this, "section $section: whitespace after shell redirection (expected none)");
   658         $done .= $this;
   659     }
   661     #   utility function: extract a single shell command
   662     sub command_extract {
   663         my ($script) = @_;
   664         my $cmd = '';
   665         while ($script ne '') {
   666             $script =~ s/^([ \t]*'[^']*')/         $cmd .= $1, ''/se && next;
   667             $script =~ s/^([ \t]*"[^"]*")/         $cmd .= $1, ''/se && next;
   668             $script =~ s/^([ \t]*[^ \t;\)\\\r\n]+)/$cmd .= $1, ''/se && next;
   669             $script =~ s/^([ \t]*\\[ \t]*\r?\n)/   $cmd .= $1, ''/se && next;
   670             last;
   671         }
   672         return ($cmd, $script);
   673     }
   675     #   utility function: join a multi-line command
   676     sub multiline_join {
   677         my ($cmd) = @_;
   678         $cmd =~ s/([ \t]*\\[ \t]*\r?\n[ \t]*)/ /sg;
   679         return $cmd;
   680     }
   682     #   utility function: split command into arguments
   683     sub shell_tokenize {
   684         my ($cmd) = @_;
   685         my @cmd = ();
   686         while ($cmd ne '') {
   687             $cmd =~ s/^\s*('[^']*')/push(@cmd, $1), ''/se && next;
   688             $cmd =~ s/^\s*("[^"]*")/push(@cmd, $1), ''/se && next;
   689             $cmd =~ s/^\s*(\S+)/    push(@cmd, $1), ''/se && next;
   690             $cmd =~ s/^\s*$/                        ''/se && last;
   691         }
   692         return @cmd;
   693     }
   695     #   check filesystem path style
   696     $done = $outer_done; $this = ''; $todo = $outer_this;
   697     while ($todo =~ m/\%\{l_shtool\}\s+(\w+)\s+/s) {
   698         $done .= $`; $this = $&; $todo = $';
   699         ($this, $todo) = &command_extract($this . $todo);
   701         #   check for shtool options with no space before argument
   702         my $subthis = $this;
   703         $subthis =~ s/\%{[A-Za-z][A-Za-z0-9_]*}//sg;
   704         $subthis =~ s/\%{[A-Za-z][A-Za-z0-9_]*(?:\s+[^}]+?)?}//sg;
   705         #   remove content of quoted arguments as they might contain unrelated things to catch
   706         $subthis =~ s/'[^']*'/ARG/sg;
   707         $subthis =~ s/"[^"]*"/ARG/sg;
   708         $subthis =~ s/`[^`]*`/ARG/sg;
   709         #   remove us completely if line ends with a quote as this indicates we are a quoted argument to ourselfs
   710         $subthis =~ s/[^']*'$//s;
   711         $subthis =~ s/[^"]*"$//s;
   712         $subthis =~ s/[^`]*`$//s;
   713         $subthis =~ s/[^']*' \\\n//s;
   714         $subthis =~ s/[^"]*" \\\n//s;
   715         $subthis =~ s/[^`]*` \\\n//s;
   716         #   catch command termination by semicolon, pipe, or, and;
   717         $subthis =~ s/[;|&].*$//s;
   718         if ($subthis =~ m/\s-[a-zA-Z]\S/) {
   719             &lint_warning($file, $done, $this, "found use of shtool option with space omitted before argument");
   720         }
   722         #   openpkg-rc is special because does bootstrap things
   723         last if ($file =~ m|openpkg-rc\.spec$|);
   725         my @cmd = &shell_tokenize(&multiline_join($this));
   726         if ($cmd[1] eq 'mkdir') {
   727             #   join flags with their arguments
   728             for (my $i = 2; $i <= $#cmd; $i++) {
   729                 if ($cmd[$i] eq '-m') {
   730                     splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]);
   731                 }
   732             }
   733             #   check paths
   734             for (my $i = $#cmd; $i > 1 and $cmd[$i] !~ m|^-| and $cmd[$i] !~ m|^\d+$|; $i--) {
   735                 if ($cmd[$i] =~ m|/$|) {
   736                     &lint_warning($file, $done, $this, "section $section: superfluous trailing slash on " .
   737                                   "created path in \"shtool mkdir\" command (expected none)");
   738                 }
   739             }
   740         }
   741         elsif ($cmd[1] eq 'install') {
   742             #   join flags with their arguments
   743             for (my $i = 2; $i <= $#cmd; $i++) {
   744                 if ($cmd[$i] =~ m/-(e|m|o|g)$/) {
   745                     splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]);
   746                 }
   747                 elsif ($cmd[$i] eq '%{SOURCE') {
   748                     splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]);
   749                 }
   750                 elsif ($cmd[$i] eq '%{l_value') {
   751                     while ($i < $#cmd and $cmd[$i+1] !~ m|\}$|s) {
   752                         splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]);
   753                     }
   754                     splice(@cmd, $i, 2);
   755                     $i--;
   756                 }
   757             }
   759             #   determine last path argument
   760             my $i = $#cmd; $i-- while ($i > 1 and $cmd[$i] !~ m|^-| and $cmd[$i] !~ m|^\d+$|);
   762             #   check paths
   763             if (($#cmd - $i) > 2 and $cmd[-1] !~ m|/$|) {
   764                 #   simple case: multiple sources require target to be a directory
   765                 &lint_warning($file, $done, $this, "section $section: missing trailing slash on " .
   766                               "destination path in \"shtool install\" command (expected one) 1");
   767             }
   768             elsif (($#cmd - $i) == 2 and $cmd[-1] !~ m|/$|) {
   769                 #   complex case: single source, so we can check only known destination paths
   770                 if (   $cmd[-1] =~ m/\%\{l_prefix\}\/(bin|cgi|include|info|lib|pub|sbin)$/
   771                     or $cmd[-1] =~ m/\%\{l_prefix\}\/(etc|libexec|man|share|var)(\/[^\/]+)?$/) {
   772                     &lint_warning($file, $done, $this, "section $section: missing trailing slash on " .
   773                                   "destination path in \"shtool install\" command (expected one) 2");
   774                 }
   775             }
   776             else {
   777                 #   special case: if any path contains '*', then globbing might occur
   778                 for (my $i = 2; $i <= $#cmd; $i++) {
   779                     if ($cmd[$i] =~ m/\*/ and $cmd[-1] !~ m|/$|) {
   780                         &lint_warning($file, $done, $this, "section $section: missing trailing slash on " .
   781                                       "destination path in \"shtool install\" command (expected one) 3");
   782                         last;
   783                     }
   784                 }
   785             }
   786         }
   787         $done .= $this;
   788     }
   790     #   check for redundant 'export PATH' statements
   791     #$done = $outer_done; $this = ''; $todo = $outer_this;
   792     #while ($todo =~ m/\bexport[ \t]+([a-zA-Z_][a-zA-Z0-9_]*[ \t]+)*PATH\b/s) {
   793     #    $done .= $`; $this = $&; $todo = $';
   794     #    &lint_warning($file, $done, $this, "section $section: redundant \"export PATH\" statement (expected none)");
   795     #    $done .= $this;
   796     #}
   798     #   check for obsolete cpp build flags
   799     $done = $outer_done; $this = ''; $todo = $outer_this;
   800     while ($todo =~ m/-I\%\{l_prefix\}\/include(\/([^ \t"';]+?))?[ \t"';]/s) {
   801         $done .= $`; $this = $&; $todo = $';
   802         my ($subdir) = $2;
   803         &lint_warning($file, $done, $this, "section $section: hard-coded C pre-processor path option " .
   804                       "(use \"%{l_cppflags".($subdir ? " $subdir" : "")."}\" instead)");
   805         $done .= $this;
   806     }
   808     #   check for obsolete ld build flags
   809     $done = $outer_done; $this = ''; $todo = $outer_this;
   810     while ($todo =~ m/-L\%\{l_prefix\}\/lib(\/([^ \t"';]+?))?[ \t"';]/s) {
   811         $done .= $`; $this = $&; $todo = $';
   812         my ($subdir) = $2;
   813         &lint_warning($file, $done, $this, "section $section: hard-coded linker path option " .
   814                       "(use \"%{l_ldflags".($subdir ? " $subdir" : "")."}\" instead)");
   815         $done .= $this;
   816     }
   818     #   check for "raw" tool usage
   819     if ($section ne '%description' and $file !~ m|openpkg\.spec$|) {
   820         foreach my $token (&shell_tokenize($outer_this)) {
   821             if ($token =~ m/^(rpmtool|shtool|curl|bash|gzip|bzip2|tar|cc|cxx|make|patch)$/s) {
   822                 &lint_warning($file, undef, undef, "section $section: raw usage of standard tool \"$token\"" .
   823                               " (use \"%{l_${token}}\" instead)");
   824             }
   825         }
   826     }
   828     #   check for setup and patch macro usage
   829     if ($section eq '%prep') {
   830         $done = $outer_done; $this = ''; $todo = $outer_this;
   831         my @tokens = &shell_tokenize($outer_this);
   832         while ($todo =~ m/([^\n]*)\n/s) {
   833             $done .= $`; $this = $&; $todo = $';
   834             if ($this =~ m/^ *%setup.*  .*$/) {
   835                 &lint_warning($file, undef, undef, "section $section: multiple spaces in \"\%setup\" macro" .
   836                               " (reduce to single space)");
   837             }
   838             if ($this =~ m/^ *%setup.*-[ab]$/ and grep(/^\%setup+$/, @tokens) == 1) {
   839                 &lint_warning($file, undef, undef, "section $section: -[ab] option superflous for single \"\%setup\" macro" .
   840                               " (remove it)");
   841             }
   842             if ($this =~ m/^ *%patch.*  .*$/) {
   843                 &lint_warning($file, undef, undef, "section $section: multiple spaces in \"\%patch\" macro" .
   844                               " (reduce to single space)");
   845             }
   846             if ($this =~ m/^ *%patch.*-p +[01]/) {
   847                 &lint_warning($file, undef, undef, "section $section: syntactically wrong space in \"\%patch\" macro" .
   848                               " (use -pN)");
   849             }
   850             if ($this =~ m/^ *%patch.*-P 0\s*$/ and grep(/^\%patch+$/, @tokens) == 1) {
   851                 &lint_warning($file, undef, undef, "section $section: syntactically wrong space in \"\%patch\" macro" .
   852                               " (omit -P 0 for single patch)");
   853             }
   854             if ($this =~ m/^ *%patch.*-P [^0-9]/ and grep(/^\%patch+$/, @tokens)  > 1) {
   855                 &lint_warning($file, undef, undef, "section $section: syntactically wrong space in \"\%patch\" macro" .
   856                               " (use -P for multiple patches)");
   857             }
   858             $done .= $this;
   859         }
   860         if (grep(/^\%setup\d+$/, @tokens)) {
   861             &lint_warning($file, undef, undef, "section $section: numbered \"\%setup\" macro" .
   862                           " (do not use %setup directly followed by a number, replace with %setup ... -[ab])");
   863         }
   864         if (grep(/^\%patch\d+$/, @tokens)) {
   865             &lint_warning($file, undef, undef, "section $section: numbered \"\%patch\" macro" .
   866                           " (do not use %patch directly followed by a number, replace with %patch ... -P)");
   867         }
   868     }
   870     #   check for hard-coded prefix in %pre[un],%post[un] scripts
   871     if ($section =~ m/^\%(preun|postun|pre|post)$/s and $file !~ m|openpkg\.spec$|) {
   872         $done = $outer_done; $this = ''; $todo = $outer_this;
   873         while ($todo =~ m/\%\{l_prefix\}/s) {
   874             $done .= $`; $this = $&; $todo = $';
   875             &lint_warning($file, $done, $this, "section $section: hard-coded prefix (\%{l_prefix}) found " .
   876                           "(use \"\$RPM_INSTALL_PREFIX\" to allow relocation)");
   877             $done .= $this;
   878         }
   879     }
   881     #   check for sub-shell parenthesis style
   882     #   (this is such complicated because the Bourne Shell has a
   883     #   construct "case <value> in <pattern> ) <script> ;; ... esac"
   884     #   where the closing parenthesis makes heavy problems for our
   885     #   check. So we first have to get rid of this. Unfortunately this
   886     #   is again not easy because there exists nested(!) case/esac
   887     #   constructs. Hence, we have to use a small recursive descent
   888     #   parser which replaces the ")" in case/esac constructs with
   889     #   "PCLOSE". A similar thing is done with string literals, although
   890     #   here the job is more trivial).
   891     if ($section !~ m/^\%files$/s) {
   892         $done = $outer_done; $this = ''; $todo = $outer_this;
   893         $todo =~ s/`[^`]*`/STRING/sg;
   894         $todo =~ s/'[^']*'/STRING/sg;
   895         $todo =~ s/"[^"]*"/STRING/sg;
   896         $todo = (&parse_sh("", $todo))[0];
   897         sub parse_sh {
   898             my ($done, $todo) = @_;
   899             while ($todo =~ m/^(.*?)(case|;;)/s) {
   900                 if ($2 eq 'case') {
   901                     ($done, $todo) = &parse_case($done.$1, $2.$');
   902                 }
   903                 else {
   904                     ($done, $todo) = ($done.$1, $2.$');
   905                     return ($done, $todo);
   906                 }
   907             }
   908             ($done, $todo) = ($done . $todo, "");
   909             return ($done, $todo);
   910         }
   911         sub parse_case {
   912             my ($done, $todo) = @_;
   913             $todo =~ m|^\s*case\s+\S+\s+in\b|s or return (undef, undef);
   914             ($done, $todo) = ($done . $&, $');
   915             while ($todo =~ m|^(\s*[^)\n]+)(\))|s) {
   916                 ($done, $todo) = ($done . $1 . "PCLOSE", $');
   917                 ($done, $todo) = &parse_sh($done, $todo) or return (undef, undef);
   918                 $todo =~ m|^\s*;;|s or return (undef, undef);
   919                 ($done, $todo) = ($done . $&, $');
   920             }
   921             $todo =~ m|^\s*esac|s or return (undef, undef);
   922             ($done, $todo) = ($done . $&, $');
   923             return ($done, $todo);
   924         }
   925         if ($file !~ m|openpkg\.spec$| and $todo eq '') {
   926             &lint_warning($file, $outer_done, $outer_this,
   927                           "unable to correctly parse case/esac constructs in shell-script" .
   928                           " (sub-shell parenthesis check skipped)");
   929         }
   930         else {
   931             while ($todo =~ m/\)[ \t]*\n/s) {
   932                 $done .= $`; $this = $&; $todo = $';
   933                 &lint_warning($file, $done, $this, "problematic sub-shell construct without error handling " .
   934                               "(use \"...) || exit \$?\" to make sure it correctly exits on nested failure)");
   935                 $done .= $this;
   936             }
   937         }
   938     }
   939 }
   941 ##  _________________________________________________________________
   942 ##
   943 ##  CHECK "global": globals
   944 ##  _________________________________________________________________
   945 ##
   947 sub check_global {
   948     my ($file, $spec) = @_;
   950     #   check for deprecated use of %{name} macro
   951     my $done = ''; my $this = ''; my $todo = $spec;
   952     while ($todo =~ m/\%\{name\}/s) {
   953         $done .= $`; $this = $&; $todo = $';
   954         &lint_warning($file, $done, $this, "deprecated usage of %{name} macro (expected none)");
   955         $done .= $this;
   956     }
   958     #   FIXME: activate later
   959     #   check for over-long lines
   960     #$done = ''; $this = ''; $todo = $spec;
   961     #while ($todo =~ m/([^\n]{80,})\n/s) {
   962     #    $done .= $`; $this = $&; $todo = $';
   963     #    &lint_warning($file, $done, $this, "over-long line with ".sprintf("%d", length($1))." characters " .
   964     #                  "(expected maximum of 79 characters)");
   965     #    $done .= $this;
   966     #}
   968     #   check for "shtool install" with -e on same line
   969     my $done = ''; my $this = ''; my $todo = $spec;
   970     while ($todo =~ m/%{l_shtool} install[^\n]*-e/s) {
   971         $done .= $`; $this = $&; $todo = $';
   972         &lint_warning($file, $done, $this, "found -e option of \"shtool install\" on same line (expected separate lines)");
   973         $done .= $this;
   974     }
   976     #   check for "shtool install" with multiple -e on same line
   977     my $done = ''; my $this = ''; my $todo = $spec;
   978     while ($todo =~ m/%{l_shtool} install[^\n]+\\\n([^\n]*?-e[^\n]*?-e[^\n]*\\\n)+/s) {
   979         $done .= $`; $this = $&; $todo = $';
   980         &lint_warning($file, $done, $this, "found multiple -e options of \"shtool install\" on same line (expected separate lines)");
   981         $done .= $this;
   982     }
   984     #   check for deprecated use of "shtool install|subst -e @l_...@ %{l_...}"
   985     if ($file !~ m|openpkg-tool|) {
   986         my $done = ''; my $this = ''; my $todo = $spec;
   987         while ($todo =~ m/%{l_shtool} (install|subst)(?:\\\n|[^\n])*?-e(?:\\\n|[^\n])*?\@l_(prefix|[smrn](usr|grp|uid|gid))\@[^n]+%{l_\2}/s) {
   988             $done .= $`; $this = $&; $todo = $';
   989             &lint_warning($file, $done, $this, "deprecated usage of \"shtool $1 -e \@l_$2\@\" (expected use of %{l_value -s -a})");
   990             $done .= $this;
   991         }
   992     }
   994     #   check for constant "root" in %attr
   995     my $done = ''; my $this = ''; my $todo = $spec;
   996     while ($todo =~ m/%attr\(\s*(\d+)\s*,\s*root\s*,/s) {
   997         $done .= $`; $this = $&; $todo = $';
   998         &lint_warning($file, $done, $this, "found constant \"root\" in %attr (expected variable %{l_susr})");
   999         $done .= $this;
  1002     #   check for valid RPM macro usage
  1003     if ($file !~ m|^(.+/)?openpkg\.spec$|s) {
  1004         my $ok = `$rpm --eval '\%dump' 2>&1 | egrep "^-*[0-9][0-9]*: l_" | sed -e 's;^[^:]*: *\\(l_[a-zA-Z0-9_]*\\).*;\\1;' | sort -u`;
  1005         my @ok = split(/\n/, $ok);
  1006         my $todo = $spec;
  1007         $todo =~ s|^([a-zA-Z][a-zA-Z0-9]+):\s+(.+)$|push(@ok, lc($1)), ''|mge;
  1008         $todo =~ s/^\%(?:define|option)\s+(\S+)\s+.+$/push(@ok, $1), ''/mge;
  1009         my $done = ''; my $this = ''; my $todo = $spec;
  1010         while ($todo =~ m|\%\{([a-zA-Z][a-zA-Z0-9_]+)|s) {
  1011             $done .= $`; $this = $&; $todo = $';
  1012             my $name = $1;
  1013             next if ($name =~ m/^(SOURCE|PATCH)\d*$/ or $name =~ m/^(expand|echo|warn|error)$/);
  1014             if (not grep(/^\Q$name\E$/, @ok)) {
  1015                 &lint_error($file, $done, $this, "unknown macro \"\%{$name}\"");
  1017             $done .= $this;
  1022 ##  _________________________________________________________________
  1023 ##
  1024 ##  CHECK "sources": source file consistency
  1025 ##  _________________________________________________________________
  1026 ##
  1028 sub check_sources {
  1029     my ($file, $spec) = @_;
  1031     ##
  1032     ##  PREPARATION
  1033     ##
  1035     my $D = {};
  1036     my $S = {};
  1037     my $i = 99;
  1039     #   determine defines values
  1040     my $done = ''; my $this = ''; my $todo = $spec;
  1041     while ($todo =~ m/^\%(define|option)\s+(\S+)\s+(.+)\s*$/m) {
  1042         $done .= $`; $this = $&; $todo = $';
  1043         my ($var, $val) = ($2, $3);
  1044         $val =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge;
  1045         $D->{$var} = $val;
  1046         $done .= $this;
  1048     $done = ''; $this = ''; $todo = $spec;
  1049     while ($todo =~ m/^([a-zA-Z][a-zA-Z0-9_]+):\s+(.*)$/m) {
  1050         $done .= $`; $this = $&; $todo = $';
  1051         my ($var, $val) = (lc($1), $2);
  1052         $val =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge;
  1053         $D->{$var} = $val;
  1054         $done .= $this;
  1057     #   expand define values (allow one level of nesting)
  1058     $spec =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge;
  1059     $spec =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge;
  1061     #   parse Source# and Patch# headers
  1062     $done = ''; $this = ''; $todo = $spec;
  1063     while ($todo =~ m/^(Source|Patch)(\d+):[ \t]*(.*?)$/m) {
  1064         $done .= $`; $this = $&; $todo = $';
  1065         my ($type, $num, $file) = (lc($1), $2, $3);
  1066         my $url = "";
  1067         if ($file =~ m|^(.+/)([^/]+)$|s) {
  1068             ($url, $file) = ($1, $2);
  1070         if (not defined($S->{"$type$num"})) {
  1071             $S->{"$type$num"} = {
  1072                 -type   => $type,
  1073                 -num    => $num,
  1074                 -url    => $url,
  1075                 -file   => $file,
  1076                 -refhdr => 1,
  1077                 -refcmd => 0,
  1078                 -refmac => 0,
  1079                 -refvar => 0,
  1080             };
  1082         else {
  1083             $S->{"$type$num"}->{-refhdr}++;
  1085         $done .= $this;
  1088     #   parse %setup and %patch commands
  1089     $done = ''; $this = ''; $todo = $spec;
  1090     $todo =~ s/^(\s*\%patch)((?:\s+-\S+|\s+-d\s+\S+)*)((?:\s+\d+)+)\s*$/&expand_patch($1, $2, $3)/mge;
  1091     sub expand_patch {
  1092         my ($cmd, $opts, $nums) = @_;
  1093         my $cmds = '';
  1094         $cmds .= "${cmd}0${opts}\n" if ($opts !~ m|\s+-P|s);
  1095         $opts =~ s|\s+-P||s;
  1096         while ($nums =~ s|^\s+(\d+)||) {
  1097             my $num = $1;
  1098             $cmds .= "${cmd}${num}${opts}\n";
  1100         return $cmds;
  1102     while ($todo =~ m/^\s*\%(setup|patch)(\d+)?(\s+.*)?$/m) {
  1103         $done .= $`; $this = $&; $todo = $';
  1104         my ($type, $num, $opts) = ($1, $2, $3);
  1105         $type = 'source' if ($type eq 'setup');
  1106         my $num_force = '';
  1107         if ($opts =~ m/\s-(?:a|b)\s*(\d+)/s) {
  1108             $num_force = $1;
  1110         if ($num ne '' and $num_force ne '' and $num ne $num_force) {
  1111             &lint_warning($file, $done, $this, "macro and enforced $type number conflict: $num <-> $num_force" .
  1112                         " (expected either just enforced number or make them match)");
  1114         $num = $num_force if ($num_force ne '');
  1115         $num = 0 if ($num eq '');
  1116         next if ($opts =~ m|\s-T|s and $num == 0);
  1117         if (not defined($S->{"$type$num"})) {
  1118             $S->{"$type$num"} = {
  1119                 -type   => $type,
  1120                 -num    => $num,
  1121                 -url    => "",
  1122                 -file   => "",
  1123                 -refhdr => 0,
  1124                 -refcmd => 1,
  1125                 -refmac => 0,
  1126                 -refvar => 0,
  1127             };
  1129         else {
  1130             $S->{"$type$num"}->{-refcmd}++;
  1132         $done .= $this;
  1135     #   parse %{SOURCE#} and %{PATCH#} macros
  1136     $done = ''; $this = ''; $todo = $spec;
  1137     while ($todo =~ m/\%\{(SOURCE|PATCH)(\d+)\}/m) {
  1138         $done .= $`; $this = $&; $todo = $';
  1139         my ($type, $num) = (lc($1), $2);
  1140         if (not defined($S->{"$type$num"})) {
  1141             $S->{"$type$num"} = {
  1142                 -type   => $type,
  1143                 -num    => $num,
  1144                 -url    => "",
  1145                 -file   => "",
  1146                 -refhdr => 0,
  1147                 -refcmd => 0,
  1148                 -refmac => 1,
  1149                 -refvar => 0,
  1150             };
  1152         else {
  1153             $S->{"$type$num"}->{-refmac}++;
  1155         $done .= $this;
  1158     #   parse %{SOURCE ...} and %{PATCH ...} macros
  1159     $done = ''; $this = ''; $todo = $spec;
  1160     while ($todo =~ m/\%\{(SOURCE|PATCH)\s+([^\s}]+)\}/m) {
  1161         $done .= $`; $this = $&; $todo = $';
  1162         my ($type, $file) = (lc($1), $2);
  1163         my $key = "";
  1164         foreach my $src (keys(%{$S})) {
  1165             if ($S->{$src}->{-type} eq $type and $S->{$src}->{-file} eq $file) {
  1166                 $key = $src;
  1169         if ($key eq '') {
  1170             $S->{"$type$i"} = {
  1171                 -type   => $type,
  1172                 -num    => $i,
  1173                 -url    => "",
  1174                 -file   => $file,
  1175                 -refhdr => 0,
  1176                 -refcmd => 0,
  1177                 -refmac => 1,
  1178                 -refvar => 0,
  1179             };
  1180             $i++;
  1182         else {
  1183             $S->{$key}->{-refmac}++;
  1185         $done .= $this;
  1188     #   parse $RPM_SOURCE_DIR based references
  1189     $done = ''; $this = ''; $todo = $spec;
  1190     while ($todo =~ m/\$RPM_SOURCE_DIR\/([^\s;"']+)/m) {
  1191         $done .= $`; $this = $&; $todo = $';
  1192         my ($file) = ($1);
  1193         my $key = "";
  1194         foreach my $src (keys(%{$S})) {
  1195             if ($S->{$src}->{-file} eq $file) {
  1196                 $key = $src;
  1199         if ($key eq '') {
  1200             $S->{"source$i"} = {
  1201                 -type   => "source",
  1202                 -num    => $i,
  1203                 -url    => "",
  1204                 -file   => $file,
  1205                 -refhdr => 0,
  1206                 -refcmd => 0,
  1207                 -refmac => 0,
  1208                 -refvar => 1,
  1209             };
  1210             $i++;
  1212         else {
  1213             $S->{$key}->{-refmac}++;
  1215         $done .= $this;
  1218     ##
  1219     ##  CHECKING
  1220     ##
  1222     #   check reference counts
  1223     foreach my $src (%{$S}) {
  1224         my $s = $S->{$src};
  1225         my $str_hdr = ($s->{-type} eq 'source' ? 'Source' : 'Patch' ) . $s->{-num};
  1226         my $str_cmd = ($s->{-type} eq 'source' ? '%setup' : '%patch') . $s->{-num};
  1227         my $str_mac = ($s->{-type} eq 'source' ? '%{SOURCE ...}' : '%{PATCH ...}');
  1228         my $str_var = "\$RPM_SOURCE_DIR";
  1229         if ($file !~ m/\b(openpkg-.*|openpkg|vim|ncurses|openssh)\.spec$/) {
  1230             if ($s->{-refhdr} > 0) {
  1231                 if ($s->{-refcmd} == 0 and $s->{-refmac} == 0 and $s->{-refvar} == 0) {
  1232                     &lint_error($file, undef, undef, "source \"$str_hdr\" defined (file $s->{-file}), but never used" .
  1233                                 " (expected at least one reference via \"$str_cmd\", \"$str_mac\" or \"$str_var\")");
  1235                 elsif ($s->{-refcmd} > 1) {
  1236                     &lint_error($file, undef, undef, "source \"$str_hdr\" defined (file $s->{-file}), but used multiple times" .
  1237                                 " (expected just one reference via \"$str_cmd\")");
  1240             else {
  1241                 if ($s->{-refcmd}+$s->{-refmac}+$s->{-refvar} > 0) {
  1242                     &lint_error($file, undef, undef, "source \"$str_hdr\" not defined, but used ".
  1243                                 ($s->{-file} ? "(file ".$s->{-file}.")" : "").
  1244                                 " (expected at least one definition via \"$str_hdr\")");
  1250     #   check for existing of sources on filesystem
  1251     my $name = $file;
  1252     $name =~ s|.*/([^/]+)$|$1|s;
  1253     $name =~ s|\.spec$||s;
  1254     my $cvsdir = `$rpm --define 'name $name' --eval '%{_specdir}'`;
  1255     $cvsdir =~ s|\n$||s;
  1256     $cvsdir =~ s|/+$||s;
  1257     my $dstdir = `$rpm --define 'name $name' --eval '%{_sourcedir}'`;
  1258     $dstdir =~ s|\n$||s;
  1259     $dstdir =~ s|/+$||s;
  1260     foreach my $src (%{$S}) {
  1261         my $s = $S->{$src};
  1262         next if (not $s->{-refhdr});
  1263         if ($s->{-url} eq '' and not -f $cvsdir."/".$s->{-file} and not -f $dstdir."/".$s->{-file}) {
  1264             &lint_error($file, undef, undef, "source \"$s->{-file}\" neither found in SRC nor DST locations (you have to provide it)");
  1266         elsif ($s->{-url} eq '' and not -f $cvsdir."/".$s->{-file} and -f $dstdir."/".$s->{-file}) {
  1267             &lint_error($file, undef, undef, "source \"$s->{-file}\" not found in SRC, but in DST (expected it in SRC or want URL)");
  1269         elsif ($s->{-url} ne '' and not -f $cvsdir."/".$s->{-file} and not -f $dstdir."/".$s->{-file}) {
  1270             &lint_error($file, undef, undef, "source \"$s->{-file}\" neither found in SRC nor DST locations (you have to download it)");
  1272         elsif ($s->{-url} ne '' and -f $cvsdir."/".$s->{-file} and not -f $dstdir."/".$s->{-file}) {
  1273             &lint_error($file, undef, undef, "source \"$s->{-file}\" not found in DST, but in SRC (expected it in DST or want URL)");
  1275         #if ($s->{-url} ne '' and $s->{-file} !~ m/\d/) {
  1276         #    &lint_warning($file, undef, undef, "vendor source \"$s->{-file}\" not versioned");
  1277         #}
  1280     #   check for patch file naming
  1281     foreach my $src (%{$S}) {
  1282         my $s = $S->{$src};
  1283         next if (not $s->{-refhdr});
  1284         if (    $s->{-url} eq '' and $s->{-type} eq 'patch'
  1285             and $s->{-file} !~ m|^${name}\.patch(\.[^./]+)?$|) {
  1286             &lint_warning($file, undef, undef, "non-canonical patch filename \"$s->{-file}\"" .
  1287                           " (expected to match \"${name}\\.patch(\\.[^./]+)?\$\")");
  1292 ##  _________________________________________________________________
  1293 ##
  1294 ##  CHECK "digest": digest consistency
  1295 ##  _________________________________________________________________
  1296 ##
  1298 sub check_digest {
  1299     my ($file, $spec) = @_;
  1301     ##
  1302     ##  PREPARATION
  1303     ##
  1305     my $D = {};
  1306     my $S = {};
  1308     #   determine defines values
  1309     my $done = ''; my $this = ''; my $todo = $spec;
  1310     while ($todo =~ m/^\%(define|option)\s+(\S+)\s+(.+)\s*$/m) {
  1311         $done .= $`; $this = $&; $todo = $';
  1312         my ($var, $val) = ($2, $3);
  1313         $val =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge;
  1314         $D->{$var} = $val;
  1315         $done .= $this;
  1317     $done = ''; $this = ''; $todo = $spec;
  1318     while ($todo =~ m/^([a-zA-Z][a-zA-Z0-9_]+):\s+(.*)$/m) {
  1319         $done .= $`; $this = $&; $todo = $';
  1320         my ($var, $val) = (lc($1), $2);
  1321         $val =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge;
  1322         $D->{$var} = $val;
  1323         $done .= $this;
  1326     #   expand define values (allow one level of nesting)
  1327     $spec =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge;
  1328     $spec =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge;
  1330     #   determine locations
  1331     my $name = $file;
  1332     $name =~ s|.*/([^/]+)$|$1|s;
  1333     $name =~ s|\.spec$||s;
  1334     my $srcdir = `$rpm --define 'name $name' --eval '%{_specdir}'`;
  1335     $srcdir =~ s|\n$||s;
  1336     $srcdir =~ s|/+$||s;
  1337     my $dstdir = `$rpm --define 'name $name' --eval '%{_sourcedir}'`;
  1338     $dstdir =~ s|\n$||s;
  1339     $dstdir =~ s|/+$||s;
  1341     #   parse Source# and Patch# headers
  1342     $done = ''; $this = ''; $todo = $spec;
  1343     while ($todo =~ m/^(Source|Patch)(\d+):[ \t]*(.*?)$/m) {
  1344         $done .= $`; $this = $&; $todo = $';
  1345         my ($type, $num, $url) = (lc($1), $2, $3);
  1346         my $file;
  1347         if ($url =~ m|^.+/([^/]+)$|s) {
  1348             $file = "$dstdir/$1";
  1350         else {
  1351             $file = "$srcdir/$url";
  1353         if (not defined($S->{"$type$num"})) {
  1354             $S->{"$type$num"} = {
  1355                 -type   => $type,
  1356                 -num    => $num,
  1357                 -url    => $url,
  1358                 -file   => $file
  1359             };
  1361         else {
  1362             $S->{"$type$num"}->{-refhdr}++;
  1364         $done .= $this;
  1367     #   parse BuildPreReq headers
  1368     $done = ''; $this = ''; $todo = $spec;
  1369     while ($todo =~ m/^BuildPreReq:[ \t]+digest\(sha1:\%\{(.+?)\}\)[ \t]+=[ \t]+([0-9a-fA-F]+)$/m) {
  1370         $done .= $`; $this = $&; $todo = $';
  1371         my ($macro, $sha1) = ($1, $2);
  1373         my $url = $S->{lc($macro)}->{-url};
  1374         my $filename = $S->{lc($macro)}->{-file};
  1375         my $basename = $filename;
  1376         $basename =~ s/^.+\/([^\/]+)$/$1/s;
  1378         my $sha1_real = `$openssl sha1 $filename 2>/dev/null`;
  1379         $sha1_real =~ s/^SHA1\s*\(.+?\)\s*=\s*([0-9a-fA-F]+)\s*$/$1/s;
  1381         if (lc($sha1) ne lc($sha1_real)) {
  1382             &lint_error($file, undef, undef, "source \"$basename\" (\%{$macro}) has a digest(sha1) requirement of \"$sha1\" (expected \"$sha1_real\")");
  1385         $done .= $this;

mercurial