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