#!/bin/bash # Configurables: TMP=${TMP:-/tmp/SBo} OUTPUT=${OUTPUT:-/tmp} BUILDLOG=${BUILDLOG:-build.log} DEFAULT_MAKEFLAGS="-j8" # 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 .gitignore. # 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" \ "$0" "$@" fi # Inherit MAKEFLAGS from env, if present. MAKEFLAGS="${MAKEFLAGS:-$DEFAULT_MAKEFLAGS}" # Defaults, changed by -options. NETWORK="no" TRACK="yes" STRACE="no" CLEANUP="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.$$ # Why does sbrun exist? Why not use sbopkg or sbotools? sbrun is targeted # more towards a SlackBuild developer/maintainer than an end user. My # workflow is to edit the script in one terminal and repeatedly execute it # in another. If you're editing a SlackBuild, you keep running it over & # over again. So I wrote a 1-liner shell script that did this: # sudo sh ./$( pwd | sed 's,.*/,,' ).SlackBuild # I have /sbin:/usr/sbin in my user's PATH so sudo works fine, but it's a # PITA to pass environment variables using the above script (sudo strips # them out of the env). So I made the script take arguments, and treat # those as env vars (place them between 'sudo' and 'sh'). # Then I added MAKEFLAGS support to it (-jN option). Then I found out # some of my builds were writing outside of $TMP (due to either my # own mistakes, or upstream bugs) and decided I needed a way to # reliably detect that, hence the trackfs stuff. Also someone on # the mailing list posted output from running a SlackBuild under # bubblewrap, which prevented network access the script was trying # to do. Which seems like a nifty feature to have, but bubblewrap # is overkill for just running a shell script. # The strace and sh -x options were added because those are things I do # fairly often with buggy SlackBuilds. The elapsed time display is a nice # convenience (I complain that "This thing takes 2 hours to build!" when # really it's closer to 1 hour). # The -c option is the only "end user" option, really. I mostly use it # for building dependencies maintained by other people, not my own stuff. # Basically, sbrun started out as a lazy typist's tool, and grew into # something more generally useful. It's lower-level than sbotools or # sbopkg, but more convenient than just executing ./*.SlackBuild. It # requires no initial setup or configuration (it's a self-contained # shell script). Since it's *not* intended to replace sbotools or sbopkg, # sbrun doesn't do any of these things: # - download source files # - check source file md5sums # - allow building multiple packages at once (queue files) # - install/upgrade/remove packages (it *just* builds them) # - dependency resolution # - 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). # Possible future options: # -f Run script with fakeroot. (still need root/sudo for unshare/nsenter, # and trackfs won't track failed writes, maybe this isn't that useful?) # -i Install package after it's built. Could count as scope creep. # -H long help (the existing help is long already, -h would be an extract) long_help() { # note: root's pager is used, not the user's, since we use sudo. # not going to care about this one. 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_build() { [ "$CLEANUP" = "yes" ] && ( rm -rf "$TMP" ; rm -f $BUILDLOG ) } # 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_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 } ### 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 # parse -options while printf -- "$1" | grep -q ^-; do case "$1" in -j*) MAKEFLAGS="$1" ;; -n) NETWORK=yes ;; -t) TRACK=no ;; -s) TRACK=no; STRACE=yes ;; -x) X="-x" ;; -c) CLEANUP="yes" ;; -h|-help|--help) show_help ; exit 0 ;; -H) long_help ; exit 0 ;; -*) show_help ; exit 1 ;; esac shift done # warn and die append to the log, make sure it starts out empty. > $BUILDLOG # 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 # $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 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'" done # 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... # TODO: find out why trackfs sometimes segfaults when I hit ^C. No # harm done (it was already killed by SIGINT), just irritating. trap signal_handler INT TERM if [ "$TRACK" = "yes" ]; then if ! /bin/which trackfs &>/dev/null; then warn "File tracking enabled, but trackfs not installed!" die "Install system/trackfs or re-run $SELF with -t." fi LOGDIR="$( mktemp -d /tmp/sbrun.XXXXXX )" if [ -z "$LOGDIR" ] || [ ! -d "$LOGDIR" ]; then die "Can't create temp log dir in /tmp, bailing" fi LOG=$LOGDIR/log # Fun fact: trackfs uses GNU-style -- to mean "no more options", # but it's undocumented in the man page and --help output. # The readlink stuff is here in case $TMP or $OUTPUT has a symlink # in its path: trackfs will log the real path, with the links resolved. TRACKFS=\ "trackfs -l $LOG \ -I$( readlink -f "$TMP" )\\* \ -I$( readlink -f "$OUTPUT")\\* \ -I/tmp\\* \ -I/var/tmp\* \ -I/root/.ccache\\* \ --" fi if [ "$STRACE" = "yes" ]; then TRACKFS="strace -f -ostrace.out" fi SCRIPT="./$( pwd | sed 's,.*/,,' ).SlackBuild" if [ ! -e "$SCRIPT" ]; then die "$SCRIPT not found, bailing" fi { echo "Running $SCRIPT, logging to $BUILDLOG" echo "Environment: $ENV" echo "File tracking: $TRACK" echo "Network access: $NETWORK" if [ "$STRACE" = "yes" ]; 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. 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 )" # Actually run the script. Note that the 'tee' command isn't being # tracked by trackfs. eval $NSENTER $TRACKFS sh $X $SCRIPT 2>&1 | tee -a $BUILDLOG RET=$? echo "$SCRIPT exit status: $RET" | tee -a $BUILDLOG END_TIME="$( date +%s )" { echo -n "Elapsed time: " print_hms $(( $END_TIME - $START_TIME )) } | tee -a $BUILDLOG cleanup_nonet if [ "$TRACK" = "yes" ]; then if [ -s $LOG ]; then warn "WARNING: files altered outside the sandbox:" cat $LOG 1>&2 cat $LOG >> $BUILDLOG fi cleanup_log fi cleanup_build # Our return status is that of the SlackBuild. exit $RET