aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorB. Watson <yalhcru@gmail.com>2022-04-04 14:07:14 -0400
committerB. Watson <yalhcru@gmail.com>2022-04-04 14:07:14 -0400
commit533599f74e56dc42bdac215e2d152f9769b6b56e (patch)
tree9a8d349d138937a7c2880589ae16078b68f4009e
downloadsbo-maintainer-tools-533599f74e56dc42bdac215e2d152f9769b6b56e.tar.gz
initial commit
-rw-r--r--Makefile41
-rw-r--r--README40
-rw-r--r--TODO44
-rw-r--r--pre-commit-sbolint63
-rwxr-xr-xsbolint1408
-rw-r--r--sbolint.1288
-rwxr-xr-xsbopkglint490
-rw-r--r--sbopkglint.1363
-rw-r--r--sbopkglint.d/05-basic-sanity.t.sh152
-rw-r--r--sbopkglint.d/10-docs.t.sh40
-rw-r--r--sbopkglint.d/15-noarch.t.sh14
-rw-r--r--sbopkglint.d/20-arch.t.sh69
-rw-r--r--sbopkglint.d/25-lafiles.t.sh22
-rw-r--r--sbopkglint.d/30-manpages.t.sh110
-rw-r--r--sbopkglint.d/35-desktop.t.sh35
-rw-r--r--sbopkglint.d/40-newconfig.t.sh22
-rw-r--r--sbopkglint.d/45-doinst.t.sh53
17 files changed, 3254 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..05b4e3c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,41 @@
+PROJ=sbo-maintainer-tools
+VERSION=0.4
+
+PREFIX=/usr/local
+DESTDIR=
+
+SHAREDIR=$(PREFIX)/share
+TESTDIR=$(SHAREDIR)/$(PROJ)/sbopkglint.d
+BINDIR=$(PREFIX)/bin
+MANDIR=$(PREFIX)/man
+MAN1DIR=$(MANDIR)/man1
+DOCDIR=$(PREFIX)/doc/$(PROJ)-$(VERSION)
+
+DOCS=README
+SCRIPTS=sbopkglint sbolint
+
+all:
+ @echo "Use 'make install PREFIX=<path>' to install $(PROJ)."
+
+install: man
+ install -d -oroot -groot -m0755 \
+ $(DESTDIR)$(TESTDIR) \
+ $(DESTDIR)$(BINDIR) \
+ $(DESTDIR)$(MAN1DIR) \
+ $(DESTDIR)$(DOCDIR)
+ install -oroot -groot -m0755 $(SCRIPTS) $(DESTDIR)$(BINDIR)/
+ install -oroot -groot -m0644 sbopkglint.d/* $(DESTDIR)$(TESTDIR)/
+ install -oroot -groot -m0644 $(DOCS) $(DESTDIR)$(DOCDIR)/
+ gzip -9c < sbolint.1 > $(DESTDIR)$(MAN1DIR)/sbolint.1.gz
+ gzip -9c < sbopkglint.1 > $(DESTDIR)$(MAN1DIR)/sbopkglint.1.gz
+
+clean:
+ rm -f sbopkglint.1 sbolint.1
+
+man: sbopkglint.1 sbolint.1
+
+sbolint.1: sbolint
+ ./sbolint --man > sbolint.1
+
+sbopkglint.1: sbopkglint
+ ./sbopkglint --man > sbopkglint.1
diff --git a/README b/README
new file mode 100644
index 0000000..7c06d83
--- /dev/null
+++ b/README
@@ -0,0 +1,40 @@
+sbo-maintainer-tools
+--------------------
+
+These are "lint" tools to make life easier for SlackBuilds.org
+maintainers and admins.
+
+Included tools:
+
+- sbolint: checks your SlackBuild, README, .info file, and slack-desc.
+ Also there's a git pre-commit hook you should use, that automatically
+ calls sbolint and stops you from committing bad code.
+
+- sbopkglint: checks your package, after it's built.
+
+Note that the SBo admins use these tools as part of the approval
+process, when you submit an update. Failure to pass the lint checks
+is a valid reason for rejecting your submission, so you should either
+make sure your scripts and packages pass the tests, or give a good
+explanation why a failure isn't relevant to your build (because the
+tests aren't perfect, of course).
+
+Installation:
+
+The best way to install sbo-maintainer-tools is to install them from
+SBo (system/sbo-maintainer-tools).
+
+If you prefer, you can install them with "make install" (defaults to
+/usr/local; add 'PREFIX=/usr' if you'd rather). You can also run them
+directly from the source directory, if you can think of a reason for
+that (e.g. if you're hacking on the tests).
+
+To use the git hook, copy pre-commit-sbolint to .git/hooks/pre-commit
+in your git work tree (wherever you cloned the SBo repository),
+and make sure it's executable (chmod +x). If you already have a
+pre-commit hook, you can copy/paste the code, or (possibly) just
+append pre-commit-sbolint to your existing hook (if it doesn't end
+with "exit 0").
+
+For more information, see the sbolint and sbopkglint man pages (or run
+the scripts with --doc; it's the same thing).
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..45d013a
--- /dev/null
+++ b/TODO
@@ -0,0 +1,44 @@
+Future options:
+
+-e Only log errors, not warnings (right now, everything is a warning).
+
+Future test ideas:
+
+- extract the doinst.sh separately and check it. If we e.g. have
+ an icon cache or desktop file cache in $PKG, but no
+ gtk-update-icon-cache or update-desktop-database command in the
+ doinst.sh, that's definitely an error.
+
+- more forbidden files. for fonts.{dir|scale}, we need the doinst.sh
+ test (doinst can and should create these; they should *not* just be
+ files in the package).
+
+- noarch could recommend a package be made noarch, if it contains no
+ ELF files and doesn't use lib or lib64 dirs. Should this just
+ be a recommendation, or should it count as a failed test?
+
+- static libraries? some packages ship these because upstream doesn't
+ support shared libs, though. maybe only complain if libfoo.so.* and
+ libfoo.a both exist (if we have a shared lib, we shouldn't also have
+ a static one). Maybe this should be a disable-able warning?
+
+- icons. Make sure they are what their filename says (I've run into .png
+ files named .svg, and .gif files named .png, etc). If they're in
+ /usr/share/icons/<size>x<size>/, make sure they actually are the correct
+ size (or that they're SVG, if they're in scalable/). Really large icons
+ in /usr/share/pixmaps are probably useless (most stuff that uses the
+ old-style icons expects them to be 48x48 or 64x64). Icons must be
+ readable by everyone, non-executable, and owned by root:root.
+
+- duplicate files, maybe the error message could suggest a "ln -s" command
+ to use if the file really does need to appear in multiple dirs.
+
+- fonts. make sure they are what their filename says, and are installed to
+ the correct /usr/share/fonts/* or /usr/share/kbd/consolefonts dir.
+
+Other ideas:
+
+When linting multiple packages, print a summary: 100 packages checked,
+10 failed, 90 passed. Maybe with percentages too.
+
+Clean up the output. Make it easier to grep.
diff --git a/pre-commit-sbolint b/pre-commit-sbolint
new file mode 100644
index 0000000..06cb7a5
--- /dev/null
+++ b/pre-commit-sbolint
@@ -0,0 +1,63 @@
+#!/bin/bash
+
+# 20220315 bkw: SBo pre-commit hook, wrapper for sbolint.
+
+# Installation:
+
+# Copy this to <gitdir>/.git/hooks, mode 0755 (or anyway, make it
+# executable). Also get sbolint and install it somewhere on $PATH,
+# like /usr/local/bin. sbolint comes from:
+
+# https://slackware.uk/~urchlay/repos/sbostuff/plain/sbolint
+
+# That's a wgettable URL. You can also clone the sbostuff
+# repo from https://slackware.uk/~urchlay/repos/sbostuff if you want.
+
+# Usage:
+
+# Just do your usual "git commit". When you do, sbolint will run on
+# the build you're updating. If it finds any issues, it will cause the
+# commit to abort, so you can fix whatever's wrong and try the commit
+# again.
+
+# Since sbolint isn't perfect, you can skip the check for any commit
+# by running e.g:
+
+# SBOLINT=no git commit <arguments>
+
+# You can also run sbolint by itself, and read its documentation with
+# "sbolint --docs".
+
+set -e
+exec 1>&2
+
+# There should normally only be one changed build per commit, but this
+# rule may get broken when the repo's frozen pending a new Slackware
+# release. So use a loop.
+
+# The weird-looking "< <(command)" syntax is why this script must
+# have a #!/bin/bash at the top: it won't work with #!/bin/sh, even if
+# /bin/sh is a symlink to bash.
+
+if [ "${SBOLINT:-yes}" = "yes" ]; then
+ sbolintfailed=""
+ if ! which sbolint &>/dev/null; then
+ echo "WARNING: can't find sbolint in PATH, no linting will be done"
+ else
+ while read build; do
+ # if there's no slack-desc or README, assume the build has been removed.
+ # the directory still might exist after a "git rm -rf" because it
+ # might contain untracked files (e.g. the source tarball).
+ if [ -e "$build/slack-desc" -o -e "$build/README" ]; then
+ sbolint "$build" || sbolintfailed=1
+ fi
+ done < <(git diff --cached --name-only | cut -d/ -f1,2 | sort -u)
+ fi
+ if [ -n "$sbolintfailed" ]; then
+ echo "*** sbolint failed, fix the errors or set SBOLINT=no"
+ echo "*** in the environment to commit anyway."
+ exit 1
+ fi
+fi
+
+exit 0
diff --git a/sbolint b/sbolint
new file mode 100755
index 0000000..788489f
--- /dev/null
+++ b/sbolint
@@ -0,0 +1,1408 @@
+#!/usr/bin/perl -w
+
+# ChangeLog:
+
+# 0.4 20220314 bkw: add -a option to check all builds in the git repo.
+
+# 0.3 20200420 bkw:
+# - Check github URLs for validity.
+
+# 0.2 20200103 bkw:
+# - Use "git rev-parse" to decide if we're in a git repo, because
+# "git status" traverses the whole repo looking for untracked files.
+# It does this even if you use -uno (it won't *print* the untracked
+# files, but it still searches for them). Thanks to alienBOB for cluing
+# me in to using rev-parse for this.
+# - Skip the junkfiles check when we're in a git repo. It's more
+# annoying than it is useful.
+# - Allow possible -e/-u arguments in the shebang.
+# - Avoid false positives when the script does a "cd $PKG" and then
+# uses relative paths for install/*.
+# - Require VERSION= to appear within the first 10 non-comment/non-blank
+# lines, and don't check it anywhere else in the script.
+# - Allow scripts to skip lint checks via ###sbolint on/off comments.
+
+# 0.1 20141114 bkw, Initial release.
+
+$VERSION="0.4";
+
+# This script is meant to be fairly self-contained, prefer not to
+# require a huge pile of perl module dependencies. In some cases this
+# means using system() or backticks or such (e.g. to run tar, instead of
+# using Archive::Tar). Please don't "improve" the script by using a ton
+# of modules. The POSIX module ships with perl, not afraid of using that.
+
+# future options:
+# -l list packages with errs/warnings, don't give details
+# possibly some way to selectively disable the checks (does anyone
+# really need this?)
+
+# future ideas for checks:
+# - REQUIRES= packages have to exist? annoying if you're working on a batch
+# of stuff to be submitted together.
+# - Validate images, e.g. icon.png or .xpm or such. ImageMagick's identify
+# command can tell a non-image or a wrong-format image (a .jpg filename
+# that's actually a PNG image), but it doesn't detect truncated images.
+# Also we have to parse its stdout/stderr, it returns 0.
+
+=pod
+
+=head1 NAME
+
+sbolint - check SlackBuild directories or tarballs for common errors.
+
+=head1 SYNOPSIS
+
+B<sbolint> [-a] [-g] [-q] [-u] [-n] [build [build ...]]
+
+=head1 DESCRIPTION
+
+sbolint checks for common errors in SlackBuilds.org scripts. It's
+intended for slackbuild authors and maintainers, and can cut down on
+"There was a problem with your upload" errors from the submission form.
+
+The [build] arguments must be either directories or tarballs, each
+containing a SlackBuild script, slack-desc, README, and .info file.
+With no [build] arguments, the current directory is checked.
+
+sbolint will flag errors for problems that would prevent the build from
+being accepted by the upload form (or by the SBo admins, if if passes
+the upload checks). There may also be warnings, which are things that
+(probably) won't stop your build from being accepted, but may cause the
+SBo admins extra work.
+
+sbolint was not written by the SlackBuilds.org team, and shares no code
+with the upload form's submission checker. Lack of errors/warnings from
+sbolint does not guarantee that your build will be accepted!
+
+sbolint doesn't check built packages, and never executes the build
+script. If you want a lint tool for binary Slackware packages, try
+pprkut's B<lintpkg>.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<-a>
+
+Check all builds in the git repository. This must be run from within a
+git tree (e.g. one made with "git clone").
+
+=item B<-q>
+
+Quiet. Suppresses 'xxx checks out OK' and the total errors/warnings summary.
+
+=item B<-u>
+
+URL check. Uses B<curl> to make HTTP HEAD requests for the B<HOMEPAGE>,
+B<DOWNLOAD>, and B<DOWNLOAD_x86_64> links. This won't guarantee that
+the links are good, but some kinds of failure (e.g. site down, 404)
+means they're definitely bad. Unfortunately a lot of sites have stopped
+responding to HEAD requests in the name of "security", so your mileage
+man vary.
+
+=item B<-n>
+
+Suppress warnings. Only errors will be listed. This also affects the
+exit status (see below).
+
+=back
+
+=head1 CHECKS
+
+For tar files only:
+
+=over 4
+
+=item -
+
+File size must not be bigger than the upload form's limit (currently one
+megabyte).
+
+=item -
+
+File must be a tar archive, possibly compressed with gzip, bzip2, or xz,
+extractable by the B<tar>(1) command.
+
+=item -
+
+Filename extension must match compression type.
+
+=item -
+
+Archive must contain a directory with the same name as the archive's base name,
+e.g. I<foo.tar.gz> must contain I<foo/>. Everything else in the archive must be
+inside this directory.
+
+=item -
+
+Archive must contain I<dirname/Idirname.SlackBuild>.
+
+=back
+
+For all builds:
+
+=over 4
+
+=item -
+
+The SlackBuild, .info, README files must have Unix \n line endings (not
+DOS \r\n), and the last line of each must have a \n.
+
+=item -
+
+The SlackBuild script must exist, with mode 0755 (or 0644, if in a git repo),
+and be a I<#!/bin/bash> script.
+
+=item -
+
+The script must contain the standard variable assignments for PRGNAM,
+VERSION, BUILD, and TAG. BUILD must be numeric.
+
+=item -
+
+I<PRGNAM> in the script must match I<PRGNAM> in the .info file. Both must
+match the script name (I<PRGNAM.SlackBuild>) and the directory name.
+
+=item -
+
+I<VERSION> must match the I<VERSION> in the .info file.
+
+=item -
+
+TAG=${TAG:-_SBo} must occur in the script.
+
+=item -
+
+The I<VERSION>, I<BUILD>, and I<TAG> variables must respect the environment.
+
+=item -
+
+The script must install the slack-desc in I<$PKG/install>.
+
+=item -
+
+If there is a doinst.sh script, the SlackBuild must install it to I<$PKG/install>.
+
+=item -
+
+Template boilerplate comments should be removed, e.g. I<"REMOVE THIS ENTIRE BLOCK OF TEXT">
+or I<"Automatically determine the architecture we're building on">.
+
+=item -
+
+Script must contain exactly one B<makepkg> command.
+
+=item -
+
+README must exist, have mode 0644, its character encoding must be
+either ASCII or UTF-8 without BOM, and it may not contain tab characters.
+
+=item -
+
+slack-desc must exist, have mode 0644, its character encoding must be ASCII,
+and it may not contain tab characters.
+
+=item -
+
+slack-desc contents must match the SBo template, including the "handy-ruler",
+comments, and correct spacing/indentation.
+
+=item -
+
+.info file must exist, have mode 0644, and match the SBo template.
+
+=item -
+
+.info file URLs must be valid URLs (for a very loose definition of "valid": they
+must begin with B<ftp://>, B<http://>, or B<https://>).
+
+=item -
+
+Optionally, .info file URLs can be checked for existence with an HTTP HEAD
+request (see the B<-u> option).
+
+=back
+
+The following tests are only done when sbolint's starting directory
+was NOT in a git repo:
+
+=over 4
+
+=item -
+
+Any files other than the .SlackBuild, .info, slack-desc, and README are
+checked for permissions (should be 0644) and excessive size.
+
+=item -
+
+The source archive(s) must not exist. Also sbolint attempts to detect
+extracted source trees (but isn't all that good at it).
+
+=item -
+
+Files named 'build.log' or 'strace.out*' must not exist. The B<sbrun>
+tool creates these.
+
+=back
+
+The rationale for skipping the above tests when in a git repo is that
+maintainers will be using git to track files and push changes, so we
+don't need to check them here.
+
+=head1 EXIT STATUS
+
+Exit status from sbolint will normally be 0 (success) if there were no
+errors or warnings in any of the builds checked. With the B<-n> option,
+exit status will be 0 if there are no errors.
+
+Exit status 1 indicates there was at least one warning or error (or, with
+B<-n>, at least one error).
+
+Any other exit status means sbolint itself failed somehow (e.g. called
+with nonexistent filename).
+
+=head1 BUGS
+
+Probably quite a few. Watch this space for details.
+
+=head1 AUTHOR
+
+B. Watson (yalhcru at gmail dot com, or Urchlay on Libera IRC)
+
+=head1 SEE ALSO
+
+B<sbofixinfo>(1), B<sbosearch>(1)
+
+=cut
+
+use POSIX qw/getcwd/;
+
+@boilerplate = (
+ qr/#\s*REMOVE THIS ENTIRE BLOCK OF TEXT/,
+ qr/#\s*replace with (?:version:name) of program/,
+ qr/#\s*the "_SBo" is required/,
+ qr/#\s*Automatically determine the architecture we're building on/,
+ qr/#\s*Unless \$ARCH is already set,/,
+ qr/#\s*For consistency's sake, use this/,
+ qr/#\s*Drop the package in \/tmp/,
+ qr/#\s*Exit on most errors/,
+ qr/#\s*If you prefer to do selective error checking with/,
+ qr/#\s*Your application will probably need/,
+ qr/#\s*Compile the application and install it into the/,
+ qr/#\s*Strip binaries and libraries - this can be done with/,
+ qr/#\s*Compress man pages$/,
+ qr/#\s*Compress info pages and remove the/,
+ qr/#\s*Copy program documentation into the package/,
+ qr/#\s*Copy the slack-desc \(and a custom doinst\.sh if necessary\)/,
+ qr/#\s*Make the package; be sure to leave it in/,
+);
+
+# this was scraped from the HTML source for the upload form:
+$MAX_TARBALL_SIZE = 1048576;
+
+($SELF = $0) =~ s,.*/,,;
+
+$buildname = $build = "";
+$g_warncount = 0;
+$g_errcount = 0;
+$warncount = 0;
+$errcount = 0;
+
+$tempdir = 0;
+
+our %info = (); # has to be global, check_info sets it, check_script needs it
+
+# main() {
+#check_github_url("testing", $_) for @ARGV;
+#exit 0;
+
+while(@ARGV && ($ARGV[0] =~ /^-/)) {
+ my $opt = shift;
+ $opt =~ /^-a/ && do { $recursive_git = 1; next; };
+ $opt =~ /^-u/ && do { $url_head = 1; next; };
+ $opt =~ /^-d/ && do { $url_download = 1; next; };
+ $opt =~ /^--?q(uiet)?/ && do { $quiet = 1; next; };
+ $opt =~ /^-$/ && do { $stdin = 1; next; };
+ $opt =~ /^--?h(elp)?/ && do { usage(); exit 0; };
+ $opt =~ /^-n$/ && do { $nowarn = 1; next; };
+ $opt =~ /^-r$/ && do { $suppress_readme_len = 1; next; };
+ $opt =~ /^--doc$/ && do { exec("perldoc $0"); };
+ $opt =~ /^--man$/ && do { exec("pod2man --stderr -s1 -cSBoStuff -r$VERSION $0"); };
+ die_usage("Unrecognized option '$opt'");
+}
+
+if($url_head && $url_download) {
+ die_usage("-u and -d options are mutually exclusive");
+}
+
+if($url_head || $url_download) {
+ if(system("curl --version > /dev/null") != 0) {
+ die "$SELF: -u and -d options require curl, can't find it in your \$PATH.\n";
+ }
+}
+
+if($stdin) {
+ @ARGV = <STDIN>;
+ chomp for @ARGV;
+}
+
+if($recursive_git) {
+ @ARGV=();
+ my $pwd;
+
+ # find root of the SBo git repo, if we're somewhere inside it.
+ while(! -d ".git" && ! -d "system") {
+ chdir("..");
+ chomp($pwd = `pwd`);
+ die "$SELF: -a option only works if you run $SELF from a git worktree\n" if $pwd eq "/";
+ }
+
+ chomp($pwd = `pwd`);
+
+ for(`git ls-files '*/*/*.SlackBuild' | cut -d/ -f1,2`) {
+ chomp;
+ push @ARGV, $_;
+ }
+
+ warn "$SELF: linting " . scalar(@ARGV) . " builds from git repo at $pwd\n" unless $quiet;
+ $quiet = 1;
+}
+
+push @ARGV, "." unless @ARGV;
+
+# are we in a git repo? build scripts are mode 0644 there, plus
+# the junkfile check is skipped.
+$in_git_repo = system("git rev-parse >/dev/null 2>/dev/null") == 0;
+
+for(@ARGV) {
+ run_checks($_);
+ $g_errcount += $errcount;
+ $g_warncount += $warncount;
+
+ if(!$quiet) {
+ if($errcount == 0 and $warncount == 0) {
+ print "$SELF: $buildname checks out OK\n";
+ } else {
+ print "$SELF: $buildname: errors $errcount, warnings $warncount\n";
+ }
+ }
+}
+
+# print total errs/warns only if >1 build checked
+if(!$quiet && @ARGV > 1) {
+ print "$SELF: Total errors: $g_errcount\n";
+ print "$SELF: Total warnings: $g_warncount\n" unless $nowarn;
+}
+
+exit ($g_errcount > 0 || (!$nowarn && $g_warncount > 0));
+# }
+
+sub dequote {
+ my $a = shift;
+ #warn "dequote arg: $a\n";
+ $a =~ s/^("|')(\S+)(\1)$/$2/;
+ #warn "dequote ret: $a\n";
+ return $a;
+}
+
+sub logmsg {
+ my $severity = shift;
+ my $format = shift;
+ printf("$buildname: $severity: $format\n", @_);
+}
+
+sub log_error {
+ logmsg("ERR", @_);
+ $errcount++;
+}
+
+sub log_warning {
+ return if $nowarn;
+ logmsg("WARN", @_);
+ $warncount++;
+}
+
+sub usage {
+ if(@_) {
+ warn "$SELF: $_\n" for @_;
+ }
+
+ warn <<EOF;
+
+$SELF - check SlackBuilds.org scripts for common problems.
+
+Usage: $SELF [-q] [-u] [-n] [-r] <build <build ...>>
+Usage: $SELF --help | --man
+
+builds may be directories or tarballs. If no build arguments given,
+. (current directory) is assumed. Use - to read a list of tarballs/dirs
+from stdin.
+
+Options:
+
+-a Lint all builds in the git repo.
+-q Quiet: only emit errors/warnings, no 'checks out OK' or totals.
+-u URL Check: use HTTP HEAD request to verify download/homepage URLs exist.
+-n Suppress warnings, log only errors.
+-r Suppress warning about README lines being too long.
+--doc See the full documentation, in your pager.
+--man Convert the full documentation to a man page, on stdout.
+
+Do not bundle options (say "-q -r", not "-qr").
+
+See the full documentation for more details.
+EOF
+# not yet:
+#-d URL Download: as -u, plus download & check md5sums of download URLs.
+}
+
+sub die_usage {
+ usage(@_);
+ exit 1;
+}
+
+sub chdir_or_die {
+ chdir($_[0]) or die "$SELF: chdir($_[0]): $!\n";
+}
+
+sub make_temp_dir {
+ return if $tempdir;
+ my $tmp = $ENV{TMP} || "/tmp";
+ $tempdir = "$tmp/$SELF." . int(rand(2**32-1));
+ system("rm -rf $tempdir");
+ system("mkdir -p $tempdir");
+ if(! -d $tempdir) {
+ die "$SELF: can't create temp dir $tempdir\n";
+ }
+}
+
+sub rm_temp_dir {
+ if($tempdir && (-d $tempdir)) {
+ system("rm -rf $tempdir");
+ $tempdir = 0;
+ }
+}
+
+sub check_tarball_mime {
+ my $file = shift;
+
+ ### This stuff is a little pedantic. It also relies on having a recent-ish
+ ### version of GNU file (the one in Slack 14.1 works fine).
+ my %types = (
+ 'tar' => 'application/x-tar',
+ 'tar.gz' => 'application/x-gzip',
+ 'tar.bz2' => 'application/x-bzip2',
+ 'tar.xz' => 'application/x-xz',
+ );
+
+ (my $basename = $file) =~ s,.*/,,;
+ my (undef, $ext) = split /\./, $basename, 2;
+ my $mime = `file --brief --mime-type $file`;
+ chomp $mime;
+
+ if(!grep { $_ eq $mime } values %types) {
+ log_error("$file is not a tarball (mime type is '$mime')");
+ } elsif(!$ext) {
+ log_error("$file: filename has no extension (will be rejected by upload form)");
+ } elsif($types{$ext} ne $mime) {
+ log_error("$file mime type '$mime' doesn't match filename (should be $types{$ext})");
+ } elsif($ext ne 'tar') {
+ my $realmime = `file -z --brief --mime-type $file`;
+ chomp $realmime;
+ if($realmime ne 'application/x-tar') {
+ log_error("$file doesn't contain a tar archive (content mime type is $realmime, should be application/x-tar)");
+ }
+ }
+}
+
+sub check_tarball {
+ my $file = shift;
+
+ ### First, mime type checks. None of this will be fatal (no return 0 on error).
+ check_tarball_mime($file);
+
+ ### one more pre-extraction check:
+ if(-s "$file" > $MAX_TARBALL_SIZE) {
+ log_warning("$file is larger than $MAX_TARBALL_SIZE bytes, upload may be rejected");
+ }
+
+ ### now call tar to list the contents, and start returning 0 on failure.
+ my @list = split "\n", `tar tf $file`;
+ if($?) {
+ log_error("$file: tar failed to list contents");
+ return 0;
+ }
+
+ if(!@list) {
+ log_error("$file is empty archive?");
+ return 0;
+ }
+
+ if($list[0] ne "$buildname/") {
+ log_error("$file not a SBo-compliant tarball, first element should be '$buildname/', not '$list[0]'");
+ return 0;
+ }
+
+ my $foundsb = 0;
+ shift @list; # 1st element is dirname/, we already checked it
+ for(@list) {
+ my $bn = quotemeta($buildname); # some builds have + in the name
+ if(not /^$bn\//) {
+ log_error("$file not a SBo-compliant tarball, contains extra junk '$_'");
+ return 0;
+ }
+
+ if(/^$bn\/$bn.SlackBuild$/) {
+ $foundsb = 1;
+ }
+ }
+
+ if(not $foundsb) {
+ log_error("$file not a SBo-compliant tarball, doesn't contain '$buildname/$buildname.SlackBuild'");
+ return 0;
+ }
+
+ return 1;
+}
+
+sub extract_tarball {
+ my $file = shift;
+ $file = `readlink -n -e $file`;
+ make_temp_dir();
+ chdir_or_die($tempdir);
+ system("tar xf $file");
+ return "$tempdir/$buildname";
+}
+
+# run_checks will extract its argument (then cd to it) if it's a tarball,
+# otherwise cd to its argument if it's a dir, otherwise error.
+sub run_checks {
+ $build = shift;
+ my $oldcwd = getcwd();
+
+ $errcount = $warncount = 0;
+
+ if(-f $build || -l $build) {
+ ($buildname = $build) =~ s,\.tar(\..*)?$,,;
+ $buildname =~ s,.*/,,;
+ if(check_tarball($build)) {
+ chdir_or_die(extract_tarball($build));
+ } else {
+ return 0;
+ }
+ } elsif(-d $build) {
+ chdir_or_die($build);
+ } else {
+ die_usage "'$build' not a file or a directory.";
+ }
+
+ # last component of directory is the build name
+ $buildname = `readlink -n -e .`;
+ $buildname =~ s,.*/,,;
+
+ my @checks = (
+ \&check_readme,
+ \&check_slackdesc,
+ \&check_info,
+ \&check_script,
+ \&check_images,
+ );
+
+ # if we're in a git repo, it's assumed we're going to track extra
+ # files with git, and use git to update the build, not tar it up
+ # and use the web form.
+ push @checks, \&check_junkfiles unless $in_git_repo;
+ for(@checks) {
+ $_->($build);
+ }
+
+ chdir_or_die($oldcwd);
+ rm_temp_dir();
+}
+
+sub check_mode {
+ my ($file, $wantmode) = @_;
+ if(! -e $file) {
+ log_error("$file does not exist");
+ return 0;
+ }
+
+ my $gotmode = 07777 & ((stat($file))[2]);
+ if($wantmode != $gotmode) {
+ log_error("$file should be mode %04o, not %04o", $wantmode, $gotmode);
+ return 0;
+ }
+
+ return 1;
+}
+
+sub check_crlf {
+ my $file = shift;
+ for(@_) {
+ if(/\r/) {
+ log_error("$file has DOS-style CRLF line endings");
+ return 0;
+ }
+ }
+ return 1;
+}
+
+sub check_and_read {
+ my ($file, $mode) = @_;
+
+ my $crlf_err;
+ my @lines;
+ my $lastline_nonl;
+
+ check_mode($file, $mode);
+
+ if(open my $fh, "<$file") {
+ while(<$fh>) {
+ $lastline_nonl = 1 unless /\n$/;
+ chomp;
+ $crlf_err = 1 if s/\r$//;
+ push @lines, $_;
+ }
+ if(scalar @lines == 0) {
+ log_error("$file exists but is empty");
+ }
+ }
+
+ log_error("$file has DOS-style CRLF line endings") if $crlf_err;
+ log_error("$file has no newline at EOF") if $lastline_nonl;
+ return @lines;
+}
+
+# 20220315 bkw: warn if a file isn't ASCII or UTF-8 without BOM.
+# Used for README and slack-desc...
+sub check_encoding {
+ my $file = shift;
+ my $ascii_only = shift;
+ my $ftype;
+
+ # 20220314 bkw: the -e options make file faster and turn off checks
+ # we don't need, ones that sometimes cause false detection too.
+ chomp($ftype = `file -b -e cdf -e compress -e csv -e elf -e json -e soft -e tar $file`);
+
+ if($ascii_only && ($ftype !~ /ASCII text/)) {
+ log_warning("$file must be ASCII text, not $ftype");
+ }
+
+ if($ftype =~ /ASCII text/ || $ftype =~ /UTF-8/) {
+ # encoding is OK, but:
+ if($ftype =~ /BOM/) {
+ log_warning("$file has BOM, remove with: LANG=C sed -i '1s/^\\xEF\\xBB\\xBF//' $file");
+ }
+ } elsif($ftype =~ /ISO-8859/) {
+ log_warning("$file has ISO-8859 encoding, fix with: mv $file $file.old; iconv -f iso-8859-1 -t utf-8 $file.old > $file; rm $file.old");
+ } else {
+ log_warning("$file isn't ASCII or UTF-8, file(1) says it's '$ftype'");
+ }
+}
+
+sub check_readme {
+ my $maxlen = $ENV{'SBOLINT_README_MAX'} || 72;
+ my @lines = check_and_read("README", 0644);
+ return unless @lines;
+
+ check_encoding("README", 0);
+
+ if(grep { /\t/ } @lines) {
+ log_warning("README has tabs, these should be replaced with spaces");
+ }
+
+ return if $suppress_readme_len;
+
+ # 20220205 bkw: don't complain about long lines if they're URLs,
+ # not much we can do about them.
+ if(grep { !/^\s*(ftp|https?):\/\// && length > $maxlen } @lines) {
+ log_warning("README has lines >$maxlen characters");
+ }
+}
+
+# the slack-desc checking code offends me (the author), on the one hand it's
+# overly complex, and on the other hand it assumes the slack-desc is at
+# least close to being right...
+sub check_slackdesc {
+ my @lines = check_and_read("slack-desc", 0644);
+ return unless scalar @lines;
+
+ check_encoding("slack-desc", 1);
+
+ if(grep { /\t/ } @lines) {
+ log_warning("slack-desc has tabs, these should be replaced with spaces");
+ }
+
+ my $lineno = 1;
+
+ if($lines[0] =~ /^# HOW TO EDIT THIS FILE:$/) {
+ shift @lines;
+ $lineno++;
+ } else {
+ log_warning("slack-desc doesn't start with how-to-edit comment");
+ }
+
+ my $count = 0;
+ while($lines[0] =~ /^#/) {
+ $count++;
+ $lineno++;
+ shift @lines;
+ }
+
+ if($count != 5) {
+ log_warning("slack-desc doesn't have standard how-to-edit stanza");
+ }
+
+ $count = 0;
+ while($lines[0] eq "") {
+ $count++;
+ $lineno++;
+ shift @lines;
+ }
+
+ if($count == 0) {
+ log_warning("slack-desc missing blank line before handy-ruler");
+ } elsif($count > 1) {
+ log_warning("slack-desc has extra blank lines before handy-ruler");
+ }
+
+ if($lines[0] =~ /handy-ruler/) {
+ my $ruler = shift @lines;
+ $lineno++;
+ my ($spaces, $prefix, $hr, $suffix, $junk) = ($ruler =~ /^( *)(\|-+)(handy-ruler)(-+\|)(.*)$/);
+
+ if(length($spaces) != length($buildname)) {
+ log_error("slack-desc:$lineno: handy-ruler has wrong number of indent spaces (%d, should be %d)",
+ length($spaces),
+ length($buildname));
+ }
+
+ if(length($junk) > 0) {
+ log_error("slack-desc:$lineno: handy-ruler has %d characters of trailing junk after last |", length($junk));
+ }
+
+ my $rlen = length($prefix . $hr . $suffix);
+ if($rlen != 72) {
+ log_error("slack-desc:$lineno: handy-ruler must be 72 characters, not %d", $rlen);
+ } elsif(length($prefix) != 6) {
+ log_error("slack-desc:$lineno: handy-ruler malformed, has '$prefix' instead of '|-----'");
+ }
+ } else {
+ log_error("slack-desc missing handy-ruler");
+ }
+
+ $count = 0;
+ for(@lines) {
+ $count++;
+ if(my ($prefix, $text) = /^([^\s]+:)(.*)/) {
+ if($prefix ne "$buildname:") {
+ log_error("slack-desc:$lineno: wrong prefix '$prefix', should be '$buildname:'");
+ } elsif($text =~ /^\s+$/) {
+ log_error("slack-desc:$lineno: trailing whitespace after colon, on otherwise-blank line");
+ } elsif(length($text) > 72) {
+ log_error("slack-desc:$lineno: text too long, %d characters, should be <= 72", length($text));
+ } elsif(length($text) && $text !~ /^ /) {
+ log_error("slack-desc:$lineno: missing whitespace after colon, on non-blank line");
+ }
+
+ my $bn = quotemeta($buildname); # some builds have + in the name
+ if(($count == 1) && ($text !~ /^ $bn \(.+\)$/)) {
+ log_warning("slack-desc:$lineno: first description line should be '$buildname: $buildname (short desc)'");
+ }
+ } else {
+ log_error("slack-desc:$lineno: malformed line in description section");
+ }
+
+ $lineno++;
+ }
+
+ if($count < 11) {
+ log_error("slack-desc only has $count description lines, should be 11 (add some empties)");
+ } elsif($count > 11) {
+ log_error("slack-desc has too many description lines ($count, should be 11)");
+ }
+}
+
+# This is a damn mess. Needs refactoring badly.
+sub check_info {
+ my $file = $buildname . ".info";
+ my @lines = check_and_read($file, 0644);
+ return unless scalar @lines;
+
+ my $lineno = 0;
+ my $file_lineno = 0;
+ my @expected = qw/PRGNAM VERSION HOMEPAGE
+ DOWNLOAD MD5SUM
+ DOWNLOAD_x86_64 MD5SUM_x86_64
+ REQUIRES MAINTAINER EMAIL/;
+ my $next_exp = 0;
+ my @keys;
+ my $continuation = 0;
+
+ # parse and bitch about bad syntax...
+ for(@lines) {
+ $file_lineno++;
+ if($continuation) {
+ s/^\s*//;
+ $_ = "$continuation $_";
+ $continuation = 0;
+ $lineno = $file_lineno - 1;
+ } else {
+ $lineno = $file_lineno;
+ }
+
+ if(s/\s*\\$//) {
+ $continuation = $_;
+ next;
+ }
+
+ if(/^\s*$/) {
+ log_error("$file:$lineno: blank line (get rid of it)");
+ next;
+ }
+
+ unless(/=/) {
+ log_error("$file:$lineno: malformed line (no = sign, missing \\ on prev line?)");
+ next;
+ }
+
+ if(s/^\s+//) {
+ log_error("$file:$lineno: leading whitespace before key");
+ }
+
+ if(s/\s+$//) {
+ log_error("$file:$lineno: trailing whitespace at EOL");
+ }
+
+ if(my ($k, $s1, $s2, $q1, $val, $q2) = /^(\w+)(\s*)=(\s*)("?)(.*?)("?)$/) {
+ if(!grep { $k eq $_ } @expected) {
+ log_error("$file:$lineno: invalid key '$k'");
+ } else {
+ if($k ne $expected[$next_exp]) {
+ log_warning("$file:$lineno: out of order, expected $expected[$next_exp], got $k");
+ }
+ $next_exp++;
+ }
+
+ if(not $q1) {
+ log_error("$file:$lineno: missing opening double-quote");
+ }
+
+ if(not $q2) {
+ log_error("$file:$lineno: missing closing double-quote");
+ }
+
+ if(length($s1) || length($s2)) {
+ log_error("$file:$lineno: no spaces allowed before/after = sign");
+ }
+
+ my $oldval = $val;
+ if($val =~ s/^\s+//) {
+ log_error("$file:$lineno: leading space in value: \"$oldval\"");
+ }
+
+ if($val =~ s/\s+$//) {
+ log_error("$file:$lineno: trailing space in value: \"$oldval\"");
+ }
+
+ $info{$k} = $val;
+ } else {
+ log_error("$file:$lineno: malformed line");
+ }
+ }
+
+ # parsing done, now for semantic checks
+
+ my @missing;
+ for(@expected) {
+ if(not exists($info{$_})) {
+ push @missing, $_;
+ }
+ }
+
+ log_error("$file: missing required key(s): " . (join ", ", @missing)) if @missing;
+
+ # init this to avoid checking undef values below
+ $info{$_} ||= "" for @expected;
+
+ if($info{PRGNAM} && ($info{PRGNAM} ne $buildname)) {
+ log_error("$file: PRGNAM is '$info{PRGNAM}', should be '$buildname'");
+ }
+
+ if($info{VERSION} =~ /-/) {
+ log_error("$file: VERSION may not contain - (dash) characters");
+ }
+
+ if(!check_url($info{HOMEPAGE})) {
+ log_error("$file: HOMEPAGE=\"$info{HOMEPAGE}\" doesn't look like a valid URL (http, https, or ftp)");
+ }
+
+ # use a HEAD request for homepage, even if downloading other files
+ if($url_head || $url_download) {
+ curl_head_request($file, $info{HOMEPAGE}) || do {
+ log_warning("$file: HOMEPAGE URL broken?");
+ };
+ }
+
+ if($info{MD5SUM} =~ /^\s*$/) {
+ log_error("$file: MD5SUM is missing or blank") unless $info{DOWNLOAD} eq 'UNSUPPORTED';
+ } else {
+ check_dl_and_md5($file, "");
+ }
+
+ my $dl64 = $info{DOWNLOAD_x86_64};
+ if($dl64 =~ /^(?:|UNSUPPORTED|UNTESTED)$/) {
+ if($info{MD5SUM_x86_64} ne "") {
+ log_error("$file: MD5SUM_x86_64 must be blank if DOWNLOAD_x86_64 is not set");
+ }
+ } elsif($info{MD5SUM_x86_64} eq "") {
+ log_error("$file: MD5SUM_x86_64 may not be blank if DOWNLOAD_x86_64 is set");
+ } else {
+ check_dl_and_md5($file, "_x86_64");
+ }
+}
+
+sub check_dl_and_md5 {
+ my($file, $suffix) = @_;
+ my $md5key = "MD5SUM" . $suffix;
+ my $dlkey = "DOWNLOAD" . $suffix;
+
+ my @dlurls = split /\s+/, $info{$dlkey};
+ my @md5s = split /\s+/, $info{$md5key};
+
+ if(@md5s != @dlurls) {
+ log_error("$file: we have " . @dlurls . " $dlkey URLs but " . @md5s . " $md5key" . " values");
+ }
+
+ for my $u (@dlurls) {
+ if(!check_url($u)) {
+ log_error("$file: $dlkey URL '$u' doesn't look like a valid URL (http, https, or ftp)");
+ next;
+ }
+
+ #check_github_url($file, $u);
+
+ if($url_head) {
+ curl_head_request($file, $u) || do {
+ warn '$u is '. $u;
+ log_warning("$file: $dlkey URL '$u' broken?");
+ };
+ } elsif($url_download) {
+ warn "$SELF: -d option not yet implemented\n";
+ }
+ }
+
+ for(@md5s) {
+ unless(/^[0-9a-f]{32}$/) {
+ log_error("$file: $md5key '$_' is invalid (must be 32 hex digits)");
+ }
+ }
+
+ # TODO: maybe actually download and check md5sums.
+}
+
+sub check_url {
+ # url is bad if:
+ return 0 if $_[0] =~ /\s/; # ...it contains a space,
+ return 0 if $_[0] !~ /\./; # ...it has no dots, or
+ return 0 if $_[0] !~ /\//; # ...it has no slashes, or
+ return ($_[0] =~ /^(?:ftp|https?):\/\//); # ...it doesn't have a known protocol,
+ # ...which doesn't necessarily mean it's a good URL either.
+}
+
+sub curl_head_request {
+ #return !system("curl --head --location --silent --fail $_[0] >/dev/null");
+ #warn $_[1];
+ my $file = $_[0];
+ my $client_filename = $_[1];
+ $client_filename =~ s,.*/,,;
+ my $curlcmd = "curl -m20 --head --location --silent --fail $_[1]";
+ open my $pipe, "$curlcmd|";
+ #warn "$curlcmd";
+ while(<$pipe>) {
+ chomp;
+ s/\r//;
+ if(/^content-disposition:\s+attachment;\s+filename=["']?(.*?)["']?$/i) {
+ #warn $1;
+ if(defined($client_filename) && ($client_filename ne $1)) {
+ log_warning("$file: download filename varies based on content disposition: '$1' vs. '$client_filename'");
+ }
+ }
+ }
+ return close($pipe);
+}
+
+# WIP, maybe no longer needed
+## sub check_github_url {
+## my $file = shift;
+## my $url = shift;
+## return unless $url =~ m{(https?:)//github\.com};
+##
+## if($1 eq "http:") {
+## log_warning("$file: github URL $url should be https");
+## }
+##
+## (my $expect_filename = $url) =~ s,.*/,,;
+## my(undef, undef, undef, $user, $prog, $archive, $ver, $filename) = split /\//, $url;
+## warn "user $user, prog $prog, archive $archive, ver $ver, filename $filename, expect_filename $expect_filename\n";
+##
+## # assume these are correct, for now
+## return if $user eq 'downloads';
+## return if $archive eq 'releases';
+##
+## # TODO: work out what to do about /raw/
+## return if $archive eq 'raw';
+##
+## if($archive ne 'archive') {
+## log_warning("$file: unknown github URL type: $url");
+## return;
+## }
+##
+## # OK, good URLs look like this:
+## # https://github.com/jeetsukumaran/DendroPy/archive/v4.4.0/DendroPy-4.4.0.tar.gz
+## # ...and bad ones look like this:
+## # https://github.com/haiwen/seafile-client/archive/v4.4.2.tar.gz
+## # Corrected version of the bad one would be:
+## # https://github.com/haiwen/seafile-client/archive/v4.4.2/seafile-client-4.4.2.tar.gz
+## # Notice the "v" isn't part of the version number. It's not always there,
+## # and sometimes it's a different letter (r, or g, or capital V, etc).
+## }
+
+# NOT going to police the script too much. Would end up rewriting most of
+# the shell, in perl. Plus, it'd become a straitjacket. Here's what I'll
+# implement:
+# - #!/bin/bash on line 1
+# - PRGNAM must match $buildname
+# - VERSION must match the .info VERSION
+# - BUILD line must be present
+# - TAG line must be present
+# - If VERSION, BUILD, TAG don't respect the env, it's a warning
+# - Check for strings like slack-desc, $PKG/install, makepkg, stuff
+# that's standard for SBo. Don't be too specific here.
+# - If there's a doinst.sh, it must mentioned in the script. If not,
+# it better not be mentioned.
+# - Check for leftover boilerplate
+# - cp -a <documentation> is an error
+
+sub check_script {
+ my $file = $buildname . ".SlackBuild";
+ my $wantmode = $in_git_repo ? 0644 : 0755;
+
+ my @lines = check_and_read($file, $wantmode);
+ return unless scalar @lines;
+
+ if($lines[0] !~ /^#!/) {
+ log_error("$file:1: missing or invalid shebang line (should be '#!/bin/bash')");
+ } elsif($lines[0] !~ m,#!/bin/bash(?: (?:-e|-eu|-ue|-e -u|-u -e))?$,) {
+ log_warning("$file:1: shebang line should be #!/bin/bash (possibly with -e/-u arg(s)), not '$lines[0]'");
+ }
+
+ my $lineno = 0;
+ my ($prgnam, $version, $build, $tag, $need_doinst, $slackdesc, $makepkg, $install);
+ my ($cdpkg, $codestart, $lint_enabled, $print_pkg_name);
+ $lint_enabled = 1;
+
+ for(@lines) {
+ $lineno++;
+
+ if(/^\s*[^#]/ && !defined($codestart)) {
+ $codestart = $lineno;
+ }
+
+ if(/^###sbolint\s*(\S+)/) {
+ my $arg = $1;
+ if(lc($arg) eq "on") {
+ $lint_enabled = 1;
+ } elsif(lc($arg) eq "off") {
+ $lint_enabled = 0;
+ } else {
+ log_warning("$file:$lineno: unknown ###sbolint argument '$arg' (should be 'on' or 'off')");
+ }
+ }
+
+ next unless $lint_enabled;
+
+ # TODO: cp without -a (or -p, or a couple other flags) is OK.
+## if(/^[^#]*cp\s+(?:-\w+\s+)*[\"\$\{]*CWD/) {
+## log_error("$file:$lineno: copying files from CWD with cp (use cat instead)");
+## }
+
+ if(/^PRGNAM=(\S+)/) {
+ if($prgnam) {
+ log_error("$file:$lineno: PRGNAM redefined");
+ }
+ $prgnam = dequote($1);
+ if($prgnam ne $buildname) {
+ log_error("$file:$lineno: PRGNAM doesn't match dir name ($prgnam != $buildname)");
+ }
+ } elsif(/^VERSION=(\S+)/ && ($lineno <= $codestart + 10)) {
+ $version = dequote($1);
+ if(not ($version =~ s/\$\{VERSION:-([^}]+)\}/$1/)) {
+ log_warning("$file:$lineno: VERSION ignores environment, try VERSION=\${VERSION:-$version}");
+ }
+ $version = dequote($1);
+ if($version ne $info{VERSION}) {
+ log_error("$file:$lineno: VERSION ($version) doesn't match VERSION in the .info file ($info{VERSION})");
+ }
+ } elsif(/^BUILD=(\S+)/) {
+ $build = dequote($1);
+ if(not ($build =~ /\d/)) {
+ log_error("$file:$lineno: BUILD is non-numeric");
+ } elsif(not ($build =~ /\$\{BUILD:-\d+}/)) {
+ log_warning("$file:$lineno: BUILD ignores environment (try BUILD=\${BUILD:-$build}");
+ }
+ } elsif(/^TAG=(\S+)/) {
+ $tag = dequote($1);
+ if($tag !~ /\$\{TAG:-(?:_SBo|("|')_SBo(\1))\}/) {
+ log_error("$file:$lineno: TAG=\${TAG:-_SBo} is required");
+ }
+ } elsif(/^[^#]*\$\{?CWD\}?\/doinst\.sh/) {
+ # 20220205 bkw: some scripts don't have a doinst.sh in the
+ # script dir, but they create one with >> (the jack rt audio stuff
+ # does this).
+ $need_doinst = $lineno;
+ } elsif(/^[^#]*slack-desc/) {
+ $slackdesc = $lineno;
+ $install = $lineno if m,install/,; # assume OK
+ } elsif(/^[^#]*?cd\s+[{\$"]*PKG[}"]*/) {
+ $cdpkg = $lineno;
+ } elsif(/^[^#]*?["{\$]+PKG[}"]*\/install/) {
+ $install = $lineno;
+ } elsif($cdpkg && /^[^#]*mkdir[^#]*install/) {
+ $install = $lineno;
+ } elsif(/^[^#]*makepkg/) {
+ if($makepkg) {
+ log_error("$file:$lineno: makepkg called twice (here and line $makepkg");
+ }
+ $makepkg = $lineno;
+ }
+
+ if(/^[^#]*<documentation>/) {
+ log_error("$file:$lineno: copy actual documentation, not <documentation>");
+ }
+
+ my $line = $_;
+ if(grep { $line =~ /$_/ } @boilerplate) {
+ log_warning("$file:$lineno: template comment should be removed");
+ }
+
+ # special case here: don't complain about this comment if it's a perl-* build
+ if($file !~ /^perl-/) {
+ if($line =~ /#\s*Remove perllocal.pod and other special files/) {
+ log_warning("$file:$lineno: template comment should be removed");
+ }
+ }
+
+ # 20220312 bkw: 15.0 template
+ if(/^[^#]*\$.*PRINT_PACKAGE_NAME/) {
+ $print_pkg_name = 1;
+ }
+ }
+
+ if(not defined($prgnam)) {
+ log_error("$file: no PRGNAM= line");
+ }
+
+ if(not defined($version)) {
+ log_error("$file: no VERSION= line");
+ }
+
+ if(not defined($build)) {
+ log_error("$file: no BUILD= line");
+ }
+
+ if(not defined($tag)) {
+ log_error("$file: no TAG= line");
+ }
+
+ if(not defined($slackdesc)) {
+ log_error("$file: doesn't seem to install slack-desc in \$PKG/install");
+ }
+
+ if(not defined($makepkg)) {
+ log_error("$file: no makepkg command found");
+ }
+
+ if(not defined($print_pkg_name)) {
+ log_error("$file: missing PRINT_PACKAGE_NAME stanza (Slackware >= 15.0)");
+ }
+
+ if(not defined($install)) {
+ log_error("$file: nothing gets installed in \$PKG/install");
+ }
+
+ my $have_doinst = (-f "doinst.sh");
+ if($have_doinst) {
+ check_and_read("doinst.sh", 0644);
+ }
+ if($need_doinst && !$have_doinst) {
+ log_error("$file:$need_doinst: script installs doinst.sh, but it doesn't exist");
+ } elsif($have_doinst && !$need_doinst) {
+ log_error("$file: doinst.sh exists, but the script doesn't install it");
+ }
+}
+
+# stuff like editor backups and dangling symlinks.
+# maybe *any* symlinks?
+# ELF objects are bad, too.
+# Big-ass files...
+# directories are OK, but hidden dirs are not.
+sub check_junkfiles {
+ my @sources = split(/\s+/, $info{DOWNLOAD} . " " . $info{DOWNLOAD_x86_64});
+ s,.*/,, for @sources;
+ @sources = grep { $_ !~ /^(?:\s*|UNTESTED|UNSUPPORTED)$/ } @sources;
+ if(!grep { $_ =~ /^v$info{VERSION}\./ } @sources) {
+ push @sources, "v$info{VERSION}.$_" for qw /zip tar.gz tar.bz2 tar.xz/;
+ }
+
+ open my $fh, "-|", "find . ! -type d -print0 | xargs -0 file --mime-type";
+ FILE: while(<$fh>) {
+ chomp;
+ my ($file, $type) = split /: */, $_, 2;
+ $file =~ s,\./,,;
+
+ # skip the files caught by other checks
+ next if $file eq "$buildname.SlackBuild";
+ next if $file eq "$buildname.info";
+ next if $file eq "README";
+ next if $file eq "slack-desc";
+ next if $file =~ /(?:diff|patch)$/;
+
+ check_mode($file, 0644);
+
+ if(grep { $_ eq $file } @sources) {
+ log_error("source archive found: $file");
+ next FILE;
+ }
+
+ for($file) {
+ (/\.swp\w*$/ || /#/ || /~/ ) && do {
+ log_error("editor backup found: $file");
+ next FILE;
+ };
+ /^\./ && do {
+ log_error("hidden file found: $file");
+ next FILE;
+ };
+ /\.(?:orig|bak|old)[^.]*$/ && do {
+ log_warning("$file looks like sort some of backup file");
+ next FILE;
+ };
+ /^(?:build.log|strace.out)/ && do {
+ log_warning("$file is a build log");
+ next FILE;
+ };
+ /\.desktop$/ && do {
+ system("desktop-file-validate $file");
+ if($? != 0) {
+ log_warning("$file fails desktop-file-validate");
+ next FILE;
+ }
+ }
+ }
+
+ for($type) {
+ ($_ eq "inode/x-empty") && do {
+ log_error("$file is empty (0 bytes long)");
+ next FILE;
+ };
+ ($_ =~ /^inode/) && do {
+ log_error("$file is $type, not a regular file or directory");
+ next FILE;
+ };
+ ($_ =~ m,application/x-(?:executable|dosexec|object|coredump),) && do {
+ log_error("$file is object code ($type)");
+ next FILE;
+ };
+ }
+
+ my $size = -s $file;
+ if($size > 1024 * 100) {
+ log_warning("$file is large ($size bytes), may be rejected by submission form");
+ }
+ }
+ close $fh;
+
+ open $fh, "-|", "find . -type d -mindepth 1";
+ while(<$fh>) {
+ chomp;
+ s,\./,,;
+
+ if(/^\./) {
+ log_error("found hidden directory: $_");
+ next;
+ }
+
+ if(glob("$_/*.o")) {
+ log_error("$_ contains compiled object files (leftover source tree?)");
+ next;
+ }
+
+ for my $badfile (qw/Makefile configure CmakeLists.txt makefile.pl SConstruct/) {
+ if(-f "$_/$badfile") {
+ log_error("$_ looks like extracted source tree (contains $badfile)");
+ }
+ }
+ }
+ close $fh;
+
+# # this won't always catch everything (e.g. PRGNAM=foo VERSION=1, but the
+# # extracted dir is foo1 or foo_1 or foo-source-1).
+# if(-d "$buildname-$version") {
+# log_warning("$buildname-$version/ looks like extracted source dir");
+# }
+}
+
+# if anything *.diff or *.patch contains \r, warn the
+# user about git stripping the \r's (better gzip it).
+sub check_patches {
+ for(<*.diff>,<*.patch>) {
+ check_and_read($_, 0644);
+ }
+}
+
+# checking an image is a bit of a PITA. "file" can tell us if it's
+# not an image, or has the wrong extension.
+# ImageMagick's "identify" command won't detect truncated images.
+# "convert" will, but it always returns 0/success, so we have to
+# parse its output.
+sub im_check_img {
+ our %ext2mime;
+ my $mime;
+ my $ok = 1;
+
+ %ext2mime = (
+ png => 'image/png',
+ jpg => 'image/jpeg',
+ xpm => 'image/x-xpm',
+ gif => 'image/gif',
+ ) unless %ext2mime;
+
+ my $img = shift;
+ my $ext = $img;
+ $ext =~ s,.*\.,,;
+ $ext = lc $ext;
+
+ chomp($mime = `file -L --brief --mime "$img"`);
+ if($mime !~ /$ext2mime{$ext}/) {
+ log_error("$img has wrong extension $ext (MIME type is $mime)");
+ return;
+ }
+
+ open my $im, "convert \"$img\" png:/dev/null 2>&1 |";
+ while(<$im>) {
+ $ok = 0 if /premature|corrupt/i;
+ }
+ close $im;
+
+ log_error("$img appears to be corrupt") unless $ok;
+}
+
+sub check_images {
+ my $images = `find . \\( -iname '*.jpg' -o -iname '*.png' -o -iname '*.xpm' -o -iname '*.gif' \\) -print0`;
+ for(split /\x00/, $images) {
+ check_mode($_, 0644);
+ im_check_img($_);
+ }
+}
diff --git a/sbolint.1 b/sbolint.1
new file mode 100644
index 0000000..8ee2499
--- /dev/null
+++ b/sbolint.1
@@ -0,0 +1,288 @@
+.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.42)
+.\"
+.\" Standard preamble:
+.\" ========================================================================
+.de Sp \" Vertical space (when we can't use .PP)
+.if t .sp .5v
+.if n .sp
+..
+.de Vb \" Begin verbatim text
+.ft CW
+.nf
+.ne \\$1
+..
+.de Ve \" End verbatim text
+.ft R
+.fi
+..
+.\" Set up some character translations and predefined strings. \*(-- will
+.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left
+.\" double quote, and \*(R" will give a right double quote. \*(C+ will
+.\" give a nicer C++. Capital omega is used to do unbreakable dashes and
+.\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff,
+.\" nothing in troff, for use with C<>.
+.tr \(*W-
+.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p'
+.ie n \{\
+. ds -- \(*W-
+. ds PI pi
+. if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch
+. if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch
+. ds L" ""
+. ds R" ""
+. ds C` ""
+. ds C' ""
+'br\}
+.el\{\
+. ds -- \|\(em\|
+. ds PI \(*p
+. ds L" ``
+. ds R" ''
+. ds C`
+. ds C'
+'br\}
+.\"
+.\" Escape single quotes in literal strings from groff's Unicode transform.
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.\"
+.\" If the F register is >0, we'll generate index entries on stderr for
+.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index
+.\" entries marked with X<> in POD. Of course, you'll have to process the
+.\" output yourself in some meaningful fashion.
+.\"
+.\" Avoid warning from groff about undefined register 'F'.
+.de IX
+..
+.nr rF 0
+.if \n(.g .if rF .nr rF 1
+.if (\n(rF:(\n(.g==0)) \{\
+. if \nF \{\
+. de IX
+. tm Index:\\$1\t\\n%\t"\\$2"
+..
+. if !\nF==2 \{\
+. nr % 0
+. nr F 2
+. \}
+. \}
+.\}
+.rr rF
+.\"
+.\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2).
+.\" Fear. Run. Save yourself. No user-serviceable parts.
+. \" fudge factors for nroff and troff
+.if n \{\
+. ds #H 0
+. ds #V .8m
+. ds #F .3m
+. ds #[ \f1
+. ds #] \fP
+.\}
+.if t \{\
+. ds #H ((1u-(\\\\n(.fu%2u))*.13m)
+. ds #V .6m
+. ds #F 0
+. ds #[ \&
+. ds #] \&
+.\}
+. \" simple accents for nroff and troff
+.if n \{\
+. ds ' \&
+. ds ` \&
+. ds ^ \&
+. ds , \&
+. ds ~ ~
+. ds /
+.\}
+.if t \{\
+. ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u"
+. ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u'
+. ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u'
+. ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u'
+. ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u'
+. ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u'
+.\}
+. \" troff and (daisy-wheel) nroff accents
+.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V'
+.ds 8 \h'\*(#H'\(*b\h'-\*(#H'
+.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#]
+.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H'
+.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u'
+.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#]
+.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#]
+.ds ae a\h'-(\w'a'u*4/10)'e
+.ds Ae A\h'-(\w'A'u*4/10)'E
+. \" corrections for vroff
+.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u'
+.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u'
+. \" for low resolution devices (crt and lpr)
+.if \n(.H>23 .if \n(.V>19 \
+\{\
+. ds : e
+. ds 8 ss
+. ds o a
+. ds d- d\h'-1'\(ga
+. ds D- D\h'-1'\(hy
+. ds th \o'bp'
+. ds Th \o'LP'
+. ds ae ae
+. ds Ae AE
+.\}
+.rm #[ #] #H #V #F C
+.\" ========================================================================
+.\"
+.IX Title "SBOLINT 1"
+.TH SBOLINT 1 "2022-04-03" "0.4" "SBoStuff"
+.\" For nroff, turn off justification. Always turn off hyphenation; it makes
+.\" way too many mistakes in technical documents.
+.if n .ad l
+.nh
+.SH "NAME"
+sbolint \- check SlackBuild directories or tarballs for common errors.
+.SH "SYNOPSIS"
+.IX Header "SYNOPSIS"
+\&\fBsbolint\fR [\-a] [\-g] [\-q] [\-u] [\-n] [build [build ...]]
+.SH "DESCRIPTION"
+.IX Header "DESCRIPTION"
+sbolint checks for common errors in SlackBuilds.org scripts. It's
+intended for slackbuild authors and maintainers, and can cut down on
+\&\*(L"There was a problem with your upload\*(R" errors from the submission form.
+.PP
+The [build] arguments must be either directories or tarballs, each
+containing a SlackBuild script, slack-desc, \s-1README,\s0 and .info file.
+With no [build] arguments, the current directory is checked.
+.PP
+sbolint will flag errors for problems that would prevent the build from
+being accepted by the upload form (or by the SBo admins, if if passes
+the upload checks). There may also be warnings, which are things that
+(probably) won't stop your build from being accepted, but may cause the
+SBo admins extra work.
+.PP
+sbolint was not written by the SlackBuilds.org team, and shares no code
+with the upload form's submission checker. Lack of errors/warnings from
+sbolint does not guarantee that your build will be accepted!
+.PP
+sbolint doesn't check built packages, and never executes the build
+script. If you want a lint tool for binary Slackware packages, try
+pprkut's \fBlintpkg\fR.
+.SH "OPTIONS"
+.IX Header "OPTIONS"
+.IP "\fB\-a\fR" 4
+.IX Item "-a"
+Check all builds in the git repository. This must be run from within a
+git tree (e.g. one made with \*(L"git clone\*(R").
+.IP "\fB\-q\fR" 4
+.IX Item "-q"
+Quiet. Suppresses 'xxx checks out \s-1OK\s0' and the total errors/warnings summary.
+.IP "\fB\-u\fR" 4
+.IX Item "-u"
+\&\s-1URL\s0 check. Uses \fBcurl\fR to make \s-1HTTP HEAD\s0 requests for the \fB\s-1HOMEPAGE\s0\fR,
+\&\fB\s-1DOWNLOAD\s0\fR, and \fBDOWNLOAD_x86_64\fR links. This won't guarantee that
+the links are good, but some kinds of failure (e.g. site down, 404)
+means they're definitely bad. Unfortunately a lot of sites have stopped
+responding to \s-1HEAD\s0 requests in the name of \*(L"security\*(R", so your mileage
+man vary.
+.IP "\fB\-n\fR" 4
+.IX Item "-n"
+Suppress warnings. Only errors will be listed. This also affects the
+exit status (see below).
+.SH "CHECKS"
+.IX Header "CHECKS"
+For tar files only:
+.IP "\-" 4
+File size must not be bigger than the upload form's limit (currently one
+megabyte).
+.IP "\-" 4
+File must be a tar archive, possibly compressed with gzip, bzip2, or xz,
+extractable by the \fBtar\fR(1) command.
+.IP "\-" 4
+Filename extension must match compression type.
+.IP "\-" 4
+Archive must contain a directory with the same name as the archive's base name,
+e.g. \fIfoo.tar.gz\fR must contain \fIfoo/\fR. Everything else in the archive must be
+inside this directory.
+.IP "\-" 4
+Archive must contain \fIdirname/Idirname.SlackBuild\fR.
+.PP
+For all builds:
+.IP "\-" 4
+The SlackBuild, .info, \s-1README\s0 files must have Unix \en line endings (not
+\&\s-1DOS\s0 \er\en), and the last line of each must have a \en.
+.IP "\-" 4
+The SlackBuild script must exist, with mode 0755 (or 0644, if in a git repo),
+and be a \fI#!/bin/bash\fR script.
+.IP "\-" 4
+The script must contain the standard variable assignments for \s-1PRGNAM,
+VERSION, BUILD,\s0 and \s-1TAG. BUILD\s0 must be numeric.
+.IP "\-" 4
+\&\fI\s-1PRGNAM\s0\fR in the script must match \fI\s-1PRGNAM\s0\fR in the .info file. Both must
+match the script name (\fI\s-1PRGNAM\s0.SlackBuild\fR) and the directory name.
+.IP "\-" 4
+\&\fI\s-1VERSION\s0\fR must match the \fI\s-1VERSION\s0\fR in the .info file.
+.IP "\-" 4
+TAG=${TAG:\-_SBo} must occur in the script.
+.IP "\-" 4
+The \fI\s-1VERSION\s0\fR, \fI\s-1BUILD\s0\fR, and \fI\s-1TAG\s0\fR variables must respect the environment.
+.IP "\-" 4
+The script must install the slack-desc in \fI\f(CI$PKG\fI/install\fR.
+.IP "\-" 4
+If there is a doinst.sh script, the SlackBuild must install it to \fI\f(CI$PKG\fI/install\fR.
+.IP "\-" 4
+Template boilerplate comments should be removed, e.g. \fI\*(L"\s-1REMOVE THIS ENTIRE BLOCK OF TEXT\*(R"\s0\fR
+or \fI\*(L"Automatically determine the architecture we're building on\*(R"\fR.
+.IP "\-" 4
+Script must contain exactly one \fBmakepkg\fR command.
+.IP "\-" 4
+\&\s-1README\s0 must exist, have mode 0644, its character encoding must be
+either \s-1ASCII\s0 or \s-1UTF\-8\s0 without \s-1BOM,\s0 and it may not contain tab characters.
+.IP "\-" 4
+slack-desc must exist, have mode 0644, its character encoding must be \s-1ASCII,\s0
+and it may not contain tab characters.
+.IP "\-" 4
+slack-desc contents must match the SBo template, including the \*(L"handy-ruler\*(R",
+comments, and correct spacing/indentation.
+.IP "\-" 4
+\&.info file must exist, have mode 0644, and match the SBo template.
+.IP "\-" 4
+\&.info file URLs must be valid URLs (for a very loose definition of \*(L"valid\*(R": they
+must begin with \fBftp://\fR, \fBhttp://\fR, or \fBhttps://\fR).
+.IP "\-" 4
+Optionally, .info file URLs can be checked for existence with an \s-1HTTP HEAD\s0
+request (see the \fB\-u\fR option).
+.PP
+The following tests are only done when sbolint's starting directory
+was \s-1NOT\s0 in a git repo:
+.IP "\-" 4
+Any files other than the .SlackBuild, .info, slack-desc, and \s-1README\s0 are
+checked for permissions (should be 0644) and excessive size.
+.IP "\-" 4
+The source archive(s) must not exist. Also sbolint attempts to detect
+extracted source trees (but isn't all that good at it).
+.IP "\-" 4
+Files named 'build.log' or 'strace.out*' must not exist. The \fBsbrun\fR
+tool creates these.
+.PP
+The rationale for skipping the above tests when in a git repo is that
+maintainers will be using git to track files and push changes, so we
+don't need to check them here.
+.SH "EXIT STATUS"
+.IX Header "EXIT STATUS"
+Exit status from sbolint will normally be 0 (success) if there were no
+errors or warnings in any of the builds checked. With the \fB\-n\fR option,
+exit status will be 0 if there are no errors.
+.PP
+Exit status 1 indicates there was at least one warning or error (or, with
+\&\fB\-n\fR, at least one error).
+.PP
+Any other exit status means sbolint itself failed somehow (e.g. called
+with nonexistent filename).
+.SH "BUGS"
+.IX Header "BUGS"
+Probably quite a few. Watch this space for details.
+.SH "AUTHOR"
+.IX Header "AUTHOR"
+B. Watson (yalhcru at gmail dot com, or Urchlay on Libera \s-1IRC\s0)
+.SH "SEE ALSO"
+.IX Header "SEE ALSO"
+\&\fBsbofixinfo\fR(1), \fBsbosearch\fR(1)
diff --git a/sbopkglint b/sbopkglint
new file mode 100755
index 0000000..c7f88f7
--- /dev/null
+++ b/sbopkglint
@@ -0,0 +1,490 @@
+#!/bin/bash
+
+: <<EOF
+=pod
+
+=head1 NAME
+
+sbopkglint - check Slackware binary packages for common errrors.
+
+=head1 SYNOPSIS
+
+B<sbopkglint> [-k] [-i] [package.t?z ...]
+
+=head1 DESCRIPTION
+
+B<sbopkglint> installs a Slackware package to a temporary directory, then
+examines the contents. It finds lots of common problems that aren't
+always noticed by SBo script maintainers or the admins.
+
+This is for built packages. If you want to lint your build scripts,
+use B<sbolint>(1) instead.
+
+With no package arguments, it looks for a SlackBuild in the current
+directory, extracts the PRINT_PACKAGE_NAME information, and tries
+to find a package in $OUTPUT (/tmp by default). If found, it checks
+that package. It's up to you to know whether the package needs to be
+rebuilt (e.g. if you've edited the SlackBuild since the package was
+built).
+
+With arguments, it checks the given packages. These must be
+supported Slackware package files (.tgz, .txz, .tlz, etc). There's no
+requirement that these have to be SBo packages, but a couple of the
+tests (e.g. the check for $PRGNAM.SlackBuild in the doc dir) might not
+apply to non-SBo builds.
+
+Diagnostics will be logged to stdout and stderr. Exit status will
+be 0 if all tests passed, non-zero otherwise.
+
+This script must run as root. If you run it as a normal user, it tries
+to re-execute itself via sudo(8).
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<-k>
+
+Keep the temporary package install directory instead of deleting it on exit.
+
+=item B<-i>
+
+Disable the "useless-looking install instructions" test. This is
+intended for SBo admins mass-linting a ton of packages. INSTALL in
+the doc dir is something that exists in thousands of existing builds,
+and it's not a major problem. New builds and updates should be linted
+without this option, however.
+
+=item B<--help>
+
+Show the short built-in help.
+
+=item B<--doc>
+
+View this documentation using perldoc(1), which generally uses your
+pager (e.g. less(1) or more(1)) to display it.
+
+=item B<--man>
+
+Convert this documentation to a man page, on stdout.
+
+=back
+
+=head1 EXIT STATUS
+
+0 (success) if all tests passed for all packages, non-zero if there
+were any test failures (or if installpkg failed for some reason).
+
+=head1 FILES
+
+=over 4
+
+=item B<sbolint.d/*.t.sh>
+
+These are the actual tests. They're installed to
+I<PREFIX>/share/sbo-maintainer-tools, and they're sourced by
+B<sbopkglint> at runtime. Each test script begins with (hopefully)
+useful comments that go into more detail than the documentation here.
+
+=back
+
+=head1 TESTS
+
+=head2 basic-sanity
+
+=over 4
+
+=item B<->
+
+Top-level directories inside the package must be recognized ones,
+such as /bin /etc /usr /opt. Packages shouldn't be installing files
+in /tmp, /dev, or /home... and they really shouldn't be inventing
+new top-level directories.
+
+=item B<->
+
+The documentation directory must exist and be correctly named, as
+/usr/doc/$PRGNAM-$VERSION. It must contain $PRGNAM.SlackBuild, too.
+
+=item B<->
+
+The directories /usr/local, /usr/share/doc, /usr/share/man, /usr/etc
+are not allowed in SBo packages.
+
+=item B<->
+
+Some directories (e.g. /usr/bin) may not contain subdirectories.
+
+=item B<->
+
+Some directories (e.g. /usr/share) must *only* contain subdirectories.
+
+=item B<->
+
+Some directories (e.g. /usr/man, /usr/share/applications) must not
+contain files with executable permissions. /usr/doc is not in this
+list; neither is /etc (too many existing packages install +x files
+there).
+
+=item B<->
+
+Broken symlinks may not exist.
+
+=item B<->
+
+Absolute symlinks may not exist (they should be converted to
+relative symlinks). This may seem like nitpicking, but packages
+may be installed somewhere besides / (the root dir) with the
+-root option to installpkg. If /usr/bin/foo is a link to /usr/bin/bar,
+it should be a link to just bar.
+
+=back
+
+=head2 docs
+
+=over 4
+
+=item B<->
+
+Documentation must be installed to /usr/doc/$PRGNAM-$VERSION. If
+there's any other directory under /usr/doc, it's incorrect. Some
+builds use mis-named doc directories (if it hasn't been fixed by
+now, an example is gcc5, which installs docs to /usr/doc/gcc-$VERSION
+when it should be gcc5-$VERSION).
+
+=item B<->
+
+Documentation files must be readable by everyone, and owned by root:root.
+
+=item B<->
+
+Doc dir shouldn't contain empty files (0 bytes in length).
+
+=item B<->
+
+Doc dir shouldn't contain install instructions. Specifically, files
+named INSTALL, INSTALL.*, or install.txt are flagged (it's impossible
+to make this test 100% perfect).
+
+=back
+
+=head2 noarch
+
+=over 4
+
+=item B<->
+
+If a package has its architecture set to "noarch", it must not contain
+any ELF binaries/libraries.
+
+=back
+
+=head2 arch
+
+=over 4
+
+=item B<->
+
+If a package has its architecture set to i?86 or x86_64, all ELF
+binaries/libraries must be for the correct arch (no 32-bit code in
+64-bit packages, and vice versa).
+
+=item B<->
+
+If a package is i?86, it must not contain /usr/lib64.
+
+=item B<->
+
+If a package is x86_64 and contains shared libraries, they must be
+in /lib64 or /usr/lib64 (not /lib or /usr/lib).
+
+=back
+
+=head2 lafiles
+
+=over 4
+
+=item B<->
+
+Packages are no longer allowed to contain libtool archive files (.la)
+in /lib, /lib64, /usr/lib, or /usr/lib64. However, subdirectories
+such as /usr/lib64/someprogram/ are not checked, since some applications
+which use plugins actually use the .la files.
+
+=back
+
+=head2 manpages
+
+=over 4
+
+=item B<->
+
+All man pages must be readable by everyone, and owned by root:root.
+
+=item B<->
+
+All man pages must be gzipped.
+
+=item B<->
+
+All man pages must be in /usr/man/man[1-9n] or /usr/man/<lang>/man[1-9n].
+
+=item B<->
+
+Man page directories must be mode 755, owned by root:root.
+
+=item B<->
+
+The section numbers in man page filenames must match the section number
+in the directory name (e.g. /usr/man/man1/ls.1.gz is OK,
+/usr/man/man1/tetris.6.gz is an error).
+
+=item B<->
+
+Man pages must actually be man pages (troff markup).
+
+=back
+
+=head2 desktop
+
+=over 4
+
+=item B<->
+
+If there are .desktop files, doinst.sh must run update-desktop-database.
+
+=item B<->
+
+.desktop files must be mode 644, owned by root:root. Slackware's KDE
+packages actually break this rule (they install executable .desktop
+files), but SBo packages are not allowed to.
+
+=item B<->
+
+Only .desktop files are allowed in /usr/share/applications.
+
+=item B<->
+
+.desktop files must be valid, according to the desktop-file-validate
+command. Only actual errors count; warnings don't cause this test to
+fail.
+
+=back
+
+=head2 newconfig
+
+=over 4
+
+=item B<->
+
+Any files (outside of /usr/doc) with names ending in .new are flagged.
+This might be a bit too restrictive (possibly only check /etc and
+/usr/share?)
+
+=back
+
+=head2 doinst
+
+=over 4
+
+=item B<->
+
+If there are icons in /usr/share/icons, .desktop files in /usr/share/applications,
+or glib2 schemas in /usr/share/glib-2.0/schemas, there must be a doinst.sh
+with appropriate command(s), e.g. update-desktop-database, gtk-update-icon-cache,
+glib-compile-schemas.
+
+=back
+
+=head1 BUGS
+
+Probably many. This is still a work in progress.
+
+One known problem is that the same file can fail multiple tests. E.g.
+if you have a man page that's installed executable, it will fail both
+the basic-sanity test and the manpages test. This isn't really a huge
+problem, so it might not be fixed any time soon.
+
+=head1 AUTHOR
+
+B. Watson <urchlay@slackware.uk>, AKA Urchlay on Libera IRC.
+
+=head1 SEE ALSO
+
+B<sbolint>(1)
+
+=cut
+EOF
+
+# lint a binary Slackware package. primarily intended for use with
+# SBo packages, but could be used for any Slack pkg.
+
+# this must be run as root, as it installs the package in a temp dir,
+# with "installpkg -root". if it's not running as root, it tries to
+# run itself via sudo.
+
+SELF="$( basename $0 )"
+
+usage() {
+ cat 1>&2 <<EOF
+$SELF - check SBo binary packages for various problems
+
+Usage:
+ $0 [-k] [-i] [/path/to/package-file] [...]
+ $0 --doc | --man
+
+Options:
+-k Keep (don't delete) the package install directory at exit.
+-i Do not check for INSTALL in the doc dir.
+--doc See the full documentation in your pager.
+--man Convert the full documentation to a man page, on stdout.
+
+With no package arguments, it looks for a SlackBuild in the current
+directory, extracts the PRINT_PACKAGE_NAME information, and tries to
+find a package in \$OUTPUT (/tmp by default).
+
+With arguments, it checks the given packages. These must be supported
+Slackware package files (.tgz, .txz, .tlz, etc).
+
+Diagnostics will be logged to stdout and stderr. Exit status will
+be 0 if all tests passed, non-zero otherwise.
+
+This script must run as root. If you run it as a normal user, it tries
+to re-execute itself via sudo(8).
+EOF
+}
+
+while true; do
+ case "$1" in
+ --doc) exec perldoc "$0" ;;
+ --man) exec pod2man --stderr -s1 -csbo-maintainer-tools -r0.4 "$0" ;;
+ -k) KEEP=1 ; shift ;;
+ -i) INSTALL_DOCS_OK=1 ; shift;;
+ -h*|--h*) usage; exit 0 ;;
+ -*) echo "$SELF: invalid option '$1', try '$SELF --help'" ; exit 1 ;;
+ *) break ;;
+ esac
+done
+
+# where the test scripts live, space-separated list.
+SBOPKGLINT_PATH=${SBOPKGLINT_PATH:-"./sbopkglint.d /usr/share/sbo-maintainer-tools/sbopkglint.d /usr/local/share/sbo-maintainer-tools/sbopkglint.d"}
+
+if [ "$(id -u)" != "0" ]; then
+ exec sudo \
+ TMP="$TMP" \
+ OUTPUT="$OUTPUT" \
+ INSTALL_DOCS_OK="$INSTALL_DOCS_OK" \
+ KEEP="$KEEP" \
+ SBOPKGLINT_PATH="$SBOPKGLINT_PATH" \
+ "$0" "$@"
+fi
+
+warn() {
+ [ "$warncount" = "0" ] && echo
+ : $(( warncount ++ ))
+ echo "--- $@" 1>&2
+}
+
+die() {
+ warn "$@"
+ exit 1
+}
+
+TMP=${TMP:-/tmp}
+OUTPUT=${OUTPUT:-$TMP}
+
+exit_status=0
+
+if [ -n "$1" ]; then
+ packages="$@"
+else
+ cnt="$( /bin/ls *.SlackBuild | wc -l )"
+ case "$cnt" in
+ 0) die "No argument given and no SlackBuild script in current dir" ;;
+ 1) ;; # OK
+ *) die "Multiple SlackBuild scripts in current dir" ;;
+ esac
+ script="$( /bin/ls *.SlackBuild )"
+ if ! grep -q PRINT_PACKAGE_NAME $script; then
+ die "$script doesn't support PRINT_PACKAGE_NAME"
+ fi
+ filename="$( PRINT_PACKAGE_NAME=1 sh $script )"
+ packages="$OUTPUT/$filename"
+ if [ ! -e "$packages" ]; then
+ die "Can't find $packages"
+ fi
+fi
+
+for testdir in $SBOPKGLINT_PATH; do
+ [ -d $testdir ] || continue
+ testdir="$( realpath $testdir )"
+ break
+done
+[ -z "$testdir" -o "$testdir/*.t.sh" = '*.t.sh' ] && \
+ die "Can't find any tests to run, looked in: $SBOPKGLINT_PATH"
+
+echo "Using tests from $testdir"
+
+for package in $packages; do
+ filename="$( basename $package )"
+
+ ARCH="$( echo $filename | rev | cut -d- -f2 | rev )"
+ PRGNAM="$( echo $filename | rev | cut -d- -f4- | rev )"
+ VERSION="$( echo $filename | rev | cut -d- -f3 | rev )"
+ PKG="$( mktemp -d $TMP/sbopkglint.XXXXXX )"
+
+ echo -n "Installing $package to $PKG..."
+ /sbin/installpkg -root "$PKG" "$package" &> $PKG/.tmp.$$
+ S="$?"
+
+ if [ "$S" != "0" ]; then
+ echo "FAILED"
+ cat $PKG/.tmp.$$
+ echo "installpkg exited with status $S"
+ [ "$KEEP" = "" ] && rm -rf $PKG
+ exit_status=1
+ continue
+ fi
+
+ echo "OK"
+ rm -f $PKG/.tmp.$$
+
+ cd "$PKG"
+
+ totalwarns=0
+ foundtests=0
+ for testscript in $testdir/*.t.sh; do
+ foundtests=1
+ (
+ warncount=0
+ echo -n "Running test: $( basename $testscript .t.sh )..."
+ source "$testscript"
+ if [ "$warncount" = "0" ]; then
+ echo "OK"
+ else
+ echo "FAILED"
+ echo "$warncount" > .tmp.warncount
+ fi
+ )
+ if [ -e .tmp.warncount ]; then
+ warns="$( cat .tmp.warncount )"
+ : $(( totalwarns += warns ))
+ fi
+ rm -f .tmp.warncount
+ done
+
+ [ "$KEEP" = "" ] && rm -rf "$PKG"
+
+ if [ "$foundtests" = "0" ]; then
+ die "!!! can't find any tests to run in $testdir."
+ fi
+
+ if [ "$totalwarns" = "0" ]; then
+ echo "=== $filename: All tests passed"
+ else
+ exit_status=1
+ echo "!!! $filename: $totalwarns failures"
+ fi
+done
+
+exit $exit_status
diff --git a/sbopkglint.1 b/sbopkglint.1
new file mode 100644
index 0000000..274cfe2
--- /dev/null
+++ b/sbopkglint.1
@@ -0,0 +1,363 @@
+.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.42)
+.\"
+.\" Standard preamble:
+.\" ========================================================================
+.de Sp \" Vertical space (when we can't use .PP)
+.if t .sp .5v
+.if n .sp
+..
+.de Vb \" Begin verbatim text
+.ft CW
+.nf
+.ne \\$1
+..
+.de Ve \" End verbatim text
+.ft R
+.fi
+..
+.\" Set up some character translations and predefined strings. \*(-- will
+.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left
+.\" double quote, and \*(R" will give a right double quote. \*(C+ will
+.\" give a nicer C++. Capital omega is used to do unbreakable dashes and
+.\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff,
+.\" nothing in troff, for use with C<>.
+.tr \(*W-
+.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p'
+.ie n \{\
+. ds -- \(*W-
+. ds PI pi
+. if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch
+. if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch
+. ds L" ""
+. ds R" ""
+. ds C` ""
+. ds C' ""
+'br\}
+.el\{\
+. ds -- \|\(em\|
+. ds PI \(*p
+. ds L" ``
+. ds R" ''
+. ds C`
+. ds C'
+'br\}
+.\"
+.\" Escape single quotes in literal strings from groff's Unicode transform.
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.\"
+.\" If the F register is >0, we'll generate index entries on stderr for
+.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index
+.\" entries marked with X<> in POD. Of course, you'll have to process the
+.\" output yourself in some meaningful fashion.
+.\"
+.\" Avoid warning from groff about undefined register 'F'.
+.de IX
+..
+.nr rF 0
+.if \n(.g .if rF .nr rF 1
+.if (\n(rF:(\n(.g==0)) \{\
+. if \nF \{\
+. de IX
+. tm Index:\\$1\t\\n%\t"\\$2"
+..
+. if !\nF==2 \{\
+. nr % 0
+. nr F 2
+. \}
+. \}
+.\}
+.rr rF
+.\"
+.\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2).
+.\" Fear. Run. Save yourself. No user-serviceable parts.
+. \" fudge factors for nroff and troff
+.if n \{\
+. ds #H 0
+. ds #V .8m
+. ds #F .3m
+. ds #[ \f1
+. ds #] \fP
+.\}
+.if t \{\
+. ds #H ((1u-(\\\\n(.fu%2u))*.13m)
+. ds #V .6m
+. ds #F 0
+. ds #[ \&
+. ds #] \&
+.\}
+. \" simple accents for nroff and troff
+.if n \{\
+. ds ' \&
+. ds ` \&
+. ds ^ \&
+. ds , \&
+. ds ~ ~
+. ds /
+.\}
+.if t \{\
+. ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u"
+. ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u'
+. ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u'
+. ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u'
+. ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u'
+. ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u'
+.\}
+. \" troff and (daisy-wheel) nroff accents
+.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V'
+.ds 8 \h'\*(#H'\(*b\h'-\*(#H'
+.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#]
+.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H'
+.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u'
+.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#]
+.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#]
+.ds ae a\h'-(\w'a'u*4/10)'e
+.ds Ae A\h'-(\w'A'u*4/10)'E
+. \" corrections for vroff
+.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u'
+.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u'
+. \" for low resolution devices (crt and lpr)
+.if \n(.H>23 .if \n(.V>19 \
+\{\
+. ds : e
+. ds 8 ss
+. ds o a
+. ds d- d\h'-1'\(ga
+. ds D- D\h'-1'\(hy
+. ds th \o'bp'
+. ds Th \o'LP'
+. ds ae ae
+. ds Ae AE
+.\}
+.rm #[ #] #H #V #F C
+.\" ========================================================================
+.\"
+.IX Title "SBOPKGLINT 1"
+.TH SBOPKGLINT 1 "2022-04-04" "0.4" "sbo-maintainer-tools"
+.\" For nroff, turn off justification. Always turn off hyphenation; it makes
+.\" way too many mistakes in technical documents.
+.if n .ad l
+.nh
+.SH "NAME"
+sbopkglint \- check Slackware binary packages for common errrors.
+.SH "SYNOPSIS"
+.IX Header "SYNOPSIS"
+\&\fBsbopkglint\fR [\-k] [\-i] [package.t?z ...]
+.SH "DESCRIPTION"
+.IX Header "DESCRIPTION"
+\&\fBsbopkglint\fR installs a Slackware package to a temporary directory, then
+examines the contents. It finds lots of common problems that aren't
+always noticed by SBo script maintainers or the admins.
+.PP
+This is for built packages. If you want to lint your build scripts,
+use \fBsbolint\fR(1) instead.
+.PP
+With no package arguments, it looks for a SlackBuild in the current
+directory, extracts the \s-1PRINT_PACKAGE_NAME\s0 information, and tries
+to find a package in \f(CW$OUTPUT\fR (/tmp by default). If found, it checks
+that package. It's up to you to know whether the package needs to be
+rebuilt (e.g. if you've edited the SlackBuild since the package was
+built).
+.PP
+With arguments, it checks the given packages. These must be
+supported Slackware package files (.tgz, .txz, .tlz, etc). There's no
+requirement that these have to be SBo packages, but a couple of the
+tests (e.g. the check for \f(CW$PRGNAM\fR.SlackBuild in the doc dir) might not
+apply to non-SBo builds.
+.PP
+Diagnostics will be logged to stdout and stderr. Exit status will
+be 0 if all tests passed, non-zero otherwise.
+.PP
+This script must run as root. If you run it as a normal user, it tries
+to re-execute itself via \fBsudo\fR\|(8).
+.SH "OPTIONS"
+.IX Header "OPTIONS"
+.IP "\fB\-k\fR" 4
+.IX Item "-k"
+Keep the temporary package install directory instead of deleting it on exit.
+.IP "\fB\-i\fR" 4
+.IX Item "-i"
+Disable the \*(L"useless-looking install instructions\*(R" test. This is
+intended for SBo admins mass-linting a ton of packages. \s-1INSTALL\s0 in
+the doc dir is something that exists in thousands of existing builds,
+and it's not a major problem. New builds and updates should be linted
+without this option, however.
+.IP "\fB\-\-help\fR" 4
+.IX Item "--help"
+Show the short built-in help.
+.IP "\fB\-\-doc\fR" 4
+.IX Item "--doc"
+View this documentation using \fBperldoc\fR\|(1), which generally uses your
+pager (e.g. \fBless\fR\|(1) or \fBmore\fR\|(1)) to display it.
+.IP "\fB\-\-man\fR" 4
+.IX Item "--man"
+Convert this documentation to a man page, on stdout.
+.SH "EXIT STATUS"
+.IX Header "EXIT STATUS"
+0 (success) if all tests passed for all packages, non-zero if there
+were any test failures (or if installpkg failed for some reason).
+.SH "FILES"
+.IX Header "FILES"
+.IP "\fBsbolint.d/*.t.sh\fR" 4
+.IX Item "sbolint.d/*.t.sh"
+These are the actual tests. They're installed to
+\&\fI\s-1PREFIX\s0\fR/share/sbo\-maintainer\-tools, and they're sourced by
+\&\fBsbopkglint\fR at runtime. Each test script begins with (hopefully)
+useful comments that go into more detail than the documentation here.
+.SH "TESTS"
+.IX Header "TESTS"
+.SS "basic-sanity"
+.IX Subsection "basic-sanity"
+.IP "\fB\-\fR" 4
+.IX Item "-"
+Top-level directories inside the package must be recognized ones,
+such as /bin /etc /usr /opt. Packages shouldn't be installing files
+in /tmp, /dev, or /home... and they really shouldn't be inventing
+new top-level directories.
+.IP "\fB\-\fR" 4
+.IX Item "-"
+The documentation directory must exist and be correctly named, as
+/usr/doc/$PRGNAM\-$VERSION. It must contain \f(CW$PRGNAM\fR.SlackBuild, too.
+.IP "\fB\-\fR" 4
+.IX Item "-"
+The directories /usr/local, /usr/share/doc, /usr/share/man, /usr/etc
+are not allowed in SBo packages.
+.IP "\fB\-\fR" 4
+.IX Item "-"
+Some directories (e.g. /usr/bin) may not contain subdirectories.
+.IP "\fB\-\fR" 4
+.IX Item "-"
+Some directories (e.g. /usr/share) must *only* contain subdirectories.
+.IP "\fB\-\fR" 4
+.IX Item "-"
+Some directories (e.g. /usr/man, /usr/share/applications) must not
+contain files with executable permissions. /usr/doc is not in this
+list; neither is /etc (too many existing packages install +x files
+there).
+.IP "\fB\-\fR" 4
+.IX Item "-"
+Broken symlinks may not exist.
+.IP "\fB\-\fR" 4
+.IX Item "-"
+Absolute symlinks may not exist (they should be converted to
+relative symlinks). This may seem like nitpicking, but packages
+may be installed somewhere besides / (the root dir) with the
+\&\-root option to installpkg. If /usr/bin/foo is a link to /usr/bin/bar,
+it should be a link to just bar.
+.SS "docs"
+.IX Subsection "docs"
+.IP "\fB\-\fR" 4
+.IX Item "-"
+Documentation must be installed to /usr/doc/$PRGNAM\-$VERSION. If
+there's any other directory under /usr/doc, it's incorrect. Some
+builds use mis-named doc directories (if it hasn't been fixed by
+now, an example is gcc5, which installs docs to /usr/doc/gcc\-$VERSION
+when it should be gcc5\-$VERSION).
+.IP "\fB\-\fR" 4
+.IX Item "-"
+Documentation files must be readable by everyone, and owned by root:root.
+.IP "\fB\-\fR" 4
+.IX Item "-"
+Doc dir shouldn't contain empty files (0 bytes in length).
+.IP "\fB\-\fR" 4
+.IX Item "-"
+Doc dir shouldn't contain install instructions. Specifically, files
+named \s-1INSTALL, INSTALL\s0.*, or install.txt are flagged (it's impossible
+to make this test 100% perfect).
+.SS "noarch"
+.IX Subsection "noarch"
+.IP "\fB\-\fR" 4
+.IX Item "-"
+If a package has its architecture set to \*(L"noarch\*(R", it must not contain
+any \s-1ELF\s0 binaries/libraries.
+.SS "arch"
+.IX Subsection "arch"
+.IP "\fB\-\fR" 4
+.IX Item "-"
+If a package has its architecture set to i?86 or x86_64, all \s-1ELF\s0
+binaries/libraries must be for the correct arch (no 32\-bit code in
+64\-bit packages, and vice versa).
+.IP "\fB\-\fR" 4
+.IX Item "-"
+If a package is i?86, it must not contain /usr/lib64.
+.IP "\fB\-\fR" 4
+.IX Item "-"
+If a package is x86_64 and contains shared libraries, they must be
+in /lib64 or /usr/lib64 (not /lib or /usr/lib).
+.SS "lafiles"
+.IX Subsection "lafiles"
+.IP "\fB\-\fR" 4
+.IX Item "-"
+Packages are no longer allowed to contain libtool archive files (.la)
+in /lib, /lib64, /usr/lib, or /usr/lib64. However, subdirectories
+such as /usr/lib64/someprogram/ are not checked, since some applications
+which use plugins actually use the .la files.
+.SS "manpages"
+.IX Subsection "manpages"
+.IP "\fB\-\fR" 4
+.IX Item "-"
+All man pages must be readable by everyone, and owned by root:root.
+.IP "\fB\-\fR" 4
+.IX Item "-"
+All man pages must be gzipped.
+.IP "\fB\-\fR" 4
+.IX Item "-"
+All man pages must be in /usr/man/man[1\-9n] or /usr/man/<lang>/man[1\-9n].
+.IP "\fB\-\fR" 4
+.IX Item "-"
+Man page directories must be mode 755, owned by root:root.
+.IP "\fB\-\fR" 4
+.IX Item "-"
+The section numbers in man page filenames must match the section number
+in the directory name (e.g. /usr/man/man1/ls.1.gz is \s-1OK,\s0
+/usr/man/man1/tetris.6.gz is an error).
+.IP "\fB\-\fR" 4
+.IX Item "-"
+Man pages must actually be man pages (troff markup).
+.SS "desktop"
+.IX Subsection "desktop"
+.IP "\fB\-\fR" 4
+.IX Item "-"
+If there are .desktop files, doinst.sh must run update-desktop-database.
+.IP "\fB\-\fR" 4
+.IX Item "-"
+\&.desktop files must be mode 644, owned by root:root. Slackware's \s-1KDE\s0
+packages actually break this rule (they install executable .desktop
+files), but SBo packages are not allowed to.
+.IP "\fB\-\fR" 4
+.IX Item "-"
+Only .desktop files are allowed in /usr/share/applications.
+.IP "\fB\-\fR" 4
+.IX Item "-"
+\&.desktop files must be valid, according to the desktop-file-validate
+command. Only actual errors count; warnings don't cause this test to
+fail.
+.SS "newconfig"
+.IX Subsection "newconfig"
+.IP "\fB\-\fR" 4
+.IX Item "-"
+Any files (outside of /usr/doc) with names ending in .new are flagged.
+This might be a bit too restrictive (possibly only check /etc and
+/usr/share?)
+.SS "doinst"
+.IX Subsection "doinst"
+.IP "\fB\-\fR" 4
+.IX Item "-"
+If there are icons in /usr/share/icons, .desktop files in /usr/share/applications,
+or glib2 schemas in /usr/share/glib\-2.0/schemas, there must be a doinst.sh
+with appropriate command(s), e.g. update-desktop-database, gtk-update-icon-cache,
+glib-compile-schemas.
+.SH "BUGS"
+.IX Header "BUGS"
+Probably many. This is still a work in progress.
+.PP
+One known problem is that the same file can fail multiple tests. E.g.
+if you have a man page that's installed executable, it will fail both
+the basic-sanity test and the manpages test. This isn't really a huge
+problem, so it might not be fixed any time soon.
+.SH "AUTHOR"
+.IX Header "AUTHOR"
+B. Watson <urchlay@slackware.uk>, \s-1AKA\s0 Urchlay on Libera \s-1IRC.\s0
+.SH "SEE ALSO"
+.IX Header "SEE ALSO"
+\&\fBsbolint\fR(1)
diff --git a/sbopkglint.d/05-basic-sanity.t.sh b/sbopkglint.d/05-basic-sanity.t.sh
new file mode 100644
index 0000000..6709203
--- /dev/null
+++ b/sbopkglint.d/05-basic-sanity.t.sh
@@ -0,0 +1,152 @@
+#!/bin/sh
+
+# sbopkglint test, must be sourced by sbopkglint (not run standalone).
+
+# PKG, PRGNAM, VERSION, ARCH are set by sbopkglint. also the current
+# directory is the root of the installed package tree.
+
+#######################################################################
+# these directories are allowed to exist in the package, but they
+# must be mode 0755 and owned by root:root. if a dir from this list
+# exists but is empty, that's an error. if a top-level directory
+# exists that's *not* in this list (such as /dev), that's an error.
+topleveldirs="bin boot etc lib lib64 opt sbin srv usr var run"
+
+# these directories are *required* to exist, and must be mode 0755, root:root.
+# if a dir from this list exists but is empty, that's an error. note
+# that the install/ dir no longer exists by the time we run (installpkg
+# deleted it already).
+requireddirs="usr/doc/$PRGNAM-$VERSION"
+
+# these directories *must not* exist. no need to list top-level dirs here,
+# the topleveldirs check already catches those.
+baddirs="usr/local usr/share/doc usr/share/man usr/etc usr/share/info usr/X11 usr/X11R6"
+
+# these directories may exist, but must contain only files or symlinks,
+# and must be mode 0755, root:root. I thought usr/share/pixmaps
+# belonged here, but quite a few packages create subdirs there for
+# images required at runtime that aren't the app icon.
+fileonlydirs="bin usr/bin sbin usr/sbin"
+
+# these directories may exist, but must contain only subdirectories
+# (no files, symlinks, devices, etc). "." (the top-level package dir)
+# doesn't need to be included here; it's checked separately.
+nofiledirs="usr usr/doc usr/share usr/man"
+
+# these directories may exist but must not have executable files
+# anywhere under them. I would put usr/doc and etc here, but too many
+# packages break that rule. usr/share/applications is listed here,
+# even though Slackware's KDE packages (erroneously) install .desktop
+# files +x.
+noexecdirs="usr/man usr/share/pixmaps usr/share/icons usr/share/applications usr/share/appdata usr/share/mime usr/share/mime-info usr/share/glib-2.0"
+
+# these files must exist.
+requiredfiles="usr/doc/$PRGNAM-$VERSION/$PRGNAM.SlackBuild"
+
+# these files must not exist.
+badfiles="\
+usr/info/dir \
+usr/info/dir.gz \
+usr/lib64/perl5/perllocal.pod \
+usr/lib/perl5/perllocal.pod \
+usr/share/perl5/perllocal.pod \
+usr/share/perl5/vendor_perl/perllocal.pod \
+etc/passwd \
+etc/passwd.new \
+etc/shadow \
+etc/shadow.new \
+etc/group \
+etc/group.new \
+etc/gshadow \
+etc/gshadow.new \
+etc/ld.so.conf"
+
+#######################################################################
+
+# include 'hidden' files/dirs in * wildcard expansion.
+shopt -s dotglob
+
+dir_ok() {
+ [ -d "$1" ] && \
+ [ "$( stat -c '%A %U %G' "$1" )" = "drwxr-xr-x root root" ]
+}
+
+dir_empty() {
+ [ "$( find "$1" -mindepth 1 -maxdepth 1 )" = "" ]
+}
+
+warn_badperms() {
+ warn "bad permissions/owner (should be 0755 root:root): $1"
+}
+
+for i in *; do
+ if [ ! -d "$i" ]; then
+ warn "package root dir contains non-directory: $i"
+ elif ! echo "$topleveldirs" | grep -q "\\<$i\\>"; then
+ warn "package root dir contains non-standard directory: $i"
+ elif ! dir_ok "$i"; then
+ warn_badperms "$i"
+ elif dir_empty "$i"; then
+ warn "package contains empty top-level directory: $i"
+ fi
+done
+
+for i in $requireddirs; do
+ if [ ! -d "$i" ]; then
+ warn "missing required directory: $i"
+ elif ! dir_ok "$i"; then
+ warn_badperms "$i"
+ fi
+done
+
+for i in $baddirs; do
+ if [ -d "$i" ]; then
+ warn "forbidden directory exists: $i"
+ elif [ -e "$i" ]; then
+ warn "forbidden directory exists as a non-directory: $i"
+ fi
+done
+
+for i in $fileonlydirs; do
+ [ -d "$i" ] || continue
+ dir_ok "$i" || warn_badperms "$i"
+ badstuff="$( find -L "$i" -mindepth 1 -maxdepth 1 \! -type f )"
+ [ -n "$badstuff" ] && warn "$i should only contain files, not:" && ls -ld $badstuff
+done
+
+for i in $nofiledirs; do
+ [ -d "$i" ] || continue
+ dir_ok "$i" || warn_badperms "$i"
+ badstuff="$( find -L "$i" -mindepth 1 -maxdepth 1 \! -type d )"
+ [ -n "$badstuff" ] && warn "$i should only contain directories, not:" && ls -ld $badstuff
+done
+
+for i in $requiredfiles; do
+ [ -f "$i" ] || warn "missing required file: $i"
+done
+
+for i in $noexecdirs; do
+ [ -d "$i" ] || continue
+ found="$( find "$i" -type f -a -perm /0111 )"
+ if [ -n "$found" ]; then
+ warn "$i should not contain files with executable permission:"
+ ls -l $found
+ fi
+done
+
+for i in $badfiles; do
+ [ -e "$i" ] && warn "forbidden file: $i"
+done
+
+badlinks="$( find -L . -type l )"
+[ -n "$badlinks" ] && for i in $badlinks; do
+ target="$( readlink "$i" )"
+ case "$target" in
+ /*) abslinks+="$i " ;;
+ *) brokenlinks+="$i " ;;
+ esac
+done
+
+[ -n "$abslinks" ] && warn "package contains absolute symlinks (should be relative):" && ls -ld $abslinks
+[ -n "$brokenlinks" ] && warn "package contains broken symlinks:" && ls -ld $brokenlinks
+
diff --git a/sbopkglint.d/10-docs.t.sh b/sbopkglint.d/10-docs.t.sh
new file mode 100644
index 0000000..e1bb8d8
--- /dev/null
+++ b/sbopkglint.d/10-docs.t.sh
@@ -0,0 +1,40 @@
+#!/bin/sh
+
+# sbopkglint test, must be sourced by sbopkglint (not run standalone).
+
+# PKG, PRGNAM, VERSION, ARCH are set by sbopkglint. also the current
+# directory is the root of the installed package tree.
+
+########################################################################
+# checks file permissions and ownership in the package doc dir. files
+# should all be mode 644, directories should be 755. everything should
+# be owned by root:root. also checks for empty files or (possibly)
+# install instructions.
+
+
+# ideally, we'd require all files under the doc dir to be mode 0644.
+# however, too many existing packages (including core Slackware ones)
+# break that rule. so check for the minimum set of desired permissions:
+# a doc file should be readable by all users (at least 444).
+
+DOCDIR=usr/doc/$PRGNAM-$VERSION
+
+# existence of the doc dir was already checked by a previous test,
+# so just don't do anything if it's missing.
+
+if [ -d "$DOCDIR" ]; then
+ badpermfiles="$( find $DOCDIR -mindepth 1 -type f -a \! -perm -444 )"
+ badpermdirs="$( find $DOCDIR -mindepth 1 -type d -a \! -perm 0755 )"
+ badowners="$( find $DOCDIR -mindepth 1 -user root -a -group root -o -print )"
+ empty="$( find $DOCDIR -mindepth 1 -empty )"
+ bogus="$( find $DOCDIR -mindepth 1 -maxdepth 1 -type f -a \( -name INSTALL -o -name INSTALL.\* \) )"
+
+ [ -n "$badpermfiles" ] && warn "bad file perms (should be 644, or at least 444) in doc dir:" && ls -l $badpermfiles
+ [ -n "$badpermdirs" ] && warn "bad directory perms (should be 755) in doc dir:" && ls -ld $badpermdirs
+ [ -n "$badowners" ] && warn "bad ownership (should be root:root) in doc dir:" && ls -ld $badowners
+ [ -n "$empty" ] && warn "empty files/dirs in doc dir: $empty"
+ [ -n "$bogus" ] && [ -z "$INSTALL_DOCS_OK" ] && warn "useless-looking install instructions in doc dir: $bogus"
+fi
+
+baddocs="$( find usr/doc -mindepth 1 -maxdepth 1 \! -name $PRGNAM-$VERSION )"
+[ -n "$baddocs" ] && warn "docs outside of $DOCDIR:" && ls -ld $baddocs
diff --git a/sbopkglint.d/15-noarch.t.sh b/sbopkglint.d/15-noarch.t.sh
new file mode 100644
index 0000000..55c17f6
--- /dev/null
+++ b/sbopkglint.d/15-noarch.t.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+# sbopkglint test, must be sourced by sbopkglint (not run standalone).
+
+# PKG, PRGNAM, VERSION, ARCH are set by sbopkglint. also the current
+# directory is the root of the installed package tree.
+
+########################################################################
+# makes sure "noarch" packages really are noarch.
+
+if [ "$ARCH" = "noarch" ]; then
+ elfbins="$( find * -type f -print0 | xargs -0 file -m /etc/file/magic/elf | grep ELF | cut -d: -f1 )"
+ [ -n "$elfbins" ] && warn "package claims to be noarch, but contains ELF binaries:" && ls -l $elfbins
+fi
diff --git a/sbopkglint.d/20-arch.t.sh b/sbopkglint.d/20-arch.t.sh
new file mode 100644
index 0000000..81a91a4
--- /dev/null
+++ b/sbopkglint.d/20-arch.t.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+
+# sbopkglint test, must be sourced by sbopkglint (not run standalone).
+
+# PKG, PRGNAM, VERSION, ARCH are set by sbopkglint. also the current
+# directory is the root of the installed package tree.
+
+########################################################################
+# for noarch packages, do nothing.
+# for everything else, make sure any ELF binaries/libraries match the
+# ARCH, and that libs are in the correct directory (lib vs. lib64).
+
+# warnings:
+# if an i?86 package has any 64-bit ELF objects (libs or bins)
+# if an x86_64 package has any 32-bit ELF objects (libs or bins)
+# if an i?86 package has lib64 or usr/lib64 at all
+# if an x86_64 package has 64-bit libs in lib or usr/lib
+
+# note: sometimes files in /lib/firmware are ELF, and would cause
+# false "wrong directory" warnings, so we exclude that dir from the
+# search.
+
+case "$ARCH" in
+ noarch) ;; # ok, do nothing.
+ i?86) WRONGDIR="lib64"; CPU="80386" ;;
+ x86_64) WRONGDIR="lib"; CPU="x86-64" ;;
+ *) warn "ARCH isn't noarch, i?86, or x86_64. don't know how to check binaries." ;;
+esac
+
+INWRONGDIR=""
+WRONGARCH=""
+NOTSTRIPPED=""
+if [ -n "$WRONGDIR" ]; then
+ find * -type f -print0 | \
+ xargs -0 file -m /etc/file/magic/elf | \
+ grep 'ELF.*\(executable\|shared object\)' > .tmp.$$
+
+ while read line; do
+ file="$( echo $line | cut -d: -f1 )"
+ filetype="$( echo $line | cut -d: -f2 )"
+ case "$file" in
+ lib/firmware/*) continue ;;
+ $WRONGDIR/*|usr/$WRONGDIR/*)
+ INWRONGDIR+="$file " ;;
+ esac
+
+ # 64-bit packages can contain 2 types of 32-bit binaries:
+ # - statically linked.
+ # - statified. very few of these exist, and we can't make
+ # them on 15.0 (statifier can't handle modern kernel/glibc
+ # and the author hasn't updated it).
+ if [ "$ARCH" = "x86_64" ]; then
+ echo "$filetype" | grep -q 'statically linked' && continue
+ grep -q DL_RO_DYN_TEMP_CNT "$file" && continue
+ fi
+
+ if ! echo "$filetype" | grep -q "$CPU"; then
+ WRONGARCH+="$file "
+ fi
+ if echo "$filetype" | grep -q "not stripped"; then
+ NOTSTRIPPED+="$file "
+ fi
+ done < .tmp.$$
+ rm -f .tmp.$$
+fi
+
+[ -n "$INWRONGDIR" ] && warn "shared lib(s) in wrong dir for ARCH:" && ls -l $INWRONGDIR
+[ -n "$WRONGARCH" ] && warn "ELF object(s) with wrong arch (should be $CPU):" && ls -l $WRONGARCH
+[ -n "$NOTSTRIPPED" ] && warn "ELF object(s) not stripped:" && ls -l $NOTSTRIPPED
diff --git a/sbopkglint.d/25-lafiles.t.sh b/sbopkglint.d/25-lafiles.t.sh
new file mode 100644
index 0000000..d4c56f7
--- /dev/null
+++ b/sbopkglint.d/25-lafiles.t.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+# sbopkglint test, must be sourced by sbopkglint (not run standalone).
+
+# PKG, PRGNAM, VERSION, ARCH are set by sbopkglint. also the current
+# directory is the root of the installed package tree.
+
+########################################################################
+# check for .la files, according to the Slackware 15.0 guidelines.
+# they're not allowed directly in /lib /lib64 /usr/lib /usr/lib64, but
+# they're OK in subdirectories (e.g. /usr/lib64/appname/plugins/foo.la).
+
+for i in lib lib64 usr/lib usr/lib64; do
+ [ -d "$i" ] || continue
+ found="$( find $i -maxdepth 1 -name '*.la' )"
+ [ -n "$found" ] && LAFILES+="$found "
+done
+
+if [ -n "$LAFILES" ]; then
+ warn "package contains .la files:"
+ ls -l $LAFILES
+fi
diff --git a/sbopkglint.d/30-manpages.t.sh b/sbopkglint.d/30-manpages.t.sh
new file mode 100644
index 0000000..e0978b3
--- /dev/null
+++ b/sbopkglint.d/30-manpages.t.sh
@@ -0,0 +1,110 @@
+#!/bin/sh
+
+# sbopkglint test, must be sourced by sbopkglint (not run standalone).
+
+# PKG, PRGNAM, VERSION, ARCH are set by sbopkglint. also the current
+# directory is the root of the installed package tree.
+
+#######################################################################
+# if the package contains the usr/man dir, make sure that:
+# all the dirs match usr/man/man[1-9n] or usr/man/<locale>/man[1-9n].
+# all the files under /usr/man are gzipped and end in <section>.gz.
+# all the files under /usr/man have mode 0644. executable man pages
+# don't make sense, and man pages that aren't world-readable are
+# annoying and wrong.
+# all the filenames match the sections (e.g. usr/man/man1/foo.1.gz
+# matches, usr/man/man1/bar.2.gz doesn't).
+# all the gzipped files look like they contain *roff.
+
+BADPERMS=""
+BADDIRPERMS=""
+BADDIRS=""
+BADNAMES=""
+NOTGZIPPED=""
+NONTROFF=""
+WRONGSECT=""
+
+check_gzipped_page() {
+ local f="$1"
+ local d="$( dirname $f )"
+ local s="$( stat -c '%a %U %G' "$f" )"
+
+ if [ "$s" = "444 root root" ]; then
+ : # we could warn here someday
+ elif [ "$s" != "644 root root" ]; then
+ BADPERMS+="$f "
+ fi
+
+ if [ "$( file -L -b --mime-type "$f" )" != "application/gzip" ]; then
+ NOTGZIPPED+="$f "
+ fi
+
+ # I have ~42,000 man pages on my dev box, file(1) fails to identify
+ # 12 of them as troff, but adding the check for .T catches them all.
+ if [ "$( file -z -m /etc/file/magic/troff -L -b --mime-type "$f" )" != "text/troff" ]; then
+ if ! zgrep -q '^\.T' "$f"; then
+ NONTROFF+="$f "
+ fi
+ fi
+
+ # checking the section is tricky because e.g. /usr/man/man1 may contain
+ # files with with names like .1.gz, .1x.gz, .1.pm.gz.
+
+ # if the section in the directory name is bad, don't complain here, it
+ # was already reported.
+ dir_s="$( echo "$d" | sed -n 's,.*man\([0-9n]\)$,\1,p' )"
+ if [ "$dir_s" = "" ]; then
+ return
+ fi
+
+ s="$( basename "$f" | sed -n 's,.*\.\([0-9n]\)[^.]*\.gz,\1,p' )"
+ if [ "$s" = "" ]; then
+ BADNAMES+="$f "
+ elif [ "$s" != "$dir_s" ]; then
+ WRONGSECT+="$f "
+ fi
+}
+
+# called for paths like /usr/man/de. right now, it accepts names like
+# xx_XX or xx.UTF-8, or actually anything whose first 2 characters match
+# one of the dirs in /usr/share/locale. could use some refining. I don't
+# even know if the *.UTF-8 or *.ISO8859-1 dirs get searched by man-db.
+# Note that slackware's own libcdio-paranoia install man pages in
+# /usr/man/jp, which is invalid.
+check_locale_dir() {
+ l="$( echo "$1" | sed 's,^.*/\(..\).*$,\1,' )"
+ [ -e /usr/share/locale/"$l" ] || warn "bad locale dir in /usr/man: $l"
+}
+
+if [ -d usr/man ]; then
+ find usr/man -type f > .manpages.$$
+ find usr/man -mindepth 1 -type d > .mandirs.$$
+
+ while read d; do
+ case "$d" in
+ usr/man/man[1-9n]|usr/man/*/man[1-9n])
+ [ "$( stat -c '%a %U %G' "$d" )" != "755 root root" ] && BADDIRPERMS+="$d "
+ ;;
+ usr/man/??*) check_locale_dir "$d" ;;
+ *) BADDIRS+="$d " ;;
+ esac
+ done < .mandirs.$$
+
+ while read f; do
+ case "$f" in
+ *.gz) check_gzipped_page "$f" ;;
+ *) BADNAMES+="$f " ;;
+ esac
+ done < .manpages.$$
+
+ rm -f .manpages.$$ .mandirs.$$
+
+ [ -n "$BADPERMS" ] && warn "bad man page owner/permissions (should be 0644, root:root)" && ls -ld $BADPERMS
+ [ -n "$BADDIRPERMS" ] && warn "bad man directory owner/permissions (should be 0755, root:root)" && ls -ld $BADDIRPERMS
+ [ -n "$BADDIRS" ] && warn "bad directory names in /usr/man:" && ls -ld $BADDIRS
+ [ -n "$BADNAMES" ] && warn "bad man page names (not *.gz):" && ls -ld $BADNAMES
+ [ -n "$NOTGZIPPED" ] && warn "non-gzip (but named *.gz) man pages:" && ls -ld $NOTGZIPPED
+ [ -n "$NONTROFF" ] && warn "invalid man pages (not troff):" && ls -ld $NONTROFF
+ [ -n "$WRONGSECT" ] && warn "man pages in wrong section:" && ls -ld $WRONGSECT
+fi
+
diff --git a/sbopkglint.d/35-desktop.t.sh b/sbopkglint.d/35-desktop.t.sh
new file mode 100644
index 0000000..9650ef4
--- /dev/null
+++ b/sbopkglint.d/35-desktop.t.sh
@@ -0,0 +1,35 @@
+#!/bin/sh
+
+# sbopkglint test, must be sourced by sbopkglint (not run standalone).
+
+# PKG, PRGNAM, VERSION, ARCH are set by sbopkglint. also the current
+# directory is the root of the installed package tree.
+
+#######################################################################
+# if the package contains any files in /usr/share/applications/, they
+# must be named *.desktop, must pass desktop-file-validate, and must
+# be mode 644, owner root:root.
+
+BADPERMS=""
+BADDESKTOP=""
+NONDESKTOP=""
+if [ -d usr/share/applications ]; then
+ # doinst.sh creates this, don't check here now that we have a doinst test.
+ #[ -e "usr/share/applications/mimeinfo.cache" ] || warn "doinst.sh is missing update-desktop-database"
+
+ for f in usr/share/applications/*; do
+ [ -e "$f" ] || continue
+
+ [ "$f" = "usr/share/applications/mimeinfo.cache" ] && continue
+
+ [ "$( stat -Lc '%a %U %G' "$f" )" = "644 root root" ] || BADPERMS+="$f "
+ case "$f" in
+ *.desktop) desktop-file-validate "$f" || BADDESKTOP+="$f " ;;
+ *) NONDESKTOP+="$f " ;;
+ esac
+ done
+
+ [ -n "$BADPERMS" ] && warn "bad permissions/owner on .desktop files (should be 0644 root:root):" && ls -ld $BADPERMS
+ [ -n "$BADDESKTOP" ] && warn ".desktop files fail to validate:" && ls -ld $BADDESKTOP
+ [ -n "$NONDESKTOP" ] && warn "unknown file (not .desktop) in desktop dir:" && ls -ld $NONDESKTOP
+fi
diff --git a/sbopkglint.d/40-newconfig.t.sh b/sbopkglint.d/40-newconfig.t.sh
new file mode 100644
index 0000000..ee0aec5
--- /dev/null
+++ b/sbopkglint.d/40-newconfig.t.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+# sbopkglint test, must be sourced by sbopkglint (not run standalone).
+
+# PKG, PRGNAM, VERSION, ARCH are set by sbopkglint. also the current
+# directory is the root of the installed package tree.
+
+#######################################################################
+# check for .new config files. there shouldn't be any, because when
+# sbopkglint installed the package, it ran the doinst.sh, which
+# should have renamed them.
+
+NEWFILES=""
+for f in "$( find -type f -name \*.new )"; do
+ [ -z "$f" ] && continue
+ case "$f" in
+ ./usr/doc/*) continue ;;
+ esac
+ NEWFILES+="$f "
+done
+
+[ -n "$NEWFILES" ] && warn "doinst.sh doesn't handle .new config files:" && ls -l $NEWFILES
diff --git a/sbopkglint.d/45-doinst.t.sh b/sbopkglint.d/45-doinst.t.sh
new file mode 100644
index 0000000..ae47c72
--- /dev/null
+++ b/sbopkglint.d/45-doinst.t.sh
@@ -0,0 +1,53 @@
+#!/bin/sh
+
+# sbopkglint test, must be sourced by sbopkglint (not run standalone).
+
+# PKG, PRGNAM, VERSION, ARCH are set by sbopkglint. also the current
+# directory is the root of the installed package tree. This test
+# also uses the filename variable.
+
+#######################################################################
+# check the doinst.sh. its job is to generate various files by running
+# e.g. update-desktop-database, which creates a cache file. if the
+# cache file exists, and doinst.sh either doesn't exist, or doesn't
+# contain an update-desktop-database, that's very bad: it means
+# the cache file is actually included in the package. which means,
+# installing such a package would overwrite the user's local cache,
+# breaking his desktop, until he fixes it (either manually or giving
+# up and logging out or rebooting).
+
+doinst=var/lib/pkgtools/scripts/"$( echo $filename | sed 's,\.[^.]*$,,' )"
+
+have_doinst() {
+ [ -e "$doinst" ]
+ return $?
+}
+
+grep_doinst() {
+ have_doinst && grep -q "$@" $doinst
+ return $?
+}
+
+doinst_warn() {
+ local msg="doinst.sh is missing, package needs one, with"
+ have_doinst && msg="doinst.sh exists, but is missing"
+ warn "$msg $@"
+}
+
+doinst_chk_command() {
+ local cmd="$1"
+ grep_doinst "$cmd" || doinst_warn "$cmd"
+}
+
+[ "$( find -L usr/share/icons -type f 2>/dev/null )" != "" ] && \
+ doinst_chk_command "gtk-update-icon-cache"
+
+[ "$( find -L usr/share/applications -type f 2>/dev/null )" != "" ] && \
+ doinst_chk_command "update-desktop-database"
+
+[ "$( find -L usr/share/glib-2.0/schemas -type f 2>/dev/null )" != "" ] && \
+ doinst_chk_command "glib-compile-schemas"
+
+[ "$( find -L usr/share/fonts -type f 2>/dev/null )" != "" ] && \
+ doinst_chk_command "fc-cache"
+