diff options
-rw-r--r-- | Makefile | 41 | ||||
-rw-r--r-- | README | 40 | ||||
-rw-r--r-- | TODO | 44 | ||||
-rw-r--r-- | pre-commit-sbolint | 63 | ||||
-rwxr-xr-x | sbolint | 1408 | ||||
-rw-r--r-- | sbolint.1 | 288 | ||||
-rwxr-xr-x | sbopkglint | 490 | ||||
-rw-r--r-- | sbopkglint.1 | 363 | ||||
-rw-r--r-- | sbopkglint.d/05-basic-sanity.t.sh | 152 | ||||
-rw-r--r-- | sbopkglint.d/10-docs.t.sh | 40 | ||||
-rw-r--r-- | sbopkglint.d/15-noarch.t.sh | 14 | ||||
-rw-r--r-- | sbopkglint.d/20-arch.t.sh | 69 | ||||
-rw-r--r-- | sbopkglint.d/25-lafiles.t.sh | 22 | ||||
-rw-r--r-- | sbopkglint.d/30-manpages.t.sh | 110 | ||||
-rw-r--r-- | sbopkglint.d/35-desktop.t.sh | 35 | ||||
-rw-r--r-- | sbopkglint.d/40-newconfig.t.sh | 22 | ||||
-rw-r--r-- | sbopkglint.d/45-doinst.t.sh | 53 |
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 @@ -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). @@ -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 @@ -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" + |