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