1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/openpkg/lint-spec.pl Tue Jul 31 12:23:42 2012 +0200 1.3 @@ -0,0 +1,1388 @@ 1.4 +## 1.5 +## lint-spec.pl -- OpenPKG *.spec File Checker 1.6 +## Copyright (c) 2000-2012 OpenPKG GmbH <http://openpkg.com/> 1.7 +## 1.8 +## This software is property of the OpenPKG GmbH, DE MUC HRB 160208. 1.9 +## All rights reserved. Licenses which grant limited permission to use, 1.10 +## copy, modify and distribute this software are available from the 1.11 +## OpenPKG GmbH. 1.12 +## 1.13 +## THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED 1.14 +## WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 1.15 +## MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 1.16 +## IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR 1.17 +## CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 1.18 +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 1.19 +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF 1.20 +## USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 1.21 +## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 1.22 +## OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 1.23 +## OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 1.24 +## SUCH DAMAGE. 1.25 +## 1.26 + 1.27 +# Perl run-time requirement 1.28 +require 5; 1.29 +BEGIN { 1.30 + eval "use Getopt::Long; use IO;"; 1.31 + if ($@) { 1.32 + print STDERR 1.33 + "lint-spec: ERROR: This command requires a full-size Perl installation!\n" . 1.34 + "lint-spec: HINT: Install OpenPKG \"perl\" package to use this command.\n"; 1.35 + exit(1); 1.36 + } 1.37 +} 1.38 + 1.39 +# OpenPKG instance prefix 1.40 +my $my_prefix = $ENV{'OPENPKG_PREFIX'}; 1.41 +delete $ENV{'OPENPKG_PREFIX'}; 1.42 + 1.43 +# program information 1.44 +my $progname = "lint-spec"; 1.45 +my $progvers = "1.0.0"; 1.46 + 1.47 +# parameters (defaults) 1.48 +my $version = 0; 1.49 +my $verbose = 0; 1.50 +my $help = 0; 1.51 +my $check = 'all'; 1.52 +my $tmpdir = ($ENV{TMPDIR} || $ENV{TEMPDIR} || "/tmp") . "/$progname"; 1.53 +my $rpm = "$my_prefix/bin/openpkg rpm"; 1.54 + 1.55 +# exception handling support 1.56 +$SIG{__DIE__} = sub { 1.57 + my ($err) = @_; 1.58 + $err =~ s|\s+at\s+.*||s if (not $verbose); 1.59 + print STDERR "$progname:ERROR: $err ". ($! ? "($!)" : "") . "\n"; 1.60 + exit(1); 1.61 +}; 1.62 + 1.63 +# command line parsing 1.64 +Getopt::Long::Configure("bundling"); 1.65 +my $result = GetOptions( 1.66 + 'V|version' => \$version, 1.67 + 'v|verbose' => \$verbose, 1.68 + 'h|help' => \$help, 1.69 + 'c|check=s' => \$check, 1.70 + 't|tmpdir=s' => \$tmpdir, 1.71 + 'r|rpm=s' => \$rpm, 1.72 +) || die "option parsing failed"; 1.73 +if ($help) { 1.74 + print "Usage: $progname [options] [SPECFILE ...]\n" . 1.75 + "Available options:\n" . 1.76 + " -v,--verbose enable verbose run-time mode\n" . 1.77 + " -h,--help print out this usage page\n" . 1.78 + " -c,--check=CHECKS select checks to perform (default='all')\n" . 1.79 + " -r,--rpm=FILE filesystem path to RPM program\n" . 1.80 + " -t,--tmpdir=PATH filesystem path to temporary directory\n" . 1.81 + " -V,--version print program version\n"; 1.82 + exit(0); 1.83 +} 1.84 +if ($version) { 1.85 + print "OpenPKG $progname $progvers\n"; 1.86 + exit(0); 1.87 +} 1.88 + 1.89 +# verbose message printing 1.90 +sub msg_verbose { 1.91 + my ($msg) = @_; 1.92 + print STDERR "$msg\n" if ($verbose); 1.93 +} 1.94 + 1.95 +# warning message printing 1.96 +sub msg_warning { 1.97 + my ($msg) = @_; 1.98 + print STDERR "$progname:WARNING: $msg\n"; 1.99 +} 1.100 + 1.101 +# error message printing 1.102 +sub msg_error { 1.103 + my ($msg) = @_; 1.104 + print STDERR "$progname:ERROR: $msg\n"; 1.105 +} 1.106 + 1.107 +# determine check list 1.108 +my @check_list = (qw( 1.109 + blank 1.110 + comment 1.111 + license 1.112 + header 1.113 + section 1.114 + preproc 1.115 + script 1.116 + global 1.117 + sources 1.118 + digest 1.119 +)); 1.120 +my @checks = (); 1.121 +if ($check eq 'all') { 1.122 + @checks = @check_list; 1.123 +} 1.124 +else { 1.125 + foreach my $c (split(/,/, $check)) { 1.126 + if (not grep($c, @check_list)) { 1.127 + die "invalid check \"$c\""; 1.128 + } 1.129 + push(@checks, $c); 1.130 + } 1.131 +} 1.132 + 1.133 +# global return code 1.134 +$main::GRC = 0; 1.135 + 1.136 +# iterate over all .spec files 1.137 +foreach my $filename (@ARGV) { 1.138 + my $io = new IO::File "<$filename" 1.139 + or die "unable to open file \"$filename\" for reading"; 1.140 + my $spec; { local $/ = undef; $spec = <$io>; } 1.141 + $io->close; 1.142 + foreach my $check (@checks) { 1.143 + eval "\&check_$check(\$filename, \$spec);"; 1.144 + } 1.145 +} 1.146 + 1.147 +# die gracefully 1.148 +exit($main::GRC); 1.149 + 1.150 +## _________________________________________________________________ 1.151 +## 1.152 +## COMMON SUBROUTINES 1.153 +## _________________________________________________________________ 1.154 +## 1.155 + 1.156 +sub lines { 1.157 + my ($txt) = @_; 1.158 + my $l = 0; 1.159 + $txt =~ s|\n|$l++, ''|sge; 1.160 + return $l; 1.161 +} 1.162 + 1.163 +sub lint_message { 1.164 + my ($type, $file, $done, $this, $msg) = @_; 1.165 + if (defined($done) and defined($this)) { 1.166 + my $start = &lines($done) + 1; 1.167 + my $end = $start + &lines($this); 1.168 + my $pos = $start; 1.169 + $pos .= "-". $end if ($end > $start); 1.170 + printf("%s:%s: %s:%s: %s\n", $progname, $type, $file, $pos, $msg); 1.171 + } 1.172 + else { 1.173 + printf("%s:%s: %s: %s\n", $progname, $type, $file, $msg); 1.174 + } 1.175 +} 1.176 + 1.177 +sub lint_warning { 1.178 + my ($file, $done, $this, $msg) = @_; 1.179 + &lint_message("WARNING", $file, $done, $this, $msg); 1.180 + $main::GRC = 1 if ($main::GRC < 1); 1.181 +} 1.182 + 1.183 +sub lint_error { 1.184 + my ($file, $done, $this, $msg) = @_; 1.185 + &lint_message("ERROR", $file, $done, $this, $msg); 1.186 + $main::GRC = 2 if ($main::GRC < 2); 1.187 +} 1.188 + 1.189 +## _________________________________________________________________ 1.190 +## 1.191 +## CHECK "blank": whitespace and blank lines 1.192 +## _________________________________________________________________ 1.193 +## 1.194 + 1.195 +sub check_blank { 1.196 + my ($file, $spec) = @_; 1.197 + 1.198 + # check for CR-LF combination 1.199 + my $done = ''; my $this = ''; my $todo = $spec; 1.200 + while ($todo =~ m/\r\n/s) { 1.201 + $done .= $`; $this = $&; $todo = $'; 1.202 + &lint_warning($file, $done, $this, "carriage-return (CR, 0x0d) line-feed (NL, 0x0a) combination (expected just line-feed)"); 1.203 + $done .= $this; 1.204 + } 1.205 + 1.206 + # check for multiple blank lines 1.207 + $done = ''; $this = ''; $todo = $spec; 1.208 + while ($todo =~ m/(\r?\n[ \t]*){3,}/s) { 1.209 + $done .= $`; $this = $&; $todo = $'; 1.210 + &lint_warning($file, $done, $this, "multiple subsequent blank lines (expected single blank line)"); 1.211 + $done .= $this; 1.212 + } 1.213 + 1.214 + # check for trailing whitespaces 1.215 + $done = ''; $this = ''; $todo = $spec; 1.216 + while ($todo =~ m/[ \t]+\r?\n/s) { 1.217 + $done .= $`; $this = $&; $todo = $'; 1.218 + if ($done eq '' or $done =~ m|\n$|s) { 1.219 + &lint_warning($file, $done, $this, "whitespace on empty line (expected none)"); 1.220 + } 1.221 + else { 1.222 + &lint_warning($file, $done, $this, "trailing whitespace (expected none)"); 1.223 + } 1.224 + $done .= $this; 1.225 + } 1.226 + 1.227 + # check for bogus line continuations 1.228 + $done = ''; $this = ''; $todo = $spec; 1.229 + while ($todo =~ m/\\[ \t]*\r?\n(?=[ \t]*\r?\n)/s) { 1.230 + $done .= $`; $this = $&; $todo = $'; 1.231 + &lint_warning($file, $done, $this, "bogus line continuation for following empty line (expect no line continuation)"); 1.232 + $done .= $this; 1.233 + } 1.234 + 1.235 + # check for leading whitespaces before line continuations 1.236 + $done = ''; $this = ''; $todo = $spec; 1.237 + while ($todo =~ m/[ \t]{2,}\\[ \t]*\r?\n/s) { 1.238 + $done .= $`; $this = $&; $todo = $'; 1.239 + &lint_warning($file, $done, $this, "multiple leading whitespace before line continuation (expected just a single space)"); 1.240 + $done .= $this; 1.241 + } 1.242 + 1.243 + # check for leading tabs 1.244 + $done = ''; $this = ''; $todo = $spec; 1.245 + while ($todo =~ m/^ *\t+ *[^ \t]/m) { 1.246 + $done .= $`; $this = $&; $todo = $'; 1.247 + &lint_warning($file, $done, $this, "leading tabs (expected spaces)"); 1.248 + $done .= $this; 1.249 + } 1.250 + 1.251 + # check for mandatory/wished trailing blank line 1.252 + if ($spec !~ m|\n\n$|) { 1.253 + &lint_warning($file, $spec, "", "mandatory/wished trailing blank line missing (expected one)"); 1.254 + } 1.255 +} 1.256 + 1.257 +## _________________________________________________________________ 1.258 +## 1.259 +## CHECK "comment": sharp-comments 1.260 +## _________________________________________________________________ 1.261 +## 1.262 + 1.263 +sub check_comment { 1.264 + my ($file, $spec) = @_; 1.265 + 1.266 + # check for comment indentation and contents 1.267 + my $done = ''; my $this = ''; my $todo = $spec; 1.268 + while ($todo =~ m/^([ \t]*)(#+)([ \t]*)(.*?)$/m) { 1.269 + $done .= $`; $this = $&; $todo = $'; 1.270 + my ($lead, $sharp, $pad, $text) = ($1, $2, $3, $4); 1.271 + if (length($lead) % 2 != 0) { 1.272 + &lint_warning($file, $done, $this, "incorrect comment indentation (expected a multiple of 2 spaces)"); 1.273 + } 1.274 + if (length($lead) > 1 && length($sharp) > 1) { 1.275 + &lint_warning($file, $done, $this, "indented comment has introduced with multiple sharps (expected single sharp character)"); 1.276 + } 1.277 + if (length($pad.$text) > 0 && length($sharp.$pad) % 4 != 0) { 1.278 + &lint_warning($file, $done, $this, "incorrect comment text padding (expected a multiple of 4 sharps or spaces)"); 1.279 + } 1.280 + if (length($pad) == 0 && length($text) > 0) { 1.281 + &lint_warning($file, $done, $this, "missing leading space before comment text (expected padding spaces)"); 1.282 + } 1.283 + if (length($pad) > 0 && length($text) == 0) { 1.284 + &lint_warning($file, $done, $this, "empty comment text (expected a reasonable text)"); 1.285 + } 1.286 + $done .= $this; 1.287 + } 1.288 + 1.289 + # check for comment contents only 1.290 + if ($file !~ m|openpkg\.spec$|) { 1.291 + $done = ''; $this = ''; $todo = $spec; 1.292 + while ($todo =~ m/\n\n[ \t]*#[ \t]+([^\n]+)(?!\n([ \t]*#))/s) { 1.293 + $done .= $`; $this = $&; $todo = $'; 1.294 + my $text = $1; 1.295 + if (length($text) > 0 and $text =~ m|^[A-Z][^A-Z]|) { 1.296 + &lint_warning($file, $done, $this, "comment text starts with upper-case letter (expected lower-case letter)"); 1.297 + } 1.298 + $done .= $this; 1.299 + } 1.300 + } 1.301 +} 1.302 + 1.303 +## _________________________________________________________________ 1.304 +## 1.305 +## CHECK "license": license header 1.306 +## _________________________________________________________________ 1.307 +## 1.308 + 1.309 +sub check_license { 1.310 + my ($file, $spec) = @_; 1.311 + 1.312 + my $name = "[a-z][a-z0-9-]*"; 1.313 + if ($file =~ m|^.*/([a-z][a-z0-9-]*)\.spec$|) { 1.314 + $name = $1; 1.315 + } 1.316 + elsif ($file =~ m|^([a-z][a-z0-9-]*)\.spec$|) { 1.317 + $name = $1; 1.318 + } 1.319 + my $re = ""; 1.320 + $re .= "##\\n"; 1.321 + $re .= "## $name\\.spec -- OpenPKG RPM Package Specification\\n"; 1.322 + $re .= "## Copyright \\(c\\) 200[0-9]-2012 OpenPKG Foundation e\.V\. <http://openpkg\\.net/>\\n"; 1.323 + $re .= "##\\n"; 1.324 + $re .= "## Permission to use, copy, modify, and distribute this software for\\n"; 1.325 + $re .= "## any purpose with or without fee is hereby granted, provided that\\n"; 1.326 + $re .= "## the above copyright notice and this permission notice appear in all\\n"; 1.327 + $re .= "## copies\\.\\n"; 1.328 + $re .= "##\\n"; 1.329 + $re .= "## THIS SOFTWARE IS PROVIDED \\`\\`AS IS'' AND ANY EXPRESSED OR IMPLIED\\n"; 1.330 + $re .= "## WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\\n"; 1.331 + $re .= "## MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED\\.\\n"; 1.332 + $re .= "## IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR\\n"; 1.333 + $re .= "## CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\\n"; 1.334 + $re .= "## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES \\(INCLUDING, BUT NOT\\n"; 1.335 + $re .= "## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF\\n"; 1.336 + $re .= "## USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION\\) HOWEVER CAUSED AND\\n"; 1.337 + $re .= "## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\\n"; 1.338 + $re .= "## OR TORT \\(INCLUDING NEGLIGENCE OR OTHERWISE\\) ARISING IN ANY WAY OUT\\n"; 1.339 + $re .= "## OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF\\n"; 1.340 + $re .= "## SUCH DAMAGE\\.\\n"; 1.341 + $re .= "##\\n"; 1.342 + if ($name ne "openpkg" and $spec !~ m|^$re|os) { 1.343 + &lint_warning($file, "", "", "invalid license header"); 1.344 + } 1.345 +} 1.346 + 1.347 +## _________________________________________________________________ 1.348 +## 1.349 +## CHECK "header": RPM headers 1.350 +## _________________________________________________________________ 1.351 +## 1.352 + 1.353 +sub check_header { 1.354 + my ($file, $spec) = @_; 1.355 + 1.356 + my @headers = (qw( 1.357 + m:Name:^[a-z][a-z0-9-]*$ 1.358 + m:Summary:^[A-Z]\S*(\s+([A-Z]\S*|of|for|from|in|at|on|\(\S+\)))* 1.359 + m:URL:^((https?|ftp)://.+|-)$ 1.360 + m:Vendor:.+ 1.361 + m:Packager:^(OpenPKG\sFoundation\se\.V\.|OpenPKG\sGmbH)$ 1.362 + m:Distribution:^(OpenPKG|OpenPKG\sCommunity|OpenPKG\sEnterprise)$ 1.363 + m:Class:^(BOOT|CORE|BASE|PLUS|EVAL|JUNK|PRIV)$ 1.364 + m:Group:^[A-Z][a-zA-Z0-9]+$ 1.365 + m:License:.+ 1.366 + m:Version:^[^-]+$ 1.367 + 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+|%\{[^\}]+\})$ 1.368 + o:Source\d+:^((https?|ftp)://.+|[^/]+)$ 1.369 + o:Patch\d+:^((https?|ftp)://.+|[^/]+)$ 1.370 + o:Prefix:^%{l_prefix}$ 1.371 + o:BuildRoot:^%{l_buildroot}$ 1.372 + 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+)?)+)$ 1.373 + 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+)?)+$ 1.374 + o:AutoReq:^no$ 1.375 + o:AutoReqProv:^no$ 1.376 + o:Provides:^((,\s)?([a-z][a-z0-9-]*(\:\:with_[a-z][a-z0-9_]+)?|[A-Z][A-Z0-9-]*)(\s==?\s\S+)?)+$ 1.377 + o:Conflicts:^((,\s)?([a-z][a-z0-9-]*(\:\:with_[a-z][a-z0-9_]+)?|[A-Z][A-Z0-9-]*)(\s(>=?|==?|<=?|!=)\s\S+)?)+$ 1.378 + )); 1.379 + my @headers_def = (qw( 1.380 + Summary:^Badly\sPackaged\sProgram$ 1.381 + Vendor:^John\sDoe$ 1.382 + Group:^Unknown$ 1.383 + License:^DoePL$ 1.384 + )); 1.385 + my @headers_mult = (qw( 1.386 + BuildPreReq PreReq Provides Conflicts 1.387 + )); 1.388 + 1.389 + my @seen = (); 1.390 + my %count = (); 1.391 + my $done = ''; my $this = ''; my $todo = $spec; 1.392 + while ($todo =~ m/^(\S+):([ \t]*)(.*?)$/m) { 1.393 + $done .= $`; $this = $&; $todo = $'; 1.394 + my ($header, $pad, $value) = ($1, $2, $3); 1.395 + 1.396 + # check for layouting 1.397 + if (length($value) == 0) { 1.398 + &lint_error($file, $done, $this, "empty RPM header value"); 1.399 + } 1.400 + if (length($header.":".$pad) != 14) { 1.401 + &lint_warning($file, $done, $this, "invalid RPM header name/value padding (expected value at column 15)"); 1.402 + } 1.403 + 1.404 + # check for valid header name and value 1.405 + if (not grep { $header =~ m|^$_$|s } map { m/^[^:]:([^:]+):/, $1 } @headers) { 1.406 + &lint_error($file, $done, $this, "invalid RPM header name \"$header\""); 1.407 + } 1.408 + else { 1.409 + my $hn = quotemeta((grep { $header =~ m|^$_$|s } map { m/^[^:]+:([^:]+):/, $1 } @headers)[0]); 1.410 + my $re = (map { m/^[^:]+:${hn}:(.+)$/s } @headers)[0]; 1.411 + my $re_match = $re; 1.412 + $re_match =~ s|^\(|(?:|sg; 1.413 + $re_match =~ s|([^\\])\(|\1(?:|sg; 1.414 + if ( not ($file =~ m|openpkg\.spec$| and $header eq 'Provides' and $value eq '%{l_prefix}') 1.415 + and $value !~ m|${re_match}|s) { 1.416 + &lint_warning($file, $done, $this, "RPM header \"$header\": " . 1.417 + "invalid value \"$value\" (expected to match \"$re\")"); 1.418 + } 1.419 + my $re_def = (map { m/^${hn}:(.+)$/s } @headers_def)[0]; 1.420 + if (defined($re_def)) { 1.421 + my $re_def_match = $re_def; 1.422 + $re_def_match =~ s|^\(|(?:|sg; 1.423 + $re_def_match =~ s|([^\\])\(|\1(?:|sg; 1.424 + if ($value =~ m|${re_def_match}|s) { 1.425 + &lint_warning($file, $done, $this, "RPM header \"$header\": " . 1.426 + "default value \"$value\" (expected to match \"$re\", except for this)"); 1.427 + } 1.428 + } 1.429 + if ($header =~ m/^Vendor/ and $value =~ m/et\sal/ and $value !~ m/et\sal\./) { 1.430 + &lint_warning($file, $done, $this, "RPM header \"$header\": " . 1.431 + "has value \"$value\" (expected \"et al.\" with dot)"); 1.432 + } 1.433 + } 1.434 + push(@seen, $header); 1.435 + $count{$header}++; 1.436 + $done .= $this; 1.437 + } 1.438 + 1.439 + # check for existence of mandatory headers 1.440 + foreach my $header (@headers) { 1.441 + my ($type, $name, $regex) = split(/:/, $header, 3); 1.442 + if ($type eq 'm') { 1.443 + if ($file =~ m|openpkg\.spec$| and 1.444 + ($name eq "BuildPreReq" or $name eq "PreReq")) { 1.445 + # the bootstrap package is an obvious exception 1.446 + next; 1.447 + } 1.448 + if (not grep(/^$name$/, @seen)) { 1.449 + &lint_warning($file, undef, undef, "mandatory RPM header \"$name\" not found"); 1.450 + } 1.451 + } 1.452 + } 1.453 + 1.454 + # check for multiple occurrence headers 1.455 + foreach my $seen (@seen) { 1.456 + if ($count{$seen} > 1 and not (grep { $_ eq $seen } @headers_mult)) { 1.457 + &lint_error($file, undef, undef, "RPM header \"$seen\" occurs multiple times (expected just once)"); 1.458 + } 1.459 + } 1.460 +} 1.461 + 1.462 +## _________________________________________________________________ 1.463 +## 1.464 +## CHECK "section": RPM sections 1.465 +## _________________________________________________________________ 1.466 +## 1.467 + 1.468 +sub check_section { 1.469 + my ($file, $spec) = @_; 1.470 + 1.471 + my $require = qq{ 1.472 + (%define,)* 1.473 + Name:, 1.474 + Summary:, 1.475 + URL:, 1.476 + Vendor:, 1.477 + Packager:, 1.478 + Distribution:, 1.479 + Class:, 1.480 + Group:, 1.481 + License:, 1.482 + Version:, 1.483 + Release:, 1.484 + (%option,)* 1.485 + (%define,|%undefine,)* 1.486 + (Source\\d+:,)* 1.487 + (Patch\\d+:,)* 1.488 + (%NoSource,)* 1.489 + (%NoPatch,)* 1.490 + (Prefix:,)? 1.491 + (BuildRoot:,)? 1.492 + ((BuildPreReq:,) 1.493 + |(PreReq:,))* 1.494 + (AutoReq:,)? 1.495 + (AutoReqProv:,)? 1.496 + (Provides:,)* 1.497 + (Conflicts:,)* 1.498 + %description, 1.499 + (%track,)? 1.500 + %prep, 1.501 + %build, 1.502 + %install, 1.503 + (%check,)? 1.504 + %files, 1.505 + %clean, 1.506 + (%pre,)? 1.507 + (%post,)? 1.508 + (%preun,)? 1.509 + (%postun,)? 1.510 + (%trigger,)? 1.511 + (%triggerin,)? 1.512 + (%triggerun,)? 1.513 + (%triggerpostun,)? 1.514 + (%verifyscript,)? 1.515 + }; 1.516 + 1.517 + # check for order of headers 1.518 + my $sections = ""; 1.519 + my $done = ''; my $this = ''; my $todo = $spec; 1.520 + while ($todo =~ m/^(\S+:|%\S+).*$/m) { 1.521 + $done .= $`; $this = $&; $todo = $'; 1.522 + my $section = $1; 1.523 + next if ($section =~ m/^%(if|else|endif)/); 1.524 + $sections .= "$section,"; 1.525 + $done .= $this; 1.526 + } 1.527 + my $regex = $require; 1.528 + $regex =~ s|\s+||sg; 1.529 + if ($sections !~ m/^$regex$/s) { 1.530 + $regex =~ s|,| |sg; 1.531 + &lint_error($file, undef, undef, "invalid RPM section order: $sections (expected \"$regex\")"); 1.532 + } 1.533 +} 1.534 + 1.535 +## _________________________________________________________________ 1.536 +## 1.537 +## CHECK "preproc": RPM macro pre-processor 1.538 +## _________________________________________________________________ 1.539 +## 1.540 + 1.541 +sub check_preproc { 1.542 + my ($file, $spec) = @_; 1.543 + 1.544 + my $done = ''; my $this = ''; my $todo = $spec; 1.545 + while ($todo =~ m/^(%(?:if|else|endif|define|undefine|option))(.*)$/m) { 1.546 + $done .= $`; $this = $&; $todo = $'; 1.547 + my ($cmd, $args) = ($1, $2); 1.548 + if ($cmd eq '%if') { 1.549 + # FIXME: either try to really parse the boolean expression 1.550 + # FIXME: or at least try to guess its correct syntax 1.551 + } 1.552 + elsif ($cmd eq '%else' or $cmd eq '%endif') { 1.553 + if (length($args) > 0) { 1.554 + &lint_warning($file, $done, $this, "garbage after pre-processor directive " . 1.555 + "\"$cmd\" (expected no arguments)"); 1.556 + } 1.557 + } 1.558 + elsif ($cmd eq '%undefine') { 1.559 + if ($args =~ m|^\s+(\S+)\s*$|) { 1.560 + my $var = $1; 1.561 + if ($var !~ m/^(V|with)_[a-z][a-zA-Z0-9_]*$/) { 1.562 + &lint_warning($file, $done, $this, "unusually named macro: \"$var\" " . 1.563 + "(expected \"(V|with)_[a-z][a-zA-Z0-9_]*\")"); 1.564 + next; 1.565 + } 1.566 + } 1.567 + else { 1.568 + &lint_error($file, $done, $this, "invalid number of arguments to pre-processor " . 1.569 + "directive \"$cmd\" (expected exactly 1 argument)"); 1.570 + } 1.571 + } 1.572 + elsif ($cmd eq '%define' or $cmd eq '%option') { 1.573 + if ($args =~ m|^\s+(\S+)\s+(.*)$|) { 1.574 + my ($var, $val) = ($1, $2); 1.575 + if ($var !~ m/^(V|with)_[a-z][a-zA-Z0-9_]*$/) { 1.576 + &lint_warning($file, $done, $this, "unusually named macro: \"$var\" " . 1.577 + "(expected \"(V|with)_[a-z][a-zA-Z0-9_]*\")"); 1.578 + next; 1.579 + } 1.580 + if (length($val) == 0) { 1.581 + &lint_error($file, $done, $this, "empty macro value"); 1.582 + } 1.583 + } 1.584 + else { 1.585 + &lint_error($file, $done, $this, "invalid number of arguments to pre-processor " . 1.586 + "directive \"$cmd\" (expected exactly 2 arguments)"); 1.587 + } 1.588 + } 1.589 + $done .= $this; 1.590 + } 1.591 + 1.592 + # check correct if/endif nesting 1.593 + my @stack = (); 1.594 + $done = ''; $this = ''; $todo = $spec; 1.595 + while ($todo =~ m/^(%(?:if|else|endif|define|undefine|option)).*$/m) { 1.596 + $done .= $`; $this = $&; $todo = $'; 1.597 + my $directive = $1; 1.598 + if ($directive eq '%if') { 1.599 + push(@stack, &lines($done . $this)); 1.600 + } 1.601 + elsif ($directive eq '%endif') { 1.602 + if (@stack == 0) { 1.603 + &lint_error($file, $done, $this, "found \%endif without corresponding opening \%if"); 1.604 + last; 1.605 + } 1.606 + pop(@stack); 1.607 + } 1.608 + $done .= $this; 1.609 + } 1.610 + my $line; 1.611 + while (defined($line = pop(@stack))) { 1.612 + &lint_error($file, undef, undef, "\%if at line $line never closed by \%endif"); 1.613 + } 1.614 + 1.615 + # check for indented preprocessor constructs 1.616 + $done = ''; $this = ''; $todo = $spec; 1.617 + while ($todo =~ m/^[ \t]+(%(?:if|else|endif|define|undefine|option)).*$/m) { 1.618 + $done .= $`; $this = $&; $todo = $'; 1.619 + my $directive = $1; 1.620 + &lint_error($file, $done, $this, "found indented \"$directive\" preprocessor directive (expected no indentation)"); 1.621 + $done .= $this; 1.622 + } 1.623 +} 1.624 + 1.625 +## _________________________________________________________________ 1.626 +## 1.627 +## CHECK "script": shell scripts 1.628 +## _________________________________________________________________ 1.629 +## 1.630 + 1.631 +sub check_script { 1.632 + my ($file, $spec) = @_; 1.633 + 1.634 + my $done = ''; my $this = ''; my $todo = $spec; 1.635 + 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) { 1.636 + $done .= $`; $this = $&; $todo = $'; 1.637 + my ($section, $args, $script) = ($1, $2, $3); 1.638 + 1.639 + # perform checks for a single script section 1.640 + &check_script_section($file, $done, $this, $section, $args, $script); 1.641 + 1.642 + $done .= $this; 1.643 + } 1.644 +} 1.645 + 1.646 +sub check_script_section { 1.647 + my ($file, $outer_done, $outer_this, $section, $args, $script) = @_; 1.648 + 1.649 + # skip plain-text/non-scripting section %description 1.650 + return if ($section eq '%description'); 1.651 + 1.652 + # remove comment contents 1.653 + $outer_this =~ s|^[ \t]*#[^\n]*||mg; 1.654 + 1.655 + # check shell redirections 1.656 + my $done = $outer_done; my $this = ''; my $todo = $outer_this; 1.657 + while ( $todo =~ m/[ \t]+(\d+)?[><][ \t]+\S+/s 1.658 + or $todo =~ m/[ \t]+[><](\&\d+)?[ \t]+\S+/s) { 1.659 + $done .= $`; $this = $&; $todo = $'; 1.660 + &lint_warning($file, $done, $this, "section $section: whitespace after shell redirection (expected none)"); 1.661 + $done .= $this; 1.662 + } 1.663 + 1.664 + # utility function: extract a single shell command 1.665 + sub command_extract { 1.666 + my ($script) = @_; 1.667 + my $cmd = ''; 1.668 + while ($script ne '') { 1.669 + $script =~ s/^([ \t]*'[^']*')/ $cmd .= $1, ''/se && next; 1.670 + $script =~ s/^([ \t]*"[^"]*")/ $cmd .= $1, ''/se && next; 1.671 + $script =~ s/^([ \t]*[^ \t;\)\\\r\n]+)/$cmd .= $1, ''/se && next; 1.672 + $script =~ s/^([ \t]*\\[ \t]*\r?\n)/ $cmd .= $1, ''/se && next; 1.673 + last; 1.674 + } 1.675 + return ($cmd, $script); 1.676 + } 1.677 + 1.678 + # utility function: join a multi-line command 1.679 + sub multiline_join { 1.680 + my ($cmd) = @_; 1.681 + $cmd =~ s/([ \t]*\\[ \t]*\r?\n[ \t]*)/ /sg; 1.682 + return $cmd; 1.683 + } 1.684 + 1.685 + # utility function: split command into arguments 1.686 + sub shell_tokenize { 1.687 + my ($cmd) = @_; 1.688 + my @cmd = (); 1.689 + while ($cmd ne '') { 1.690 + $cmd =~ s/^\s*('[^']*')/push(@cmd, $1), ''/se && next; 1.691 + $cmd =~ s/^\s*("[^"]*")/push(@cmd, $1), ''/se && next; 1.692 + $cmd =~ s/^\s*(\S+)/ push(@cmd, $1), ''/se && next; 1.693 + $cmd =~ s/^\s*$/ ''/se && last; 1.694 + } 1.695 + return @cmd; 1.696 + } 1.697 + 1.698 + # check filesystem path style 1.699 + $done = $outer_done; $this = ''; $todo = $outer_this; 1.700 + while ($todo =~ m/\%\{l_shtool\}\s+(\w+)\s+/s) { 1.701 + $done .= $`; $this = $&; $todo = $'; 1.702 + ($this, $todo) = &command_extract($this . $todo); 1.703 + 1.704 + # check for shtool options with no space before argument 1.705 + my $subthis = $this; 1.706 + $subthis =~ s/\%{[A-Za-z][A-Za-z0-9_]*}//sg; 1.707 + $subthis =~ s/\%{[A-Za-z][A-Za-z0-9_]*(?:\s+[^}]+?)?}//sg; 1.708 + # remove content of quoted arguments as they might contain unrelated things to catch 1.709 + $subthis =~ s/'[^']*'/ARG/sg; 1.710 + $subthis =~ s/"[^"]*"/ARG/sg; 1.711 + $subthis =~ s/`[^`]*`/ARG/sg; 1.712 + # remove us completely if line ends with a quote as this indicates we are a quoted argument to ourselfs 1.713 + $subthis =~ s/[^']*'$//s; 1.714 + $subthis =~ s/[^"]*"$//s; 1.715 + $subthis =~ s/[^`]*`$//s; 1.716 + $subthis =~ s/[^']*' \\\n//s; 1.717 + $subthis =~ s/[^"]*" \\\n//s; 1.718 + $subthis =~ s/[^`]*` \\\n//s; 1.719 + # catch command termination by semicolon, pipe, or, and; 1.720 + $subthis =~ s/[;|&].*$//s; 1.721 + if ($subthis =~ m/\s-[a-zA-Z]\S/) { 1.722 + &lint_warning($file, $done, $this, "found use of shtool option with space omitted before argument"); 1.723 + } 1.724 + 1.725 + # openpkg-rc is special because does bootstrap things 1.726 + last if ($file =~ m|openpkg-rc\.spec$|); 1.727 + 1.728 + my @cmd = &shell_tokenize(&multiline_join($this)); 1.729 + if ($cmd[1] eq 'mkdir') { 1.730 + # join flags with their arguments 1.731 + for (my $i = 2; $i <= $#cmd; $i++) { 1.732 + if ($cmd[$i] eq '-m') { 1.733 + splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]); 1.734 + } 1.735 + } 1.736 + # check paths 1.737 + for (my $i = $#cmd; $i > 1 and $cmd[$i] !~ m|^-| and $cmd[$i] !~ m|^\d+$|; $i--) { 1.738 + if ($cmd[$i] =~ m|/$|) { 1.739 + &lint_warning($file, $done, $this, "section $section: superfluous trailing slash on " . 1.740 + "created path in \"shtool mkdir\" command (expected none)"); 1.741 + } 1.742 + } 1.743 + } 1.744 + elsif ($cmd[1] eq 'install') { 1.745 + # join flags with their arguments 1.746 + for (my $i = 2; $i <= $#cmd; $i++) { 1.747 + if ($cmd[$i] =~ m/-(e|m|o|g)$/) { 1.748 + splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]); 1.749 + } 1.750 + elsif ($cmd[$i] eq '%{SOURCE') { 1.751 + splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]); 1.752 + } 1.753 + elsif ($cmd[$i] eq '%{l_value') { 1.754 + while ($i < $#cmd and $cmd[$i+1] !~ m|\}$|s) { 1.755 + splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]); 1.756 + } 1.757 + splice(@cmd, $i, 2); 1.758 + $i--; 1.759 + } 1.760 + } 1.761 + 1.762 + # determine last path argument 1.763 + my $i = $#cmd; $i-- while ($i > 1 and $cmd[$i] !~ m|^-| and $cmd[$i] !~ m|^\d+$|); 1.764 + 1.765 + # check paths 1.766 + if (($#cmd - $i) > 2 and $cmd[-1] !~ m|/$|) { 1.767 + # simple case: multiple sources require target to be a directory 1.768 + &lint_warning($file, $done, $this, "section $section: missing trailing slash on " . 1.769 + "destination path in \"shtool install\" command (expected one) 1"); 1.770 + } 1.771 + elsif (($#cmd - $i) == 2 and $cmd[-1] !~ m|/$|) { 1.772 + # complex case: single source, so we can check only known destination paths 1.773 + if ( $cmd[-1] =~ m/\%\{l_prefix\}\/(bin|cgi|include|info|lib|pub|sbin)$/ 1.774 + or $cmd[-1] =~ m/\%\{l_prefix\}\/(etc|libexec|man|share|var)(\/[^\/]+)?$/) { 1.775 + &lint_warning($file, $done, $this, "section $section: missing trailing slash on " . 1.776 + "destination path in \"shtool install\" command (expected one) 2"); 1.777 + } 1.778 + } 1.779 + else { 1.780 + # special case: if any path contains '*', then globbing might occur 1.781 + for (my $i = 2; $i <= $#cmd; $i++) { 1.782 + if ($cmd[$i] =~ m/\*/ and $cmd[-1] !~ m|/$|) { 1.783 + &lint_warning($file, $done, $this, "section $section: missing trailing slash on " . 1.784 + "destination path in \"shtool install\" command (expected one) 3"); 1.785 + last; 1.786 + } 1.787 + } 1.788 + } 1.789 + } 1.790 + $done .= $this; 1.791 + } 1.792 + 1.793 + # check for redundant 'export PATH' statements 1.794 + #$done = $outer_done; $this = ''; $todo = $outer_this; 1.795 + #while ($todo =~ m/\bexport[ \t]+([a-zA-Z_][a-zA-Z0-9_]*[ \t]+)*PATH\b/s) { 1.796 + # $done .= $`; $this = $&; $todo = $'; 1.797 + # &lint_warning($file, $done, $this, "section $section: redundant \"export PATH\" statement (expected none)"); 1.798 + # $done .= $this; 1.799 + #} 1.800 + 1.801 + # check for obsolete cpp build flags 1.802 + $done = $outer_done; $this = ''; $todo = $outer_this; 1.803 + while ($todo =~ m/-I\%\{l_prefix\}\/include(\/([^ \t"';]+?))?[ \t"';]/s) { 1.804 + $done .= $`; $this = $&; $todo = $'; 1.805 + my ($subdir) = $2; 1.806 + &lint_warning($file, $done, $this, "section $section: hard-coded C pre-processor path option " . 1.807 + "(use \"%{l_cppflags".($subdir ? " $subdir" : "")."}\" instead)"); 1.808 + $done .= $this; 1.809 + } 1.810 + 1.811 + # check for obsolete ld build flags 1.812 + $done = $outer_done; $this = ''; $todo = $outer_this; 1.813 + while ($todo =~ m/-L\%\{l_prefix\}\/lib(\/([^ \t"';]+?))?[ \t"';]/s) { 1.814 + $done .= $`; $this = $&; $todo = $'; 1.815 + my ($subdir) = $2; 1.816 + &lint_warning($file, $done, $this, "section $section: hard-coded linker path option " . 1.817 + "(use \"%{l_ldflags".($subdir ? " $subdir" : "")."}\" instead)"); 1.818 + $done .= $this; 1.819 + } 1.820 + 1.821 + # check for "raw" tool usage 1.822 + if ($section ne '%description' and $file !~ m|openpkg\.spec$|) { 1.823 + foreach my $token (&shell_tokenize($outer_this)) { 1.824 + if ($token =~ m/^(rpmtool|shtool|curl|bash|gzip|bzip2|tar|cc|cxx|make|patch)$/s) { 1.825 + &lint_warning($file, undef, undef, "section $section: raw usage of standard tool \"$token\"" . 1.826 + " (use \"%{l_${token}}\" instead)"); 1.827 + } 1.828 + } 1.829 + } 1.830 + 1.831 + # check for setup and patch macro usage 1.832 + if ($section eq '%prep') { 1.833 + $done = $outer_done; $this = ''; $todo = $outer_this; 1.834 + my @tokens = &shell_tokenize($outer_this); 1.835 + while ($todo =~ m/([^\n]*)\n/s) { 1.836 + $done .= $`; $this = $&; $todo = $'; 1.837 + if ($this =~ m/^ *%setup.* .*$/) { 1.838 + &lint_warning($file, undef, undef, "section $section: multiple spaces in \"\%setup\" macro" . 1.839 + " (reduce to single space)"); 1.840 + } 1.841 + if ($this =~ m/^ *%setup.*-[ab]$/ and grep(/^\%setup+$/, @tokens) == 1) { 1.842 + &lint_warning($file, undef, undef, "section $section: -[ab] option superflous for single \"\%setup\" macro" . 1.843 + " (remove it)"); 1.844 + } 1.845 + if ($this =~ m/^ *%patch.* .*$/) { 1.846 + &lint_warning($file, undef, undef, "section $section: multiple spaces in \"\%patch\" macro" . 1.847 + " (reduce to single space)"); 1.848 + } 1.849 + if ($this =~ m/^ *%patch.*-p +[01]/) { 1.850 + &lint_warning($file, undef, undef, "section $section: syntactically wrong space in \"\%patch\" macro" . 1.851 + " (use -pN)"); 1.852 + } 1.853 + if ($this =~ m/^ *%patch.*-P 0\s*$/ and grep(/^\%patch+$/, @tokens) == 1) { 1.854 + &lint_warning($file, undef, undef, "section $section: syntactically wrong space in \"\%patch\" macro" . 1.855 + " (omit -P 0 for single patch)"); 1.856 + } 1.857 + if ($this =~ m/^ *%patch.*-P [^0-9]/ and grep(/^\%patch+$/, @tokens) > 1) { 1.858 + &lint_warning($file, undef, undef, "section $section: syntactically wrong space in \"\%patch\" macro" . 1.859 + " (use -P for multiple patches)"); 1.860 + } 1.861 + $done .= $this; 1.862 + } 1.863 + if (grep(/^\%setup\d+$/, @tokens)) { 1.864 + &lint_warning($file, undef, undef, "section $section: numbered \"\%setup\" macro" . 1.865 + " (do not use %setup directly followed by a number, replace with %setup ... -[ab])"); 1.866 + } 1.867 + if (grep(/^\%patch\d+$/, @tokens)) { 1.868 + &lint_warning($file, undef, undef, "section $section: numbered \"\%patch\" macro" . 1.869 + " (do not use %patch directly followed by a number, replace with %patch ... -P)"); 1.870 + } 1.871 + } 1.872 + 1.873 + # check for hard-coded prefix in %pre[un],%post[un] scripts 1.874 + if ($section =~ m/^\%(preun|postun|pre|post)$/s and $file !~ m|openpkg\.spec$|) { 1.875 + $done = $outer_done; $this = ''; $todo = $outer_this; 1.876 + while ($todo =~ m/\%\{l_prefix\}/s) { 1.877 + $done .= $`; $this = $&; $todo = $'; 1.878 + &lint_warning($file, $done, $this, "section $section: hard-coded prefix (\%{l_prefix}) found " . 1.879 + "(use \"\$RPM_INSTALL_PREFIX\" to allow relocation)"); 1.880 + $done .= $this; 1.881 + } 1.882 + } 1.883 + 1.884 + # check for sub-shell parenthesis style 1.885 + # (this is such complicated because the Bourne Shell has a 1.886 + # construct "case <value> in <pattern> ) <script> ;; ... esac" 1.887 + # where the closing parenthesis makes heavy problems for our 1.888 + # check. So we first have to get rid of this. Unfortunately this 1.889 + # is again not easy because there exists nested(!) case/esac 1.890 + # constructs. Hence, we have to use a small recursive descent 1.891 + # parser which replaces the ")" in case/esac constructs with 1.892 + # "PCLOSE". A similar thing is done with string literals, although 1.893 + # here the job is more trivial). 1.894 + if ($section !~ m/^\%files$/s) { 1.895 + $done = $outer_done; $this = ''; $todo = $outer_this; 1.896 + $todo =~ s/`[^`]*`/STRING/sg; 1.897 + $todo =~ s/'[^']*'/STRING/sg; 1.898 + $todo =~ s/"[^"]*"/STRING/sg; 1.899 + $todo = (&parse_sh("", $todo))[0]; 1.900 + sub parse_sh { 1.901 + my ($done, $todo) = @_; 1.902 + while ($todo =~ m/^(.*?)(case|;;)/s) { 1.903 + if ($2 eq 'case') { 1.904 + ($done, $todo) = &parse_case($done.$1, $2.$'); 1.905 + } 1.906 + else { 1.907 + ($done, $todo) = ($done.$1, $2.$'); 1.908 + return ($done, $todo); 1.909 + } 1.910 + } 1.911 + ($done, $todo) = ($done . $todo, ""); 1.912 + return ($done, $todo); 1.913 + } 1.914 + sub parse_case { 1.915 + my ($done, $todo) = @_; 1.916 + $todo =~ m|^\s*case\s+\S+\s+in\b|s or return (undef, undef); 1.917 + ($done, $todo) = ($done . $&, $'); 1.918 + while ($todo =~ m|^(\s*[^)\n]+)(\))|s) { 1.919 + ($done, $todo) = ($done . $1 . "PCLOSE", $'); 1.920 + ($done, $todo) = &parse_sh($done, $todo) or return (undef, undef); 1.921 + $todo =~ m|^\s*;;|s or return (undef, undef); 1.922 + ($done, $todo) = ($done . $&, $'); 1.923 + } 1.924 + $todo =~ m|^\s*esac|s or return (undef, undef); 1.925 + ($done, $todo) = ($done . $&, $'); 1.926 + return ($done, $todo); 1.927 + } 1.928 + if ($file !~ m|openpkg\.spec$| and $todo eq '') { 1.929 + &lint_warning($file, $outer_done, $outer_this, 1.930 + "unable to correctly parse case/esac constructs in shell-script" . 1.931 + " (sub-shell parenthesis check skipped)"); 1.932 + } 1.933 + else { 1.934 + while ($todo =~ m/\)[ \t]*\n/s) { 1.935 + $done .= $`; $this = $&; $todo = $'; 1.936 + &lint_warning($file, $done, $this, "problematic sub-shell construct without error handling " . 1.937 + "(use \"...) || exit \$?\" to make sure it correctly exits on nested failure)"); 1.938 + $done .= $this; 1.939 + } 1.940 + } 1.941 + } 1.942 +} 1.943 + 1.944 +## _________________________________________________________________ 1.945 +## 1.946 +## CHECK "global": globals 1.947 +## _________________________________________________________________ 1.948 +## 1.949 + 1.950 +sub check_global { 1.951 + my ($file, $spec) = @_; 1.952 + 1.953 + # check for deprecated use of %{name} macro 1.954 + my $done = ''; my $this = ''; my $todo = $spec; 1.955 + while ($todo =~ m/\%\{name\}/s) { 1.956 + $done .= $`; $this = $&; $todo = $'; 1.957 + &lint_warning($file, $done, $this, "deprecated usage of %{name} macro (expected none)"); 1.958 + $done .= $this; 1.959 + } 1.960 + 1.961 + # FIXME: activate later 1.962 + # check for over-long lines 1.963 + #$done = ''; $this = ''; $todo = $spec; 1.964 + #while ($todo =~ m/([^\n]{80,})\n/s) { 1.965 + # $done .= $`; $this = $&; $todo = $'; 1.966 + # &lint_warning($file, $done, $this, "over-long line with ".sprintf("%d", length($1))." characters " . 1.967 + # "(expected maximum of 79 characters)"); 1.968 + # $done .= $this; 1.969 + #} 1.970 + 1.971 + # check for "shtool install" with -e on same line 1.972 + my $done = ''; my $this = ''; my $todo = $spec; 1.973 + while ($todo =~ m/%{l_shtool} install[^\n]*-e/s) { 1.974 + $done .= $`; $this = $&; $todo = $'; 1.975 + &lint_warning($file, $done, $this, "found -e option of \"shtool install\" on same line (expected separate lines)"); 1.976 + $done .= $this; 1.977 + } 1.978 + 1.979 + # check for "shtool install" with multiple -e on same line 1.980 + my $done = ''; my $this = ''; my $todo = $spec; 1.981 + while ($todo =~ m/%{l_shtool} install[^\n]+\\\n([^\n]*?-e[^\n]*?-e[^\n]*\\\n)+/s) { 1.982 + $done .= $`; $this = $&; $todo = $'; 1.983 + &lint_warning($file, $done, $this, "found multiple -e options of \"shtool install\" on same line (expected separate lines)"); 1.984 + $done .= $this; 1.985 + } 1.986 + 1.987 + # check for deprecated use of "shtool install|subst -e @l_...@ %{l_...}" 1.988 + if ($file !~ m|openpkg-tool|) { 1.989 + my $done = ''; my $this = ''; my $todo = $spec; 1.990 + while ($todo =~ m/%{l_shtool} (install|subst)(?:\\\n|[^\n])*?-e(?:\\\n|[^\n])*?\@l_(prefix|[smrn](usr|grp|uid|gid))\@[^n]+%{l_\2}/s) { 1.991 + $done .= $`; $this = $&; $todo = $'; 1.992 + &lint_warning($file, $done, $this, "deprecated usage of \"shtool $1 -e \@l_$2\@\" (expected use of %{l_value -s -a})"); 1.993 + $done .= $this; 1.994 + } 1.995 + } 1.996 + 1.997 + # check for constant "root" in %attr 1.998 + my $done = ''; my $this = ''; my $todo = $spec; 1.999 + while ($todo =~ m/%attr\(\s*(\d+)\s*,\s*root\s*,/s) { 1.1000 + $done .= $`; $this = $&; $todo = $'; 1.1001 + &lint_warning($file, $done, $this, "found constant \"root\" in %attr (expected variable %{l_susr})"); 1.1002 + $done .= $this; 1.1003 + } 1.1004 + 1.1005 + # check for valid RPM macro usage 1.1006 + if ($file !~ m|^(.+/)?openpkg\.spec$|s) { 1.1007 + my $ok = `$rpm --eval '\%dump' 2>&1 | egrep "^-*[0-9][0-9]*: l_" | sed -e 's;^[^:]*: *\\(l_[a-zA-Z0-9_]*\\).*;\\1;' | sort -u`; 1.1008 + my @ok = split(/\n/, $ok); 1.1009 + my $todo = $spec; 1.1010 + $todo =~ s|^([a-zA-Z][a-zA-Z0-9]+):\s+(.+)$|push(@ok, lc($1)), ''|mge; 1.1011 + $todo =~ s/^\%(?:define|option)\s+(\S+)\s+.+$/push(@ok, $1), ''/mge; 1.1012 + my $done = ''; my $this = ''; my $todo = $spec; 1.1013 + while ($todo =~ m|\%\{([a-zA-Z][a-zA-Z0-9_]+)|s) { 1.1014 + $done .= $`; $this = $&; $todo = $'; 1.1015 + my $name = $1; 1.1016 + next if ($name =~ m/^(SOURCE|PATCH)\d*$/ or $name =~ m/^(expand|echo|warn|error)$/); 1.1017 + if (not grep(/^\Q$name\E$/, @ok)) { 1.1018 + &lint_error($file, $done, $this, "unknown macro \"\%{$name}\""); 1.1019 + } 1.1020 + $done .= $this; 1.1021 + } 1.1022 + } 1.1023 +} 1.1024 + 1.1025 +## _________________________________________________________________ 1.1026 +## 1.1027 +## CHECK "sources": source file consistency 1.1028 +## _________________________________________________________________ 1.1029 +## 1.1030 + 1.1031 +sub check_sources { 1.1032 + my ($file, $spec) = @_; 1.1033 + 1.1034 + ## 1.1035 + ## PREPARATION 1.1036 + ## 1.1037 + 1.1038 + my $D = {}; 1.1039 + my $S = {}; 1.1040 + my $i = 99; 1.1041 + 1.1042 + # determine defines values 1.1043 + my $done = ''; my $this = ''; my $todo = $spec; 1.1044 + while ($todo =~ m/^\%(define|option)\s+(\S+)\s+(.+)\s*$/m) { 1.1045 + $done .= $`; $this = $&; $todo = $'; 1.1046 + my ($var, $val) = ($2, $3); 1.1047 + $val =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge; 1.1048 + $D->{$var} = $val; 1.1049 + $done .= $this; 1.1050 + } 1.1051 + $done = ''; $this = ''; $todo = $spec; 1.1052 + while ($todo =~ m/^([a-zA-Z][a-zA-Z0-9_]+):\s+(.*)$/m) { 1.1053 + $done .= $`; $this = $&; $todo = $'; 1.1054 + my ($var, $val) = (lc($1), $2); 1.1055 + $val =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge; 1.1056 + $D->{$var} = $val; 1.1057 + $done .= $this; 1.1058 + } 1.1059 + 1.1060 + # expand define values (allow one level of nesting) 1.1061 + $spec =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge; 1.1062 + $spec =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge; 1.1063 + 1.1064 + # parse Source# and Patch# headers 1.1065 + $done = ''; $this = ''; $todo = $spec; 1.1066 + while ($todo =~ m/^(Source|Patch)(\d+):[ \t]*(.*?)$/m) { 1.1067 + $done .= $`; $this = $&; $todo = $'; 1.1068 + my ($type, $num, $file) = (lc($1), $2, $3); 1.1069 + my $url = ""; 1.1070 + if ($file =~ m|^(.+/)([^/]+)$|s) { 1.1071 + ($url, $file) = ($1, $2); 1.1072 + } 1.1073 + if (not defined($S->{"$type$num"})) { 1.1074 + $S->{"$type$num"} = { 1.1075 + -type => $type, 1.1076 + -num => $num, 1.1077 + -url => $url, 1.1078 + -file => $file, 1.1079 + -refhdr => 1, 1.1080 + -refcmd => 0, 1.1081 + -refmac => 0, 1.1082 + -refvar => 0, 1.1083 + }; 1.1084 + } 1.1085 + else { 1.1086 + $S->{"$type$num"}->{-refhdr}++; 1.1087 + } 1.1088 + $done .= $this; 1.1089 + } 1.1090 + 1.1091 + # parse %setup and %patch commands 1.1092 + $done = ''; $this = ''; $todo = $spec; 1.1093 + $todo =~ s/^(\s*\%patch)((?:\s+-\S+|\s+-d\s+\S+)*)((?:\s+\d+)+)\s*$/&expand_patch($1, $2, $3)/mge; 1.1094 + sub expand_patch { 1.1095 + my ($cmd, $opts, $nums) = @_; 1.1096 + my $cmds = ''; 1.1097 + $cmds .= "${cmd}0${opts}\n" if ($opts !~ m|\s+-P|s); 1.1098 + $opts =~ s|\s+-P||s; 1.1099 + while ($nums =~ s|^\s+(\d+)||) { 1.1100 + my $num = $1; 1.1101 + $cmds .= "${cmd}${num}${opts}\n"; 1.1102 + } 1.1103 + return $cmds; 1.1104 + } 1.1105 + while ($todo =~ m/^\s*\%(setup|patch)(\d+)?(\s+.*)?$/m) { 1.1106 + $done .= $`; $this = $&; $todo = $'; 1.1107 + my ($type, $num, $opts) = ($1, $2, $3); 1.1108 + $type = 'source' if ($type eq 'setup'); 1.1109 + my $num_force = ''; 1.1110 + if ($opts =~ m/\s-(?:a|b)\s*(\d+)/s) { 1.1111 + $num_force = $1; 1.1112 + } 1.1113 + if ($num ne '' and $num_force ne '' and $num ne $num_force) { 1.1114 + &lint_warning($file, $done, $this, "macro and enforced $type number conflict: $num <-> $num_force" . 1.1115 + " (expected either just enforced number or make them match)"); 1.1116 + } 1.1117 + $num = $num_force if ($num_force ne ''); 1.1118 + $num = 0 if ($num eq ''); 1.1119 + next if ($opts =~ m|\s-T|s and $num == 0); 1.1120 + if (not defined($S->{"$type$num"})) { 1.1121 + $S->{"$type$num"} = { 1.1122 + -type => $type, 1.1123 + -num => $num, 1.1124 + -url => "", 1.1125 + -file => "", 1.1126 + -refhdr => 0, 1.1127 + -refcmd => 1, 1.1128 + -refmac => 0, 1.1129 + -refvar => 0, 1.1130 + }; 1.1131 + } 1.1132 + else { 1.1133 + $S->{"$type$num"}->{-refcmd}++; 1.1134 + } 1.1135 + $done .= $this; 1.1136 + } 1.1137 + 1.1138 + # parse %{SOURCE#} and %{PATCH#} macros 1.1139 + $done = ''; $this = ''; $todo = $spec; 1.1140 + while ($todo =~ m/\%\{(SOURCE|PATCH)(\d+)\}/m) { 1.1141 + $done .= $`; $this = $&; $todo = $'; 1.1142 + my ($type, $num) = (lc($1), $2); 1.1143 + if (not defined($S->{"$type$num"})) { 1.1144 + $S->{"$type$num"} = { 1.1145 + -type => $type, 1.1146 + -num => $num, 1.1147 + -url => "", 1.1148 + -file => "", 1.1149 + -refhdr => 0, 1.1150 + -refcmd => 0, 1.1151 + -refmac => 1, 1.1152 + -refvar => 0, 1.1153 + }; 1.1154 + } 1.1155 + else { 1.1156 + $S->{"$type$num"}->{-refmac}++; 1.1157 + } 1.1158 + $done .= $this; 1.1159 + } 1.1160 + 1.1161 + # parse %{SOURCE ...} and %{PATCH ...} macros 1.1162 + $done = ''; $this = ''; $todo = $spec; 1.1163 + while ($todo =~ m/\%\{(SOURCE|PATCH)\s+([^\s}]+)\}/m) { 1.1164 + $done .= $`; $this = $&; $todo = $'; 1.1165 + my ($type, $file) = (lc($1), $2); 1.1166 + my $key = ""; 1.1167 + foreach my $src (keys(%{$S})) { 1.1168 + if ($S->{$src}->{-type} eq $type and $S->{$src}->{-file} eq $file) { 1.1169 + $key = $src; 1.1170 + } 1.1171 + } 1.1172 + if ($key eq '') { 1.1173 + $S->{"$type$i"} = { 1.1174 + -type => $type, 1.1175 + -num => $i, 1.1176 + -url => "", 1.1177 + -file => $file, 1.1178 + -refhdr => 0, 1.1179 + -refcmd => 0, 1.1180 + -refmac => 1, 1.1181 + -refvar => 0, 1.1182 + }; 1.1183 + $i++; 1.1184 + } 1.1185 + else { 1.1186 + $S->{$key}->{-refmac}++; 1.1187 + } 1.1188 + $done .= $this; 1.1189 + } 1.1190 + 1.1191 + # parse $RPM_SOURCE_DIR based references 1.1192 + $done = ''; $this = ''; $todo = $spec; 1.1193 + while ($todo =~ m/\$RPM_SOURCE_DIR\/([^\s;"']+)/m) { 1.1194 + $done .= $`; $this = $&; $todo = $'; 1.1195 + my ($file) = ($1); 1.1196 + my $key = ""; 1.1197 + foreach my $src (keys(%{$S})) { 1.1198 + if ($S->{$src}->{-file} eq $file) { 1.1199 + $key = $src; 1.1200 + } 1.1201 + } 1.1202 + if ($key eq '') { 1.1203 + $S->{"source$i"} = { 1.1204 + -type => "source", 1.1205 + -num => $i, 1.1206 + -url => "", 1.1207 + -file => $file, 1.1208 + -refhdr => 0, 1.1209 + -refcmd => 0, 1.1210 + -refmac => 0, 1.1211 + -refvar => 1, 1.1212 + }; 1.1213 + $i++; 1.1214 + } 1.1215 + else { 1.1216 + $S->{$key}->{-refmac}++; 1.1217 + } 1.1218 + $done .= $this; 1.1219 + } 1.1220 + 1.1221 + ## 1.1222 + ## CHECKING 1.1223 + ## 1.1224 + 1.1225 + # check reference counts 1.1226 + foreach my $src (%{$S}) { 1.1227 + my $s = $S->{$src}; 1.1228 + my $str_hdr = ($s->{-type} eq 'source' ? 'Source' : 'Patch' ) . $s->{-num}; 1.1229 + my $str_cmd = ($s->{-type} eq 'source' ? '%setup' : '%patch') . $s->{-num}; 1.1230 + my $str_mac = ($s->{-type} eq 'source' ? '%{SOURCE ...}' : '%{PATCH ...}'); 1.1231 + my $str_var = "\$RPM_SOURCE_DIR"; 1.1232 + if ($file !~ m/\b(openpkg-.*|openpkg|vim|ncurses|openssh)\.spec$/) { 1.1233 + if ($s->{-refhdr} > 0) { 1.1234 + if ($s->{-refcmd} == 0 and $s->{-refmac} == 0 and $s->{-refvar} == 0) { 1.1235 + &lint_error($file, undef, undef, "source \"$str_hdr\" defined (file $s->{-file}), but never used" . 1.1236 + " (expected at least one reference via \"$str_cmd\", \"$str_mac\" or \"$str_var\")"); 1.1237 + } 1.1238 + elsif ($s->{-refcmd} > 1) { 1.1239 + &lint_error($file, undef, undef, "source \"$str_hdr\" defined (file $s->{-file}), but used multiple times" . 1.1240 + " (expected just one reference via \"$str_cmd\")"); 1.1241 + } 1.1242 + } 1.1243 + else { 1.1244 + if ($s->{-refcmd}+$s->{-refmac}+$s->{-refvar} > 0) { 1.1245 + &lint_error($file, undef, undef, "source \"$str_hdr\" not defined, but used ". 1.1246 + ($s->{-file} ? "(file ".$s->{-file}.")" : ""). 1.1247 + " (expected at least one definition via \"$str_hdr\")"); 1.1248 + } 1.1249 + } 1.1250 + } 1.1251 + } 1.1252 + 1.1253 + # check for existing of sources on filesystem 1.1254 + my $name = $file; 1.1255 + $name =~ s|.*/([^/]+)$|$1|s; 1.1256 + $name =~ s|\.spec$||s; 1.1257 + my $cvsdir = `$rpm --define 'name $name' --eval '%{_specdir}'`; 1.1258 + $cvsdir =~ s|\n$||s; 1.1259 + $cvsdir =~ s|/+$||s; 1.1260 + my $dstdir = `$rpm --define 'name $name' --eval '%{_sourcedir}'`; 1.1261 + $dstdir =~ s|\n$||s; 1.1262 + $dstdir =~ s|/+$||s; 1.1263 + foreach my $src (%{$S}) { 1.1264 + my $s = $S->{$src}; 1.1265 + next if (not $s->{-refhdr}); 1.1266 + if ($s->{-url} eq '' and not -f $cvsdir."/".$s->{-file} and not -f $dstdir."/".$s->{-file}) { 1.1267 + &lint_error($file, undef, undef, "source \"$s->{-file}\" neither found in SRC nor DST locations (you have to provide it)"); 1.1268 + } 1.1269 + elsif ($s->{-url} eq '' and not -f $cvsdir."/".$s->{-file} and -f $dstdir."/".$s->{-file}) { 1.1270 + &lint_error($file, undef, undef, "source \"$s->{-file}\" not found in SRC, but in DST (expected it in SRC or want URL)"); 1.1271 + } 1.1272 + elsif ($s->{-url} ne '' and not -f $cvsdir."/".$s->{-file} and not -f $dstdir."/".$s->{-file}) { 1.1273 + &lint_error($file, undef, undef, "source \"$s->{-file}\" neither found in SRC nor DST locations (you have to download it)"); 1.1274 + } 1.1275 + elsif ($s->{-url} ne '' and -f $cvsdir."/".$s->{-file} and not -f $dstdir."/".$s->{-file}) { 1.1276 + &lint_error($file, undef, undef, "source \"$s->{-file}\" not found in DST, but in SRC (expected it in DST or want URL)"); 1.1277 + } 1.1278 + #if ($s->{-url} ne '' and $s->{-file} !~ m/\d/) { 1.1279 + # &lint_warning($file, undef, undef, "vendor source \"$s->{-file}\" not versioned"); 1.1280 + #} 1.1281 + } 1.1282 + 1.1283 + # check for patch file naming 1.1284 + foreach my $src (%{$S}) { 1.1285 + my $s = $S->{$src}; 1.1286 + next if (not $s->{-refhdr}); 1.1287 + if ( $s->{-url} eq '' and $s->{-type} eq 'patch' 1.1288 + and $s->{-file} !~ m|^${name}\.patch(\.[^./]+)?$|) { 1.1289 + &lint_warning($file, undef, undef, "non-canonical patch filename \"$s->{-file}\"" . 1.1290 + " (expected to match \"${name}\\.patch(\\.[^./]+)?\$\")"); 1.1291 + } 1.1292 + } 1.1293 +} 1.1294 + 1.1295 +## _________________________________________________________________ 1.1296 +## 1.1297 +## CHECK "digest": digest consistency 1.1298 +## _________________________________________________________________ 1.1299 +## 1.1300 + 1.1301 +sub check_digest { 1.1302 + my ($file, $spec) = @_; 1.1303 + 1.1304 + ## 1.1305 + ## PREPARATION 1.1306 + ## 1.1307 + 1.1308 + my $D = {}; 1.1309 + my $S = {}; 1.1310 + 1.1311 + # determine defines values 1.1312 + my $done = ''; my $this = ''; my $todo = $spec; 1.1313 + while ($todo =~ m/^\%(define|option)\s+(\S+)\s+(.+)\s*$/m) { 1.1314 + $done .= $`; $this = $&; $todo = $'; 1.1315 + my ($var, $val) = ($2, $3); 1.1316 + $val =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge; 1.1317 + $D->{$var} = $val; 1.1318 + $done .= $this; 1.1319 + } 1.1320 + $done = ''; $this = ''; $todo = $spec; 1.1321 + while ($todo =~ m/^([a-zA-Z][a-zA-Z0-9_]+):\s+(.*)$/m) { 1.1322 + $done .= $`; $this = $&; $todo = $'; 1.1323 + my ($var, $val) = (lc($1), $2); 1.1324 + $val =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge; 1.1325 + $D->{$var} = $val; 1.1326 + $done .= $this; 1.1327 + } 1.1328 + 1.1329 + # expand define values (allow one level of nesting) 1.1330 + $spec =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge; 1.1331 + $spec =~ s|(\%\{([a-zA-Z_][a-zA-Z0-9_]*)\})|defined($D->{$2}) ? $D->{$2} : $1|sge; 1.1332 + 1.1333 + # determine locations 1.1334 + my $name = $file; 1.1335 + $name =~ s|.*/([^/]+)$|$1|s; 1.1336 + $name =~ s|\.spec$||s; 1.1337 + my $srcdir = `$rpm --define 'name $name' --eval '%{_specdir}'`; 1.1338 + $srcdir =~ s|\n$||s; 1.1339 + $srcdir =~ s|/+$||s; 1.1340 + my $dstdir = `$rpm --define 'name $name' --eval '%{_sourcedir}'`; 1.1341 + $dstdir =~ s|\n$||s; 1.1342 + $dstdir =~ s|/+$||s; 1.1343 + 1.1344 + # parse Source# and Patch# headers 1.1345 + $done = ''; $this = ''; $todo = $spec; 1.1346 + while ($todo =~ m/^(Source|Patch)(\d+):[ \t]*(.*?)$/m) { 1.1347 + $done .= $`; $this = $&; $todo = $'; 1.1348 + my ($type, $num, $url) = (lc($1), $2, $3); 1.1349 + my $file; 1.1350 + if ($url =~ m|^.+/([^/]+)$|s) { 1.1351 + $file = "$dstdir/$1"; 1.1352 + } 1.1353 + else { 1.1354 + $file = "$srcdir/$url"; 1.1355 + } 1.1356 + if (not defined($S->{"$type$num"})) { 1.1357 + $S->{"$type$num"} = { 1.1358 + -type => $type, 1.1359 + -num => $num, 1.1360 + -url => $url, 1.1361 + -file => $file 1.1362 + }; 1.1363 + } 1.1364 + else { 1.1365 + $S->{"$type$num"}->{-refhdr}++; 1.1366 + } 1.1367 + $done .= $this; 1.1368 + } 1.1369 + 1.1370 + # parse BuildPreReq headers 1.1371 + $done = ''; $this = ''; $todo = $spec; 1.1372 + while ($todo =~ m/^BuildPreReq:[ \t]+digest\(sha1:\%\{(.+?)\}\)[ \t]+=[ \t]+([0-9a-fA-F]+)$/m) { 1.1373 + $done .= $`; $this = $&; $todo = $'; 1.1374 + my ($macro, $sha1) = ($1, $2); 1.1375 + 1.1376 + my $url = $S->{lc($macro)}->{-url}; 1.1377 + my $filename = $S->{lc($macro)}->{-file}; 1.1378 + my $basename = $filename; 1.1379 + $basename =~ s/^.+\/([^\/]+)$/$1/s; 1.1380 + 1.1381 + my $sha1_real = `$openssl sha1 $filename 2>/dev/null`; 1.1382 + $sha1_real =~ s/^SHA1\s*\(.+?\)\s*=\s*([0-9a-fA-F]+)\s*$/$1/s; 1.1383 + 1.1384 + if (lc($sha1) ne lc($sha1_real)) { 1.1385 + &lint_error($file, undef, undef, "source \"$basename\" (\%{$macro}) has a digest(sha1) requirement of \"$sha1\" (expected \"$sha1_real\")"); 1.1386 + } 1.1387 + 1.1388 + $done .= $this; 1.1389 + } 1.1390 +} 1.1391 +