#!/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 .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" \ "$0" "$@" fi # Inherit MAKEFLAGS from env, if present. MAKEFLAGS="${MAKEFLAGS:-$DEFAULT_MAKEFLAGS}" # Defaults, changed by -options. NETWORK="no" TRACK="yes" 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.$$ # 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. long_help() { # note: root's pager is used, not the user's, since we use sudo. # not going to care about this one. cat <". -x Run the script with "sh -x", enables shell command tracing. -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. -p Run an interactive shell in the \$PKG directory, after the script completes (successfully or otherwise, provided the directory exists). Useful for development. -c Clean up (remove) source and package directories after the build completes. This option overrides \$TMP from the environment. -h, --help Show short usage message and exit. -H Show long help message (you're reading it now) and exit. [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. [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: $SELF -j1 SDL2=no DOCS=yes Leave off the -j1 to use the default number of jobs. After the SlackBuild exits, any files written to outside of \$TMP, \$OUTPUT, /tmp, /var/tmp, or /root/.ccache (collectively referred to as "the sandbox") are logged to stdout. See trackfs(1) for details of the log format, but any write outside the sandbox means a bug in the SlackBuild and should be reported to its maintainer. The exit status of $SELF is the exit status of the SlackBuild. Note: the current directory needs to be writable, since the log and (with -s/-S) strace output are written there. ====================================================================== The rest of this help message is a long-winded discussion that doesn't include any more usage information. Feel free to stop reading at any time :) ====================================================================== 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 -i option exists because I used to be in the habit of placing 'exec bash -login' part of the way through a script as a way to get a shell in the source dir... but this stopped being possible once I added build.log to sbrun. So now I can stick 'exit 0' part way through the script and use 'sbrun -i' to get the same thing. -p was added for similar reasons. 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), except you have to install trackfs. 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). 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 maybe strace.out there. EOF } 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 <". -x Run the script with "sh -x", enables shell command tracing. -c Clean up (remove) source and package dirs after build completes. -i Run an interactive shell in the source directory. -p Run an interactive shell in the \$PKG directory. -h, --help Show short usage message (you're reading it now) and exit. -H Show long help message and exit. 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_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=-f ;; -S) TRACK=no; STRACE=-ff ;; -x) X="-x" ;; -c) CLEANUP="yes" ;; -i) SRCSH="yes" ;; -p) PKGSH="yes" ;; -h|-help|--help) show_help ; exit 0 ;; -H) long_help ; exit 0 ;; -*) show_help "$1"; exit 1 ;; esac shift done # 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: $0" "$@" echo } | tee $BUILDLOG # rest of arg parsing can use warn or die. if echo "$1" | grep -qv '='; then SCRIPT="$1" shift #[ ! -e "$SCRIPT" ] && warn "$SCRIPT not found, I hope you know what you're doing!" 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... # 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" != "" ]; then TRACKFS="strace $STRACE -ostrace.out" fi # 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 "File tracking: $TRACK" 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. 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. The rigmarole with $? and RET might not be the # best way to get the exit status, TODO: see if I can do this cleaner. # Also, using { } instead of ( ) utterly fails. ( eval $NSENTER $TRACKFS sh $X $SCRIPT 2>&1; echo "$?" > $LOGDIR/ret ) | tee -a $BUILDLOG RET="$( cat $LOGDIR/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 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 # spawn shell(s) if requested. -i and -p are not mutually exclusive. # TODO: maybe do this cleaner, using trackfs? 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 # Our return status is that of the SlackBuild. exit $RET