diff options
author | rillig <rillig@pkgsrc.org> | 2015-11-25 16:42:21 +0000 |
---|---|---|
committer | rillig <rillig@pkgsrc.org> | 2015-11-25 16:42:21 +0000 |
commit | f631f44d1bc6cde1a081022f2bb0a6637fa08677 (patch) | |
tree | 0ac4f352fef7f807bff85bb9861afe2b69c21513 /pkgtools | |
parent | 353ff0d24230a862293b12e8df69306ed961b035 (diff) | |
download | pkgsrc-f631f44d1bc6cde1a081022f2bb0a6637fa08677.tar.gz |
Reimported pkglint-4.518 from pkgtools/pkglint as pkglint4
The Perl version of pkglint (pkglint<5.0) runs on all platforms that
are supported by pkgsrc. Not so the Go version (pkglint>=5.0).
To support development of packages on all platforms, this version is
provided, and it will be supported equally. Its output differs a bit
from pkglint>=5.0, but the basic checks are the same.
Diffstat (limited to 'pkgtools')
38 files changed, 11410 insertions, 1 deletions
diff --git a/pkgtools/Makefile b/pkgtools/Makefile index cc2b57f7840..67dfc51bbaf 100644 --- a/pkgtools/Makefile +++ b/pkgtools/Makefile @@ -1,4 +1,4 @@ -# $NetBSD: Makefile,v 1.111 2015/11/22 21:58:36 agc Exp $ +# $NetBSD: Makefile,v 1.112 2015/11/25 16:42:21 rillig Exp $ # COMMENT= Tools for use in the packages collection @@ -56,6 +56,7 @@ SUBDIR+= pkgdiff SUBDIR+= pkgfind SUBDIR+= pkgin SUBDIR+= pkglint +SUBDIR+= pkglint4 SUBDIR+= pkgse SUBDIR+= pkgsrc-todo SUBDIR+= pkgsurvey diff --git a/pkgtools/pkglint4/DESCR b/pkgtools/pkglint4/DESCR new file mode 100644 index 00000000000..bff78e0924e --- /dev/null +++ b/pkgtools/pkglint4/DESCR @@ -0,0 +1,7 @@ +pkglint is for pkgsrc packages what lint(1) is for C files. It checks +for various things that the used languages cannot detect, for example +application-specific restrictions on certain variables. + +Before importing a new package or making changes to an existing package, +pkglint should be run in the package's directory to check for common +errors. diff --git a/pkgtools/pkglint4/Makefile b/pkgtools/pkglint4/Makefile new file mode 100644 index 00000000000..7adf676a4e4 --- /dev/null +++ b/pkgtools/pkglint4/Makefile @@ -0,0 +1,78 @@ +# $NetBSD: Makefile,v 1.1 2015/11/25 16:42:21 rillig Exp $ + +PKGNAME= pkglint4-4.158 +CATEGORIES= pkgtools + +OWNER= wiz@NetBSD.org +HOMEPAGE= http://www.NetBSD.org/docs/pkgsrc/ +COMMENT= Verifier for NetBSD packages (old version, for all platforms) +LICENSE= 2-clause-bsd + +DEPENDS+= p5-Digest-SHA1-[0-9]*:../../security/p5-Digest-SHA1 +DEPENDS+= p5-enum>=1.016:../../devel/p5-enum +DEPENDS+= p5-pkgsrc-Dewey>=1.0:../../pkgtools/p5-pkgsrc-Dewey +CONFLICTS+= pkglint>=5.0 + +#BUILD_DEPENDS+= p5-Test-Deep-[0-9]*:../../devel/p5-Test-Deep +#BUILD_DEPENDS+= p5-Test-Trap-[0-9]*:../../devel/p5-Test-Trap + +WRKSRC= ${WRKDIR} +NO_CHECKSUM= yes +USE_LANGUAGES= # none +USE_TOOLS+= perl:run +AUTO_MKDIRS= yes +PREV_PKGPATH= pkgtools/pkglint + +.include "../../mk/bsd.prefs.mk" + +SUBST_CLASSES+= pkglint +SUBST_STAGE.pkglint= post-configure +SUBST_FILES.pkglint+= build.pl pkglint.pl pkglint.t plist-clash.pl +.if defined(BATCH) +SUBST_SED.pkglint+= -e s\|@PKGSRCDIR@\|/usr/pkgsrc\|g +.else +SUBST_SED.pkglint+= -e s\|@PKGSRCDIR@\|${PKGSRCDIR}\|g +.endif +SUBST_SED.pkglint+= -e s\|@PREFIX@\|${PREFIX}\|g +SUBST_SED.pkglint+= -e s\|@DISTVER@\|${PKGNAME:S/pkglint-//}\|g +SUBST_SED.pkglint+= -e s\|@MAKE@\|${MAKE:Q}\|g +SUBST_SED.pkglint+= -e s\|@PERL@\|${PERL5:Q}\|g +SUBST_SED.pkglint+= -e s\|@DATADIR@\|${FILESDIR}\|g + +SUBST_CLASSES+= mappaths +SUBST_STAGE.mappaths= pre-install +SUBST_FILES.mappaths+= pkglint.pl +SUBST_SED.mappaths+= -e s\|${FILESDIR}\|${PREFIX}/share/pkglint\|g + +do-extract: + cd ${FILESDIR} && ${CP} build.pl pkglint.0 pkglint.1 pkglint.pl pkglint.t plist-clash.pl ${WRKSRC} + mkdir ${WRKSRC}/PkgLint + cd ${FILESDIR} && ${CP} PkgLint/*.pm ${WRKSRC}/PkgLint + +do-build: + cd ${WRKSRC} && ${PERL5} build.pl < pkglint.pl > pkglint.pl.inlined \ + && mv pkglint.pl.inlined pkglint.pl + +do-test: + cd ${WRKSRC} && prove pkglint.t + +do-install: + ${INSTALL_SCRIPT} ${WRKSRC}/pkglint.pl ${DESTDIR}${PREFIX}/bin/pkglint + ${INSTALL_SCRIPT} ${WRKSRC}/plist-clash.pl ${DESTDIR}${PREFIX}/bin/plist-clash +.if !empty(MANINSTALL:Mcatinstall) +. if defined(CATMAN_SECTION_SUFFIX) && !empty(CATMAN_SECTION_SUFFIX:M[Yy][Ee][Ss]) + ${INSTALL_MAN} ${WRKSRC}/pkglint.0 ${DESTDIR}${PREFIX}/${PKGMANDIR}/cat1/pkglint.1 +. else + ${INSTALL_MAN} ${WRKSRC}/pkglint.0 ${DESTDIR}${PREFIX}/${PKGMANDIR}/cat1 +. endif +.endif +.if !empty(MANINSTALL:Mmaninstall) + ${INSTALL_MAN} ${WRKSRC}/pkglint.1 ${DESTDIR}${PREFIX}/${PKGMANDIR}/man1 +.endif + ${INSTALL_DATA} ${FILESDIR}/makevars.map ${DESTDIR}${PREFIX}/share/pkglint/ + ${INSTALL_DATA} ${FILESDIR}/deprecated.map ${DESTDIR}${PREFIX}/share/pkglint/ + +selftest: .PHONY + ${PREFIX}/bin/pkglint + +.include "../../mk/bsd.pkg.mk" diff --git a/pkgtools/pkglint4/PLIST b/pkgtools/pkglint4/PLIST new file mode 100644 index 00000000000..cf8a9737107 --- /dev/null +++ b/pkgtools/pkglint4/PLIST @@ -0,0 +1,7 @@ +@comment $NetBSD: PLIST,v 1.1 2015/11/25 16:42:21 rillig Exp $ +bin/pkglint +bin/plist-clash +man/cat1/pkglint.0 +man/man1/pkglint.1 +share/pkglint/deprecated.map +share/pkglint/makevars.map diff --git a/pkgtools/pkglint4/README b/pkgtools/pkglint4/README new file mode 100644 index 00000000000..6c5eb9d7479 --- /dev/null +++ b/pkgtools/pkglint4/README @@ -0,0 +1,27 @@ +$NetBSD: README,v 1.1 2015/11/25 16:42:21 rillig Exp $ + +== Current problems == + +There are finally some automated tests that document a few intended +and actual behaviors. There is still plenty of poorly expressed +code not yet under test and therefore not yet safe to refactor. + +The current pkglint architecture will not scale much further. What is +needed next are parsers for nested, non-context-free languages (make(1), +sh(1), sed(1)). The parsers should be able to recognize partial +structures, as well as structures containing foreign parts. This is +because most of pkgsrc is heavily based on preprocessors: + +- The .if and .for directives in Makefiles are preprocessed by make(1) + before building dependencies and shell commands out of the remaining + text. + +- make(1) assembles shell commands from literal text and variables like + ${PKGNAME}. + +- Shell commands often use dynamic evaluation of variables. + +All this makes enhancing pkglint non-trivial. If you know of any +academic papers that might be of help in this case, please tell me. + +The pkglint source code is much too big for a single file. diff --git a/pkgtools/pkglint4/TODO b/pkgtools/pkglint4/TODO new file mode 100644 index 00000000000..52b0d5d2ffa --- /dev/null +++ b/pkgtools/pkglint4/TODO @@ -0,0 +1,37 @@ +$NetBSD: TODO,v 1.1 2015/11/25 16:42:21 rillig Exp $ + +Please add your own entries at the bottom of this file. If possible, +include the name of an example package where a warning should occur. + +* When you understand some behavior of the code, document it by + adding automated tests to pkglint.t! +* warn about the use of ${WRKDIR:=...}, as this construct should only + be used with lists. +* Add checks for binary packages. See Debian/lintian for ideas. +* Of the user-defined variables, some may be used at load-time and some + don't. Find out how pkglint can distinguish them. +* Make sure that no variable is modified at load-time after it has been + used once. This should at least flag BUILD_DEFS in bsd.pkg.mk. +* Invent an annotation scheme for files that intentionally define + variables for use in other files. +* ${MACHINE_ARCH}-${LOWER_OPSYS}elf in PLISTs etc. is a NetBSD config.guess + problem ==> use of ${APPEND_ELF} +* Packages including lang/python/extension.mk must follow the Python version + scheme. Enforcing PYPKGPREFIX for those is most likely a good idea. +* Check for parallel files/dirs whose names differ only in case. +* Check for license files that are completely unused. +* If a dependency depends on an option (in options.mk), it should also + depend on the same option in the buildlink3.mk file. +* Complain about ${PKGSRC_COMPILER} == "sunpro", which should be + !empty(PKGSRC_COMPILER:Msunpro). +* If USE_TOOLS has autoconf213, and the package does stuff like + cd ${WRKSRC} && autoconf, then an incorrect warning is issued. +* LOCALBASE should not be used in normal Makefiles +* don't complain about "procedure calls", like for pkg-build-options in + the various buildlink3.mk files. +* if package A conflicts with B, then B should also conflict with A. +* When pkglint runs on a case-insensitive filesystem, it should still + point out problems that only occur on case-sensitive filesystems. For + example, devel/p5-Net-LDAP and devel/p5-Net-ldap should be considered + different paths. +* Warn about using REPLACE_PYTHON without including application.mk. diff --git a/pkgtools/pkglint4/files/PkgLint/CVS_Entry.pm b/pkgtools/pkglint4/files/PkgLint/CVS_Entry.pm new file mode 100644 index 00000000000..3fe7559fd65 --- /dev/null +++ b/pkgtools/pkglint4/files/PkgLint/CVS_Entry.pm @@ -0,0 +1,21 @@ +# $NetBSD: CVS_Entry.pm,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# +# One line from a CVS/Entries file. +# +package PkgLint::CVS_Entry; + +use strict; +use warnings; + +use enum qw(FNAME REVISION MTIME TAG); + +sub new($$$$$) { + my ($class, $fname, $revision, $date, $tag) = @_; + my $self = [ $fname, $revision, $date, $tag ]; + bless($self, $class); + return $self; +} +sub fname($) { return shift()->[FNAME]; } +sub revision($) { return shift()->[REVISION]; } +sub mtime($) { return shift()->[MTIME]; } +sub tag($) { return shift()->[TAG]; } diff --git a/pkgtools/pkglint4/files/PkgLint/Change.pm b/pkgtools/pkglint4/files/PkgLint/Change.pm new file mode 100644 index 00000000000..89368c465fe --- /dev/null +++ b/pkgtools/pkglint4/files/PkgLint/Change.pm @@ -0,0 +1,21 @@ +# $NetBSD: Change.pm,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# +# A change entry from doc/CHANGES-* +# +package PkgLint::Change; + +use strict; +use warnings; + +sub new($$$$$$) { + my ($class, $line, $action, $pkgpath, $version, $author, $date) = @_; + my $self = [ $line, $action, $pkgpath, $version, $author, $date ]; + bless($self, $class); + return $self; +} +sub line($) { return shift()->[0]; } +sub action($) { return shift()->[1]; } +sub pkgpath($) { return shift()->[2]; } +sub version($) { return shift()->[3]; } +sub author($) { return shift()->[4]; } +sub date($) { return shift()->[5]; } diff --git a/pkgtools/pkglint4/files/PkgLint/FileUtil.pm b/pkgtools/pkglint4/files/PkgLint/FileUtil.pm new file mode 100644 index 00000000000..f29c2bdccc7 --- /dev/null +++ b/pkgtools/pkglint4/files/PkgLint/FileUtil.pm @@ -0,0 +1,166 @@ +# $NetBSD: FileUtil.pm,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# +# Subroutines for loading and saving line-oriented files. +# The load_file() subroutine loads a file completely into memory, +# optionally handling continuation line folding. The load_lines() subrou- +# tine is an abbreviation for the common case of loading files without +# continuation lines. The save_autofix_changes() subroutine examines an +# array of lines if some of them have changed. It then saves the modified +# files. +# +package PkgLint::FileUtil; + +use strict; +use warnings; + +BEGIN { + use Exporter; + use vars qw(@ISA @EXPORT_OK); + @ISA = qw(Exporter); + @EXPORT_OK = qw( + load_file load_lines + save_autofix_changes + ); + + import PkgLint::Util qw( + false true + ); + import PkgLint::Logging qw( + NO_LINE_NUMBER + log_error log_note + ); +} + +sub load_physical_lines($) { + my ($fname) = @_; + my ($physlines, $line, $lineno); + + $physlines = []; + open(my $f, "<", $fname) or return; + $lineno = 0; + while (defined($line = <$f>)) { + $lineno++; + push(@{$physlines}, [$lineno, $line]); + } + close($f) or return; + return $physlines; +} + +sub get_logical_line($$$) { + my ($fname, $lines, $ref_lineno) = @_; + my ($value, $lineno, $first, $firstlineno, $lastlineno, $physlines); + + $value = ""; + $first = true; + $lineno = ${$ref_lineno}; + $firstlineno = $lines->[$lineno]->[0]; + $physlines = []; + + for (; $lineno <= $#{$lines}; $lineno++) { + if ($lines->[$lineno]->[1] =~ m"^([ \t]*)(.*?)([ \t]*)(\\?)\n?$") { + my ($indent, $text, $outdent, $cont) = ($1, $2, $3, $4); + + if ($first) { + $value .= $indent; + $first = false; + } + + $value .= $text; + push(@{$physlines}, $lines->[$lineno]); + + if ($cont eq "\\") { + $value .= " "; + } else { + $value .= $outdent; + last; + } + } + } + + if ($lineno > $#{$lines}) { + # The last line in the file is a continuation line + $lineno--; + } + $lastlineno = $lines->[$lineno]->[0]; + ${$ref_lineno} = $lineno + 1; + + return PkgLint::Line->new($fname, + $firstlineno == $lastlineno + ? $firstlineno + : "$firstlineno--$lastlineno", + $value, + $physlines); +} + +sub load_lines($$) { + my ($fname, $fold_backslash_lines) = @_; + my ($physlines, $seen_newline, $loglines); + + $physlines = load_physical_lines($fname); + if (!$physlines) { + return false; + } + + $seen_newline = true; + $loglines = []; + if ($fold_backslash_lines) { + for (my $lineno = 0; $lineno <= $#{$physlines}; ) { + push(@{$loglines}, get_logical_line($fname, $physlines, \$lineno)); + } + } else { + foreach my $physline (@{$physlines}) { + my $text = $physline->[1]; + + $text =~ s/\n$//; + push(@{$loglines}, PkgLint::Line->new($fname, $physline->[0], $text, [$physline])); + } + } + + if (0 <= $#{$physlines} && $physlines->[-1]->[1] !~ m"\n$") { + log_error($fname, $physlines->[-1]->[0], "File must end with a newline."); + } + + return $loglines; +} + +sub load_file($) { + my ($fname) = @_; + + return load_lines($fname, false); +} + +sub save_autofix_changes($) { + my ($lines) = @_; + + my (%changed, %physlines); + + foreach my $line (@{$lines}) { + if ($line->is_changed) { + $changed{$line->fname}++; + } + push(@{$physlines{$line->fname}}, @{$line->physlines}); + } + + foreach my $fname (sort(keys(%changed))) { + my $new = "${fname}.pkglint.tmp"; + my $f; + + if (!open($f, ">", $new)) { + log_error($new, NO_LINE_NUMBER, "$!"); + next; + } + foreach my $physline (@{$physlines{$fname}}) { + print $f ($physline->[1]); + } + if (!close($f)) { + log_error($new, NO_LINE_NUMBER, "$!"); + next; + } + + if (!rename($new, $fname)) { + log_error($fname, NO_LINE_NUMBER, "$!"); + next; + } + log_note($fname, NO_LINE_NUMBER, "Has been autofixed. Please re-run pkglint."); + } +} diff --git a/pkgtools/pkglint4/files/PkgLint/Line.pm b/pkgtools/pkglint4/files/PkgLint/Line.pm new file mode 100644 index 00000000000..cc93af4f59e --- /dev/null +++ b/pkgtools/pkglint4/files/PkgLint/Line.pm @@ -0,0 +1,217 @@ +# $NetBSD: Line.pm,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# +# When files are read in by pkglint, they are interpreted in terms of +# lines. For Makefiles, line continuations are handled properly, allowing +# multiple physical lines to end in a single logical line. For other files +# there is a 1:1 translation. +# +# A difference between the physical and the logical lines is that the +# physical lines include the line end sequence, whereas the logical lines +# do not. +# +# A logical line is a class having the read-only fields C<file>, +# C<lines>, C<text>, C<physlines> and C<is_changed>, as well as some +# methods for printing diagnostics easily. +# +# Some other methods allow modification of the physical lines, but leave +# the logical line (the C<text>) untouched. These methods are used in the +# --autofix mode. +# +# A line can have some "extra" fields that allow the results of parsing to +# be saved under a name. +# +package PkgLint::Line; + +use strict; +use warnings; + +BEGIN { + import PkgLint::Util qw( + false true + assert + ); +} + +use enum qw(FNAME LINES TEXT PHYSLINES CHANGED BEFORE AFTER EXTRA); + +sub new($$$$) { + my ($class, $fname, $lines, $text, $physlines) = @_; + my ($self) = ([$fname, $lines, $text, $physlines, false, [], [], {}]); + bless($self, $class); + return $self; +} + +sub fname($) { return shift()->[FNAME]; } +sub lines($) { return shift()->[LINES]; } +sub text($) { return shift()->[TEXT]; } +# Note: physlines is _not_ a simple getter method. +sub is_changed($) { return shift()->[CHANGED]; } + +# querying, getting and setting the extra values. +sub has($$) { + my ($self, $name) = @_; + return exists($self->[EXTRA]->{$name}); +} +sub get($$) { + my ($self, $name) = @_; + assert(false, "Field ${name} does not exist.") + unless exists($self->[EXTRA]->{$name}); + return $self->[EXTRA]->{$name}; +} +sub set($$$) { + my ($self, $name, $value) = @_; + assert(false, "Field ${name} already exists.") + if exists($self->[EXTRA]->{$name}); + + # Make sure that the line does not become a cyclic data structure. + my $type = ref($value); + if ($type eq "") { + # ok + } elsif ($type eq "ARRAY") { + foreach my $element (@{$value}) { + my $element_type = ref($element); + assert(false, "Invalid array data type: name=${name}, type=${element_type}.") + unless $element_type eq "" || $element_type eq "PkgLint::SimpleMatch"; + } + } else { + assert(false, "Invalid data: name=${name}, value=${value}."); + } + + $self->[EXTRA]->{$name} = $value; +} + +sub physlines($) { + my ($self) = @_; + return [@{$self->[BEFORE]}, @{$self->[PHYSLINES]}, @{$self->[AFTER]}]; +} + +# Only for PkgLint::String support +sub substring($$$$) { + my ($self, $line, $start, $end) = @_; + + return substr($self->[PHYSLINES]->[$line]->[1], $start, $end); +} + +sub show_source($$) { + my ($self, $out) = @_; + + if (PkgLint::Logging::get_show_source_flag()) { + print $out ("\n"); + foreach my $line (@{$self->physlines}) { + print $out ("> " . $line->[1]); + } + } +} + +sub log_fatal($$) { + my ($self, $text) = @_; + + $self->show_source(*STDERR); + PkgLint::Logging::log_fatal($self->fname, $self->[LINES], $text); +} +sub log_error($$) { + my ($self, $text) = @_; + + $self->show_source(*STDOUT); + PkgLint::Logging::log_error($self->fname, $self->[LINES], $text); +} +sub log_warning($$) { + my ($self, $text) = @_; + + $self->show_source(*STDOUT); + PkgLint::Logging::log_warning($self->fname, $self->[LINES], $text); +} +sub log_note($$) { + my ($self, $text) = @_; + + $self->show_source(*STDOUT); + PkgLint::Logging::log_note($self->fname, $self->[LINES], $text); +} +sub log_debug($$) { + my ($self, $text) = @_; + + $self->show_source(*STDOUT); + PkgLint::Logging::log_debug($self->fname, $self->[LINES], $text); +} +sub explain_error($@) { + my ($self, @texts) = @_; + + PkgLint::Logging::explain_error($self->fname, $self->[LINES], @texts); +} +sub explain_warning($@) { + my ($self, @texts) = @_; + + PkgLint::Logging::explain_warning($self->fname, $self->[LINES], @texts); +} +sub explain_note($@) { + my ($self, @texts) = @_; + + PkgLint::Logging::explain_note($self->fname, $self->[LINES], @texts); +} +sub explain_info($@) { + my ($self, @texts) = @_; + + PkgLint::Logging::explain_info($self->fname, $self->[LINES], @texts); +} + +sub to_string($) { + my ($self) = @_; + + return $self->fname . ":" . $self->[LINES] . ": " . $self->[TEXT]; +} + +sub prepend_before($$) { + my ($self, $text) = @_; + + unshift(@{$self->[BEFORE]}, [0, "$text\n"]); + $self->[CHANGED] = true; +} +sub append_before($$) { + my ($self, $text) = @_; + + push(@{$self->[BEFORE]}, [0, "$text\n"]); + $self->[CHANGED] = true; +} +sub prepend_after($$) { + my ($self, $text) = @_; + + unshift(@{$self->[AFTER]}, [0, "$text\n"]); + $self->[CHANGED] = true; +} +sub append_after($$) { + my ($self, $text) = @_; + + push(@{$self->[AFTER]}, [0, "$text\n"]); + $self->[CHANGED] = true; +} +sub delete($) { + my ($self) = @_; + + $self->[PHYSLINES] = []; + $self->[CHANGED] = true; +} +sub replace($$$) { + my ($self, $from, $to) = @_; + my $phys = $self->[PHYSLINES]; + + foreach my $i (0..$#{$phys}) { + if ($phys->[$i]->[0] != 0 && $phys->[$i]->[1] =~ s/\Q$from\E/$to/g) { + $self->[CHANGED] = true; + } + } +} +sub replace_regex($$$) { + my ($self, $from_re, $to) = @_; + my $phys = $self->[PHYSLINES]; + + foreach my $i (0..$#{$phys}) { + if ($phys->[$i]->[0] != 0 && $phys->[$i]->[1] =~ s/$from_re/$to/) { + $self->[CHANGED] = true; + } + } +} +sub set_text($$) { + my ($self, $text) = @_; + $self->[PHYSLINES] = [[0, "$text\n"]]; + $self->[CHANGED] = true; +} diff --git a/pkgtools/pkglint4/files/PkgLint/Logging.pm b/pkgtools/pkglint4/files/PkgLint/Logging.pm new file mode 100644 index 00000000000..eecb17a6a59 --- /dev/null +++ b/pkgtools/pkglint4/files/PkgLint/Logging.pm @@ -0,0 +1,134 @@ +# $NetBSD: Logging.pm,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# +# Subroutines for printing messages to the user in a common format. +# The subroutines all have the parameters C<$fname>, +# C<$lineno> and C<$message>. In case there's no appropriate filename for +# the message, NO_FILE may be passed, likewise for C<$lineno> and +# NO_LINES. Before printing, the filename is normalized, that is, +# "/foo/bar/../../" components are removed, as well as "." components. +# At the end of the program, the subroutine print_summary_and_exit should +# be called. +# +# Examples: +# log_error(NO_FILE, NO_LINES, "Invalid command line."); +# log_warning($fname, NO_LINES, "Not found."); +# log_debug($fname, $lineno, sprintf("invalid character (0x%02x).", $c)); +# +package PkgLint::Logging; + +use strict; +use warnings; + +BEGIN { + use Exporter; + use vars qw(@ISA @EXPORT_OK); + @ISA = qw(Exporter); + @EXPORT_OK = qw( + NO_FILE NO_LINE_NUMBER NO_LINES + log_fatal log_error log_warning log_note log_debug + explain_error explain_warning explain_info + print_summary_and_exit + set_explain set_gcc_output_format + get_show_source_flag set_show_source_flag + ); + import PkgLint::Util qw( + false true + normalize_pathname + ); +} + +use constant NO_FILE => undef; +use constant NO_LINE_NUMBER => undef; +use constant NO_LINES => undef; + +use enum qw(:LL_ FATAL ERROR WARNING NOTE DEBUG); + +use constant traditional_type => ["FATAL", "ERROR", "WARN", "NOTE", "DEBUG"]; +use constant gcc_type => ["fatal", "error", "warning", "note", "debug"]; + +my $errors = 0; +my $warnings = 0; +my $gcc_output_format = false; +my $explain_flag = false; +my $show_source_flag = false; + +sub strxvis($) { + my ($s) = @_; + + $s =~ s/([^\x09\x20-\x7e])/"\\x" . unpack("H*", $1)/eg; + return $s; +} + +sub log_message { # no prototype due to Perl weirdness + my ($level, $fname, $lineno, $message) = @_; + my ($text, $sep); + + if (defined($fname)) { + $fname = normalize_pathname($fname); + } + + $text = ""; + $sep = ""; + if (!$gcc_output_format) { + $text .= "${sep}" . traditional_type->[$level] . ":"; + $sep = " "; + } + if (defined($fname)) { + $text .= defined($lineno) + ? "${sep}${fname}:${lineno}" + : "${sep}${fname}"; + $sep = ": "; + } + if ($gcc_output_format) { + $text .= "${sep}" . gcc_type->[$level] . ":"; + $sep = " "; + } + if (defined($message)) { + $text .= $sep . strxvis($message); + $sep = ""; + } + + if ($level == LL_FATAL) { + print STDERR ("${text}\n"); + } else { + print STDOUT ("${text}\n"); + } +} + +sub log_fatal($$$) { log_message(LL_FATAL, @_); exit(1); } +sub log_error($$$) { log_message(LL_ERROR, @_); $errors++; } +sub log_warning($$$) { log_message(LL_WARNING, @_); $warnings++; } +sub log_note($$$) { log_message(LL_NOTE, @_); } +sub log_debug($$$) { log_message(LL_DEBUG, @_); } + +sub explain { # no prototype due to Perl weirdness + my ($loglevel, $fname, $lines, @texts) = @_; + my $out = ($loglevel == LL_FATAL) ? *STDERR : *STDOUT; + + if ($explain_flag) { + foreach my $text ("", @texts, "") { + print $out ("\t${text}\n"); + } + } +} +sub explain_error($$@) { explain(LL_ERROR, @_); } +sub explain_warning($$@) { explain(LL_WARNING, @_); } +sub explain_note($$@) { explain(LL_NOTE, @_); } + +sub print_summary_and_exit($) { + my ($quiet) = @_; + + if (!$quiet) { + if ($errors != 0 || $warnings != 0) { + print("$errors errors and $warnings warnings found." . ($explain_flag ? "" : " (Use -e for more details.)") . "\n"); + } else { + print "looks fine.\n"; + } + } + exit($errors != 0); +} + +sub set_explain() { $explain_flag = true; } +sub set_gcc_output_format() { $gcc_output_format = true; } +sub get_show_source_flag() { return $show_source_flag; } +sub set_show_source_flag() { $show_source_flag = true; } diff --git a/pkgtools/pkglint4/files/PkgLint/Patches.pm b/pkgtools/pkglint4/files/PkgLint/Patches.pm new file mode 100644 index 00000000000..b96e7daa5f6 --- /dev/null +++ b/pkgtools/pkglint4/files/PkgLint/Patches.pm @@ -0,0 +1,639 @@ +# $NetBSD: Patches.pm,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# +# Everything concerning checks for patch files. +# + +use strict; +use warnings; + +# Guess the type of file based on the filename. This is used to select +# the proper subroutine for detecting absolute pathnames. +# +# Returns one of "source", "shell", "make", "text", "configure", +# "ignore", "unknown". +# +sub get_filetype($$) { + my ($line, $fname) = @_; + my $basename = basename($fname); + + # The trailig .in part is not needed, since it does not + # influence the type of contents. + $basename =~ s,\.in$,,; + + # Let's assume that everything else that looks like a Makefile + # is indeed a Makefile. + if ($basename =~ m"^I?[Mm]akefile(?:\..*|)?|.*\.ma?k$") { + return "make"; + } + + # Too many false positives for shell scripts, so configure + # scripts get their own category. + if ($basename =~ m"^configure(?:|\.ac)$") { + $opt_debug_unchecked and $line->log_debug("Skipped check for absolute pathnames."); + return "configure"; + } + + if ($basename =~ m"\.(?:sh|m4)$"i) { + return "shell"; + } + + if ($basename =~ m"\.(?:cc?|cpp|cxx|el|hh?|hpp|l|pl|pm|py|s|t|y)$"i) { + return "source"; + } + + if ($basename =~ m"^.+\.(?:\d+|conf|html|info|man|po|tex|texi|texinfo|txt|xml)$"i) { + return "text"; + } + + # Filenames without extension are hard to guess right. :( + if ($basename !~ m"\.") { + return "unknown"; + } + + $opt_debug_misc and $line->log_debug("Don't know the file type of ${fname}."); + + return "unknown"; +} + +sub checkline_cpp_macro_names($$) { + my ($line, $text) = @_; + my ($rest); + + use constant good_macros => PkgLint::Util::array_to_hash(qw( + __STDC__ + + __GNUC__ __GNUC_MINOR__ + __SUNPRO_C + + __i386 + __mips + __sparc + + __APPLE__ + __bsdi__ + __CYGWIN__ + __DragonFly__ + __FreeBSD__ __FreeBSD_version + __INTERIX + __linux__ + __MINGW32__ + __NetBSD__ __NetBSD_Version__ + __OpenBSD__ + __SVR4 + __sgi + __sun + + __GLIBC__ + )); + use constant bad_macros => { + "__sgi__" => "__sgi", + "__sparc__" => "__sparc", + "__sparc_v9__" => "__sparcv9", + "__sun__" => "__sun", + "__svr4__" => "__SVR4", + }; + + $rest = $text; + while ($rest =~ s/defined\((__[\w_]+)\)// || $rest =~ s/\b(_\w+)\(//) { + my ($macro) = ($1); + + if (exists(good_macros->{$macro})) { + $opt_debug_misc and $line->log_debug("Found good macro \"${macro}\"."); + } elsif (exists(bad_macros->{$macro})) { + $line->log_warning("The macro \"${macro}\" is not portable enough. Please use \"".bad_macros->{$macro}."\" instead."); + $line->explain_warning("See the pkgsrc guide, section \"CPP defines\" for details."); + + } elsif ($macro eq "__NetBSD_Prereq__") { + $line->log_warning("Please use __NetBSD_Version__ instead of __NetBSD_Prereq__."); + $line->explain_warning( +"The __NetBSD_Prereq__ macro is pretty new. It was born in NetBSD", +"4.99.3, and maybe it won't survive for long. A better (and compatible)", +"way is to compare __NetBSD_Version__ directly to the required version", +"number."); + + } elsif ($macro =~ m"^_+NetBSD_+Version_+$"i && $macro ne "__NetBSD_Version__") { + $line->log_warning("Misspelled variant \"${macro}\" of \"__NetBSD_Version__\"."); + + } else { + $opt_debug_unchecked and $line->log_debug("Unchecked macro \"${macro}\"."); + } + } +} + +# Checks whether the line contains text that looks like absolute +# pathnames, assuming that the file uses the common syntax with +# single or double quotes to represent strings. +# +sub checkline_source_absolute_pathname($$) { + my ($line, $text) = @_; + my ($abspath); + + $opt_debug_trace and $line->log_debug("checkline_source_absolute_pathname(${text})"); + + if ($text =~ m"(.*)([\"'])(/[^\"']*)\2") { + my ($before, $delim, $string) = ($1, $2, $3); + + $opt_debug_misc and $line->log_debug("checkline_source_absolute_pathname(before=${before}, string=${string})"); + if ($before =~ m"[A-Z_]+\s*$") { + # allowed: PREFIX "/bin/foo" + + } elsif ($string =~ m"^/[*/]") { + # This is more likely to be a C or C++ comment. + + } elsif ($string !~ m"^/\w") { + # Assume that pathnames start with a letter or digit. + + } elsif ($before =~ m"\+\s*$") { + # Something like foodir + '/lib' + + } else { + $abspath = $string; + } + } + + if (defined($abspath)) { + checkword_absolute_pathname($line, $abspath); + } +} + +# Last resort if the file does not look like a Makefile or typical +# source code. All strings that look like pathnames and start with +# one of the typical Unix prefixes are found. +# +sub checkline_other_absolute_pathname($$) { + my ($line, $text) = @_; + + $opt_debug_trace and $line->log_debug("checkline_other_absolute_pathname(\"${text}\")"); + + if ($text =~ m"^#[^!]") { + # Don't warn for absolute pathnames in comments, + # except for shell interpreters. + + } elsif ($text =~ m"^(.*?)((?:/[\w.]+)*/(?:bin|dev|etc|home|lib|mnt|opt|proc|sbin|tmp|usr|var)\b[\w./\-]*)(.*)$") { + my ($before, $path, $after) = ($1, $2, $3); + + if ($before =~ m"\@$") { + # Something like @PREFIX@/bin + + } elsif ($before =~ m"[)}]$") { + # Something like ${prefix}/bin or $(PREFIX)/bin + + } elsif ($before =~ m"\+\s*[\"']$") { + # Something like foodir + '/lib' + + } elsif ($before =~ m"\w$") { + # Something like $dir/lib + + } elsif ($before =~ m"\.$") { + # ../foo is not an absolute pathname. + + } else { + $opt_debug_misc and $line->log_debug("before=${before}"); + checkword_absolute_pathname($line, $path); + } + } +} + +sub checkfile_patch($) { + my ($fname) = @_; + my ($lines); + my ($state, $redostate, $nextstate, $dellines, $addlines, $hunks); + my ($seen_comment, $current_fname, $current_ftype, $patched_files); + my ($leading_context_lines, $trailing_context_lines, $context_scanning_leading); + + # Abbreviations used: + # style: [c] = context diff, [u] = unified diff + # scope: [f] = file, [h] = hunk, [l] = line + # action: [d] = delete, [m] = modify, [a] = add, [c] = context + use constant re_patch_rcsid => qr"^\$.*\$$"; + use constant re_patch_text => qr"^(.+)$"; + use constant re_patch_empty => qr"^$"; + use constant re_patch_cfd => qr"^\*\*\*\s(\S+)(.*)$"; + use constant re_patch_cfa => qr"^---\s(\S+)(.*)$"; + use constant re_patch_ch => qr"^\*{15}(.*)$"; + use constant re_patch_chd => qr"^\*{3}\s(\d+)(?:,(\d+))?\s\*{4}$"; + use constant re_patch_cha => qr"^-{3}\s(\d+)(?:,(\d+))?\s-{4}$"; + use constant re_patch_cld => qr"^(?:-\s(.*))?$"; + use constant re_patch_clm => qr"^(?:!\s(.*))?$"; + use constant re_patch_cla => qr"^(?:\+\s(.*))?$"; + use constant re_patch_clc => qr"^(?:\s\s(.*))?$"; + use constant re_patch_ufd => qr"^---\s(\S+)(?:\s+(.*))?$"; + use constant re_patch_ufa => qr"^\+{3}\s(\S+)(?:\s+(.*))?$"; + use constant re_patch_uh => qr"^\@\@\s-(?:(\d+),)?(\d+)\s\+(?:(\d+),)?(\d+)\s\@\@(.*)$"; + use constant re_patch_uld => qr"^-(.*)$"; + use constant re_patch_ula => qr"^\+(.*)$"; + use constant re_patch_ulc => qr"^\s(.*)$"; + use constant re_patch_ulnonl => qr"^\\ No newline at end of file$"; + + use enum qw(:PST_ + START CENTER TEXT + CFA CH CHD CLD0 CLD CLA0 CLA + UFA UH UL + ); + + my @comment_explanation = ( +"Each patch must document why it is necessary. If it has been applied", +"because of a security issue, a reference to the CVE should be mentioned", +"as well.", +"", +"Since it is our goal to have as few patches as possible, all patches", +"should be sent to the upstream maintainers of the package. After you", +"have done so, you should add a reference to the bug report containing", +"the patch."); + + my ($line, $m); + + my $check_text = sub($) { + my ($text) = @_; + + if ($text =~ m"(\$(Author|Date|Header|Id|Locker|Log|Name|RCSfile|Revision|Source|State|$opt_rcsidstring)(?::[^\$]*)?\$)") { + my ($tag) = ($2); + + if ($text =~ re_patch_uh) { + $line->log_warning("Found RCS tag \"\$${tag}\$\". Please remove it."); + $line->set_text($1); + } else { + $line->log_warning("Found RCS tag \"\$${tag}\$\". Please remove it by reducing the number of context lines using pkgdiff or \"diff -U[210]\"."); + } + } + }; + + my $check_contents = sub() { + + if ($m->has(1)) { + $check_text->($m->text(1)); + } + }; + + my $check_added_contents = sub() { + my $text; + + return unless $m->has(1); + $text = $m->text(1); + checkline_cpp_macro_names($line, $text); + + # XXX: This check is not as accurate as the similar one in + # checkline_mk_shelltext(). + if (defined($current_fname)) { + if ($current_ftype eq "shell" || $current_ftype eq "make") { + my ($mm, $rest) = match_all($text, $regex_shellword); + + foreach my $m (@{$mm}) { + my $shellword = $m->text(1); + + if ($shellword =~ m"^#") { + last; + } + checkline_mk_absolute_pathname($line, $shellword); + } + + } elsif ($current_ftype eq "source") { + checkline_source_absolute_pathname($line, $text); + + } elsif ($current_ftype eq "configure") { + if ($text =~ m": Avoid regenerating within pkgsrc$") { + $line->log_error("This code must not be included in patches."); + $line->explain_error( +"It is generated automatically by pkgsrc after the patch phase.", +"", +"For more details, look for \"configure-scripts-override\" in", +"mk/configure/gnu-configure.mk."); + } + + } elsif ($current_ftype eq "ignore") { + # Ignore it. + + } else { + checkline_other_absolute_pathname($line, $text); + } + } + }; + + my $check_hunk_end = sub($$$) { + my ($deldelta, $adddelta, $newstate) = @_; + + if ($deldelta > 0 && $dellines == 0) { + $redostate = $newstate; + if (defined($addlines) && $addlines > 0) { + $line->log_error("Expected ${addlines} more lines to be added."); + } + } elsif ($adddelta > 0 && $addlines == 0) { + $redostate = $newstate; + if (defined($dellines) && $dellines > 0) { + $line->log_error("Expected ${dellines} more lines to be deleted."); + } + } else { + if (defined($context_scanning_leading)) { + if ($deldelta != 0 && $adddelta != 0) { + if ($context_scanning_leading) { + $leading_context_lines++; + } else { + $trailing_context_lines++; + } + } else { + if ($context_scanning_leading) { + $context_scanning_leading = false; + } else { + $trailing_context_lines = 0; + } + } + } + + if ($deldelta != 0) { + $dellines -= $deldelta; + } + if ($adddelta != 0) { + $addlines -= $adddelta; + } + if (!((defined($dellines) && $dellines > 0) || + (defined($addlines) && $addlines > 0))) { + if (defined($context_scanning_leading)) { + if ($leading_context_lines != $trailing_context_lines) { + $opt_debug_patches and $line->log_warning("The hunk that ends here does not have as many leading (${leading_context_lines}) as trailing (${trailing_context_lines}) lines of context."); + } + } + $nextstate = $newstate; + } + } + }; + + # @param deldelta + # The number of lines that are deleted from the patched file. + # @param adddelta + # The number of lines that are added to the patched file. + # @param newstate + # The follow-up state when this line is the last line to be + # added in this hunk of the patch. + # + my $check_hunk_line = sub($$$) { + my ($deldelta, $adddelta, $newstate) = @_; + + $check_contents->(); + $check_hunk_end->($deldelta, $adddelta, $newstate); + + # If -Wextra is given, the context lines are checked for + # absolute paths and similar things. If it is not given, + # only those lines that really add something to the patched + # file are checked. + if ($adddelta != 0 && ($deldelta == 0 || $opt_warn_extra)) { + $check_added_contents->(); + } + }; + + # [ regex, to state, action ] + my $transitions = { + PST_START() => + [ [re_patch_rcsid, PST_CENTER, sub() { + checkline_rcsid($line, ""); + }], [undef, PST_CENTER, sub() { + checkline_rcsid($line, ""); + }]], + PST_CENTER() => + [ [re_patch_empty, PST_TEXT, sub() { + # + }], [re_patch_cfd, PST_CFA, sub() { + if ($seen_comment) { + $opt_warn_space and $line->log_note("Empty line expected."); + } else { + $line->log_error("Comment expected."); + $line->explain_error(@comment_explanation); + } + $line->log_warning("Please use unified diffs (diff -u) for patches."); + }], [re_patch_ufd, PST_UFA, sub() { + if ($seen_comment) { + $opt_warn_space and $line->log_note("Empty line expected."); + } else { + $line->log_error("Comment expected."); + $line->explain_error(@comment_explanation); + } + }], [undef, PST_TEXT, sub() { + $opt_warn_space and $line->log_note("Empty line expected."); + }]], + PST_TEXT() => + [ [re_patch_cfd, PST_CFA, sub() { + if (!$seen_comment) { + $line->log_error("Comment expected."); + $line->explain_error(@comment_explanation); + } + $line->log_warning("Please use unified diffs (diff -u) for patches."); + }], [re_patch_ufd, PST_UFA, sub() { + if (!$seen_comment) { + $line->log_error("Comment expected."); + $line->explain_error(@comment_explanation); + } + }], [re_patch_text, PST_TEXT, sub() { + $seen_comment = true; + }], [re_patch_empty, PST_TEXT, sub() { + # + }], [undef, PST_TEXT, sub() { + # + }]], + PST_CFA() => + [ [re_patch_cfa, PST_CH, sub() { + $current_fname = $m->text(1); + $current_ftype = get_filetype($line, $current_fname); + $opt_debug_patches and $line->log_debug("fname=$current_fname ftype=$current_ftype"); + $patched_files++; + $hunks = 0; + }]], + PST_CH() => + [ [re_patch_ch, PST_CHD, sub() { + $hunks++; + }]], + PST_CHD() => + [ [re_patch_chd, PST_CLD0, sub() { + $dellines = ($m->has(2)) + ? (1 + $m->text(2) - $m->text(1)) + : ($m->text(1)); + }]], + PST_CLD0() => + [ [re_patch_clc, PST_CLD, sub() { + $check_hunk_line->(1, 0, PST_CLD0); + }], [re_patch_cld, PST_CLD, sub() { + $check_hunk_line->(1, 0, PST_CLD0); + }], [re_patch_clm, PST_CLD, sub() { + $check_hunk_line->(1, 0, PST_CLD0); + }], [re_patch_cha, PST_CLA0, sub() { + $dellines = undef; + $addlines = ($m->has(2)) + ? (1 + $m->text(2) - $m->text(1)) + : ($m->text(1)); + }]], + PST_CLD() => + [ [re_patch_clc, PST_CLD, sub() { + $check_hunk_line->(1, 0, PST_CLD0); + }], [re_patch_cld, PST_CLD, sub() { + $check_hunk_line->(1, 0, PST_CLD0); + }], [re_patch_clm, PST_CLD, sub() { + $check_hunk_line->(1, 0, PST_CLD0); + }], [undef, PST_CLD0, sub() { + if ($dellines != 0) { + $line->log_warning("Invalid number of deleted lines (${dellines} missing)."); + } + }]], + PST_CLA0() => + [ [re_patch_clc, PST_CLA, sub() { + $check_hunk_line->(0, 1, PST_CH); + }], [re_patch_clm, PST_CLA, sub() { + $check_hunk_line->(0, 1, PST_CH); + }], [re_patch_cla, PST_CLA, sub() { + $check_hunk_line->(0, 1, PST_CH); + }], [undef, PST_CH, sub() { + # + }]], + PST_CLA() => + [ [re_patch_clc, PST_CLA, sub() { + $check_hunk_line->(0, 1, PST_CH); + }], [re_patch_clm, PST_CLA, sub() { + $check_hunk_line->(0, 1, PST_CH); + }], [re_patch_cla, PST_CLA, sub() { + $check_hunk_line->(0, 1, PST_CH); + }], [undef, PST_CLA0, sub() { + if ($addlines != 0) { + $line->log_warning("Invalid number of added lines (${addlines} missing)."); + } + }]], + PST_CH() => + [ [undef, PST_TEXT, sub() { + # + }]], + PST_UFA() => + [ [re_patch_ufa, PST_UH, sub() { + $current_fname = $m->text(1); + $current_ftype = get_filetype($line, $current_fname); + $opt_debug_patches and $line->log_debug("fname=$current_fname ftype=$current_ftype"); + $patched_files++; + $hunks = 0; + }]], + PST_UH() => + [ [re_patch_uh, PST_UL, sub() { + $dellines = ($m->has(1) ? $m->text(2) : 1); + $addlines = ($m->has(3) ? $m->text(4) : 1); + $check_text->($line->text); + if ($line->text =~ m"\r$") { + $line->log_error("The hunk header must not end with a CR character."); + $line->explain_error( +"The MacOS X patch utility cannot handle these."); + } + $hunks++; + $context_scanning_leading = (($m->has(1) && $m->text(1) ne "1") ? true : undef); + $leading_context_lines = 0; + $trailing_context_lines = 0; + }], [undef, PST_TEXT, sub() { + ($hunks != 0) || $line->log_warning("No hunks for file ${current_fname}."); + }]], + PST_UL() => + [ [re_patch_uld, PST_UL, sub() { + $check_hunk_line->(1, 0, PST_UH); + }], [re_patch_ula, PST_UL, sub() { + $check_hunk_line->(0, 1, PST_UH); + }], [re_patch_ulc, PST_UL, sub() { + $check_hunk_line->(1, 1, PST_UH); + }], [re_patch_ulnonl, PST_UL, sub() { + # + }], [re_patch_empty, PST_UL, sub() { + $opt_warn_space and $line->log_note("Leading white-space missing in hunk."); + $check_hunk_line->(1, 1, PST_UH); + }], [undef, PST_UH, sub() { + if ($dellines != 0 || $addlines != 0) { + $line->log_warning("Unexpected end of hunk (-${dellines},+${addlines} expected)."); + } + }]]}; + + $opt_debug_trace and log_debug($fname, NO_LINES, "checkfile_patch()"); + + checkperms($fname); + if (!($lines = load_lines($fname, false))) { + log_error($fname, NO_LINE_NUMBER, "Could not be read."); + return; + } + if (@{$lines} == 0) { + log_error($fname, NO_LINE_NUMBER, "Must not be empty."); + return; + } + + $state = PST_START; + $dellines = undef; + $addlines = undef; + $patched_files = 0; + $seen_comment = false; + $current_fname = undef; + $current_ftype = undef; + $hunks = undef; + + for (my $lineno = 0; $lineno <= $#{$lines}; ) { + $line = $lines->[$lineno]; + my $text = $line->text; + + $opt_debug_patches and $line->log_debug("[${state} ${patched_files}/".($hunks||0)."/-".($dellines||0)."+".($addlines||0)."] $text"); + + my $found = false; + foreach my $t (@{$transitions->{$state}}) { + if (!defined($t->[0])) { + $m = undef; + } elsif ($text =~ $t->[0]) { + $opt_debug_patches and $line->log_debug($t->[0]); + $m = PkgLint::SimpleMatch->new($text, \@-, \@+); + } else { + next; + } + $redostate = undef; + $nextstate = $t->[1]; + $t->[2]->(); + if (defined($redostate)) { + $state = $redostate; + } else { + $state = $nextstate; + if (defined($t->[0])) { + $lineno++; + } + } + $found = true; + last; + } + + if (!$found) { + $line->log_error("Parse error: state=${state}"); + $state = PST_TEXT; + $lineno++; + } + } + + while ($state != PST_TEXT) { + $opt_debug_patches and log_debug($fname, "EOF", "[${state} ${patched_files}/".($hunks||0)."/-".($dellines||0)."+".($addlines||0)."]"); + + my $found = false; + foreach my $t (@{$transitions->{$state}}) { + if (!defined($t->[0])) { + my $newstate; + + $m = undef; + $redostate = undef; + $nextstate = $t->[1]; + $t->[2]->(); + $newstate = (defined($redostate)) ? $redostate : $nextstate; + if ($newstate == $state) { + log_fatal($fname, "EOF", "Internal error in the patch transition table."); + } + $state = $newstate; + $found = true; + last; + } + } + + if (!$found) { + log_error($fname, "EOF", "Parse error: state=${state}"); + $state = PST_TEXT; + } + } + + if ($patched_files > 1) { + log_warning($fname, NO_LINE_NUMBER, "Contains patches for $patched_files files, should be only one."); + + } elsif ($patched_files == 0) { + log_error($fname, NO_LINE_NUMBER, "Contains no patch."); + } + + checklines_trailing_empty_lines($lines); +} + diff --git a/pkgtools/pkglint4/files/PkgLint/Shell.pm b/pkgtools/pkglint4/files/PkgLint/Shell.pm new file mode 100644 index 00000000000..c9251b9ba04 --- /dev/null +++ b/pkgtools/pkglint4/files/PkgLint/Shell.pm @@ -0,0 +1,708 @@ +# $NetBSD: Shell.pm,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# +# Parsing and checking shell commands embedded in Makefiles +# + +sub checkline_mk_shellword($$$) { + my ($line, $shellword, $check_quoting) = @_; + my ($rest, $state, $value); + + $opt_debug_trace and $line->log_debug("checkline_mk_shellword(\"${shellword}\", ${check_quoting})."); + use constant shellcommand_context_type => PkgLint::Type->new( + LK_NONE, "ShellCommand", [[ qr".*", "adsu" ]], NOT_GUESSED + ); + use constant shellword_vuc => PkgLint::VarUseContext->new( + VUC_TIME_UNKNOWN, + shellcommand_context_type, + VUC_SHELLWORD_PLAIN, + VUC_EXTENT_WORD + ); + + if ($shellword =~ m"^\$\{(${regex_varname})(:[^{}]+)?\}$") { + my ($varname, $mod) = ($1, $2); + + checkline_mk_varuse($line, $varname, defined($mod) ? $mod : "", shellword_vuc); + return; + } + + if ($shellword =~ m"\$\{PREFIX\}/man(?:$|/)") { + $line->log_warning("Please use \${PKGMANDIR} instead of \"man\"."); + } + + if ($shellword =~ m"etc/rc\.d") { + $line->log_warning("Please use the RCD_SCRIPTS mechanism to install rc.d scripts automatically to \${RCD_SCRIPTS_EXAMPLEDIR}."); + } + + # Note: SWST means [S]hell[W]ord [ST]ate + use enum qw(:SWST_ PLAIN SQUOT DQUOT DQUOT_BACKT BACKT); + use constant statename => [ + "SWST_PLAIN", "SWST_SQUOT", "SWST_DQUOT", + "SWST_DQUOT_BACKT", "SWST_BACKT", + ]; + use constant user_statename => [ + "unquoted string", "single quoted string", + "double quoted string", "backticks inside double quoted string", + "backticks", + ]; + + $rest = ($shellword =~ m"^#") ? "" : $shellword; + $state = SWST_PLAIN; + while ($rest ne "") { + + $opt_debug_shell and $line->log_debug(statename->[$state] . ": ${rest}"); + + # When we are parsing inside backticks, it is more + # reasonable to check the whole shell command + # recursively, instead of splitting off the first + # make(1) variable (see the elsif below). + if ($state == SWST_BACKT || $state == SWST_DQUOT_BACKT) { + + # Scan for the end of the backticks, checking + # for single backslashes and removing one level + # of backslashes. Backslashes are only removed + # before a dollar, a backslash or a backtick. + # + # References: + # * http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_03 + my $stripped = ""; + while ($rest ne "") { + if ($rest =~ s/^\`//) { #` + $state = ($state == SWST_BACKT) ? SWST_PLAIN : SWST_DQUOT; + goto end_of_backquotes; + } elsif ($rest =~ s/^\\([\\\`\$])//) { #` + $stripped .= $1; + } elsif ($rest =~ s/^(\\)//) { + $line->log_warning("Backslashes should be doubled inside backticks."); + $stripped .= $1; + } elsif ($state == SWST_DQUOT_BACKT && $rest =~ s/^"//) { #" + $line->log_warning("Double quotes inside backticks inside double quotes are error prone."); + $line->explain_warning( +"According to the SUSv3, they produce undefined results.", +"", +"See the paragraph starting \"Within the backquoted ...\" in", +"http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html"); + } elsif ($rest =~ s/^([^\\\`]+)//) { #` + $stripped .= $1; + } else { + assert(false, "rest=$rest"); + } + } + $line->log_error("Unfinished backquotes: rest=$rest"); + + end_of_backquotes: + # Check the resulting command. + checkline_mk_shelltext($line, $stripped); + + # make(1) variables have the same syntax, no matter in + # which state we are currently. + } elsif ($rest =~ s/^\$\{(${regex_varname}|[\@])(:[^\{]+)?\}// + || $rest =~ s/^\$\((${regex_varname}|[\@])(:[^\)]+)?\)// + || $rest =~ s/^\$([\w\@])//) { + my ($varname, $mod) = ($1, $2); + + if ($varname eq "\@") { + $line->log_warning("Please use \"\${.TARGET}\" instead of \"\$\@\"."); + $line->explain_warning( +"The variable \$\@ can easily be confused with the shell variable of the", +"same name, which has a completely different meaning."); + $varname = ".TARGET"; + } + + if ($state == SWST_PLAIN && defined($mod) && $mod =~ m":Q$") { + # Fine. + + } elsif ($state == SWST_BACKT) { + # Don't check here, to avoid false positives + # for tool names. + + } elsif (($state == SWST_SQUOT || $state == SWST_DQUOT) && $varname =~ m"^(?:.*DIR|.*FILE|.*PATH|.*_VAR|PREFIX|.*BASE|PKGNAME)$") { + # This is ok if we don't allow these + # variables to have embedded [\$\\\"\'\`]. + + } elsif ($state == SWST_DQUOT && defined($mod) && $mod =~ m":Q$") { + $line->log_warning("Please don't use the :Q operator in double quotes."); + $line->explain_warning( +"Either remove the :Q or the double quotes. In most cases, it is more", +"appropriate to remove the double quotes."); + + } + + my $ctx = PkgLint::VarUseContext->new_from_pool( + VUC_TIME_UNKNOWN, + shellcommand_context_type, + ($state == SWST_PLAIN) ? VUC_SHELLWORD_PLAIN + : ($state == SWST_DQUOT) ? VUC_SHELLWORD_DQUOT + : ($state == SWST_SQUOT) ? VUC_SHELLWORD_SQUOT + : ($state == SWST_BACKT) ? VUC_SHELLWORD_BACKT + : VUC_SHELLWORD_UNKNOWN, + VUC_EXTENT_WORD_PART + ); + if ($varname ne "\@") { + checkline_mk_varuse($line, $varname, defined($mod) ? $mod : "", $ctx); + } + + # The syntax of the variable modifiers can get quite + # hairy. In lack of motivation, we just skip anything + # complicated, hoping that at least the braces are + # balanced. + } elsif ($rest =~ s/^\$\{//) { + my $braces = 1; + while ($rest ne "" && $braces > 0) { + if ($rest =~ s/^\}//) { + $braces--; + } elsif ($rest =~ s/^\{//) { + $braces++; + } elsif ($rest =~ s/^[^{}]+//) { + # Nothing to do here. + } else { + last; + } + } + + } elsif ($state == SWST_PLAIN) { + + # XXX: This is only true for the "first" word in a + # shell command, not for every word. For example, + # FOO_ENV+= VAR=`command` may be underquoted. + if (false && $rest =~ m"([\w_]+)=\"\`") { + $line->log_note("In the assignment to \"$1\", you don't need double quotes around backticks."); + $line->explain_note( +"Assignments are a special context, where no double quotes are needed", +"around backticks. In other contexts, the double quotes are necessary."); + } + + if ($rest =~ s/^[!#\%&\(\)*+,\-.\/0-9:;<=>?\@A-Z\[\]^_a-z{|}~]+//) { + } elsif ($rest =~ s/^\'//) { + $state = SWST_SQUOT; + } elsif ($rest =~ s/^\"//) { + $state = SWST_DQUOT; + } elsif ($rest =~ s/^\`//) { #` + $state = SWST_BACKT; + } elsif ($rest =~ s/^\\(?:[ !"#'\(\)*;?\\^{|}]|\$\$)//) { + } elsif ($rest =~ s/^\$\$([0-9A-Z_a-z]+|\#)// + || $rest =~ s/^\$\$\{([0-9A-Z_a-z]+|\#)\}// + || $rest =~ s/^\$\$(\$)\$//) { + my ($shvarname) = ($1); + if ($opt_warn_quoting && $check_quoting) { + $line->log_warning("Unquoted shell variable \"${shvarname}\"."); + $line->explain_warning( +"When a shell variable contains white-space, it is expanded (split into", +"multiple words) when it is written as \$variable in a shell script.", +"If that is not intended, you should add quotation marks around it,", +"like \"\$variable\". Then, the variable will always expand to a single", +"word, preserving all white-space and other special characters.", +"", +"Example:", +"\tfname=\"Curriculum vitae.doc\"", +"\tcp \$fname /tmp", +"\t# tries to copy the two files \"Curriculum\" and \"Vitae.doc\"", +"\tcp \"\$fname\" /tmp", +"\t# copies one file, as intended"); + } + + } elsif ($rest =~ s/^\$\@//) { + $line->log_warning("Please use \"\${.TARGET}\" instead of \"\$@\"."); + $line->explain_warning( +"It is more readable and prevents confusion with the shell variable of", +"the same name."); + + } elsif ($rest =~ s/^\$\$\@//) { + $line->log_warning("The \$@ shell variable should only be used in double quotes."); + + } elsif ($rest =~ s/^\$\$\?//) { + $line->log_warning("The \$? shell variable is often not available in \"set -e\" mode."); + + } elsif ($rest =~ s/^\$\$\(/(/) { + $line->log_warning("Invoking subshells via \$(...) is not portable enough."); + $line->explain_warning( +"The Solaris /bin/sh does not know this way to execute a command in a", +"subshell. Please use backticks (\`...\`) as a replacement."); + + } else { + last; + } + + } elsif ($state == SWST_SQUOT) { + if ($rest =~ s/^\'//) { + $state = SWST_PLAIN; + } elsif ($rest =~ s/^[^\$\']+//) { #' + } elsif ($rest =~ s/^\$\$//) { + } else { + last; + } + + } elsif ($state == SWST_DQUOT) { + if ($rest =~ s/^\"//) { + $state = SWST_PLAIN; + } elsif ($rest =~ s/^\`//) { #` + $state = SWST_DQUOT_BACKT; + } elsif ($rest =~ s/^[^\$"\\\`]+//) { #` + } elsif ($rest =~ s/^\\(?:[\\\"\`]|\$\$)//) { #` + } elsif ($rest =~ s/^\$\$\{([0-9A-Za-z_]+)\}// + || $rest =~ s/^\$\$([0-9A-Z_a-z]+|[!#?\@]|\$\$)//) { + my ($shvarname) = ($1); + $opt_debug_shell and $line->log_debug("[checkline_mk_shellword] Found double-quoted variable ${shvarname}."); + } elsif ($rest =~ s/^\$\$//) { + $line->log_warning("Unquoted \$ or strange shell variable found."); + } elsif ($rest =~ s/^\\(.)//) { + my ($char) = ($1); + $line->log_warning("Please use \"\\\\${char}\" instead of \"\\${char}\"."); + $line->explain_warning( +"Although the current code may work, it is not good style to rely on", +"the shell passing \"\\${char}\" exactly as is, and not discarding the", +"backslash. Alternatively you can use single quotes instead of double", +"quotes."); + } else { + last; + } + + } else { + last; + } + } + if ($rest !~ m"^\s*$") { + $line->log_error("Internal pkglint error: " . statename->[$state] . ": rest=${rest}"); + } +} + +# Some shell commands should not be used in the install phase. +# +sub checkline_mk_shellcmd_use($$) { + my ($line, $shellcmd) = @_; + + use constant allowed_install_commands => array_to_hash(qw( + ${INSTALL} + ${INSTALL_DATA} ${INSTALL_DATA_DIR} + ${INSTALL_LIB} ${INSTALL_LIB_DIR} + ${INSTALL_MAN} ${INSTALL_MAN_DIR} + ${INSTALL_PROGRAM} ${INSTALL_PROGRAM_DIR} + ${INSTALL_SCRIPT} + ${LIBTOOL} + ${LN} + ${PAX} + )); + use constant discouraged_install_commands => array_to_hash(qw( + sed ${SED} + tr ${TR} + )); + + if (defined($mkctx_target) && $mkctx_target =~ m"^(?:pre|do|post)-install") { + + if (exists(allowed_install_commands->{$shellcmd})) { + # Fine. + + } elsif (exists(discouraged_install_commands->{$shellcmd})) { + $line->log_warning("The shell command \"${shellcmd}\" should not be used in the install phase."); + $line->explain_warning( +"In the install phase, the only thing that should be done is to install", +"the prepared files to their final location. The file's contents should", +"not be changed anymore."); + + } elsif ($shellcmd eq "\${CP}") { + $line->log_warning("\${CP} should not be used to install files."); + $line->explain_warning( +"The \${CP} command is highly platform dependent and cannot overwrite", +"files that don't have write permission. Please use \${PAX} instead.", +"", +"For example, instead of", +"\t\${CP} -R \${WRKSRC}/* \${PREFIX}/foodir", +"you should use", +"\tcd \${WRKSRC} && \${PAX} -wr * \${PREFIX}/foodir"); + + } else { + $opt_debug_misc and $line->log_debug("May \"${shellcmd}\" be used in the install phase?"); + } + } +} + +sub checkline_mk_shelltext($$) { + my ($line, $text) = @_; + my ($vartools, $state, $rest, $set_e_mode); + + $opt_debug_trace and $line->log_debug("checkline_mk_shelltext(\"$text\")"); + + # Note: SCST is the abbreviation for [S]hell [C]ommand [ST]ate. + use constant scst => qw( + START CONT + INSTALL INSTALL_D + MKDIR + PAX PAX_S + SED SED_E + SET SET_CONT + COND COND_CONT + CASE CASE_IN CASE_LABEL CASE_LABEL_CONT CASE_PAREN + FOR FOR_IN FOR_CONT + ECHO + INSTALL_DIR INSTALL_DIR2 + ); + use enum (":SCST_", scst); + use constant scst_statename => [ map { "SCST_$_" } scst ]; + + use constant forbidden_commands => array_to_hash(qw( + ktrace + mktexlsr + strace + texconfig truss + )); + + if ($text =~ m"\$\{SED\}" && $text =~ m"\$\{MV\}") { + $line->log_note("Please use the SUBST framework instead of \${SED} and \${MV}."); + $line->explain_note( +"When converting things, pay attention to \"#\" characters. In shell", +"commands make(1) does not interpret them as comment character, but", +"in other lines it does. Therefore, instead of the shell command", +"", +"\tsed -e 's,#define foo,,'", +"", +"you need to write", +"", +"\tSUBST_SED.foo+=\t's,\\#define foo,,'"); + } + + if ($text =~ m"^\@*-(.*(MKDIR|INSTALL.*-d|INSTALL_.*_DIR).*)") { + my ($mkdir_cmd) = ($1); + + $line->log_note("You don't need to use \"-\" before ${mkdir_cmd}."); + } + + $vartools = get_vartool_names(); + $rest = $text; + + use constant hidden_shell_commands => array_to_hash(qw( + ${DELAYED_ERROR_MSG} ${DELAYED_WARNING_MSG} + ${DO_NADA} + ${ECHO} ${ECHO_MSG} ${ECHO_N} ${ERROR_CAT} ${ERROR_MSG} + ${FAIL_MSG} + ${PHASE_MSG} ${PRINTF} + ${SHCOMMENT} ${STEP_MSG} + ${WARNING_CAT} ${WARNING_MSG} + )); + + $set_e_mode = false; + + if ($rest =~ s/^\s*([-@]*)(\$\{_PKG_SILENT\}\$\{_PKG_DEBUG\}|\$\{RUN\}|)//) { + my ($hidden, $macro) = ($1, $2); + + if ($hidden !~ m"\@") { + # Nothing is hidden at all. + + } elsif (defined($mkctx_target) && $mkctx_target =~ m"^(?:show-.*|.*-message)$") { + # In some targets commands may be hidden. + + } elsif ($rest =~ m"^#") { + # Shell comments may be hidden, as they have no side effects + + } elsif ($rest =~ $regex_shellword) { + my ($cmd) = ($1); + + if (!exists(hidden_shell_commands->{$cmd})) { + $line->log_warning("The shell command \"${cmd}\" should not be hidden."); + $line->explain_warning( +"Hidden shell commands do not appear on the terminal or in the log file", +"when they are executed. When they fail, the error message cannot be", +"assigned to the command, which is very difficult to debug."); + } + } + + if ($hidden =~ m"-") { + $line->log_warning("The use of a leading \"-\" to suppress errors is deprecated."); + $line->explain_warning( +"If you really want to ignore any errors from this command (including", +"all errors you never thought of), append \"|| \${TRUE}\" to the", +"command."); + } + + if ($macro eq "\${RUN}") { + $set_e_mode = true; + } + } + + $state = SCST_START; + while ($rest =~ s/^$regex_shellword//) { + my ($shellword) = ($1); + + $opt_debug_shell and $line->log_debug(scst_statename->[$state] . ": ${shellword}"); + + checkline_mk_shellword($line, $shellword, !( + $state == SCST_CASE + || $state == SCST_FOR_CONT + || $state == SCST_SET_CONT + || ($state == SCST_START && $shellword =~ regex_sh_varassign))); + + # + # Actions associated with the current state + # and the symbol on the "input tape". + # + + if ($state == SCST_START || $state == SCST_COND) { + my ($type); + + if ($shellword eq "\${RUN}") { + # Just skip this one. + + } elsif (exists(forbidden_commands->{basename($shellword)})) { + $line->log_error("${shellword} must not be used in Makefiles."); + $line->explain_error( +"This command must appear in INSTALL scripts, not in the package", +"Makefile, so that the package also works if it is installed as a binary", +"package via pkg_add."); + + } elsif (exists(get_tool_names()->{$shellword})) { + if (!exists($mkctx_tools->{$shellword}) && !exists($mkctx_tools->{"g$shellword"})) { + $line->log_warning("The \"${shellword}\" tool is used but not added to USE_TOOLS."); + } + + if (exists(get_required_vartools()->{$shellword})) { + $line->log_warning("Please use \"\${" . get_vartool_names()->{$shellword} . "}\" instead of \"${shellword}\"."); + } + + checkline_mk_shellcmd_use($line, $shellword); + + } elsif ($shellword =~ m"^\$\{([\w_]+)\}$" && exists(get_varname_to_toolname()->{$1})) { + my ($vartool) = ($1); + my $plain_tool = get_varname_to_toolname()->{$vartool}; + + if (!exists($mkctx_tools->{$plain_tool})) { + $line->log_warning("The \"${plain_tool}\" tool is used but not added to USE_TOOLS."); + } + + # Deactivated to allow package developers to write + # consistent code that uses ${TOOL} in all places. + if (false && defined($mkctx_target) && $mkctx_target =~ m"^(?:pre|do|post)-(?:extract|patch|wrapper|configure|build|install|package|clean)$") { + if (!exists(get_required_vartool_varnames()->{$vartool})) { + $opt_warn_extra and $line->log_note("You can write \"${plain_tool}\" instead of \"${shellword}\"."); + $opt_warn_extra and $line->explain_note( +"The wrapper framework from pkgsrc takes care that a sufficiently", +"capable implementation of that tool will be selected.", +"", +"Calling the commands by their plain name instead of the macros is", +"only available in the {pre,do,post}-* targets. For all other targets,", +"you should still use the macros."); + } + } + + checkline_mk_shellcmd_use($line, $shellword); + + } elsif ($shellword =~ m"^\$\{([\w_]+)\}$" && defined($type = get_variable_type($line, $1)) && $type->basic_type eq "ShellCommand") { + checkline_mk_shellcmd_use($line, $shellword); + + } elsif ($shellword =~ m"^\$\{(\w+)\}$" && defined($pkgctx_vardef) && exists($pkgctx_vardef->{$1})) { + # When the package author has explicitly + # defined a command variable, assume it + # to be valid. + + } elsif ($shellword =~ m"^(?:\(|\)|:|;|;;|&&|\|\||\{|\}|break|case|cd|continue|do|done|elif|else|esac|eval|exec|exit|export|fi|for|if|read|set|shift|then|umask|unset|while)$") { + # Shell builtins are fine. + + } elsif ($shellword =~ m"^[\w_]+=.*$") { + # Variable assignment. + + } elsif ($shellword =~ m"^\./.*$") { + # All commands from the current directory are fine. + + } elsif ($shellword =~ m"^#") { + my $semicolon = ($shellword =~ m";"); + my $multiline = ($line->lines =~ m"--"); + + if ($semicolon) { + $line->log_warning("A shell comment should not contain semicolons."); + } + if ($multiline) { + $line->log_warning("A shell comment does not stop at the end of line."); + } + + if ($semicolon || $multiline) { + $line->explain_warning( +"When you split a shell command into multiple lines that are continued", +"with a backslash, they will nevertheless be converted to a single line", +"before the shell sees them. That means that even if it _looks_ like that", +"the comment only spans one line in the Makefile, in fact it spans until", +"the end of the whole shell command. To insert a comment into shell code,", +"you can pass it as an argument to the \${SHCOMMENT} macro, which expands", +"to a command doing nothing. Note that any special characters are", +"nevertheless interpreted by the shell."); + } + + } else { + $opt_warn_extra and $line->log_warning("Unknown shell command \"${shellword}\"."); + $opt_warn_extra and $line->explain_warning( +"If you want your package to be portable to all platforms that pkgsrc", +"supports, you should only use shell commands that are covered by the", +"tools framework."); + + } + } + + if ($state == SCST_COND && $shellword eq "cd") { + $line->log_error("The Solaris /bin/sh cannot handle \"cd\" inside conditionals."); + $line->explain_error( +"When the Solaris shell is in \"set -e\" mode and \"cd\" fails, the", +"shell will exit, no matter if it is protected by an \"if\" or the", +"\"||\" operator."); + } + + if (($state != SCST_PAX_S && $state != SCST_SED_E && $state != SCST_CASE_LABEL)) { + checkline_mk_absolute_pathname($line, $shellword); + } + + if (($state == SCST_INSTALL_D || $state == SCST_MKDIR) && $shellword =~ m"^(?:\$\{DESTDIR\})?\$\{PREFIX(?:|:Q)\}/") { + $line->log_warning("Please use AUTO_MKDIRS instead of " + . (($state == SCST_MKDIR) ? "\${MKDIR}" : "\${INSTALL} -d") + . "."); + $line->explain_warning( +"Setting AUTO_MKDIRS=yes automatically creates all directories that are", +"mentioned in the PLIST. If you need additional directories, specify", +"them in INSTALLATION_DIRS, which is a list of directories relative to", +"\${PREFIX}."); + } + + if (($state == SCST_INSTALL_DIR || $state == SCST_INSTALL_DIR2) && $shellword !~ regex_mk_shellvaruse && $shellword =~ m"^(?:\$\{DESTDIR\})?\$\{PREFIX(?:|:Q)\}/(.*)") { + my ($dirname) = ($1); + + $line->log_note("You can use AUTO_MKDIRS=yes or INSTALLATION_DIRS+= ${dirname} instead of this command."); + $line->explain_note( +"This saves you some typing. You also don't have to think about which of", +"the many INSTALL_*_DIR macros is appropriate, since INSTALLATION_DIRS", +"takes care of that.", +"", +"Note that you should only do this if the package creates _all_", +"directories it needs before trying to install files into them.", +"", +"Many packages include a list of all needed directories in their PLIST", +"file. In that case, you can just set AUTO_MKDIRS=yes and be done."); + } + + if ($state == SCST_INSTALL_DIR2 && $shellword =~ m"^\$") { + $line->log_warning("The INSTALL_*_DIR commands can only handle one directory at a time."); + $line->explain_warning( +"Many implementations of install(1) can handle more, but pkgsrc aims at", +"maximum portability."); + } + + if ($state == SCST_PAX && $shellword eq "-pe") { + $line->log_warning("Please use the -pp option to pax(1) instead of -pe."); + $line->explain_warning( +"The -pe option tells pax to preserve the ownership of the files, which", +"means that the installed files will belong to the user that has built", +"the package. That's a Bad Thing."); + } + + if ($state == SCST_PAX_S || $state == SCST_SED_E) { + if (false && $shellword !~ m"^[\"\'].*[\"\']$") { + $line->log_warning("Substitution commands like \"${shellword}\" should always be quoted."); + $line->explain_warning( +"Usually these substitution commands contain characters like '*' or", +"other shell metacharacters that might lead to lookup of matching", +"filenames and then expand to more than one word."); + } + } + + if ($state == SCST_ECHO && $shellword eq "-n") { + $line->log_warning("Please use \${ECHO_N} instead of \"echo -n\"."); + } + + if ($opt_warn_extra && $state != SCST_CASE_LABEL_CONT && $shellword eq "|") { + $line->log_warning("The exitcode of the left-hand-side command of the pipe operator is ignored."); + $line->explain_warning( +"If you need to detect the failure of the left-hand-side command, use", +"temporary files to save the output of the command."); + } + + if ($opt_warn_extra && $shellword eq ";" && $state != SCST_COND_CONT && $state != SCST_FOR_CONT && !$set_e_mode) { + $line->log_warning("Please switch to \"set -e\" mode before using a semicolon to separate commands."); + $line->explain_warning( +"Older versions of the NetBSD make(1) had run the shell commands using", +"the \"-e\" option of /bin/sh. In 2004, this behavior has been changed to", +"follow the POSIX conventions, which is to not use the \"-e\" option.", +"The consequence of this change is that shell programs don't terminate", +"as soon as an error occurs, but try to continue with the next command.", +"Imagine what would happen for these commands:", +" cd \"\$HOME\"; cd /nonexistent; rm -rf *", +"To fix this warning, either insert \"set -e\" at the beginning of this", +"line or use the \"&&\" operator instead of the semicolon."); + } + + # + # State transition. + # + + if ($state == SCST_SET && $shellword =~ m"^-.*e") { + $set_e_mode = true; + } + if ($state == SCST_START && $shellword eq "\${RUN}") { + $set_e_mode = true; + } + + $state = ($shellword eq ";;") ? SCST_CASE_LABEL + # Note: The order of the following two lines is important. + : ($state == SCST_CASE_LABEL_CONT && $shellword eq "|") ? SCST_CASE_LABEL + : ($shellword =~ m"^[;&\|]+$") ? SCST_START + : ($state == SCST_START) ? ( + ($shellword eq "\${INSTALL}") ? SCST_INSTALL + : ($shellword eq "\${MKDIR}") ? SCST_MKDIR + : ($shellword eq "\${PAX}") ? SCST_PAX + : ($shellword eq "\${SED}") ? SCST_SED + : ($shellword eq "\${ECHO}") ? SCST_ECHO + : ($shellword eq "\${RUN}") ? SCST_START + : ($shellword eq "echo") ? SCST_ECHO + : ($shellword eq "set") ? SCST_SET + : ($shellword =~ m"^(?:if|elif|while)$") ? SCST_COND + : ($shellword =~ m"^(?:then|else|do)$") ? SCST_START + : ($shellword eq "case") ? SCST_CASE + : ($shellword eq "for") ? SCST_FOR + : ($shellword eq "(") ? SCST_START + : ($shellword =~ m"^\$\{INSTALL_[A-Z]+_DIR\}$") ? SCST_INSTALL_DIR + : ($shellword =~ regex_sh_varassign) ? SCST_START + : SCST_CONT) + : ($state == SCST_MKDIR) ? SCST_MKDIR + : ($state == SCST_INSTALL && $shellword eq "-d") ? SCST_INSTALL_D + : ($state == SCST_INSTALL || $state == SCST_INSTALL_D) ? ( + ($shellword =~ m"^-[ogm]$") ? SCST_CONT + : $state) + : ($state == SCST_INSTALL_DIR) ? ( + ($shellword =~ m"^-") ? SCST_CONT + : ($shellword =~ m"^\$") ? SCST_INSTALL_DIR2 + : $state) + : ($state == SCST_INSTALL_DIR2) ? $state + : ($state == SCST_PAX) ? ( + ($shellword eq "-s") ? SCST_PAX_S + : ($shellword =~ m"^-") ? SCST_PAX + : SCST_CONT) + : ($state == SCST_PAX_S) ? SCST_PAX + : ($state == SCST_SED) ? ( + ($shellword eq "-e") ? SCST_SED_E + : ($shellword =~ m"^-") ? SCST_SED + : SCST_CONT) + : ($state == SCST_SED_E) ? SCST_SED + : ($state == SCST_SET) ? SCST_SET_CONT + : ($state == SCST_SET_CONT) ? SCST_SET_CONT + : ($state == SCST_CASE) ? SCST_CASE_IN + : ($state == SCST_CASE_IN && $shellword eq "in") ? SCST_CASE_LABEL + : ($state == SCST_CASE_LABEL && $shellword eq "esac") ? SCST_CONT + : ($state == SCST_CASE_LABEL) ? SCST_CASE_LABEL_CONT + : ($state == SCST_CASE_LABEL_CONT && $shellword eq ")") ? SCST_START + : ($state == SCST_CONT) ? SCST_CONT + : ($state == SCST_COND) ? SCST_COND_CONT + : ($state == SCST_COND_CONT) ? SCST_COND_CONT + : ($state == SCST_FOR) ? SCST_FOR_IN + : ($state == SCST_FOR_IN && $shellword eq "in") ? SCST_FOR_CONT + : ($state == SCST_FOR_CONT) ? SCST_FOR_CONT + : ($state == SCST_ECHO) ? SCST_CONT + : do { + $line->log_warning("[" . scst_statename->[$state] . " ${shellword}] Keeping the current state."); + $state; + }; + } + + if ($rest !~ m"^\s*$") { + $line->log_error("Internal pkglint error: " . scst_statename->[$state] . ": rest=${rest}"); + } +} + +sub checkline_mk_shellcmd($$) { + my ($line, $shellcmd) = @_; + + checkline_mk_text($line, $shellcmd); + checkline_mk_shelltext($line, $shellcmd); +} + diff --git a/pkgtools/pkglint4/files/PkgLint/SimpleMatch.pm b/pkgtools/pkglint4/files/PkgLint/SimpleMatch.pm new file mode 100644 index 00000000000..a4934372758 --- /dev/null +++ b/pkgtools/pkglint4/files/PkgLint/SimpleMatch.pm @@ -0,0 +1,44 @@ +# $NetBSD: SimpleMatch.pm,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# +# A SimpleMatch is the result of applying a regular expression to a Perl +# scalar value. It can return the range and the text of the captured +# groups. +# +package PkgLint::SimpleMatch; + +use strict; +use warnings; + +use enum qw(STRING STARTS ENDS N); + +sub new($$) { + my ($class, $string, $starts, $ends) = @_; + my ($self) = ([$string, [@{$starts}], [@{$ends}], $#{$ends}]); + bless($self, $class); + return $self; +} + +sub string($) { return shift()->[STRING]; } +sub n($) { return shift()->[N]; } + +sub has($$) { + my ($self, $n) = @_; + + return 0 <= $n && $n <= $self->n + && defined($self->[STARTS]->[$n]) + && defined($self->[ENDS]->[$n]); +} + +sub text($$) { + my ($self, $n) = @_; + + my $start = $self->[STARTS]->[$n]; + my $end = $self->[ENDS]->[$n]; + return substr($self->string, $start, $end - $start); +} + +sub range($$) { + my ($self, $n) = @_; + + return ($self->[STARTS]->[$n], $self->[ENDS]->[$n]); +} diff --git a/pkgtools/pkglint4/files/PkgLint/SubstContext.pm b/pkgtools/pkglint4/files/PkgLint/SubstContext.pm new file mode 100644 index 00000000000..f21e57546d0 --- /dev/null +++ b/pkgtools/pkglint4/files/PkgLint/SubstContext.pm @@ -0,0 +1,198 @@ +# $NetBSD: SubstContext.pm,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# +# This class records the state of a block of variable assignments that make +# up a SUBST class. As these variable assignments are not easy to get right +# unless you do it every day, and the possibility of typos is high, pkglint +# provides additional checks for them. +# +package PkgLint::SubstContext; + +use strict; +use warnings; + +BEGIN { + import PkgLint::Util qw( + false true + ); + import PkgLint::Logging qw( + log_warning + ); +} + +use enum qw(:SUBST_ ID CLASS STAGE MESSAGE FILES SED VARS FILTER_CMD); + +sub new($) { + my ($class) = @_; + my ($self) = ([undef, undef, undef, undef, [], [], [], undef]); + bless($self, $class); + return $self; +} + +sub subst_class($) { return shift()->[SUBST_CLASS]; } +sub subst_stage($) { return shift()->[SUBST_STAGE]; } +sub subst_message($) { return shift()->[SUBST_MESSAGE]; } +sub subst_files($) { return shift()->[SUBST_FILES]; } +sub subst_sed($) { return shift()->[SUBST_SED]; } +sub subst_vars($) { return shift()->[SUBST_VARS]; } +sub subst_filter_cmd($) { return shift()->[SUBST_FILTER_CMD]; } +sub subst_id($) { return shift()->[SUBST_ID]; } + +sub init($) { + my ($self) = @_; + + $self->[SUBST_ID] = undef; + $self->[SUBST_CLASS] = undef; + $self->[SUBST_STAGE] = undef; + $self->[SUBST_MESSAGE] = undef; + $self->[SUBST_FILES] = []; + $self->[SUBST_SED] = []; + $self->[SUBST_VARS] = []; + $self->[SUBST_FILTER_CMD] = undef; +} + +sub check_end($$) { + my ($self, $line) = @_; + + return unless defined($self->subst_id); + + if (!defined($self->subst_class)) { + $main::opt_warn_extra and $line->log_warning("Incomplete SUBST block: SUBST_CLASSES missing."); + } + if (!defined($self->subst_stage)) { + $main::opt_warn_extra and $line->log_warning("Incomplete SUBST block: SUBST_STAGE missing."); + } + if (@{$self->subst_files} == 0) { + $main::opt_warn_extra and $line->log_warning("Incomplete SUBST block: SUBST_FILES missing."); + } + if (@{$self->subst_sed} == 0 && @{$self->subst_vars} == 0 && !defined($self->subst_filter_cmd)) { + $main::opt_warn_extra and $line->log_warning("Incomplete SUBST block: SUBST_SED or SUBST_VARS missing."); + } + $self->init(); +} + +sub is_complete($) { + my ($self) = @_; + + return false unless defined($self->subst_id); + return false unless defined($self->subst_class); + return false unless defined($self->subst_files); + return false if @{$self->subst_sed} == 0 && @{$self->subst_vars} == 0; + return true; +} + +sub check_varassign($$$$$) { + my ($self, $line, $varname, $op, $value) = @_; + my ($varbase, $varparam, $id); + + if ($varname eq "SUBST_CLASSES") { + + if ($value =~ m"^(\S+)\s") { + $main::opt_warn_extra and $line->log_warning("Please add only one class at a time to SUBST_CLASSES."); + $self->[SUBST_CLASS] = $1; + $self->[SUBST_ID] = $1; + + } else { + if (defined($self->subst_class)) { + $main::opt_warn_extra and $line->log_warning("SUBST_CLASSES should only appear once in a SUBST block."); + } + $self->[SUBST_CLASS] = $value; + $self->[SUBST_ID] = $value; + } + return; + } + + $id = $self->subst_id; + + if ($varname =~ m"^(SUBST_(?:STAGE|MESSAGE|FILES|SED|VARS|FILTER_CMD))\.([\-\w_]+)$") { + ($varbase, $varparam) = ($1, $2); + + if (!defined($id)) { + $main::opt_warn_extra and $line->log_note("SUBST_CLASSES should precede the definition of ${varbase}.${varparam}."); + + $id = $self->[SUBST_ID] = $varparam; + } + } else { + if (defined($id)) { + $main::opt_warn_extra and $line->log_warning("Foreign variable in SUBST block."); + } + return; + } + + if ($varparam ne $id) { + + # XXX: This code sometimes produces weird warnings. See + # meta-pkgs/xorg/Makefile.common 1.41 for an example. + if ($self->is_complete()) { + $self->check_end($line); + + # The following assignment prevents an additional warning, + # but from a technically viewpoint, it is incorrect. + $self->[SUBST_CLASS] = $varparam; + $self->[SUBST_ID] = $varparam; + $id = $varparam; + } else { + $main::opt_warn_extra and $line->log_warning("Variable parameter \"${varparam}\" does not match SUBST class \"${id}\"."); + } + } + + if ($varbase eq "SUBST_STAGE") { + if (defined($self->subst_stage)) { + $main::opt_warn_extra and $line->log_warning("Duplicate definition of SUBST_STAGE.${id}."); + } else { + $self->[SUBST_STAGE] = $value; + } + + } elsif ($varbase eq "SUBST_MESSAGE") { + if (defined($self->subst_message)) { + $main::opt_warn_extra and $line->log_warning("Duplicate definition of SUBST_MESSAGE.${id}."); + } else { + $self->[SUBST_MESSAGE] = $value; + } + + } elsif ($varbase eq "SUBST_FILES") { + if (@{$self->subst_files} > 0) { + if ($op ne "+=") { + $main::opt_warn_extra and $line->log_warning("All but the first SUBST_FILES line should use the \"+=\" operator."); + } + } + push(@{$self->subst_files}, $value); + + } elsif ($varbase eq "SUBST_SED") { + if (@{$self->subst_sed} > 0) { + if ($op ne "+=") { + $main::opt_warn_extra and $line->log_warning("All but the first SUBST_SED line should use the \"+=\" operator."); + } + } + push(@{$self->subst_sed}, $value); + + } elsif ($varbase eq "SUBST_FILTER_CMD") { + if (defined($self->subst_filter_cmd)) { + $main::opt_warn_extra and $line->log_warning("Duplicate definition of SUBST_FILTER_CMD.${id}."); + } else { + $self->[SUBST_FILTER_CMD] = $value; + } + + } elsif ($varbase eq "SUBST_VARS") { + if (@{$self->subst_vars} > 0) { + if ($op ne "+=") { + $main::opt_warn_extra and $line->log_warning("All but the first SUBST_VARS line should use the \"+=\" operator."); + } + } + push(@{$self->subst_vars}, $value); + + } else { + $main::opt_warn_extra and $line->log_warning("Foreign variable in SUBST block."); + } +} + +sub to_string($) { + my ($self) = @_; + + return sprintf("SubstContext(%s %s %s %s %s %s)", + (defined($self->subst_class) ? $self->subst_class : "(undef)"), + (defined($self->subst_stage) ? $self->subst_stage : "(undef)"), + (defined($self->subst_message) ? $self->subst_message : "(undef)"), + scalar(@{$self->subst_files}), + scalar(@{$self->subst_sed}), + (defined($self->subst_id) ? $self->subst_id : "(undef)")); +} diff --git a/pkgtools/pkglint4/files/PkgLint/Type.pm b/pkgtools/pkglint4/files/PkgLint/Type.pm new file mode 100644 index 00000000000..1eb7cd2a74c --- /dev/null +++ b/pkgtools/pkglint4/files/PkgLint/Type.pm @@ -0,0 +1,102 @@ +# $NetBSD: Type.pm,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# +# A Type in pkglint is a combination of a data type and a permission +# specification. Further details can be found in the chapter ``The pkglint +# type system'' of the pkglint book. +# +package PkgLint::Type; + +use strict; +use warnings; + +BEGIN { + import PkgLint::Util qw( + false true + ); + import PkgLint::Logging qw( + log_warning NO_LINES + ); + use Exporter; + use vars qw(@ISA @EXPORT_OK); + @ISA = qw(Exporter); + @EXPORT_OK = qw( + LK_NONE LK_INTERNAL LK_EXTERNAL + GUESSED NOT_GUESSED + ); +} + +use enum qw(KIND_OF_LIST BASIC_TYPE ACLS IS_GUESSED); +use enum qw(:LK_ NONE INTERNAL EXTERNAL); +use enum qw(:ACLE_ SUBJECT_RE PERMS); +use enum qw(NOT_GUESSED GUESSED); + +sub new($$$) { + my ($class, $kind_of_list, $basic_type, $acls, $guessed) = @_; + my ($self) = ([$kind_of_list, $basic_type, $acls, $guessed]); + bless($self, $class); + return $self; +} + +sub kind_of_list($) { return shift()->[KIND_OF_LIST]; } +sub basic_type($) { return shift()->[BASIC_TYPE]; } +# no getter method for acls +sub is_guessed($) { return shift()->[IS_GUESSED]; } + +sub perms($$) { + my ($self, $fname) = @_; + my ($perms); + + foreach my $acl_entry (@{$self->[ACLS]}) { + if ($fname =~ $acl_entry->[ACLE_SUBJECT_RE]) { + return $acl_entry->[ACLE_PERMS]; + } + } + return; +} + +# Returns the union of all possible permissions. This can be used to +# check whether a variable may be defined or used at all, or if it is +# read-only. +sub perms_union($) { + my ($self) = @_; + my ($perms); + + $perms = ""; + foreach my $acl_entry(@{$self->[ACLS]}) { + $perms .= $acl_entry->[ACLE_PERMS]; + } + return $perms; +} + +# Returns whether the type is considered an external list. All external +# lists are, of course, as well as some other data types that are not +# defined as lists to make the implementation of checkline_mk_vartype +# easier. +sub is_practically_a_list($) { + my ($self) = @_; + + return ($self->kind_of_list == LK_EXTERNAL) ? true + : ($self->kind_of_list == LK_INTERNAL) ? false + : ($self->basic_type eq "BuildlinkPackages") ? true + : ($self->basic_type eq "SedCommands") ? true + : ($self->basic_type eq "ShellCommand") ? true + : false; +} + +# Returns whether variables of this type may be extended using the "+=" +# operator. +sub may_use_plus_eq($) { + my ($self) = @_; + + return ($self->kind_of_list != LK_NONE) ? true + : ($self->basic_type eq "AwkCommand") ? true + : ($self->basic_type eq "BuildlinkPackages") ? true + : ($self->basic_type eq "SedCommands") ? true + : false; +} + +sub to_string($) { + my ($self) = @_; + + return (["", "InternalList of ", "List of "]->[$self->kind_of_list]) . $self->basic_type; +} diff --git a/pkgtools/pkglint4/files/PkgLint/Util.pm b/pkgtools/pkglint4/files/PkgLint/Util.pm new file mode 100644 index 00000000000..350f96f6b65 --- /dev/null +++ b/pkgtools/pkglint4/files/PkgLint/Util.pm @@ -0,0 +1,97 @@ +# $NetBSD: Util.pm,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# +# This package is a catch-all for subroutines that are not application-spe- +# cific. Currently it contains the boolean constants C<false> and C<true>, +# as well as a function to print text in a table format, and a function +# that converts an array into a hash. The latter is just for convenience +# because I don't know of a Perl operator similar to qw() that can be used +# for creating a hash. +# +package PkgLint::Util; + +use strict; +use warnings; + +BEGIN { + use Exporter; + use vars qw(@ISA @EXPORT_OK); + @ISA = qw(Exporter); + @EXPORT_OK = qw( + assert + false true dont_know doesnt_matter + array_to_hash normalize_pathname print_table + ); +} + +use enum qw(false true dont_know doesnt_matter); + +sub assert($$) { + my ($cond, $msg) = @_; + my (@callers, $n); + + if (!$cond) { + print STDERR ("FATAL: Assertion failed: ${msg}.\n"); + + for ($n = 0; my @info = caller($n); $n++) { + push(@callers, [$info[2], $info[3]]); + } + + for (my $i = $#callers; $i >= 0; $i--) { + my $info = $callers[$i]; + printf STDERR (" line %4d called %s\n", $info->[0], $info->[1]); + } + exit(1); + } +} + +# Prints the C<$table> on the C<$out> stream. The C<$table> shall be an +# array of rows, each row shall be an array of cells, and each cell shall +# be a string. +sub print_table($$) { + my ($out, $table) = @_; + my (@width) = (); + foreach my $row (@{$table}) { + foreach my $i (0..$#{$row}) { + if (!defined($width[$i]) || length($row->[$i]) > $width[$i]) { + $width[$i] = length($row->[$i]); + } + } + } + foreach my $row (@{$table}) { + my ($max) = ($#{$row}); + foreach my $i (0..$max) { + if ($i != 0) { + print $out (" "); + } + print $out ($row->[$i]); + if ($i != $max) { + print $out (" " x ($width[$i] - length($row->[$i]))); + } + } + print $out ("\n"); + } +} + +sub array_to_hash(@) { + my ($result) = {}; + + foreach my $arg (@_) { + $result->{$arg} = 1; + } + return $result; +} + +sub normalize_pathname($) { + my ($fname) = @_; + + # strip "." path components + $fname =~ s,^(?:\./)+,,; + $fname =~ s,/(?:\./)+,/,g; + $fname =~ s,/+,/,g; + + # strip intermediate "../.." path components + while ($fname =~ s,/[^.][^/]*/[^.][^/]*/\.\./\.\./,/,) { + } + + return $fname; +} diff --git a/pkgtools/pkglint4/files/PkgLint/VarUseContext.pm b/pkgtools/pkglint4/files/PkgLint/VarUseContext.pm new file mode 100644 index 00000000000..8311624340b --- /dev/null +++ b/pkgtools/pkglint4/files/PkgLint/VarUseContext.pm @@ -0,0 +1,67 @@ +# The various contexts in which make(1) variables can appear in pkgsrc. +# Further details can be found in the chapter ``The pkglint type system'' +# of the pkglint book. +# +package PkgLint::VarUseContext; + +use strict; +use warnings; + +BEGIN { + import PkgLint::Util qw( + false true + ); + import PkgLint::Logging qw( + log_warning NO_LINES + ); + use Exporter; + use vars qw(@ISA @EXPORT_OK); + @ISA = qw(Exporter); + @EXPORT_OK = qw( + VUC_TIME_UNKNOWN VUC_TIME_LOAD VUC_TIME_RUN + VUC_TYPE_UNKNOWN + VUC_SHELLWORD_UNKNOWN VUC_SHELLWORD_PLAIN VUC_SHELLWORD_DQUOT + VUC_SHELLWORD_SQUOT VUC_SHELLWORD_BACKT VUC_SHELLWORD_FOR + VUC_EXTENT_UNKNOWN VUC_EXTENT_FULL VUC_EXTENT_WORD + VUC_EXTENT_WORD_PART + ); +} + +use enum qw(TIME TYPE SHELLWORD EXTENT); +use enum qw(:VUC_TIME_ UNKNOWN LOAD RUN); +use constant VUC_TYPE_UNKNOWN => undef; +use enum qw(:VUC_SHELLWORD_ UNKNOWN PLAIN DQUOT SQUOT BACKT FOR); +use enum qw(:VUC_EXTENT_ UNKNOWN FULL WORD WORD_PART); + +my $pool = {}; + +sub new($$$$$) { + my ($class, $time, $type, $shellword, $extent) = @_; + my ($self) = ([$time, $type, $shellword, $extent]); + bless($self, $class); + return $self; +} +sub new_from_pool($$$$$) { + my ($class, $time, $type, $shellword, $extent) = @_; + my $key = "${time}-${type}-${shellword}-${extent}"; + + if (!exists($pool->{$key})) { + $pool->{$key} = $class->new($time, $type, $shellword, $extent); + } + return $pool->{$key}; +} + +sub time($) { return shift()->[TIME]; } +sub type($) { return shift()->[TYPE]; } +sub shellword($) { return shift()->[SHELLWORD]; } +sub extent($) { return shift()->[EXTENT]; } + +sub to_string($) { + my ($self) = @_; + + return sprintf("(%s %s %s %s)", + ["unknown-time", "load-time", "run-time"]->[$self->time], + (defined($self->type) ? $self->type->to_string() : "no-type"), + ["none", "plain", "squot", "dquot", "backt", "for"]->[$self->shellword], + ["unknown", "full", "word", "word-part"]->[$self->extent]); +} diff --git a/pkgtools/pkglint4/files/build.pl b/pkgtools/pkglint4/files/build.pl new file mode 100644 index 00000000000..e58b4e90ae4 --- /dev/null +++ b/pkgtools/pkglint4/files/build.pl @@ -0,0 +1,25 @@ +#! @PERL@ +# $NetBSD: build.pl,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# + +use strict; +use warnings; + +sub readfile { + my $file = shift; + + local $/ = undef; + open(my $in, "<", $file) || die "failed to read $file: $!"; + my $contents = <$in>; + close($in) || die "failed to read $file: $!"; + + return $contents; +} + +while (my $line = <>) { + if ($line =~ m"^#include (.+)$/") { + print readfile($1); + } else { + print $line; + } +} diff --git a/pkgtools/pkglint4/files/deprecated.map b/pkgtools/pkglint4/files/deprecated.map new file mode 100644 index 00000000000..f8b25c4d16c --- /dev/null +++ b/pkgtools/pkglint4/files/deprecated.map @@ -0,0 +1,166 @@ +# $NetBSD: deprecated.map,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# + +# This file contains names of Makefile variables and a short explanation +# what to do to make the warning disappear. Lines should only be removed +# if the explanation changes, in which case the new explanation should +# be added to the current date. + +# December 2003 +FIX_RPATH It has been removed from pkgsrc in 2003. + +# February 2005 +ALL_TARGET Use BUILD_TARGET instead. +NO_WRKSUBDIR Use WRKSRC=${WRKDIR} instead. +LIB_DEPENDS Use DEPENDS instead. +MASTER_SITE_SUBDIR Use ${MASTER_SITE_FOO:=subdir/} instead. +PATCH_SITE_SUBDIR Use ${PATCH_SITES_FOO:=subdir/} instead. +ONLY_FOR_ARCHS Use ONLY_FOR_PLATFORM instead. +NOT_FOR_ARCHS Use NOT_FOR_PLATFORM instead. +ONLY_FOR_OPSYS Use ONLY_FOR_PLATFORM instead. +NOT_FOR_OPSYS Use NOT_FOR_PLATFORM instead. + +# May 2005 +ALL_TARGET Use BUILD_TARGET instead. +DIGEST_FILE Use DISTINFO_FILE instead. +IGNORE Use PKG_FAIL_REASON or PKG_SKIP_REASON instead. +IS_INTERACTIVE Use INTERACTIVE_STAGE instead. +KERBEROS Use the PKG_OPTIONS framework instead. +MASTER_SITE_SUBDIR Use some form of MASTER_SITES instead. +MD5_FILE Use DISTINFO_FILE instead. +MIRROR_DISTFILE Use NO_BIN_ON_FTP and/or NO_SRC_ON_FTP instead. +NO_CDROM Use NO_BIN_ON_CDROM and/or NO_SRC_ON_CDROM instead. +NO_PATCH You can just remove it. +NO_WRKSUBDIR Use WRKSRC=${WRKDIR} instead. +PATCH_SITE_SUBDIR Use some form of PATCHES_SITES instead. +PATCH_SUM_FILE Use DISTINFO_FILE instead. +PKG_JVM Use PKG_DEFAULT_JVM instead. +USE_BUILDLINK2 You can just remove it. +USE_BUILDLINK3 You can just remove it. +USE_CANNA Use the PKG_OPTIONS framework instead. +USE_DB4 Use the PKG_OPTIONS framework instead. +USE_DIRS You can just remove it. +USE_ESOUND Use the PKG_OPTIONS framework instead. +USE_GIF Use the PKG_OPTIONS framework instead. +USE_GMAKE Use USE_TOOLS+=gmake instead. +USE_GNU_TOOLS Use USE_TOOLS instead. +USE_IDEA Use the PKG_OPTIONS framework instead. +USE_LIBCRACK Use the PKG_OPTIONS framework instead. +USE_MMX Use the PKG_OPTIONS framework instead. +USE_PKGLIBTOOL Use USE_LIBTOOL instead. +USE_SSL Include "../../security/openssl/buildlink3.mk" instead. + +# July 2005 +USE_PERL5 Use USE_TOOLS+=perl or USE_TOOLS+=perl:run instead. + +# October 2005 +NO_TOOLS You can just remove it. +NO_WRAPPER You can just remove it. + +# November 2005 +ALLFILES Use CKSUMFILES instead. +DEPENDS_TARGET Use DEPENDS instead. +FETCH_DEPENDS Use DEPENDS instead. +RUN_DEPENDS Use DEPENDS instead. + +# December 2005 +USE_CUPS Use the PKG_OPTIONS framework (option cups) instead. +USE_I586 Use the PKG_OPTIONS framework (option i586) instead. +USE_INN Use the PKG_OPTIONS framework instead. +USE_OPENLDAP Use the PKG_OPTIONS framework (option openldap) instead. +USE_OSS Use the PKG_OPTIONS framework (option oss) instead. +USE_RSAREF2 Use the PKG_OPTIONS framework (option rsaref) instead. +USE_SASL Use the PKG_OPTIONS framework (option sasl) instead. +USE_SASL2 Use the PKG_OPTIONS framework (option sasl) instead. +USE_SJ3 Use the PKG_OPTIONS framework (option sj3) instead. +USE_SOCKS Use the PKG_OPTIONS framework (socks4 and socks5 options) instead. +USE_WNN4 Use the PKG_OPTIONS framework (option wnn4) instead. +USE_XFACE Use the PKG_OPTIONS framework instead. + +# February 2006 +TOOLS_DEPMETHOD Use the :build or :run modifiers in USE_TOOLS instead. +MANDIR Please use ${PREFIX}/${PKGMANDIR} instead. +DOWNLOADED_DISTFILE Use the shell variable $$extract_file instead. +DECOMPRESS_CMD Use EXTRACT_CMD instead. + +# March 2006 +INSTALL_EXTRA_TMPL Use INSTALL_TEMPLATE instead. +DEINSTALL_EXTRA_TMPL Use DEINSTALL_TEMPLATE instead. + +# April 2006 +RECOMMENDED Use ABI_DEPENDS instead. +BUILD_USES_MSGFMT Use USE_TOOLS+=msgfmt instead. +USE_MSGFMT_PLURALS Use USE_TOOLS+=msgfmt instead. + +# May 2006 +EXTRACT_USING_PAX Use "EXTRACT_OPTS=-t pax" instead. +NO_EXTRACT It doesn't exist anymore. +_FETCH_MESSAGE Use FETCH_MESSAGE (different format) instead. +BUILDLINK_DEPENDS.* Use BUILDLINK_API_DEPENDS.* instead. +BUILDLINK_RECOMMENDED.* Use BUILDLINK_ABI_DEPENDS.* instead. +SHLIB_HANDLING Use CHECK_SHLIBS_SUPPORTED instead. +USE_RMAN It has been removed. + +# June 2006 +DEINSTALL_SRC Use the pkginstall framework instead. +INSTALL_SRC Use the pkginstall framework instead. +DEINSTALL_TEMPLATE Use DEINSTALL_TEMPLATES instead. +INSTALL_TEMPLATE Use INSTALL_TEMPLATES instead. +HEADER_TEMPLATE Use HEADER_TEMPLATES instead. +_REPLACE.* Use REPLACE.* instead. +_REPLACE_FILES.* Use REPLACE_FILES.* instead. +MESSAGE Use MESSAGE_SRC instead. +INSTALL_FILE It may only be used internally by pkgsrc. +DEINSTALL_FILE It may only be used internally by pkgsrc. + +# July 2006 +USE_DIGEST You can just remove it. +LTCONFIG_OVERRIDE You can just remove it. +USE_GNU_GETTEXT You can just remove it. +BUILD_ENV Use PKGSRC_MAKE_ENV instead. +DYNAMIC_MASTER_SITES You can just remove it. + +# September 2006 +MAKEFILE Use MAKE_FILE instead. + +# November 2006 +SKIP_PORTABILITY_CHECK Use CHECK_PORTABILITY_SKIP (a list of patterns) instead. +PKG_SKIP_REASON Use PKG_FAIL_REASON instead. + +# January 2007 +BUILDLINK_TRANSFORM.* Use BUILDLINK_FNAME_TRANSFORM.* instead. + +# March 2007 +SCRIPTDIR You can just remove it. +NO_PKG_REGISTER You can just remove it. +NO_DEPENDS You can just remove it. + +# October 2007 +_PKG_SILENT Use RUN (with more error checking) instead. +_PKG_DEBUG Use RUN (with more error checking) instead. +LICENCE Use LICENSE instead. +# The following variable is not yet deprecated, as there has been +# a large disagreement on the proper spelling. +#ACCEPTABLE_LICENCES Use ACCEPTABLE_LICENSES instead. + +# November 2007 +#USE_NCURSES Include "../../devel/ncurses/buildlink3.mk" instead. + +# December 2007 +INSTALLATION_DIRS_FROM_PLIST Use AUTO_MKDIRS instead. + +# April 2009 +NO_PACKAGE It doesn't exist anymore. +NO_MTREE You can just remove it. + +# July 2012 +SETGIDGAME Use USE_GAMESGROUP instead. +GAMEGRP Use GAMES_GROUP instead. +GAMEOWN Use GAMES_USER instead. + +# July 2013 +USE_GNU_READLINE Include "../../devel/readline/buildlink3.mk" instead. + +# October 2014 +SVR4_PKGNAME Just remove it. +PKG_INSTALLATION_TYPES Just remove it. diff --git a/pkgtools/pkglint4/files/doc/Makefile b/pkgtools/pkglint4/files/doc/Makefile new file mode 100644 index 00000000000..049a20531c0 --- /dev/null +++ b/pkgtools/pkglint4/files/doc/Makefile @@ -0,0 +1,27 @@ +# $NetBSD: Makefile,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# + +XMLDOCS+= pkglint.xml +XMLDOCS+= chap.intro.xml +XMLDOCS+= chap.defs.xml +XMLDOCS+= chap.types.xml +XMLDOCS+= chap.code.xml +XMLDOCS+= chap.statemachines.xml +XMLDOCS+= chap.future.xml + +IMAGES+= statemachine.patch.png +IMAGES+= statemachine.shellcmd.png + +.PHONY: all +all: pkglint.html + +pkglint.html: ${XMLDOCS} ${IMAGES} stylesheet.xsl + xmlto -m stylesheet.xsl html-nochunks pkglint.xml + +.PHONY: clean +clean: + rm -f *.html *.png + +.SUFFIXES: .dia .png +.dia.png: + dia -e ${.TARGET:Q} -t png ${.IMPSRC:Q} diff --git a/pkgtools/pkglint4/files/doc/chap.code.xml b/pkgtools/pkglint4/files/doc/chap.code.xml new file mode 100644 index 00000000000..44b02f9f128 --- /dev/null +++ b/pkgtools/pkglint4/files/doc/chap.code.xml @@ -0,0 +1,307 @@ +<!-- $NetBSD: chap.code.xml,v 1.1 2015/11/25 16:42:21 rillig Exp $ --> + +<chapter id="code"> +<title>Code structure</title> + + <para>In this chapter, I give an overview of how the &pkglint; + code is organized, starting with the <function>main</function> + function, passing the functions that check a single line and + finally arriving at the infrastructure that makes writing the + other functions easier.</para> + +<sect1 id="code.overview"> +<title>Overview</title> + + <para>The &pkglint; code is structured in modular, easy to + understand procedures. These procedures can be further + classified with respect to what they do. There are procedures + that check a file, others check the lines of a file, again + others check a single line. These classes of procedures are + described in the following sections in a top-down + fashion.</para> + + <para>If nothing special is said about which procedures call + which others, you may assume that procedures of a certain rank + only call procedures that are of a strictly lower rank. For + example, no <function>checkline_*</function> will ever call + <function>checkfile_*</function>. Sometimes, functions of the + same rank are called, but these cases are documented + explicitly.</para> + +</sect1> + +<sect1 id="code.select"> +<title>Selecting the proper checking function</title> + + <para>The <function>main</function> procedure of &pkglint; is a + simple loop around a TODO list containing pathnames of items (I + couldn't think of a better name here). The decision of which + checks to apply to a given item is done in + <function>checkitem</function>, which checks whether the item is + a file or a directory and dispatches the actual checking to + specialized procedures.</para> + +</sect1> + +<sect1 id="code.dir"> +<title>Checking a directory</title> + + <para>The procedures that check a directory are + <function>checkdir_root</function> for the pkgsrc root + directory, <function>checkdir_category</function> for a category + of packages and <function>checkdir_package</function> for a + single package.</para> + +</sect1> + +<sect1 id="code.file"> +<title>Checking a file</title> + + <para>Since the dispatching for files requires much code, it has + been put into a separate procedure called + <function>checkfile</function>, which further dispatches the + call to the other procedures.</para> + + <para>The procedures that check a specific file are + <function>checkfile_ALTERNATIVES</function>, + <function>checkfile_DESCR</function>, + <function>checkfile_distinfo</function>, + <function>checkfile_extra</function>, + <function>checkfile_INSTALL</function>, + <function>checkfile_MESSAGE</function>, + <function>checkfile_mk</function>, + <function>checkfile_patch</function> and + <function>checkfile_PLIST</function>. For most of the + procedures, it should be obvious to which files they are + applied. A distinction is made between buildlink3 files and + other <filename>Makefiles</filename>, as some additional checks + apply to buildlink3 files. Of course, these procedures use + pretty much the same code for checking, and this is where the + <function>checklines_*</function> functions step in.</para> + + <para>The <function>checkfile_package_Makefile</function> + function is somewhat special in that it expects four parameters + instead of only one. This is because loading the package data + has been separated from the actual checking.</para> + +</sect1> + +<sect1 id="code.lines"> +<title>Checking the lines in a file</title> + + <para>This class of procedures consists of + <function>checklines_trailing_empty_lines</function>, + <function>checklines_package_Makefile_varorder</function> and + <function>checklines_mk</function>. The middle one is too + complex to be included in + <function>checkfile_package_Makefile</function>, and the other + ones are of so generic use that they deserved to be procedures + of their own.</para> + + <para>The <function>checklines_mk</function> makes heavy use of + the various <function>checkline_*</function> functions that are + explained in the next chapter.</para> + +</sect1> + +<sect1 id="code.line"> +<title>Checking a single line in a file</title> + + <para>This class of procedures checks a single line of a file. + The number of parameters differs for most of these procedures, + as some need more context information and others don't.</para> + + <para>The procedures that are applicable to any file type are + <function>checkline_length</function>, + <function>checkline_valid_characters</function>, + <function>checkline_valid_characters_in_variable</function>, + <function>checkline_trailing_whitespace</function>, + <function>checkline_rcsid_regex</function>, + <function>checkline_rcsid</function>, + <function>checkline_relative_path</function>, + <function>checkline_relative_pkgdir</function>, + <function>checkline_spellcheck</function> and + <function>checkline_cpp_macro_names</function>.</para> + + <para>The rest of the procedures is specific to + <filename>Makefile</filename>s: + <function>checkline_mk_text</function>, + <function>checkline_mk_shellword</function>, + <function>checkline_mk_shelltext</function>, + <function>checkline_mk_shellcmd</function>, + <function>checkline_mk_vartype_basic</function>, + <function>checkline_mk_vartype_basic</function>, + <function>checkline_mk_vartype</function> and + <function>checkline_mk_varassign</function>.</para> + + <para>This class of procedures contains the most code in + &pkglint;. The procedures that check shell commands and shell + words both have around 200 lines, and the largest procedure is + the check for predefined variable types, which has almost 500 + lines. But the code is not complex at all, since this procedure + contains a large switch for all the predefined types. The checks + for a single type usually fit on a single screen.</para> + +</sect1> + +<sect1 id="code.infrastructure"> +<title>The &pkglint; infrastructure</title> + + <para>To keep the code in the checking procedures small and + legible, an additional layer of procedures is needed that + provides basic operations and abstractions for handling files as + a collection of lines and to print all diagnostics in a common + format that is suitable to further processing by software + tools.</para> + + <para>Since October 2004, this part of &pkglint; makes use of + some of the object oriented features of the Perl programming + language. It has worked quite well upto now, but it has not been + fun to write object-oriented code in Perl. The most basic + feature I am missing is that the compiler checks whether an + object has a specific method or not, as I have often written + <code>$line->warning()</code> instead of + <code>$line->log_warning()</code>. This makes refacturing quite + difficult if you don't have a 100 % coverage test, and I + don't have that.</para> + + <para>The classes are all defined in the + <varname>PkgLint</varname> namespace.</para> + + <para>The traditional class is <classname>Line</classname>, + which represents a logical line of a file. In case of + <filename>Makefile</filename>s, line continuations are parsed + properly and combined into a single line. For all other files, + each logical line corresponds to a physical line. The + <classname>Line</classname> class has accessor methods to its + fields <methodname>fname</methodname>, + <methodname>lines</methodname> and + <methodname>text</methodname>. It also has the methods + <methodname>log_fatal</methodname>, + <methodname>log_error</methodname>, + <methodname>log_warning</methodname>, + <methodname>log_info</methodname> and + <methodname>log_debug</methodname> that all have one parameter, + the diagnostics message. The other methods are used less + often.</para> + + <para>In January 2006, the logging has been improved in + functionality. Before that, a logical line could well consist of + 300 physical lines, so a diagnostic would say <quote>you have a + bug somewhere between line 100 and 400</quote>. This is not + helpful. Therefore, a new class has been invented that allows to + map each character of a logical line to its corresponding + physical location in the file. The new representation of a + logical line is called a <classname>String</classname>. This + feature is still experimental, since the only method for logging + a string is <methodname>log_warning</methodname>. The others are + still missing. It is also completely unclear how lines that have + been fixed by &pkglint; are represented since this moves + characters around in the physical lines.</para> + + <para>To make pattern matching with the new + <classname>String</classname> easy to use, the additional class + <classname>StringMatch</classname> has been created. It saves + the result of a <classname>String</classname> that is matched + against a regular expression. The canonical way to get such a + <classname>StringMatch</classname> is to call the + <methodname>String::match</methodname> method.</para> + + <para>Since the <classname>StringMatch</classname> was + convenient to use, the <classname>SimpleMatch</classname> class + represents the result of matching a Perl string against a + regular expression. The class <classname>Location</classname> is + currently unused.</para> + +</sect1> +<sect1 id="code.style"> +<title>Perl programming style</title> + + <para>The &pkglint; source code has evolved from FreeBSD's portlint, + which has been written in Perl, and up to now, &pkglint; is written + in Perl. Since one of the main ingredients to &pkglint; are regular + expressions, this choice seems natural, and indeed the Perl regular + expressions are a great help to keep the code short. But &pkglint; + is more than just throwing regular expressions at the package + files.</para> + + <para>In 2004, when the &pkglint; source code comprised about + 40 kilobytes, this was quite appropriate. Since then, the code + has become much more structured and various abstraction layers have + been inserted. It became more and more clear that the Perl + programming language has not been designed with nice-looking source + code in mind.</para> + + <para>The first example are subroutines and their parameters. In + most other languages, the names of the parameters are mentioned in + the subroutine definition. Not so in Perl. The parameters to each + subroutine are passed in the <literal>@_</literal> array. The usual + way to get named parameters is to write assign the parameter array + to a list of local variables. This extra statement is a nuisance, + but it is merely syntactical.</para> + + <para>More serious is the way the arguments are passed to a + subroutine. Perl allows the programmer to define subroutines with a + weak form of prototypes, which helps to catch calls to subroutines + that provide a wrong number of arguments. This feature catches many + bugs that are easily overlooked. The downside is that anything + besides using scalars as parameter types is difficult to understand + and quickly leads to unexpected behavior. Therefore the subroutines + in &pkglint; only use this style for parameter passing. Oh, and by + the way, the subroutine prototypes are only checked for in certain + situations like direct calls. In method calls, nothing is checked at + all. Since almost all diagnostics are produced by calling + <code>$line->log_warning()</code> or + <code>$line->log_error()</code>, most of the subroutine calls in + &pkglint; go unchecked.</para> + + <para>Instead of using magic numbers, well written code defines + named constants for these numbers and then refers to them using + their names, giving the reader extra information that plain numbers + could not give. Although the constant definitions look quite good in + &pkglint; there is one big caveat. The Perl programming language + does not know constants. So these definitions are rather shortcuts + for defining functions that return the value of the constant. And as + functions in Perl have package-wide scope, so have these constants. + This is why the namespace prefixes like <varname>SWST_</varname> are + necessary to avoid name clashes.</para> + + <para>Most of the constants would be written as an enumeration data + type if Perl had one. The same limitation applies for many of the + classes (implemented as packages in Perl) that are simply structs. + The typical Perl implementation of structs are classes, er, packages + which then use methods for accessing the fields. Again, the names of + these methods are only checked at runtime, so there is no language + support for detecting spelling mistakes in field names.</para> + + <para>Another area where Perl fails to detect many errors is the + loose type system. You can apply almost every operator to almost + every data type, and the Perl language will give you more or less + what you want. Especially it does not prevent you from matching + a regular expression against a reference. It will simply compute + a string representation of the reference and match the regular + expression against that.</para> + + <para>The current Perl interpreter is very inefficient when + copying strings. This happens really often in pkglint, for + example when passing arguments to functions or saving the result + of a regular expression match in <quote>real</quote> variables. + For a great speed-up, an implementation that handles string + objects by reference-counting them would be better. (Lua comes + to mind.)</para> + +</sect1> +<sect1 id="code.lang"> +<title>Switching to another language</title> + + <para>Switching to C++ is not an option, since the typing + overhead would be more than twice the current amount. As a + consequence the code would become much less readable.</para> + + <para>Switching to OCaml looks nice (because of the type + inference), but the regular expressions that are provided by the + system are by no means sufficient. On the other hand, since + today there is a PCRE package for OCaml in pkgsrc.</para> + +</sect1> +</chapter> diff --git a/pkgtools/pkglint4/files/doc/chap.defs.xml b/pkgtools/pkglint4/files/doc/chap.defs.xml new file mode 100644 index 00000000000..1bbee6f911a --- /dev/null +++ b/pkgtools/pkglint4/files/doc/chap.defs.xml @@ -0,0 +1,26 @@ +<!-- $NetBSD: chap.defs.xml,v 1.1 2015/11/25 16:42:21 rillig Exp $ --> +<!-- don't include useless definitions. +<chapter id="defs"> +<title>Definitions</title> + + <para>In every non-toy program, the need arises to define new + words or redefine and clarify existing words. This is the list + of words that are used in pkglint.</para> + + <variablelist> + + <varlistentry><term>function</term><listitem><para>A subroutine + that is called to obtain a return value, rather than for its + side effects. Functions should restrict the user-visible side + effects to the necessary minimum.</para> + </listitem></varlistentry> + + <varlistentry><term>procedure</term><listitem><para>A subroutine + that is not called to obtain a return value, but rather called + because of its side effects, like input/output.</para> + </listitem></varlistentry> + + </variablelist> + +</chapter> +--> diff --git a/pkgtools/pkglint4/files/doc/chap.design.xml b/pkgtools/pkglint4/files/doc/chap.design.xml new file mode 100644 index 00000000000..8ee3e98da96 --- /dev/null +++ b/pkgtools/pkglint4/files/doc/chap.design.xml @@ -0,0 +1,117 @@ +<!-- $NetBSD: chap.design.xml,v 1.1 2015/11/25 16:42:21 rillig Exp $ --> + +<chapter id="design"> +<title>Design goals</title> + + <para>&pkglint; should be simple to use. It should be consistent + and predictable in what it does. The diagnostics should be + understandable. The number of false positive and false negative + diagnostics should be minimal.</para> + +<sect1 id="design.simple-to-use"> +<title>Simple to use</title> + + <para><emphasis>Requirement:</emphasis> Using &pkglint; should + not require any knowledge about obscure command line options or + hidden features.</para> + + <para>Calling &pkglint; without options gives a useful amount of + warnings. No further knowledge is needed. Users that are + accustomed to GNU software will quickly find the + <literal>--help</literal> command line option, which gives a + quite verbose description of the available options. Users that + know the GNU compilers will easily remember the + <literal>-W</literal> class of options, especially + <literal>-Wall</literal>. Other than with the GNU compilers, the + latter option enables really <emphasis>all</emphasis> warnings + that are intended to be user-visible.</para> + + <para>The command line options come in two flavors: short and + long options. The long options are meant to be used when + explaining them to others, while the short options are meant to + be used when invoking &pkglint; in an interactive shell.</para> + +</sect1> +<sect1 id="design.consistent-and-predictable"> +<title>Consistent and predictable</title> + + <para><emphasis>Requirement:</emphasis> &pkglint; should behave + such that the user quickly gets an impression about what + &pkglint; does. This impression should be persistent, that is, + the output format for diagnostics should be stable over time, + and diagnostic messages should not be changed without + reason.</para> + + <para>There are only two cases of what the output of + &pkglint; is. One is a single line containing the text + <quote>looks fine.</quote>, the other is a list of diagnostics, + followed by a summary on the number of diagnostics.</para> + + <para>If no warnings are printed, the single line <quote>looks + fine.</quote> gives a little motivation to the user. This + message is one of the few things that have been kept in + &pkglint; since it has been adopted from FreeBSD. It just makes + pkglint a more friendly tool. :)</para> + + <para>All error and warning messages are formatted by a single + procedure, <function>PkgLint::Logging::log_message</function>. This + way, all messages are formatted the same way, which allows easy + recognition by human users as well as other tools. There are two + different formats available, the traditional one and the gcc-like + one. In both formats, each diagnostic occupies exactly one line. Up + to the year 2005, some of the longer messages used to take more than + one line, but this behavior has been removed.</para> + + <para>The default format is the traditional one. It consists of the + severity, in upper-case letters, followed by the filename, the line + number and finally the message text. It allows easy recognition of + the severity of the messages. Even if errors and warnings are + intermixed in the output, the filenames start almost in the same + column.</para> + + <para>The gcc-like output format consists of the filename, the line + numbers, the severity and finally the message text. It has been + added to make it easier to integrate the &pkglint; diagnostics into + various text editors, for example Emacs. Since in this format the + filename is the first word, it can be easily seen which warning + originates in which file.</para> + + <para>There are some other procedures that affect the output, but + they have to be enabled explicitly.</para> + +</sect1> +<sect1 id="design.understandable"> +<title>Understandable diagnostics</title> + + <para><emphasis>Requirement:</emphasis> The diagnostics are + intended to help the user in writing better package definitions. + They should use an unambiguous, clear language and point + directly to the problem. If possible, they should include a hint + on how the problem can be fixed properly.</para> + +</sect1> +<sect1 id="design.false-diagnostics"> +<title>Few false diagnostics</title> + + <para><emphasis>Requirement:</emphasis> The number of + <firstterm>false positives</firstterm>, that is diagnostics + where no problem actually exists, should be minimal. On the + other hand, &pkglint; should detect as many problems as + possible. If it fails to detect a problem, this is called a + <firstterm>false negative</firstterm>.</para> + + <para>Currently, there are very few false positives. The way + &pkglint; parses the files is already close to the way + <command>make</command> and <command>sh</command> parse them, so + the structure of the files is modelled quite well.</para> + + <para>Since &pkglint; is also very strict in what it accepts, + many problems can already be detected. But since the pkgsrc + developers are quite creative when it comes to solving problems, + &pkglint; cannot detect everything. After all, the language used + to define packages is turing-complete, so it cannot be decided + in every case whether a package is valid or not. Luckily, most + packages are quite simple.</para> + +</sect1> +</chapter> diff --git a/pkgtools/pkglint4/files/doc/chap.future.xml b/pkgtools/pkglint4/files/doc/chap.future.xml new file mode 100644 index 00000000000..e76cd43c4f6 --- /dev/null +++ b/pkgtools/pkglint4/files/doc/chap.future.xml @@ -0,0 +1,91 @@ +<!-- $NetBSD: chap.future.xml,v 1.1 2015/11/25 16:42:21 rillig Exp $ --> + +<chapter id="future"> +<title>Future directions</title> + +<sect1 id="future.tokenize"> +<title>Tokenizing the input</title> + + <para>For providing more exact diagnostics, it would be nice if + &pkglint; could point the user to the exact character position + of the smallest problematic text in a file. To do this, the + file's contents has to be splitted into tokens.</para> + + <para>Doing this is nontrivial, since the tokenizing scheme + depends on the context in which the tokens are used. For + example, the <varname>COMMENT</varname> variable may contain + arbitrary characters (including <literal>'</literal> and + <literal>"</literal>), whereas in many other contexts these are + parts of quoted shell words.</para> + +</sect1> +<sect1 id="future.ast"> +<title>Working on abstract syntax trees (AST)</title> + + <para>When the tokenizing above is done, the tokens could be + parsed by a grammar to form abstract syntax trees. These would + consist mainly of function application so that pkglint can infer + types and valid values over these trees. The following functions + are likely to appear.</para> + + <table id="future.ast.func"> + <title>Functions in the abstract syntax trees</title> + <tgroup cols="2"> + <thead><row><entry>Function</entry><entry>Purpose</entry></row></thead> + <tbody> + <row><entry><function>quote</function>(Val)</entry><entry>The <literal>:Q</literal> modifier</entry></row> + <row><entry><function>append</function>(Val, Val)</entry><entry>The <literal>+=</literal> operator</entry></row> + <row><entry><function>concat</function>(Val, Val)</entry><entry>The direct concatenation of two values</entry></row> + <row><entry><function>subst</function>(Val, Subst)</entry><entry>The <literal>:S</literal> and <literal>:C</literal> modifiers</entry></row> + <row><entry><function>shell</function>(Val)</entry><entry>The <literal>!=</literal> operator and the <literal>:sh</literal> modifier</entry></row> + <row><entry><function>literal</function>(Val)</entry><entry>Introduces literal values</entry></row> + </tbody> + </tgroup> + </table> + + <para>Examples:</para> + +<programlisting> + WRKSRC= ${WRKDIR} + SUBST_SED.pkglint+= -e s\|@DATADIR@\|${PREFIX:Q}/share/pkglint\|g +</programlisting> + + <para>The first line would be parsed as + <literal>assign(var("WRKSRC"), varuse("WRKDIR"))</literal>. The + second line would be parsed as + <literal>assign(var("SUBST_SED.pkglint"), + append(varuse("SUBST_SED.pkglint"), concat(concat(str("-e + s\\|@DATADIR@\\|"), quote(varuse("PREFIX"))), + str("/share/pkglint\\|g"))))</literal>.</para> + + <para>At this point, unification together with a pattern matcher + on tree structures would come in handy, to allow the parser for + the shell commands to still operate on this parse tree. This + might eventually enable cross-language type inference.</para> + +</sect1> +<sect1 id="future.vars"> +<title>Even more restricted variables</title> + + <para>Currently there are mainly two restrictions for variables: + What values they may contain (data types) and where they may be + defined and used, on a per-file basis.</para> + + <para>The <filename>makevars.map</filename> file already + contains annotations to distinguish user-defined from + system-defined variables, but they are currently only used as + abbreviations and not further exploited. Based on these + definitions, sequence points may be defined in the pkgsrc + infrastructure where the values of these variables must have + certain properties, like being defined or being fixed (which + means that the variable will not change further).</para> + + <para>For example, user-defined variables may then be specified + as follows. They are given default values in + <filename>mk/defaults/mk.conf</filename>, may be overridden by + any file that is included inside <varname>MAKECONF</varname>, + and after that, their value is fixed. They may then be used at + both load and run time.</para> + +</sect1> +</chapter> diff --git a/pkgtools/pkglint4/files/doc/chap.intro.xml b/pkgtools/pkglint4/files/doc/chap.intro.xml new file mode 100644 index 00000000000..f9c118eff8c --- /dev/null +++ b/pkgtools/pkglint4/files/doc/chap.intro.xml @@ -0,0 +1,15 @@ +<!-- $NetBSD: chap.intro.xml,v 1.1 2015/11/25 16:42:21 rillig Exp $ --> + +<chapter id="intro"> +<title>Introduction</title> + + <para>&pkglint; is a static analysis tool for pkgsrc packages. + It finds many errors and problematic issues in those packages. + Starting in June 2004, &pkglint; has evolved into a powerful + tool that gives precise warnings wherever possible. With that + power comes much additional complexity, which cannot be + understood from reading the source code alone. This document + provides the necessary background information to understand what + the actual code does and why it is done this way.</para> + +</chapter> diff --git a/pkgtools/pkglint4/files/doc/chap.statemachines.xml b/pkgtools/pkglint4/files/doc/chap.statemachines.xml new file mode 100644 index 00000000000..42f5b1792fe --- /dev/null +++ b/pkgtools/pkglint4/files/doc/chap.statemachines.xml @@ -0,0 +1,77 @@ +<!-- $NetBSD: chap.statemachines.xml,v 1.1 2015/11/25 16:42:21 rillig Exp $ --> + +<chapter id="statemachines"> +<title>State machines</title> + + <para>This chapter explains the various state machines that are + used in &pkglint;. It also provides graphical representations of + them that are much easier to read than the source code.</para> + + <para>The opaque arrows in the figures represent transitions + that have a regular expression as condition. The hollow arrows + are the default transitions if nothing else matches. When + multiple regular expressions match in a state, the one that + appears first in the source code is chosen.</para> + +<sect1 id="statemachines.shellword"> +<title>The state machine for shell words</title> + + <para>The state machine for single shell words is pretty simple, + and I think it can be understood from the source code alone. So + no graphical representation is provided.</para> + +</sect1> + +<sect1 id="statemachines.shellcommand"> +<title>The state machine for shell commands</title> + + <figure id="statemachine.shellcommand"> + <title>The state transitions for shell commands</title> + <mediaobject> + <imageobject> + <imagedata fileref="statemachine.shellcmd.png" format="PNG"/> + </imageobject> + <textobject><para>(Here should be a drawing of the state transitions.)</para></textobject> + </mediaobject> + </figure> + + <para>The punch card symbols provide a means to go to a certain + state whenever the input matches the text on the punch + card.</para> + +</sect1> + +<sect1 id="statemachines.patch"> +<title>The state machine for patch files</title> + + <para>The state machine for patch files is the newest of the + state machines. Here, the state transitions are separated from + the code, which makes the code itself pretty small. I don't know + yet if this programming style is elegant or not. Time will + show.</para> + + <figure id="statemachine.patch"> + <title>The state transitions for patch files</title> + <mediaobject> + <imageobject> + <imagedata fileref="statemachine.patch.png" format="PNG"/> + </imageobject> + <textobject><para>(Here should be a drawing of the state transitions.)</para></textobject> + </mediaobject> + </figure> + + <para>The states on the left side are for parsing context diffs, + the ones on the right side are for unified diffs. Some of the + state names are highly abbreviated as follows. The first letter + gives the format of the patch, which is <quote>c</quote> for + context diffs and <quote>u</quote> for unified diffs. The second + letter gives the current syntactical level, which is + <quote>f</quote> for a file header, <quote>h</quote> for a hunk + header, or <quote>l</quote> for the hunk lines. The third letter + describes the action that belongs to the line, which is + <quote>a</quote> for an addition, and <quote>d</quote> for a + deletion.</para> + +</sect1> + +</chapter> diff --git a/pkgtools/pkglint4/files/doc/chap.types.xml b/pkgtools/pkglint4/files/doc/chap.types.xml new file mode 100644 index 00000000000..77f21c6548f --- /dev/null +++ b/pkgtools/pkglint4/files/doc/chap.types.xml @@ -0,0 +1,545 @@ +<!-- $NetBSD: chap.types.xml,v 1.1 2015/11/25 16:42:21 rillig Exp $ --> + +<chapter id="types"> +<title>The &pkglint; type system</title> + + <para>One of the most notable additions to &pkglint; is the + introduction of typed variables. Traditionally, in + <filename>Makefile</filename>s, all variables have the type + <type>String</type>. This prevents many useful checks from being + done before executing the code.</para> + + <para>Up to 2004, &pkglint; already did some checks based on + the value of the variables, but these checks had no common + structure that could be described easily.</para> + +<sect1 id="types.history"> +<title>History</title> + + <para>In February 2005, initial support for the &pkglint; type + system has been added. Some of the common variables have been + assigned types such as <literal><type>Boolean</type></literal> + or <literal><type>Yes_Or_Undefined</type></literal>, which are + the two common ways to represent boolean variables in pkgsrc. + The list of typed variables has been moved from the &pkglint; + code to an external file, <filename>makevars.map</filename>. + Many more basic types have been added later.</para> + + <para>In October 2005, the type system has been extended to + allow <literal><type>List of + <replaceable>simple-type</replaceable></type></literal>, which + allowed to handle variables like <varname>DEPENDS</varname> and + <varname>CFLAGS</varname>. One month later, enumeration types + have been added, allowing the type of + <varname>PTHREAD_OPTS</varname> to be expressed as <literal>List + of { require native }</literal>.</para> + + <para>In May 2006, the definition and use of variables has been + further restricted by introducing ACLs, which define the + permitted operations (write, append, default, read, preprocess-read) + depending on the current file.</para> + +</sect1> + +<sect1 id="types.syntax"> +<title>Syntax for defining types</title> + +<programlisting> + type ::= (list-type)? simple-type (acls)? + + list-type ::= ("List" | "InternalList") "of" + + simple-type ::= predefined-type + | enumeration + predefined-type ::= [A-Za-z][0-9A-Z_a-z]* + enumeration ::= "{" (enumeration-item)* "}" + enumeration-item ::= [-0-9A-Z_a-z]+ + + acls ::= "[" (acl-entry ("," acl-entry)*)? "]" + acl-entry ::= acl-subject ":" acl-perms + acl-subject ::= [.0-9A-Za-z]+ | "_" + acl-perms ::= [adprs]* +</programlisting> + +</sect1> +<sect1 id="types.semantics"> +<title>Semantics of the types</title> + + <para>The <firstterm>simple types</firstterm> in &pkglint; are + either predefined types or enumeration types. A + <firstterm>predefined type</firstterm> is used by its name. See + <xref linkend="types.predefined"/> for the list of predefined + types.</para> + + <para>An expression of an enumeration type may have either of + the enumeration-items as a value. It may not reference other + variables.</para> + + <para>A list type can be constructed from a predefined type or + an enumeration. It is not possible to construct lists of lists, + since I have never needed that. There are two types of lists, + called <literal>List</literal> and + <literal>InternalList</literal>, which are described in the + <ulink url="&pkgsrc-guide;/makefile.html">pkgsrc guide, the + chapter about <filename>Makefile</filename>s</ulink>.</para> + +</sect1> +<sect1 id="types.acls"> +<title>Access Control Lists</title> + + <para>Additionally to the data type, which specifies + <emphasis>what</emphasis> a variable can contain, the ACLs + define <emphasis>where</emphasis> the variable can be defined or + used (this is called the <firstterm>ACL subject</firstterm>) and + which operations are allowed (these are the <firstterm>ACL + permissions</firstterm>).</para> + + <para>The ACL subjects are specified by the filename. For + example, <filename>Makefile</filename> and + <filename>buildlink3.mk</filename> are valid ACL subjects. Since + some names occur over an over in pkgsrc, these can be + abbreviated as shown in <xref linkend="types.acl.subjects.abbr" + />. The character <literal>*</literal> is a placeholder for zero + or more arbitrary characters, like in the shell. The possible + actions on a variable are shown in <xref + linkend="types.acl.perms" />.</para> + + <table id="types.acl.subjects.abbr"> + <title>ACL Subjects</title> + <tgroup cols="2"> + <thead><row><entry>Subject</entry><entry>Abbreviation</entry></row></thead> + <tbody> + <row><entry><filename>Makefile</filename></entry><entry>m</entry></row> + <row><entry><filename>Makefile.common</filename></entry><entry>c</entry></row> + <row><entry><filename>buildlink3.mk</filename></entry><entry>b</entry></row> + <row><entry><filename>hacks.mk</filename></entry><entry>h</entry></row> + <row><entry><filename>options.mk</filename></entry><entry>o</entry></row> + <row><entry>any file</entry><entry>*</entry></row> + </tbody> + </tgroup> + </table> + + <table id="types.acl.perms"> + <title>ACL Permissions</title> + <tgroup cols="2"> + <thead><row><entry>Permission</entry><entry>Description</entry></row></thead> + <tbody> + <row><entry><filename>a</filename></entry><entry>Append to the + variable using the <literal>+=</literal> operator.</entry></row> + <row><entry><filename>d</filename></entry><entry>Provide a + default value for the variable using the <literal>?=</literal> + operator.</entry></row> + <row><entry><filename>s</filename></entry><entry>Set the + variable unconditionally using the <literal>=</literal>, + <literal>:=</literal> or <literal>!=</literal> + operator.</entry></row> + <row><entry><filename>u</filename></entry><entry>Use the value + of the variable.</entry></row> + <row><entry><filename>p</filename></entry><entry>Use the value + of the variable during preprocessing.</entry></row> + </tbody> + </tgroup> + </table> + + <para>If a variable has no ACL definition at all, all operations + are allowed on it. Otherwise exactly those operations of the + first ACL entry whose subject matches the current filename are + allowed. If no entry matches, nothing is allowed.</para> + + <para>For determining if a variable is used in the correct + place, the filename is only one part of the whole decision. The + other one is the context in which the variable appears. There + are many factors that influence whether the variable is used + correctly.</para> + + <itemizedlist> + + <listitem><para>The variable may be either used at preprocessing time + or at runtime. Some variables are defined in + <filename>bsd.pkg.mk</filename> and thus are not available until + that file has been included. As this should always be the very + last thing a package includes, it practically means that these + variables are only available at runtime.</para></listitem> + + <listitem><para>The variable may appear as the whole right hand side + of an assignment, as a single word, or even as part of a word. + First, the types on the right and left side should be + compatible. Second, some variables need to be quoted correctly, + depending on whether they are part of a word or not.</para></listitem> + + <listitem><para>In shell commands, the variable may also appear as a + whole word or as part of a word. This is similar to the case + above.</para></listitem> + + <listitem><para>The variable may appear inside some sort of quotes. + For some variables this is acceptable, as they are assumed to + never contain special characters. For others it + isn't.</para></listitem> + + <listitem><para>The various operators in conditional statements like + <literal>.if</literal> may further restrict the valid values. + For example, the <varname>OPSYS</varname> variable should only + be compared to well-known operating system names. The + <varname>exists()</varname> function should only be called on + pathnames. The <varname>defined()</varname> operator only checks + if the variable is defined and does not access its + value.</para></listitem> + + </itemizedlist> + +<sect2 id="types.acls.future"> +<title>Future Directions</title> + + <para>Currently the ACLs only cover the <quote>user + space</quote> of pkgsrc. They will be extended later to also + check for valid variable definition and use in the pkgsrc + infrastructure, as well as the user configuration file. For + completeness, those variables that are intended to be specified + on the command line will be added to the + <filename>makevars.map</filename> file.</para> + +</sect2> +</sect1> +<sect1 id="types.predefined"> +<title>Predefined types</title> + + <para>There are many predefined types in &pkglint;, which are + described below.</para> + + <!-- reference: pkglint.pl, revision 1.532 --> + <variablelist> + + <varlistentry><term><literal><type>AwkCommand</type></literal></term> + <listitem><para>An awk command. Currently nothing is checked + here.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>BuildlinkDepmethod</type></literal></term> + <listitem><para>Must be either <literal>build</literal> or + <literal>full</literal>.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>BuildlinkDepth</type></literal></term> + <listitem><para>This type is only intended for one variable, + namely <varname>BUILDLINK_DEPTH</varname>, which is only + modified in <filename>buildlink3.mk</filename> + files.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>BuildlinkPackages</type></literal></term> + <listitem><para>The type of the variable + <varname>BUILDLINK_PACKAGES</varname>. Like + <literal><type>BuildlinkDepth</type></literal> above, this is + only used in <filename>buildlink3.mk</filename> files. This + variable has two different patterns to be modified. The first is + to remove the current package from itself, and the second is to + append the current package. This prevents a package from showing + up twice in the list.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>Category</type></literal></term> + <listitem><para>One of the categories that a package may be + placed in. The list of categories has been assembled manually + when the type was introduced. There is no further agreement on + which valid categories are valid, besides the top level + directory names in pkgsrc.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>CFlag</type></literal></term> + <listitem><para>One word in a <varname>CFLAGS</varname> or + <varname>CPPFLAGS</varname> variable. &pkglint; knows the flags + starting with <literal>-D</literal>, <literal>-U</literal>, + <literal>-I</literal>. Flags starting with + <literal>-O</literal>, <literal>-W</literal>, + <literal>-f</literal>, <literal>-g</literal> or + <literal>-m</literal> are silently accepted since they are + commonly used for the GNU compilers. As the pkgsrc framework + does not know how to handle most of these flags, care should be + taken.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>Comment</type></literal></term> + <listitem><para>The comment of a package.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>Dependency</type></literal></term> + <listitem><para>A simple dependency like + <literal>foopkg>=1.0</literal>, <literal>foopkg-[0-9]*</literal> + or <literal>foopkg-1.0</literal>.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>DependencyWithPath</type></literal></term> + <listitem><para>A dependency (see above), followed by a colon + and a relative directory. For some packages, special variables + like <varname>USE_TOOLS</varname> should be used instead of an + explicit dependency.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>DistSuffix</type></literal></term> + <listitem><para>The value of the variable + <varname>EXTRACT_SUFX</varname>. The difference in the name is + intentional here, since <varname>EXTRACT_SUFX</varname> is a + misnomer. <varname>DIST_SUFX</varname> or + <varname>DIST_SUFFIX</varname> would be more appropriate.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>EmulPlatform</type></literal></term> + <listitem><para>An emulated platform consists of the operating + system (in lowercase, as opposed to <type>PlatformTriple</type>) + and the hardware architecture.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>Filename</type></literal></term> + <listitem><para>A filename, as defined in <ulink + url="http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_169">POSIX</ulink>. + This type further restricts the set of allowed characters. + See also <literal><type>Pathname</type></literal>.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>Filemask</type></literal></term> + <listitem><para>A shell globbing pattern that does not contain a + slash. See also <literal><type>Pathmask</type></literal>.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>Identifier</type></literal></term> + <listitem><para>In various places in pkgsrc, identifiers are + used. This type collects the most common naming conventions. + When you need a more specific check, you have to write your own + check.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>LdFlag</type></literal></term> + <listitem><para>A flag that is passed to the linker. Flags + starting with <literal>-L</literal> or <literal>-l</literal> are + accepted, as well as some others that are assumed to be handled + by the wrapper framework.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>Mail_Address</type></literal></term> + <listitem><para>Checks for a very restricted subset of <ulink + url="http://www.ietf.org/rfc/rfc2822.txt">RFC + 2822</ulink>.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>Message</type></literal></term> + <listitem><para>Messages are printed to the user as status + indicators. <ulink + url="http://www.freebsd.org/cgi/cvsweb.cgi/ports/devel/portlint/src/portlint.pl#rev1.77">As + opposed to FreeBSD</ulink>, they should not be quoted since they + may be used in contexts where quoting should be done + differently.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>Option</type></literal></term> + <listitem><para>An option from the + <literal>PKG_OPTIONS</literal> framework. Options should not + contain underscores. They should be documented in + <filename>pkgsrc/mk/defaults/options.description</filename>.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>Pathlist</type></literal></term> + <listitem><para>A list of directories that are separated by + colons, like the popular environment variable + <varname>PATH</varname>. This type differs from the type + <literal><type>List of Pathname</type></literal> in the + character that is used as a separator.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>Pathmask</type></literal></term> + <listitem><para>A shell globbing expression that may include + slashes.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>Pathname</type></literal></term> + <listitem><para>A pathname, as defined in <ulink + url="http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_266">POSIX</ulink>. + See also <literal><type>Filename</type></literal>.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>Perl5Packlist</type></literal></term> + <listitem><para>A common error has been to refer to + <varname>INSTALLARCHLIB</varname> in the location of the packing + list. Therefore no references to other variables are + allowed.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>PkgName</type></literal></term> + <listitem><para>A package name should conform to some + restrictions, since the filename of the binary package is + created from it, which is then interpreted by pkg_add and the + like.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>PkgOptionsVar</type></literal></term> + <listitem><para>I had once made the mistake of referencing + <varname>PKGBASE</varname> in this variable, not knowing that + <varname>PKG_OPTIONS_VAR</varname> is used during preprocessing, + when <varname>PKGBASE</varname> is not yet defined. This type + prevent that mistake from being done again.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>PkgPath</type></literal></term> + <listitem><para>A directory name that is relative to the + top-level pkgsrc directory. This is only used in specifying + specific packages in bulk builds. Despite its name, this type is + more similar to <type>RelativePkgDir</type> than to + <type>RelativePkgPath</type>.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>PkgRevision</type></literal></term> + <listitem><para>The package revision must be a small integer. + The only place where this definition may occur is the package + <filename>Makefile</filename> itself, as this variable says + something about the individual package. There is no mechanism in + pkgsrc for something similar to <varname>PKGREVISION</varname> + that can be used in <filename>Makefile.common</filename> + files.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>PlatformTriple</type></literal></term> + <listitem><para>pkgsrc has been ported to many platforms, all of + which are identified using a triple of operating system, + operating system version and hardware + architecture.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>Readonly</type></literal></term> + <listitem><para>This type is used to mark a variable as being + read-only to a package author. As this is not really a data type + but an access restriction, it will disappear in the next version + of the type system.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>RelativePkgDir</type></literal></term> + <listitem><para>A directory name that is relative to the package + directory. Mostly used for dependencies. See also + <literal><type>RelativePkgPath</type></literal>.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>RelativePkgPath</type></literal></term> + <listitem><para>A pathname that is relative to the package + directory. It may point to either a regular file or a directory. + See also <literal><type>RelativePkgDir</type></literal>.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>ShellCommand</type></literal></term> + <listitem><para>A shell command is similar to a + <literal><type>List of ShellWord</type></literal>, except that + additional checks are performed on the direct use of tool names + or certain other deprecated shell commands.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>ShellWord</type></literal></term> + <listitem><para>A shell word is what the shell would regard as a + single word.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>Stage</type></literal></term> + <listitem><para>In pkgsrc, there are phases, stages and steps. + Especially for the <varname>SUBST_STAGE</varname> variable, this + should always be one of the few predefined names, otherwise the + whole substitution group will be ignored.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>Tool</type></literal></term> + <listitem><para>The pkgsrc tools framework contains very few + plausibility checks. To prevent spelling mistakes, the list of + valid tool names is loaded from the pkgsrc infrastructure files + and compared with the names that are used in the + <varname>USE_TOOLS</varname> variable.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>URL</type></literal></term> + <listitem><para>URLs appear in <varname>MASTER_SITES</varname> + and the <varname>HOMEPAGE</varname>. If a + <varname>MASTER_SITES</varname> group exists for a given URL, it + should be used instead of listing the URL directly.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>UserGroupName</type></literal></term> + <listitem><para>User and group names should consist only of + alphanumeric characters and the underscore. This restriction + ensures maximum portability of pkgsrc.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>Userdefined</type></literal></term> + <listitem><para>Another instance of misuse of the type system. + But it helps to catch some errors in packages. This type will + disappear in the next version of the type system. See also + <literal><type>Readonly</type></literal>.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>Varname</type></literal></term> + <listitem><para>Variable names are restricted to only uppercase + letters and the underscore in the basename, and arbitrary + characters in the parameterized part, following the dot.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>WrkdirSubdirectory</type></literal></term> + <listitem><para>The variable <varname>WRKSRC</varname> is + usually defined with reference to <varname>WRKDIR</varname>. + This check currently does nothing, and I don't know if it's + worth to check anything here.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>WrksrcSubdirectory</type></literal></term> + <listitem><para>Subdirectories of <varname>WRKSRC</varname> can + be used in <varname>CONFIGURE_DIRS</varname> and some other + variables. For convenience, they are interpreted relative to + <varname>WRKSRC</varname>, so package authors don't have to type + <literal>${WRKSRC}</literal> all the time.</para> + </listitem></varlistentry> + + <varlistentry><term><literal><type>Yes</type></literal></term> + <listitem><para>This type is used for variables that are checked + using <literal>defined(VARNAME)</literal>. Their value is + interpreted as <quote>true</quote> if they are defined, no + matter if they are set to <literal>yes</literal> or + <literal>no</literal>.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>YesNo</type></literal></term> + <listitem><para>This type is used for variables that are checked + using <literal>defined(VARNAME) && + !empty(VARNAME:M[Yy][Ee][Ss])</literal>. A value of + <varname>no</varname> means <quote>no</quote> for + them.</para></listitem></varlistentry> + + <varlistentry><term><literal><type>YesNoFromCommand</type></literal></term> + <listitem><para>Like <literal><type>YesNo</type></literal>, but + the value may be produced by a shell command using the + <literal>!=</literal> operator.</para></listitem></varlistentry> + + </variablelist> + +</sect1> +<sect1 id="types.future"> +<title>Future directions</title> + +<sect2 id="types.kinds"> +<title>Different interpretation of the same data types</title> + + <para>As explained above, there are internal lists and external + lists in pkgsrc. But that is not the only attribute that a list + can have. They also differ in the way they are defined, which + files may access them, and what it means to append to append a + value to it.</para> + + <para>For example, <varname>NOT_FOR_PLATFORM</varname> is a list + that every file may append to without leading to unexpected + behavior. Compare this with + <varname>ONLY_FOR_PLATFORM</varname>, which should only be set + in a single place throughout pkgsrc. Let's say in the package + <filename>Makefile</filename> it is set to + <literal>NetBSD-*-*</literal>, because this file's author knows + for sure that the package is only usable on NetBSD. Now when + some <filename>*.mk</filename> file from a dependency package + adds <literal>DragonFly-*-*</literal> to it, the intent of the + package <filename>Makefile</filename> is undermined by the + dependency package, because now it is possible to build the + package on DragonFly, too.</para> + + <para>The same problem arises with the various variables that + can be either <literal>yes</literal> or undefined. They should + always be chosen so that two definitions in different files + don't undermine each other. A good example is + <varname>USE_LIBTOOL</varname>, a bad example is + <varname>NO_BUILD</varname>.</para> + + <para>TODO: What are the general properties of + <quote>good</quote> and <quote>bad</quote> variables? How can it + be decided of which kind a certain variable is?</para> + + <para>For most lists, the only valid operation is to append + something at the end. Therefore it is good practice to warn if a + list is assigned using another operator that + <literal>+=</literal>. For <varname>SUBST_CLASSES</varname> this + fits perfectly. But for <varname>SUBST_FILES.*</varname> it + doesn't. Usually all occurences of a + <varname>SUBST_FILES.*</varname> variable occur in the same + file, and there should be no other file modifying these + variables. Therefore it is better to use the + <literal>=</literal> operator for the first of the + assignments.</para> + +</sect2> +</sect1> +</chapter> diff --git a/pkgtools/pkglint4/files/doc/pkglint.xml b/pkgtools/pkglint4/files/doc/pkglint.xml new file mode 100644 index 00000000000..d49282e747d --- /dev/null +++ b/pkgtools/pkglint4/files/doc/pkglint.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" + "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd" +[ + <!ENTITY pkglint "<literal>pkglint</literal>"> + <!ENTITY pkgsrc-guide "http://www.NetBSD.org/docs/pkgsrc"> + + <!ENTITY chap.intro SYSTEM "chap.intro.xml"> + <!ENTITY chap.defs SYSTEM "chap.defs.xml"> + <!ENTITY chap.design SYSTEM "chap.design.xml"> + <!ENTITY chap.types SYSTEM "chap.types.xml"> + <!ENTITY chap.code SYSTEM "chap.code.xml"> + <!ENTITY chap.statemachines SYSTEM "chap.statemachines.xml"> + <!ENTITY chap.future SYSTEM "chap.future.xml"> +]> + +<!-- $NetBSD: pkglint.xml,v 1.1 2015/11/25 16:42:21 rillig Exp $ --> + +<book> +<title>Design and implementation of &pkglint;</title> + +<bookinfo> +<author> + <firstname>Roland</firstname> + <surname>Illig</surname> + <email>rillig@NetBSD.org</email> +</author> +</bookinfo> + +&chap.intro; +&chap.defs; +&chap.design; +&chap.types; +&chap.code; +&chap.statemachines; +&chap.future; + +</book> diff --git a/pkgtools/pkglint4/files/doc/statemachine.patch.dia b/pkgtools/pkglint4/files/doc/statemachine.patch.dia Binary files differnew file mode 100644 index 00000000000..bf1aac9919d --- /dev/null +++ b/pkgtools/pkglint4/files/doc/statemachine.patch.dia diff --git a/pkgtools/pkglint4/files/doc/statemachine.shellcmd.dia b/pkgtools/pkglint4/files/doc/statemachine.shellcmd.dia Binary files differnew file mode 100644 index 00000000000..76b7442f210 --- /dev/null +++ b/pkgtools/pkglint4/files/doc/statemachine.shellcmd.dia diff --git a/pkgtools/pkglint4/files/doc/stylesheet.xsl b/pkgtools/pkglint4/files/doc/stylesheet.xsl new file mode 100644 index 00000000000..078cb37470d --- /dev/null +++ b/pkgtools/pkglint4/files/doc/stylesheet.xsl @@ -0,0 +1,6 @@ +<!-- $NetBSD: stylesheet.xsl,v 1.1 2015/11/25 16:42:21 rillig Exp $ --> + +<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + version="1.0"> + <xsl:param name="html.longdesc" select="0"/> +</xsl:stylesheet> diff --git a/pkgtools/pkglint4/files/makevars.map b/pkgtools/pkglint4/files/makevars.map new file mode 100644 index 00000000000..d2bd19afcd7 --- /dev/null +++ b/pkgtools/pkglint4/files/makevars.map @@ -0,0 +1,768 @@ +# $NetBSD: makevars.map,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# + +# This file contains the guessed type of some variables, according to +# their current use in pkgsrc. +# +# The type YesNo is used for variables that are checked using +# .if defined(VAR) && !empty(VAR:M[Yy][Ee][Ss]) +# +# The type Yes is used for variables that are checked using +# .if defined(VAR) +# +# The type List is used for lists of things. There are two types of lists, +# InternalList and List, which are described in the pkgsrc guide, chapter +# "Makefiles". +# +# The other types are described in pkglint.pl, checkline_mk_vartype_basic. +# + +# +# Some commonly used ACLs. For further documentation, see the chapter +# ``The pkglint type system'' in the pkglint developer documentation. +# + +# A package-defined variable may be set in Makefile, Makefile.common or +# options.mk, since these are commonly used in pkgsrc. Due to the +# special nature of buildlink3.mk and builtin.mk files, these files may +# not define those variables. All other .mk files may, allowing for +# application.mk or similar helper files. +acl package = [m:su, c:dsu, b:, builtin.mk:, *.mk:dsu] + +# A package_list may be appended to in all "normal" Makefile fragments. +# This excludes buildlink3.mk and builtin.mk, since they are very +# special-purpose. Because at the beginning, all lists are empty, in the +# primary Makefile a direct assignment may be used instead of appending. +# Since Makefile.common files are usually used by packages that know +# what they are doing, they may set variables directly, too. This rule +# can be removed to show a lot of possible problems in pkgsrc. +acl package_list = [m:asu, c:asu, b:, builtin.mk:, *.mk:au] +acl bl_list = [b:a, builtin.mk:a] + +# A user-defined or system-defined variable must not be set by any +# package file. It also must not be used in buildlink3.mk and +# builtin.mk files or at load-time, since the system/user preferences +# may not have been loaded when these files are included. +acl user = [b:, builtin.mk:, *:u] +acl system = [b:, builtin.mk:u, *:u] +acl cmdline = [b:, builtin.mk:, *:pu] + +# The following variables are taken from mk/defaults/mk.conf, 1.118 + +ALLOW_VULNERABLE_PACKAGES Yes [$user] +MANINSTALL List of { maninstall catinstall } [$user] +MANZ Yes [$user] +GZIP List of ShellWord [$user] +MKCRYPTO YesNo [$user] +OBJHOSTNAME Yes [$user] +OBJMACHINE Yes [$user] +PKG_SUFX Filename [$user] +PKGSRC_LOCKTYPE { none sleep once } [$user] +PKGSRC_SLEEPSECS Integer [$user] +USETBL Yes [$user] +ABI { 32 64 } [$user] +PKG_DEVELOPER Yes [$user] +USE_ABI_DEPENDS YesNo [$user] +PKG_REGISTER_SHELLS { YES NO } [$user] +PKGSRC_COMPILER List of { \ + ccache ccc clang distcc f2c gcc hp icc \ + ido gcc mipspro mipspro-ucode pcc \ + sunpro xlc \ + } [$user] +PKGSRC_MESSAGE_RECIPIENTS List of Mail_Address [$user] +PKGSRC_SHOW_BUILD_DEFS YesNo [$user] +PKGSRC_SHOW_PATCH_ERRORMSG YesNo [$user] +PKGSRC_RUN_TEST YesNo [$user] +PREFER_PKGSRC List of Identifier [$user] +PREFER_NATIVE List of Identifier [$user] +PREFER_NATIVE_PTHREADS YesNo [$user] +LOCALBASE Pathname [$user] +CROSSBASE Pathname [$user] +VARBASE Pathname [$user] +X11_TYPE { modular native } [$user] +X11BASE Pathname [$user] +MOTIFBASE Pathname [$user] +PKGINFODIR Pathname [$user] +PKGMANDIR Pathname [$user] +USE_XPKGWEDGE YesNo [$user] +BSDSRCDIR Pathname [$user] +BSDXSRCDIR Pathname [$user] +DISTDIR Pathname [$user] +DIST_PATH Pathlist [$user] +DEFAULT_VIEW Unchecked [$user] +FETCH_CMD ShellCommand [$user] +FETCH_USING { curl custom fetch ftp manual wget } [$user] +FETCH_RESUME_ARGS List of ShellWord [$user] +FETCH_OUTPUT_ARGS List of ShellWord [$user] +LIBTOOLIZE_PLIST YesNo [$user] +PKG_RESUME_TRANSFERS YesNo [$user] +PKG_SYSCONFBASE Pathname [$user] +RCD_SCRIPTS_DIR Pathname [$user] +PACKAGES Pathname [$user] +PKGVULNDIR Pathname [$user] +PASSIVE_FETCH Yes [$user] +PATCH_FUZZ_FACTOR { -F0 -F1 -F2 -F3 } [$user] +ACCEPTABLE_LICENSES List of Identifier [$user] +SPECIFIC_PKGS Yes [$user] +SITE_SPECIFIC_PKGS List of PkgPath [$user] +HOST_SPECIFIC_PKGS List of PkgPath [$user] +GROUP_SPECIFIC_PKGS List of PkgPath [$user] +USER_SPECIFIC_PKGS List of PkgPath [$user] +FAILOVER_FETCH Yes [$user] +MASTER_SORT List of Unchecked [$user] +MASTER_SORT_REGEX List of Unchecked [$user] +PATCH_DEBUG Yes [$user] +PKG_FC ShellCommand [$user] +IMAKE ShellCommand [$user] +IMAKEOPTS List of ShellWord [$user] +PRE_ROOT_CMD ShellCommand [$user] +USE_GAMESGROUP YesNo [$package] +SU_CMD ShellCommand [$user] +SU_CMD_PATH_APPEND Pathlist [$user] +FATAL_OBJECT_FMT_SKEW YesNo [$user] +WARN_NO_OBJECT_FMT YesNo [$user] +SMART_MESSAGES Yes [$user] +BINPKG_SITES List of URL [$user] +BIN_INSTALL_FLAG List of ShellWord [$user] +LOCALPATCHES Pathname [$user] + +# some other variables, sorted alphabetically + +.CURDIR Pathname [$system] +.TARGET Pathname [$system] +ALL_ENV List of ShellWord +ALTERNATIVES_FILE Filename +ALTERNATIVES_SRC List of Pathname +APACHE_MODULE Yes [$package] +AR ShellCommand [$system] +AS ShellCommand [$system] +AUTOCONF_REQD List of Version [$package_list] +AUTOMAKE_OVERRIDE List of Pathmask +AUTOMAKE_REQD List of Version [$package_list] +AUTO_MKDIRS YesNo [$package] +BATCH Yes [$user] +BDB185_DEFAULT Unchecked [] +BDBBASE Pathname [$system] +BDB_ACCEPTED List of { db1 db2 db3 db4 db5 } [$package] +BDB_DEFAULT { db1 db2 db3 db4 db5 } [] +BDB_LIBS List of LdFlag [$system] +BDB_TYPE { db1 db2 db3 db4 db5 } [$system] +BINGRP UserGroupName [$system] +BINMODE FileMode [$system] +BINOWN UserGroupName [$system] +BOOTSTRAP_DEPENDS InternalList of DependencyWithPath [c:a,m:a,o:a,*.mk:a] +BOOTSTRAP_PKG YesNo [$package] +BROKEN Message [] +BROKEN_GETTEXT_DETECTION YesNo [$package] +BROKEN_EXCEPT_ON_PLATFORM List of PlatformTriple [$package_list] +BROKEN_ON_PLATFORM InternalList of PlatformTriple [$package_list] +BSD_MAKE_ENV List of ShellWord [$system] +BUILDLINK_ABI_DEPENDS.* InternalList of Dependency [*:a] +BUILDLINK_API_DEPENDS.* InternalList of Dependency [*:a] +BUILDLINK_CONTENTS_FILTER List of ShellWord [] +# ^^ ShellCommand +BUILDLINK_CFLAGS List of CFlag [$system] +BUILDLINK_CFLAGS.* List of CFlag [$bl_list] +BUILDLINK_CPPFLAGS List of CFlag [$system] +BUILDLINK_CPPFLAGS.* List of CFlag [$bl_list] +BUILDLINK_CONTENTS_FILTER.* ShellCommand [b:s] +BUILDLINK_DEPENDS InternalList of Identifier [b:a] +BUILDLINK_DEPMETHOD.* List of BuildlinkDepmethod [b:ad,m:as,c:a,*.mk:a] +# ^^ FIXME: b:d may lead to unexpected behavior. +BUILDLINK_DEPTH BuildlinkDepth [b:ps, builtin.mk:ps] +BUILDLINK_DIR Pathname [$system] +BUILDLINK_FILES.* List of Pathmask [$bl_list] +BUILDLINK_FILES_CMD.* List of ShellWord [] +# ^^ ShellCommand +BUILDLINK_INCDIRS.* List of Pathname [b:ad] # b:d? +BUILDLINK_JAVA_PREFIX.* Pathname [b:s] +BUILDLINK_LDADD.* List of LdFlag [builtin.mk:adsu, b:, m:u, c:u, *.mk:u] +BUILDLINK_LDFLAGS List of LdFlag [$system] +BUILDLINK_LDFLAGS.* List of LdFlag [$bl_list] +BUILDLINK_LIBDIRS.* List of Pathname [$bl_list] +BUILDLINK_LIBS.* List of LdFlag [b:a] +BUILDLINK_PACKAGES BuildlinkPackages [b:aps] +BUILDLINK_PASSTHRU_DIRS List of Pathname [m:a,c:a,b:a,h:a] +BUILDLINK_PASSTHRU_RPATHDIRS List of Pathname [m:a,c:a,b:a,h:a] +BUILDLINK_PKGSRCDIR.* RelativePkgDir [b:dp] +BUILDLINK_PREFIX.* Pathname [builtin.mk:su, b:, m:u, c:u, *.mk:u] +BUILDLINK_RPATHDIRS.* List of Pathname [b:a] +BUILDLINK_TARGETS List of Identifier [] +BUILDLINK_FNAME_TRANSFORM.* SedCommands [m:a,builtin.mk:a,h:a,b:a] +BUILDLINK_TRANSFORM List of WrapperTransform [*:a] +BUILDLINK_TREE List of Identifier [b:a] +BUILD_DEFS List of Varname [m:a,c:a,o:a] +BUILD_DEPENDS InternalList of DependencyWithPath [c:a,m:a,o:a,*.mk:a] +BUILD_DIRS List of WrksrcSubdirectory [$package_list] +BUILD_ENV List of ShellWord [$package_list] +BUILD_MAKE_CMD ShellCommand [$system] +BUILD_MAKE_FLAGS List of ShellWord [$package_list] +BUILD_TARGET List of Identifier [$package] +BUILD_USES_MSGFMT Yes [$package] +BUILTIN_PKG Identifier [builtin.mk:psu] +BUILTIN_PKG.* PkgName [builtin.mk:psu] +BUILTIN_FIND_FILES_VAR List of Varname [builtin.mk:s] +BUILTIN_FIND_FILES.* List of Pathname [builtin.mk:s] +BUILTIN_FIND_GREP.* String [builtin.mk:s] +BUILTIN_FIND_LIBS List of Pathname [builtin.mk:s] +BUILTIN_IMAKE_CHECK List of Unchecked [builtin.mk:s] +BUILTIN_IMAKE_CHECK.* YesNo [] +BUILTIN_X11_TYPE Unchecked [$system] +BUILTIN_X11_VERSION Unchecked [$system] +CATEGORIES List of Category [m:as,c:ads] +CC_VERSION Message [$system] +CC ShellCommand [$system] +CFLAGS* List of CFlag [$package_list] +# ^^ may also be changed by the user +CHECK_BUILTIN YesNo [builtin.mk:d,m:s] +CHECK_BUILTIN.* YesNo [*:p] +CHECK_FILES_SKIP List of Pathmask [m:a,c:a] +CHECK_FILES_SUPPORTED YesNo [$package] +CHECK_HEADERS YesNo [$user] +CHECK_HEADERS_SKIP List of Pathmask [$package_list] +CHECK_INTERPRETER YesNo [$user] +CHECK_INTERPRETER_SKIP List of Pathmask [$package_list] +CHECK_PERMS YesNo [$user] +CHECK_PERMS_SKIP List of Pathmask [$package_list] +#CHECK_PERMS_AUTOFIX YesNo [$package] +# ^^ experimental +CHECK_PORTABILITY YesNo [$user] +CHECK_PORTABILITY_SKIP List of Pathmask [$package_list] +CHECK_SHLIBS YesNo [m:s] +CHECK_SHLIBS_SKIP List of Pathmask [$package_list] +CHECK_SHLIBS_SUPPORTED YesNo [m:s] +CHECK_WRKREF_SKIP List of Pathmask [$package_list] +CMAKE_ARG_PATH Pathname [$package] +CMAKE_ARGS List of ShellWord [$package_list] +COMMENT Comment [m:as,c:as] +COMPILER_RPATH_FLAG { -Wl,-rpath } [$system] +CONFIGURE_ARGS List of ShellWord [$package_list] +CONFIGURE_DIRS List of WrksrcSubdirectory [$package_list] +CONFIGURE_ENV List of ShellWord [$package_list] +CONFIGURE_HAS_INFODIR YesNo [$package] +CONFIGURE_HAS_LIBDIR YesNo [$package] +CONFIGURE_HAS_MANDIR YesNo [$package] +CONFIGURE_SCRIPT Pathname [$package] +CONFIG_GUESS_OVERRIDE List of Pathmask [m:as,c:as] +CONFIG_STATUS_OVERRIDE List of Pathmask [m:as,c:as] +CONFIG_SHELL Pathname [m:s,c:s] +CONFIG_SUB_OVERRIDE List of Pathmask [m:as,c:as] +CONFLICTS InternalList of Dependency [$package_list] +CONF_FILES List of ShellWord [$package_list] +CONF_FILES_MODE { 0644 0640 0600 0400 } [$package] +CONF_FILES_PERMS List of ShellWord [$package_list] +COPY { -c } [$system] +# ^^ the flag that tells ${INSTALL} to copy a file +CPP ShellCommand [$system] +CPPFLAGS* List of CFlag [$package_list] +CRYPTO Yes [m:s] +CXX ShellCommand [$system] +CXXFLAGS* List of CFlag [$package_list] +DEINSTALL_FILE Pathname [m:s] +DEINSTALL_SRC List of Pathname [m:s,c:ds] +DEINSTALL_TEMPLATES List of Pathname [m:as,c:ads] +DELAYED_ERROR_MSG ShellCommand [$system] +DELAYED_WARNING_MSG ShellCommand [$system] +DEPENDS InternalList of DependencyWithPath [$package_list] +DEPENDS_TARGET List of Identifier [$user] +DESCR_SRC List of Pathname [m:s,c:ds] +DESTDIR Pathname [$system] +DESTDIR_VARNAME Varname [m:s,c:s] +DEVOSSAUDIO Pathname [$system] +DEVOSSSOUND Pathname [$system] +DISTFILES List of Filename [$package_list] +DISTINFO_FILE RelativePkgPath [$package] +DISTNAME Filename [$package] +DIST_SUBDIR Pathname [$package] +DJB_BUILD_ARGS List of ShellWord +DJB_BUILD_TARGETS List of Identifier +DJB_CONFIG_CMDS List of ShellWord [o:s] +# ^^ ShellCommand, terminated by a semicolon +DJB_CONFIG_DIRS List of WrksrcSubdirectory +DJB_CONFIG_HOME Filename +DJB_CONFIG_PREFIX Pathname +DJB_INSTALL_TARGETS List of Identifier +DJB_MAKE_TARGETS YesNo +DJB_RESTRICTED YesNo [m:s] +DJB_SLASHPACKAGE YesNo +DLOPEN_REQUIRE_PTHREADS YesNo +DL_AUTO_VARS Yes [m:s,c:s,o:s] +DL_LIBS List of LdFlag +DOCOWN UserGroupName [$system] +DOCGRP UserGroupName [$system] +DOCMODE FileMode [$system] +DOWNLOADED_DISTFILE Pathname [$system] +DO_NADA ShellCommand [$system] +DYNAMIC_SITES_CMD ShellCommand [$package] +DYNAMIC_SITES_SCRIPT Pathname [$package] +ECHO ShellCommand [$system] +ECHO_MSG ShellCommand [$system] +ECHO_N ShellCommand [$system] +EGDIR Pathname [$package] +# ^^ This variable is not defined by the system, but has been established +# as a convention. +EMACS_BIN Pathname [$system] +EMACS_ETCPREFIX Pathname [$system] +EMACS_FLAVOR { emacs xemacs } [$system] +EMACS_INFOPREFIX Pathname [$system] +EMACS_LISPPREFIX Pathname [$system] +EMACS_MODULES List of Identifier [m:as,c:as] +EMACS_PKGNAME_PREFIX Identifier [$system] +# ^^ or the empty string. +EMACS_TYPE { emacs xemacs } [$system] +EMACS_USE_LEIM Yes +EMACS_VERSIONS_ACCEPTED List of { emacs25 emacs24 emacs24nox emacs23 emacs23nox emacs22 emacs22nox emacs21 emacs21nox emacs20 xemacs215 xemacs215nox xemacs214 xemacs214nox} [m:s] +EMACS_VERSION_MAJOR Integer [$system] +EMACS_VERSION_MINOR Integer [$system] +EMACS_VERSION_REQD List of { emacs24 emacs24nox emacs23 emacs23nox emacs22 emacs22nox emacs21 emacs21nox emacs20 xemacs215 xemacs214 } [m:as] +EMULDIR Pathname [$system] +EMULSUBDIR Pathname [$system] +OPSYS_EMULDIR Pathname [$system] +EMULSUBDIRSLASH Pathname [$system] +EMUL_ARCH { i386 none } [$system] +EMUL_DISTRO Identifier [$system] +EMUL_IS_NATIVE Yes [$system] +EMUL_MODULES.* List of Identifier [$package] +EMUL_OPSYS { freebsd hpux irix linux osf1 solaris sunos none } [$system] +EMUL_PKG_FMT { plain rpm } [$package] +EMUL_PLATFORM EmulPlatform [$user] +EMUL_PLATFORMS List of EmulPlatform [$package] +EMUL_PREFER List of EmulPlatform [$user] +EMUL_REQD InternalList of Dependency [$package] +EMUL_TYPE.* { native builtin suse suse-9.1 suse-9.x suse-10.0 suse-10.x } [$user] +ERROR_CAT ShellCommand [$system] +ERROR_MSG ShellCommand [$system] +EVAL_PREFIX InternalList of ShellWord [m:a,c:a] +# ^^ FIXME: Looks like a type mismatch. +EXPORT_SYMBOLS_LDFLAGS List of LdFlag [$system] +EXTRACT_CMD ShellCommand [$system] +EXTRACT_DIR Pathname [$package] +EXTRACT_ELEMENTS List of Pathmask [$package_list] +EXTRACT_ENV List of ShellWord [$package_list] +EXTRACT_ONLY List of Pathname [$package_list] +EXTRACT_OPTS List of ShellWord [m:as,c:as] +EXTRACT_OPTS_BIN List of ShellWord [m:as,c:as] +EXTRACT_OPTS_LHA List of ShellWord [m:as,c:as] +EXTRACT_OPTS_PAX List of ShellWord [m:as,c:as] +EXTRACT_OPTS_RAR List of ShellWord [m:as,c:as] +EXTRACT_OPTS_TAR List of ShellWord [m:as,c:as] +EXTRACT_OPTS_ZIP List of ShellWord [m:as,c:as] +EXTRACT_OPTS_ZOO List of ShellWord [m:as,c:as] +EXTRACT_SUFX DistSuffix [$package] +EXTRACT_USING { bsdtar gtar nbtar pax } [$package] +FAIL_MSG ShellCommand [$system] +FAMBASE Pathname [$system] +FAM_ACCEPTED List of { fam gamin } [$package] +FAM_DEFAULT { fam gamin } [$user] +FAM_TYPE { fam gamin } [$system] +FETCH_BEFORE_ARGS List of ShellWord [m:as] +FETCH_MESSAGE List of ShellWord [$package_list] +FILESDIR RelativePkgPath [$package] +FILES_SUBST List of ShellWord [$package_list] +FILES_SUBST_SED List of ShellWord +FIX_RPATH List of Varname [$package_list] +FLEX_REQD List of Version [$package_list] +FONTS_DIRS.* List of Pathname [m:as,c:a] +GAMEDATAMODE FileMode [$system] +GAMES_GROUP UserGroupName [$system] +GAMEMODE FileMode [$system] +GAMES_USER UserGroupName [$system] +GCC_REQD List of Version [$package_list] +GENERATE_PLIST List of ShellWord [$package_list] +# ^^ List of Shellcommand, terminated with a semicolon +GITHUB_PROJECT Identifier [$package] +GITHUB_TAG Identifier [$package] +GITHUB_RELEASE Filename [$package] +GITHUB_TYPE { tag release } [$package] +GNU_ARCH { mips } +GNU_CONFIGURE Yes [c:s,m:s] +GNU_CONFIGURE_INFODIR Pathname [m:s,c:s] +GNU_CONFIGURE_LIBDIR Pathname [m:s,c:s] +GNU_CONFIGURE_LIBSUBDIR Pathname [$package] +GNU_CONFIGURE_MANDIR Pathname [m:s,c:s] +GNU_CONFIGURE_PREFIX Pathname [m:s] +HAS_CONFIGURE Yes [m:s,c:s] +HEADER_TEMPLATES List of Pathname [$package_list] +HOMEPAGE URL [$package] +IGNORE_PKG.* Yes [*:sp] +INCOMPAT_CURSES InternalList of PlatformTriple [m:as] +INCOMPAT_ICONV InternalList of PlatformTriple +INFO_DIR Pathname +# ^^ relative to PREFIX +INFO_FILES Yes [$package] +INSTALL ShellCommand [$system] +INSTALLATION_DIRS List of PrefixPathname [$package_list] +INSTALLATION_DIRS_FROM_PLIST Yes [$package] +INSTALL_DATA ShellCommand [$system] +INSTALL_DATA_DIR ShellCommand [$system] +INSTALL_DIRS List of WrksrcSubdirectory [$package_list] +INSTALL_ENV List of ShellWord [$package_list] +INSTALL_FILE Pathname [m:s] +INSTALL_GAME ShellCommand [$system] +INSTALL_GAME_DATA ShellCommand [$system] +INSTALL_LIB ShellCommand [$system] +INSTALL_LIB_DIR ShellCommand [$system] +INSTALL_MAKE_FLAGS List of ShellWord [$package_list] +INSTALL_MAN ShellCommand [$system] +INSTALL_MAN_DIR ShellCommand [$system] +INSTALL_PROGRAM ShellCommand [$system] +INSTALL_PROGRAM_DIR ShellCommand [$system] +INSTALL_SCRIPT ShellCommand [$system] +INSTALL_SCRIPTS_ENV List of ShellWord +INSTALL_SCRIPT_DIR ShellCommand [$system] +INSTALL_SRC List of Pathname [m:s,c:ds] +INSTALL_TARGET List of Identifier [$package] +INSTALL_TEMPLATES List of Pathname [m:as,c:ads] +INSTALL_UNSTRIPPED YesNo [m:s,c:s] +INTERACTIVE_STAGE List of { fetch extract configure build install } [$package] +IS_BUILTIN.* YesNo_Indirectly [builtin.mk:psu] +JAVA_BINPREFIX Pathname [$system] +JAVA_CLASSPATH ShellWord [$package] +JAVA_HOME Pathname [$package] +JAVA_NAME Filename [$package] +JAVA_UNLIMIT List of { cmdsize datasize stacksize } [$package_list] +JAVA_WRAPPERS InternalList of Filename [$package_list] +JAVA_WRAPPER_BIN.* Pathname [$package] +KRB5BASE Pathname [$system] +KRB5_ACCEPTED List of { heimdal mit-krb5 } +KRB5_DEFAULT { heimdal mit-krb5 } [$user] +KRB5_TYPE Unchecked [$system] +LD ShellCommand [$system] +LDFLAGS* List of LdFlag [$package_list] +LIBGRP UserGroupName [$system] +LIBMODE FileMode [$system] +LIBOWN UserGroupName [$system] +LIBOSSAUDIO Pathname [$system] +LIBS* List of LdFlag [$package_list] +LIBTOOL ShellCommand [$system] +LIBTOOL_OVERRIDE List of Pathmask [m:as] +LIBTOOL_REQD List of Version [$package_list] +LICENCE License [m:s,c:s,o:s] +LICENSE License [m:s,c:s,o:s] +LICENSE_FILE Pathname [$package] +LINKER_RPATH_FLAG ShellWord [$system] +LOWER_OPSYS Identifier [$system] +LTCONFIG_OVERRIDE List of Pathmask [m:as,c:a] +MACHINE_ARCH Identifier [$system] +MACHINE_GNU_PLATFORM PlatformTriple [$system] +MAINTAINER Mail_Address [m:s,c:d] +MAKE ShellCommand [$system] +MAKEFLAGS List of ShellWord [$package_list] +MAKEVARS List of Varname [builtin.mk:a,b:a,h:a] +MAKE_DIRS List of Pathname [$package_list] +MAKE_DIRS_PERMS List of ShellWord [$package_list] +MAKE_ENV List of ShellWord [$package_list] +MAKE_FILE Pathname [$package] +MAKE_FLAGS List of ShellWord [$package_list] +MAKE_JOBS Integer [$user] +MAKE_JOBS_SAFE YesNo [$package] +MAKE_PROGRAM ShellCommand [$package] +MANCOMPRESSED YesNo [m:s,c:ds] +MANCOMPRESSED_IF_MANZ Yes [m:s,c:ds] +MANGRP UserGroupName [$system] +MANMODE FileMode [$system] +MANOWN UserGroupName [$system] +MASTER_SITES List of FetchURL [$package_list] +MASTER_SITE_APACHE List of FetchURL [$system] +MASTER_SITE_BACKUP List of FetchURL [$system] +MASTER_SITE_CYGWIN List of FetchURL [$system] +MASTER_SITE_DEBIAN List of FetchURL [$system] +MASTER_SITE_FREEBSD List of FetchURL [$system] +MASTER_SITE_FREEBSD_LOCAL List of FetchURL [$system] +MASTER_SITE_GENTOO List of FetchURL [$system] +MASTER_SITE_GITHUB List of FetchURL [$system] +MASTER_SITE_GNOME List of FetchURL [$system] +MASTER_SITE_GNU List of FetchURL [$system] +MASTER_SITE_GNUSTEP List of FetchURL [$system] +MASTER_SITE_IFARCHIVE List of FetchURL [$system] +MASTER_SITE_HASKELL_HACKAGE List of FetchURL [$system] +MASTER_SITE_KDE List of FetchURL [$system] +MASTER_SITE_LOCAL List of FetchURL [$system] +MASTER_SITE_MOZILLA List of FetchURL [$system] +MASTER_SITE_MOZILLA_ALL List of FetchURL [$system] +MASTER_SITE_MOZILLA_ESR List of FetchURL [$system] +MASTER_SITE_MYSQL List of FetchURL [$system] +MASTER_SITE_NETLIB List of FetchURL [$system] +MASTER_SITE_OPENOFFICE List of FetchURL [$system] +MASTER_SITE_OSDN List of FetchURL [$system] +MASTER_SITE_PERL_CPAN List of FetchURL [$system] +MASTER_SITE_R_CRAN List of FetchURL [$system] +MASTER_SITE_RUBYGEMS List of FetchURL [$system] +MASTER_SITE_SOURCEFORGE List of FetchURL [$system] +MASTER_SITE_SUNSITE List of FetchURL [$system] +MASTER_SITE_SUSE List of FetchURL [$system] +MASTER_SITE_TEX_CTAN List of FetchURL [$system] +MASTER_SITE_XCONTRIB List of FetchURL [$system] +MASTER_SITE_XEMACS List of FetchURL [$system] +MESSAGE_SRC List of Pathname [$package_list] +MESSAGE_SUBST List of ShellWord [c:a,m:a,o:a] +META_PACKAGE Yes [$package] +MISSING_FEATURES List of Identifier [$system] +MYSQL_VERSIONS_ACCEPTED List of { 51 55 56 } [m:s] +MYSQL_VERSION_DEFAULT Version [$user] +NM ShellCommand [$system] +NONBINMODE FileMode [$system] +NOT_FOR_COMPILER List of { ccache ccc clang distcc f2c gcc hp icc ido mipspro mipspro-ucode pcc sunpro xlc } [$package] +NOT_FOR_PLATFORM InternalList of PlatformTriple [$package_list] +NOT_FOR_UNPRIVILEGED YesNo [$package] +NO_BIN_ON_CDROM Restricted [m:s,c:s] +NO_BIN_ON_FTP Restricted [m:s,c:s] +NO_BUILD Yes [m:s,c:s,Makefile.*:ds] +NO_CHECKSUM Yes [$package] +NO_CONFIGURE Yes [$package] +NO_EXPORT_CPP Yes [m:s] +NO_EXTRACT Yes [$package] +NO_INSTALL_MANPAGES Yes [$package] +# ^^ only has an effect for Imake packages. +NO_PKGTOOLS_REQD_CHECK Yes [m:s] +NO_SRC_ON_CDROM Restricted [m:s,c:s] +NO_SRC_ON_FTP Restricted [m:s,c:s] +ONLY_FOR_COMPILER List of { ccc clang gcc hp icc ido mipspro mipspro-ucode pcc sunpro xlc } [$package_list] +ONLY_FOR_PLATFORM InternalList of PlatformTriple [$package_list] +ONLY_FOR_UNPRIVILEGED YesNo [$package] +OPSYS Identifier [$system] +OPSYSVARS List of Varname [m:a,c:a] +OSVERSION_SPECIFIC Yes [m:s,c:s] +OS_VERSION Version [$system] +OVERRIDE_DIRDEPTH* Integer [$package] +OVERRIDE_GNU_CONFIG_SCRIPTS Yes [$package] +OWNER Mail_Address [m:s,c:d] +OWN_DIRS List of Pathname [$package_list] +OWN_DIRS_PERMS List of ShellWord [$package_list] +PAMBASE Pathname [$system] +PAM_DEFAULT { linux-pam openpam solaris-pam } [$user] +PATCHDIR RelativePkgPath [m:s,c:ds] +PATCHFILES List of Filename [$package_list] +PATCH_ARGS List of ShellWord +PATCH_DIST_ARGS List of ShellWord [m:as] +PATCH_DIST_CAT ShellCommand +PATCH_DIST_STRIP* ShellWord [m:s, c:s, b:, builtin.mk:, *.mk:s] +PATCH_SITES List of URL [m:s,o:s,c:s] +PATCH_STRIP ShellWord +PERL5_USE_PACKLIST YesNo [$package] +PERL5_PACKLIST List of Perl5Packlist [m:s,o:sa] +PERL5_PACKLIST_DIR Pathname [] +PGSQL_PREFIX Pathname [$system] +PGSQL_VERSIONS_ACCEPTED List of { 91 92 93 94} +PGSQL_VERSION_DEFAULT Version [$user] +PG_LIB_EXT { dylib so } [$system] +PGSQL_TYPE { postgresql81-client postgresql80-client } [$system] +PGPKGSRCDIR Pathname [$system] +PHASE_MSG ShellCommand [$system] +PHP_VERSION_REQD Version [$user] +PKGBASE Identifier [$system] +PKGCONFIG_OVERRIDE List of Pathmask [m:as,c:a] +PKGCONFIG_OVERRIDE_STAGE Stage [$package] +PKGDIR RelativePkgDir [$package] +PKGDIRMODE FileMode [$system] +PKGLOCALEDIR Pathname [$system] +PKGNAME PkgName [$package] +PKGNAME_NOREV PkgName [$system] +PKGPATH Pathname [$system] +PKGREPOSITORY Unchecked [] +PKGREVISION PkgRevision [m:s] +PKGSRCDIR Pathname [$system] +PKGSRCTOP Yes [m:s] +PKGTOOLS_ENV List of ShellWord +PKGVERSION Version [$system] +PKGWILDCARD Filemask [$system] +PKG_ADMIN ShellCommand [$system] +PKG_APACHE { apache22 apache24 } [$system] +PKG_APACHE_ACCEPTED List of { apache22 apache24 } [$package] +PKG_APACHE_DEFAULT { apache22 apache24 } [$user] +PKG_CONFIG Yes [$user] +# ^^ No, this is not the popular command from GNOME, but the setting +# whether the pkgsrc user wants configuration files automatically +# installed or not. +PKG_CREATE ShellCommand [$system] +PKG_DBDIR Pathname [$system] +PKG_DEBUG_LEVEL Integer [$cmdline] +PKG_DEFAULT_OPTIONS List of Option [$user] +PKG_DELETE ShellCommand [$system] +PKG_DESTDIR_SUPPORT List of { destdir user-destdir } [m:s,c:s] +PKG_FAIL_REASON List of ShellWord [$package_list] +PKG_GECOS.* Message [m:s] +PKG_GID.* Integer [m:s] +PKG_GROUPS List of ShellWord [m:as] +PKG_GROUPS_VARS List of Varname [$package_list] +PKG_HOME.* Pathname [m:s] +PKG_HACKS List of Identifier [h:a] +PKG_INFO ShellCommand [$system] +PKG_JAVA_HOME Pathname [$system] +PKG_JVM { \ + blackdown-jdk13 \ + jdk jdk14 \ + kaffe \ + sun-jdk13 sun-jdk14 sun-jdk15 sun-jdk6 \ + openjdk7 openjdk7-bin sun-jdk7 \ + } [$system] +PKG_JVMS_ACCEPTED List of { \ + blackdown-jdk13 \ + jdk jdk14 \ + kaffe \ + sun-jdk13 sun-jdk14 sun-jdk15 sun-jdk6 \ + openjdk7 openjdk7-bin sun-jdk7 \ + } [m:s,c:ds] +PKG_JVM_DEFAULT { \ + blackdown-jdk13 \ + jdk jdk14 \ + kaffe \ + sun-jdk13 sun-jdk14 sun-jdk15 sun-jdk6 \ + openjdk7 openjdk7-bin sun-jdk7 \ + } [$user] +PKG_LEGACY_OPTIONS List of Option +PKG_LIBTOOL Pathname [m:s] +PKG_OPTIONS InternalList of Option [bsd.options.mk:s,*:pu] +PKG_OPTIONS.* InternalList of Option [$user] +PKG_OPTIONS_DEPRECATED_WARNINGS List of ShellWord +PKG_OPTIONS_GROUP.* InternalList of Option [o:s,m:s] +PKG_OPTIONS_LEGACY_OPTS InternalList of Unchecked [m:a,c:a,o:a] +PKG_OPTIONS_LEGACY_VARS InternalList of Unchecked [m:a,c:a,o:a] +PKG_OPTIONS_NONEMPTY_SETS InternalList of Identifier +PKG_OPTIONS_OPTIONAL_GROUPS InternalList of Identifier [o:as] +PKG_OPTIONS_REQUIRED_GROUPS InternalList of Identifier [o:s,m:s] +PKG_OPTIONS_SET.* InternalList of Option +PKG_OPTIONS_VAR PkgOptionsVar [o:s,m:s,c:s, bsd.options.mk:p] +PKG_PRESERVE Yes [m:s] +PKG_SHELL Pathname [m:s,c:s] +PKG_SHELL.* Pathname [m:s,c:s] +PKG_SHLIBTOOL Pathname +PKG_SKIP_REASON List of ShellWord [$package_list] +PKG_SUGGESTED_OPTIONS List of Option [o:as,m:as,c:s] +PKG_SUPPORTED_OPTIONS List of Option [o:as,m:as,c:s] +PKG_SYSCONFDIR* Pathname [$package] +PKG_SYSCONFDIR_PERMS List of ShellWord [$package_list] +PKG_SYSCONFBASEDIR Pathname [$system] +PKG_SYSCONFSUBDIR Pathname [$package] +PKG_SYSCONFVAR Identifier +# ^^ FIXME: name/type mismatch. +PKG_UID Integer [m:s] +PKG_USERS List of ShellWord [m:as] +PKG_USERS_VARS List of Varname [$package] +PKG_USE_KERBEROS Yes [m:s,c:s] +#PLIST.* # has special handling code +PLIST_VARS List of Identifier [$package_list] +PLIST_SRC List of RelativePkgPath [$package_list] +PLIST_SUBST List of ShellWord [$package_list] +PLIST_TYPE { dynamic static } +PREPEND_PATH List of Pathname +PREFIX Pathname [*:u] # ??? +PREV_PKGPATH Pathname [*:u] # doesn't exist any longer +PRINT_PLIST_AWK AwkCommand [*:a] +PRIVILEGED_STAGES List of { install package clean } +PTHREAD_AUTO_VARS YesNo [m:s] +PTHREAD_CFLAGS List of CFlag [$system] +PTHREAD_LDFLAGS List of LdFlag [$system] +PTHREAD_LIBS List of LdFlag [$system] +PTHREAD_OPTS List of { native optional require } [m:as,c:a,b:a] +PTHREAD_TYPE Identifier [$system] +# ^^ or "native" or "none". +PY_PATCHPLIST Yes [$package] +PYPKGPREFIX { py27 py33 py34 } [*:pu, pyversion.mk:s, *:] +PYTHON_FOR_BUILD_ONLY Yes [$package] +REPLACE_PYTHON List of Pathmask [$package_list] +PYTHON_VERSIONS_ACCEPTED List of Version [$package] +PYTHON_VERSIONS_INCOMPATIBLE List of Version [$package] +PYTHON_VERSION_DEFAULT Version [$user] +PYTHON_VERSION_REQD Version [$user] +PYTHON_VERSIONED_DEPENDENCIES List of PythonDependency [$package_list] +RANLIB ShellCommand [$system] +RCD_SCRIPTS List of Filename [$package_list] +RCD_SCRIPT_SRC.* List of Pathname [m:s] +REPLACE.* String [m:s] +REPLACE_AWK List of Pathmask [$package_list] +REPLACE_BASH List of Pathmask [$package_list] +REPLACE_CSH List of Pathmask [$package_list] +REPLACE_EMACS List of Pathmask +REPLACE_FILES.* List of Pathmask [m:as,c:as] +REPLACE_INTERPRETER List of Identifier [m:a,c:a] +REPLACE_KSH List of Pathmask [$package_list] +REPLACE_LOCALEDIR_PATTERNS List of Filemask [$package_list] +REPLACE_LUA List of Pathmask [$package_list] +REPLACE_PERL List of Pathmask [$package_list] +REPLACE_PYTHON List of Pathmask [$package_list] +REPLACE_SH List of Pathmask [$package_list] +REQD_DIRS List of Pathname [$package_list] +REQD_DIRS_PERMS List of ShellWord [$package_list] +REQD_FILES List of Pathname [$package_list] +REQD_FILES_MODE { 0644 0640 0600 0400 } [$package] +REQD_FILES_PERMS List of ShellWord [$package_list] +RESTRICTED Message [$package] +ROOT_USER UserGroupName [$user] +ROOT_GROUP UserGroupName [$user] +RUBY_VERSION_REQD Version [$user] +RUN ShellCommand [$system] +SCRIPTS_ENV List of ShellWord [m:a,c:a] +SETUID_ROOT_PERMS List of ShellWord [$user] +SHAREGRP UserGroupName [$system] +SHAREMODE FileMode [$system] +SHAREOWN UserGroupName [$system] +SHCOMMENT ShellCommand [$system] +SHLIB_HANDLING { YES NO no } +SHLIBTOOL ShellCommand [] +SHLIBTOOL_OVERRIDE List of Pathmask [m:as,c:a] +SITES.* List of FetchURL [m:asu,c:asu,o:asu] +SPECIAL_PERMS List of ShellWord [$package_list] +STEP_MSG ShellCommand [$system] +SUBDIR List of Filename [Makefile:a,*:] +SUBST_CLASSES List of Identifier [m:a,c:a,h:a,Makefile.*:a] +SUBST_FILES.* List of Pathmask [m:as,c:as,h:as,o:as,Makefile.*:as] +SUBST_FILTER_CMD.* ShellCommand [m:s,c:s,h:s,o:s,Makefile.*:s] +SUBST_MESSAGE.* Message [m:s,c:s,h:s,o:s,Makefile.*:s] +SUBST_SED.* SedCommands [m:as,c:as,h:as,o:as,Makefile.*:as] +SUBST_STAGE.* Stage [$package] +SUBST_VARS.* List of Varname [$package_list] +SUPERSEDES InternalList of Dependency [$package_list] +TEST_DIRS List of WrksrcSubdirectory [$package_list] +TEST_ENV List of ShellWord [$package_list] +TEST_TARGET List of Identifier [m:s,c:ds,o:as] +TEX_ACCEPTED List of { teTeX1 teTeX2 teTeX3 } [m:s,c:s] +TEX_DEPMETHOD { build run } [m:s,c:s] +TEXINFO_REQD List of Version [$package_list] +TOOL_DEPENDS InternalList of DependencyWithPath [c:a,m:a,o:a,*.mk:a] +TOOLS_ALIASES List of Filename [$system] +TOOLS_BROKEN List of Tool [$system] +TOOLS_CREATE List of Tool [$system] +TOOLS_DEPENDS.* InternalList of DependencyWithPath [$system] +TOOLS_GNU_MISSING List of Tool [$system] +TOOLS_NOOP List of Tool [$system] +TOOLS_PATH.* Pathname [$system] +TOOLS_PLATFORM.* ShellCommand [$system] +TOUCH_FLAGS List of ShellWord [$system] +UAC_REQD_EXECS List of PrefixPathname [$package_list] +UNLIMIT_RESOURCES List of { datasize stacksize memorysize } [m:as,c:a] +UNPRIVILEGED_USER UserGroupName [$user] +UNPRIVILEGED_GROUP UserGroupName [$user] +UNWRAP_FILES List of Pathmask [$package_list] +UPDATE_TARGET List of Identifier [$user] +USE_BSD_MAKEFILE Yes [$package] +USE_BUILTIN.* YesNo_Indirectly [builtin.mk:s] +USE_CMAKE Yes [$package] +USE_CROSSBASE Yes [m:s] +USE_FEATURES List of Identifier [$package] +USE_GCC_RUNTIME YesNo [$package] +USE_GNU_CONFIGURE_HOST YesNo [$package] +USE_GNU_ICONV Yes [m:s,c:s,o:s] +USE_IMAKE Yes [m:s] +USE_JAVA { run yes build } [$package] +USE_JAVA2 { YES yes no 1.4 1.5 6 7 8 } [$package] +USE_LANGUAGES List of { ada c c99 c++ fortran fortran77 java objc } [m:s,c:s,o:s] +USE_LIBTOOL Yes [$package] +USE_MAKEINFO Yes [$package] +USE_MSGFMT_PLURALS Yes [$package] +USE_NCURSES Yes [$package] +USE_OLD_DES_API YesNo [$package] +USE_PKGINSTALL Yes [$package] +USE_PKGLOCALEDIR YesNo [$package] +USE_PKGSRC_GCC Yes [$user] +USE_TOOLS List of Tool [*:a] +USE_X11 Yes [$package] +WARNING_MSG ShellCommand [$system] +WARNING_CAT ShellCommand [$system] +WRAPPER_REORDER_CMDS List of WrapperReorder [b:a,c:a,m:a] +WRAPPER_TRANSFORM_CMDS List of WrapperTransform [b:a,c:a,m:a] +WRKDIR Pathname [$system] +WRKSRC WrkdirSubdirectory [$package] +X11_PKGSRCDIR.* Pathname [$system] +XAW_TYPE { 3d neXtaw standard xpm } [$user] +XMKMF_FLAGS List of ShellWord diff --git a/pkgtools/pkglint4/files/pkglint.0 b/pkgtools/pkglint4/files/pkglint.0 new file mode 100644 index 00000000000..d814bdaf4e6 --- /dev/null +++ b/pkgtools/pkglint4/files/pkglint.0 @@ -0,0 +1,213 @@ +PKGLINT(1) General Commands Manual PKGLINT(1) + +NNAAMMEE + ppkkgglliinntt -- static analyzer for pkgsrc packages + +SSYYNNOOPPSSIISS + ppkkgglliinntt [--ooppttiioonnss] [_d_i_r _._._.] + +DDEESSCCRRIIPPTTIIOONN + ppkkgglliinntt attempts to detect features of the named pkgsrc packages that are + likely to be bugs, or that are simply deprecated. + + + OOppttiioonnss + --CC{{[[nnoo--]]cchheecckk,,......}} Enable or disable specific checks. For a list of + checks, see below. + + --DD{{[[nnoo--]]ddeebbuugg,,......}} Enable or disable debugging categories. For a list + of categories, see below. + + --FF|----aauuttooffiixx Repair trivial things automatically. + + --II Show the _M_a_k_e_f_i_l_e that is constructed by including + all the files that are slurped in via `.include' + directives. This flag is mainly for debugging. + + --RR|----rrccssiiddssttrriinngg Allow other RCS Id strings than NetBSD. Multiple + strings may be concatenated with `|'. + + --VV|----vveerrssiioonn Print the current ppkkgglliinntt version number and exit. + + --WW{{[[nnoo--]]wwaarrnn,,......}} Enable or disable specific warnings. For a list of + warnings, see below. + + --ee|----eexxppllaaiinn Print further explanations for diagnostics. + Sometimes the reasons for diagnostics are not obvious + and need further explanation. + + --gg|----ggcccc--oouuttppuutt--ffoorrmmaatt + Use a format for the diagnostics that is understood + by most programs, especially editors, so they can + provide a point-and-goto interface. + + --hh|----hheellpp Show the summary of command line options, then exit. + + --ii|----iimmppoorrtt Check if a package is ready to be imported into + pkgsrc. This is especially useful for packages from + the pkgsrc-wip project. + + --qq|----qquuiieett Don't print the errors and warnings summary before + terminating. + + --rr|----rreeccuurrssiivvee Check subdirectories, too. The subdirectories are + those that are mentioned in a `SUBDIR+=' line. + + --ss|----ssoouurrccee For all diagnostics having file and line number + information, show the source code along with the + diagnostics. + + CChheecckkss + aallll Enable all checks. + + nnoonnee Disable all checks. + + [[nnoo--]]AALLTTEERRNNAATTIIVVEESS Check the alternatives file. + + [[nnoo--]]DDEESSCCRR Check the DESCR file. + + [[nnoo--]]IINNSSTTAALLLL Check the INSTALL and DEINSTALL scripts. + + [[nnoo--]]MMaakkeeffiillee Check the package Makefile, including all included + files. + + [[nnoo--]]MMEESSSSAAGGEE Check MESSAGE files. + + [[nnoo--]]PPLLIISSTT Check PLIST files. + + [[nnoo--]]bbll33 Check buildlink3 Makefiles. + + [[nnoo--]]ddiissttiinnffoo Check the distinfo file. + + [[nnoo--]]eexxttrraa Check remaining files in the package directory. + + [[nnoo--]]mmkk Check Makefile fragments besides buildlink3. + + [[nnoo--]]ppaattcchheess Check the pkgsrc specific patch files. + + DDeebbuuggggiinngg OOppttiioonnss + aallll Enable all debugging options. + + nnoonnee Disable all debugging options. + + [[nnoo--]]iinncclluuddee Show the pathnames of the included Makefiles. + + [[nnoo--]]mmiisscc Some debugging stuff that hasn't made it into its own + category. + + [[nnoo--]]ppaattcchheess Print the states of the patch file parser. + + [[nnoo--]]qquuoottiinngg Additional information about why variables should be + quoted or not. + + [[nnoo--]]sshheellll Parser information from the shell word and the shell + command parsers. + + [[nnoo--]]ttoooollss Additional information about the tools from the tools + framework. + + [[nnoo--]]ttrraaccee Print the names of subroutines and their arguments as + they are entered. + + [[nnoo--]]uunncchheecckkeedd Show the things that pkglint cannot currently check. + These are mostly due to unresolved make variables. + + [[nnoo--]]uunnuusseedd Show which variables are detected as used, and so + will not generate an ``unused variable'' warning. + + [[nnoo--]]vvaarrttyyppeess Additional information about the variable types. + + [[nnoo--]]vvaarruussee Information about the contexts in which variables are + used. + + WWaarrnniinnggss + aallll Enable all warnings. + + nnoonnee Disable all warnings. + + [[nnoo--]]aabbssnnaammee Warn if a file contains an absolute pathname. + + [[nnoo--]]ddiirreeccttccmmdd Warn if a system command name is used instead of a + variable (e.g. sed instead of ${SED}). + + [[nnoo--]]eexxttrraa Emit some additional warnings that are not enabled by + default, for whatever reason. + + [[nnoo--]]oorrddeerr Warn if Makefile variables are not in the preferred + order. + + [[nnoo--]]ppeerrmm Warn if a variable is used or defined outside its + specified scope. The available permissions are: + append + append something using += + default + set a default value using ?= + preprocess + use a variable during preprocessing + runtime + use a variable at runtime + set set a variable using :=, =, != + unknown + permissions for this variable currently not + known + A `?' means that it is not yet clear which + permissions are allowed and which aren't. + + [[nnoo--]]pplliisstt--ddeepprr Warn if deprecated pathnames are used in _P_L_I_S_T files. + This warning is disabled by default. + + [[nnoo--]]pplliisstt--ssoorrtt Warn if items of a PLIST file are not sorted + alphabetically. This warning is disabled by default. + + [[nnoo--]]qquuoottiinngg Warn for possibly invalid quoting of make variables + in shell programs and shell variables themselves. + + [[nnoo--]]ssppaaccee Emit notes for inconsistent use of white-space. + + [[nnoo--]]ssttyyllee Warn for stylistic issues that don't affect the build + process. + + [[nnoo--]]ttyyppeess Warn for some _M_a_k_e_f_i_l_e variables if their assigned + values do not match their type. + + [[nnoo--]]vvaarroorrddeerr Warn if the variables in a package _M_a_k_e_f_i_l_es are not + ordered in the way it is described the pkgsrc guide. + + OOtthheerr aarrgguummeennttss + _d_i_r _._._. The pkgsrc directories to be checked. If + omitted, the current directory is checked. + +FFIILLEESS + _p_k_g_s_r_c_/_m_k_/_* Files from the pkgsrc infrastructure. + +EEXXAAMMPPLLEESS + ppkkgglliinntt --CCnnoonnee,,ppaattcchheess .. + Checks the patches of the package in the current directory. + + ppkkgglliinntt --WWaallll //uussrr//ppkkggssrrcc//ddeevveell + Checks the category Makefile and reports any warnings it can + find. + + ppkkgglliinntt --rr --RR ''NNeettBBSSDD||IIdd'' //uussrr//ppkkggssrrcc + Check the whole pkgsrc tree while allowing `NetBSD' or `Id' + as the RCS Id. + +DDIIAAGGNNOOSSTTIICCSS + Diagnostics are written to the standard output. + + ERROR: ... Errors should be fixed before a package is committed to + pkgsrc. + + WARN: ... Warnings generally should be fixed, but they are not as + critical as errors. + +AAUUTTHHOORRSS + Roland Illig <rillig@NetBSD.org> + +BBUUGGSS + Many more checks could be added. + + If you don't understand the messages, feel free to ask on the + <tech-pkg@NetBSD.org> mailing list. + +NetBSD 6.99.8 July 14, 2012 NetBSD 6.99.8 diff --git a/pkgtools/pkglint4/files/pkglint.1 b/pkgtools/pkglint4/files/pkglint.1 new file mode 100644 index 00000000000..811746f41b9 --- /dev/null +++ b/pkgtools/pkglint4/files/pkglint.1 @@ -0,0 +1,244 @@ +.\" $NetBSD: pkglint.1,v 1.1 2015/11/25 16:42:21 rillig Exp $ +.\" From FreeBSD: portlint.1,v 1.8 1997/11/25 14:53:14 itojun Exp +.\" +.\" Copyright (c) 1997 by Jun-ichiro Itoh <itojun@itojun.org>. +.\" All Rights Reserved. Absolutely no warranty. +.\" +.\" Roland Illig <roland.illig@gmx.de>, 2004, 2005. +.\" Thomas Klausner <wiz@NetBSD.org>, 2012. +.\" +.Dd July 14, 2012 +.Dt PKGLINT 1 +.Os +.Sh NAME +.Nm pkglint +.Nd static analyzer for pkgsrc packages +.Sh SYNOPSIS +.Nm pkglint +.Op Fl options +.Op Ar dir ... +.Sh DESCRIPTION +.Nm +attempts to detect features of the named pkgsrc packages that are likely +to be bugs, or that are simply deprecated. +.Pp +.\" ======================================================================= +.Ss Options +.Bl -tag -width 18n +.It Fl C{[no-]check,...} +Enable or disable specific checks. +For a list of checks, see below. +.It Fl D{[no-]debug,...} +Enable or disable debugging categories. +For a list of categories, see below. +.It Fl F Ns | Ns Fl -autofix +Repair trivial things automatically. +.It Fl I +Show the +.Pa Makefile +that is constructed by including all the files that +are slurped in via +.Ql .include +directives. +This flag is mainly for debugging. +.It Fl R Ns | Ns Fl -rcsidstring +Allow other RCS Id strings than NetBSD. +Multiple strings may be concatenated with +.Ql | . +.It Fl V Ns | Ns Fl -version +Print the current +.Nm +version number and exit. +.It Fl W{[no-]warn,...} +Enable or disable specific warnings. +For a list of warnings, see below. +.It Fl e Ns | Ns Fl -explain +Print further explanations for diagnostics. +Sometimes the reasons for diagnostics are not obvious and need further +explanation. +.It Fl g Ns | Ns Fl -gcc-output-format +Use a format for the diagnostics that is understood by most programs, +especially editors, so they can provide a point-and-goto interface. +.It Fl h Ns | Ns Fl -help +Show the summary of command line options, then exit. +.It Fl i Ns | Ns Fl -import +Check if a package is ready to be imported into pkgsrc. +This is especially useful for packages from the pkgsrc-wip project. +.It Fl q Ns | Ns Fl -quiet +Don't print the errors and warnings summary before terminating. +.It Fl r Ns | Ns Fl -recursive +Check subdirectories, too. +The subdirectories are those that are mentioned in a +.Ql SUBDIR+= +line. +.It Fl s Ns | Ns Fl -source +For all diagnostics having file and line number information, show the +source code along with the diagnostics. +.El +.\" ======================================================================= +.Ss Checks +.Bl -tag -width 18n +.It Cm all +Enable all checks. +.It Cm none +Disable all checks. +.It Cm [no-]ALTERNATIVES +Check the alternatives file. +.It Cm [no-]DESCR +Check the DESCR file. +.It Cm [no-]INSTALL +Check the INSTALL and DEINSTALL scripts. +.It Cm [no-]Makefile +Check the package Makefile, including all included files. +.It Cm [no-]MESSAGE +Check MESSAGE files. +.It Cm [no-]PLIST +Check PLIST files. +.It Cm [no-]bl3 +Check buildlink3 Makefiles. +.It Cm [no-]distinfo +Check the distinfo file. +.It Cm [no-]extra +Check remaining files in the package directory. +.It Cm [no-]mk +Check Makefile fragments besides buildlink3. +.It Cm [no-]patches +Check the pkgsrc specific patch files. +.El +.\" ======================================================================= +.Ss Debugging Options +.Bl -tag -width 18n +.It Cm all +Enable all debugging options. +.It Cm none +Disable all debugging options. +.It Cm [no-]include +Show the pathnames of the included Makefiles. +.It Cm [no-]misc +Some debugging stuff that hasn't made it into its own category. +.It Cm [no-]patches +Print the states of the patch file parser. +.It Cm [no-]quoting +Additional information about why variables should be quoted or not. +.It Cm [no-]shell +Parser information from the shell word and the shell command parsers. +.It Cm [no-]tools +Additional information about the tools from the tools framework. +.It Cm [no-]trace +Print the names of subroutines and their arguments as they are entered. +.It Cm [no-]unchecked +Show the things that pkglint cannot currently check. +These are mostly due to unresolved make variables. +.It Cm [no-]unused +Show which variables are detected as used, and so will not generate an +.Dq unused variable +warning. +.It Cm [no-]vartypes +Additional information about the variable types. +.It Cm [no-]varuse +Information about the contexts in which variables are used. +.El +.\" ======================================================================= +.Ss Warnings +.Bl -tag -width 18n +.It Cm all +Enable all warnings. +.It Cm none +Disable all warnings. +.It Cm [no-]absname +Warn if a file contains an absolute pathname. +.It Cm [no-]directcmd +Warn if a system command name is used instead of a variable (e.g. sed +instead of ${SED}). +.It Cm [no-]extra +Emit some additional warnings that are not enabled by default, +for whatever reason. +.It Cm [no-]order +Warn if Makefile variables are not in the preferred order. +.It Cm [no-]perm +Warn if a variable is used or defined outside its specified scope. +The available permissions are: +.Bl -tag -width 3n -compact +.It append +append something using += +.It default +set a default value using ?= +.It preprocess +use a variable during preprocessing +.It runtime +use a variable at runtime +.It set +set a variable using :=, =, != +.It unknown +permissions for this variable currently not known +.El +A +.Sq \&? +means that it is not yet clear which permissions are allowed and which aren't. +.It Cm [no-]plist-depr +Warn if deprecated pathnames are used in +.Pa PLIST +files. +This warning is disabled by default. +.It Cm [no-]plist-sort +Warn if items of a PLIST file are not sorted alphabetically. +This warning is disabled by default. +.It Cm [no-]quoting +Warn for possibly invalid quoting of make variables in shell programs +and shell variables themselves. +.It Cm [no-]space +Emit notes for inconsistent use of white-space. +.It Cm [no-]style +Warn for stylistic issues that don't affect the build process. +.It Cm [no-]types +Warn for some +.Pa Makefile +variables if their assigned values do not match +their type. +.It Cm [no-]varorder +Warn if the variables in a package +.Pa Makefile Ns +s are not ordered in the way it is described the pkgsrc guide. +.El +.\" ======================================================================= +.Ss Other arguments +.Bl -tag -width 18n -offset indent +.It Ar dir ... +The pkgsrc directories to be checked. +If omitted, the current directory is checked. +.El +.Sh FILES +.Bl -tag -width pkgsrc/mk/* -compact +.It Pa pkgsrc/mk/* +Files from the pkgsrc infrastructure. +.El +.Sh EXAMPLES +.Bl -tag -width Fl +.It Ic pkglint \-Cnone,patches \&. +Checks the patches of the package in the current directory. +.It Ic pkglint \-Wall /usr/pkgsrc/devel +Checks the category Makefile and reports any warnings it can find. +.It Ic pkglint -r \-R 'NetBSD|Id' /usr/pkgsrc +Check the whole pkgsrc tree while allowing +.Ql NetBSD +or +.Ql Id +as the RCS Id. +.El +.Sh DIAGNOSTICS +Diagnostics are written to the standard output. +.Bl -tag -width "WARN: foobaa" +.It ERROR: ... +Errors should be fixed before a package is committed to pkgsrc. +.It WARN: ... +Warnings generally should be fixed, but they are not as critical as +errors. +.El +.Sh AUTHORS +.An Roland Illig Aq Mt rillig@NetBSD.org +.Sh BUGS +Many more checks could be added. +.Pp +If you don't understand the messages, feel free to ask on the +.Aq tech-pkg@NetBSD.org +mailing list. diff --git a/pkgtools/pkglint4/files/pkglint.pl b/pkgtools/pkglint4/files/pkglint.pl new file mode 100644 index 00000000000..ddb6c1f8c48 --- /dev/null +++ b/pkgtools/pkglint4/files/pkglint.pl @@ -0,0 +1,5915 @@ +#! @PERL@ +# $NetBSD: pkglint.pl,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# + +# pkglint - static analyzer and checker for pkgsrc packages +# +# Written by: +# Roland Illig <rillig@NetBSD.org> +# +# Based on work by: +# Hubert Feyrer <hubertf@NetBSD.org> +# Thorsten Frueauf <frueauf@NetBSD.org> +# Thomas Klausner <wiz@NetBSD.org> +# and others. +# +# Based on FreeBSD's portlint by: +# Jun-ichiro itojun Hagino <itojun@itojun.org> +# Yoshishige Arai <ryo2@on.rim.or.jp> +# +# FreeBSD Id: portlint.pl,v 1.64 1998/02/28 02:34:05 itojun Exp +# Copyright(c) 1997 by Jun-ichiro Hagino <itojun@itojun.org>. +# All rights reserved. +# Freely redistributable. Absolutely no warranty. + +# To get an overview of the code, run: +# sed -n -e 's,^\(sub .*\) {.*, \1,p' -e '/^package/p' pkglint.pl + +#========================================================================== +# Note: The @EXPORT clauses in the packages must be in a BEGIN block, +# because otherwise the names starting with an uppercase letter are not +# recognized as subroutines but as file handles. +#========================================================================== + +use v5.12; +use warnings; + +#include PkgLint/Util.pm +#include PkgLint/Logging.pm +#include PkgLint/SimpleMatch.pm +#include PkgLint/Line.pm +#include PkgLint/FileUtil.pm +#include PkgLint/Type.pm +#include PkgLint/VarUseContext.pm +#include PkgLint/SubstContext.pm +#include PkgLint/CVS_Entry.pm +#include PkgLint/Change.pm + +package pkglint; +#========================================================================== +# This package contains the application-specific code of pkglint. +# Most subroutines in this package follow a strict naming convention: +# +# The get_*() functions provide easy access to important non-trivial data +# structures that are loaded from external files and are therefore cached. +# +# The is_*() functions return a boolean value and have no side effects. +# +# The checkline_*() procedures check a single line for compliance with some +# rules. +# +# The checklines_*() procedures check an array of lines for compliance. +# Usually they make use of several checkline_*() procedures. +# +# The checkfile_*() procedures load a file and check the lines of that +# file. Usually they make use of several checklines_*() and checkline_*() +# procedures. +# +# The checkdir_*() procedures check the files of a directory and call +# checkfile_*() on them. +# +# Note: I have tried to order the subroutines so that there are no +# back-references, that is, if you start reading the code from the top to +# the bottom you should not find a call to a subroutine you haven't yet +# seen. +#========================================================================== +use strict; +use warnings; + +use Data::Dumper; +use Digest::SHA1; +use Getopt::Long qw(:config no_ignore_case bundling require_order); +use Fcntl qw(:mode); +use File::Basename; +use File::stat; +use Cwd; +use pkgsrc::Dewey; + +BEGIN { + import PkgLint::Util qw( + array_to_hash assert + false true dont_know doesnt_matter + normalize_pathname + ); + import PkgLint::Logging qw( + NO_FILE NO_LINE_NUMBER NO_LINES + log_fatal log_error log_warning log_note log_debug + explain_error explain_warning explain_info + ); + import PkgLint::FileUtil qw( + load_file load_lines + save_autofix_changes + ); + import PkgLint::Type qw( + LK_NONE LK_INTERNAL LK_EXTERNAL + GUESSED NOT_GUESSED + ); + import PkgLint::VarUseContext qw( + VUC_TIME_UNKNOWN VUC_TIME_LOAD VUC_TIME_RUN + VUC_TYPE_UNKNOWN + VUC_SHELLWORD_UNKNOWN VUC_SHELLWORD_PLAIN VUC_SHELLWORD_DQUOT + VUC_SHELLWORD_SQUOT VUC_SHELLWORD_BACKT VUC_SHELLWORD_FOR + VUC_EXTENT_UNKNOWN VUC_EXTENT_FULL VUC_EXTENT_WORD + VUC_EXTENT_WORD_PART + ); +} + +# +# Buildtime configuration +# + +use constant conf_distver => '@DISTVER@'; +use constant conf_make => '@MAKE@'; +use constant conf_datadir => '@DATADIR@'; + +# +# Global variables that can be modified via command line options. +# + +# The pkgsrc directory, relative to the current working directory of +# pkglint. +my $cwd_pkgsrcdir = undef; + +# The pkgsrc directory, relative to the directory that is currently +# checked. +my $cur_pkgsrcdir = undef; + +# +# Command Line Options +# + +my $opt_check_ALTERNATIVES = true; +my $opt_check_bl3 = true; +my $opt_check_DESCR = true; +my $opt_check_distinfo = true; +my $opt_check_extra = false; +my $opt_check_global = false; +my $opt_check_INSTALL = true; +my $opt_check_Makefile = true; +my $opt_check_MESSAGE = true; +my $opt_check_mk = true; +my $opt_check_patches = true; +my $opt_check_PLIST = true; +my (%checks) = ( + "ALTERNATIVES" => [\$opt_check_ALTERNATIVES, "check ALTERNATIVES files"], + "bl3" => [\$opt_check_bl3, "check buildlink3 files"], + "DESCR" => [\$opt_check_DESCR, "check DESCR file"], + "distinfo" => [\$opt_check_distinfo, "check distinfo file"], + "extra" => [\$opt_check_extra, "check various additional files"], + "global" => [\$opt_check_global, "inter-package checks"], + "INSTALL" => [\$opt_check_INSTALL, "check INSTALL and DEINSTALL scripts"], + "Makefile" => [\$opt_check_Makefile, "check Makefiles"], + "MESSAGE" => [\$opt_check_MESSAGE, "check MESSAGE files"], + "mk" => [\$opt_check_mk, "check other .mk files"], + "patches" => [\$opt_check_patches, "check patches"], + "PLIST" => [\$opt_check_PLIST, "check PLIST files"], +); + +my $opt_debug_include = false; +my $opt_debug_misc = false; +my $opt_debug_patches = false; +my $opt_debug_quoting = false; +my $opt_debug_shell = false; +my $opt_debug_tools = false; +my $opt_debug_trace = false; +my $opt_debug_unchecked = false; +my $opt_debug_unused = false; +my $opt_debug_vartypes = false; +my $opt_debug_varuse = false; +my (%debug) = ( + "include" => [\$opt_debug_include, "included files"], + "misc" => [\$opt_debug_misc, "all things that didn't fit elsewhere"], + "patches" => [\$opt_debug_patches, "the states of the patch parser"], + "quoting" => [\$opt_debug_quoting, "additional information about quoting"], + "shell" => [\$opt_debug_shell, "the parsers for shell words and shell commands"], + "tools" => [\$opt_debug_tools, "the tools framework"], + "trace" => [\$opt_debug_trace, "follow subroutine calls"], + "unchecked" => [\$opt_debug_unchecked, "show the current limitations of pkglint"], + "unused" => [\$opt_debug_unused, "unused variables"], + "vartypes" => [\$opt_debug_vartypes, "additional type information"], + "varuse" => [\$opt_debug_varuse, "contexts where variables are used"], +); + +my $opt_warn_absname = true; +my $opt_warn_directcmd = true; +our $opt_warn_extra = false; # used by PkgLint::SubstContext +my $opt_warn_order = true; +my $opt_warn_perm = false; +my $opt_warn_plist_depr = false; +my $opt_warn_plist_sort = false; +my $opt_warn_quoting = false; +my $opt_warn_space = false; +my $opt_warn_style = false; +my $opt_warn_types = true; +my $opt_warn_varorder = false; +my (%warnings) = ( + "absname" => [\$opt_warn_absname, "warn about use of absolute file names"], + "directcmd" => [\$opt_warn_directcmd, "warn about use of direct command names instead of Make variables"], + "extra" => [\$opt_warn_extra, "enable some extra warnings"], + "order" => [\$opt_warn_order, "warn if Makefile entries are unordered"], + "perm" => [\$opt_warn_perm, "warn about unforeseen variable definition and use"], + "plist-depr" => [\$opt_warn_plist_depr, "warn about deprecated paths in PLISTs"], + "plist-sort" => [\$opt_warn_plist_sort, "warn about unsorted entries in PLISTs"], + "quoting" => [\$opt_warn_quoting, "warn about quoting issues"], + "space" => [\$opt_warn_space, "warn about inconsistent use of white-space"], + "style" => [\$opt_warn_style, "warn about stylistic issues"], + "types" => [\$opt_warn_types, "do some simple type checking in Makefiles"], + "varorder" => [\$opt_warn_varorder, "warn about the ordering of variables"], +); + +my $opt_autofix = false; +my $opt_dumpmakefile = false; +my $opt_import = false; +my $opt_quiet = false; +my $opt_recursive = false; +my $opt_rcsidstring = "NetBSD"; +my (@options) = ( + # [ usage-opt, usage-message, getopt-opt, getopt-action ] + [ "-C{check,...}", "Enable or disable specific checks", + "check|C=s", + sub { + my ($opt, $val) = @_; + parse_multioption($val, \%checks); + } ], + [ "-D{debug,...}", "Enable or disable debugging categories", + "debugging|D=s", + sub ($$) { + my ($opt, $val) = @_; + parse_multioption($val, \%debug); + } ], + [ "-F|--autofix", "Try to automatically fix some errors (experimental)", + "autofix|F", \$opt_autofix ], + [ "-I|--dumpmakefile", "Dump the Makefile after parsing", + "dumpmakefile|I", \$opt_dumpmakefile ], + [ "-R|--rcsidstring", "Set the allowed RCS Id strings", + "rcsidstring|R=s", \$opt_rcsidstring ], + [ "-V|--version", "print the version number of pkglint", + "version|V", + sub { + print(conf_distver . "\n"); + exit(0); + } ], + [ "-W{warn,...}", "enable or disable specific warnings", + "warning|W=s", + sub { + my ($opt, $val) = @_; + parse_multioption($val, \%warnings); + } ], + [ "-e|--explain", "Explain the diagnostics or give further help", + "explain|e", sub { + PkgLint::Logging::set_explain(); + } ], + [ "-g|--gcc-output-format", "Mimic the gcc output format", + "gcc-output-format|g", + sub { + PkgLint::Logging::set_gcc_output_format(); + } ], + [ "-h|--help", "print a detailed help message", + "help|h", + sub { + help(*STDOUT, 0, 1); + } ], + [ "-i|--import", "Prepare the import of a wip package", + "import|i", \$opt_import ], + # Note: This is intentionally undocumented. + [ "--pkgsrcdir", "Set the root directory of pkgsrc explicitly.", + "pkgsrcdir=s", \$cwd_pkgsrcdir ], + [ "-q|--quiet", "Don't print a summary line when finishing", + "quiet|q", \$opt_quiet ], + [ "-r|--recursive", "Recursive---check subdirectories, too", + "recursive|r", \$opt_recursive ], + [ "-s|--source", "Show the source lines together with diagnostics", + "source|s", + sub { + PkgLint::Logging::set_show_source_flag(); + } ], +); + +our $program = $0; + +# +# Commonly used regular expressions. +# + +use constant regex_dependency_lge => qr"^((?:\$\{[\w_]+\}|[\w_\.+]|-[^\d])+)[<>]=?(\d[^-*?\[\]]*)$"; +use constant regex_dependency_wildcard + => qr"^((?:\$\{[\w_]+\}|[\w_\.+]|-[^\d\[])+)-(?:\[0-9\]\*|\d[^-]*)$"; +use constant regex_gnu_configure_volatile_vars + => qr"^(?:.*_)?(?:CFLAGS||CPPFLAGS|CXXFLAGS|FFLAGS|LDFLAGS|LIBS)$"; +use constant regex_mk_comment => qr"^ *\s*#(.*)$"; +use constant regex_mk_cond => qr"^\.(\s*)(if|ifdef|ifndef|else|elif|endif|for|endfor|undef)(?:\s+([^\s#][^#]*?))?\s*(?:#.*)?$"; +use constant regex_mk_dependency=> qr"^([^\s:]+(?:\s*[^\s:]+)*)(\s*):\s*([^#]*?)(?:\s*#.*)?$"; +use constant regex_mk_include => qr"^\.\s*(s?include)\s+\"([^\"]+)\"\s*(?:#.*)?$"; +use constant regex_mk_sysinclude=> qr"^\.\s*s?include\s+<([^>]+)>\s*(?:#.*)?$"; +use constant regex_mk_shellvaruse => qr"(?:^|[^\$])\$\$\{?(\w+)\}?"; # XXX: not perfect +use constant regex_pkgname => qr"^([\w\-.+]+)-(\d(?:\w|\.\d)*)$"; +use constant regex_mk_shellcmd => qr"^\t(.*)$"; +use constant regex_rcs_conflict => qr"^(<<<<<<<|=======|>>>>>>>)"; +use constant regex_unresolved => qr"\$\{"; +use constant regex_validchars => qr"[\011\040-\176]"; +# Note: the following regular expression looks more complicated than +# necessary to avoid a stack overflow in the Perl interpreter. +# The leading white-space may only consist of \040 characters, otherwise +# the order of regex_varassign and regex_mk_shellcmd becomes important. +use constant regex_varassign => qr"^ *([-*+A-Z_a-z0-9.\${}\[]+?)\s*(=|\?=|\+=|:=|!=)\s*((?:[^\\#\s]+|\s+?|(?:\\#)+|\\)*?)(?:\s*(#.*))?$"; +use constant regex_sh_varassign => qr"^([A-Z_a-z][0-9A-Z_a-z]*)="; + +# The following "constants" are often used in contexts where +# interpolation comes handy, so they are variables. Nevertheless they +# are not modified. + +# This regular expression cannot parse all kinds of shell programs, but +# it will catch almost all shell programs that are portable enough to be +# used in pkgsrc. +my $regex_shellword = qr"\s*( + \#.* # shell comment + | + (?: '[^']*' # single quoted string + | \"(?:\\.|[^\"\\])*\" # double quoted string + | \`[^\`]*\` # backticks string + | \\\$\$ # an escaped dollar sign + | \\[^\$] # other escaped characters + | \$[\w_] # one-character make(1) variable + | \$\{[^{}]+\} # make(1) variable + | \$\([^()]+\) # make(1) variable, $(...) + | \$[/\@<^] # special make(1) variables + | \$\$[0-9A-Z_a-z]+ # shell variable + | \$\$[\#?@] # special shell variables + | \$\$\$\$ # the special pid shell variable + | \$\$\{[0-9A-Z_a-z]+\} # shell variable in braces + | \$\$\( # POSIX-style backticks replacement + | [^\(\)'\"\\\s;&\|<>\`\$] # non-special character + | \$\{[^\s\"'`]+ # HACK: nested make(1) variables + )+ | ;;? | &&? | \|\|? | \( | \) | >& | <<? | >>? | \#.*)"sx; +my $regex_varname = qr"(?:[-*+.0-9A-Z_a-z{}\[]+|\$\{[\w_]+\})+"; +my $regex_pkgbase = qr"(?:[+.0-9A-Z_a-z]|-[A-Z_a-z])+"; +my $regex_pkgversion = qr"\d(?:\w|\.\d)*"; + +# +# Commonly used explanations for diagnostics. +# + +use constant expl_relative_dirs => ( + "Directories in the form \"../../category/package\" make it easier to", + "move a package around in pkgsrc, for example from pkgsrc-wip to the", + "main pkgsrc repository."); + +# +# Global variables. +# + +my $current_dir; # The currently checked directory. +my $is_wip; # Is the current directory from pkgsrc-wip? +my $is_internal; # Is the current item from the infrastructure? + +# +# Variables for inter-package checks. +# + +my $ipc_distinfo; # Maps "$alg:$fname" => "checksum". +my %ipc_used_licenses; # { license name => true } +my $ipc_checking_root_recursively; # For checking unused licenses + +# Context of the package that is currently checked. +my $pkgpath; # The relative path to the package within PKGSRC +my $pkgdir; # PKGDIR from the package Makefile +my $filesdir; # FILESDIR from the package Makefile +my $patchdir; # PATCHDIR from the package Makefile +my $distinfo_file; # DISTINFO_FILE from the package Makefile +my $effective_pkgname; # PKGNAME or DISTNAME from the package Makefile +my $effective_pkgbase; # The effective PKGNAME without the version +my $effective_pkgversion; # The version part of the effective PKGNAME +my $effective_pkgname_line; # The origin of the three effective_* values +my $seen_bsd_prefs_mk; # Has bsd.prefs.mk already been included? + +my $pkgctx_vardef; # { varname => line } +my $pkgctx_varuse; # { varname => line } +my $pkgctx_bl3; # { buildlink3.mk name => line } (contains + # only buildlink3.mk files that are directly + # included) +my $pkgctx_plist_subst_cond; # { varname => 1 } list of all variables + # that are used as conditionals (@comment + # or nothing) in PLISTs. +my $pkgctx_included; # { fname => line } +my $seen_Makefile_common; # Does the package have any .includes? + +# Context of the Makefile that is currently checked. +my $mkctx_for_variables; # The variables currently used in .for loops +my $mkctx_indentations; # Indentation depth of preprocessing directives +my $mkctx_target; # Current make(1) target +my $mkctx_vardef; # { varname => line } for all variables that + # are defined in the current file +my $mkctx_varuse; # { varname => line } for all variables + # that are used in the current file +my $mkctx_build_defs; # Set of variables that are registered in + # BUILD_DEFS, to assure that all user-defined + # variables are added to it. +my $mkctx_plist_vars; # The same for PLIST_VARS. +my $mkctx_tools; # Set of tools that are declared to be used. + +my @todo_items; # The list of directory entries that still need + # to be checked. Mostly relevant with + # --recursive. + +# +# Command line parsing and handling. +# + +sub help($$$) { + my ($out, $exitval, $show_all) = @_; + my ($prog) = (basename($program, '.pl')); + print $out ("usage: $prog [options] [package_directory]\n\n"); + + my (@option_table) = (); + foreach my $opt (@options) { + push(@option_table, [" ", $opt->[0], $opt->[1]]); + } + print $out ("options:\n"); + PkgLint::Util::print_table($out, \@option_table); + print $out ("\n"); + + if (!$show_all) { + exit($exitval); + } + + my $categories = [ + # options, leading text, + [ \%checks, "checks", "check" ], + [ \%debug, "debugging options", "debug" ], + [ \%warnings, "warnings", "warning" ], + ]; + foreach my $category (@{$categories}) { + my ($options, $leading, $name) = (@{$category}); + my $table = [ + [" ", "all", "", "enable all ".$category->[1]], + [" ", "none", "", "disable all ".$category->[1]], + ]; + + foreach my $opt (sort keys %{$options}) { + push(@{$table}, [ " ", $opt, + (${$options->{$opt}->[0]} ? "(enabled)" : "(disabled)"), + $options->{$opt}->[1]]); + } + + print $out ("${leading}: (use \"${name}\" to enable, \"no-${name}\" to disable)\n"); + PkgLint::Util::print_table($out, $table); + print $out ("\n"); + } + + exit($exitval); +} + +sub parse_multioption($$) { + my ($value, $optdefs) = @_; + foreach my $opt (split(qr",", $value)) { + if ($opt eq "none") { + foreach my $key (keys %{$optdefs}) { + ${$optdefs->{$key}->[0]} = false; + } + + } elsif ($opt eq "all") { + foreach my $key (keys %{$optdefs}) { + ${$optdefs->{$key}->[0]} = true; + } + + } else { + my ($value) = (($opt =~ s/^no-//) ? false : true); + if (exists($optdefs->{$opt})) { + ${$optdefs->{$opt}->[0]} = $value; + } else { + print STDERR ("Invalid option: ${opt}\n"); + help(*STDERR, 1, 0); + } + } + } +} + +sub parse_command_line() { + my (%options); + + foreach my $opt (@options) { + $options{$opt->[2]} = $opt->[3]; + } + + { + local $SIG{__WARN__} = sub {}; + if (!GetOptions(%options)) { + help(*STDERR, 1, false); + } + } +} + +# +# Caching subroutines. +# + +# The get_regex_plurals() function returns a regular expression that +# matches for all make(1) variable names that are considered lists +# of something. +# +# Rationale: +# +# The pkglint author thinks that variables containing lists of things +# should have a name indicating some plural form. Sadly, there are other +# reasons like backwards compatibility and other developer's +# expectations that make changes to most of the following variables +# highly unlikely. +sub get_regex_plurals() { + state $result = undef; + return $result if defined($result); + + my @plurals_ok = qw( + .*S + .*LIST + .*_AWK + .*_ENV + .*_REQD + .*_SED + .*_SKIP + BUILDLINK_LDADD + COMMENT + EXTRACT_ONLY + FETCH_MESSAGE + GENERATE_PLIST + PLIST_CAT + PLIST_PRE + PREPEND_PATH + ); + my @plurals_missing_an_s = qw( + .*_OVERRIDE + .*_PREREQ + .*_SRC + .*_SUBST + .*_TARGET + .*_TMPL + BROKEN_EXCEPT_ON_PLATFORM + BROKEN_ON_PLATFORM + BUILDLINK_DEPMETHOD + BUILDLINK_TRANSFORM + EVAL_PREFIX + INTERACTIVE_STAGE + LICENSE + MASTER_SITE_.* + MASTER_SORT_REGEX + NOT_FOR_COMPILER + NOT_FOR_PLATFORM + ONLY_FOR_COMPILER + ONLY_FOR_PLATFORM + PERL5_PACKLIST + PKG_FAIL_REASON + PKG_SKIP_REASON + ); + my @plurals_reluctantly_accepted = qw( + CRYPTO + DEINSTALL_TEMPLATE + FIX_RPATH + INSTALL_TEMPLATE + PYTHON_VERSIONS_INCOMPATIBLE + REPLACE_INTERPRETER + REPLACE_PERL + REPLACE_RUBY + RESTRICTED + SITES_.* + TOOLS_ALIASES\.* + TOOLS_BROKEN + TOOLS_CREATE + TOOLS_GNU_MISSING + TOOLS_NOOP + ); + my $plurals = join("|", + @plurals_ok, + @plurals_missing_an_s, + @plurals_reluctantly_accepted + ); + + $result = qr"^(?:${plurals})$"; + return $result; +} + +# +# Loading pkglint-specific data from files. +# + +sub load_existing_lines($$) { + my ($fname, $fold_backslash_lines) = @_; + + my $lines = load_lines($fname, $fold_backslash_lines) + or log_fatal($fname, NO_LINE_NUMBER, "Cannot be read."); + return $lines; +} + +# The symbol table for ACL definitions maps ACL names to ACLs. +my $acl_definitions = {}; + +sub parse_acls($$) { + my ($line, $acltext) = @_; + my ($acls); + + use constant ACL_shortcuts => { + "b" => qr"(?:^|/)buildlink3\.mk$", + "c" => qr"(?:^|/)Makefile\.common$", + "h" => qr"(?:^|/)hacks\.mk$", + "m" => qr"(?:^|/)Makefile$", + "o" => qr"(?:^|/)options\.mk$", + }; + + my $regex_acl_entry = qr"^(?: + \$([\w_]+) # $acl_name + | ([\w.*]+|_):([adpsu]*) # file*mask:perms + ) (?:\,\s*|$)"x; + + if (!defined($acltext)) { + return undef; + } + + $acls = []; + while ($acltext =~ s,$regex_acl_entry,,) { + my ($acldef, $subject, $perms) = ($1, $2, $3); + + if (defined($acldef)) { + if (!exists($acl_definitions->{$acldef})) { + $line->log_fatal("ACL definition ${acldef} not found."); + } else { + push(@{$acls}, @{$acl_definitions->{$acldef}}); + } + + } else { + # Transform $subject to a regular expression. + $subject =~ s/\./[.]/g; + $subject =~ s/\*/.*/g; + + push(@{$acls}, [exists(ACL_shortcuts->{$subject}) ? ACL_shortcuts->{$subject} : qr"(?:^|/)${subject}$", $perms]); + } + } + if ($acltext ne "") { + $line->log_fatal("Invalid ACL: ${acltext}."); + } + + return $acls; +} + +sub get_vartypes_basictypes() { + state $result = undef; + return $result if defined($result); + + my $lines = load_file($program); + my $types = {}; + assert($lines, "Couldn't load pkglint.pl from $program"); + foreach my $line (@$lines) { + if ($line->text =~ m"^\s+(\w+) => sub \{$") { + # XXX lookup in %type_dispatch instead + $types->{$1} = 1; + } + } + return ($result = $types); +} + +sub get_vartypes_map() { + state $result = undef; + return $result if defined($result); + + my ($fname, $vartypes); + + use constant re_acl_def => qr"^ + acl \s+ + (\w+) \s+ # ACL name + = \s+ + \[ ([^\]]*) \] # ACL value + (?:\s*\#.*)? # optional comment + $"x; + + use constant re_vartypedef => qr"^ + ([\w\d_.]+?) # $1 = variable name + (\*|\.\*|) \s+ # $2 = parameterized? + (?:(InternalList|List) \s+ of \s+)? # $3 ?= kind of list + (?:([\w\d_]+) # $4 ?= basic type + | \{\s*([\w\d_+,\-.\s]+?)\s*\}) # $5 ?= enumeration values + (?:\s+ \[ ([^\]]*) \])? # $6 ?= optional ACL + (?:\s*\#.*)? # $7 ?= optional comment + $"x; + + $fname = conf_datadir."/makevars.map"; + $vartypes = {}; + + my $lines = load_existing_lines($fname, true); + foreach my $line (@{$lines}) { + if ($line->text =~ m"^(?:#.*|\s*)$") { + # ignore empty and comment lines + + } elsif ($line->text =~ re_acl_def) { + my ($aclname, $aclvalue) = ($1, $2); + + $acl_definitions->{$aclname} = parse_acls($line, $aclvalue); + + } elsif ($line->text =~ re_vartypedef) { + my ($varname, $par, $kind_of_list_text, $typename, $enums, $acltext) = ($1, $2, $3, $4, $5, $6); + my $kind_of_list = !defined($kind_of_list_text) ? LK_NONE + : ($kind_of_list_text eq "List") ? LK_EXTERNAL + : LK_INTERNAL; + + if (defined($typename) && !exists(get_vartypes_basictypes()->{$typename})) { + $line->log_fatal("Unknown basic type \"$typename\" for variable $varname. " + . "Valid basic types are " + . join(", ", sort keys %{get_vartypes_basictypes()}) + . "."); + } + + my $basic_type = defined($enums) + ? array_to_hash(split(qr"\s+", $enums)) + : $typename; + my $type = PkgLint::Type->new($kind_of_list, $basic_type, parse_acls($line, $acltext), NOT_GUESSED); + if ($par eq "" || $par eq "*") { + $vartypes->{$varname} = $type; + } + if ($par eq "*" || $par eq ".*") { + $vartypes->{"${varname}.*"} = $type; + } + + } else { + $line->log_fatal("Unknown line format."); + } + } + +# TODO: Enable when the time is ripe. +if (false) { + # Additionally, scan mk/defaults/mk.conf for variable + # definitions. All these variables are reserved for the user and + # must not be set within packages. + $fname = "${cwd_pkgsrcdir}/mk/defaults/mk.conf"; + if ((my $lines = load_file($fname))) { + foreach my $line (@{$lines}) { + if ($line->text =~ m"^#?([\w_]+)\?=") { + my ($varname) = ($1); + $opt_debug_misc and $line->log_debug("Found user-definable variable ${varname}."); + $vartypes->{$varname} = "Userdefined"; # FIXME: type error + } + } + } else { + log_fatal($fname, NO_LINE_NUMBER, "Cannot be read."); + } +} + + return ($result = $vartypes); +} + +sub get_deprecated_map() { + state $result = undef; + return $result if defined($result); + + my ($fname, $lines, $vars); + + $fname = conf_datadir."/deprecated.map"; + if (!($lines = load_file($fname))) { + log_fatal($fname, NO_LINE_NUMBER, "Cannot be read."); + } + + $vars = {}; + foreach my $line (@{$lines}) { + if ($line->text =~ m"^#" || $line->text =~ m"^\s*$") { + # Ignore empty and comment lines. + + } elsif ($line->text =~ m"^(\S+)\s+(.*)$") { + $vars->{$1} = $2; + + } else { + $line->log_fatal("Unknown line format."); + } + } + return ($result = $vars); +} + +my $load_dist_sites_url2name = undef; +my $load_dist_sites_names = undef; +sub load_dist_sites() { + my ($fname) = ("${cwd_pkgsrcdir}/mk/fetch/sites.mk"); + my ($lines) = load_file($fname); + my ($varname) = undef; + my ($ignoring) = false; + my ($url2name) = {}; + my ($names) = {}; + + if (!$lines) { + log_error($fname, NO_LINE_NUMBER, "Could not be read."); + $load_dist_sites_url2name = $url2name; + $load_dist_sites_names = $names; + return; + } + foreach my $line (@{$lines}) { + my $text = $line->text; + + if ($text =~ m"^(MASTER_SITE_\w+)\+=\s*\\$"o) { + $varname = $1; + $names->{$varname} = true; + $ignoring = false; + + } elsif ($text eq "MASTER_SITE_BACKUP?=\t\\") { + $ignoring = true; + + } elsif ($text =~ m"^\t((?:http://|https://|ftp://)\S+/)(?:|\s*\\)$"o) { + if (!$ignoring) { + if (defined($varname)) { + $url2name->{$1} = $varname; + } else { + $line->log_error("Lonely URL found."); + } + } + + } elsif ($text =~ m"^(?:#.*|\s*)$") { + # ignore empty and comment lines + + } elsif ($text =~ m"BSD_SITES_MK") { + # ignore multiple inclusion guards + + } else { + $line->log_fatal("Unknown line type."); + } + } + + # Explicitly allowed, although not defined in mk/fetch/sites.mk. + $names->{"MASTER_SITE_SUSE_UPD"} = true; + $names->{"MASTER_SITE_LOCAL"} = true; + + $opt_debug_misc and log_debug($fname, NO_LINES, "Loaded " . scalar(keys(%{$url2name})) . " MASTER_SITE_* definitions."); + $load_dist_sites_url2name = $url2name; + $load_dist_sites_names = $names; +} + +sub get_dist_sites() { + if (!defined($load_dist_sites_url2name)) { + load_dist_sites(); + } + return $load_dist_sites_url2name; +} + +sub get_dist_sites_names() { + if (!defined($load_dist_sites_names)) { + load_dist_sites(); + } + return $load_dist_sites_names; +} + +sub get_pkg_options() { + state $result = undef; + return $result if defined($result); + + my ($fname) = ("${cwd_pkgsrcdir}/mk/defaults/options.description"); + my ($lines, $options); + + if (!($lines = load_file($fname))) { + log_fatal($fname, NO_LINE_NUMBER, "Cannot be read."); + } + + $options = {}; + foreach my $line (@{$lines}) { + if ($line->text =~ m"^([-0-9a-z_+]+)(?:\s+(.*))?$") { + my ($optname, $optdescr) = ($1, $2); + + $options->{$optname} = defined($optdescr) + ? $optdescr + : ""; + } else { + $line->log_error("Unknown line format."); + } + } + + return ($result = $options); +} + +my $load_tool_names_system_build_defs = undef; # XXX: misplaced, but works +my $load_tool_names_tools = undef; +my $load_tool_names_vartools = undef; +my $load_tool_names_varname_to_toolname = undef; +my $load_tool_names_predefined_tools = undef; +sub load_tool_names() { + my ($tools, $vartools, $predefined_tools, $varname_to_toolname, @tool_files); + my ($system_build_defs); + + # + # Get the list of files that define the tools from bsd.tools.mk. + # + + @tool_files = ("defaults.mk"); + { + my $fname = "${cwd_pkgsrcdir}/mk/tools/bsd.tools.mk"; + my $lines = load_existing_lines($fname, true); + foreach my $line (@{$lines}) { + if ($line->text =~ regex_mk_include) { + my (undef, $includefile) = ($1, $2); + if ($includefile =~ m"^(?:\$\{PKGSRCDIR\}/mk/tools/)?([^/]+)$") { + push(@tool_files, $1); + } + } + } + } + assert(scalar(@tool_files) > 1, "Too few tool files. Maybe the files have been renamed again?"); + + # + # Scan the tool files for the actual definitions of the tools. + # + + $tools = {}; + $vartools = {}; + $predefined_tools = {}; + $varname_to_toolname = {}; + $system_build_defs = {}; + foreach my $basename (@tool_files) { + my $fname = "${cwd_pkgsrcdir}/mk/tools/${basename}"; + my $lines = load_existing_lines($fname, true); + foreach my $line (@{$lines}) { + if ($line->text =~ regex_varassign) { + my ($varname, undef, $value, undef) = ($1, $2, $3, $4); + if ($varname eq "TOOLS_CREATE" && $value =~ m"^([-\w.]+|\[)$") { + $tools->{$value} = true; + + } elsif ($varname =~ m"^(?:_TOOLS_VARNAME)\.([-\w.]+|\[)$") { + $tools->{$1} = true; + $vartools->{$1} = $value; + $varname_to_toolname->{$value} = $1; + + } elsif ($varname =~ m"^(?:TOOLS_PATH|_TOOLS_DEPMETHOD)\.([-\w.]+|\[)$") { + $tools->{$1} = true; + + } elsif ($varname =~ m"^_TOOLS\.(.*)") { + $tools->{$1} = true; + foreach my $tool (split(qr"\s+", $value)) { + $tools->{$tool} = true; + } + } + } + } + } + + foreach my $basename ("bsd.pkg.mk") { + my $fname = "${cwd_pkgsrcdir}/mk/${basename}"; + my $cond_depth = 0; + + my $lines = load_existing_lines($fname, true); + foreach my $line (@{$lines}) { + my $text = $line->text; + + if ($text =~ regex_varassign) { + my ($varname, undef, $value, undef) = ($1, $2, $3, $4); + + if ($varname eq "USE_TOOLS") { + $opt_debug_tools and $line->log_debug("[cond_depth=${cond_depth}] $value"); + if ($cond_depth == 0) { + foreach my $tool (split(qr"\s+", $value)) { + if ($tool !~ regex_unresolved && exists($tools->{$tool})) { + $predefined_tools->{$tool} = true; + # The path (without arguments) to the tool + $predefined_tools->{"TOOLS_${tool}"} = true; + } + } + } + } elsif ($varname eq "_BUILD_DEFS") { + foreach my $bdvar (split(qr"\s+", $value)) { + $system_build_defs->{$bdvar} = true; + } + } + + } elsif ($text =~ regex_mk_cond) { + my ($indent, $cond, $args, $comment) = ($1, $2, $3, $4); + + if ($cond =~ m"^(?:if|ifdef|ifndef|for)$") { + $cond_depth++; + } elsif ($cond =~ m"^(?:endif|endfor)$") { + $cond_depth--; + } + } + } + } + + $opt_debug_tools and log_debug(NO_FILE, NO_LINE_NUMBER, "Known tools: ".join(" ", sort(keys(%{$tools})))); + $opt_debug_tools and log_debug(NO_FILE, NO_LINE_NUMBER, "Known vartools: ".join(" ", sort(keys(%{$vartools})))); + $opt_debug_tools and log_debug(NO_FILE, NO_LINE_NUMBER, "Predefined tools: " . join(" ", sort(keys(%{$predefined_tools})))); + $opt_debug_tools and log_debug(NO_FILE, NO_LINE_NUMBER, "Known varnames: " . join(" ", sort(keys(%{$varname_to_toolname})))); + $opt_debug_misc and log_debug(NO_FILE, NO_LINES, "System-provided BUILD_DEFS: " . join(" ", sort(keys(%{$system_build_defs})))); + + # Some user-defined variables do not influence the binary + # package at all and therefore do not have to be added to + # BUILD_DEFS. + foreach my $bdvar (qw(DISTDIR FETCH_CMD FETCH_OUTPUT_ARGS GAMES_USER GAMES_GROUP GAMEDATAMODE GAMEDIRMODE GAMEMODE GAMEOWN GAMEGRP )) { + $system_build_defs->{$bdvar} = true; + } + #$system_build_defs->{"PACKAGES"} = true; + + $load_tool_names_tools = $tools; + $load_tool_names_vartools = $vartools; + $load_tool_names_predefined_tools = $predefined_tools; + $load_tool_names_varname_to_toolname = $varname_to_toolname; + $load_tool_names_system_build_defs = $system_build_defs; +} + +# Returns the set of known tool names and contains for example "sed" and +# "gm4". +sub get_tool_names() { + + if (!defined($load_tool_names_tools)) { + load_tool_names(); + } + return $load_tool_names_tools; +} + +# Returns the mapping from tool names to their respective variable. For +# example, "sed" => "SED", "gzip" => "GZIP_CMD". +sub get_vartool_names() { + + if (!defined($load_tool_names_vartools)) { + load_tool_names(); + } + return $load_tool_names_vartools; +} + +# Returns the set of those tools with associated variables that a +# package does not need to add to USE_TOOLS explicitly because they +# are used by the pkgsrc infrastructure, too. +sub get_predefined_tool_names() { + if (!defined($load_tool_names_predefined_tools)) { + load_tool_names(); + } + return $load_tool_names_predefined_tools; +} + +# Returns a mapping from tool variable names to the tool name they use. +# For example, "GZIP_CMD" => "gzip" and "SED" => "sed". +sub get_varname_to_toolname() { + if (!defined($load_tool_names_varname_to_toolname)) { + load_tool_names(); + } + return $load_tool_names_varname_to_toolname; +} + +# Returns the set of tool variable names that may not be converted to +# their "direct" form, that is: ${CP} => cp. +sub get_required_vartool_varnames() { + use constant required_vartool_varnames => array_to_hash(qw(ECHO ECHO_N FALSE TEST TRUE)); + + return required_vartool_varnames; +} + +# Returns the set of tools that must be used by their variable name. +sub get_required_vartools() { + use constant required_vartools => array_to_hash(qw(echo false test true)); + + return required_vartools; +} + + +# Returns the set of user-defined variables that are added to BUILD_DEFS +# within the bsd.pkg.mk file. +sub get_system_build_defs() { + if (!defined($load_tool_names_system_build_defs)) { + load_tool_names(); + } + return $load_tool_names_system_build_defs; +} + +sub load_doc_TODO_updates($) { + my ($fname) = @_; + my ($lines, $updates, $state, $re_suggested_update); + + if (!($lines = load_file($fname))) { + log_fatal($fname, NO_LINE_NUMBER, "Cannot be read."); + } + + $updates = []; + $state = 0; + foreach my $line (@{$lines}) { + my $text = $line->text; + + if ($state == 0 && $text eq "Suggested package updates") { + $state = 1; + } elsif ($state == 1 && $text eq "") { + $state = 2; + } elsif ($state == 2) { + $state = 3; + } elsif ($state == 3 && $text eq "") { + $state = 4; + } + + if ($state == 3) { + if ($text =~ m"^\to\s(\S+)(?:\s*(.+))?$") { + my ($spuname, $comment) = ($1, $2); + if ($spuname =~ regex_pkgname) { + push(@{$updates}, [$line, $1, $2, $comment]); + } else { + $line->log_warning("Invalid package name $spuname"); + } + } else { + $line->log_warning("Invalid line format $text"); + } + } + } + + return $updates; +} + +sub get_doc_TODO_updates() { + state $result = load_doc_TODO_updates("${cwd_pkgsrcdir}/doc/TODO"); + return $result; +} + +sub get_wip_TODO_updates() { + state $result = load_doc_TODO_updates("${cwd_pkgsrcdir}/wip/TODO"); + return $result; +} + +sub load_doc_CHANGES($) { + my ($fname) = @_; + my $lines = load_file($fname) or die; + + my $changes = {}; # { pkgpath -> @changes } + foreach my $line (@$lines) { + my $text = $line->text; + next unless $text =~ m"^\t[A-Z]"; + + if ($text =~ m"^\t(Updated) (\S+) to (\S+) \[(\S+) (\d\d\d\d-\d\d-\d\d)\]$") { + push(@{$changes->{$2}}, PkgLint::Change->new($line, $1, $2, $3, $4, $5)); + } elsif ($text =~ m"^\t(Added) (\S+) version (\S+) \[(\S+) (\d\d\d\d-\d\d-\d\d)\]$") { + push(@{$changes->{$2}}, PkgLint::Change->new($line, $1, $2, $3, $4, $5)); + } elsif ($text =~ m"^\t(Removed) (\S+) (?:successor (\S+) )?\[(\S+) (\d\d\d\d-\d\d-\d\d)\]$") { + push(@{$changes->{$2}}, PkgLint::Change->new($line, $1, $2, undef, $3, $4)); + } elsif ($text =~ m"^\t(Downgraded) (\S+) to (\S+) \[(\S+) (\d\d\d\d-\d\d-\d\d)\]$") { + push(@{$changes->{$2}}, PkgLint::Change->new($line, $1, $2, $3, $4, $5)); + } elsif ($text =~ m"^\t(Renamed|Moved) (\S+) to (\S+) \[(\S+) (\d\d\d\d-\d\d-\d\d)\]$") { + push(@{$changes->{$2}}, PkgLint::Change->new($line, $1, $2, $3, $4, $5)); + } else { + $line->log_warning("Unknown doc/CHANGES line: " . $line->text); + $line->explain_warning( +"Maybe some developer didn't stick to the conventions that have been", +"established by mk/misc/developer.mk?"); + } + } + return $changes; +} + +my $get_doc_CHANGES_docs = undef; # [ $fname, undef or { pkgpath -> @changes } ] +sub get_doc_CHANGES($) { + my ($pkgpath) = @_; + + $opt_debug_trace and log_debug(NO_FILE, NO_LINES, "get_doc_CHANGES(\"$pkgpath\")"); + + # Make a reversed list of all the CHANGES-* files, but don't load + # them yet. + if (!defined($get_doc_CHANGES_docs)) { + opendir(DIR, "${cwd_pkgsrcdir}/doc") or die; + my @files = readdir(DIR); + closedir(DIR) or die; + foreach my $file (reverse sort @files) { + if ($file =~ m"^CHANGES-(\d+)$" && (0 + $1 >= 2008)) { + push(@$get_doc_CHANGES_docs, [ $file, undef ]); + } + } + $opt_debug_misc and log_debug(NO_FILE, NO_LINES, "Found " . (scalar @$get_doc_CHANGES_docs) . " changes files."); + } + + # Scan the *-CHANGES files in reverse order until some action + # matches the package directory. + my @result = (); + foreach my $doc (@$get_doc_CHANGES_docs) { + if (!defined($doc->[1])) { + $opt_debug_misc and log_debug(NO_FILE, NO_LINES, "loading $doc->[0]"); + $doc->[1] = load_doc_CHANGES("${cwd_pkgsrcdir}/doc/$doc->[0]"); + } + + foreach my $change (@{$doc->[1]->{$pkgpath}}) { + next unless $pkgpath eq $change->pkgpath; + push(@result, $change); + } + if (@result != 0) { + return @result; + } + } + return (); +} + +sub get_suggested_package_updates() { + + return ($is_wip) + ? get_wip_TODO_updates() + : get_doc_TODO_updates(); +} + +# Load all variables from mk/defaults/mk.conf. Since pkglint does not +# load the infrastructure files during normal operation, these +# definitions have to be added explicitly. +sub load_userdefined_variables() { + my $fname = "${cwd_pkgsrcdir}/mk/defaults/mk.conf"; + my $vars = {}; + + my $lines = load_existing_lines($fname, true); + foreach my $line (@{$lines}) { + if ($line->text =~ regex_varassign) { + my ($varname, $op, $value, $comment) = ($1, $2, $3, $4); + + $vars->{$varname} = $line; + } + } + + return $vars; +} + +sub get_userdefined_variables() { + state $result = load_userdefined_variables(); + return $result; +} + +# +# Miscellaneous functions +# + +sub match_all($$) { + my ($text, $re) = @_; + my ($mm, $rest, $lastpos); + + $mm = []; + $rest = $text; + $lastpos = 0; + pos(undef); + while ($rest =~ m/$re/gc) { + my @starts = @-; + my @ends = @+; + + $lastpos = $ends[0]; + + push(@{$mm}, PkgLint::SimpleMatch->new($text, \@starts, \@ends)); + } + return ($mm, substr($rest, $lastpos)); +} + +sub autofix($) { + my ($lines) = @_; + + if ($opt_autofix) { + save_autofix_changes($lines); + } +} + +# Checks whether a file is already committed to the CVS repository or not. +sub is_committed($) { + my ($fname) = @_; + my ($basename, $entries); + + $basename = basename($fname); + $entries = load_file(dirname($fname) . "/CVS/Entries"); + if (!$entries) { + return false; + } + foreach my $entry (@{$entries}) { + if ($entry->text =~ m"^/\Q${basename}\E/") { + return true; + } + } + return false; +} + +# Checks whether a directory is practically empty, that is, all it +# contains are ".", ".." and "CVS", recursively. +sub is_emptydir($); +sub is_emptydir($) { + my ($dir) = @_; + my ($rv); + + if (!opendir(DIR, $dir)) { + return true; + } + my @subdirs = readdir(DIR); + closedir(DIR) or die; + + $rv = true; + foreach my $subdir (@subdirs) { + next if $subdir eq "." || $subdir eq ".." || $subdir eq "CVS"; + next if -d "${dir}/${subdir}" && is_emptydir("${dir}/${subdir}"); + + $rv = false; + last; + } + + return $rv; +} + +# Returns the list of subdirectories of a directory, except "CVS". +sub get_subdirs($) { + my ($dir) = @_; + my (@result) = (); + + if (opendir(DIR, $dir)) { + my @subdirs = readdir(DIR); + closedir(DIR) or die; + foreach my $subdir (@subdirs) { + if ($subdir ne "." && $subdir ne ".." && $subdir ne "CVS" && -d "${dir}/${subdir}" && !is_emptydir("${dir}/${subdir}")) { + push(@result, $subdir); + } + } + } + return @result; +} + +# No package file should ever be executable. Even the INSTALL and +# DEINSTALL scripts are usually not usable in the form they have in the +# package, as the pathnames get adjusted during installation. So there is +# no need to have any file executable. +sub checkperms($) { + my ($fname) = @_; + + if (-f $fname && -x $fname && !is_committed($fname)) { + log_warning($fname, NO_LINE_NUMBER, "Should not be executable."); + } +} + +sub resolve_relative_path($$) { + my ($relpath, $adjust_depth) = @_; + + my $arg = $relpath; + $relpath =~ s,\$\{PKGSRCDIR\},$cur_pkgsrcdir,; + $relpath =~ s,\$\{\.CURDIR\},.,; + $relpath =~ s,\$\{\.PARSEDIR\},.,; + $relpath =~ s,\$\{LUA_PKGSRCDIR\},../../lang/lua52,; + $relpath =~ s,\$\{PHPPKGSRCDIR\},../../lang/php54,; + $relpath =~ s,\$\{SUSE_DIR_PREFIX\},suse100,; + $relpath =~ s,\$\{PYPKGSRCDIR\},../../lang/python27,; + $relpath =~ s,\$\{FILESDIR\},$filesdir, if defined($filesdir); + if ($adjust_depth && $relpath =~ m"^\.\./\.\./([^.].*)$") { + $relpath = "${cur_pkgsrcdir}/$1"; + } + if (defined($pkgdir)) { + $relpath =~ s,\$\{PKGDIR\},$pkgdir,g; + } + + $opt_debug_misc and log_debug(NO_FILE, NO_LINES, "resolve_relative_path: $arg => $relpath"); + return $relpath; +} + +# Takes two pathnames and returns the relative pathname to get from +# the first to the second. +sub relative_path($$) { + my ($from, $to) = @_; + + my $abs_from = Cwd::abs_path($from); + my $abs_to = Cwd::abs_path($to); + if ($abs_to =~ m"^\Q$abs_from/\E(.*)$") { + return $1; + } elsif ($abs_to eq $abs_from) { + return "."; + } else { + assert(false, "$abs_from is not a prefix of $abs_to"); + } +} + +sub resolve_variable_rec1($$); +sub resolve_variable_rec2($$); + +sub resolve_variable_rec1($$) { + my ($varname, $visited) = @_; + $opt_debug_trace and log_debug(NO_FILE, NO_LINES, "resolve_variable_rec1($varname)"); + + if (!exists($visited->{$varname})) { + $visited->{$varname} = true; + if (defined($pkgctx_vardef) && exists($pkgctx_vardef->{$varname})) { + return resolve_variable_rec2($pkgctx_vardef->{$varname}->get("value"), $visited); + } + if (defined($mkctx_vardef) && exists($mkctx_vardef->{$varname})) { + return resolve_variable_rec2($mkctx_vardef->{$varname}->get("value"), $visited); + } + } + return "\${$varname}"; +} + +sub resolve_variable_rec2($$) { + my ($string, $visited) = @_; + $opt_debug_trace and log_debug(NO_FILE, NO_LINES, "resolve_variable_rec2(\"$string\")"); + + my $expanded = $string; + $expanded =~ s/\$\{(\w+)\}/resolve_variable_rec1($1, $visited)/eg; + return $expanded; +} + +sub expand_variable($) { + my ($varname) = @_; + + return unless exists($pkgctx_vardef->{$varname}); + my $line = $pkgctx_vardef->{$varname}; + my $value = $line->get("value"); + + $value = resolve_relative_path($value, true); + if ($value =~ regex_unresolved) { + $opt_debug_misc and log_debug(NO_FILE, NO_LINES, "[expand_variable] Trying harder to resolve variable references in ${varname}=\"${value}\"."); + $value = resolve_variable_rec2($value, {}); + if ($value =~ regex_unresolved) { + $opt_debug_misc and log_debug(NO_FILE, NO_LINES, "[expand_variable] Failed to resolve ${varname}=\"${value}\"."); + } + } + return $value; +} + +sub set_default_value($$) { + my ($varref, $value) = @_; + + if (!defined(${$varref}) || ${$varref} =~ regex_unresolved) { + ${$varref} = $value; + } +} + +sub strip_mk_comment($) { + my ($text) = @_; + + $text =~ s/(^|[^\\])#.*/$1/; + $text =~ s/\\#/#/g; + return $text; +} + +# Removes all uses of make variables from a string. +sub remove_variables($) { + my ($text) = @_; + + while ($text =~ s/\$\{([^{}]*)\}//g) { + } + return $text; +} + +sub backtrace($) { + my $msg = shift(); + my (@callers); + + my $n = 0; + while (my @info = caller($n)) { + push(@callers, [$info[2], $info[3]]); + $n++; + } + + log_debug(NO_FILE, NO_LINE_NUMBER, $msg); + for (my $i = $#callers; $i >= 0; $i--) { + my $info = $callers[$i]; + log_debug(NO_FILE, NO_LINE_NUMBER, sprintf(" line %4d called %s", $info->[0], $info->[1])); + } +} + +# Returns the number of columns that a string occupies when printed with +# a tabulator size of 8. +sub tablen($) { + my ($s) = @_; + my ($len); + + $len = 0; + foreach my $c (split(qr"", $s)) { + if ($c eq "\t") { + $len = ($len + 7) & ~7; + } else { + $len++; + } + } + return $len; +} + +sub shell_split($) { + my ($text) = @_; + my ($words); + + $words = []; + while ($text =~ s/^$regex_shellword//) { + push(@{$words}, $1); + } + return (($text =~ m"^\s*$") ? $words : false); +} + +sub varname_base($) { + my ($varname) = @_; + + return ($varname =~ m"^(.*?)\..*$") ? $1 : $varname; +} + +sub varname_canon($) { + my ($varname) = @_; + + return ($varname =~ m"^(.*?)\..*$") ? "$1.*" : $varname; +} + +sub varname_param($) { + my ($varname) = @_; + + return ($varname =~ m"^.*?\.(.*)$") ? $2 : undef; +} + +sub use_var($$) { + my ($line, $varname) = @_; + my $varcanon = varname_canon($varname); + + if (defined($mkctx_varuse)) { + $mkctx_varuse->{$varname} = $line; + $mkctx_varuse->{$varcanon} = $line; + } + + if (defined($pkgctx_varuse)) { + $pkgctx_varuse->{$varname} = $line; + $pkgctx_varuse->{$varcanon} = $line; + } +} + +sub var_is_used($) { + my ($varname) = @_; + my $varcanon = varname_canon($varname); + + if (defined($mkctx_varuse)) { + return $mkctx_varuse->{$varname} if exists($mkctx_varuse->{$varname}); + return $mkctx_varuse->{$varcanon} if exists($mkctx_varuse->{$varcanon}); + } + if (defined($pkgctx_varuse)) { + return $pkgctx_varuse->{$varname} if exists($pkgctx_varuse->{$varname}); + return $pkgctx_varuse->{$varcanon} if exists($pkgctx_varuse->{$varcanon}); + } + return false; +} + +sub def_var($$) { + my ($line, $varname) = @_; + my $varcanon = varname_canon($varname); + + if (defined($mkctx_vardef)) { + $mkctx_vardef->{$varname} = $line; + $mkctx_vardef->{$varcanon} = $line; + } + + if (defined($pkgctx_vardef)) { + $pkgctx_vardef->{$varname} = $line; + $pkgctx_vardef->{$varcanon} = $line; + } +} + +sub var_is_defined($) { + my ($varname) = @_; + my $varcanon = varname_canon($varname); + + if (defined($mkctx_vardef)) { + return $mkctx_vardef->{$varname} if exists($mkctx_vardef->{$varname}); + return $mkctx_vardef->{$varcanon} if exists($mkctx_vardef->{$varcanon}); + } + if (defined($pkgctx_vardef)) { + return $pkgctx_vardef->{$varname} if exists($pkgctx_vardef->{$varname}); + return $pkgctx_vardef->{$varcanon} if exists($pkgctx_vardef->{$varcanon}); + } + return false; +} + +sub determine_used_variables($) { + my ($lines) = @_; + my ($rest); + + foreach my $line (@{$lines}) { + $rest = $line->text; + while ($rest =~ s/(?:\$\{|\$\(|defined\(|empty\()([0-9+.A-Z_a-z]+)[:})]//) { + my ($varname) = ($1); + use_var($line, $varname); + $opt_debug_unused and $line->log_debug("Variable ${varname} is used."); + } + } +} + +sub extract_used_variables($$) { + my ($line, $text) = @_; + my ($rest, $result); + + $rest = $text; + $result = []; + while ($rest =~ s/^(?:[^\$]+|\$[\$*<>?\@]|\$\{([.0-9A-Z_a-z]+)(?::(?:[^\${}]|\$[^{])+)?\})//) { + my ($varname) = ($1); + + if (defined($varname)) { + push(@{$result}, $varname); + } + } + + if ($rest ne "") { + $opt_debug_misc and $line->log_warning("Could not extract variables: ${rest}"); + } + + return $result; +} + +sub get_nbpart() { + my $line = $pkgctx_vardef->{"PKGREVISION"}; + return "" unless defined($line); + my $pkgrevision = $line->get("value"); + return "" unless $pkgrevision =~ m"^\d+$"; + return "" unless $pkgrevision + 0 != 0; + return "nb$pkgrevision"; +} + +sub check_pkglint_version() { + state $done = false; + return if $done; + $done = true; + + my $lines = load_lines("${cwd_pkgsrcdir}/pkgtools/pkglint/Makefile", true); + return unless $lines; + + my $pkglint_version = undef; + foreach my $line (@{$lines}) { + if ($line->text =~ regex_varassign) { + my ($varname, undef, $value, undef) = ($1, $2, $3, $4); + + if ($varname eq "DISTNAME" || $varname eq "PKGNAME") { + if ($value =~ regex_pkgname) { + $pkglint_version = $2; + } + } + } + } + return unless defined($pkglint_version); + + if (dewey_cmp($pkglint_version, ">", conf_distver)) { + log_note(NO_FILE, NO_LINE_NUMBER, "A newer version of pkglint is available."); + } elsif (dewey_cmp($pkglint_version, "<", conf_distver)) { + log_error(NO_FILE, NO_LINE_NUMBER, "The pkglint version is newer than the tree to check."); + } +} + +# When processing a file using the expect* subroutines below, it may +# happen that $lineno points past the end of the file. In that case, +# print the warning without associated source code. +sub lines_log_warning($$$) { + my ($lines, $lineno, $msg) = @_; + + assert(false, "The line number is negative (${lineno}).") + unless 0 <= $lineno; + assert(@{$lines} != 0, "The lines may not be empty."); + + if ($lineno <= $#{$lines}) { + $lines->[$lineno]->log_warning($msg); + } else { + log_warning($lines->[0]->fname, "EOF", $msg); + } +} + +# Checks if the current line ($lines->{${$lineno_ref}}) matches the +# regular expression, and if it does, increments ${${lineno_ref}}. +# @param $lines +# The lines that are checked. +# @param $lineno_ref +# A reference to the line number, an integer variable. +# @param $regex +# The regular expression to be checked. +# @return +# The result of the regular expression match or false. +sub expect($$$) { + my ($lines, $lineno_ref, $regex) = @_; + my $lineno = ${$lineno_ref}; + + if ($lineno <= $#{$lines} && $lines->[$lineno]->text =~ $regex) { + ${$lineno_ref}++; + return PkgLint::SimpleMatch->new($lines->[$lineno]->text, \@-, \@+); + } else { + return false; + } +} + +sub expect_empty_line($$) { + my ($lines, $lineno_ref) = @_; + + if (expect($lines, $lineno_ref, qr"^$")) { + return true; + } else { + $opt_warn_space and $lines->[${$lineno_ref}]->log_note("Empty line expected."); + return false; + } +} + +sub expect_text($$$) { + my ($lines, $lineno_ref, $text) = @_; + + my $rv = expect($lines, $lineno_ref, qr"^\Q${text}\E$"); + $rv or lines_log_warning($lines, ${$lineno_ref}, "Expected \"${text}\"."); + return $rv; +} + +sub expect_re($$$) { + my ($lines, $lineno_ref, $re) = @_; + + my $rv = expect($lines, $lineno_ref, $re); + $rv or lines_log_warning($lines, ${$lineno_ref}, "Expected text matching $re."); + return $rv; +} + +# Returns an object of type Pkglint::Type that represents the type of +# the variable (maybe guessed based on the variable name), or undef if +# the type cannot even be guessed. +# +sub get_variable_type($$) { + my ($line, $varname) = @_; + my ($type); + + assert(defined($varname), "The varname parameter must be defined."); + + if (exists(get_vartypes_map()->{$varname})) { + return get_vartypes_map()->{$varname}; + } + + my $varcanon = varname_canon($varname); + if (exists(get_vartypes_map()->{$varcanon})) { + return get_vartypes_map()->{$varcanon}; + } + + if (exists(get_varname_to_toolname()->{$varname})) { + return PkgLint::Type->new(LK_NONE, "ShellCommand", [[ qr".*", "u" ]], NOT_GUESSED); + } + + if ($varname =~ m"^TOOLS_(.*)" && exists(get_varname_to_toolname()->{$1})) { + return PkgLint::Type->new(LK_NONE, "Pathname", [[ qr".*", "u" ]], NOT_GUESSED); + } + + use constant allow_all => [[ qr".*", "adpsu" ]]; + use constant allow_runtime => [[ qr".*", "adsu" ]]; + + # Guess the datatype of the variable based on + # naming conventions. + $type = ($varname =~ m"DIRS$") ? PkgLint::Type->new(LK_EXTERNAL, "Pathmask", allow_runtime, GUESSED) + : ($varname =~ m"(?:DIR|_HOME)$") ? PkgLint::Type->new(LK_NONE, "Pathname", allow_runtime, GUESSED) + : ($varname =~ m"FILES$") ? PkgLint::Type->new(LK_EXTERNAL, "Pathmask", allow_runtime, GUESSED) + : ($varname =~ m"FILE$") ? PkgLint::Type->new(LK_NONE, "Pathname", allow_runtime, GUESSED) + : ($varname =~ m"PATH$") ? PkgLint::Type->new(LK_NONE, "Pathlist", allow_runtime, GUESSED) + : ($varname =~ m"PATHS$") ? PkgLint::Type->new(LK_EXTERNAL, "Pathname", allow_runtime, GUESSED) + : ($varname =~ m"_USER$") ? PkgLint::Type->new(LK_NONE, "UserGroupName", allow_all, GUESSED) + : ($varname =~ m"_GROUP$") ? PkgLint::Type->new(LK_NONE, "UserGroupName", allow_all, GUESSED) + : ($varname =~ m"_ENV$") ? PkgLint::Type->new(LK_EXTERNAL, "ShellWord", allow_runtime, GUESSED) + : ($varname =~ m"_CMD$") ? PkgLint::Type->new(LK_NONE, "ShellCommand", allow_runtime, GUESSED) + : ($varname =~ m"_ARGS$") ? PkgLint::Type->new(LK_EXTERNAL, "ShellWord", allow_runtime, GUESSED) + : ($varname =~ m"_(?:C|CPP|CXX|LD|)FLAGS$") ? PkgLint::Type->new(LK_EXTERNAL, "ShellWord", allow_runtime, GUESSED) + : ($varname =~ m"_MK$") ? PkgLint::Type->new(LK_NONE, "Unchecked", allow_all, GUESSED) + : ($varname =~ m"^PLIST.") ? PkgLint::Type->new(LK_NONE, "Yes", allow_all, GUESSED) + : undef; + + if (defined($type)) { + $opt_debug_vartypes and $line->log_debug("The guessed type of ${varname} is \"" . $type->to_string . "\"."); + return $type; + } + + $opt_debug_vartypes and $line->log_debug("No type definition found for ${varcanon}."); + return undef; +} + +sub get_variable_perms($$) { + my ($line, $varname) = @_; + + my $type = get_variable_type($line, $varname); + if (!defined($type)) { + $opt_debug_misc and $line->log_debug("No type definition found for ${varname}."); + return "adpsu"; + } + + my $perms = $type->perms($line->fname, $varname); + if (!defined($perms)) { + $opt_debug_misc and $line->log_debug("No permissions specified for ${varname}."); + return "?"; + } + return $perms; +} + +# This function returns whether a variable needs the :Q operator in a +# certain context. There are four possible outcomes: +# +# false: The variable should not be quoted. +# true: The variable should be quoted. +# doesnt_matter: +# Since the values of the variable usually don't contain +# special characters, it does not matter whether the +# variable is quoted or not. +# dont_know: pkglint cannot say whether the variable should be quoted +# or not, most likely because type information is missing. +# +sub variable_needs_quoting($$$) { + my ($line, $varname, $context) = @_; + my $type = get_variable_type($line, $varname); + my ($want_list, $have_list); + + $opt_debug_trace and $line->log_debug("variable_needs_quoting($varname, " . $context->to_string() . ")"); + + use constant safe_types => array_to_hash(qw( + DistSuffix + FileMode Filename + Identifier + Option + Pathname PkgName PkgOptionsVar PkgRevision + RelativePkgDir RelativePkgPath + UserGroupName + Varname Version + WrkdirSubdirectory + )); + + if (!defined($type) || !defined($context->type)) { + return dont_know; + } + + # Variables of certain predefined types, as well as all + # enumerations, are expected to not require the :Q operator. + if (ref($type->basic_type) eq "HASH" || exists(safe_types->{$type->basic_type})) { + if ($type->kind_of_list == LK_NONE) { + return doesnt_matter; + + } elsif ($type->kind_of_list == LK_EXTERNAL && $context->extent != VUC_EXTENT_WORD_PART) { + return false; + } + } + + # In .for loops, the :Q operator is always misplaced, since + # the items are broken up at white-space, not as shell words + # like in all other parts of make(1). + if ($context->shellword == VUC_SHELLWORD_FOR) { + return false; + } + + # Determine whether the context expects a list of shell words or + # not. + $want_list = $context->type->is_practically_a_list() && ($context->shellword == VUC_SHELLWORD_BACKT || $context->extent != VUC_EXTENT_WORD_PART); + $have_list = $type->is_practically_a_list(); + + $opt_debug_quoting and $line->log_debug("[variable_needs_quoting]" + . " varname=$varname" + . " context=" . $context->to_string() + . " type=" . $type->to_string() + . " want_list=" . ($want_list ? "yes" : "no") + . " have_list=" . ($have_list ? "yes" : "no") + . "."); + + # A shell word may appear as part of a shell word, for example + # COMPILER_RPATH_FLAG. + if ($context->extent == VUC_EXTENT_WORD_PART && $context->shellword == VUC_SHELLWORD_PLAIN) { + if ($type->kind_of_list == LK_NONE && $type->basic_type eq "ShellWord") { + return false; + } + } + + # Assume that the tool definitions don't include very special + # characters, so they can safely be used inside any quotes. + if (exists(get_varname_to_toolname()->{$varname})) { + my $sw = $context->shellword; + + if ($sw == VUC_SHELLWORD_PLAIN && $context->extent != VUC_EXTENT_WORD_PART) { + return false; + + } elsif ($sw == VUC_SHELLWORD_BACKT) { + return false; + + } elsif ($sw == VUC_SHELLWORD_DQUOT || $sw == VUC_SHELLWORD_SQUOT) { + return doesnt_matter; + + } else { + # Let the other rules decide. + } + } + + # Variables that appear as parts of shell words generally need + # to be quoted. An exception is in the case of backticks, + # because the whole backticks expression is parsed as a single + # shell word by pkglint. + # + # XXX: When the shell word parser gets rewritten the next time, + # this test can be refined. + if ($context->extent == VUC_EXTENT_WORD_PART && $context->shellword != VUC_SHELLWORD_BACKT) { + return true; + } + + # Assigning lists to lists does not require any quoting, though + # there may be cases like "CONFIGURE_ARGS+= -libs ${LDFLAGS:Q}" + # where quoting is necessary. So let's hope that no developer + # ever makes the mistake of using :Q when appending a list to + # a list. + if ($want_list && $have_list) { + return doesnt_matter; + } + + # Appending elements to a list requires quoting, as well as + # assigning a list value to a non-list variable. + if ($want_list != $have_list) { + return true; + } + + $opt_debug_quoting and $line->log_debug("Don't know whether :Q is needed for ${varname}."); + return dont_know; +} + +# +# Parsing. +# + +# Checks whether $tree matches $pattern, and if so, instanciates the +# variables in $pattern. If they don't match, some variables may be +# instanciated nevertheless, but the exact behavior is unspecified. +# +sub tree_match($$); +sub tree_match($$) { + my ($tree, $pattern) = @_; + + my $d1 = Data::Dumper->new([$tree, $pattern])->Terse(true)->Indent(0); + my $d2 = Data::Dumper->new([$pattern])->Terse(true)->Indent(0); + $opt_debug_trace and log_debug(NO_FILE, NO_LINES, sprintf("tree_match(%s, %s)", $d1->Dump, $d2->Dump)); + + return true if (!defined($tree) && !defined($pattern)); + return false if (!defined($tree) || !defined($pattern)); + my $aref = ref($tree); + my $pref = ref($pattern); + if ($pref eq "SCALAR" && !defined($$pattern)) { + $$pattern = $tree; + return true; + } + if ($aref eq "" && ($pref eq "" || $pref eq "SCALAR")) { + return $tree eq $pattern; + } + if ($aref eq "ARRAY" && $pref eq "ARRAY") { + return false if scalar(@$tree) != scalar(@$pattern); + for (my $i = 0; $i < scalar(@$tree); $i++) { + return false unless tree_match($tree->[$i], $pattern->[$i]); + } + return true; + } + return false; +} + +# TODO: Needs to be redesigned to handle more complex expressions. +sub parse_mk_cond($$); +sub parse_mk_cond($$) { + my ($line, $cond) = @_; + + $opt_debug_trace and $line->log_debug("parse_mk_cond(\"${cond}\")"); + + my $re_simple_varname = qr"[A-Z_][A-Z0-9_]*(?:\.[\w_+\-]+)?"; + while ($cond ne "") { + if ($cond =~ s/^!//) { + return ["not", parse_mk_cond($line, $cond)]; + } elsif ($cond =~ s/^defined\((${re_simple_varname})\)$//) { + return ["defined", $1]; + } elsif ($cond =~ s/^empty\((${re_simple_varname})\)$//) { + return ["empty", $1]; + } elsif ($cond =~ s/^empty\((${re_simple_varname}):M([^\$:{})]+)\)$//) { + return ["empty", ["match", $1, $2]]; + } elsif ($cond =~ s/^\$\{(${re_simple_varname})\}\s+(==|!=)\s+"([^"\$\\]*)"$//) { + return [$2, ["var", $1], ["string", $3]]; + } else { + $opt_debug_unchecked and $line->log_debug("parse_mk_cond: ${cond}"); + return ["unknown", $cond]; + } + } +} + +sub parse_licenses($) { + my ($licenses) = @_; + + $licenses =~ s,\$\{PERL5_LICENSE},gnu-gpl-v2 OR artistic,g; + $licenses =~ s,[()]|AND|OR,,g; # XXX: treats OR like AND + my @licenses = split(/\s+/, $licenses); + return \@licenses; +} + +# This procedure fills in the extra fields of a line, depending on the +# line type. These fields can later be queried without having to parse +# them again and again. +# +sub parseline_mk($) { + my ($line) = @_; + my $text = $line->text; + + if ($text =~ regex_varassign) { + my ($varname, $op, $value, $comment) = ($1, $2, $3, $4); + + # In variable assignments, a '#' character is preceded + # by a backslash. In shell commands, it is interpreted + # literally. + $value =~ s/\\\#/\#/g; + + $line->set("is_varassign", true); + $line->set("varname", $varname); + $line->set("varcanon", varname_canon($varname)); + my $varparam = varname_param($varname); + defined($varparam) and $line->set("varparam", $varparam); + $line->set("op", $op); + $line->set("value", $value); + defined($comment) and $line->set("comment", $comment); + + } elsif ($text =~ regex_mk_shellcmd) { + my ($shellcmd) = ($1); + + # Shell command lines cannot have embedded comments. + $line->set("is_shellcmd", true); + $line->set("shellcmd", $shellcmd); + + my ($shellwords, $rest) = match_all($shellcmd, $regex_shellword); + $line->set("shellwords", $shellwords); + if ($rest !~ m"^\s*$") { + $line->set("shellwords_rest", $rest); + } + + } elsif ($text =~ regex_mk_comment) { + my ($comment) = ($1); + + $line->set("is_comment", true); + $line->set("comment", $comment); + + } elsif ($text =~ m"^\s*$") { + + $line->set("is_empty", true); + + } elsif ($text =~ regex_mk_cond) { + my ($indent, $directive, $args, $comment) = ($1, $2, $3, $4); + + $line->set("is_cond", true); + $line->set("indent", $indent); + $line->set("directive", $directive); + defined($args) and $line->set("args", $args); + defined($comment) and $line->set("comment", $comment); + + } elsif ($text =~ regex_mk_include) { + my (undef, $includefile, $comment) = ($1, $2, $3); + + $line->set("is_include", true); + $line->set("includefile", $includefile); + defined($comment) and $line->set("comment", $comment); + + } elsif ($text =~ regex_mk_sysinclude) { + my ($includefile, $comment) = ($1, $2); + + $line->set("is_sysinclude", true); + $line->set("includefile", $includefile); + defined($comment) and $line->set("comment", $comment); + + } elsif ($text =~ regex_mk_dependency) { + my ($targets, $whitespace, $sources, $comment) = ($1, $2, $3, $4); + + $line->set("is_dependency", true); + $line->set("targets", $targets); + $line->set("sources", $sources); + $line->log_warning("Space before colon in dependency line: " . $line->to_string()) if ($whitespace); + defined($comment) and $line->set("comment", $comment); + + } elsif ($text =~ regex_rcs_conflict) { + # This line is useless + + } else { + $line->log_fatal("Unknown line format: " . $line->to_string()); + } +} + +sub parselines_mk($) { + my ($lines) = @_; + + foreach my $line (@{$lines}) { + parseline_mk($line); + } +} + +# +# Loading package-specific data from files. +# + +sub readmakefile($$$$); +sub readmakefile($$$$) { + my ($fname, $main_lines, $all_lines, $seen_Makefile_include) = @_; + my ($includefile, $dirname, $lines, $is_main_Makefile); + + $lines = load_lines($fname, true); + if (!$lines) { + return false; + } + parselines_mk($lines); + + $is_main_Makefile = (@{$main_lines} == 0); + + foreach my $line (@{$lines}) { + my $text = $line->text; + + if ($is_main_Makefile) { + push(@{$main_lines}, $line); + } + push(@{$all_lines}, $line); + + # try to get any included file + my $is_include_line = false; + if ($text =~ m"^\.\s*include\s+\"(.*)\"$") { + $includefile = resolve_relative_path($1, true); + if ($includefile =~ regex_unresolved) { + if ($fname !~ m"/mk/") { + $line->log_note("Skipping include file \"${includefile}\". This may result in false warnings."); + } + + } else { + $is_include_line = true; + } + } + + if ($is_include_line) { + if ($fname !~ m"buildlink3\.mk$" && $includefile =~ m"^\.\./\.\./(.*)/buildlink3\.mk$") { + my ($bl3_file) = ($1); + + $pkgctx_bl3->{$bl3_file} = $line; + $opt_debug_misc and $line->log_debug("Buildlink3 file in package: ${bl3_file}"); + } + } + + if ($is_include_line && !exists($seen_Makefile_include->{$includefile})) { + $seen_Makefile_include->{$includefile} = true; + + if ($includefile =~ m"^\.\./[^./][^/]*/[^/]+") { + $line->log_warning("Relative directories should look like \"../../category/package\", not \"../package\"."); + $line->explain_warning(expl_relative_dirs); + } + if ($includefile =~ m"(?:^|/)Makefile.common$" + || ($includefile =~ m"^(?:\.\./(\.\./[^/]+/)?[^/]+/)?([^/]+)$" + && (!defined($1) || $1 ne "../mk") + && $2 ne "buildlink3.mk" + && $2 ne "options.mk")) { + $opt_debug_include and $line->log_debug("including ${includefile} sets seen_Makefile_common."); + $seen_Makefile_common = true; + } + if ($includefile =~ m"/mk/") { + # skip these files + + } else { + $dirname = dirname($fname); + # Only look in the directory relative to the + # current file and in the current working directory. + # We don't have an include dir list, like make(1) does. + if (!-f "$dirname/$includefile") { + $dirname = $current_dir; + } + if (!-f "$dirname/$includefile") { + $line->log_error("Cannot read $dirname/$includefile."); + } else { + $opt_debug_include and $line->log_debug("Including \"$dirname/$includefile\"."); + my $last_lineno = $#{$all_lines}; + readmakefile("$dirname/$includefile", $main_lines, $all_lines, $seen_Makefile_include) or return false; + + # Check that there is a comment in each + # Makefile.common that says which files + # include it. + if ($includefile =~ m"/Makefile\.common$") { + my @mc_lines = @{$all_lines}[$last_lineno+1 .. $#{$all_lines}]; + my $expected = "# used by " . relative_path($cwd_pkgsrcdir, $fname); + + if (!(grep { $_->text eq $expected } @mc_lines)) { + my $lineno = 0; + while ($lineno < $#mc_lines && $mc_lines[$lineno]->has("is_comment")) { + $lineno++; + } + my $iline = $mc_lines[$lineno]; + $iline->log_warning("Please add a line \"$expected\" here."); + $iline->explain_warning( +"Since Makefile.common files usually don't have any comments and", +"therefore not a clearly defined interface, they should at least contain", +"references to all files that include them, so that it is easier to see", +"what effects future changes may have.", +"", +"If there are more than five packages that use a Makefile.common,", +"you should think about giving it a proper name (maybe plugin.mk) and", +"documenting its interface."); + $iline->append_before($expected); + autofix(\@mc_lines); + } + } + } + } + + } elsif ($line->has("is_varassign")) { + my ($varname, $op, $value) = ($line->get("varname"), $line->get("op"), $line->get("value")); + + # Record all variables that are defined in these lines, so that they + # are not reported as "used but not defined". + if ($op ne "?=" || !exists($pkgctx_vardef->{$varname})) { + $opt_debug_misc and $line->log_debug("varassign(${varname}, ${op}, ${value})"); + $pkgctx_vardef->{$varname} = $line; + } + } + } + + return true; +} + +sub load_package_Makefile($$) { + my ($fname, $ref_lines) = @_; + my ($subr) = "load_package_Makefile"; + my ($lines, $all_lines, $seen_php_pecl_version); + + $opt_debug_trace and log_debug($fname, NO_LINES, "load_package_Makefile()"); + + if (!readmakefile($fname, $lines = [], $all_lines = [], $pkgctx_included = {})) { + log_error($fname, NO_LINE_NUMBER, "Cannot be read."); + return false; + } + + if ($opt_dumpmakefile) { + log_debug(NO_FILE, NO_LINES, "Whole Makefile (with all included files) follows:"); + foreach my $line (@{$all_lines}) { + print($line->to_string() . "\n"); + } + } + + determine_used_variables($all_lines); + + $pkgdir = expand_variable("PKGDIR"); + set_default_value(\$pkgdir, "."); + $distinfo_file = expand_variable("DISTINFO_FILE"); + set_default_value(\$distinfo_file, "distinfo"); + $filesdir = expand_variable("FILESDIR"); + set_default_value(\$filesdir, "files"); + $patchdir = expand_variable("PATCHDIR"); + set_default_value(\$patchdir, "patches"); + + if (var_is_defined("PHPEXT_MK")) { + if (!var_is_defined("USE_PHP_EXT_PATCHES")) { + $patchdir = "patches"; + } + if (var_is_defined("PECL_VERSION")) { + $distinfo_file = "distinfo"; + } + } + + $opt_debug_misc and log_debug(NO_FILE, NO_LINE_NUMBER, "[${subr}] DISTINFO_FILE=$distinfo_file"); + $opt_debug_misc and log_debug(NO_FILE, NO_LINE_NUMBER, "[${subr}] FILESDIR=$filesdir"); + $opt_debug_misc and log_debug(NO_FILE, NO_LINE_NUMBER, "[${subr}] PATCHDIR=$patchdir"); + $opt_debug_misc and log_debug(NO_FILE, NO_LINE_NUMBER, "[${subr}] PKGDIR=$pkgdir"); + + ${$ref_lines} = $lines; + return true; +} + +sub warn_about_PLIST_imake_mannewsuffix($) { + my ($line) = @_; + + $line->log_warning("IMAKE_MANNEWSUFFIX is not meant to appear in PLISTs."); + $line->explain_warning( +"This is the result of a print-PLIST call that has _not_ been checked", +"thoroughly by the developer. Please replace the IMAKE_MANNEWSUFFIX with", +"", +"\tIMAKE_MAN_SUFFIX for programs,", +"\tIMAKE_LIBMAN_SUFFIX for library functions,", +"\tIMAKE_FILEMAN_SUFFIX for file formats,", +"\tIMAKE_GAMEMAN_SUFFIX for games,", +"\tIMAKE_MISCMAN_SUFFIX for other man pages."); +} + +# +# Subroutines to check part of a single line. +# + +sub checkword_absolute_pathname($$) { + my ($line, $word) = @_; + + $opt_debug_trace and $line->log_debug("checkword_absolute_pathname(\"${word}\")"); + + if ($word =~ m"^/dev/(?:null|tty|zero)$") { + # These are defined by POSIX. + + } elsif ($word eq "/bin/sh") { + # This is usually correct, although on Solaris, it's pretty + # feature-crippled. + + } elsif ($word !~ m"/(?:[a-z]|\$[({])") { + # Assume that all pathnames start with a lowercase letter. + + } else { + $line->log_warning("Found absolute pathname: ${word}"); + $line->explain_warning( +"Absolute pathnames are often an indicator for unportable code. As", +"pkgsrc aims to be a portable system, absolute pathnames should be", +"avoided whenever possible.", +"", +"A special variable in this context is \${DESTDIR}, which is used in GNU", +"projects to specify a different directory for installation than what", +"the programs see later when they are executed. Usually it is empty, so", +"if anything after that variable starts with a slash, it is considered", +"an absolute pathname."); + } +} + +sub check_unused_licenses() { + + for my $licensefile (glob("${cwd_pkgsrcdir}/licenses/*")) { + if (-f $licensefile) { + my $licensename = basename($licensefile); + if (!exists($ipc_used_licenses{$licensename})) { + log_warning($licensefile, NO_LINES, "This license seems to be unused."); + } + } + } +} + +sub checkpackage_possible_downgrade() { + + $opt_debug_trace and log_debug(NO_FILE, NO_LINES, "checkpackage_possible_downgrade"); + + return unless defined $effective_pkgname; + return unless $effective_pkgname =~ regex_pkgname; + my ($pkgbase, $pkgversion) = ($1, $2); + my $line = $effective_pkgname_line; + + my @changes = get_doc_CHANGES($pkgpath); + if (@changes == 0) { + $opt_debug_misc and $line->log_debug("No changes have been recorded for package $pkgpath."); + return; + } + + my $last_change = $changes[-1]; + return unless $last_change->action eq "Updated"; + + my $last_version = $last_change->version; + + if (dewey_cmp($pkgversion, "<", $last_version)) { + $line->log_warning("The package is being downgraded from $last_version to $pkgversion."); + } +} + +# +# Subroutines to check a single line. +# + +sub checkline_length($$) { + my ($line, $maxlength) = @_; + + if (length($line->text) > $maxlength) { + $line->log_warning("Line too long (should be no more than $maxlength characters)."); + $line->explain_warning( +"Back in the old time, terminals with 80x25 characters were common.", +"And this is still the default size of many terminal emulators.", +"Moderately short lines also make reading easier."); + } +} + +sub checkline_valid_characters($$) { + my ($line, $re_validchars) = @_; + my ($rest); + + ($rest = $line->text) =~ s/$re_validchars//g; + if ($rest ne "") { + my @chars = map { sprintf("0x%02x", ord($_)); } split(//, $rest); + $line->log_warning("Line contains invalid characters (" . join(", ", @chars) . ")."); + } +} + +sub checkline_valid_characters_in_variable($$) { + my ($line, $re_validchars) = @_; + my ($varname, $rest); + + $varname = $line->get("varname"); + $rest = $line->get("value"); + + $rest =~ s/$re_validchars//g; + if ($rest ne "") { + my @chars = map { sprintf("0x%02x", ord($_)); } split(//, $rest); + $line->log_warning("${varname} contains invalid characters (" . join(", ", @chars) . ")."); + } +} + +sub checkline_trailing_whitespace($) { + my ($line) = @_; + + $opt_debug_trace and $line->log_debug("checkline_trailing_whitespace()"); + + if ($line->text =~ /\s+$/) { + $line->log_note("Trailing white-space."); + $line->explain_note( +"When a line ends with some white-space, that space is in most cases", +"irrelevant and can be removed, leading to a \"normal form\" syntax.", +"", +"Note: This is mostly for aesthetic reasons."); + $line->replace_regex(qr"\s+\n$", "\n"); + } +} + +sub checkline_rcsid_regex($$$) { + my ($line, $prefix_regex, $prefix) = @_; + my ($id) = ($opt_rcsidstring . ($is_wip ? "|Id" : "")); + + $opt_debug_trace and $line->log_debug("checkline_rcsid_regex(${prefix_regex}, ${prefix})"); + + if ($line->text !~ m"^${prefix_regex}\$(${id})(?::[^\$]+|)\$$") { + $line->log_error("\"${prefix}\$${opt_rcsidstring}\$\" expected."); + $line->explain_error( +"Several files in pkgsrc must contain the CVS Id, so that their current", +"version can be traced back later from a binary package. This is to", +"ensure reproducible builds, for example for finding bugs.", +"", +"Please insert the text from the above error message (without the quotes)", +"at this position in the file."); + return false; + } + return true; +} + +sub checkline_rcsid($$) { + my ($line, $prefix) = @_; + + checkline_rcsid_regex($line, quotemeta($prefix), $prefix); +} + +sub checkline_mk_absolute_pathname($$) { + my ($line, $text) = @_; + my $abspath; + + $opt_debug_trace and $line->log_debug("checkline_mk_absolute_pathname(${text})"); + + # In the GNU coding standards, DESTDIR is defined as a (usually + # empty) prefix that can be used to install files to a different + # location from what they have been built for. Therefore + # everything following it is considered an absolute pathname. + # Another commonly used context is in assignments like + # "bindir=/bin". + if ($text =~ m"(?:^|\$\{DESTDIR\}|\$\(DESTDIR\)|[\w_]+\s*=\s*)(/(?:[^\"'\`\s]|\"[^\"*]\"|'[^']*'|\`[^\`]*\`)*)") { + my $path = $1; + + if ($path =~ m"^/\w") { + $abspath = $path; + } + } + + if (defined($abspath)) { + checkword_absolute_pathname($line, $abspath); + } +} + +sub checkline_relative_path($$$) { + my ($line, $path, $must_exist) = @_; + my ($res_path); + + if (!$is_wip && $path =~ m"/wip/") { + $line->log_error("A pkgsrc package must not depend on any outside package."); + } + $res_path = resolve_relative_path($path, true); + if ($res_path =~ regex_unresolved) { + $opt_debug_unchecked and $line->log_debug("Unchecked path: \"${path}\"."); + } elsif (!-e ((($res_path =~ m"^/") ? "" : "${current_dir}/") . $res_path)) { + $must_exist and $line->log_error("\"${res_path}\" does not exist."); + } elsif ($path =~ m"^\.\./\.\./([^/]+)/([^/]+)(.*)") { + my ($cat, $pkg, $rest) = ($1, $2, $3); + } elsif ($path =~ m"^\.\./\.\./mk/") { + # There need not be two directory levels for mk/ files. + } elsif ($path =~ m"^\.\./mk/" && $cur_pkgsrcdir eq "..") { + # That's fine for category Makefiles. + } elsif ($path =~ m"^\.\.") { + $line->log_warning("Invalid relative path \"${path}\"."); + } +} + +sub checkline_relative_pkgdir($$) { + my ($line, $path) = @_; + + checkline_relative_path($line, $path, true); + $path = resolve_relative_path($path, false); + + if ($path =~ m"^(?:\./)?\.\./\.\./([^/]+/[^/]+)$") { + my $otherpkgpath = $1; + if (! -f "$cwd_pkgsrcdir/$otherpkgpath/Makefile") { + $line->log_error("There is no package in $otherpkgpath."); + } + + } else { + $line->log_warning("\"${path}\" is not a valid relative package directory."); + $line->explain_warning( +"A relative pathname always starts with \"../../\", followed", +"by a category, a slash and a the directory name of the package.", +"For example, \"../../misc/screen\" is a valid relative pathname."); + } +} + +sub checkline_mk_varuse($$$$) { + my ($line, $varname, $mod, $context) = @_; + + assert(defined($varname), "The varname parameter must be defined"); + assert(defined($context), "The context parameter must be defined"); + $opt_debug_trace and $line->log_debug("checkline_mk_varuse(\"${varname}\", \"${mod}\", ".$context->to_string().")"); + + # Check for spelling mistakes. + my $type = get_variable_type($line, $varname); + if (defined($type) && !($type->is_guessed)) { + # Great. + + } elsif (var_is_used($varname)) { + # Fine. + + } elsif (defined($mkctx_for_variables) && exists($mkctx_for_variables->{$varname})) { + # Variables defined in .for loops are also ok. + + } else { + $opt_warn_extra and $line->log_warning("${varname} is used but not defined. Spelling mistake?"); + } + + if ($opt_warn_perm) { + my $perms = get_variable_perms($line, $varname); + my $is_load_time; # Will the variable be used at load time? + my $is_indirect; # Might the variable be used indirectly at load time, + # for example by assigning it to another variable + # which then gets evaluated? + + # Don't warn about variables that are not recorded in the + # pkglint variable definition. + if (defined($context->type) && $context->type->is_guessed()) { + $is_load_time = false; + + } elsif ($context->time == VUC_TIME_LOAD && $perms !~ m"p") { + $is_load_time = true; + $is_indirect = false; + + } elsif (defined($context->type) && $context->type->perms_union() =~ m"p" && $perms !~ m"p") { + $is_load_time = true; + $is_indirect = true; + + } else { + $is_load_time = false; + } + + if ($is_load_time && !$is_indirect) { + $line->log_warning("${varname} should not be evaluated at load time."); + $line->explain_warning( +"Many variables, especially lists of something, get their values", +"incrementally. Therefore it is generally unsafe to rely on their value", +"until it is clear that it will never change again. This point is", +"reached when the whole package Makefile is loaded and execution of the", +"shell commands starts, in some cases earlier.", +"", +"Additionally, when using the \":=\" operator, each \$\$ is replaced", +"with a single \$, so variables that have references to shell variables", +"or regular expressions are modified in a subtle way."); + } + if ($is_load_time && $is_indirect) { + $line->log_warning("${varname} should not be evaluated indirectly at load time."); + $line->explain_warning( +"The variable on the left-hand side may be evaluated at load time, but", +"the variable on the right-hand side may not. Due to this assignment, it", +"might be used indirectly at load-time, when it is not guaranteed to be", +"properly defined."); + } + + if ($perms !~ m"p" && $perms !~ m"u") { + $line->log_warning("${varname} may not be used in this file."); + } + } + + if ($varname eq "LOCALBASE" && !$is_internal) { + $line->log_warning("The LOCALBASE variable should not be used by packages."); + $line->explain_warning( +# from jlam via private mail. +"Currently, LOCALBASE is typically used in these cases:", +"", +"(1) To locate a file or directory from another package.", +"(2) To refer to own files after installation.", +"", +"In the first case, the example is:", +"", +" STRLIST= \${LOCALBASE}/bin/strlist", +" do-build:", +" cd \${WRKSRC} && \${STRLIST} *.str", +"", +"This should really be:", +"", +" EVAL_PREFIX= STRLIST_PREFIX=strlist", +" STRLIST= \${STRLIST_PREFIX}/bin/strlist", +" do-build:", +" cd \${WRKSRC} && \${STRLIST} *.str", +"", +"In the second case, the example is:", +"", +" CONFIGURE_ENV+= --with-datafiles=\${LOCALBASE}/share/battalion", +"", +"This should really be:", +"", +" CONFIGURE_ENV+= --with-datafiles=\${PREFIX}/share/battalion"); + } + + my $needs_quoting = variable_needs_quoting($line, $varname, $context); + + if ($context->shellword == VUC_SHELLWORD_FOR) { + if (!defined($type)) { + # Cannot check anything here. + + } elsif ($type->kind_of_list == LK_INTERNAL) { + # Fine. + + } elsif ($needs_quoting == doesnt_matter || $needs_quoting == false) { + # Fine, these variables are assumed to not + # contain special characters. + + } else { + $line->log_warning("The variable ${varname} should not be used in .for loops."); + $line->explain_warning( +"The .for loop splits its argument at sequences of white-space, as", +"opposed to all other places in make(1), which act like the shell.", +"Therefore only variables that are specifically designed to match this", +"requirement should be used here."); + } + } + + if ($opt_warn_quoting && $context->shellword != VUC_SHELLWORD_UNKNOWN && $needs_quoting != dont_know) { + + # In GNU configure scripts, a few variables need to be + # passed through the :M* operator before they reach the + # configure scripts. + my $need_mstar = false; + if ($varname =~ regex_gnu_configure_volatile_vars) { + # When we are not checking a package, but some other file, + # the :M* operator is needed for safety. + if (!defined($pkgctx_vardef) || exists($pkgctx_vardef->{"GNU_CONFIGURE"})) { + $need_mstar = true; + } + } + + my $stripped_mod = ($mod =~ m"(.*?)(?::M\*)?(?::Q)?$") ? $1 : $mod; + my $correct_mod = $stripped_mod . ($need_mstar ? ":M*:Q" : ":Q"); + + if ($mod eq ":M*:Q" && !$need_mstar) { + $line->log_note("The :M* modifier is not needed here."); + + } elsif ($mod ne $correct_mod && $needs_quoting == true) { + if ($context->shellword == VUC_SHELLWORD_PLAIN) { + $line->log_warning("Please use \${${varname}${correct_mod}} instead of \${${varname}${mod}}."); + #$line->replace("\${${varname}}", "\${${varname}:Q}"); + } else { + $line->log_warning("Please use \${${varname}${correct_mod}} instead of \${${varname}${mod}} and make sure the variable appears outside of any quoting characters."); + } + $line->explain_warning("See the pkgsrc guide, section \"quoting guideline\", for details."); + } + + if ($mod =~ m":Q$") { + my @expl = ( +"Many variables in pkgsrc do not need the :Q operator, since they", +"are not expected to contain white-space or other evil characters.", +"", +"Another case is when a variable of type ShellWord appears in a context", +"that expects a shell word, it does not need to have a :Q operator. Even", +"when it is concatenated with another variable, it still stays _one_ word.", +"", +"Example:", +"\tWORD1= Have\\ fun # 1 word", +"\tWORD2= \"with BSD Make\" # 1 word, too", +"", +"\tdemo:", +"\t\techo \${WORD1}\${WORD2} # still 1 word"); + + if ($needs_quoting == false) { + $line->log_warning("The :Q operator should not be used for \${${varname}} here."); + $line->explain_warning(@expl); + } elsif ($needs_quoting == doesnt_matter) { + $line->log_note("The :Q operator isn't necessary for \${${varname}} here."); + $line->explain_note(@expl); + } + } + } + + assert(defined($mkctx_build_defs), "The build_defs variable must be defined here."); + if (exists(get_userdefined_variables()->{$varname}) && !exists(get_system_build_defs()->{$varname}) && !exists($mkctx_build_defs->{$varname})) { + $line->log_warning("The user-defined variable ${varname} is used but not added to BUILD_DEFS."); + $line->explain_warning( +"When a pkgsrc package is built, many things can be configured by the", +"pkgsrc user in the mk.conf file. All these configurations should be", +"recorded in the binary package, so the package can be reliably rebuilt.", +"The BUILD_DEFS variable contains a list of all these user-settable", +"variables, so please add your variable to it, too."); + } +} + +sub checkline_mk_text($$) { + my ($line, $text) = @_; + my ($rest, $state, $vartools, $depr_map); + + if ($text =~ m"^(?:[^#]*[^\$])?\$(\w+)") { + my ($varname) = ($1); + $line->log_warning("\$$varname is ambiguous. Use \${$varname} if you mean a Makefile variable or \$\$$varname if you mean a shell variable."); + } + + if ($line->lines eq "1") { + checkline_rcsid_regex($line, qr"#\s+", "# "); + } + + if ($text =~ m"\$\{WRKSRC\}/\.\./") { + $line->log_warning("Using \"\${WRKSRC}/..\" is conceptually wrong. Please use a combination of WRKSRC, CONFIGURE_DIRS and BUILD_DIRS instead."); + $line->explain_warning( +"You should define WRKSRC such that all of CONFIGURE_DIRS, BUILD_DIRS", +"and INSTALL_DIRS are subdirectories of it."); + } + + if ($text =~ m"\b(-Wl,--rpath,|-Wl,-rpath-link,|-Wl,-rpath,|-Wl,-R)\b") { + $line->log_warning("Please use \${COMPILER_RPATH_FLAG} instead of $1."); + } + # Note: A simple -R is not detected, as the rate of false + # positives is too high. + + $rest = $text; + $depr_map = get_deprecated_map(); + while ($rest =~ s/(?:^|[^\$])\$\{([-A-Z0-9a-z_]+)(\.[\-0-9A-Z_a-z]+)?(?::[^\}]+)?\}//) { + my ($varbase, $varext) = ($1, $2); + my $varname = $varbase . (defined($varext) ? $varext : ""); + my $varcanon = varname_canon($varname); + my $instead = + (exists($depr_map->{$varname})) ? $depr_map->{$varname} + : (exists($depr_map->{$varcanon})) ? $depr_map->{$varcanon} + : undef; + + if (defined($instead)) { + $line->log_warning("Use of ${varname} is deprecated. ${instead}"); + } + } + + # Don't enforce purely aesthetic changes before more correct behaviour is implemented: + # $rest = $text; + # while ($rest =~ s/(?:^|[^\$])\$\(([-A-Z0-9a-z_]+)(?::[^\}]+)?\)//) { + # my ($varname) = ($1); + + # $line->log_warning("Please use \${${varname}\} instead of \$(${varname})."); + # } + +} + +#include PkgLint/Shell.pm + +sub expand_permission($) { + my ($perm) = @_; + my %fullperm = ( "a" => "append", "d" => "default", "p" => "preprocess", "s" => "set", "u" => "runtime", "?" => "unknown" ); + my $result = join(", ", map { $fullperm{$_} } split //, $perm); + $result =~ s/, $//g; + + return $result; +} + +sub checkline_mk_vardef($$$) { + my ($line, $varname, $op) = @_; + + $opt_debug_trace and $line->log_debug("checkline_mk_vardef(${varname}, ${op})"); + + # If we are checking a whole package, add it to the package-wide + # list of defined variables. + if (defined($pkgctx_vardef) && !exists($pkgctx_vardef->{$varname})) { + $pkgctx_vardef->{$varname} = $line; + } + + # Add it to the file-wide list of defined variables. + if (!exists($mkctx_vardef->{$varname})) { + $mkctx_vardef->{$varname} = $line; + } + + return unless $opt_warn_perm; + + my $perms = get_variable_perms($line, $varname); + my $needed = { "=" => "s", "!=" => "s", "?=" => "d", "+=" => "a", ":=" => "s" }->{$op}; + if (index($perms, $needed) == -1) { + $line->log_warning("Permission [" . expand_permission($needed) . "] requested for ${varname}, but only [" . expand_permission($perms) . "] is allowed."); + $line->explain_warning( +"The available permissions are:", +"\tappend\t\tappend something using +=", +"\tdefault\t\tset a default value using ?=", +"\tpreprocess\tuse a variable during preprocessing", +"\truntime\t\tuse a variable at runtime", +"\tset\t\tset a variable using :=, =, !=", +"", +"A \"?\" means that it is not yet clear which permissions are allowed", +"and which aren't."); + } +} + +# @param $op +# The operator that is used for reading or writing to the variable. +# One of: "=", "+=", ":=", "!=", "?=", "use", "pp-use", "". +# For some variables (like BuildlinkDepth or BuildlinkPackages), the +# operator influences the valid values. +# @param $comment +# In assignments, a part of the line may be commented out. If there +# is no comment, pass C<undef>. +# +sub checkline_mk_vartype_basic($$$$$$$$); +sub checkline_mk_vartype_basic($$$$$$$$) { + my ($line, $varname, $type, $op, $value, $comment, $list_context, $is_guessed) = @_; + my ($value_novar); + + $opt_debug_trace and $line->log_debug(sprintf("checkline_mk_vartype_basic(%s, %s, %s, %s, %s, %s, %s)", + $varname, $type, $op, $value, defined($comment) ? $comment : "<undef>", $list_context, $is_guessed)); + + $value_novar = $value; + while ($value_novar =~ s/\$\{([^{}]*)\}//g) { + my ($varuse) = ($1); + if (!$list_context && $varuse =~ m":Q$") { + $line->log_warning("The :Q operator should only be used in lists and shell commands."); + } + } + + my %type_dispatch = ( + AwkCommand => sub { + $opt_debug_unchecked and $line->log_debug("Unchecked AWK command: ${value}"); + }, + + BrokenIn => sub { + if ($value ne $value_novar) { + $line->log_error("${varname} must not refer to other variables."); + + } elsif ($value =~ m"^pkgsrc-(\d\d\d\d)Q(\d)$") { + my ($year, $quarter) = ($1, $2); + + # Fine. + + } else { + $line->log_warning("Invalid value \"${value}\" for ${varname}."); + } + $line->log_note("Please remove this line if the package builds for you."); + }, + + BuildlinkDepmethod => sub { + # Note: this cannot be replaced with { build full } because + # enumerations may not contain references to other variables. + if ($value ne $value_novar) { + # No checks yet. + } elsif ($value ne "build" && $value ne "full") { + $line->log_warning("Invalid dependency method \"${value}\". Valid methods are \"build\" or \"full\"."); + } + }, + + BuildlinkDepth => sub { + if (!($op eq "use" && $value eq "+") + && $value ne "\${BUILDLINK_DEPTH}+" + && $value ne "\${BUILDLINK_DEPTH:S/+\$//}") { + $line->log_warning("Invalid value for ${varname}."); + } + }, + + BuildlinkPackages => sub { + my $re_del = qr"\$\{BUILDLINK_PACKAGES:N(?:[+\-.0-9A-Z_a-z]|\$\{[^\}]+\})+\}"; + my $re_add = qr"(?:[+\-.0-9A-Z_a-z]|\$\{[^\}]+\})+"; + + if (($op eq ":=" && $value =~ m"^${re_del}$") || + ($op eq ":=" && $value =~ m"^${re_del}\s+${re_add}$") || + ($op eq "+=" && $value =~ m"^${re_add}$")) { + # Fine. + + } else { + $line->log_warning("Invalid value for ${varname}."); + } + }, + + Category => sub { + my $allowed_categories = join("|", qw( + archivers audio + benchmarks biology + cad chat chinese comms converters cross crosspkgtools + databases devel + editors emulators + filesystems finance fonts + games geography gnome gnustep graphics + ham + inputmethod + japanese java + kde korean + lang linux local + mail math mbone meta-pkgs misc multimedia + net news + packages parallel perl5 pkgtools plan9 print python + ruby + scm security shells sysutils + tcl textproc time tk + windowmaker wm www + x11 xmms + )); + if ($value !~ m"^(?:${allowed_categories})$") { + $line->log_error("Invalid category \"${value}\"."); + } + }, + + CFlag => sub { + if ($value =~ m"^-D([0-9A-Z_a-z]+)=(.*)") { + my ($macname, $macval) = ($1, $2); + + # No checks needed, since the macro definitions + # are usually directory names, which don't need + # any quoting. + + } elsif ($value =~ m"^-[DU]([0-9A-Z_a-z]+)") { + my ($macname) = ($1); + + $opt_debug_unchecked and $line->log_debug("Unchecked macro ${macname} in ${varname}."); + + } elsif ($value =~ m"^-I(.*)") { + my ($dirname) = ($1); + + $opt_debug_unchecked and $line->log_debug("Unchecked directory ${dirname} in ${varname}."); + + } elsif ($value eq "-c99") { + # Only works on IRIX, but is usually enclosed with + # the proper preprocessor conditional. + + } elsif ($value =~ m"^-[OWfgm]|^-std=.*") { + $opt_debug_unchecked and $line->log_debug("Unchecked compiler flag ${value} in ${varname}."); + + } elsif ($value =~ m"^-.*") { + $line->log_warning("Unknown compiler flag \"${value}\"."); + + } elsif ($value =~ regex_unresolved) { + $opt_debug_unchecked and $line->log_debug("Unchecked CFLAG: ${value}"); + + } else { + $line->log_warning("Compiler flag \"${value}\" does not start with a dash."); + } + }, + + Comment => sub { + if ($value eq "SHORT_DESCRIPTION_OF_THE_PACKAGE") { + $line->log_error("COMMENT must be set."); + } + if ($value =~ m"^(a|an)\s+"i) { + $line->log_warning("COMMENT should not begin with '$1'."); + } + if ($value =~ m"^[a-z]") { + $line->log_warning("COMMENT should start with a capital letter."); + } + if ($value =~ m"\.$") { + $line->log_warning("COMMENT should not end with a period."); + } + if (length($value) > 70) { + $line->log_warning("COMMENT should not be longer than 70 characters."); + } + }, + + Dependency => sub { + if ($value =~ m"^(${regex_pkgbase})(<|=|>|<=|>=|!=|-)(${regex_pkgversion})$") { + my ($depbase, $depop, $depversion) = ($1, $2, $3); + + } elsif ($value =~ m"^(${regex_pkgbase})-(?:\[(.*)\]\*|(\d+(?:\.\d+)*(?:\.\*)?)(\{,nb\*\}|\*|)|(.*))?$") { + my ($depbase, $bracket, $version, $version_wildcard, $other) = ($1, $2, $3, $4, $5); + + if (defined($bracket)) { + if ($bracket ne "0-9") { + $line->log_warning("Only [0-9]* is allowed in the numeric part of a dependency."); + } + + } elsif (defined($version) && defined($version_wildcard) && $version_wildcard ne "") { + # Great. + + } elsif (defined($version)) { + $line->log_warning("Please append {,nb*} to the version number of this dependency."); + $line->explain_warning( +"Usually, a dependency should stay valid when the PKGREVISION is", +"increased, since those changes are most often editorial. In the", +"current form, the dependency only matches if the PKGREVISION is", +"undefined."); + + } elsif ($other eq "*") { + $line->log_warning("Please use ${depbase}-[0-9]* instead of ${depbase}-*."); + $line->explain_warning( +"If you use a * alone, the package specification may match other", +"packages that have the same prefix, but a longer name. For example,", +"foo-* matches foo-1.2, but also foo-client-1.2 and foo-server-1.2."); + + } else { + $line->log_error("Unknown dependency pattern \"${value}\"."); + } + + } elsif ($value =~ m"\{") { + # Dependency patterns containing alternatives + # are just too hard to check. + $opt_debug_unchecked and $line->log_debug("Unchecked dependency pattern: ${value}"); + + } elsif ($value ne $value_novar) { + $opt_debug_unchecked and $line->log_debug("Unchecked dependency: ${value}"); + + } else { + $line->log_warning("Unknown dependency format: ${value}"); + $line->explain_warning( +"Typical dependencies have the form \"package>=2.5\", \"package-[0-9]*\"", +"or \"package-3.141\"."); + } + }, + + DependencyWithPath => sub { + if ($value =~ regex_unresolved) { + # don't even try to check anything + } elsif ($value =~ m"(.*):(\.\./\.\./([^/]+)/([^/]+))$") { + my ($pattern, $relpath, $cat, $pkg) = ($1, $2, $3, $4); + + checkline_relative_pkgdir($line, $relpath); + + if ($pkg eq "msgfmt" || $pkg eq "gettext") { + $line->log_warning("Please use USE_TOOLS+=msgfmt instead of this dependency."); + + } elsif ($pkg =~ m"^perl\d+") { + $line->log_warning("Please use USE_TOOLS+=perl:run instead of this dependency."); + + } elsif ($pkg eq "gmake") { + $line->log_warning("Please use USE_TOOLS+=gmake instead of this dependency."); + + } + + if ($pattern =~ regex_dependency_lge) { +# ($abi_pkg, $abi_version) = ($1, $2); + } elsif ($pattern =~ regex_dependency_wildcard) { +# ($abi_pkg) = ($1); + } else { + $line->log_error("Unknown dependency pattern \"${pattern}\"."); + } + + } elsif ($value =~ m":\.\./[^/]+$") { + $line->log_warning("Dependencies should have the form \"../../category/package\"."); + $line->explain_warning(expl_relative_dirs); + + } else { + $line->log_warning("Unknown dependency format."); + $line->explain_warning( +"Examples for valid dependencies are:", +" package-[0-9]*:../../category/package", +" package>=3.41:../../category/package", +" package-2.718:../../category/package"); + } + }, + + DistSuffix => sub { + if ($value eq ".tar.gz") { + $line->log_note("${varname} is \".tar.gz\" by default, so this definition may be redundant."); + } + }, + + EmulPlatform => sub { + if ($value =~ m"^(\w+)-(\w+)$") { + my ($opsys, $arch) = ($1, $2); + + if ($opsys !~ m"^(?:bsdos|cygwin|darwin|dragonfly|freebsd|haiku|hpux|interix|irix|linux|netbsd|openbsd|osf1|sunos|solaris)$") { + $line->log_warning("Unknown operating system: ${opsys}"); + } + # no check for $os_version + if ($arch !~ m"^(?:i386|alpha|amd64|arc|arm|arm32|cobalt|convex|dreamcast|hpcmips|hpcsh|hppa|ia64|m68k|m88k|mips|mips64|mipsel|mipseb|mipsn32|ns32k|pc532|pmax|powerpc|rs6000|s390|sparc|sparc64|vax|x86_64)$") { + $line->log_warning("Unknown hardware architecture: ${arch}"); + } + + } else { + $line->log_warning("\"${value}\" is not a valid emulation platform."); + $line->explain_warning( +"An emulation platform has the form <OPSYS>-<MACHINE_ARCH>.", +"OPSYS is the lower-case name of the operating system, and MACHINE_ARCH", +"is the hardware architecture.", +"", +"Examples: linux-i386, irix-mipsel."); + } + }, + + FetchURL => sub { + checkline_mk_vartype_basic($line, $varname, "URL", $op, $value, $comment, $list_context, $is_guessed); + + my $sites = get_dist_sites(); + foreach my $site (keys(%{$sites})) { + if (index($value, $site) == 0) { + my $subdir = substr($value, length($site)); + my $is_github = $value =~ m"^https://github\.com/"; + if ($is_github) { + $subdir =~ s|/.*|/|; + } + $line->log_warning(sprintf("Please use \${%s:=%s} instead of \"%s\".", $sites->{$site}, $subdir, $value)); + if ($is_github) { + $line->log_warning("Run \"".conf_make." help topic=github\" for further tips."); + } + last; + } + } + }, + + Filename => sub { + if ($value_novar =~ m"/") { + $line->log_warning("A filename should not contain a slash."); + + } elsif ($value_novar !~ m"^[-0-9\@A-Za-z.,_~+%]*$") { + $line->log_warning("\"${value}\" is not a valid filename."); + } + }, + + Filemask => sub { + if ($value_novar !~ m"^[-0-9A-Za-z._~+%*?]*$") { + $line->log_warning("\"${value}\" is not a valid filename mask."); + } + }, + + FileMode => sub { + if ($value ne "" && $value_novar eq "") { + # Fine. + } elsif ($value =~ m"^[0-7]{3,4}") { + # Fine. + } else { + $line->log_warning("Invalid file mode ${value}."); + } + }, + + Identifier => sub { + if ($value ne $value_novar) { + #$line->log_warning("Identifiers should be given directly."); + } + if ($value_novar =~ m"^[+\-.0-9A-Z_a-z]+$") { + # Fine. + } elsif ($value ne "" && $value_novar eq "") { + # Don't warn here. + } else { + $line->log_warning("Invalid identifier \"${value}\"."); + } + }, + + Integer => sub { + if ($value !~ m"^\d+$") { + $line->log_warning("${varname} must be a valid integer."); + } + }, + + LdFlag => sub { + if ($value =~ m"^-L(.*)") { + my ($dirname) = ($1); + + $opt_debug_unchecked and $line->log_debug("Unchecked directory ${dirname} in ${varname}."); + + } elsif ($value =~ m"^-l(.*)") { + my ($libname) = ($1); + + $opt_debug_unchecked and $line->log_debug("Unchecked library name ${libname} in ${varname}."); + + } elsif ($value =~ m"^(?:-static)$") { + # Assume that the wrapper framework catches these. + + } elsif ($value =~ m"^(-Wl,(?:-R|-rpath|--rpath))") { + my ($rpath_flag) = ($1); + $line->log_warning("Please use \${COMPILER_RPATH_FLAG} instead of ${rpath_flag}."); + + } elsif ($value =~ m"^-.*") { + $line->log_warning("Unknown linker flag \"${value}\"."); + + } elsif ($value =~ regex_unresolved) { + $opt_debug_unchecked and $line->log_debug("Unchecked LDFLAG: ${value}"); + + } else { + $line->log_warning("Linker flag \"${value}\" does not start with a dash."); + } + }, + + License => sub { + + use constant deprecated_licenses => array_to_hash(qw( + fee-based-commercial-use + no-commercial-use no-profit no-redistribution + shareware + )); + + my $licenses = parse_licenses($value); + foreach my $license (@$licenses) { + my $license_file = "${cwd_pkgsrcdir}/licenses/${license}"; + if (defined($pkgctx_vardef) && exists($pkgctx_vardef->{"LICENSE_FILE"})) { + my $license_file_line = $pkgctx_vardef->{"LICENSE_FILE"}; + + $license_file = "${current_dir}/" . resolve_relative_path($license_file_line->get("value"), false); + } else { + $ipc_used_licenses{$license} = true; + } + + if (!-f $license_file) { + $line->log_warning("License file ".normalize_pathname($license_file)." does not exist."); + } + + if (exists(deprecated_licenses->{$license})) { + $line->log_warning("License ${license} is deprecated."); + } + } + }, + + Mail_Address => sub { + if ($value =~ m"^([+\-.0-9A-Z_a-z]+)\@([-\w\d.]+)$") { + my ($localpart, $domain) = ($1, $2); + if ($domain =~ m"^NetBSD.org"i && $domain ne "NetBSD.org") { + $line->log_warning("Please write NetBSD.org instead of ${domain}."); + } + if ("${localpart}\@${domain}" =~ m"^(tech-pkg|packages)\@NetBSD\.org$"i) { + $line->log_warning("${localpart}\@${domain} is deprecated. Use pkgsrc-users\@NetBSD.org instead."); + } + + } else { + $line->log_warning("\"${value}\" is not a valid mail address."); + } + }, + + Message => sub { + if ($value =~ m"^[\"'].*[\"']$") { + $line->log_warning("${varname} should not be quoted."); + $line->explain_warning( +"The quoting is only needed for variables which are interpreted as", +"multiple words (or, generally speaking, a list of something). A single", +"text message does not belong to this class, since it is only printed", +"as a whole.", +"", +"On the other hand, PKG_FAIL_REASON is a _list_ of text messages, so in", +"that case, the quoting has to be done."); + } + }, + + Option => sub { + if ($value ne $value_novar) { + $opt_debug_unchecked and $line->log_debug("Unchecked option name \"${value}\"."); + + } elsif ($value_novar =~ m"^-?([a-z][-0-9a-z\+]*)$") { + my ($optname) = ($1); + + if (!exists(get_pkg_options()->{$optname})) { + $line->log_warning("Unknown option \"${value}\"."); + $line->explain_warning( +"This option is not documented in the mk/defaults/options.description", +"file. If this is not a typo, please think of a brief but precise", +"description and either update that file yourself or ask on the", +"tech-pkg\@NetBSD.org mailing list."); + } + + } elsif ($value_novar =~ m"^-?([a-z][-0-9a-z_\+]*)$") { + my ($optname) = ($1); + + $line->log_warning("Use of the underscore character in option names is deprecated."); + + } else { + $line->log_error("\"${value}\" is not a valid option name."); + } + }, + + Pathlist => sub { + + if ($value !~ m":" && $is_guessed) { + checkline_mk_vartype_basic($line, $varname, "Pathname", $op, $value, $comment, $list_context, $is_guessed); + + } else { + + # XXX: The splitting will fail if $value contains any + # variables with modifiers, for example :Q or :S/././. + foreach my $p (split(qr":", $value)) { + my $p_novar = remove_variables($p); + + if ($p_novar !~ m"^[-0-9A-Za-z._~+%/]*$") { + $line->log_warning("\"${p}\" is not a valid pathname."); + } + + if ($p !~ m"^[\$/]") { + $line->log_warning("All components of ${varname} (in this case \"${p}\") should be an absolute path."); + } + } + } + }, + + Pathmask => sub { + if ($value_novar !~ m"^[#\-0-9A-Za-z._~+%*?/\[\]]*$") { + $line->log_warning("\"${value}\" is not a valid pathname mask."); + } + checkline_mk_absolute_pathname($line, $value); + }, + + Pathname => sub { + if ($value_novar !~ m"^[#\-0-9A-Za-z._~+%/]*$") { + $line->log_warning("\"${value}\" is not a valid pathname."); + } + checkline_mk_absolute_pathname($line, $value); + }, + + Perl5Packlist => sub { + if ($value ne $value_novar) { + $line->log_warning("${varname} should not depend on other variables."); + } + }, + + PkgName => sub { + if ($value eq $value_novar && $value !~ regex_pkgname) { + $line->log_warning("\"${value}\" is not a valid package name. A valid package name has the form packagename-version, where version consists only of digits, letters and dots."); + } + }, + + PkgPath => sub { + checkline_relative_pkgdir($line, "$cur_pkgsrcdir/$value"); + }, + + PkgOptionsVar => sub { + checkline_mk_vartype_basic($line, $varname, "Varname", $op, $value, $comment, false, $is_guessed); + if ($value =~ m"\$\{PKGBASE[:\}]") { + $line->log_error("PKGBASE must not be used in PKG_OPTIONS_VAR."); + $line->explain_error( +"PKGBASE is defined in bsd.pkg.mk, which is included as the", +"very last file, but PKG_OPTIONS_VAR is evaluated earlier.", +"Use \${PKGNAME:C/-[0-9].*//} instead."); + } + }, + + PkgRevision => sub { + if ($value !~ m"^[1-9]\d*$") { + $line->log_warning("${varname} must be a positive integer number."); + } + if ($line->fname !~ m"(?:^|/)Makefile$") { + $line->log_error("${varname} only makes sense directly in the package Makefile."); + $line->explain_error( +"Usually, different packages using the same Makefile.common have", +"different dependencies and will be bumped at different times (e.g. for", +"shlib major bumps) and thus the PKGREVISIONs must be in the separate", +"Makefiles. There is no practical way of having this information in a", +"commonly used Makefile."); + } + }, + + PlatformTriple => sub { + my $part = qr"(?:\[[^\]]+\]|[^-\[])+"; + if ($value =~ m"^(${part})-(${part})-(${part})$") { + my ($opsys, $os_version, $arch) = ($1, $2, $3); + + if ($opsys !~ m"^(?:\*|BSDOS|Cygwin|Darwin|DragonFly|FreeBSD|Haiku|HPUX|Interix|IRIX|Linux|NetBSD|OpenBSD|OSF1|QNX|SunOS)$") { + $line->log_warning("Unknown operating system: ${opsys}"); + } + # no check for $os_version + if ($arch !~ m"^(?:\*|i386|alpha|amd64|arc|arm|arm32|cobalt|convex|dreamcast|hpcmips|hpcsh|hppa|ia64|m68k|m88k|mips|mips64|mipsel|mipseb|mipsn32|ns32k|pc532|pmax|powerpc|rs6000|s390|sparc|sparc64|vax|x86_64)$") { + $line->log_warning("Unknown hardware architecture: ${arch}"); + } + + } else { + $line->log_warning("\"${value}\" is not a valid platform triple."); + $line->explain_warning( +"A platform triple has the form <OPSYS>-<OS_VERSION>-<MACHINE_ARCH>.", +"Each of these components may be a shell globbing expression.", +"Examples: NetBSD-*-i386, *-*-*, Linux-*-*."); + } + }, + + PrefixPathname => sub { + if ($value =~ m"^man/(.*)") { + my ($mansubdir) = ($1); + + $line->log_warning("Please use \"\${PKGMANDIR}/${mansubdir}\" instead of \"${value}\"."); + } + }, + + PythonDependency => sub { + if ($value ne $value_novar) { + $line->log_warning("Python dependencies should not contain variables."); + } + if ($value_novar !~ m"^[+\-.0-9A-Z_a-z]+(?:|:link|:build)$") { + $line->log_warning("Invalid Python dependency \"${value}\"."); + $line->explain_warning( +"Python dependencies must be an identifier for a package, as specified", +"in lang/python/versioned_dependencies.mk. This identifier may be", +"followed by :build for a build-time only dependency, or by :link for", +"a run-time only dependency."); + } + }, + + RelativePkgDir => sub { + checkline_relative_pkgdir($line, $value); + }, + + RelativePkgPath => sub { + checkline_relative_path($line, $value, true); + }, + + Restricted => sub { + if ($value ne "\${RESTRICTED}") { + $line->log_warning("The only valid value for ${varname} is \${RESTRICTED}."); + $line->explain_warning( +"These variables are used to control which files may be mirrored on FTP", +"servers or CD-ROM collections. They are not intended to mark packages", +"whose only MASTER_SITES are on ftp.NetBSD.org."); + } + }, + + SedCommand => sub { + }, + + SedCommands => sub { + my $words = shell_split($value); + if (!$words) { + $line->log_error("Invalid shell words in sed commands."); + $line->explain_error( +"If your sed commands have embedded \"#\" characters, you need to escape", +"them with a backslash, otherwise make(1) will interpret them as a", +"comment, no matter if they occur in single or double quotes or", +"whatever."); + + } else { + my $nwords = scalar(@{$words}); + my $ncommands = 0; + + for (my $i = 0; $i < $nwords; $i++) { + my $word = $words->[$i]; + checkline_mk_shellword($line, $word, true); + + if ($word eq "-e") { + if ($i + 1 < $nwords) { + # Check the real sed command here. + $i++; + $ncommands++; + if ($ncommands > 1) { + $line->log_warning("Each sed command should appear in an assignment of its own."); + $line->explain_warning( +"For example, instead of", +" SUBST_SED.foo+= -e s,command1,, -e s,command2,,", +"use", +" SUBST_SED.foo+= -e s,command1,,", +" SUBST_SED.foo+= -e s,command2,,", +"", +"This way, short sed commands cannot be hidden at the end of a line."); + } + checkline_mk_shellword($line, $words->[$i - 1], true); + checkline_mk_shellword($line, $words->[$i], true); + checkline_mk_vartype_basic($line, $varname, "SedCommand", $op, $words->[$i], $comment, $list_context, $is_guessed); + } else { + $line->log_error("The -e option to sed requires an argument."); + } + } elsif ($word eq "-E") { + # Switch to extended regular expressions mode. + + } elsif ($word eq "-n") { + # Don't print lines per default. + + } elsif ($i == 0 && $word =~ m"^([\"']?)(?:\d*|/.*/)s(.).*\2g?\1$") { + $line->log_warning("Please always use \"-e\" in sed commands, even if there is only one substitution."); + + } else { + $line->log_warning("Unknown sed command ${word}."); + } + } + } + }, + + ShellCommand => sub { + checkline_mk_shelltext($line, $value); + }, + + ShellWord => sub { + if (!$list_context) { + checkline_mk_shellword($line, $value, true); + } + }, + + Stage => sub { + if ($value !~ m"^(?:pre|do|post)-(?:extract|patch|configure|build|install)$") { + $line->log_warning("Invalid stage name. Use one of {pre,do,post}-{extract,patch,configure,build,install}."); + } + }, + + String => sub { + # No further checks possible. + }, + + Tool => sub { + if ($varname eq "TOOLS_NOOP" && $op eq "+=") { + # no warning for package-defined tool definitions + + } elsif ($value =~ m"^([-\w]+|\[)(?::(\w+))?$") { + my ($toolname, $tooldep) = ($1, $2); + if (!exists(get_tool_names()->{$toolname})) { + $line->log_error("Unknown tool \"${toolname}\"."); + } + if (defined($tooldep) && $tooldep !~ m"^(?:bootstrap|build|pkgsrc|run)$") { + $line->log_error("Unknown tool dependency \"${tooldep}\". Use one of \"build\", \"pkgsrc\" or \"run\"."); + } + } else { + $line->log_error("Invalid tool syntax: \"${value}\"."); + } + }, + + Unchecked => sub { + # Do nothing, as the name says. + }, + + URL => sub { + if ($value eq "" && defined($comment) && $comment =~ m"^#") { + # Ok + + } elsif ($value =~ m"\$\{(MASTER_SITE_[^:]*).*:=(.*)\}$") { + my ($name, $subdir) = ($1, $2); + + if (!exists(get_dist_sites_names()->{$name})) { + $line->log_error("${name} does not exist."); + } + if ($subdir !~ m"/$") { + $line->log_error("The subdirectory in ${name} must end with a slash."); + } + + } elsif ($value =~ regex_unresolved) { + # No further checks + + } elsif ($value =~ m"^(https?|ftp|gopher)://([-0-9A-Za-z.]+)(?::(\d+))?/([-%&+,./0-9:=?\@A-Z_a-z~]|#)*$") { + my ($proto, $host, $port, $path) = ($1, $2, $3, $4); + + if ($host =~ m"\.NetBSD\.org$"i && $host !~ m"\.NetBSD\.org$") { + $line->log_warning("Please write NetBSD.org instead of ${host}."); + } + + } elsif ($value =~ m"^([0-9A-Za-z]+)://([^/]+)(.*)$") { + my ($scheme, $host, $abs_path) = ($1, $2, $3); + + if ($scheme ne "ftp" && $scheme ne "http" && $scheme ne "https" && $scheme ne "gopher") { + $line->log_warning("\"${value}\" is not a valid URL. Only ftp, gopher, http, and https URLs are allowed here."); + + } elsif ($abs_path eq "") { + $line->log_note("For consistency, please add a trailing slash to \"${value}\"."); + + } else { + $line->log_warning("\"${value}\" is not a valid URL."); + } + + } else { + $line->log_warning("\"${value}\" is not a valid URL."); + } + }, + + UserGroupName => sub { + if ($value ne $value_novar) { + # No checks for now. + } elsif ($value !~ m"^[0-9_a-z]+$") { + $line->log_warning("Invalid user or group name \"${value}\"."); + } + }, + + Varname => sub { + if ($value ne "" && $value_novar eq "") { + # The value of another variable + + } elsif ($value_novar !~ m"^[A-Z_][0-9A-Z_]*(?:[.].*)?$") { + $line->log_warning("\"${value}\" is not a valid variable name."); + } + }, + + Version => sub { + if ($value !~ m"^([\d.])+$") { + $line->log_warning("Invalid version number \"${value}\"."); + } + }, + + WrapperReorder => sub { + if ($value =~ m"^reorder:l:([\w\-]+):([\w\-]+)$") { + my ($lib1, $lib2) = ($1, $2); + # Fine. + } else { + $line->log_warning("Unknown wrapper reorder command \"${value}\"."); + } + }, + + WrapperTransform => sub { + if ($value =~ m"^rm:(?:-[DILOUWflm].*|-std=.*)$") { + # Fine. + + } elsif ($value =~ m"^l:([^:]+):(.+)$") { + my ($lib, $replacement_libs) = ($1, $2); + # Fine. + + } elsif ($value =~ m"^'?(?:opt|rename|rm-optarg|rmdir):.*$") { + # FIXME: This is cheated. + # Fine. + + } elsif ($value eq "-e" || $value =~ m"^\"?'?s[|:,]") { + # FIXME: This is cheated. + # Fine. + + } else { + $line->log_warning("Unknown wrapper transform command \"${value}\"."); + } + }, + + WrkdirSubdirectory => sub { + checkline_mk_vartype_basic($line, $varname, "Pathname", $op, $value, $comment, $list_context, $is_guessed); + if ($value eq "\${WRKDIR}") { + # Fine. + } else { + $opt_debug_unchecked and $line->log_debug("Unchecked subdirectory \"${value}\" of \${WRKDIR}."); + } + }, + + WrksrcSubdirectory => sub { + if ($value =~ m"^(\$\{WRKSRC\})(?:/(.*))?") { + my ($prefix, $rest) = ($1, $2); + $line->log_note("You can use \"" . (defined($rest) ? $rest : ".") . "\" instead of \"${value}\"."); + + } elsif ($value ne "" && $value_novar eq "") { + # The value of another variable + + } elsif ($value_novar !~ m"^(?:\.|[0-9A-Za-z_\@][-0-9A-Za-z_\@./+]*)$") { + $line->log_warning("\"${value}\" is not a valid subdirectory of \${WRKSRC}."); + } + }, + + Yes => sub { + if ($value !~ m"^(?:YES|yes)(?:\s+#.*)?$") { + $line->log_warning("${varname} should be set to YES or yes."); + $line->explain_warning( +"This variable means \"yes\" if it is defined, and \"no\" if it is", +"undefined. Even when it has the value \"no\", this means \"yes\".", +"Therefore when it is defined, its value should correspond to its", +"meaning."); + } + }, + + YesNo => sub { + if ($value !~ m"^(?:YES|yes|NO|no)(?:\s+#.*)?$") { + $line->log_warning("${varname} should be set to YES, yes, NO, or no."); + } + }, + + YesNo_Indirectly => sub { + if ($value_novar ne "" && $value !~ m"^(?:YES|yes|NO|no)(?:\s+#.*)?$") { + $line->log_warning("${varname} should be set to YES, yes, NO, or no."); + } + }, + ); + + if (ref($type) eq "HASH") { + if (!exists($type->{$value})) { + $line->log_warning("\"${value}\" is not valid for ${varname}. Use one of { ".join(" ", sort(keys(%{$type})))." } instead."); + } + + } elsif (defined $type_dispatch{$type}) { + $type_dispatch{$type}->(); + + } else { + $line->log_fatal("Type ${type} unknown."); + } +} + +# Checks whether the list of version numbers that are given as the +# C<value> of the variable C<varname> are in decreasing order. +sub checkline_decreasing_order($$$) { + my ($line, $varname, $value) = @_; + + my @pyver = split(qr"\s+", $value); + if (!@pyver) { + $line->log_error("There must be at least one value for ${varname}."); + return; + } + + my $ver = shift(@pyver); + if ($ver !~ m"^\d+$") { + $line->log_error("All values for ${varname} must be numeric."); + return; + } + + while (@pyver) { + my $nextver = shift(@pyver); + if ($nextver !~ m"^\d+$") { + $line->log_error("All values for ${varname} must be numeric."); + return; + } + + if ($nextver >= $ver) { + $line->log_warning("The values for ${varname} should be in decreasing order."); + $line->explain_warning( +"If they aren't, it may be possible that needless versions of packages", +"are installed."); + } + $ver = $nextver; + } +} + +sub checkline_mk_vartype($$$$$) { + my ($line, $varname, $op, $value, $comment) = @_; + + return unless $opt_warn_types; + + my $vartypes = get_vartypes_map(); + my $varbase = varname_base($varname); + my $varcanon = varname_canon($varname); + + my $type = get_variable_type($line, $varname); + + if ($op eq "+=") { + if (defined($type)) { + if (!$type->may_use_plus_eq()) { + $line->log_warning("The \"+=\" operator should only be used with lists."); + } + } elsif ($varbase !~ m"^_" && $varbase !~ get_regex_plurals()) { + $line->log_warning("As ${varname} is modified using \"+=\", its name should indicate plural."); + } + } + + if (!defined($type)) { + # Cannot check anything if the type is not known. + $opt_debug_unchecked and $line->log_debug("Unchecked variable assignment for ${varname}."); + + } elsif ($op eq "!=") { + $opt_debug_misc and $line->log_debug("Use of !=: ${value}"); + + } elsif ($type->kind_of_list != LK_NONE) { + my (@words, $rest); + + if ($type->kind_of_list == LK_INTERNAL) { + @words = split(qr"\s+", $value); + $rest = ""; + } else { + @words = (); + $rest = $value; + while ($rest =~ s/^$regex_shellword//) { + my ($word) = ($1); + last if ($word =~ m"^#"); + push(@words, $1); + } + } + + foreach my $word (@words) { + checkline_mk_vartype_basic($line, $varname, $type->basic_type, $op, $word, $comment, true, $type->is_guessed); + if ($type->kind_of_list != LK_INTERNAL) { + checkline_mk_shellword($line, $word, true); + } + } + + if ($rest !~ m"^\s*$") { + $line->log_error("Internal pkglint error: rest=${rest}"); + } + + } else { + checkline_mk_vartype_basic($line, $varname, $type->basic_type, $op, $value, $comment, $type->is_practically_a_list(), $type->is_guessed); + } +} + +sub checkline_mk_varassign($$$$$) { + my ($line, $varname, $op, $value, $comment) = @_; + my ($used_vars); + my $varbase = varname_base($varname); + my $varcanon = varname_canon($varname); + + $opt_debug_trace and $line->log_debug("checkline_mk_varassign($varname, $op, $value)"); + + checkline_mk_vardef($line, $varname, $op); + + if ($op eq "?=" && defined($seen_bsd_prefs_mk) && !$seen_bsd_prefs_mk) { + if ($varbase eq "BUILDLINK_PKGSRCDIR" + || $varbase eq "BUILDLINK_DEPMETHOD" + || $varbase eq "BUILDLINK_ABI_DEPENDS") { + # FIXME: What about these ones? They occur quite often. + } else { + $opt_warn_extra and $line->log_warning("Please include \"../../mk/bsd.prefs.mk\" before using \"?=\"."); + $opt_warn_extra and $line->explain_warning( +"The ?= operator is used to provide a default value to a variable. In", +"pkgsrc, many variables can be set by the pkgsrc user in the mk.conf", +"file. This file must be included explicitly. If a ?= operator appears", +"before mk.conf has been included, it will not care about the user's", +"preferences, which can result in unexpected behavior. The easiest way", +"to include the mk.conf file is by including the bsd.prefs.mk file,", +"which will take care of everything."); + } + } + + checkline_mk_text($line, $value); + checkline_mk_vartype($line, $varname, $op, $value, $comment); + + # If the variable is not used and is untyped, it may be a + # spelling mistake. + if ($op eq ":=" && $varname eq lc($varname)) { + $opt_debug_unchecked and $line->log_debug("${varname} might be unused unless it is an argument to a procedure file."); + # TODO: check $varname against the list of "procedure files". + + } elsif (!var_is_used($varname)) { + my $vartypes = get_vartypes_map(); + my $deprecated = get_deprecated_map(); + + if (exists($vartypes->{$varname}) || exists($vartypes->{$varcanon})) { + # Ok + } elsif (exists($deprecated->{$varname}) || exists($deprecated->{$varcanon})) { + # Ok + } else { + $line->log_warning("${varname} is defined but not used. Spelling mistake?"); + } + } + + if ($value =~ m"/etc/rc\.d") { + $line->log_warning("Please use the RCD_SCRIPTS mechanism to install rc.d scripts automatically to \${RCD_SCRIPTS_EXAMPLEDIR}."); + } + + if (!$is_internal && $varname =~ m"^_") { + $line->log_warning("Variable names starting with an underscore are reserved for internal pkgsrc use."); + } + + if ($varname eq "PERL5_PACKLIST" && defined($effective_pkgbase) && $effective_pkgbase =~ m"^p5-(.*)") { + my ($guess) = ($1); + $guess =~ s/-/\//g; + $guess = "auto/${guess}/.packlist"; + + my ($ucvalue, $ucguess) = (uc($value), uc($guess)); + if ($ucvalue ne $ucguess && $ucvalue ne "\${PERL5_SITEARCH\}/${ucguess}") { + $line->log_warning("Unusual value for PERL5_PACKLIST -- \"${guess}\" expected."); + } + } + + if ($varname eq "CONFIGURE_ARGS" && $value =~ m"=\$\{PREFIX\}/share/kde") { + $line->log_note("Please .include \"../../meta-pkgs/kde3/kde3.mk\" instead of this line."); + $line->explain_note( +"That file probably does many things automatically and consistently that", +"this package also does. When using kde3.mk, you can probably also leave", +"out some explicit dependencies."); + } + + if ($varname eq "EVAL_PREFIX" && $value =~ m"^([\w_]+)=") { + my ($eval_varname) = ($1); + + # The variables mentioned in EVAL_PREFIX will later be + # defined by find-prefix.mk. Therefore, they are marked + # as known in the current file. + $mkctx_vardef->{$eval_varname} = $line; + } + + if ($varname eq "PYTHON_VERSIONS_ACCEPTED") { + checkline_decreasing_order($line, $varname, $value); + } + + if (defined($comment) && $comment eq "# defined" && $varname !~ m".*(?:_MK|_COMMON)$") { + $line->log_note("Please use \"# empty\", \"# none\" or \"yes\" instead of \"# defined\"."); + $line->explain_note( +"The value #defined says something about the state of the variable, but", +"not what that _means_. In some cases a variable that is defined means", +"\"yes\", in other cases it is an empty list (which is also only the", +"state of the variable), whose meaning could be described with \"none\".", +"It is this meaning that should be described."); + } + + if ($value =~ m"\$\{(PKGNAME|PKGVERSION)[:\}]") { + my ($pkgvarname) = ($1); + if ($varname =~ m"^PKG_.*_REASON$") { + # ok + } elsif ($varname =~ m"^(?:DIST_SUBDIR|WRKSRC)$") { + $line->log_warning("${pkgvarname} should not be used in ${varname}, as it sometimes includes the PKGREVISION. Please use ${pkgvarname}_NOREV instead."); + } else { + $opt_debug_misc and $line->log_debug("Use of PKGNAME in ${varname}."); + } + } + + if (exists(get_deprecated_map()->{$varname})) { + $line->log_warning("Definition of ${varname} is deprecated. ".get_deprecated_map()->{$varname}); + } elsif (exists(get_deprecated_map()->{$varcanon})) { + $line->log_warning("Definition of ${varname} is deprecated. ".get_deprecated_map()->{$varcanon}); + } + + if ($varname =~ m"^SITES_") { + $line->log_warning("SITES_* is deprecated. Please use SITES.* instead."); + } + + if ($value =~ m"^[^=]\@comment") { + $line->log_warning("Please don't use \@comment in ${varname}."); + $line->explain_warning( +"Here you are defining a variable containing \@comment. As this value", +"typically includes a space as the last character you probably also used", +"quotes around the variable. This can lead to confusion when adding this", +"variable to PLIST_SUBST, as all other variables are quoted using the :Q", +"operator when they are appended. As it is hard to check whether a", +"variable that is appended to PLIST_SUBST is already quoted or not, you", +"should not have pre-quoted variables at all. To solve this, you should", +"directly use PLIST_SUBST+= ${varname}=${value} or use any other", +"variable for collecting the list of PLIST substitutions and later", +"append that variable with PLIST_SUBST+= \${MY_PLIST_SUBST}."); + } + + # Mark the variable as PLIST condition. This is later used in + # checkfile_PLIST. + if (defined($pkgctx_plist_subst_cond) && $value =~ m"(.+)=.*\@comment.*") { + $pkgctx_plist_subst_cond->{$1}++; + } + + use constant op_to_use_time => { + ":=" => VUC_TIME_LOAD, + "!=" => VUC_TIME_LOAD, + "=" => VUC_TIME_RUN, + "+=" => VUC_TIME_RUN, + "?=" => VUC_TIME_RUN + }; + + $used_vars = extract_used_variables($line, $value); + my $vuc = PkgLint::VarUseContext->new( + op_to_use_time->{$op}, + get_variable_type($line, $varname), + VUC_SHELLWORD_UNKNOWN, # XXX: maybe PLAIN? + VUC_EXTENT_UNKNOWN + ); + foreach my $used_var (@{$used_vars}) { + checkline_mk_varuse($line, $used_var, "", $vuc); + } +} + +# The bmake parser is way too sloppy about syntax, so we need to check +# that here. +# +sub checkline_mk_cond($$) { + my ($line, $cond) = @_; + my ($op, $varname, $match, $value); + + $opt_debug_trace and $line->log_debug("checkline_mk_cond($cond)"); + my $tree = parse_mk_cond($line, $cond); + if (tree_match($tree, ["not", ["empty", ["match", \$varname, \$match]]])) { + #$line->log_note("tree_match: varname=$varname, match=$match"); + + my $type = get_variable_type($line, $varname); + my $btype = defined($type) ? $type->basic_type : undef; + if (defined($btype) && ref($type->basic_type) eq "HASH") { + if ($match !~ m"[\$\[*]" && !exists($btype->{$match})) { + $line->log_warning("Invalid :M value \"$match\". Only { " . join(" ", sort keys %$btype) . " } are allowed."); + } + } + + # Currently disabled because the valid options can also be defined in PKG_OPTIONS_GROUP.*. + # Additionally, all these variables may have multiple assigments (+=). + if (false && $varname eq "PKG_OPTIONS" && defined($pkgctx_vardef) && exists($pkgctx_vardef->{"PKG_SUPPORTED_OPTIONS"})) { + my $options = $pkgctx_vardef->{"PKG_SUPPORTED_OPTIONS"}->get("value"); + + if ($match !~ m"[\$\[*]" && index(" $options ", " $match ") == -1) { + $line->log_warning("Invalid option \"$match\". Only { $options } are allowed."); + } + } + + # TODO: PKG_BUILD_OPTIONS. That requires loading the + # complete package definitition for the package "pkgbase" + # or some other database. If we could confine all option + # definitions to options.mk, this would become easier. + + } elsif (tree_match($tree, [\$op, ["var", \$varname], ["string", \$value]])) { + checkline_mk_vartype($line, $varname, "use", $value, undef); + + } + # XXX: When adding new cases, be careful that the variables may have + # been partially initialized by previous calls to tree_match. + # XXX: Maybe it is better to clear these references at the beginning + # of tree_match. +} + +# +# Procedures to check an array of lines. +# + +sub checklines_trailing_empty_lines($) { + my ($lines) = @_; + my ($last, $max); + + $max = $#{$lines} + 1; + for ($last = $max; $last > 1 && $lines->[$last - 1]->text eq ""; ) { + $last--; + } + if ($last != $max) { + $lines->[$last]->log_note("Trailing empty lines."); + } +} + +sub checklines_package_Makefile_varorder($) { + my ($lines) = @_; + + return unless $opt_warn_varorder; + + use enum qw(once optional many); + my (@sections) = ( + [ "Initial comments", once, + [ + ] + ], + [ "Unsorted stuff, part 1", once, + [ + [ "DISTNAME", optional ], + [ "PKGNAME", optional ], + [ "PKGREVISION", optional ], + [ "CATEGORIES", once ], + [ "MASTER_SITES", optional ], + [ "DIST_SUBDIR", optional ], + [ "EXTRACT_SUFX", optional ], + [ "DISTFILES", many ], + [ "SITES.*", many ], + ] + ], + [ "Distribution patches", optional, + [ + [ "PATCH_SITES", optional ], # or once? + [ "PATCH_SITE_SUBDIR", optional ], + [ "PATCHFILES", optional ], # or once? + [ "PATCH_DIST_ARGS", optional ], + [ "PATCH_DIST_STRIP", optional ], + [ "PATCH_DIST_CAT", optional ], + ] + ], + [ "Unsorted stuff, part 2", once, + [ + [ "MAINTAINER", optional ], + [ "OWNER", optional ], + [ "HOMEPAGE", optional ], + [ "COMMENT", once ], + [ "LICENSE", once ], + ] + ], + [ "Legal issues", optional, + [ + [ "LICENSE_FILE", optional ], + [ "RESTRICTED", optional ], + [ "NO_BIN_ON_CDROM", optional ], + [ "NO_BIN_ON_FTP", optional ], + [ "NO_SRC_ON_CDROM", optional ], + [ "NO_SRC_ON_FTP", optional ], + ] + ], + [ "Technical restrictions", optional, + [ + [ "BROKEN_EXCEPT_ON_PLATFORM", many ], + [ "BROKEN_ON_PLATFORM", many ], + [ "NOT_FOR_PLATFORM", many ], + [ "ONLY_FOR_PLATFORM", many ], + [ "NOT_FOR_COMPILER", many ], + [ "ONLY_FOR_COMPILER", many ], + [ "NOT_FOR_UNPRIVILEGED", optional ], + [ "ONLY_FOR_UNPRIVILEGED", optional ], + ] + ], + [ "Dependencies", optional, + [ + [ "BUILD_DEPENDS", many ], + [ "TOOL_DEPENDS", many ], + [ "DEPENDS", many ], + ] + ] + ); + + if (!defined($seen_Makefile_common) || $seen_Makefile_common) { + return; + } + + my ($lineno, $sectindex, $varindex) = (0, -1, 0); + my ($next_section, $vars, $below, $below_what) = (true, undef, {}, undef); + + # If the current section is optional but contains non-optional + # fields, the complete section may be skipped as long as there + # has not been a non-optional variable. + my $may_skip_section = false; + + # In each iteration, one of the following becomes true: + # - new.lineno > old.lineno + # - new.sectindex > old.sectindex + # - new.sectindex == old.sectindex && new.varindex > old.varindex + # - new.next_section == true && old.next_section == false + while ($lineno <= $#{$lines}) { + my $line = $lines->[$lineno]; + my $text = $line->text; + + $opt_debug_misc and $line->log_debug("[varorder] section ${sectindex} variable ${varindex}."); + + if ($next_section) { + $next_section = false; + $sectindex++; + last if ($sectindex > $#sections); + $vars = $sections[$sectindex]->[2]; + $may_skip_section = ($sections[$sectindex]->[1] == optional); + $varindex = 0; + } + + if ($text =~ m"^#") { + $lineno++; + + } elsif ($line->has("varcanon")) { + my $varcanon = $line->get("varcanon"); + + if (exists($below->{$varcanon})) { + if (defined($below->{$varcanon})) { + $line->log_warning("${varcanon} appears too late. Please put it below $below->{$varcanon}."); + } else { + $line->log_warning("${varcanon} appears too late. It should be the very first definition."); + } + $lineno++; + next; + } + + while ($varindex <= $#{$vars} && $varcanon ne $vars->[$varindex]->[0] && ($vars->[$varindex]->[1] != once || $may_skip_section)) { + if ($vars->[$varindex]->[1] == once) { + $may_skip_section = false; + } + $below->{$vars->[$varindex]->[0]} = $below_what; + $varindex++; + } + if ($varindex > $#{$vars}) { + if ($sections[$sectindex]->[1] != optional) { + $line->log_warning("Empty line expected."); + } + $next_section = true; + + } elsif ($varcanon ne $vars->[$varindex]->[0]) { + $line->log_warning("Expected " . $vars->[$varindex]->[0] . ", but found " . $varcanon . "."); + $lineno++; + + } else { + if ($vars->[$varindex]->[1] != many) { + $below->{$vars->[$varindex]->[0]} = $below_what; + $varindex++; + } + $lineno++; + } + $below_what = $varcanon; + + } else { + while ($varindex <= $#{$vars}) { + if ($vars->[$varindex]->[1] == once && !$may_skip_section) { + $line->log_warning($vars->[$varindex]->[0] . " should be set here."); + } + $below->{$vars->[$varindex]->[0]} = $below_what; + $varindex++; + } + $next_section = true; + if ($text eq "") { + $below_what = "the previous empty line"; + $lineno++; + } + } + } +} + +sub checklines_mk($) { + my ($lines) = @_; + my ($allowed_targets) = ({}); + my ($substcontext) = PkgLint::SubstContext->new(); + + assert(@{$lines} != 0, "checklines_mk may only be called with non-empty lines."); + $opt_debug_trace and log_debug($lines->[0]->fname, NO_LINES, "checklines_mk()"); + + # Define global variables for the Makefile context. + $mkctx_indentations = [0]; + $mkctx_target = undef; + $mkctx_for_variables = {}; + $mkctx_vardef = {}; + $mkctx_build_defs = {}; + $mkctx_plist_vars = {}; + $mkctx_tools = {%{get_predefined_tool_names()}}; + $mkctx_varuse = {}; + + determine_used_variables($lines); + + foreach my $prefix (qw(pre do post)) { + foreach my $action (qw(fetch extract patch tools wrapper configure build test install package clean)) { + $allowed_targets->{"${prefix}-${action}"} = true; + } + } + + # + # In the first pass, all additions to BUILD_DEFS and USE_TOOLS + # are collected to make the order of the definitions irrelevant. + # + + foreach my $line (@{$lines}) { + next unless $line->has("is_varassign"); + my $varcanon = $line->get("varcanon"); + + if ($varcanon eq "BUILD_DEFS" || $varcanon eq "PKG_GROUPS_VARS" || $varcanon eq "PKG_USERS_VARS") { + foreach my $varname (split(qr"\s+", $line->get("value"))) { + $mkctx_build_defs->{$varname} = true; + $opt_debug_misc and $line->log_debug("${varname} is added to BUILD_DEFS."); + } + + } elsif ($varcanon eq "PLIST_VARS") { + foreach my $id (split(qr"\s+", $line->get("value"))) { + $mkctx_plist_vars->{"PLIST.$id"} = true; + $opt_debug_misc and $line->log_debug("PLIST.${id} is added to PLIST_VARS."); + use_var($line, "PLIST.$id"); + } + + } elsif ($varcanon eq "USE_TOOLS") { + foreach my $tool (split(qr"\s+", $line->get("value"))) { + $tool =~ s/:(build|run)//; + $mkctx_tools->{$tool} = true; + $opt_debug_misc and $line->log_debug("${tool} is added to USE_TOOLS."); + } + + } elsif ($varcanon eq "SUBST_VARS.*") { + foreach my $svar (split(/\s+/, $line->get("value"))) { + use_var($svar, varname_canon($svar)); + $opt_debug_misc and $line->log_debug("varuse $svar"); + } + + } elsif ($varcanon eq "OPSYSVARS") { + foreach my $osvar (split(/\s+/, $line->get("value"))) { + use_var($line, "$osvar.*"); + def_var($line, $osvar); + } + } + } + + # + # In the second pass, all "normal" checks are done. + # + + if (0 <= $#{$lines}) { + checkline_rcsid_regex($lines->[0], qr"^#\s+", "# "); + } + + foreach my $line (@{$lines}) { + my $text = $line->text; + + checkline_trailing_whitespace($line); + + if ($line->has("is_empty")) { + $substcontext->check_end($line); + + } elsif ($line->has("is_comment")) { + # No further checks. + + } elsif ($text =~ regex_varassign) { + my ($varname, $op, undef, $comment) = ($1, $2, $3, $4); + my $space1 = substr($text, $+[1], $-[2] - $+[1]); + my $align = substr($text, $+[2], $-[3] - $+[2]); + my $value = $line->get("value"); + + if ($align !~ m"^(\t*|[ ])$") { + $opt_warn_space && $line->log_note("Alignment of variable values should be done with tabs, not spaces."); + my $prefix = "${varname}${space1}${op}"; + my $aligned_len = tablen("${prefix}${align}"); + if ($aligned_len % 8 == 0) { + my $tabalign = ("\t" x (($aligned_len - tablen($prefix) + 7) / 8)); + $line->replace("${prefix}${align}", "${prefix}${tabalign}"); + } + } + checkline_mk_varassign($line, $varname, $op, $value, $comment); + $substcontext->check_varassign($line, $varname, $op, $value); + + } elsif ($text =~ regex_mk_shellcmd) { + my ($shellcmd) = ($1); + checkline_mk_shellcmd($line, $shellcmd); + + } elsif ($text =~ regex_mk_include) { + my ($include, $includefile) = ($1, $2); + + $opt_debug_include and $line->log_debug("includefile=${includefile}"); + checkline_relative_path($line, $includefile, $include eq "include"); + + if ($includefile =~ m"../Makefile$") { + $line->log_error("Other Makefiles must not be included directly."); + $line->explain_warning( +"If you want to include portions of another Makefile, extract", +"the common parts and put them into a Makefile.common. After", +"that, both this one and the other package should include the", +"Makefile.common."); + } + + if ($includefile eq "../../mk/bsd.prefs.mk") { + if ($line->fname =~ m"buildlink3\.mk$") { + $line->log_note("For efficiency reasons, please include bsd.fast.prefs.mk instead of bsd.prefs.mk."); + } + $seen_bsd_prefs_mk = true; + } elsif ($includefile eq "../../mk/bsd.fast.prefs.mk") { + $seen_bsd_prefs_mk = true; + } + + if ($includefile =~ m"/x11-links/buildlink3\.mk$") { + $line->log_error("${includefile} must not be included directly. Include \"../../mk/x11.buildlink3.mk\" instead."); + } + if ($includefile =~ m"/jpeg/buildlink3\.mk$") { + $line->log_error("${includefile} must not be included directly. Include \"../../mk/jpeg.buildlink3.mk\" instead."); + } + if ($includefile =~ m"/intltool/buildlink3\.mk$") { + $line->log_warning("Please say \"USE_TOOLS+= intltool\" instead of this line."); + } + if ($includefile =~ m"(.*)/builtin\.mk$") { + my ($dir) = ($1); + $line->log_error("${includefile} must not be included directly. Include \"${dir}/buildlink3.mk\" instead."); + } + + } elsif ($text =~ regex_mk_sysinclude) { + my ($includefile, $comment) = ($1, $2); + + # No further action. + + } elsif ($text =~ regex_mk_cond) { + my ($indent, $directive, $args, $comment) = ($1, $2, $3, $4); + + use constant regex_directives_with_args => qr"^(?:if|ifdef|ifndef|elif|for|undef)$"; + + if ($directive =~ m"^(?:endif|endfor|elif|else)$") { + if ($#{$mkctx_indentations} >= 1) { + pop(@{$mkctx_indentations}); + } else { + $line->log_error("Unmatched .${directive}."); + } + } + + # Check the indentation + if ($indent ne " " x $mkctx_indentations->[-1]) { + $opt_warn_space and $line->log_note("This directive should be indented by ".$mkctx_indentations->[-1]." spaces."); + } + + if ($directive eq "if" && $args =~ m"^!defined\([\w]+_MK\)$") { + push(@{$mkctx_indentations}, $mkctx_indentations->[-1]); + + } elsif ($directive =~ m"^(?:if|ifdef|ifndef|for|elif|else)$") { + push(@{$mkctx_indentations}, $mkctx_indentations->[-1] + 2); + } + + if ($directive =~ regex_directives_with_args && !defined($args)) { + $line->log_error("\".${directive}\" must be given some arguments."); + + } elsif ($directive !~ regex_directives_with_args && defined($args)) { + $line->log_error("\".${directive}\" does not take arguments."); + + if ($directive eq "else") { + $line->log_note("If you meant \"else if\", use \".elif\"."); + } + + } elsif ($directive eq "if" || $directive eq "elif") { + checkline_mk_cond($line, $args); + + } elsif ($directive eq "ifdef" || $directive eq "ifndef") { + if ($args =~ m"\s") { + $line->log_error("The \".${directive}\" directive can only handle _one_ argument."); + } else { + $line->log_warning("The \".${directive}\" directive is deprecated. Please use \".if " + . (($directive eq "ifdef" ? "" : "!")) + . "defined(${args})\" instead."); + } + + } elsif ($directive eq "for") { + if ($args =~ m"^(\S+(?:\s*\S+)*?)\s+in\s+(.*)$") { + my ($vars, $values) = ($1, $2); + + foreach my $var (split(qr"\s+", $vars)) { + if (!$is_internal && $var =~ m"^_") { + $line->log_warning("Variable names starting with an underscore are reserved for internal pkgsrc use."); + } + + if ($var =~ m"^[_a-z][_a-z0-9]*$") { + # Fine. + } elsif ($var =~ m"[A-Z]") { + $line->log_warning(".for variable names should not contain uppercase letters."); + } else { + $line->log_error("Invalid variable name \"${var}\"."); + } + + $mkctx_for_variables->{$var} = true; + } + + # Check if any of the value's types is not guessed. + my $guessed = true; + foreach my $value (split(qr"\s+", $values)) { # XXX: too simple + if ($value =~ m"^\$\{(.*)\}") { + my $type = get_variable_type($line, $1); + if (defined($type) && !$type->is_guessed()) { + $guessed = false; + } + } + } + + my $for_loop_type = PkgLint::Type->new( + LK_INTERNAL, + "Unchecked", + [[qr".*", "pu"]], + $guessed + ); + my $for_loop_context = PkgLint::VarUseContext->new( + VUC_TIME_LOAD, + $for_loop_type, + VUC_SHELLWORD_FOR, + VUC_EXTENT_WORD + ); + foreach my $var (@{extract_used_variables($line, $values)}) { + checkline_mk_varuse($line, $var, "", $for_loop_context); + } + + } + + } elsif ($directive eq "undef" && defined($args)) { + foreach my $var (split(qr"\s+", $args)) { + if (exists($mkctx_for_variables->{$var})) { + $line->log_note("Using \".undef\" after a \".for\" loop is unnecessary."); + } + } + } + + } elsif ($text =~ regex_mk_dependency) { + my ($targets, $whitespace, $dependencies, $comment) = ($1, $2, $3, $4); + + $opt_debug_misc and $line->log_debug("targets=${targets}, dependencies=${dependencies}"); + $mkctx_target = $targets; + + foreach my $source (split(/\s+/, $dependencies)) { + if ($source eq ".PHONY") { + foreach my $target (split(/\s+/, $targets)) { + $allowed_targets->{$target} = true; + } + } + } + + foreach my $target (split(/\s+/, $targets)) { + if ($target eq ".PHONY") { + foreach my $dep (split(qr"\s+", $dependencies)) { + $allowed_targets->{$dep} = true; + } + + } elsif ($target eq ".ORDER") { + # TODO: Check for spelling mistakes. + + } elsif (!exists($allowed_targets->{$target})) { + $line->log_warning("Unusual target \"${target}\"."); + $line->explain_warning( +"If you really want to define your own targets, you can \"declare\"", +"them by inserting a \".PHONY: my-target\" line before this line. This", +"will tell make(1) to not interpret this target's name as a filename."); + } + } + + } elsif ($text =~ m"^\.\s*(\S*)") { + my ($directive) = ($1); + + $line->log_error("Unknown directive \".${directive}\"."); + + } elsif ($text =~ m"^ ") { + $line->log_warning("Makefile lines should not start with space characters."); + $line->explain_warning( +"If you want this line to contain a shell program, use a tab", +"character for indentation. Otherwise please remove the leading", +"white-space."); + + } else { + $line->log_error("[Internal] Unknown line format: $text"); + } + } + if (@{$lines} > 0) { + $substcontext->check_end($lines->[-1]); + } + + checklines_trailing_empty_lines($lines); + + if ($#{$mkctx_indentations} != 0) { + $lines->[-1]->log_error("Directive indentation is not 0, but ".$mkctx_indentations->[-1]." at EOF."); + } + + # Clean up global variables. + $mkctx_for_variables = undef; + $mkctx_indentations = undef; + $mkctx_target = undef; + $mkctx_vardef = undef; + $mkctx_build_defs = undef; + $mkctx_plist_vars = undef; + $mkctx_tools = undef; + $mkctx_varuse = undef; +} + +sub checklines_buildlink3_inclusion($) { + my ($lines) = @_; + my ($included_files); + + assert(@{$lines} != 0, "The lines array must be non-empty."); + $opt_debug_trace and log_debug($lines->[0]->fname, NO_LINES, "checklines_buildlink3_inclusion()"); + + if (!defined($pkgctx_bl3)) { + return; + } + + # Collect all the included buildlink3.mk files from the file. + $included_files = {}; + foreach my $line (@{$lines}) { + if ($line->text =~ regex_mk_include) { + my (undef, $file, $comment) = ($1, $2, $3); + + if ($file =~ m"^\.\./\.\./(.*)/buildlink3\.mk") { + my ($bl3) = ($1); + + $included_files->{$bl3} = $line; + if (!exists($pkgctx_bl3->{$bl3})) { + $line->log_warning("${bl3}/buildlink3.mk is included by this file but not by the package."); + } + } + } + } + + # Print debugging messages for all buildlink3.mk files that are + # included by the package but not by this buildlink3.mk file. + foreach my $package_bl3 (sort(keys(%{$pkgctx_bl3}))) { + if (!exists($included_files->{$package_bl3})) { + $opt_debug_misc and $pkgctx_bl3->{$package_bl3}->log_debug("${package_bl3}/buildlink3.mk is included by the package but not by the buildlink3.mk file."); + } + } +} + +# +# Procedures to check a single file. +# + +sub checkfile_ALTERNATIVES($) { + my ($fname) = @_; + my ($lines); + + $opt_debug_trace and log_debug($fname, NO_LINES, "checkfile_ALTERNATIVES()"); + + checkperms($fname); + if (!($lines = load_file($fname))) { + log_error($fname, NO_LINE_NUMBER, "Cannot be read."); + return; + } +} + +sub checkfile_buildlink3_mk($) { + my ($fname) = @_; + my ($lines, $lineno, $m); + + $opt_debug_trace and log_debug($fname, NO_LINES, "checkfile_buildlink3_mk()"); + + checkperms($fname); + if (!($lines = load_lines($fname, true))) { + log_error($fname, NO_LINE_NUMBER, "Cannot be read."); + return; + } + if (@{$lines} == 0) { + log_error($fname, NO_LINES, "Must not be empty."); + return; + } + + parselines_mk($lines); + checklines_mk($lines); + + $lineno = 0; + + # Header comments + while ($lineno <= $#{$lines} && (my $text = $lines->[$lineno]->text) =~ m"^#") { + if ($text =~ m"^# XXX") { + $lines->[$lineno]->log_note("Please read this comment and remove it if appropriate."); + } + $lineno++; + } + expect_empty_line($lines, \$lineno); + + if (expect($lines, \$lineno, qr"^BUILDLINK_DEPMETHOD\.(\S+)\?=.*$")) { + $lines->[$lineno - 1]->log_warning("This line belongs inside the .ifdef block."); + while ($lines->[$lineno]->text eq "") { + $lineno++; + } + } + + if (!($m = expect($lines, \$lineno, qr"^BUILDLINK_TREE\+=\s*(\S+)$"))) { + $lines->[$lineno]->log_warning("Expected a BUILDLINK_TREE line."); + return; + } + + checklines_buildlink3_mk($lines, $lineno, $m->text(1)); +} + +sub checklines_buildlink3_mk($$$) { + my ($lines, $lineno, $pkgid) = @_; + my ($m); + my ($bl_PKGBASE_line, $bl_PKGBASE); + my ($bl_pkgbase_line, $bl_pkgbase); + my ($abi_line, $abi_pkg, $abi_version); + my ($api_line, $api_pkg, $api_version); + + # First paragraph: Introduction of the package identifier + $bl_pkgbase_line = $lines->[$lineno - 1]; + $bl_pkgbase = $pkgid; + $opt_debug_misc and $bl_pkgbase_line->log_debug("bl_pkgbase=${bl_pkgbase}"); + expect_empty_line($lines, \$lineno); + + # Second paragraph: multiple inclusion protection and introduction + # of the uppercase package identifier. + return unless ($m = expect_re($lines, \$lineno, qr"^\.if !defined\((\S+)_BUILDLINK3_MK\)$")); + $bl_PKGBASE_line = $lines->[$lineno - 1]; + $bl_PKGBASE = $m->text(1); + $opt_debug_misc and $bl_PKGBASE_line->log_debug("bl_PKGBASE=${bl_PKGBASE}"); + expect_re($lines, \$lineno, qr"^\Q$bl_PKGBASE\E_BUILDLINK3_MK:=$"); + expect_empty_line($lines, \$lineno); + + my $norm_bl_pkgbase = $bl_pkgbase; + $norm_bl_pkgbase =~ s/-/_/g; + $norm_bl_pkgbase = uc($norm_bl_pkgbase); + if ($norm_bl_pkgbase ne $bl_PKGBASE) { + $bl_PKGBASE_line->log_error("Package name mismatch between ${bl_PKGBASE} ..."); + $bl_pkgbase_line->log_error("... and ${bl_pkgbase}."); + } + if (defined($effective_pkgbase) && $effective_pkgbase ne $bl_pkgbase) { + $bl_pkgbase_line->log_error("Package name mismatch between ${bl_pkgbase} ..."); + $effective_pkgname_line->log_error("... and ${effective_pkgbase}."); + } + + # Third paragraph: Package information. + my $if_level = 1; # the first .if is from the second paragraph. + while (true) { + + if ($lineno > $#{$lines}) { + lines_log_warning($lines, $lineno, "Expected .endif"); + return; + } + + my $line = $lines->[$lineno]; + + if (($m = expect($lines, \$lineno, regex_varassign))) { + my ($varname, $value) = ($m->text(1), $m->text(3)); + my $do_check = false; + + if ($varname eq "BUILDLINK_ABI_DEPENDS.${bl_pkgbase}") { + $abi_line = $line; + if ($value =~ regex_dependency_lge) { + ($abi_pkg, $abi_version) = ($1, $2); + } elsif ($value =~ regex_dependency_wildcard) { + ($abi_pkg) = ($1); + } else { + $opt_debug_unchecked and $line->log_debug("Unchecked dependency pattern \"${value}\"."); + } + $do_check = true; + } + if ($varname eq "BUILDLINK_API_DEPENDS.${bl_pkgbase}") { + $api_line = $line; + if ($value =~ regex_dependency_lge) { + ($api_pkg, $api_version) = ($1, $2); + } elsif ($value =~ regex_dependency_wildcard) { + ($api_pkg) = ($1); + } else { + $opt_debug_unchecked and $line->log_debug("Unchecked dependency pattern \"${value}\"."); + } + $do_check = true; + } + if ($do_check && defined($abi_pkg) && defined($api_pkg)) { + if ($abi_pkg ne $api_pkg) { + $abi_line->log_warning("Package name mismatch between ${abi_pkg} ..."); + $api_line->log_warning("... and ${api_pkg}."); + } + } + if ($do_check && defined($abi_version) && defined($api_version)) { + if (!dewey_cmp($abi_version, ">=", $api_version)) { + $abi_line->log_warning("ABI version (${abi_version}) should be at least ..."); + $api_line->log_warning("... API version (${api_version})."); + } + } + + if ($varname =~ m"^BUILDLINK_[\w_]+\.(.*)$") { + my ($varparam) = ($1); + + if ($varparam ne $bl_pkgbase) { + $line->log_warning("Only buildlink variables for ${bl_pkgbase}, not ${varparam} may be set in this file."); + } + } + + if ($varname eq "pkgbase") { + expect_re($lines, \$lineno, qr"^\.\s*include \"../../mk/pkg-build-options.mk\"$"); + } + + # TODO: More checks. + + } elsif (expect($lines, \$lineno, qr"^(?:#.*)?$")) { + # Comments and empty lines are fine here. + + } elsif (expect($lines, \$lineno, qr"^\.\s*include \"\.\./\.\./([^/]+/[^/]+)/buildlink3\.mk\"$") + || expect($lines, \$lineno, qr"^\.\s*include \"\.\./\.\./mk/(\S+)\.buildlink3\.mk\"$")) { + # TODO: Maybe check dependency lines. + + } elsif (expect($lines, \$lineno, qr"^\.if\s")) { + $if_level++; + + } elsif (expect($lines, \$lineno, qr"^\.endif.*$")) { + $if_level--; + last if $if_level == 0; + + } else { + $opt_debug_unchecked and lines_log_warning($lines, $lineno, "Unchecked line in third paragraph."); + $lineno++; + } + } + if (!defined($api_line)) { + $lines->[$lineno - 1]->log_warning("Definition of BUILDLINK_API_DEPENDS is missing."); + } + expect_empty_line($lines, \$lineno); + + # Fourth paragraph: Cleanup, corresponding to the first paragraph. + return unless expect_re($lines, \$lineno, qr"^BUILDLINK_TREE\+=\s*-\Q$bl_pkgbase\E$"); + + if ($lineno <= $#{$lines}) { + $lines->[$lineno]->log_warning("The file should end here."); + } + + checklines_buildlink3_inclusion($lines); +} + +sub checkfile_DESCR($) { + my ($fname) = @_; + my ($maxchars, $maxlines) = (80, 24); + my ($lines); + + $opt_debug_trace and log_debug($fname, NO_LINES, "checkfile_DESCR()"); + + checkperms($fname); + if (!($lines = load_file($fname))) { + log_error($fname, NO_LINE_NUMBER, "Cannot be read."); + return; + } + if (@{$lines} == 0) { + log_error($fname, NO_LINE_NUMBER, "Must not be empty."); + return; + } + + foreach my $line (@{$lines}) { + checkline_length($line, $maxchars); + checkline_trailing_whitespace($line); + checkline_valid_characters($line, regex_validchars); + if ($line->text =~ m"\$\{") { + $line->log_warning("Variables are not expanded in the DESCR file."); + } + } + checklines_trailing_empty_lines($lines); + + if (@{$lines} > $maxlines) { + my $line = $lines->[$maxlines]; + + $line->log_warning("File too long (should be no more than $maxlines lines)."); + $line->explain_warning( +"A common terminal size is 80x25 characters. The DESCR file should", +"fit on one screen. It is also intended to give a _brief_ summary", +"about the package's contents."); + } + autofix($lines); +} + +sub checkfile_distinfo($) { + my ($fname) = @_; + my ($lines, %in_distinfo, $patches_dir, $di_is_committed, $current_fname, $is_patch, @seen_algs); + + $opt_debug_trace and log_debug($fname, NO_LINES, "checkfile_distinfo()"); + + $di_is_committed = is_committed($fname); + + checkperms($fname); + if (!($lines = load_file($fname))) { + log_error($fname, NO_LINE_NUMBER, "Cannot be read."); + return; + } + + if (@{$lines} == 0) { + log_error($fname, NO_LINE_NUMBER, "Must not be empty."); + return; + } + + checkline_rcsid($lines->[0], ""); + if (1 <= $#{$lines} && $lines->[1]->text ne "") { + $lines->[1]->log_note("Empty line expected."); + $lines->[1]->explain_note("This is merely for aesthetical purposes."); + } + + $patches_dir = $patchdir; + if (!defined($patches_dir) && -d "${current_dir}/patches") { + $patches_dir = "patches"; + } + + my $on_filename_change = sub($$) { + my ($line, $new_fname) = @_; + + if (defined($current_fname)) { + my $seen_algs = join(", ", @seen_algs); + $opt_debug_misc and $line->log_debug("File ${current_fname} has checksums ${seen_algs}."); + if ($is_patch) { + if ($seen_algs ne "SHA1") { + $line->log_error("Expected SHA1 checksum for ${current_fname}, got ${seen_algs}."); + } + } else { + if ($seen_algs ne "SHA1, RMD160, Size" && $seen_algs ne "SHA1, RMD160, SHA512, Size") { + $line->log_error("Expected SHA1, RMD160, Size checksums for ${current_fname}, got ${seen_algs}."); + } + } + } + + $is_patch = defined($new_fname) && $new_fname =~ m"^patch-.+$" && -f "${current_dir}/${patches_dir}/${new_fname}"; + $current_fname = $new_fname; + @seen_algs = (); + }; + + foreach my $line (@{$lines}[2..$#{$lines}]) { + if ($line->text !~ m"^(\w+) \(([^)]+)\) = (.*)(?: bytes)?$") { + $line->log_error("Unknown line type."); + next; + } + my ($alg, $chksum_fname, $sum) = ($1, $2, $3); + + if (!defined($current_fname) || $chksum_fname ne $current_fname) { + $on_filename_change->($line, $chksum_fname); + } + + if ($chksum_fname !~ m"^\w") { + $line->log_error("All file names should start with a letter."); + } + + # Inter-package check for differing distfile checksums. + if ($opt_check_global && !$is_patch) { + # Note: Perl-specific auto-population. + if (exists($ipc_distinfo->{$alg}->{$chksum_fname})) { + my $other = $ipc_distinfo->{$alg}->{$chksum_fname}; + + if ($other->[1] eq $sum) { + # Fine. + } else { + $line->log_error("The ${alg} checksum for ${chksum_fname} differs ..."); + $other->[0]->log_error("... from this one."); + } + } else { + $ipc_distinfo->{$alg}->{$chksum_fname} = [$line, $sum]; + } + } + + push(@seen_algs, $alg); + + if ($is_patch && defined($patches_dir) && !(defined($distinfo_file) && $distinfo_file eq "./../../lang/php5/distinfo")) { + my $fname = "${current_dir}/${patches_dir}/${chksum_fname}"; + if ($di_is_committed && !is_committed($fname)) { + $line->log_warning("${patches_dir}/${chksum_fname} is registered in distinfo but not added to CVS."); + } + + if (open(my $patchfile, "<", $fname)) { + my $sha1 = Digest::SHA1->new(); + while (defined(my $patchline = <$patchfile>)) { + $sha1->add($patchline) unless $patchline =~ m"\$[N]etBSD"; + } + close($patchfile); + my $chksum = $sha1->hexdigest(); + if ($sum ne $chksum) { + $line->log_error("${alg} checksum of ${chksum_fname} differs (expected ${sum}, got ${chksum}). Rerun '".conf_make." makepatchsum'."); + } + } else { + $line->log_warning("${chksum_fname} does not exist."); + $line->explain_warning( +"All patches that are mentioned in a distinfo file should actually exist.", +"What's the use of a checksum if there is no file to check?"); + } + } + $in_distinfo{$chksum_fname} = true; + + } + $on_filename_change->(PkgLint::Line->new($fname, NO_LINE_NUMBER, "", []), undef); + checklines_trailing_empty_lines($lines); + + if (defined($patches_dir)) { + foreach my $patch (glob("${current_dir}/${patches_dir}/patch-*")) { + $patch = basename($patch); + if (!exists($in_distinfo{$patch})) { + log_error($fname, NO_LINE_NUMBER, "$patch is not recorded. Rerun '".conf_make." makepatchsum'."); + } + } + } +} + +sub checkfile_extra($) { + my ($fname) = @_; + + $opt_debug_trace and log_debug($fname, NO_LINES, "checkfile_extra()"); + + my $lines = load_file($fname) or return log_error($fname, NO_LINE_NUMBER, "Could not be read."); + checklines_trailing_empty_lines($lines); + checkperms($fname); +} + +sub checkfile_INSTALL($) { + my ($fname) = @_; + + $opt_debug_trace and log_debug($fname, NO_LINES, "checkfile_INSTALL()"); + + checkperms($fname); + my $lines = load_file($fname) or return log_error($fname, NO_LINE_NUMBER, "Cannot be read."); +} + +sub checkfile_MESSAGE($) { + my ($fname) = @_; + + my @explanation = ( + "A MESSAGE file should consist of a header line, having 75 \"=\"", + "characters, followed by a line containing only the RCS Id, then an", + "empty line, your text and finally the footer line, which is the", + "same as the header line."); + + $opt_debug_trace and log_debug($fname, NO_LINES, "checkfile_MESSAGE()"); + + checkperms($fname); + my $lines = load_file($fname) or return log_error($fname, NO_LINE_NUMBER, "Cannot be read."); + + if (@{$lines} < 3) { + log_warning($fname, NO_LINE_NUMBER, "File too short."); + explain_warning($fname, NO_LINE_NUMBER, @explanation); + return; + } + if ($lines->[0]->text ne "=" x 75) { + $lines->[0]->log_warning("Expected a line of exactly 75 \"=\" characters."); + explain_warning($fname, NO_LINE_NUMBER, @explanation); + } + checkline_rcsid($lines->[1], ""); + foreach my $line (@{$lines}) { + checkline_length($line, 80); + checkline_trailing_whitespace($line); + checkline_valid_characters($line, regex_validchars); + } + if ($lines->[-1]->text ne "=" x 75) { + $lines->[-1]->log_warning("Expected a line of exactly 75 \"=\" characters."); + explain_warning($fname, NO_LINE_NUMBER, @explanation); + } + checklines_trailing_empty_lines($lines); +} + +sub checkfile_mk($) { + my ($fname) = @_; + + $opt_debug_trace and log_debug($fname, NO_LINES, "checkfile_mk()"); + + checkperms($fname); + my $lines = load_lines($fname, true) or return log_error($fname, NO_LINE_NUMBER, "Cannot be read."); + + parselines_mk($lines); + checklines_mk($lines); + autofix($lines); +} + +sub checkfile_package_Makefile($$) { + my ($fname, $lines) = @_; + + $opt_debug_trace and log_debug($fname, NO_LINES, "checkfile_package_Makefile(..., ...)"); + + checkperms($fname); + + if (!exists($pkgctx_vardef->{"PLIST_SRC"}) + && !exists($pkgctx_vardef->{"GENERATE_PLIST"}) + && !exists($pkgctx_vardef->{"META_PACKAGE"}) + && defined($pkgdir) + && !-f "${current_dir}/$pkgdir/PLIST" + && !-f "${current_dir}/$pkgdir/PLIST.common") { + log_warning($fname, NO_LINE_NUMBER, "Neither PLIST nor PLIST.common exist, and PLIST_SRC is unset. Are you sure PLIST handling is ok?"); + } + + if ((exists($pkgctx_vardef->{"NO_CHECKSUM"}) || $pkgctx_vardef->{"META_PACKAGE"}) && is_emptydir("${current_dir}/${patchdir}")) { + if (-f "${current_dir}/${distinfo_file}") { + log_warning("${current_dir}/${distinfo_file}", NO_LINE_NUMBER, "This file should not exist if NO_CHECKSUM or META_PACKAGE is set."); + } + } else { + if (!-f "${current_dir}/${distinfo_file}") { + log_warning("${current_dir}/${distinfo_file}", NO_LINE_NUMBER, "File not found. Please run '".conf_make." makesum'."); + } + } + + if (exists($pkgctx_vardef->{"REPLACE_PERL"}) && exists($pkgctx_vardef->{"NO_CONFIGURE"})) { + $pkgctx_vardef->{"REPLACE_PERL"}->log_warning("REPLACE_PERL is ignored when ..."); + $pkgctx_vardef->{"NO_CONFIGURE"}->log_warning("... NO_CONFIGURE is set."); + } + + if (!exists($pkgctx_vardef->{"LICENSE"})) { + log_error($fname, NO_LINE_NUMBER, "Each package must define its LICENSE."); + } + + if (exists($pkgctx_vardef->{"GNU_CONFIGURE"}) && exists($pkgctx_vardef->{"USE_LANGUAGES"})) { + my $languages_line = $pkgctx_vardef->{"USE_LANGUAGES"}; + my $value = $languages_line->get("value"); + + if ($languages_line->has("comment") && $languages_line->get("comment") =~ m"\b(?:c|empty|none)\b"i) { + # Don't emit a warning, since the comment + # probably contains a statement that C is + # really not needed. + + } elsif ($value !~ m"(?:^|\s+)(?:c|c99|objc)(?:\s+|$)") { + $pkgctx_vardef->{"GNU_CONFIGURE"}->log_warning("GNU_CONFIGURE almost always needs a C compiler, ..."); + $languages_line->log_warning("... but \"c\" is not added to USE_LANGUAGES."); + } + } + + my $distname_line = $pkgctx_vardef->{"DISTNAME"}; + my $pkgname_line = $pkgctx_vardef->{"PKGNAME"}; + + my $distname = defined($distname_line) ? $distname_line->get("value") : undef; + my $pkgname = defined($pkgname_line) ? $pkgname_line->get("value") : undef; + my $nbpart = get_nbpart(); + + # Let's do some tricks to get the proper value of the package + # name more often. + if (defined($distname) && defined($pkgname)) { + $pkgname =~ s/\$\{DISTNAME\}/$distname/; + + if ($pkgname =~ m"^(.*)\$\{DISTNAME:S(.)([^:]*)\2([^:]*)\2(g?)\}(.*)$") { + my ($before, $separator, $old, $new, $mod, $after) = ($1, $2, $3, $4, $5, $6); + my $newname = $distname; + $old = quotemeta($old); + $old =~ s/^\\\^/^/; + $old =~ s/\\\$$/\$/; + if ($mod eq "g") { + $newname =~ s/$old/$new/g; + } else { + $newname =~ s/$old/$new/; + } + $opt_debug_misc and $pkgname_line->log_debug("old pkgname=$pkgname"); + $pkgname = $before . $newname . $after; + $opt_debug_misc and $pkgname_line->log_debug("new pkgname=$pkgname"); + } + } + + if (defined($pkgname) && defined($distname) && $pkgname eq $distname) { + $pkgname_line->log_note("PKGNAME is \${DISTNAME} by default. You probably don't need to define PKGNAME."); + } + + if (!defined($pkgname) && defined($distname) && $distname !~ regex_unresolved && $distname !~ regex_pkgname) { + $distname_line->log_warning("As DISTNAME is not a valid package name, please define the PKGNAME explicitly."); + } + + ($effective_pkgname, $effective_pkgname_line, $effective_pkgbase, $effective_pkgversion) + = (defined($pkgname) && $pkgname !~ regex_unresolved && $pkgname =~ regex_pkgname) ? ($pkgname.$nbpart, $pkgname_line, $1, $2) + : (defined($distname) && $distname !~ regex_unresolved && $distname =~ regex_pkgname) ? ($distname.$nbpart, $distname_line, $1, $2) + : (undef, undef, undef, undef); + if (defined($effective_pkgname_line)) { + $opt_debug_misc and $effective_pkgname_line->log_debug("Effective name=${effective_pkgname} base=${effective_pkgbase} version=${effective_pkgversion}."); + # XXX: too many false positives + if (false && $pkgpath =~ m"/([^/]+)$" && $effective_pkgbase ne $1) { + $effective_pkgname_line->log_warning("Mismatch between PKGNAME ($effective_pkgname) and package directory ($1)."); + } + } + + checkpackage_possible_downgrade(); + + if (!exists($pkgctx_vardef->{"COMMENT"})) { + log_warning($fname, NO_LINE_NUMBER, "No COMMENT given."); + } + + if (exists($pkgctx_vardef->{"USE_IMAKE"}) && exists($pkgctx_vardef->{"USE_X11"})) { + $pkgctx_vardef->{"USE_IMAKE"}->log_note("USE_IMAKE makes ..."); + $pkgctx_vardef->{"USE_X11"}->log_note("... USE_X11 superfluous."); + } + + if (defined($effective_pkgbase)) { + + foreach my $suggested_update (@{get_suggested_package_updates()}) { + my ($line, $suggbase, $suggver, $suggcomm) = @{$suggested_update}; + my $comment = (defined($suggcomm) ? " (${suggcomm})" : ""); + + next unless $effective_pkgbase eq $suggbase; + + if (dewey_cmp($effective_pkgversion, "<", $suggver)) { + $effective_pkgname_line->log_warning("This package should be updated to ${suggver}${comment}."); + $effective_pkgname_line->explain_warning( +"The wishlist for package updates in doc/TODO mentions that a newer", +"version of this package is available."); + } + if (dewey_cmp($effective_pkgversion, "==", $suggver)) { + $effective_pkgname_line->log_note("The update request to ${suggver} from doc/TODO${comment} has been done."); + } + if (dewey_cmp($effective_pkgversion, ">", $suggver)) { + $effective_pkgname_line->log_note("This package is newer than the update request to ${suggver}${comment}."); + } + } + } + + checklines_mk($lines); + checklines_package_Makefile_varorder($lines); + autofix($lines); +} + +#include PkgLint/Patches.pm + +sub checkfile_PLIST($) { + my ($fname) = @_; + my ($lines, $last_file_seen); + + $opt_debug_trace and log_debug($fname, NO_LINES, "checkfile_PLIST()"); + + checkperms($fname); + if (!($lines = load_file($fname))) { + log_error($fname, NO_LINE_NUMBER, "Cannot be read."); + return; + } + if (@{$lines} == 0) { + log_error($fname, NO_LINE_NUMBER, "Must not be empty."); + return; + } + checkline_rcsid($lines->[0], "\@comment "); + + if (@$lines == 1) { + $lines->[0]->log_warning("PLIST files shouldn't be empty."); + $lines->[0]->explain_warning( +"One reason for empty PLISTs is that this is a newly created package", +"and that the author didn't run \"bmake print-PLIST\" after installing", +"the files.", +"", +"Another reason, common for Perl packages, is that the final PLIST is", +"automatically generated. Since the source PLIST is not used at all,", +"you can remove it.", +"", +"Meta packages also don't need a PLIST file."); + } + + # Get the list of all files from the PLIST. + my $all_files = {}; + my $all_dirs = {}; + my $extra_lines = []; + if (basename($fname) eq "PLIST.common_end") { + my $common_lines = load_file(dirname($fname) . "/PLIST.common"); + if ($common_lines) { + $extra_lines = $common_lines; + } + } + + foreach my $line (@{$extra_lines}, @{$lines}) { + my $text = $line->text; + + if (index($text, '${') != -1 && $text =~ m"\$\{([\w_]+)\}(.*)") { + if (defined($pkgctx_plist_subst_cond) && exists($pkgctx_plist_subst_cond->{$1})) { + $opt_debug_misc and $line->log_debug("Removed PLIST_SUBST conditional $1."); + $text = $2; + } + } + + if ($text =~ m"^[\w\$]") { + $all_files->{$text} = $line; + my $dir = $text; + while ($dir =~ s,/[^/]+$,,) { + $all_dirs->{$dir} = $line; + } + } + if (substr($text, 0, 1) eq '@' && $text =~ m"^\@exec \$\{MKDIR\} %D/(.*)$") { + my $dir = $1; + do { + $all_dirs->{$dir} = $line; + } while ($dir =~ s,/[^/]+$,,); + } + } + + foreach my $line (@{$lines}) { + my $text = $line->text; + + if ($text =~ /\s$/) { + $line->log_error("pkgsrc does not support filenames ending in white-space."); + $line->explain_error( +"Each character in the PLIST is relevant, even trailing white-space."); + } + + # @foo directives. + if (index($text, '@') != -1 && $text =~ /^(?:\$\{[\w_]+\})?\@([a-z-]+)\s+(.*)/) { + my ($cmd, $arg) = ($1, $2); + + if ($cmd eq "unexec" && $arg =~ m"^(rmdir|\$\{RMDIR\} \%D/)(.*)") { + my ($rmdir, $rest) = ($1, $2); + if ($rest !~ m"(?:true|\$\{TRUE\})") { + $line->log_warning("Please remove this line. It is no longer necessary."); + } + + } elsif (($cmd eq "exec" || $cmd eq "unexec")) { + if ($arg =~ /(?:install-info|\$\{INSTALL_INFO\})/) { + $line->log_warning("\@exec/unexec install-info is deprecated."); + + } elsif ($arg =~ /ldconfig/ && $arg !~ m"/usr/bin/true") { + $line->log_error("ldconfig must be used with \"||/usr/bin/true\"."); + } + + } elsif ($cmd eq "comment") { + # nothing to do + + } elsif ($cmd eq "dirrm") { + $line->log_warning("\@dirrm is obsolete. Please remove this line."); + $line->explain_warning( +"Directories are removed automatically when they are empty.", +"When a package needs an empty directory, it can use the \@pkgdir", +"command in the PLIST"); + } elsif ($cmd eq "imake-man") { + my (@args) = split(/\s+/, $arg); + if (@args != 3) { + $line->log_warning("Invalid number of arguments for imake-man."); + } else { + if ($args[2] eq "\${IMAKE_MANNEWSUFFIX}") { + warn_about_PLIST_imake_mannewsuffix($line); + } + } + + } elsif ($cmd eq "pkgdir") { + # TODO: What can we check here? + + } else { + $line->log_warning("Unknown PLIST directive \"\@$cmd\"."); + } + + # Pathnames. + } elsif ($text =~ m"^([A-Za-z0-9\$].*)/([^/]+)$") { + my ($dirname, $basename) = ($1, $2); + + if ($opt_warn_plist_sort && $text =~ m"^\w" && $text !~ regex_unresolved) { + if (defined($last_file_seen)) { + if ($last_file_seen gt $text) { + $line->log_warning("${text} should be sorted before ${last_file_seen}."); + $line->explain_warning( +"For aesthetic reasons, the files in the PLIST should be sorted", +"alphabetically."); + } elsif ($last_file_seen eq $text) { + $line->log_warning("Duplicate filename."); + } + } + $last_file_seen = $text; + } + + if (index($basename, '${IMAKE_MANNEWSUFFIX}') != -1) { + warn_about_PLIST_imake_mannewsuffix($line); + } + + if (substr($dirname, 0, 4) eq "bin/") { + $line->log_warning("The bin/ directory should not have subdirectories."); + + } elsif ($dirname eq "bin") { + + if (exists($all_files->{"man/man1/${basename}.1"})) { + # Fine. + } elsif (exists($all_files->{"man/man6/${basename}.6"})) { + # Fine. + } elsif (exists($all_files->{"\${IMAKE_MAN_DIR}/${basename}.\${IMAKE_MANNEWSUFFIX}"})) { + # Fine. + } else { + $opt_warn_extra and $line->log_warning("Manual page missing for bin/${basename}."); + $opt_warn_extra and $line->explain_warning( +"All programs that can be run directly by the user should have a manual", +"page for quick reference. The programs in the bin/ directory should have", +"corresponding manual pages in section 1 (filename program.1), not in", +"section 8."); + } + + } elsif (substr($text, 0, 4) eq "doc/") { + $line->log_error("Documentation must be installed under share/doc, not doc."); + + } elsif (substr($text, 0, 9) eq "etc/rc.d/") { + $line->log_error("RCD_SCRIPTS must not be registered in the PLIST. Please use the RCD_SCRIPTS framework."); + + } elsif (substr($text, 0, 4) eq "etc/") { + my $f = "mk/pkginstall/bsd.pkginstall.mk"; + + assert(-f "${cwd_pkgsrcdir}/${f}", "${cwd_pkgsrcdir}/${f} is not a regular file."); + $line->log_error("Configuration files must not be registered in the PLIST. Please use the CONF_FILES framework, which is described in ${f}."); + + } elsif (substr($text, 0, 8) eq "include/" && $text =~ m"^include/.*\.(?:h|hpp)$") { + # Fine. + + } elsif ($text eq "info/dir") { + $line->log_error("\"info/dir\" must not be listed. Use install-info to add/remove an entry."); + + } elsif (substr($text, 0, 5) eq "info/" && length($text) > 5) { + if (defined($pkgctx_vardef) && !exists($pkgctx_vardef->{"INFO_FILES"})) { + $line->log_warning("Packages that install info files should set INFO_FILES."); + } + + } elsif (defined($effective_pkgbase) && $text =~ m"^lib/\Q${effective_pkgbase}\E/") { + # Fine. + + } elsif (substr($text, 0, 11) eq "lib/locale/") { + $line->log_error("\"lib/locale\" must not be listed. Use \${PKGLOCALEDIR}/locale and set USE_PKGLOCALEDIR instead."); + + } elsif (substr($text, 0, 4) eq "lib/" && $text =~ m"^(lib/(?:.*/)*)([^/]+)\.(so|a|la)$") { + my ($dir, $lib, $ext) = ($1, $2, $3); + + if ($dir eq "lib/" && $lib !~ m"^lib") { + $opt_warn_extra and $line->log_warning("Library filename does not start with \"lib\"."); + } + if ($ext eq "la") { + if (defined($pkgctx_vardef) && !exists($pkgctx_vardef->{"USE_LIBTOOL"})) { + $line->log_warning("Packages that install libtool libraries should define USE_LIBTOOL."); + } + } + + } elsif (substr($text, 0, 4) eq "man/" && $text =~ m"^man/(cat|man)(\w+)/(.*?)\.(\w+)(\.gz)?$") { + my ($cat_or_man, $section, $manpage, $ext, $gz) = ($1, $2, $3, $4, $5); + + if ($section !~ m"^[\dln]$") { + $line->log_warning("Unknown section \"${section}\" for manual page."); + } + + if ($cat_or_man eq "cat" && !exists($all_files->{"man/man${section}/${manpage}.${section}"})) { + $line->log_warning("Preformatted manual page without unformatted one."); + } + + if ($cat_or_man eq "cat") { + if ($ext ne "0") { + $line->log_warning("Preformatted manual pages should end in \".0\"."); + } + } else { + if ($section ne $ext) { + $line->log_warning("Mismatch between the section (${section}) and extension (${ext}) of the manual page."); + } + } + + if (defined($gz)) { + $line->log_note("The .gz extension is unnecessary for manual pages."); + $line->explain_note( +"Whether the manual pages are installed in compressed form or not is", +"configured by the pkgsrc user. Compression and decompression takes place", +"automatically, no matter if the .gz extension is mentioned in the PLIST", +"or not."); + } + + } elsif (substr($text, 0, 7) eq "man/cat") { + $line->log_warning("Invalid filename \"${text}\" for preformatted manual page."); + + } elsif (substr($text, 0, 7) eq "man/man") { + $line->log_warning("Invalid filename \"${text}\" for unformatted manual page."); + + } elsif (substr($text, 0, 5) eq "sbin/") { + my $binname = substr($text, 5); + + if (!exists($all_files->{"man/man8/${binname}.8"})) { + $opt_warn_extra and $line->log_warning("Manual page missing for sbin/${binname}."); + $opt_warn_extra and $line->explain_warning( +"All programs that can be run directly by the user should have a manual", +"page for quick reference. The programs in the sbin/ directory should have", +"corresponding manual pages in section 8 (filename program.8), not in", +"section 1."); + } + + } elsif (substr($text, 0, 6) eq "share/" && $text =~ m"^share/applications/.*\.desktop$") { + my $f = "../../sysutils/desktop-file-utils/desktopdb.mk"; + if (defined($pkgctx_included) && !exists($pkgctx_included->{$f})) { + $line->log_warning("Packages that install a .desktop entry may .include \"$f\"."); + $line->explain_warning( +"If *.desktop files contain MimeType keys, global MIME Type registory DB", +"must be updated by desktop-file-utils.", +"Otherwise, this warning is harmless."); + } + + } elsif (substr($text, 0, 6) eq "share/" && $pkgpath ne "graphics/hicolor-icon-theme" && $text =~ m"^share/icons/hicolor(?:$|/)") { + my $f = "../../graphics/hicolor-icon-theme/buildlink3.mk"; + if (defined($pkgctx_included) && !exists($pkgctx_included->{$f})) { + $line->log_error("Please .include \"$f\" in the Makefile"); + $line->explain_error( +"If hicolor icon themes are installed, icon theme cache must be", +"maintained. The hicolor-icon-theme package takes care of that."); + } + + } elsif (substr($text, 0, 6) eq "share/" && $pkgpath ne "graphics/gnome-icon-theme" && $text =~ m"^share/icons/gnome(?:$|/)") { + my $f = "../../graphics/gnome-icon-theme/buildlink3.mk"; + if (defined($pkgctx_included) && !exists($pkgctx_included->{$f})) { + $line->log_error("Please .include \"$f\""); + $line->explain_error( +"If Gnome icon themes are installed, icon theme cache must be maintained."); + } + } elsif ($dirname eq "share/aclocal" && $basename =~ m"\.m4$") { + # Fine. + + } elsif (substr($text, 0, 15) eq "share/doc/html/") { + $opt_warn_plist_depr and $line->log_warning("Use of \"share/doc/html\" is deprecated. Use \"share/doc/\${PKGBASE}\" instead."); + + } elsif (defined($effective_pkgbase) && $text =~ m"^share/(?:doc/|examples/|)\Q${effective_pkgbase}\E/") { + # Fine. + + } elsif ($pkgpath ne "graphics/hicolor-icon-theme" && $text =~ m"^share/icons/hicolor/icon-theme\.cache") { + $line->log_error("Please .include \"../../graphics/hicolor-icon-theme/buildlink3.mk\" and remove this line."); + + } elsif (substr($text, 0, 11) eq "share/info/") { + $line->log_warning("Info pages should be installed into info/, not share/info/."); + $line->explain_warning( +"To fix this, you should add INFO_FILES=yes to the package Makefile."); + + } elsif (substr($text, -3) eq ".mo" && $text =~ m"^share/locale/[\w\@_]+/LC_MESSAGES/[^/]+\.mo$") { + # Fine. + + } elsif (substr($text, 0, 10) eq "share/man/") { + $line->log_warning("Man pages should be installed into man/, not share/man/."); + + } else { + $opt_debug_unchecked and $line->log_debug("Unchecked pathname \"${text}\"."); + } + + if ($text =~ /\$\{PKGLOCALEDIR}/ && defined($pkgctx_vardef) && !exists($pkgctx_vardef->{"USE_PKGLOCALEDIR"})) { + $line->log_warning("PLIST contains \${PKGLOCALEDIR}, but USE_PKGLOCALEDIR was not found."); + } + + if (index($text, "/CVS/") != -1) { + $line->log_warning("CVS files should not be in the PLIST."); + } + if (substr($text, -5) eq ".orig") { + $line->log_warning(".orig files should not be in the PLIST."); + } + if (substr($text, -14) eq "/perllocal.pod") { + $line->log_warning("perllocal.pod files should not be in the PLIST."); + $line->explain_warning( +"This file is handled automatically by the INSTALL/DEINSTALL scripts,", +"since its contents changes frequently."); + } + + if ($text =~ m"^(.*)(\.a|\.so[0-9.]*)$") { + my ($basename, $ext) = ($1, $2); + + if (exists($all_files->{"${basename}.la"})) { + $line->log_warning("Redundant library found. The libtool library is in line " . $all_files->{"${basename}.la"}->lines . "."); + } + } + + } elsif ($text =~ m"^\$\{[\w_]+\}$") { + # A variable on its own line. + + } else { + $line->log_error("Unknown line type."); + } + } + checklines_trailing_empty_lines($lines); + autofix($lines); +} + +sub checkfile($) { + my ($fname) = @_; + my ($st, $basename); + + $opt_debug_trace and log_debug($fname, NO_LINES, "checkfile()"); + + $basename = basename($fname); + if ($basename =~ m"^(?:work.*|.*~|.*\.orig|.*\.rej)$") { + if ($opt_import) { + log_error($fname, NO_LINE_NUMBER, "Must be cleaned up before committing the package."); + } + return; + } + + if (!($st = lstat($fname))) { + log_error($fname, NO_LINE_NUMBER, "$!"); + return; + } + if (S_ISDIR($st->mode)) { + if ($basename eq "files" || $basename eq "patches" || $basename eq "CVS") { + # Ok + } elsif ($fname =~ m"(?:^|/)files/[^/]*$") { + # Ok + + } elsif (!is_emptydir($fname)) { + log_warning($fname, NO_LINE_NUMBER, "Unknown directory name."); + } + + } elsif (S_ISLNK($st->mode)) { + if ($basename !~ m"^work") { + log_warning($fname, NO_LINE_NUMBER, "Unknown symlink name."); + } + + } elsif (!S_ISREG($st->mode)) { + log_error($fname, NO_LINE_NUMBER, "Only files and directories are allowed in pkgsrc."); + + } elsif ($basename eq "ALTERNATIVES") { + $opt_check_ALTERNATIVES and checkfile_ALTERNATIVES($fname); + + } elsif ($basename eq "buildlink3.mk") { + $opt_check_bl3 and checkfile_buildlink3_mk($fname); + + } elsif ($basename =~ m"^DESCR") { + $opt_check_DESCR and checkfile_DESCR($fname); + + } elsif ($basename =~ m"^distinfo") { + $opt_check_distinfo and checkfile_distinfo($fname); + + } elsif ($basename eq "DEINSTALL" || $basename eq "INSTALL") { + $opt_check_INSTALL and checkfile_INSTALL($fname); + + } elsif ($basename =~ m"^MESSAGE") { + $opt_check_MESSAGE and checkfile_MESSAGE($fname); + + } elsif ($basename =~ m"^patch-[-A-Za-z0-9_.~+]*[A-Za-z0-9_]$") { + $opt_check_patches and checkfile_patch($fname); + + } elsif ($fname =~ m"(?:^|/)patches/manual[^/]*$") { + $opt_debug_unchecked and log_debug($fname, NO_LINE_NUMBER, "Unchecked file \"${fname}\"."); + + } elsif ($fname =~ m"(?:^|/)patches/[^/]*$") { + log_warning($fname, NO_LINE_NUMBER, "Patch files should be named \"patch-\", followed by letters, '-', '_', '.', and digits only."); + + } elsif ($basename =~ m"^(?:.*\.mk|Makefile.*)$" and not $fname =~ m,files/, and not $fname =~ m,patches/,) { + $opt_check_mk and checkfile_mk($fname); + + } elsif ($basename =~ m"^PLIST") { + $opt_check_PLIST and checkfile_PLIST($fname); + + } elsif ($basename eq "TODO" || $basename eq "README") { + # Ok + + } elsif ($basename =~ m"^CHANGES-.*") { + load_doc_CHANGES($fname); + + } elsif (!-T $fname) { + log_warning($fname, NO_LINE_NUMBER, "Unexpectedly found a binary file."); + + } elsif ($fname =~ m"(?:^|/)files/[^/]*$") { + # Ok + } else { + log_warning($fname, NO_LINE_NUMBER, "Unexpected file found."); + $opt_check_extra and checkfile_extra($fname); + } +} + +sub my_split($$) { + my ($delimiter, $s) = @_; + my ($pos, $next, @result); + + $pos = 0; + for ($pos = 0; $pos != -1; $pos = $next) { + $next = index($s, $delimiter, $pos); + push @result, (($next == -1) ? substr($s, $pos) : substr($s, $pos, $next - $pos)); + if ($next != -1) { + $next += length($delimiter); + } + } + return @result; +} + +# Checks that the files in the directory are in sync with CVS's status. +# +sub checkdir_CVS($) { + my ($fname) = @_; + + my $cvs_entries = load_file("$fname/CVS/Entries"); + my $cvs_entries_log = load_file("$fname/CVS/Entries.Log"); + return unless $cvs_entries; + + foreach my $line (@$cvs_entries) { + my ($type, $fname, $mtime, $date, $keyword_mode, $tag, $undef) = my_split("/", $line->text); + next if ($type eq "D" && !defined($fname)); + assert(false, "Unknown line format: " . $line->text) + unless $type eq "" || $type eq "D"; + assert(false, "Unknown line format: " . $line->text) + unless defined($tag); + assert(false, "Unknown line format: " . $line->text) + unless defined($keyword_mode); + assert(false, "Unknown line format: " . $line->text) + if defined($undef); + } +} + +# +# Procedures to check a directory including the files in it. +# + +sub checkdir_root() { + my ($fname) = "${current_dir}/Makefile"; + my ($lines, $prev_subdir, @subdirs); + + $opt_debug_trace and log_debug($fname, NO_LINES, "checkdir_root()"); + + if (!($lines = load_lines($fname, true))) { + log_error($fname, NO_LINE_NUMBER, "Cannot be read."); + return; + } + + parselines_mk($lines); + if (0 <= $#{$lines}) { + checkline_rcsid_regex($lines->[0], qr"#\s+", "# "); + } + + foreach my $line (@{$lines}) { + if ($line->text =~ m"^(#?)SUBDIR\s*\+=(\s*)(\S+)\s*(?:#\s*(.*?)\s*|)$") { + my ($comment_flag, $indentation, $subdir, $comment) = ($1, $2, $3, $4); + + if ($comment_flag eq "#" && (!defined($comment) || $comment eq "")) { + $line->log_warning("${subdir} commented out without giving a reason."); + } + + if ($indentation ne "\t") { + $line->log_warning("Indentation should be a single tab character."); + } + + if ($subdir =~ m"\$" || !-f "${current_dir}/${subdir}/Makefile") { + next; + } + + if (!defined($prev_subdir) || $subdir gt $prev_subdir) { + # correctly ordered + } elsif ($subdir eq $prev_subdir) { + $line->log_error("${subdir} must only appear once."); + } elsif ($prev_subdir eq "x11" && $subdir eq "archivers") { + # ignore that one, since it is documented in the top-level Makefile + } else { + $line->log_warning("${subdir} should come before ${prev_subdir}."); + } + + $prev_subdir = $subdir; + + if ($comment_flag eq "") { + push(@subdirs, "${current_dir}/${subdir}"); + } + } + } + + checklines_mk($lines); + + if ($opt_recursive) { + $ipc_checking_root_recursively = true; + push(@todo_items, @subdirs); + } +} + +sub checkdir_category() { + my $fname = "${current_dir}/Makefile"; + my ($lines, $lineno); + + $opt_debug_trace and log_debug($fname, NO_LINES, "checkdir_category()"); + + if (!($lines = load_lines($fname, true))) { + log_error($fname, NO_LINE_NUMBER, "Cannot be read."); + return; + } + parselines_mk($lines); + + $lineno = 0; + + # The first line must contain the RCS Id + if ($lineno <= $#{$lines} && checkline_rcsid_regex($lines->[$lineno], qr"#\s+", "# ")) { + $lineno++; + } + + # Then, arbitrary comments may follow + while ($lineno <= $#{$lines} && $lines->[$lineno]->text =~ m"^#") { + $lineno++; + } + + # Then we need an empty line + expect_empty_line($lines, \$lineno); + + # Then comes the COMMENT line + if ($lineno <= $#{$lines} && $lines->[$lineno]->text =~ m"^COMMENT=\t*(.*)") { + my ($comment) = ($1); + + checkline_valid_characters_in_variable($lines->[$lineno], qr"[-\040'(),/0-9A-Za-z]"); + $lineno++; + } else { + $lines->[$lineno]->log_error("COMMENT= line expected."); + } + + # Then we need an empty line + expect_empty_line($lines, \$lineno); + + # And now to the most complicated part of the category Makefiles, + # the (hopefully) sorted list of SUBDIRs. The first step is to + # collect the SUBDIRs in the Makefile and in the file system. + + my (@f_subdirs, @m_subdirs); + + @f_subdirs = sort(get_subdirs($current_dir)); + + my $prev_subdir = undef; + while ($lineno <= $#{$lines}) { + my $line = $lines->[$lineno]; + + if ($line->text =~ m"^(#?)SUBDIR\+=(\s*)(\S+)\s*(?:#\s*(.*?)\s*|)$") { + my ($comment_flag, $indentation, $subdir, $comment) = ($1, $2, $3, $4); + + if ($comment_flag eq "#" && (!defined($comment) || $comment eq "")) { + $line->log_warning("${subdir} commented out without giving a reason."); + } + + if ($indentation ne "\t") { + $line->log_warning("Indentation should be a single tab character."); + } + + if (defined($prev_subdir) && $subdir eq $prev_subdir) { + $line->log_error("${subdir} must only appear once."); + } elsif (defined($prev_subdir) && $subdir lt $prev_subdir) { + $line->log_warning("${subdir} should come before ${prev_subdir}."); + } else { + # correctly ordered + } + + push(@m_subdirs, [$subdir, $line, $comment_flag ? false : true]); + $prev_subdir = $subdir; + $lineno++; + + } else { + if ($line->text ne "") { + $line->log_error("SUBDIR+= line or empty line expected."); + } + last; + } + } + + # To prevent unnecessary warnings about subdirectories that are + # in one list, but not in the other, we generate the sets of + # subdirs of each list. + my (%f_check, %m_check); + foreach my $f (@f_subdirs) { $f_check{$f} = true; } + foreach my $m (@m_subdirs) { $m_check{$m->[0]} = true; } + + my ($f_index, $f_atend, $f_neednext, $f_current) = (0, false, true, undef, undef); + my ($m_index, $m_atend, $m_neednext, $m_current) = (0, false, true, undef, undef); + my ($line, $m_recurse); + my (@subdirs); + + while (!($m_atend && $f_atend)) { + + if (!$m_atend && $m_neednext) { + $m_neednext = false; + if ($m_index > $#m_subdirs) { + $m_atend = true; + $line = $lines->[$lineno]; + next; + } else { + $m_current = $m_subdirs[$m_index]->[0]; + $line = $m_subdirs[$m_index]->[1]; + $m_recurse = $m_subdirs[$m_index]->[2]; + $m_index++; + } + } + + if (!$f_atend && $f_neednext) { + $f_neednext = false; + if ($f_index > $#f_subdirs) { + $f_atend = true; + next; + } else { + $f_current = $f_subdirs[$f_index++]; + } + } + + if (!$f_atend && ($m_atend || $f_current lt $m_current)) { + if (!exists($m_check{$f_current})) { + $line->log_error("${f_current} exists in the file system, but not in the Makefile."); + $line->append_before("SUBDIR+=\t${f_current}"); + } + $f_neednext = true; + + } elsif (!$m_atend && ($f_atend || $m_current lt $f_current)) { + if (!exists($f_check{$m_current})) { + $line->log_error("${m_current} exists in the Makefile, but not in the file system."); + $line->delete(); + } + $m_neednext = true; + + } else { # $f_current eq $m_current + $f_neednext = true; + $m_neednext = true; + if ($m_recurse) { + push(@subdirs, "${current_dir}/${m_current}"); + } + } + } + + # the wip category Makefile may have its own targets for generating + # indexes and READMEs. Just skip them. + if ($is_wip) { + while ($lineno <= $#{$lines} - 2) { + $lineno++; + } + } + + expect_empty_line($lines, \$lineno); + + # And, last but not least, the .include line + my $final_line = ".include \"../mk/bsd.pkg.subdir.mk\""; + expect($lines, \$lineno, qr"\Q$final_line\E") + || expect_text($lines, \$lineno, ".include \"../mk/misc/category.mk\""); + + if ($lineno <= $#{$lines}) { + $lines->[$lineno]->log_error("The file should end here."); + } + + checklines_mk($lines); + + autofix($lines); + + if ($opt_recursive) { + unshift(@todo_items, @subdirs); + } +} + +sub checkdir_package() { + my ($lines, $have_distinfo, $have_patches); + + # Initialize global variables + $pkgdir = undef; + $filesdir = "files"; + $patchdir = "patches"; + $distinfo_file = "distinfo"; + $effective_pkgname = undef; + $effective_pkgbase = undef; + $effective_pkgversion = undef; + $effective_pkgname_line = undef; + $seen_bsd_prefs_mk = false; + $pkgctx_vardef = {%{get_userdefined_variables()}}; + $pkgctx_varuse = {}; + $pkgctx_bl3 = {}; + $pkgctx_plist_subst_cond = {}; + $pkgctx_included = {}; + $seen_Makefile_common = false; + + # we need to handle the Makefile first to get some variables + if (!load_package_Makefile("${current_dir}/Makefile", \$lines)) { + log_error("${current_dir}/Makefile", NO_LINE_NUMBER, "Cannot be read."); + goto cleanup; + } + + my @files = glob("${current_dir}/*"); + if ($pkgdir ne ".") { + push(@files, glob("${current_dir}/${pkgdir}/*")); + } + if ($opt_check_extra) { + push(@files, glob("${current_dir}/${filesdir}/*")); + } + push(@files, glob("${current_dir}/${patchdir}/*")); + if ($distinfo_file !~ m"^(?:\./)?distinfo$") { + push(@files, "${current_dir}/${distinfo_file}"); + } + $have_distinfo = false; + $have_patches = false; + + # Determine the used variables before checking any of the + # Makefile fragments. + foreach my $fname (@files) { + if (($fname =~ m"^((?:.*/)?Makefile\..*|.*\.mk)$") + && (not $fname =~ m"patch-") + && (not $fname =~ m"${pkgdir}/") + && (not $fname =~ m"${filesdir}/") + && (defined(my $lines = load_lines($fname, true)))) { + parselines_mk($lines); + determine_used_variables($lines); + } + } + + foreach my $fname (@files) { + if ($fname eq "${current_dir}/Makefile") { + $opt_check_Makefile and checkfile_package_Makefile($fname, $lines); + } else { + checkfile($fname); + } + if ($fname =~ m"/patches/patch-*$") { + $have_patches = true; + } elsif ($fname =~ m"/distinfo$") { + $have_distinfo = true; + } + } + + if ($opt_check_distinfo && $opt_check_patches) { + if ($have_patches && ! $have_distinfo) { + log_warning("${current_dir}/$distinfo_file", NO_LINE_NUMBER, "File not found. Please run '".conf_make." makepatchsum'."); + } + } + + if (!is_emptydir("${current_dir}/scripts")) { + log_warning("${current_dir}/scripts", NO_LINE_NUMBER, "This directory and its contents are deprecated! Please call the script(s) explicitly from the corresponding target(s) in the pkg's Makefile."); + } + +cleanup: + # Clean up global variables. + $pkgdir = undef; + $filesdir = undef; + $patchdir = undef; + $distinfo_file = undef; + $effective_pkgname = undef; + $effective_pkgbase = undef; + $effective_pkgversion = undef; + $effective_pkgname_line = undef; + $seen_bsd_prefs_mk = undef; + $pkgctx_vardef = undef; + $pkgctx_varuse = undef; + $pkgctx_bl3 = undef; + $pkgctx_plist_subst_cond = undef; + $pkgctx_included = undef; + $seen_Makefile_common = undef; +} + +# +# Selecting the proper checking procedures for a directory entry. +# + +sub checkitem($) { + my ($item) = @_; + my ($st, $is_dir, $is_reg); + + if (!($st = lstat($item))) { + log_error($item, NO_LINE_NUMBER, "Does not exist."); + return; + } + + $is_dir = S_ISDIR($st->mode); + $is_reg = S_ISREG($st->mode); + if (!$is_reg && !$is_dir) { + log_error($item, NO_LINE_NUMBER, "Must be a file or directory."); + return; + } + + $current_dir = $is_dir ? $item : dirname($item); + my $abs_current_dir = Cwd::abs_path($current_dir); + $is_wip = !$opt_import && ($abs_current_dir =~ m"/wip(?:/|$)"); + $is_internal = ($abs_current_dir =~ m"/mk(?:/|$)"); + + # Determine the root directory of pkgsrc. By only overwriting + # the global variable $cwd_pkgsrcdir when we are checking inside + # a pkgsrc tree, the user can specify a tree with the + # --pkgsrcdir option and then check files (but not directories) + # outside of any pkgsrc tree. + $cur_pkgsrcdir = undef; + $pkgpath = undef; + foreach my $d (".", "..", "../..", "../../..") { + if (-f "${current_dir}/${d}/mk/bsd.pkg.mk") { + $cur_pkgsrcdir = $d; + $pkgpath = relative_path("${current_dir}/${d}", $current_dir); + } + } + if (!defined($cwd_pkgsrcdir) && defined($cur_pkgsrcdir)) { + $cwd_pkgsrcdir = "${current_dir}/${cur_pkgsrcdir}"; + } + + if (!defined($cwd_pkgsrcdir)) { + log_error($item, NO_LINE_NUMBER, "Cannot determine the pkgsrc root directory."); + return; + } + + check_pkglint_version(); # (needs $cwd_pkgsrcdir) + + return if $is_dir && is_emptydir($item); + + if ($is_dir) { + checkdir_CVS($item); + } + + if ($is_reg) { + checkfile($item); + + } elsif (!defined($cur_pkgsrcdir)) { + log_error($item, NO_LINES, "Cannot check directories outside a pkgsrc tree."); + + } elsif ($cur_pkgsrcdir eq "../..") { + checkdir_package(); + + } elsif ($cur_pkgsrcdir eq "..") { + checkdir_category(); + + } elsif ($cur_pkgsrcdir eq ".") { + checkdir_root(); + + } else { + log_error($item, NO_LINE_NUMBER, "Don't know how to check this directory."); + } +} + +# +# The main program +# + +sub main() { + + local $| = true; + parse_command_line(); + + @todo_items = (@ARGV != 0) ? @ARGV : ("."); + while (@todo_items != 0) { + checkitem(shift(@todo_items)); + } + + if ($ipc_checking_root_recursively) { + check_unused_licenses(); + } + + PkgLint::Logging::print_summary_and_exit($opt_quiet); +} + +main() unless caller(); diff --git a/pkgtools/pkglint4/files/pkglint.t b/pkgtools/pkglint4/files/pkglint.t new file mode 100644 index 00000000000..13c56e61323 --- /dev/null +++ b/pkgtools/pkglint4/files/pkglint.t @@ -0,0 +1,198 @@ +#! @PERL@ +# $NetBSD: pkglint.t,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# + +require 'pkglint.pl'; # so we can test its internals +$pkglint::program = 'pkglint.pl'; # because it self-greps for vartypes + +use Test::More tests => 36; +use Test::Deep; +use Test::Trap; + +use Config; +use File::Basename; +use IO::File; +use IPC::Open3; +use Symbol qw(gensym); + +use warnings; +use strict; + +sub test_unit { + my ($unit, $params, $exitcode, $stdout_re, $stderr_re) = @_; + $stdout_re ||= '^$'; + $stderr_re ||= '^$'; + + my @results = trap { $unit->(@$params) }; + + if (defined $exitcode) { + is($trap->exit, $exitcode, qq{exits $exitcode}); + } else { + is($trap->exit, undef, q{doesn't exit}); + } + like($trap->stdout, qr/$stdout_re/sm, qq{stdout matches $stdout_re}); + like($trap->stderr, qr/$stderr_re/sm, qq{stderr matches $stderr_re}); + + return @results; +} + +sub test_program { + my ($command, $params, $exitcode, $stdout_re, $stderr_re) = @_; + $stdout_re ||= '^$'; + $stderr_re ||= '^$'; + + my $stdout = ''; + my $stderr = ''; + local *CATCHERR = IO::File->new_tmpfile; + my $pid = open3(gensym(), \*CATCHOUT, ">&CATCHERR", $command, @$params); + while (my $l = <CATCHOUT>) { + $stdout .= $l; + } + waitpid($pid, 0); + my $ret = $? >> 8; + seek CATCHERR, 0, 0; + while (my $l = <CATCHERR>) { + $stderr .= $l; + } + + if (defined $exitcode) { + is($ret, $exitcode, qq{exits $exitcode}); + } + like($stdout, qr/$stdout_re/sm, qq{stdout matches $stdout_re}); + like($stderr, qr/$stderr_re/sm, qq{stderr matches $stderr_re}); + + # return @results; +} + +sub test_get_vartypes_basictypes { + my $unit = \&pkglint::get_vartypes_basictypes; + + my @results = test_unit($unit); + my %types = %{$results[0]}; + + my @all_vartypes_basictypes = qw( + AwkCommand BrokenIn + BuildlinkDepmethod BuildlinkDepth BuildlinkPackages + CFlag Category Comment + Dependency DependencyWithPath + DistSuffix EmulPlatform + FetchURL FileMode Filemask Filename + Identifier Integer LdFlag License Mail_Address Message Option + Pathlist Pathmask Pathname + Perl5Packlist + PkgName PkgOptionsVar PkgPath PkgRevision + PlatformTriple PrefixPathname PythonDependency + RelativePkgDir RelativePkgPath + Restricted + SedCommand SedCommands + ShellCommand ShellWord + Stage String Tool URL Unchecked UserGroupName Varname Version + WrapperReorder WrapperTransform + WrkdirSubdirectory WrksrcSubdirectory + Yes YesNo YesNo_Indirectly + ); + + cmp_bag( + [ keys %types ], + \@all_vartypes_basictypes, + q{types contains all expected and no unexpected types}, + ); +} + +sub test_get_vartypes_map { + my $unit = \&pkglint::get_vartypes_map; + + my @results = test_unit($unit); + my %map = %{$results[0]}; + is($map{'BSD_MAKE_ENV'}->basic_type(), 'ShellWord', + q{a couple expected vars are typed right}); + is($map{'USE_BUILTIN.*'}->basic_type(), 'YesNo_Indirectly', + q{a couple expected vars are typed right}); +} + +sub test_checkline_mk_vartype_basic { + # this is what gets self-grepped: all that "elsif ($type eq" + # sub doesn't return anything, just warns or errors if need be + # + # TODO: + # + # test a shallow one and then a deeply nested one + # (type='Restricted', value='incorrect') + # (type='Restricted', value='RESTRICTED') + # (type='SedCommands', a few different values') + # once test coverage is persuasive, refactor to a dispatch table + # once refactored, get rid of the $pkglint::program global +} + +sub test_pkglint_main { + my $unit = \&pkglint::main; + + @ARGV = ('-h'); + test_unit($unit, undef, 0, '^usage: pkglint ', '^$'); + + @ARGV = ('..'); + test_unit($unit, undef, 0, '^looks fine', '^$'); + + @ARGV = ('.'); + test_unit($unit, undef, 1, '^ERROR:.+how to check', '^$'); + + @ARGV = (); + test_unit($unit, undef, 1, '^ERROR:.+how to check', '^$'); + + @ARGV = ('/does/not/exist'); + test_unit($unit, undef, 1, '^ERROR:.+not exist', '^$'); + + @ARGV = ($ENV{HOME}); + test_unit($unit, undef, 1, '^ERROR:.+outside a pkgsrc', '^$'); +} + +sub test_lint_some_reference_packages { + my %reference_packages = ( + 'devel/syncdir' => { + stdout_re => <<EOT, +^ERROR: .*Makefile: Each package must define its LICENSE\. +ERROR: .*patches/patch-aa:[0-9]+: Comment expected\. +2 errors and 0 warnings found\..*\$ +EOT + stderr_re => undef, + exitcode => 1, + }, + 'mail/qmail' => { + stdout_re => <<EOT, +^WARN: .*Makefile:[0-9]+: USERGROUP_PHASE is defined but not used\. Spelling mistake\\? +0 errors and 1 warnings found\..*\$ +EOT + stderr_re => undef, + exitcode => 0, + }, + 'mail/getmail' => { + stdout_re => <<EOT, +^looks fine\.\$ +EOT + stderr_re => undef, + exitcode => 0, + }, + ); + + my $dirprefix = dirname($0) || '.'; + my $pkglint = "$dirprefix/pkglint.pl"; + my $perl = $Config{perlpath}; + for my $package (keys %reference_packages) { + test_program($perl, [ $pkglint, "@PKGSRCDIR@/$package" ], + $reference_packages{$package}->{exitcode}, + $reference_packages{$package}->{stdout_re}, + $reference_packages{$package}->{stderr_re}); + } + # XXX this is JUST like test_unit(), when the tests work, refactor! + +} + +sub main { + test_get_vartypes_basictypes(); + test_get_vartypes_map(); + test_checkline_mk_vartype_basic(); + test_pkglint_main(); + test_lint_some_reference_packages(); +} + +main(); diff --git a/pkgtools/pkglint4/files/plist-clash.pl b/pkgtools/pkglint4/files/plist-clash.pl new file mode 100644 index 00000000000..1b3f1c52465 --- /dev/null +++ b/pkgtools/pkglint4/files/plist-clash.pl @@ -0,0 +1,60 @@ +#! @PERL@ +# $NetBSD: plist-clash.pl,v 1.1 2015/11/25 16:42:21 rillig Exp $ +# +# Scan all PLIST files given on the command line and report all lines +# that appear more than once. + +my %files = (); + +sub read_PLIST($) { + my ($fname) = @_; + + if (!open(F, "<", $fname)) { + warn "$!\n"; + return undef; + } + + my $lineno = 0; + foreach my $line (<F>) { + chomp($line); + $lineno++; + + # Ignore comments and commands + next if ($line =~ qr"^@"); + + # Ignore filenames with embedded variables + next if ($line =~ qr"\$"); + + if ($line =~ qr"^[A-Za-z0-9].*") { + if (!exists($files{$line})) { + $files{$line} = []; + } + push(@{$files{$line}}, "$fname:$lineno"); + + } else { + warn("ERROR: $fname:$lineno: Unknown line type\n"); + } + } + close(F); +} + +sub main() { + if (@ARGV == 0) { + die("usage: $0 <plist>...\n"); + } + + foreach my $plist (@ARGV) { + read_PLIST($plist); + } + + foreach my $file (sort keys %files) { + my $srcs = $files{$file}; + if (@{$srcs} != 1) { + foreach my $src (@{$srcs}) { + print "$src: $file\n"; + } + } + } +} + +main(); |