#!/bin/bash VERSION=0.0.1 # 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 </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 <= 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 exec pod2man --stderr -s1 -csbo-maintainer-tools -r$VERSION $0 ; exit 0 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 : < [-options ...] [script] [variable=value ...] =head1 DESCRIPTION B 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 is called as a non-root user, it re-executes itself via sudo. If you hate sudo, just run B as root. B 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(1). When combined with B<-i>, the package will only be installed if it lints successfully. =item B<-j> I Run I 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(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 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(1), B(1), B(1), B(8), B(1) =cut EOF