From 97a7e8464438e173a357ea9a272f91e26a789ac5 Mon Sep 17 00:00:00 2001 From: "B. Watson" Date: Tue, 7 Apr 2015 18:11:06 -0400 Subject: initial commit --- sbodeps | 133 ++++++++ sbodl | 88 +++++ sbolint | 1095 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ sbosearch | 516 +++++++++++++++++++++++++++++ sbosubmit | 124 +++++++ 5 files changed, 1956 insertions(+) create mode 100755 sbodeps create mode 100755 sbodl create mode 100755 sbolint create mode 100755 sbosearch create mode 100755 sbosubmit diff --git a/sbodeps b/sbodeps new file mode 100755 index 0000000..9eeb692 --- /dev/null +++ b/sbodeps @@ -0,0 +1,133 @@ +#!/bin/bash + +# This really is a bash script, please don't change to #!/bin/sh + +SBOROOT=${SBOROOT:-/home/urchlay/slackbuilds/sbo/14.1/} +if [ ! -d $SBOROOT ]; then + SBOROOT=. +fi + +SELF="$( echo "$0" | sed 's,.*/,,' )" + +usage() { + cat </dev/null && echo " (installed)" || echo " (NOT installed)" +} + +find_deps() { + local i + local req + local REQUIRES + local info=$(/bin/ls $SBOROOT/*/$2/$2.info 2>/dev/null | head -1) + + if [ -z "$info" ]; then + echo "$SELF: $2: no such package" 1>&2 + RETVAL=1 + return; + fi + + eval $(grep ^REQUIRES= $info) + if [ "$REQUIRES" = "" ]; then + return + fi + for req in $REQUIRES; do + for i in $(seq 1 $1 ); do + echo -n " " + done + echo -n $req + if [ "$req" = "%README%" ]; then + echo + else + print_installed_status $req + if [ "$QUEUE" = "yes" ]; then + if [ "$IGNORE" = "yes" ] || ! pkg_is_installed $req ; then + echo $1 $req >> $QTMPFILE + fi + fi + find_deps $(( $1 + 1 )) $req + fi + done +} + +while [ "$argsdone" != "yes" ]; do + case "$1" in + ''|-h|-help|--help) usage; exit 0 ;; + -iq|-qi) QUEUE=yes ; IGNORE=yes ; shift ;; + -q) QUEUE=yes ; shift ;; + -i) IGNORE=yes ; shift ;; + -*) echo "Unknown option $1"; usage; exit 1 ;; + *) argsdone=yes ;; + esac +done + +if [ "$QUEUE" = "yes" ]; then + QTMPFILE=${TMP:-/tmp}/sbodeps.$$.$RANDOM + rm -f $QTMPFILE + + # save old stdout to fd 3, suppress stdout since we don't print our tree + exec 3>&1 1>/dev/null +fi + +if [ ! -e "$SBOROOT/system" ]; then + echo "$SELF: path \"$SBOROOT\" doesn't look like an SBo repo, set SBOROOT in environment" 1>&2 + exit 1 +fi + +RETVAL=0 + +for arg; do + echo -n $arg + print_installed_status $arg + [ "$QUEUE" = "yes" ] && ! pkg_is_installed $arg && echo 0 $arg >> $QTMPFILE + find_deps 1 $arg +done + +# The queue temp file contains the same info as the regular +# tree, though less human-readable (depth numbers instead of indents). + +# To turn it into a .sqf, sort it depth-first, and remove any duplicates +# (can't use sort -u or uniq, as dups might occur at different depths & +# thus not be on consecutive lines). + +if [ "$QUEUE" = "yes" -a -e "$QTMPFILE" ]; then + exec 1>&3 # restore old stdout + for q in $(sort -nr $QTMPFILE | cut -d' ' -f2); do + r=seen_$(echo $q | sed 's,[^A-Za-z0-9_],_,g') + s=${!r} # bash indirect variable ref, yay + if [ "$s" != "yes" ]; then + echo $q + eval $r="yes" + fi + done + rm -f $QTMPFILE +fi + +exit $RETVAL diff --git a/sbodl b/sbodl new file mode 100755 index 0000000..724733d --- /dev/null +++ b/sbodl @@ -0,0 +1,88 @@ +#!/bin/bash + +# sbodl, initial public release. + +SELF=$( basename $0 ) + +usage() { + cat < + +Execute $SELF in the directory that contains the .info and .SlackBuild +files. It will use wget to download the source file(s), then check their +md5sums. + +$SELF doesn't take any options itself (except --help), but any options +you pass to it will be passed to wget. +EOF + exit 0 +} + +die() { + echo "$SELF: $*" 2>&1 + exit 1 +} + +# check for our one argument +case "$*" in + -h|-help|-\?|--help) usage ;; +esac + +source ./$( basename $( pwd ) ).info \ + || die "No .info file, are you sure this is a SBo directory? Try '$SELF --help'" + +# This stanza copied from the SBo template for 14.1: +if [ -z "$ARCH" ]; then + case "$( uname -m )" in + i?86) ARCH=i486 ;; + arm*) ARCH=arm ;; + *) ARCH=$( uname -m ) ;; + esac +fi + +if [ "$ARCH" = "x86_64" ]; then + DL=${DOWNLOAD_x86_64:-$DOWNLOAD} + SUM=${MD5SUM_x86_64:-$MD5SUM} +else + DL=$DOWNLOAD + SUM=${MD5SUM} +fi + +if [ -z "$DL" ]; then + die "Bad .info file (no DOWNLOAD= or DOWNLOAD_x86_64=)." +fi + +# save passed-in command line args for use with wget +WGETARGS="$@" + +set $SUM + +for dl in $DL; do + EXTRAWGETARGS="--content-disposition " + case "$dl" in + *sourceforge.net/*|*.sf.net/*) EXTRAWGETARGS="--user-agent wget" ;; + *) ;; + esac + + FILE=$( echo "$dl" | sed 's,.*/,,' ) + + wget $WGETARGS $EXTRAWGETARGS "$dl" || die "Download failed" + + if [ -e "$FILE" ]; then + GOTSUM="$( md5sum "$FILE" | cut -d' ' -f1 )" + if [ "$1" != "$GOTSUM" ]; then + echo "WARN: md5sum doesn't match, expected $1, got $GOTSUM" + else + echo "md5sum matches OK: $1" + fi + else + echo "WARN: can't find downloaded file $FILE" + fi + echo + shift +done diff --git a/sbolint b/sbolint new file mode 100755 index 0000000..0b9c794 --- /dev/null +++ b/sbolint @@ -0,0 +1,1095 @@ +#!/usr/bin/perl -w + +# sbolint, 20141114 bkw + +$VERSION="0.1"; + +# generate man page with: +# pod2man --stderr -r0.1 -s1 -c"SBo Maintainer Tools" sbolint > sbolint.1 + +# 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 [-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. + +=head1 OPTIONS + +=over 4 + +=item B<-q> + +Quiet. Suppresses 'xxx checks out OK' and the total errors/warnings summary. + +=item B<-u> + +URL check. Uses B to make HTTP HEAD requests for the B, +B, and B links. This won't guarantee that +the links are good, but failure means they're definitely bad. + +=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(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 must contain I. Everything else in the archive must be +inside this directory. + +=item - + +Archive must contain I. + +=back + +For all builds: + +=over 4 + +=item - + +The SlackBuild script must exist, with mode 0755, and be a I<#!/bin/sh> +script. + +=item - + +The script must contain the standard variable assignements for PRGNAM, +VERSION, BUILD, and TAG. + +=item - + +I in the script must match I in the .info file. Both must +match the script name (I) and the directory name. + +=item - + +I must match the I in the .info file. + +=item - + +TAG=${TAG:-_SBo} must occur in the script. + +=item - + +The I and I variables must respect the environment. + +=item - + +The script must install the slack-desc in $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 a B command. + +=item - + +README must exist and have mode 0644. + +=item - + +slack-desc must exist and have mode 0644. + +=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, B, or B). + +=item - + +Optionally, .info file URLs can be checked for existence with an HTTP HEAD +request (see the B<-u> option). + +=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). + +=back + +=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 Freenode IRC) + +=head1 SEE ALSO + +B(1), B(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*Remove perllocal.pod and other special files/, + 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() { + +while(@ARGV && ($ARGV[0] =~ /^-/)) { + my $opt = shift; + $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; }; + 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 = ; + chomp for @ARGV; +} + +push @ARGV, "." unless @ARGV; + +for(@ARGV) { + run_checks($_); + $g_errcount += $errcount; + $g_warncount += $warncount; + + if($errcount == 0 and $warncount == 0) { + print "$buildname checks out OK\n" unless $quiet; + } else { + print "$buildname: errors $errcount, warnings $warncount\n"; + } +} + +# print total errs/warns only if >1 build checked +if(!$quiet && @ARGV > 1) { + print "Total errors: $g_errcount\n"; + print "Total warnings: $g_warncount\n" unless $nowarn; +} + +exit ($g_errcount > 0 || (!$nowarn && $g_warncount > 0)); +# } + +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 <> + +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: + +-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. + +See the man page for more details. If you don't have the man page, you +can generate it with: + +pod2man --stderr -s1 -cSBoStuff -r$VERSION /path/to/sbolint > sbolint.1 +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_junkfiles, + ); + + 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; + + check_mode($file, $mode); + + if(open my $fh, "<$file") { + while(<$fh>) { + 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; + return @lines; +} + +sub check_readme { + check_and_read("README", 0644); +} + +# 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; + + 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($info{HOMEPAGE}) || do { + log_warning("$file: HOMEPAGE URL broken?"); + }; + } + + if($info{MD5SUM} =~ /^\s*$/) { + log_error("$file: MD5SUM is missing or blank"); + } 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(@dlurls) { + if(!check_url($_)) { + log_error("$file: $dlkey URL '$_' doesn't look like a valid URL (http, https, or ftp)"); + } + + if($url_head) { + for(@dlurls) { + curl_head_request($_) || do { + log_warning("$file: $dlkey URL '$_' 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"); +} + +# 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/sh 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 is an error + +sub check_script { + my $file = $buildname . ".SlackBuild"; + my @lines = check_and_read($file, 0755); + return unless scalar @lines; + + if($lines[0] !~ /^#/) { + log_error("$file:1: missing or invalid shebang line (should be '#!/bin/sh')"); + } elsif($lines[0] ne "#!/bin/sh") { + log_warning("$file:1: shebang line should be #!/bin/sh (admins always change it to that anyway)"); + } + + my $lineno = 0; + my ($prgnam, $version, $build, $tag, $need_doinst, $slackdesc, $makepkg, $install); + for(@lines) { + $lineno++; + if(/^PRGNAM=(\S+)/) { + $prgnam = $1; + if($1 ne $buildname) { + log_error("$file:$lineno: PRGNAM doesn't match dir name ($1 != $buildname)"); + } + } elsif(/^VERSION=(\S+)/) { + $version = $1; + $version =~ s/^["']|["']$//g; + if(not ($version =~ s/\$\{VERSION:-([^}]+)\}/$1/)) { + log_warning("$file:$lineno: VERSION ignores environment (try VERSION=\${VERSION:-$version}"); + } + 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 = $1; + $build =~ s/^["']|["']$//g; + 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 = $1; + if($tag ne '${TAG:-_SBo}') { + log_error("$file:$lineno: TAG=\${TAG:-_SBo} is required"); + } + } elsif(/^\s*cat\s+\$CWD\/doinst.sh/) { + $need_doinst = $lineno; + } elsif(/^[^#]*slack-desc/) { + $slackdesc = $lineno; + } elsif(/^[^#]*\$PKG\/install/) { + $install = $lineno; + } elsif(/^[^#]*makepkg/) { + $makepkg = $lineno; + } + + if(/[^#]*/) { + log_error("$file:$lineno: copy actual documentation, not "); + } + + my $line = $_; + if(grep { $line =~ /$_/ } @boilerplate) { + log_error("$file:$lineno: template comment should be removed"); + } + } + + 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($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; + }; + /\.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); + } +} diff --git a/sbosearch b/sbosearch new file mode 100755 index 0000000..e4b48d9 --- /dev/null +++ b/sbosearch @@ -0,0 +1,516 @@ +#!/bin/bash + +# This really is a bash script, uses bashisms, don't change the shebang. + +DFLTBROWSER=xdg-open + +SLACKVER=${SLACKVER:-$( cut -d' ' -f2 /etc/slackware-version )} +SLACKVER=${SLACKVER:-14.1} + +SBOROOT=${SBOROOT:-/var/lib/sbopkg/SBo/$SLACKVER} +[ ! -d "$SBOROOT" ] && SBOROOT=. + +SELF=$( echo $0 | sed 's,.*/,,' ) + +usage() { + cat < in the REQUIRES= field (only + exists in repo versions 14.0 and up). +-e email Match email address. +-m maintainer Match MAINTAINER= (real name, not email). +-h homepage Match HOMEPAGE. +-d readmetxt Match text in documentation (the README). +-c category Match builds in . Category names may be abbreviated, + e.g. "sys" for system, "ga" for games. +-k keyword Keyword search (TAGS.txt, includes build names, only exists + in repo versions 14.1 and up). + +Search modifier options: +--all List all builds. Any search options will be ignored. +-a AND all the search options together (default is OR). +-v Invert the search (find builds not matching the criteria). +-i Search for installed packages only. +-u Search for uninstalled packages only. +-x Exact match (for build names only, not -r -e -m -h -d) + +Output options: +-S Print short names (no category, "zdoom" not "games/zdoom"). +-P Print full paths to tarballs. +-X Extract tarballs into current dir. +-I cat .info files for all builds found. +-R cat README files for all builds found. +-B Open SBo repo page in browser, for each build found. +-H Open HOMEPAGE in browser, for each build found. +-C Check installed status (print "installed" or "NOT installed" + for each build found). + +The local repo must be in the current directory, or in the +directory given by the SBOROOT environment variable, which defaults to +/var/lib/sbopkg/SBo/\$SLACKVER. If SLACKVER is not set in the environment, +its value will be extracted from /etc/slackware-version (or set to +14.1, if this file doesn't exist). + +All search arguments are treated as case-insensitive regular expressions. +Required, email, maintainer, and homepage arguments are unanchored, +and can't be anchored with ^ or $. However, you can use \< and \> to +anchor on word boundaries (e.g. "-e \' (actually this will also find +things like compiz-bcop, which have a z at a word boundary). + +Search options can be given more than once, e.g. "-r foo -r bar" means +find builds that require either foo or bar (or, with -a, builds that +require both foo and bar). + +The -B and -H options use the environment variable BROWSER to set +which browser to use. If BROWSER is not set, "$DFLTBROWSER" is used. Be +careful with these: trying to open several hundred (or several thousand) +browser instances will likely eat your machine for breakfast, especially +if firefox is the default browser. Best to run the search without -B or +-H first, to see how many matches you get. +EOF +} + +die() { + if [ -n "$TMPDIR" -a -d "$TMPDIR" ]; then + rm -rf $TMPDIR + fi + echo "$SELF: $@ (try '$SELF --help')" 1>&2 + exit 1 +} + +info() { + echo "$SELF: info: $@" 1>&2 +} + +warn() { + echo "$SELF: warning: $@" 1>&2 +} + +set_input() { + if [ "$mode" = "or" ]; then + INPUT=$ALL + else + INPUT=$RESULTS + fi + : > $OUTPUT +} + +set_output() { + if [ "$mode" = "or" ]; then + cat $OUTPUT >> $RESULTS + else + mv $OUTPUT $RESULTS + fi +} + +info_search() { + set_input + egrep -i -l "^$1=.*$2" $( cat $INPUT ) > $OUTPUT + set_output +} + +# keyword search is complicated by the fact that TAGS.txt doesn't +# store the category. Also, we don't differentiate between the keywords +# and the build name at the start of the line. +keyword_search() { + local pkg + + if [ ! -e $SBOROOT/TAGS.txt ]; then + cat <&2 +$SELF: $SBOROOT/TAGS.txt not found. + +Note that this file only exists for Slackware versions 14.1 and up. We +seem to be searching version $SLACKVER. + +EOF + die "can't do keyword search" + fi + + set_input + egrep -i "$1" $SBOROOT/TAGS.txt | while read line; do + pkg=$( echo "$line" | cut -d: -f1 ) + egrep "/$pkg\.info\$" $INPUT >> $OUTPUT + done + set_output +} + +category_search() { + local categ + set_input + categ=$( cut -d/ -f1 $ALL | sort -u | egrep -i "$1" | head -1 ) + [ -z "$categ" ] && die "fatal: no such category '$1'" + egrep -i "^$categ/" $INPUT > $OUTPUT + set_output +} + +build_search() { + set_input + if [ "$exact" == "1" ]; then + fgrep "$1/$1.info" $INPUT > $OUTPUT + else + egrep -i "/$1[^/]*\.info\$" $INPUT > $OUTPUT + fi + set_output +} + +readme_search() { + local info readme + + set_input + for info in $( cat $INPUT ); do + readme=$( echo $info | sed 's,/[^/]*$,/README,' ) + egrep -q -i "$1" $readme && echo $info >> $OUTPUT + done + set_output +} + +is_installed() { + [ ! -e /var/log/packages/ ] && die "no /var/log/packages, are you sure this is Slackware?" + local pkg="$( echo $1 | cut -d/ -f2 )" + [ ! -e $TMPDIR/installed_pkgs ] && \ + ls /var/log/packages/ | rev | cut -d- -f4- | rev > $TMPDIR/installed_pkgs + fgrep -q -x $pkg $TMPDIR/installed_pkgs + return $? +} + +# works, but slow: +## is_installed() { +## local pkg candidate olddir found=1 +## +## found=1 +## pkg="$( echo $1 | cut -d/ -f2 )" +## olddir="$( pwd )" +## cd /var/log/packages || die "no /var/log/packages, are you sure this is Slackware?" +## +## for candidate in "$( ls $pkg* 2>/dev/null )"; do +## if [ "$pkg" = "$( echo $candidate | rev | cut -d- -f4- | rev )" ]; then +## found=0 +## fi +## done +## +## cd $olddir +## return $found +## } + +# also slow +## is_installed() { +## local pkg +## +## if [ ! -e $TMPDIR/installed_pkgs ]; then +## ls /var/log/packages/ > $TMPDIR/installed_pkgs || \ +## die "no /var/log/packages, are you sure this is Slackware?" +## fi +## +## pkg="$( echo $1 | cut -d/ -f2 )" +## egrep -q "^$pkg"'-[^-]+-[^-]+-[^-]+$' $TMPDIR/installed_pkgs +## return $? +## } +## +## installed_search_backend() { +## local pkg +## set_input +## for pkg in $( cat $INPUT ); do +## eval is_installed $pkg $2 echo $pkg >> $OUTPUT +## done +## set_output +## } +## +## installed_search() { +## installed_search_backend "$1" "&&" +## } +## +## uninstalled_search() { +## installed_search_backend "$1" "||" +## } + +# almost works, fast, might try to fix someday +## installed_search() { +## set_input +## ls /var/log/packages/ | rev | cut -d- -f4- | rev > $TMPDIR/installed_pkgs || \ +## die "no /var/log/packages, are you sure this is Slackware?" +## sort -t / -k 2 $INPUT > $INPUT.tmp +## join -t / -1 2 -o 1.1,1.2 $INPUT.tmp $TMPDIR/installed_pkgs > $OUTPUT +## exit 0 +## set_output +## } + +# works, slow, but not as slow as the 1st try +## installed_search() { +## local -A packages +## local pkg shortpkg +## +## [ ! -e /var/log/packages/ ] && die "no /var/log/packages, are you sure this is Slackware?" +## +## set_input +## echo "got here 1" +## for pkg in $( cat $INPUT ); do +## echo -n "." 1>&2 +## shortpkg=$( expr "$pkg" : '.*/\([^/]*\)/' ) +## #shortpkg=$( echo $pkg | cut -d/ -f2 ) +## packages[$shortpkg]=$pkg +## done +## echo "got here 2" +## ls /var/log/packages/ | rev | cut -d- -f4- | rev | while read pkg; do +## [ -n "${packages[$pkg]}" ] && echo ${packages[$pkg]} >> $OUTPUT +## done +## echo "got here 3" +## set_output +## } + +# lightning fast and works correctly, compared to the commented-out +# attempts above. +installed_search_backend() { + local pkg grepopt="$1" + + [ ! -e /var/log/packages/ ] && die "no /var/log/packages, are you sure this is Slackware?" + + set_input + cut -d/ -f2 $INPUT > $INPUT.shortnames + + # $INPUT.shortnames.found is foo => /foo.info + # fgrep -x means "match entire line only", used to avoid matching e.g. + # zathura when looking for zathura-cb. It's *much* faster than using + # egrep with ^ and $. + ls /var/log/packages/ | rev | cut -d- -f4- | rev | \ + fgrep -x -f $INPUT.shortnames | \ + sed 's,.*,/&.info,' \ + > $INPUT.shortnames.found + + fgrep $grepopt -f $INPUT.shortnames.found $INPUT > $OUTPUT + set_output +} + +installed_search() { + installed_search_backend "" +} + +uninstalled_search() { + installed_search_backend "-v" +} + +invert_results() { + fgrep -v -f $RESULTS $ALL > $OUTPUT + mv $OUTPUT $RESULTS +} + +open_in_browser() { + BROWSER="${BROWSER:-$DFLTBROWSER}" + info "opening URL '$1' with browser '$BROWSER'" + $BROWSER "$1" +} + +print_results() { + if [ "$simpleoutput" = "1" ]; then + cat $OUTPUT + return + fi + + # N.B. don't use 'cat $OUTPUT | while read line' here, console browsers + # don't like having their stdin redirected (especially not links) + for line in $( cat $OUTPUT ) ; do + # -X option + if [ "$extract" = "1" ]; then + tar -C $OLD_PWD -xvf $SBOROOT/$line.tar.gz + fi + + # -P option + if [ "$printpaths" = "1" ]; then + echo -n $SBOROOT/$line.tar.gz + else + # -S option + if [ "$shortoutput" = "1" ]; then + echo -n $( echo $line | cut -d/ -f2 ) + else + echo -n $line + fi + fi + + # -C option + if [ "$checkinstalled" = "1" ]; then + is_installed $line && echo ": installed" || echo ": NOT installed" + else + echo + fi + + # -R option + if [ -n "$readmefiles" ]; then + echo '==>' $line/README + cat $line/README + echo + fi + + # -I option + if [ -n "$infofiles" ]; then + echo '==>' $line/*.info + cat $line/*.info + echo + fi + + # -H option + if [ -n "$hpbrowser" ]; then + ( source $line/*.info ; open_in_browser "$HOMEPAGE" ) + fi + + # -B option + if [ -n "$browser" ]; then + open_in_browser "http://slackbuilds.org/repository/$SLACKVER/$line/" + fi + done +} + +# main() + +mode="or" +simpleoutput=1 + +if [ "$*" == "" ]; then + set -- --help +fi + +while [ -n "$1" ]; do + arg="$1" + shift + case "$arg" in + "--help") usage ; exit 0 ;; + "--all") showall=1 ;; + "-a") mode="and" ;; + "-r") required="$required $1" ; shift ;; + "-e") email="$email $1" ; shift ;; + "-m") maintainer="$maintainer $1" ; shift ;; + "-h") homepage="$homepage $1" ; shift ;; + "-k") keyword="$keyword $1" ; shift ;; + "-c") category="$category $1" ; shift ;; + "-d") readme="$readme $1" ; shift ;; + "-v") invert=1 ;; + "-R") readmefiles=1 ; simpleoutput=0 ;; + "-I") infofiles=1 ; simpleoutput=0 ;; + "-B") browser=1 ; simpleoutput=0 ;; + "-H") hpbrowser=1 ; simpleoutput=0 ;; + "-S") shortoutput=1 ; simpleoutput=0 ;; + "-P") printpaths=1 ; simpleoutput=0 ;; + "-X") extract=1 ; simpleoutput=0 ;; + "-C") checkinstalled=1 ; simpleoutput=0 ;; + "-i") installedonly=1 ;; + "-u") uninstalledonly=1 ;; + "-x") exact=1 ;; + -*) die "unknown option '$arg'" ;; + *) build="$build $arg" ;; + esac +done + +if [ "$installedonly" = "1" -a "$uninstalledonly" = "1" ]; then + die "-i and -u don't make sense to use together" +fi + +TMPDIR=${TMP:-/tmp}/sbosearch.$$.$RANDOM +rm -rf $TMPDIR +mkdir -p $TMPDIR + +ALL=$TMPDIR/allinfos +OUTPUT=$TMPDIR/output +RESULTS=$TMPDIR/results + +OLD_PWD=$( pwd ) +cd $SBOROOT || die "set SBOROOT or cd to the SBo// directory." + +# Unfortunately ChangeLog.txt is the only file that's present in the SBo +# tree for every Slack version. +if [ ! -e ChangeLog.txt ]; then + warn "can't find ChangeLog.txt in $SBOROOT, are you sure this is a valid repo?" +fi + +/bin/ls */*/*.info > $ALL +[ "$showall" = "1" ] && mode="and" + +# Init results. +# in 'and' mode, each search is done in the results of the previous +# search. The first search starts with the full list, so the results +# of the 'previous' (nonexistent) search must be the full list. +# in 'or' mode, every search is done against the full list, and the +# results are built up by appending (so the result starts out empty). +if [ "$mode" = "and" ]; then + cp $ALL $RESULTS +else + touch $RESULTS +fi + +# Do the searches. +# Thought about parametrizing these, so there could be a loop such as +# for i in REQUIRES EMAIL MAINTAINER HOMEPAGE; do info_search $i; done +# ...but it would lead to fugly bash code that I wouldn't care to debug +# a year from now. + +if [ "$showall" != "1" ]; then + [ -n "$required" ] && for term in $required; do + info_search REQUIRES $term + done + + [ -n "$email" ] && for term in $email; do + info_search EMAIL $term + done + + [ -n "$maintainer" ] && for term in $maintainer; do + info_search MAINTAINER $term + done + + [ -n "$homepage" ] && for term in $homepage; do + info_search HOMEPAGE $term + done + + [ -n "$keyword" ] && for term in $keyword; do + keyword_search $term + done + + [ -n "$category" ] && for term in $category; do + category_search $term + done + + [ -n "$build" ] && for term in $build; do + build_search $term + done + + [ -n "$readme" ] && for term in $readme; do + readme_search $term + done + + [ -n "$installedonly" ] && installed_search + + [ -n "$uninstalledonly" ] && uninstalled_search + + [ -n "$invert" ] && invert_results +fi + +# done with all the searches, pretty up the output. +if [ "$shortoutput" = "1" ]; then + sortopts="-t / -k 2" +fi + +sort -u $sortopts $RESULTS | cut -d/ -f1-2 > $OUTPUT + +print_results + +rm -rf $TMPDIR +exit 0 diff --git a/sbosubmit b/sbosubmit new file mode 100755 index 0000000..cbf3040 --- /dev/null +++ b/sbosubmit @@ -0,0 +1,124 @@ +#!/usr/bin/perl -w + +use LWP; +use Getopt::Std; + +@categories = qw/ +Academic +Accessibility +Audio +Business +Desktop +Development +Games +Graphics +Haskell +Libraries +Misc +Multimedia +Network +Office +Perl +Python +Ruby +System +/; + +sub usage { + my $cats = join("\n", @categories); + warn < [-e ] [-C ] [-k ] file + +If no -e option is given, the contents of ~/.sbo_email is used as the +email address (and it's a fatal error if the file's missing). + +The categories for -c are: + +$cats + +The -c argument is case-insensitive, and category names may be abbreviated +to the shortest unique prefix (e.g. "gr" for Graphics is OK, but "g" +is an error because it matches both Graphics and Games). + +No checking is done on the file before it's uploaded (other than its +existence). + +After the file is uploaded, the server's response is displayed via +"links -dump". $0's exit status does NOT reflect success/failure: it +will be zero if an upload was done, or non-zero if there's an error in +the arguments (in which case no upload is done). +EOF + exit($_[0] || 0); +} + +sub die_usage { + warn $_ . "\n" for @_; + usage(1); +} + +sub get_category { + my $in = shift; + my $orig = $in; + my @got; + + $in =~ s/[^A-Za-z]//g; + for(@categories) { + push @got, $_ if /^$in/i; + } + + if(!@got) { + die_usage("Invalid category '$orig'"); + } elsif(@got > 1) { + die_usage("Ambiguous category '$orig', matches: " . join(" ", @got)); + } else { + return $got[0]; + } +} + +if(!@ARGV || $ARGV[0] =~ /--?h(?:elp)?/i) { + usage; +} + +getopts('c:e:C:k:', \%opts); +($userfile, $junk) = @ARGV; + +die_usage("Unknown junk on command line: '$junk'") if $junk; +die_usage("No category (missing required -c arg)") unless $opts{c}; +$category = get_category($opts{c}); +chomp($submail = $opts{e} || `head -n1 ~/.sbo_email`); +die_usage("No email address (use -e or else create ~/.sbo_email)") unless $submail; +$comments = $opts{C} || ""; +$tags = $opts{k} || ""; + +if(! -e $userfile) { + die_usage("File not found: $userfile"); +} + +print STDERR "Uploading..."; + +$ua = LWP::UserAgent->new; +$ua->agent('Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)'); +$resp = $ua->post( + 'http://slackbuilds.org/process_submit/', + [ MAX_FILE_SIZE => '100000', + userfile => [ $userfile ], + category => $category, + tags => $tags, + submail => $submail, + comments => $comments, + submit => 'Upload File' ], + Content_Type => 'form-data', + Referer => 'http://slackbuilds.org/submit/', + ); + +print STDERR "\n"; + +$tmpfile = "/tmp/sbosubmit.$$." . int(rand(10 ** 10)) . ".html"; +open RESULT, ">$tmpfile" or die "$tmpfile: $!"; +print RESULT $resp->content; +close RESULT; +system("links -dump $tmpfile | sed -n '/Upload Results/,/Home.*Change Log/p'"); +unlink $tmpfile; + +warn "*** HTTP response code is " . $resp->code . "\n" unless $resp->code == 200; -- cgit v1.2.3