diff options
| author | B. Watson <urchlay@slackware.uk> | 2026-09-25 03:21:30 -0400 |
|---|---|---|
| committer | B. Watson <urchlay@slackware.uk> | 2026-09-25 03:21:30 -0400 |
| commit | 7d01fbd4e96bee5e01f9e834175ae8f87b65dd97 (patch) | |
| tree | 158fdfb7c1f2309482faaa8ea04d37630de95e4c /sbrun | |
| parent | 2f428d1a3f4b65cadab0259938d7aa3e5b0e3b80 (diff) | |
| download | sbo-maintainer-tools-7d01fbd4e96bee5e01f9e834175ae8f87b65dd97.tar.gz | |
sbrun: added (moved here from sbostuff repo).
Diffstat (limited to 'sbrun')
| -rwxr-xr-x | sbrun | 751 |
1 files changed, 751 insertions, 0 deletions
@@ -0,0 +1,751 @@ +#!/bin/bash + +# TODO: distcc masquerade dir, pump mode +# TODO: fix interactive shell option +# TODO: maybe change the cpufreq governor? + +# Configurables: + +TMP=${TMP:-/tmp/SBo} +OUTPUT=${OUTPUT:-/tmp} +BUILDLOG=${BUILDLOG:-build.log} +DEFAULT_MAKEFLAGS="-j$(( $( nproc ) + 1 ))" + +# End of configurables. It's probably best not to configure TMP or +# OUTPUT here (use the environment instead) anyway. Also it's probably +# convenient to add build.log to .git/info/exclude. + +# If we're not running as root, re-exec as root, with args. +# Anything sbrun expects to possibly inherit from the caller's environment +# must be explicity set here as sudo will strip them from the environment +# before executing anything. +if [ "$(id -u)" != "0" ]; then + exec sudo \ + TMP="$TMP" \ + OUTPUT="$OUTPUT" \ + MAKEFLAGS="$MAKEFLAGS" \ + BUILDLOG="$BUILDLOG" \ + DISTCC_HOSTS="$DISTCC_HOSTS" \ + SBODL_CACHEDIR="${SBODL_CACHEDIR:-$HOME/sbodl-cache}" \ + "$0" "$@" +fi + +# This is a bit of a hack: I keep my tools in my user's ~/bin, +# and sourcing /etc/profile blows away PATH... +OLDPATH=$PATH +source /etc/profile +PATH=$OLDPATH:$PATH + +[ -e "$BUILDLOG" ] && mv "$BUILDLOG" "$BUILDLOG".old + +# Inherit MAKEFLAGS from env, if present. +MAKEFLAGS="${MAKEFLAGS:-$DEFAULT_MAKEFLAGS}" + +# Defaults, changed by -options. +NETWORK="no" +STRACE="" +CLEANUP="no" +SRCSH="no" +PKGSH="no" +LOGDIR="" +NSENTER="" +TRACKFS="" + +SELF=$(basename $0) + +# unshare and nsenter use this. It's theoretically better to use +# an unpredictable filename (not one based on the PID), but anyone +# able to mess with /mnt already has root access. +NONET_PATH=/mnt/nonet.$SELF.$$ + +long_help() { + exec perldoc "$0" +} + +show_help() { + # don't use warn here, log isn't open yet. + if [ -n "$1" ]; then + echo "$SELF: unknown option '$1'" 2>&1 + fi + cat <<EOF +$SELF: paranoid SlackBuild wrapper. + +$SELF written by B. Watson (urchlay@slackware.uk) and released +under the WTFPL. See http://www.wtfpl.net/txt/copying/ for details. + +Usage: $SELF [-option [-option ...]] [script] [variable=value ...] + +-c Clean up (remove) source and package dirs after build completes. +-d Download sources with 'sbodl'. +-D Use distcc (enables -n, sets CC/CXX; set DISTCC_HOSTS yourself). +-I Run an interactive shell in the source directory. +-i Install built package with 'upkg'. +-jN Run N make jobs in parallel. +-l Lint the package with 'sbopkglint'. +-n Allow the SlackBuild to access the network. +-p Run an interactive shell in the \$PKG directory. +-Q Build and install all deps first. +-q Build and install deps first (skip installed deps). +-s Run the script with strace -f, output in "strace.out". +-x Run the script with "sh -x", enables shell command tracing. +-h, --help + Show short usage message (you're reading it now) and exit. +-H, --long-help + Show long help message and exit. +--man Format the long help message as a man page, on stdout. +script + Run this script instead of the default .SlackBuild script. +variable=value ... + Passed to script as environment variables. +EOF +} + +# maybe add 2>/dev/null to these, but for now I wanna know if they fail. +cleanup_nonet() { + if [ "$NETWORK" = "no" ]; then + umount $NONET_PATH + rm -f $NONET_PATH + fi +} + +cleanup_log() { + [ -n "$LOGDIR" ] && rm -rf "$LOGDIR" +} + +cleanup_privdir() { + [ "$PRIVDIR" = "" ] && return + umount $FAKEROOT/$OUTPUT + umount $FAKEROOT/$TMP + umount $FAKEROOT + if [ "$?" != "0" ]; then + cat <<EOF | tee -a $BUILDLOG + +******************** +* +* \$FAKEROOT $FAKEROOT still mounted! +* +******************** + +Can't continue. This is likely a bug in sbrun, please contact +its author: urchlay@slackware.uk +EOF + exit 1 + fi + # We know this isn't still mounted because we would have + # died with a "Can't continue" error, if it were. + rm -rf $PRIVDIR +} + +cleanup_build() { + [ "$CLEANUP" = "yes" ] && ( rm -rf "$TMP" ) +} + +# Add a dir to $PATH, if not already present. This is actually kinda +# pointless, it would work just as well to always add dirs to PATH +# even if they're redundant. +ensure_path() { + if ! echo "$PATH" | sed 's,:,\n,g' | grep -q "^$1\$"; then + #echo "$1 not in PATH, adding" + export PATH="$1:$PATH" + fi +} + +# Handle ^C gracefully. TODO: the exit status should be 128 plus +# the number of the signal received. The hard-coded 130 means SIGINT, +# the ^C signal, but we trap other signals too. Maybe use: +# http://stackoverflow.com/questions/2175647/is-it-possible-to-detect-which-trap-signal-in-bash +signal_handler() { + cleanup_log + cleanup_nonet + cleanup_privdir + cleanup_build + exit 130 +} + +# Print a number of seconds as either MM:SS (if less than 1 hour) +# or HH:MM:SS (if >= 1 hour). This function could almost be replaced +# by: TZ=GMT printf '%(%H:%M:%S)T\n' "$1" +# ...except print_hms doesn't display the hours if they're 00, and using +# printf that way won't handle durations longer than 23:59:59 because +# it's trying to print a time of day (24:00:00 would be 00:00:00 of the +# next day). Hopefully no SlackBuild takes over a day to run, but you +# never know... +print_hms() { + local sec="$1" hrs min + + hrs=$(( $sec / 3600 )) + sec=$(( $sec % 3600 )) + + min=$(( $sec / 60 )) + sec=$(( $sec % 60 )) + + if [ "$hrs" -gt "0" ]; then + printf '%02d:%02d:%02d\n' $hrs $min $sec + else + printf '%02d:%02d\n' $min $sec + fi +} + +# perl-flavoured error messenger +warn() { + echo "$SELF:" "$@" 1>&2 + echo "$SELF:" "$@" >> $BUILDLOG +} + +# Suicide squad, attack! +die() { + warn "$@" + exit 1 +} + +# -q and -Q +run_queue() { + exec sbodeps $1 . | sbqrun - +} + +### main() + +# if these are in $PATH, 99.99% of all SBo builds will run +# correctly under sudo. Or maybe even 100%. At least, I can't +# remember running into problems, for quite a few years now. +ensure_path /sbin +ensure_path /usr/sbin +ensure_path /usr/share/texmf/bin + +# we aren't using real --long-options, just these special cases: +if [ "$1" = "--help" ]; then + show_help ; exit 0 +elif [ "$1" = "--long-help" ]; then + long_help ; exit 0 +elif [ "$1" = "--man" ]; then + die "--man not yet supported" +fi + +# save original args, as we're going to permute them, below +CMD="$0 $*" + +# parse -options +OPTS="$( getopt -n $SELF -olj:nsxcIpDidqQhH -- "$@" )" +if [ "$?" != "0" ]; then + show_help + exit 1 +fi + +eval set -- "$OPTS" +while true; do + case "$1" in + -l) LINTPKG="yes" ;; + -j) shift; MAKEFLAGS="$1" ;; + -n) NETWORK=yes ;; + -s) STRACE=-f ;; + -x) X="-x" ;; + -c) CLEANUP="yes" ;; + -I) SRCSH="yes" ;; + -p) PKGSH="yes" ;; + -D) CC="distcc gcc" + CXX="distcc g++" + NETWORK=yes + export CC CXX ;; + -i) UPKG=yes ;; + -d) SBODL=yes ;; + -q) run_queue ;; + -Q) run_queue -i ;; + -h) show_help ; exit 0 ;; + -H) long_help ; exit 0 ;; + --) shift ; break ;; + *) show_help "$1"; exit 1 ;; # never happens? + esac + shift +done + +[ "$SBODL" = "yes" ] && sbodl + +# warn and die append to the log, make sure it starts out empty. +# This is the only place we use tee $BUILDLOG (everything else appends). +{ +echo -n "== $SELF starting up at " +date +echo -n "== directory: " +pwd +echo "== command: $CMD" +echo +} | tee $BUILDLOG + +# set the build log's ownership to the calling user, or at least the +# user that owns the current directory. +chown "$( stat -c %U:%G . )" $BUILDLOG + +# rest of arg parsing can use warn or die. +if echo "$1" | grep -qv '='; then + SCRIPT="$1" + shift +fi + +# $ENV is only for showing to the user +ENV="MAKEFLAGS=$MAKEFLAGS" +export MAKEFLAGS TMP OUTPUT + +# Add rest of args to environment. The echo|cut and eval stuff allows +# spaces to occur in the values. There is probably a better modern-bash +# way to do this, but (to me anyway) it'll be less readable. +for arg; do + if echo "$arg" | grep -qv '='; then + die "invalid/unknown argument '$1', try -h for help or -H for long help." + else + ENV="$ENV $arg" + #eval export "$arg" # works but doesn't allow spaces + var="$( echo "$arg" | cut -d= -f1 )" + val="$( echo "$arg" | cut -d= -f2 )" + eval "export $var='$val'" + fi +done + +# The easy way to remove the source and PKG dirs after the +# script runs is to guarantee they'll be the only things in +# $TMP. Normally, we don't create the $TMP dir, so we can +# catch 'script fails to create $TMP dir' errors. But with -c, +# we don't care about troubleshooting so much, and mktemp is +# the way to go. +if [ "$CLEANUP" = "yes" ]; then + TMP="$( mktemp -d /tmp/sbrun.build.XXXXXX )" + if [ -z "$TMP" ] || [ ! -d "$TMP" ]; then + die "Can't create temp build dir in /tmp, bailing" + fi +fi + +# I wasn't gonna trap signals, but I can't break myself of the habit +# of hitting ^C. +# TODO: we should be trapping more signals here... +trap signal_handler INT TERM + +# Used to do this, but it doesn't allow for cases where the directory +# has been renamed (foo.SlackBuild in a dir called foo.testing or foo.old). +#SCRIPT="./$( pwd | sed 's,.*/,,' ).SlackBuild" + +# This is better, but during development, a user might have copies of the +# script named foo.old.SlackBuild and foo.new.SlackBuild, so not perfect. +# To allow for this, we now take an optional script name on the command line. +SCRIPT="${SCRIPT:-$( /bin/ls ./*.SlackBuild | head -1 )}" + +if [ ! -e "$SCRIPT" ]; then + die "$SCRIPT not found, bailing" +fi + +{ + echo "Running $SCRIPT, logging to $BUILDLOG" + echo "Environment: $ENV" + echo "Network access: $NETWORK" + if [ "$STRACE" != "" ]; then + echo "strace log: strace.out" + fi + echo +} | tee -a $BUILDLOG + +# Set up no-network namespace. This isn't foolproof, there are probably +# ways for a script being run by root to escape the namespace, but +# a script that did that would hopefully never get approved by our +# beloved moderators. Even though it's "no net", lots of stuff needs +# the loopback interface, and it doesn't hurt anything. +if [ "$NETWORK" = "no" ]; then + touch $NONET_PATH + unshare --net=$NONET_PATH ifconfig lo 127.0.0.1 up + NSENTER="nsenter --net=$NONET_PATH" +fi + +START_TIME="$( date +%s )" + +### 20260923 bkw: mount the overlay here! +PRIVDIR="$( mktemp -td sbrun.priv.XXXXXXXXXX )" +UPPERDIR=$PRIVDIR/upperdir +WORKDIR=$PRIVDIR/workdir +FAKEROOT=$PRIVDIR/fake_root +mkdir -p $UPPERDIR $WORKDIR $FAKEROOT +echo "Private dir for overlay FS: $PRIVDIR" | tee -a $BUILDLOG + +if [ "$STRACE" != "" ]; then + PRECMD="strace -o $PRIVDIR/strace.out $STRACE" +fi + +# Force-load the module (Slackware >= 15.0 ships the module at least) +modprobe overlay &> /dev/null +mount -t overlay overlay \ + -olowerdir=/,upperdir=$UPPERDIR,workdir=$WORKDIR \ + $FAKEROOT +# writes to $FAKEROOT/$TMP will pass through to the real $TMP +mkdir -p $TMP $OUTPUT # these have to exist to be mounted... +mount --bind $TMP $FAKEROOT/$TMP +mount --bind $OUTPUT $FAKEROOT/$OUTPUT + +echo cd "'$( pwd )'" > $PRIVDIR/runme +echo $PRECMD bash $X $SCRIPT >> $PRIVDIR/runme + +# Actually run the script. +( + $NSENTER chroot $FAKEROOT bash $PRIVDIR/runme 2>&1; echo "$?" > $PRIVDIR/ret ) | tee -a $BUILDLOG +RET="$( cat $PRIVDIR/ret )" + +END_TIME="$( date +%s )" + +echo "$SCRIPT exit status: $RET" | tee -a $BUILDLOG + +{ + echo -n "Elapsed time: " + print_hms $(( $END_TIME - $START_TIME )) +} | tee -a $BUILDLOG + +cleanup_nonet + +# This file contains a list of files that were written during +# the build (and are left over in the upperdir). +TURDS=$TMP/sbrun.turds.$$ + +### 20260923 bkw: +# Anything in $UPPERDIR after umount was written there by the +# SlackBuild. Not everything there is worth bitching about. +# After overlay umount, exclude these dirs from complaints: +# /tmp /proc /var/tmp /root/.ccache /root/.cache /dev/pts /dev/shm +# also $TMP and $OUTPUT. +# For some reason, "root" gets created, too. +# Have to specify each dir by itself *without* trailing slash, +# then again with \/* to catch files/dirs under that dir. +( cd $PRIVDIR/upperdir + find * \ + \! -path root \ + \! -path root/.cache/\* \ + \! -path root/.ccache/\* \ + \! -path tmp/\* \ + \! -path var/tmp/\* \ + \! -path proc/\* \ + \! -path dev/pts/\* \ + \! -path dev/shm/\* \ + \! -path $TMP/\* \ + \! -path $OUTPUT/\* \ + \! -path root/.cache \ + \! -path root/.ccache \ + \! -path tmp \ + \! -path var/tmp \ + \! -path proc \ + \! -path dev/pts \ + \! -path dev/shm \ + \! -path $TMP \ + \! -path $OUTPUT \ + -print0 | xargs -r0 ls -bld > $TURDS +) 2>/dev/null + +if [ -f $PRIVDIR/strace.out ]; then + mv $PRIVDIR/strace.out . + chown --reference=$SCRIPT strace.out +fi + +cleanup_privdir + +if [ -s $TURDS ]; then + warn "WARNING: files altered outside the sandbox:" + tee -a $BUILDLOG < $TURDS +fi +rm -f $TURDS + +# If linting + installation were both requested, don't install +# the package if it fails to lint. +if [ "$LINTPKG" = "yes" ] && sbopkglint; then + RET=$? +else + UPKG="" +fi + +# spawn shell(s) if requested. -I and -p are not mutually exclusive. + +# TODO: do this cleaner? +if [ "$SRCSH" = "yes" ]; then + if [ "$RET" != "0" ]; then + warn "Script failed (status $RET), ignoring -I option" + else + SRCDIR="$( /bin/ls -td $TMP/*/ | grep -v /package- | head -1 )" + ( cd $SRCDIR && bash -login ) + fi +fi + +# PRGNAM is problematic. We don't want to use $( basename $( pwd ) ) +# because the directory might have been renamed (foo => foo.testing or +# foo.old). Reading the .info file is no good (this might not be an SBo +# build). For now, extract it from the script name, but eventually this +# won't work because I want to support passing a script name someday +# instead of hard-coding .SlackBuild. +if [ "$PKGSH" = "yes" ]; then + PRGNAM="$( echo $SCRIPT | sed 's,^\./\(.*\)\.SlackBuild$,\1,' )" + PKG=$TMP/package-$PRGNAM + if [ -d "$PKG" ]; then + ( cd $PKG && bash -login ) + else + warn "$PKG not found, ignoring -p option" + fi +fi + +cleanup_build + +# Install the package if -i. +[ "$RET" = "0" ] && [ "$UPKG" = "yes" ] && upkg + +# Our return status is that of the SlackBuild. +exit $RET + +#### rest of this file is POD +: <<EOF + +=pod + +=head1 NAME + +sbrun - paranoid SlackBuild wrapper + +=head1 SYNOPSIS + +B<sbrun> [-options ...] [script] [variable=value ...] + +=head1 DESCRIPTION + +B<sbrun> runs the SlackBuild script in the current directory, +with an optional custom environment. + +By default, the SlackBuild can't access the network, and filesystem +activity is tracked: writes to system directories are flagged and +reported. Also, a complete log of the build's standard output +and standard error is written to "$BUILDLOG". + +If B<sbrun> is called as a non-root user, it re-executes itself via +sudo. If you hate sudo, just run B<sbrun> as root. + +B<sbrun> is designed for use with SBo scripts, but will work for any +SlackBuild (it doesn't refer to the SBo .info file). + +=head1 OPTIONS + +Options may be bundled (-tn is the same as -t -n), and mixed +freely with non-option arguments. + +=over 4 + +=item B<-l> + +Lint the package after it's built, with B<sbopkglint>(1). When +combined with B<-i>, the package will only be installed if it lints +successfully. + +=item B<-j> I<N> + +Run I<N> make jobs in parallel. Default is to use MAKEFLAGS from +the environment if set, otherwise the default is the number of +cores (as reported by B<nproc>(1)) plus one. + +If a SlackBuild fails without B<-j1>, this is a bug in the SlackBuild +and you should add B<-j1> to the B<make> command in the script, or ask +its maintainer to add it. + +=item B<-n> + +Allow the SlackBuild to access the network. If a SlackBuild +fails without this flag, that's a bug in the SlackBuild and +should be fixed or reported to its maintainer (EMAIL in the .info file). + +That said, there are a few scripts in the SBo repo that are +"grandfathered in" from before the no-net rule was made... + +=item B<-s> + +Run the script with "strace -f". The strace log will be written to the +current directory as "strace.out". + +=item B<-x> + +Run the script with "sh -x", enables shell command tracing. + +=item B<-I> + +Run an interactive shell in the source directory, after the script +completes *successfully* (nothing happens if it fails). Useful for +development, e.g. place 'exit 0' in the script wherever you need to +examine the state of the source directory. May not work as expected +if the SlackBuild creates multiple directories under $TMP, or if +multiple SlackBuilds are being run simultaneously. + +=item B<-p> + +Run an interactive shell in the $PKG directory, after the script +completes (successfully or otherwise, provided the directory +exists). Useful for development. + +=item B<-c> + +Clean up (remove) source and package directories after the +build completes. This option overrides $TMP from the environment. + +=item B<-D> + +Use distcc for the compile. You still have to set DISTCC_HOSTS in the +environment, or in one of distcc's config files. This option sets +CC and CXX, and allows network access. + +=item B<-i> + +Install the package after building it. This just runs "upkg" in the +SlackBuild directory, so "sbrun -i" is just a shortcut for typing +"sbrun && upkg". No package will be installed if the build script +fails. + +=item B<-d> + +Download the source before building the package. This just runs "sbodl" +in the SlackBuild directory. Note that this is annoying, if you use +sbrun's sudo support: the file ends up owned by root, not the user +you ran sbrun as. + +=item B<-q> + +Build and install dependencies, if they're not already installed. -q is +short for "queue", as this is a shortcut for: sbodeps | sbqrun - + +=item B<-Q> + +Build and install all dependencies (even if they're already installed). +Shortcut for: sbodeps -i | sbqrun - + +=item B<-h>, B<--help> + +Show short usage message and exit. + +=item B<-H>, B<--long-help> + +Show long help message (this documentation you're reading) and exit. + +=item B<--man> + +Format long help message as a man page, on standard output. Typical +use: + + sbrun --man > sbrun.1 + +=item B<[script]> + +Run this script instead of the .SlackBuild script. Useful for +development, I hope. Useful also for Slack-derived distros that +use something other than .SlackBuild for their script names. + +=item B<[variable=value ...]> + +All arguments containing an = (and not beginning with -) are passed as +part of the SlackBuild script's environment. Options beginning with - +must occur before environment variables. Example: + + sbrun -j1 SDL2=no DOCS=yes + +Leave off the -j1 to use the default number of jobs. + +=back + +=head1 EXIT STATUS + +The exit status of sbrun is the exit status of the SlackBuild, +unless the B<-l> option is used. With B<-l>, the exit status is that +of sbopkglint. + +=head1 FILE TRACKING + +After the SlackBuild exits, any files written to outside of $TMP, +$OUTPUT, /tmp, /var/tmp, /root/.ccache, or /root/.cache +(collectively referred to as "the sandbox") are logged. Any +write outside the sandbox means a bug in the SlackBuild and should be +fixed or reported to its maintainer. + +=head1 NOTES + +=head2 Sequence of events + +1. If not running as root, re-execute with the same arguments via sudo(8). + +2. If -d was used, run sbodl(1). + +3. Initialize no-net namespace (unless -n option was used). + +4. Initialize file tracking (mount overlay filesystems) + +5. Run the SlackBuild script. + +6. Unmount the overlay filesystems and check for writes outside the sandbox. + +7. Disable and clean up the no-net namespace (if used). + +8. If the script failed, exit now. + +9. If the -l option was used, call sbopkglint. + +10. If the -I option was used, launch an interactive shell in the source directory (e.g. $TMP/whatever-1.2.3/). Wait for the shell to exit. + +11. If the -p option was used, launch an interactive shell in the $PKG directory, and wait for it to exit. + +12. If the -c option was used, remove the source and package directories. + +13. If the -i option was used, install the package (unless -l was used and sbopkglint failed). + +14. Exit. + +=head2 /etc/sudoers + +If you're going to run sbrun as a non-root user, you'll have to +add the user to /etc/sudoers. For my 'urchlay' user, I use: + + urchlay ALL=(ALL:ALL) ALL + +...which, yes, is considered horribly insecure. However, development +systems shouldn't also be used for any kind of production use, and +you shouldn't be building SlackBuilds on a secure production host. + +=head2 Why use this? + +Why does sbrun exist? Why not use sbopkg, sbotools, or slackrepo? One +reason is that sbrun tracks writes to the host filesystem. Another +is that the other tools are cumbersome to use during the +edit/test/rewrite cycle (you have to commit every tiny change to git, +push it, then let the tool update its git repo). The main use case is, +edit the script in one terminal, and run sbrun repeatedly in another, +after each change. + +Since it's *not* intended to replace sbotools or sbopkg, sbrun doesn't +do any of these things: + +- download source files (though it will call 'sbodl' with the -d option). + +- check source file md5sums. + +- allow building multiple packages at once (queue files), though it + will call 'sbqrun' with the -q/-Q options (and sbqrun calls sbrun + for each dependency). + +- install/upgrade/remove packages (it *just* builds them, though it will + call 'upkg' with the -i option). + +- sync the repo, or even have any concept of a repo (it only deals + with a single SlackBuild script, in the current directory). + +- anything to do with .info files. Nothing about sbrun is SBo-specific, + it'll work with Pat's or AlienBOB's or anyone else's scripts (which + is why it's called sbrun and not sborun). + +=head2 git integration + +Finally, a helpful hint: If you use git to push to SBo, you can't +add anything to .gitignore since it's tracked by git. But you can +use .git/info/exclude for the same purpose. Add build.log* and +strace.out there, to avoid git complaining about them being untracked. + +=head1 COPYRIGHT + +sbrun written by B. Watson (urchlay@slackware.uk) and released +under the WTFPL. See http://www.wtfpl.net/txt/copying/ for details. + +=head1 SEE ALSO + +B<sbopkglint>(1), B<sbodl>(1), B<sbofixinfo>(1), B<sbopkg>(8), B<sboinstall>(1) + +=cut + +EOF |
