2008-03-01

Using unionfs for multi-building.

As you may have noted in an earlier blog post, I tend to screw around with different configurations of software packages a lot. This leads to a pretty tedious cycle of untar->./configure->make->make install each time I do so. This becomes especially tedious when, inevitably, I am experimenting with a lot of different options under each iteration of ./configure.

For a variety of reasons, I've been watching the creation and expansion of unionfs with interest for a while. I'd just been too lazy to do the patching necessary to get it running on my system. I did find out on Thursday, however, to my embarrassment, that Ubuntu's Gutsy Gibbon release (7.10) has unionfs installed by default (although you need to do a sudo apt-get install unionfs-tools to get the full suite of commands and docs to manipulate these). There was no more interfering with my chances to play.

Now lots of blogs have posted lots of ways to use unionfs. None, however, that I found at any rate, showed the use that leaped to my mind. That use for me? Running through the various configure/build cycles *ONCE* only and then being able to incrementally change as needed in any of the configurations.

Take, for example, my builds of Erlang. I've been building Erlang under LLVM ever since the latter got stable enough (v2.1 for me) to do so. And I've build LLVM-enabled versions as well as the more traditional GCC-enabled versions so frequently -- each time any of the components involved is updated -- that it has become very tedious. With unionfs this can all change. Here's what I do:

  • I untar my Erlang source distro of choice.
  • I make a directory for the configuration.
  • I make a directory for the union.
  • I mount the source and the configuration layers with source marked read-only and configuration layered over top as read-write.
  • I do a configure.
  • I tar the configuration for future reference (doesn't take any of the source files, so I save a lot of space).
  • I reconfigure to have a build/rw layered over the config/ro and the source/ro.
  • I build.
  • I optionally tar the build.
  • Lather. Rinse. Repeat. Once for each combination of tools and build options.
What this buys me in the end is the ability to recreate in seconds a precise build and configuration environment without going through the tedious process of running ./configure (and trying to remember an often-baroque set of command line options!). My source tree is left pristine, my configurations are stored in a way that's trivial to access again as are my builds should I choose to keep those lying around. (Especially useful for debug versions, profiling versions, etc.)

Now of course doing all that layering is itself a bit time-consuming and error-prone, so naturally I've semi-automated it with this script:

1 #! /usr/bin/env bash
2
3 #DEBUG=echo # Set this on the command line to get debug output.
4

Now whenever I'm messing around with commands that can damage my file structure (mount, mkdir, rm, etc.) I like to see what command is being issued in my scripts while testing without actually doing the command. Instead of manually editing the command lines each time, I put a $DEBUG in front of destructive commands. That way I can do a DEBUG=echo ./my-command and have all the commands print out instead of executing. If I'm going through a long-term batch of debugging, I just comment out the line like the one here to get the same effect without having to type the DEBUG=echo part each and every time.
5 E_OK=0
6 E_BADOPTION=1
7 E_BADDIR=2
8
9 function do_build () {
10 # $1 is the build, $2 the config and $3 the source directory.
11 $DEBUG sudo mount -t unionfs -o dirs=$1=rw:$2=ro:$3=ro unionfs union
12 }
13

This function is a trivial function that just does the command line for the build environment. The "build" directory is read-write and layered over the config directory and the source directory, both of which are mounted read-only.
14 function do_config () {
15 # $1 is the build (unused), $2 the config and $3 the source directory.
16 $DEBUG sudo mount -t unionfs -o dirs=$2=rw:$3=ro unionfs union
17 }
18

This function sets up the environment for making a ./configure session. In this case the config directory is rw and the source is ro. The usage message function is self-explanatory so we'll skip over it and to the next working function.
19 function usage () {
20 echo "usage: $0 [options]* [environment]"
21 echo "options:"
22 echo " -h|--help"
23 echo " -v|--version <version> (default: R12B-1)"
24 echo "environment:"
25 echo " build | config (default: build)"
26 echo " llvm | gcc (default: llvm)"
27 }
28
29 function setup_dirs () {
30 # $1 is the build, $2 the config and $3 the source directory.
31 if [[ ! -d $1 ]]
32 then
33 if ! $DEBUG mkdir $1 ; then echo "Cannot make directory $1." ; exit $E_BADDIR ; fi
34 fi
35
36 if [[ ! -d $2 ]]
37 then
38 if ! $DEBUG mkdir $2 ; then echo "Cannot make directory $2." ; exit $E_BADDIR ; fi
39 fi
40
41 if [[ ! -d $3 ]] ; then echo "Source directory '$3' does not exist." ; exit $E_BADDIR ; fi
42
43 if [[ ! -d union ]]
44 then
45 if ! $DEBUG mkdir union ; then echo "Cannot make directory 'union'." ; exit $E_BADDIR ; fi
46 fi
47 }
48

Here we're setting up the directories and making sure that nothing goes wrong. Even for my quick-hack scripts I tend to like to do a bit of paranoid error handling. I've lost too many hours of work to the UNIX Attitude® of "let the user cut off his own hands" to not be paranoid these days.
49 function main () {
50 while [ $# -gt 0 ]
51 do
52 case "$1" in
53 -h|--help) usage ; exit 0 ;;
54
55 -v|--version) local SOURCE_VERSION=$2 ; shift ;;
56
57 llvm|gcc) local BUILD_TYPE=$1 ;;
58
59 build|config) local ENV_TYPE=$1 ;;
60
61 *)
62 echo "Unknown option or command '$1'."
63 usage
64 exit $E_BADOPTION
65 ;;
66 esac
67 shift
68 done
69
70 local BUILD_DIR="otp_${BUILD_TYPE=llvm}_build"
71 local CONFIG_DIR="otp_${BUILD_TYPE=llvm}_config"
72 local SRC_DIR="otp_src_${SOURCE_VERSION=R12B-1}"
73
74 setup_dirs ${BUILD_DIR} ${CONFIG_DIR} ${SRC_DIR}
75
76 do_${ENV_TYPE=build} ${BUILD_DIR} ${CONFIG_DIR} ${SRC_DIR}
77 echo "A $BUILD_TYPE/$ENV_TYPE environment has been set up in the 'union' directory. Please remember to use 'sudo umount union' once completed."
78 }
79
80 main $*

The mainline function is straightforward. Simple command-line processing, a call to the directory structure construction/sanity check, then a dynamic dispatch to the appropriate do_* function according to whichever of build or config was selected (or defaulted to).

0 comments: