diff -r 71503088f51b -r f880f219c566 openpkg/lint-spec.pl
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/openpkg/lint-spec.pl Tue Jul 31 12:23:42 2012 +0200
@@ -0,0 +1,1388 @@
+##
+## lint-spec.pl -- OpenPKG *.spec File Checker
+## Copyright (c) 2000-2012 OpenPKG GmbH
+##
+## This software is property of the OpenPKG GmbH, DE MUC HRB 160208.
+## All rights reserved. Licenses which grant limited permission to use,
+## copy, modify and distribute this software are available from the
+## OpenPKG GmbH.
+##
+## THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
+## WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+## MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+## IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR
+## CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+## USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+## OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+## OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+## SUCH DAMAGE.
+##
+
+# Perl run-time requirement
+require 5;
+BEGIN {
+ eval "use Getopt::Long; use IO;";
+ if ($@) {
+ print STDERR
+ "lint-spec: ERROR: This command requires a full-size Perl installation!\n" .
+ "lint-spec: HINT: Install OpenPKG \"perl\" package to use this command.\n";
+ exit(1);
+ }
+}
+
+# OpenPKG instance prefix
+my $my_prefix = $ENV{'OPENPKG_PREFIX'};
+delete $ENV{'OPENPKG_PREFIX'};
+
+# program information
+my $progname = "lint-spec";
+my $progvers = "1.0.0";
+
+# parameters (defaults)
+my $version = 0;
+my $verbose = 0;
+my $help = 0;
+my $check = 'all';
+my $tmpdir = ($ENV{TMPDIR} || $ENV{TEMPDIR} || "/tmp") . "/$progname";
+my $rpm = "$my_prefix/bin/openpkg rpm";
+
+# exception handling support
+$SIG{__DIE__} = sub {
+ my ($err) = @_;
+ $err =~ s|\s+at\s+.*||s if (not $verbose);
+ print STDERR "$progname:ERROR: $err ". ($! ? "($!)" : "") . "\n";
+ exit(1);
+};
+
+# command line parsing
+Getopt::Long::Configure("bundling");
+my $result = GetOptions(
+ 'V|version' => \$version,
+ 'v|verbose' => \$verbose,
+ 'h|help' => \$help,
+ 'c|check=s' => \$check,
+ 't|tmpdir=s' => \$tmpdir,
+ 'r|rpm=s' => \$rpm,
+) || die "option parsing failed";
+if ($help) {
+ print "Usage: $progname [options] [SPECFILE ...]\n" .
+ "Available options:\n" .
+ " -v,--verbose enable verbose run-time mode\n" .
+ " -h,--help print out this usage page\n" .
+ " -c,--check=CHECKS select checks to perform (default='all')\n" .
+ " -r,--rpm=FILE filesystem path to RPM program\n" .
+ " -t,--tmpdir=PATH filesystem path to temporary directory\n" .
+ " -V,--version print program version\n";
+ exit(0);
+}
+if ($version) {
+ print "OpenPKG $progname $progvers\n";
+ exit(0);
+}
+
+# verbose message printing
+sub msg_verbose {
+ my ($msg) = @_;
+ print STDERR "$msg\n" if ($verbose);
+}
+
+# warning message printing
+sub msg_warning {
+ my ($msg) = @_;
+ print STDERR "$progname:WARNING: $msg\n";
+}
+
+# error message printing
+sub msg_error {
+ my ($msg) = @_;
+ print STDERR "$progname:ERROR: $msg\n";
+}
+
+# determine check list
+my @check_list = (qw(
+ blank
+ comment
+ license
+ header
+ section
+ preproc
+ script
+ global
+ sources
+ digest
+));
+my @checks = ();
+if ($check eq 'all') {
+ @checks = @check_list;
+}
+else {
+ foreach my $c (split(/,/, $check)) {
+ if (not grep($c, @check_list)) {
+ die "invalid check \"$c\"";
+ }
+ push(@checks, $c);
+ }
+}
+
+# global return code
+$main::GRC = 0;
+
+# iterate over all .spec files
+foreach my $filename (@ARGV) {
+ my $io = new IO::File "<$filename"
+ or die "unable to open file \"$filename\" for reading";
+ my $spec; { local $/ = undef; $spec = <$io>; }
+ $io->close;
+ foreach my $check (@checks) {
+ eval "\&check_$check(\$filename, \$spec);";
+ }
+}
+
+# die gracefully
+exit($main::GRC);
+
+## _________________________________________________________________
+##
+## COMMON SUBROUTINES
+## _________________________________________________________________
+##
+
+sub lines {
+ my ($txt) = @_;
+ my $l = 0;
+ $txt =~ s|\n|$l++, ''|sge;
+ return $l;
+}
+
+sub lint_message {
+ my ($type, $file, $done, $this, $msg) = @_;
+ if (defined($done) and defined($this)) {
+ my $start = &lines($done) + 1;
+ my $end = $start + &lines($this);
+ my $pos = $start;
+ $pos .= "-". $end if ($end > $start);
+ printf("%s:%s: %s:%s: %s\n", $progname, $type, $file, $pos, $msg);
+ }
+ else {
+ printf("%s:%s: %s: %s\n", $progname, $type, $file, $msg);
+ }
+}
+
+sub lint_warning {
+ my ($file, $done, $this, $msg) = @_;
+ &lint_message("WARNING", $file, $done, $this, $msg);
+ $main::GRC = 1 if ($main::GRC < 1);
+}
+
+sub lint_error {
+ my ($file, $done, $this, $msg) = @_;
+ &lint_message("ERROR", $file, $done, $this, $msg);
+ $main::GRC = 2 if ($main::GRC < 2);
+}
+
+## _________________________________________________________________
+##
+## CHECK "blank": whitespace and blank lines
+## _________________________________________________________________
+##
+
+sub check_blank {
+ my ($file, $spec) = @_;
+
+ # check for CR-LF combination
+ my $done = ''; my $this = ''; my $todo = $spec;
+ while ($todo =~ m/\r\n/s) {
+ $done .= $`; $this = $&; $todo = $';
+ &lint_warning($file, $done, $this, "carriage-return (CR, 0x0d) line-feed (NL, 0x0a) combination (expected just line-feed)");
+ $done .= $this;
+ }
+
+ # check for multiple blank lines
+ $done = ''; $this = ''; $todo = $spec;
+ while ($todo =~ m/(\r?\n[ \t]*){3,}/s) {
+ $done .= $`; $this = $&; $todo = $';
+ &lint_warning($file, $done, $this, "multiple subsequent blank lines (expected single blank line)");
+ $done .= $this;
+ }
+
+ # check for trailing whitespaces
+ $done = ''; $this = ''; $todo = $spec;
+ while ($todo =~ m/[ \t]+\r?\n/s) {
+ $done .= $`; $this = $&; $todo = $';
+ if ($done eq '' or $done =~ m|\n$|s) {
+ &lint_warning($file, $done, $this, "whitespace on empty line (expected none)");
+ }
+ else {
+ &lint_warning($file, $done, $this, "trailing whitespace (expected none)");
+ }
+ $done .= $this;
+ }
+
+ # check for bogus line continuations
+ $done = ''; $this = ''; $todo = $spec;
+ while ($todo =~ m/\\[ \t]*\r?\n(?=[ \t]*\r?\n)/s) {
+ $done .= $`; $this = $&; $todo = $';
+ &lint_warning($file, $done, $this, "bogus line continuation for following empty line (expect no line continuation)");
+ $done .= $this;
+ }
+
+ # check for leading whitespaces before line continuations
+ $done = ''; $this = ''; $todo = $spec;
+ while ($todo =~ m/[ \t]{2,}\\[ \t]*\r?\n/s) {
+ $done .= $`; $this = $&; $todo = $';
+ &lint_warning($file, $done, $this, "multiple leading whitespace before line continuation (expected just a single space)");
+ $done .= $this;
+ }
+
+ # check for leading tabs
+ $done = ''; $this = ''; $todo = $spec;
+ while ($todo =~ m/^ *\t+ *[^ \t]/m) {
+ $done .= $`; $this = $&; $todo = $';
+ &lint_warning($file, $done, $this, "leading tabs (expected spaces)");
+ $done .= $this;
+ }
+
+ # check for mandatory/wished trailing blank line
+ if ($spec !~ m|\n\n$|) {
+ &lint_warning($file, $spec, "", "mandatory/wished trailing blank line missing (expected one)");
+ }
+}
+
+## _________________________________________________________________
+##
+## CHECK "comment": sharp-comments
+## _________________________________________________________________
+##
+
+sub check_comment {
+ my ($file, $spec) = @_;
+
+ # check for comment indentation and contents
+ my $done = ''; my $this = ''; my $todo = $spec;
+ while ($todo =~ m/^([ \t]*)(#+)([ \t]*)(.*?)$/m) {
+ $done .= $`; $this = $&; $todo = $';
+ my ($lead, $sharp, $pad, $text) = ($1, $2, $3, $4);
+ if (length($lead) % 2 != 0) {
+ &lint_warning($file, $done, $this, "incorrect comment indentation (expected a multiple of 2 spaces)");
+ }
+ if (length($lead) > 1 && length($sharp) > 1) {
+ &lint_warning($file, $done, $this, "indented comment has introduced with multiple sharps (expected single sharp character)");
+ }
+ if (length($pad.$text) > 0 && length($sharp.$pad) % 4 != 0) {
+ &lint_warning($file, $done, $this, "incorrect comment text padding (expected a multiple of 4 sharps or spaces)");
+ }
+ if (length($pad) == 0 && length($text) > 0) {
+ &lint_warning($file, $done, $this, "missing leading space before comment text (expected padding spaces)");
+ }
+ if (length($pad) > 0 && length($text) == 0) {
+ &lint_warning($file, $done, $this, "empty comment text (expected a reasonable text)");
+ }
+ $done .= $this;
+ }
+
+ # check for comment contents only
+ if ($file !~ m|openpkg\.spec$|) {
+ $done = ''; $this = ''; $todo = $spec;
+ while ($todo =~ m/\n\n[ \t]*#[ \t]+([^\n]+)(?!\n([ \t]*#))/s) {
+ $done .= $`; $this = $&; $todo = $';
+ my $text = $1;
+ if (length($text) > 0 and $text =~ m|^[A-Z][^A-Z]|) {
+ &lint_warning($file, $done, $this, "comment text starts with upper-case letter (expected lower-case letter)");
+ }
+ $done .= $this;
+ }
+ }
+}
+
+## _________________________________________________________________
+##
+## CHECK "license": license header
+## _________________________________________________________________
+##
+
+sub check_license {
+ my ($file, $spec) = @_;
+
+ my $name = "[a-z][a-z0-9-]*";
+ if ($file =~ m|^.*/([a-z][a-z0-9-]*)\.spec$|) {
+ $name = $1;
+ }
+ elsif ($file =~ m|^([a-z][a-z0-9-]*)\.spec$|) {
+ $name = $1;
+ }
+ my $re = "";
+ $re .= "##\\n";
+ $re .= "## $name\\.spec -- OpenPKG RPM Package Specification\\n";
+ $re .= "## Copyright \\(c\\) 200[0-9]-2012 OpenPKG Foundation e\.V\. \\n";
+ $re .= "##\\n";
+ $re .= "## Permission to use, copy, modify, and distribute this software for\\n";
+ $re .= "## any purpose with or without fee is hereby granted, provided that\\n";
+ $re .= "## the above copyright notice and this permission notice appear in all\\n";
+ $re .= "## copies\\.\\n";
+ $re .= "##\\n";
+ $re .= "## THIS SOFTWARE IS PROVIDED \\`\\`AS IS'' AND ANY EXPRESSED OR IMPLIED\\n";
+ $re .= "## WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\\n";
+ $re .= "## MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED\\.\\n";
+ $re .= "## IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR\\n";
+ $re .= "## CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\\n";
+ $re .= "## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES \\(INCLUDING, BUT NOT\\n";
+ $re .= "## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF\\n";
+ $re .= "## USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION\\) HOWEVER CAUSED AND\\n";
+ $re .= "## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\\n";
+ $re .= "## OR TORT \\(INCLUDING NEGLIGENCE OR OTHERWISE\\) ARISING IN ANY WAY OUT\\n";
+ $re .= "## OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF\\n";
+ $re .= "## SUCH DAMAGE\\.\\n";
+ $re .= "##\\n";
+ if ($name ne "openpkg" and $spec !~ m|^$re|os) {
+ &lint_warning($file, "", "", "invalid license header");
+ }
+}
+
+## _________________________________________________________________
+##
+## CHECK "header": RPM headers
+## _________________________________________________________________
+##
+
+sub check_header {
+ my ($file, $spec) = @_;
+
+ my @headers = (qw(
+ m:Name:^[a-z][a-z0-9-]*$
+ m:Summary:^[A-Z]\S*(\s+([A-Z]\S*|of|for|from|in|at|on|\(\S+\)))*
+ m:URL:^((https?|ftp)://.+|-)$
+ m:Vendor:.+
+ m:Packager:^(OpenPKG\sFoundation\se\.V\.|OpenPKG\sGmbH)$
+ m:Distribution:^(OpenPKG|OpenPKG\sCommunity|OpenPKG\sEnterprise)$
+ m:Class:^(BOOT|CORE|BASE|PLUS|EVAL|JUNK|PRIV)$
+ m:Group:^[A-Z][a-zA-Z0-9]+$
+ m:License:.+
+ m:Version:^[^-]+$
+ 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+|%\{[^\}]+\})$
+ o:Source\d+:^((https?|ftp)://.+|[^/]+)$
+ o:Patch\d+:^((https?|ftp)://.+|[^/]+)$
+ o:Prefix:^%{l_prefix}$
+ o:BuildRoot:^%{l_buildroot}$
+ 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+)?)+)$
+ 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+)?)+$
+ o:AutoReq:^no$
+ o:AutoReqProv:^no$
+ o:Provides:^((,\s)?([a-z][a-z0-9-]*(\:\:with_[a-z][a-z0-9_]+)?|[A-Z][A-Z0-9-]*)(\s==?\s\S+)?)+$
+ o:Conflicts:^((,\s)?([a-z][a-z0-9-]*(\:\:with_[a-z][a-z0-9_]+)?|[A-Z][A-Z0-9-]*)(\s(>=?|==?|<=?|!=)\s\S+)?)+$
+ ));
+ my @headers_def = (qw(
+ Summary:^Badly\sPackaged\sProgram$
+ Vendor:^John\sDoe$
+ Group:^Unknown$
+ License:^DoePL$
+ ));
+ my @headers_mult = (qw(
+ BuildPreReq PreReq Provides Conflicts
+ ));
+
+ my @seen = ();
+ my %count = ();
+ my $done = ''; my $this = ''; my $todo = $spec;
+ while ($todo =~ m/^(\S+):([ \t]*)(.*?)$/m) {
+ $done .= $`; $this = $&; $todo = $';
+ my ($header, $pad, $value) = ($1, $2, $3);
+
+ # check for layouting
+ if (length($value) == 0) {
+ &lint_error($file, $done, $this, "empty RPM header value");
+ }
+ if (length($header.":".$pad) != 14) {
+ &lint_warning($file, $done, $this, "invalid RPM header name/value padding (expected value at column 15)");
+ }
+
+ # check for valid header name and value
+ if (not grep { $header =~ m|^$_$|s } map { m/^[^:]:([^:]+):/, $1 } @headers) {
+ &lint_error($file, $done, $this, "invalid RPM header name \"$header\"");
+ }
+ else {
+ my $hn = quotemeta((grep { $header =~ m|^$_$|s } map { m/^[^:]+:([^:]+):/, $1 } @headers)[0]);
+ my $re = (map { m/^[^:]+:${hn}:(.+)$/s } @headers)[0];
+ my $re_match = $re;
+ $re_match =~ s|^\(|(?:|sg;
+ $re_match =~ s|([^\\])\(|\1(?:|sg;
+ if ( not ($file =~ m|openpkg\.spec$| and $header eq 'Provides' and $value eq '%{l_prefix}')
+ and $value !~ m|${re_match}|s) {
+ &lint_warning($file, $done, $this, "RPM header \"$header\": " .
+ "invalid value \"$value\" (expected to match \"$re\")");
+ }
+ my $re_def = (map { m/^${hn}:(.+)$/s } @headers_def)[0];
+ if (defined($re_def)) {
+ my $re_def_match = $re_def;
+ $re_def_match =~ s|^\(|(?:|sg;
+ $re_def_match =~ s|([^\\])\(|\1(?:|sg;
+ if ($value =~ m|${re_def_match}|s) {
+ &lint_warning($file, $done, $this, "RPM header \"$header\": " .
+ "default value \"$value\" (expected to match \"$re\", except for this)");
+ }
+ }
+ if ($header =~ m/^Vendor/ and $value =~ m/et\sal/ and $value !~ m/et\sal\./) {
+ &lint_warning($file, $done, $this, "RPM header \"$header\": " .
+ "has value \"$value\" (expected \"et al.\" with dot)");
+ }
+ }
+ push(@seen, $header);
+ $count{$header}++;
+ $done .= $this;
+ }
+
+ # check for existence of mandatory headers
+ foreach my $header (@headers) {
+ my ($type, $name, $regex) = split(/:/, $header, 3);
+ if ($type eq 'm') {
+ if ($file =~ m|openpkg\.spec$| and
+ ($name eq "BuildPreReq" or $name eq "PreReq")) {
+ # the bootstrap package is an obvious exception
+ next;
+ }
+ if (not grep(/^$name$/, @seen)) {
+ &lint_warning($file, undef, undef, "mandatory RPM header \"$name\" not found");
+ }
+ }
+ }
+
+ # check for multiple occurrence headers
+ foreach my $seen (@seen) {
+ if ($count{$seen} > 1 and not (grep { $_ eq $seen } @headers_mult)) {
+ &lint_error($file, undef, undef, "RPM header \"$seen\" occurs multiple times (expected just once)");
+ }
+ }
+}
+
+## _________________________________________________________________
+##
+## CHECK "section": RPM sections
+## _________________________________________________________________
+##
+
+sub check_section {
+ my ($file, $spec) = @_;
+
+ my $require = qq{
+ (%define,)*
+ Name:,
+ Summary:,
+ URL:,
+ Vendor:,
+ Packager:,
+ Distribution:,
+ Class:,
+ Group:,
+ License:,
+ Version:,
+ Release:,
+ (%option,)*
+ (%define,|%undefine,)*
+ (Source\\d+:,)*
+ (Patch\\d+:,)*
+ (%NoSource,)*
+ (%NoPatch,)*
+ (Prefix:,)?
+ (BuildRoot:,)?
+ ((BuildPreReq:,)
+ |(PreReq:,))*
+ (AutoReq:,)?
+ (AutoReqProv:,)?
+ (Provides:,)*
+ (Conflicts:,)*
+ %description,
+ (%track,)?
+ %prep,
+ %build,
+ %install,
+ (%check,)?
+ %files,
+ %clean,
+ (%pre,)?
+ (%post,)?
+ (%preun,)?
+ (%postun,)?
+ (%trigger,)?
+ (%triggerin,)?
+ (%triggerun,)?
+ (%triggerpostun,)?
+ (%verifyscript,)?
+ };
+
+ # check for order of headers
+ my $sections = "";
+ my $done = ''; my $this = ''; my $todo = $spec;
+ while ($todo =~ m/^(\S+:|%\S+).*$/m) {
+ $done .= $`; $this = $&; $todo = $';
+ my $section = $1;
+ next if ($section =~ m/^%(if|else|endif)/);
+ $sections .= "$section,";
+ $done .= $this;
+ }
+ my $regex = $require;
+ $regex =~ s|\s+||sg;
+ if ($sections !~ m/^$regex$/s) {
+ $regex =~ s|,| |sg;
+ &lint_error($file, undef, undef, "invalid RPM section order: $sections (expected \"$regex\")");
+ }
+}
+
+## _________________________________________________________________
+##
+## CHECK "preproc": RPM macro pre-processor
+## _________________________________________________________________
+##
+
+sub check_preproc {
+ my ($file, $spec) = @_;
+
+ my $done = ''; my $this = ''; my $todo = $spec;
+ while ($todo =~ m/^(%(?:if|else|endif|define|undefine|option))(.*)$/m) {
+ $done .= $`; $this = $&; $todo = $';
+ my ($cmd, $args) = ($1, $2);
+ if ($cmd eq '%if') {
+ # FIXME: either try to really parse the boolean expression
+ # FIXME: or at least try to guess its correct syntax
+ }
+ elsif ($cmd eq '%else' or $cmd eq '%endif') {
+ if (length($args) > 0) {
+ &lint_warning($file, $done, $this, "garbage after pre-processor directive " .
+ "\"$cmd\" (expected no arguments)");
+ }
+ }
+ elsif ($cmd eq '%undefine') {
+ if ($args =~ m|^\s+(\S+)\s*$|) {
+ my $var = $1;
+ if ($var !~ m/^(V|with)_[a-z][a-zA-Z0-9_]*$/) {
+ &lint_warning($file, $done, $this, "unusually named macro: \"$var\" " .
+ "(expected \"(V|with)_[a-z][a-zA-Z0-9_]*\")");
+ next;
+ }
+ }
+ else {
+ &lint_error($file, $done, $this, "invalid number of arguments to pre-processor " .
+ "directive \"$cmd\" (expected exactly 1 argument)");
+ }
+ }
+ elsif ($cmd eq '%define' or $cmd eq '%option') {
+ if ($args =~ m|^\s+(\S+)\s+(.*)$|) {
+ my ($var, $val) = ($1, $2);
+ if ($var !~ m/^(V|with)_[a-z][a-zA-Z0-9_]*$/) {
+ &lint_warning($file, $done, $this, "unusually named macro: \"$var\" " .
+ "(expected \"(V|with)_[a-z][a-zA-Z0-9_]*\")");
+ next;
+ }
+ if (length($val) == 0) {
+ &lint_error($file, $done, $this, "empty macro value");
+ }
+ }
+ else {
+ &lint_error($file, $done, $this, "invalid number of arguments to pre-processor " .
+ "directive \"$cmd\" (expected exactly 2 arguments)");
+ }
+ }
+ $done .= $this;
+ }
+
+ # check correct if/endif nesting
+ my @stack = ();
+ $done = ''; $this = ''; $todo = $spec;
+ while ($todo =~ m/^(%(?:if|else|endif|define|undefine|option)).*$/m) {
+ $done .= $`; $this = $&; $todo = $';
+ my $directive = $1;
+ if ($directive eq '%if') {
+ push(@stack, &lines($done . $this));
+ }
+ elsif ($directive eq '%endif') {
+ if (@stack == 0) {
+ &lint_error($file, $done, $this, "found \%endif without corresponding opening \%if");
+ last;
+ }
+ pop(@stack);
+ }
+ $done .= $this;
+ }
+ my $line;
+ while (defined($line = pop(@stack))) {
+ &lint_error($file, undef, undef, "\%if at line $line never closed by \%endif");
+ }
+
+ # check for indented preprocessor constructs
+ $done = ''; $this = ''; $todo = $spec;
+ while ($todo =~ m/^[ \t]+(%(?:if|else|endif|define|undefine|option)).*$/m) {
+ $done .= $`; $this = $&; $todo = $';
+ my $directive = $1;
+ &lint_error($file, $done, $this, "found indented \"$directive\" preprocessor directive (expected no indentation)");
+ $done .= $this;
+ }
+}
+
+## _________________________________________________________________
+##
+## CHECK "script": shell scripts
+## _________________________________________________________________
+##
+
+sub check_script {
+ my ($file, $spec) = @_;
+
+ my $done = ''; my $this = ''; my $todo = $spec;
+ 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) {
+ $done .= $`; $this = $&; $todo = $';
+ my ($section, $args, $script) = ($1, $2, $3);
+
+ # perform checks for a single script section
+ &check_script_section($file, $done, $this, $section, $args, $script);
+
+ $done .= $this;
+ }
+}
+
+sub check_script_section {
+ my ($file, $outer_done, $outer_this, $section, $args, $script) = @_;
+
+ # skip plain-text/non-scripting section %description
+ return if ($section eq '%description');
+
+ # remove comment contents
+ $outer_this =~ s|^[ \t]*#[^\n]*||mg;
+
+ # check shell redirections
+ my $done = $outer_done; my $this = ''; my $todo = $outer_this;
+ while ( $todo =~ m/[ \t]+(\d+)?[><][ \t]+\S+/s
+ or $todo =~ m/[ \t]+[><](\&\d+)?[ \t]+\S+/s) {
+ $done .= $`; $this = $&; $todo = $';
+ &lint_warning($file, $done, $this, "section $section: whitespace after shell redirection (expected none)");
+ $done .= $this;
+ }
+
+ # utility function: extract a single shell command
+ sub command_extract {
+ my ($script) = @_;
+ my $cmd = '';
+ while ($script ne '') {
+ $script =~ s/^([ \t]*'[^']*')/ $cmd .= $1, ''/se && next;
+ $script =~ s/^([ \t]*"[^"]*")/ $cmd .= $1, ''/se && next;
+ $script =~ s/^([ \t]*[^ \t;\)\\\r\n]+)/$cmd .= $1, ''/se && next;
+ $script =~ s/^([ \t]*\\[ \t]*\r?\n)/ $cmd .= $1, ''/se && next;
+ last;
+ }
+ return ($cmd, $script);
+ }
+
+ # utility function: join a multi-line command
+ sub multiline_join {
+ my ($cmd) = @_;
+ $cmd =~ s/([ \t]*\\[ \t]*\r?\n[ \t]*)/ /sg;
+ return $cmd;
+ }
+
+ # utility function: split command into arguments
+ sub shell_tokenize {
+ my ($cmd) = @_;
+ my @cmd = ();
+ while ($cmd ne '') {
+ $cmd =~ s/^\s*('[^']*')/push(@cmd, $1), ''/se && next;
+ $cmd =~ s/^\s*("[^"]*")/push(@cmd, $1), ''/se && next;
+ $cmd =~ s/^\s*(\S+)/ push(@cmd, $1), ''/se && next;
+ $cmd =~ s/^\s*$/ ''/se && last;
+ }
+ return @cmd;
+ }
+
+ # check filesystem path style
+ $done = $outer_done; $this = ''; $todo = $outer_this;
+ while ($todo =~ m/\%\{l_shtool\}\s+(\w+)\s+/s) {
+ $done .= $`; $this = $&; $todo = $';
+ ($this, $todo) = &command_extract($this . $todo);
+
+ # check for shtool options with no space before argument
+ my $subthis = $this;
+ $subthis =~ s/\%{[A-Za-z][A-Za-z0-9_]*}//sg;
+ $subthis =~ s/\%{[A-Za-z][A-Za-z0-9_]*(?:\s+[^}]+?)?}//sg;
+ # remove content of quoted arguments as they might contain unrelated things to catch
+ $subthis =~ s/'[^']*'/ARG/sg;
+ $subthis =~ s/"[^"]*"/ARG/sg;
+ $subthis =~ s/`[^`]*`/ARG/sg;
+ # remove us completely if line ends with a quote as this indicates we are a quoted argument to ourselfs
+ $subthis =~ s/[^']*'$//s;
+ $subthis =~ s/[^"]*"$//s;
+ $subthis =~ s/[^`]*`$//s;
+ $subthis =~ s/[^']*' \\\n//s;
+ $subthis =~ s/[^"]*" \\\n//s;
+ $subthis =~ s/[^`]*` \\\n//s;
+ # catch command termination by semicolon, pipe, or, and;
+ $subthis =~ s/[;|&].*$//s;
+ if ($subthis =~ m/\s-[a-zA-Z]\S/) {
+ &lint_warning($file, $done, $this, "found use of shtool option with space omitted before argument");
+ }
+
+ # openpkg-rc is special because does bootstrap things
+ last if ($file =~ m|openpkg-rc\.spec$|);
+
+ my @cmd = &shell_tokenize(&multiline_join($this));
+ if ($cmd[1] eq 'mkdir') {
+ # join flags with their arguments
+ for (my $i = 2; $i <= $#cmd; $i++) {
+ if ($cmd[$i] eq '-m') {
+ splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]);
+ }
+ }
+ # check paths
+ for (my $i = $#cmd; $i > 1 and $cmd[$i] !~ m|^-| and $cmd[$i] !~ m|^\d+$|; $i--) {
+ if ($cmd[$i] =~ m|/$|) {
+ &lint_warning($file, $done, $this, "section $section: superfluous trailing slash on " .
+ "created path in \"shtool mkdir\" command (expected none)");
+ }
+ }
+ }
+ elsif ($cmd[1] eq 'install') {
+ # join flags with their arguments
+ for (my $i = 2; $i <= $#cmd; $i++) {
+ if ($cmd[$i] =~ m/-(e|m|o|g)$/) {
+ splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]);
+ }
+ elsif ($cmd[$i] eq '%{SOURCE') {
+ splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]);
+ }
+ elsif ($cmd[$i] eq '%{l_value') {
+ while ($i < $#cmd and $cmd[$i+1] !~ m|\}$|s) {
+ splice(@cmd, $i, 2, $cmd[$i].$cmd[$i+1]);
+ }
+ splice(@cmd, $i, 2);
+ $i--;
+ }
+ }
+
+ # determine last path argument
+ my $i = $#cmd; $i-- while ($i > 1 and $cmd[$i] !~ m|^-| and $cmd[$i] !~ m|^\d+$|);
+
+ # check paths
+ if (($#cmd - $i) > 2 and $cmd[-1] !~ m|/$|) {
+ # simple case: multiple sources require target to be a directory
+ &lint_warning($file, $done, $this, "section $section: missing trailing slash on " .
+ "destination path in \"shtool install\" command (expected one) 1");
+ }
+ elsif (($#cmd - $i) == 2 and $cmd[-1] !~ m|/$|) {
+ # complex case: single source, so we can check only known destination paths
+ if ( $cmd[-1] =~ m/\%\{l_prefix\}\/(bin|cgi|include|info|lib|pub|sbin)$/
+ or $cmd[-1] =~ m/\%\{l_prefix\}\/(etc|libexec|man|share|var)(\/[^\/]+)?$/) {
+ &lint_warning($file, $done, $this, "section $section: missing trailing slash on " .
+ "destination path in \"shtool install\" command (expected one) 2");
+ }
+ }
+ else {
+ # special case: if any path contains '*', then globbing might occur
+ for (my $i = 2; $i <= $#cmd; $i++) {
+ if ($cmd[$i] =~ m/\*/ and $cmd[-1] !~ m|/$|) {
+ &lint_warning($file, $done, $this, "section $section: missing trailing slash on " .
+ "destination path in \"shtool install\" command (expected one) 3");
+ last;
+ }
+ }
+ }
+ }
+ $done .= $this;
+ }
+
+ # check for redundant 'export PATH' statements
+ #$done = $outer_done; $this = ''; $todo = $outer_this;
+ #while ($todo =~ m/\bexport[ \t]+([a-zA-Z_][a-zA-Z0-9_]*[ \t]+)*PATH\b/s) {
+ # $done .= $`; $this = $&; $todo = $';
+ # &lint_warning($file, $done, $this, "section $section: redundant \"export PATH\" statement (expected none)");
+ # $done .= $this;
+ #}
+
+ # check for obsolete cpp build flags
+ $done = $outer_done; $this = ''; $todo = $outer_this;
+ while ($todo =~ m/-I\%\{l_prefix\}\/include(\/([^ \t"';]+?))?[ \t"';]/s) {
+ $done .= $`; $this = $&; $todo = $';
+ my ($subdir) = $2;
+ &lint_warning($file, $done, $this, "section $section: hard-coded C pre-processor path option " .
+ "(use \"%{l_cppflags".($subdir ? " $subdir" : "")."}\" instead)");
+ $done .= $this;
+ }
+
+ # check for obsolete ld build flags
+ $done = $outer_done; $this = ''; $todo = $outer_this;
+ while ($todo =~ m/-L\%\{l_prefix\}\/lib(\/([^ \t"';]+?))?[ \t"';]/s) {
+ $done .= $`; $this = $&; $todo = $';
+ my ($subdir) = $2;
+ &lint_warning($file, $done, $this, "section $section: hard-coded linker path option " .
+ "(use \"%{l_ldflags".($subdir ? " $subdir" : "")."}\" instead)");
+ $done .= $this;
+ }
+
+ # check for "raw" tool usage
+ if ($section ne '%description' and $file !~ m|openpkg\.spec$|) {
+ foreach my $token (&shell_tokenize($outer_this)) {
+ if ($token =~ m/^(rpmtool|shtool|curl|bash|gzip|bzip2|tar|cc|cxx|make|patch)$/s) {
+ &lint_warning($file, undef, undef, "section $section: raw usage of standard tool \"$token\"" .
+ " (use \"%{l_${token}}\" instead)");
+ }
+ }
+ }
+
+ # check for setup and patch macro usage
+ if ($section eq '%prep') {
+ $done = $outer_done; $this = ''; $todo = $outer_this;
+ my @tokens = &shell_tokenize($outer_this);
+ while ($todo =~ m/([^\n]*)\n/s) {
+ $done .= $`; $this = $&; $todo = $';
+ if ($this =~ m/^ *%setup.* .*$/) {
+ &lint_warning($file, undef, undef, "section $section: multiple spaces in \"\%setup\" macro" .
+ " (reduce to single space)");
+ }
+ if ($this =~ m/^ *%setup.*-[ab]$/ and grep(/^\%setup+$/, @tokens) == 1) {
+ &lint_warning($file, undef, undef, "section $section: -[ab] option superflous for single \"\%setup\" macro" .
+ " (remove it)");
+ }
+ if ($this =~ m/^ *%patch.* .*$/) {
+ &lint_warning($file, undef, undef, "section $section: multiple spaces in \"\%patch\" macro" .
+ " (reduce to single space)");
+ }
+ if ($this =~ m/^ *%patch.*-p +[01]/) {
+ &lint_warning($file, undef, undef, "section $section: syntactically wrong space in \"\%patch\" macro" .
+ " (use -pN)");
+ }
+ if ($this =~ m/^ *%patch.*-P 0\s*$/ and grep(/^\%patch+$/, @tokens) == 1) {
+ &lint_warning($file, undef, undef, "section $section: syntactically wrong space in \"\%patch\" macro" .
+ " (omit -P 0 for single patch)");
+ }
+ if ($this =~ m/^ *%patch.*-P [^0-9]/ and grep(/^\%patch+$/, @tokens) > 1) {
+ &lint_warning($file, undef, undef, "section $section: syntactically wrong space in \"\%patch\" macro" .
+ " (use -P for multiple patches)");
+ }
+ $done .= $this;
+ }
+ if (grep(/^\%setup\d+$/, @tokens)) {
+ &lint_warning($file, undef, undef, "section $section: numbered \"\%setup\" macro" .
+ " (do not use %setup directly followed by a number, replace with %setup ... -[ab])");
+ }
+ if (grep(/^\%patch\d+$/, @tokens)) {
+ &lint_warning($file, undef, undef, "section $section: numbered \"\%patch\" macro" .
+ " (do not use %patch directly followed by a number, replace with %patch ... -P)");
+ }
+ }
+
+ # check for hard-coded prefix in %pre[un],%post[un] scripts
+ if ($section =~ m/^\%(preun|postun|pre|post)$/s and $file !~ m|openpkg\.spec$|) {
+ $done = $outer_done; $this = ''; $todo = $outer_this;
+ while ($todo =~ m/\%\{l_prefix\}/s) {
+ $done .= $`; $this = $&; $todo = $';
+ &lint_warning($file, $done, $this, "section $section: hard-coded prefix (\%{l_prefix}) found " .
+ "(use \"\$RPM_INSTALL_PREFIX\" to allow relocation)");
+ $done .= $this;
+ }
+ }
+
+ # check for sub-shell parenthesis style
+ # (this is such complicated because the Bourne Shell has a
+ # construct "case in )