Writing a cross platform profile

Learning Unix is to a large extent a long initiation process. One of the first things one learns is basic shell scripting commands, followed by the customization of one shell. When I started working with Unix somewhere in 1993, we used Sun stations, and the default shell was cshrc. My first .login and .cshrc were not built from scratch, instead they contained bits and pieces gathered from other students and teaching assistants. The university not only had sun station, but a bunch of Macintoshes running AU/X, Apple’s Unix before OS X. As they shared the same NFS file-system, writing cross-plateform files was needed. My configuration files were something that always kept, adapting them to new systems, rewriting parts, porting them to new shells, first tcsh, then bash.

While I rely less on NFS, I increasingly use unison for synchronization, so large parts of my home directory are shared between many machines. This is convenient as I can devellop a project on my Mac laptop and run it on the Solaris workstations or our Linux cluster. Of course this means handling the idiosyncrasies of each plateform. In this post, I will try to explain how I handle the different parts of configuration. Of course, this code is tailored for my own usage, use them it at your own risk.

What the configuration files aims to do is to define the different paths in a rational way for each plateform. They do not ensure that all plateform behave the same, more that all the possible tools are accessible.

Some structure

The first thing when sychronizing things among multiple plateforms is to have some structure in your files. This applies in particular to files in my home directory. I use the Mac OS X structure, i.e my home directory contains the following directories:

  • Documents
  • Library
  • Projects
  • Pictures

There are other directories, but those are the ones that are shared among all machines. In order to avoid problems I don’t synchronize the whole home directory, instead those four directores are synchronized totally in parts (no point in synchronizing Library/Caches for instance.

On systems where I have no root access, or where I don’t want to modify the installed programs, there is a personal usr hierarchy, i.e ~/usr/lib, ~/usr/bin, etc. This hierarchy should be included in the path if present.

One important directory is Library/bin that contains all my shell scripts, and the configuration files for the shell, called bashrc.bash and profile.bash. This directory also contains my own personal shell script commands, either in the basic directory if they work on all plateforms, or in plateform specific directories, i.e Darwin, Solaris, Linux.

The actual .bashrc and .profile file source those files. For instance, the .bashrc file on the Mac laptop contains this code:

if test -z "$ALREADY_SOURCED_RC"
then  
    . ~/Library/bin/profile.bash
fi
. ~/Library/bin/bashrc.bash 
The profile.bash file

The meat of the configuration is contained in the ~/Library/bin/profile.bash file. The first line of the file defines the variable, so the whole setup is only done onces.

export ALREADY_SOURCED_RC="true" 

The next step is to define some functions to manipulate paths, as we are going to do a lot of this. Those were shamelessly lifted from the bash default configuration files. What those function do is add some directory to some path, either in front or at the end, those function ensure that each directory only appears once in the path.

## Path remove only works for single component paths 

pathremove () {
        local IFS=':'
        local NEWPATH
        local DIR
        local PATHVARIABLE=${2:-PATH}
        for DIR in ${!PATHVARIABLE} ; do
                if [ "$DIR" != "$1" ] ; then
                  NEWPATH=${NEWPATH:+$NEWPATH:}$DIR
                fi
        done
        export $PATHVARIABLE="$NEWPATH"
}

pathprepend () {
        pathremove $1 $2
        local PATHVARIABLE=${2:-PATH}
        export $PATHVARIABLE="$1${!PATHVARIABLE:+:${!PATHVARIABLE}}"
}

pathappend () {
        pathremove $1 $2
        local PATHVARIABLE=${2:-PATH}
        export $PATHVARIABLE="${!PATHVARIABLE:+${!PATHVARIABLE}:}$1"
}

Now that we have those basic functions, we can start setting things up. First, we define two basic variables, one contains the general OS name (Darwin, Solaris, Linux), the other defining where all the configuration is (~/Library/bin).

export GENERALOSTYPE=`uname`
export HOMELIB=$HOME/Library/bin 

For each operating system, I can do some special configuration. For instance, many of the Solaris don’t have the latest Java JDK as default, so we define a variable containing the position of the JDK. I also redefine LANG, as some of the Sun station are set up with japanese as default and do not support en_US.UTF-8.

if [ "$GENERALOSTYPE" == "SunOS" ]; then 
	export LANG="C" 
	export JAVAHOME=/app/jdk1.5.0_07
fi 

Now, one of the first things we whish to do is setup a rational java. The default on the Mac laptop is OK, but on Linux, it is not the Sun JDK, and on Solaris it is not the lastest version. So we first look if there is an installation in a rational place, and define JAVAHOME in consequence, if we have in some way discovered a value for JAVAHOME, we prepend it to the PATH and the MANPATH (we want to use those settings in preference to the default ones).

# Check for Sun Java installation  

if [ -d /usr/java/j2sdk ]; then 
	export JAVAHOME=/usr/java/j2sdk
fi 

if [ -n $JAVAHOME ]; then 
	pathprepend $JAVAHOME/bin PATH
	pathprepend $JAVAHOME/man MANPATH
fi 

The next step is to setup X11 programs. We basically do the same trick, first looking at reasonable places and then seting up the variables.

# Check for X11 installation 

if [ -d /usr/X11R6 ]; then 
	export X11HOME=/usr/X11R6 
fi 

if [ -d /usr/local/X11R6 ]; then 
	export X11HOME=/usr/local/X11R6
fi

if [ -n $X11HOME ]; then 
	pathappend $X11HOME/bin PATH
	pathappend $X11HOME/man MANPATH
fi

The next step is to setup open-windows programs, they are not really cutting edge, but on Solaris, they can come in handy. Those paths also include certain utilities like xterm. The detection is done in one go, as I have not seen any cases where they are installed in a different path.

# Check if openwindow is installed 

if [ -d /usr/openwin ]; then 
      export OPENWINROOT=/usr/openwin
	pathappend $OPENWINROOT/bin PATH
	pathappend $OPENWINROOT/man MANPATH
	pathappend $OPENWINROOT/share/man MANPATH 
fi

# Check if dt in installed 

if [ -d /usr/dt ]; then 
	export DTROOT=/usr/dt 
	pathappend $DTROOT/bin PATH
	pathappend $DTROOT/man: MANPATH 
	pathappend $DTROOT/share/man MANPATH
fi 

Next step is to check for an /opt/local hierarchy. Certain package managers link fink or darwinport install files there. The JAIST solaris installation also has programs there. As they might contain more advanced programs than the default, we prepend the paths. Notice that this time, we setup more paths.
Multiple additions to the PATH must be done separatly, because pathprepend does not handle prepending multiple paths in one operation correctly.

# Check if there is an /opt/local hierarchy 

if [ -d /opt/local ]; then 
     export OPTROOT=/opt/local
     pathprepend $OPTROOT/share/info INFOPATH
	pathprepend $OPTROOT/man MANPATH
	pathprepend $OPTROOT/share/man MANPATH
	pathprepend $OPTROOT/bin PATH
	pathprepend $OPTROOT/sbin PATH
fi 

Next is the /pkg/all hiearchy, this seems to be Sun specific.

# Check for /pack/all hierarchy 

if [ -d /pkg/all ]; then  
	export PACKROOT=/pkg/all 
	pathappend $PACKROOT/info INFOPATH
	pathappend $PACKROOT/man MANPATH
	pathappend $PACKROOT/bin PATH
fi 

Next, we check for the /usr hiearchy. This is included in the PATH by default by Linux and OS X, but not Solaris.

# Check for /usr hierarchy 

if [ -d /usr ]; then 
	pathappend /usr/man MANPATH
	pathappend /usr/share/man MANPATH
	pathappend /usr/bin PATH
	pathappend /usr/local/bin PATH 
	pathappend /usr/sbin PATH
fi 

Next, we check for the Developer tools, this one is Apple specific.

#Check for Apple developer tools 

if [ -d /Developer/Tools ]; then 
	pathappend /Developer/Tools PATH
fi 

Now we check if there is a usr hierarchy in the home directory.

# Check for usr hierarchy in home directory 

if [ -d "$HOME/usr/" ]; then 
	pathprepend $HOME/usr/bin PATH
	pathprepend $HOME/usr/man MANPATH
fi 

Finally, we prepend ~/Library/bin to the path.

# Finish with commands in Library/bin directory
# Plain scripts are stored in ~/Library/bin 

pathprepend $HOMELIB PATH

We then check if there is a plateform specific directory.

# If there is a subdiretory matching the architecture, load this one. 

if [ -d "$HOMELIB/$GENERALOSTYPE" ]; then 
	pathprepend $HOMELIB/$GENERALOSTYPE PATH
fi

We finish by setting up general variables, like the PAGER, we always check if variables are defined first, as they might already be defined by other sources, because of a remote login, or for instance in the ~/.MacOSX/environment.plist file.

#Set up X11 display 

if [ -z $DISPLAY ]; then 
  export DISPLAY=":0.0" 
fi 

# Setup screen 

if [ -z $SCREENRC ]; then 
  export SCREENRC=$HOMELIB/screenrc
fi 

#Setup langage / encoding 
  
if [ -z $LANG ]; then 
  export LANG="en_US.UTF-8"
fi

# Set up prompt 

if [ "$TERM" == "screen" ]; then
    export PROMPT_COMMAND='echo -ne "\033]0;${USER}@${HOSTNAME}:${WINDOW}\007"'
else 
	export PROMPT_COMMAND='echo -ne "\033]0;${USER}@${HOSTNAME}\007"'
fi 

Notice that the screenrc file is also in ~/Library/bin/ so it is synchronized, but as it has no x bit, it is not a command.
Aliases are defined in separate files that are loaded by the profile file. There are basically three sources for aliases: the general aliases.bash file, the plateform specific one, and finally, some custom script to build aliases out of GUI programs on Mac OS X, that I lifted from the macosxhints web site.

if [ -d "$HOMELIB/$GENERALOSTYPE/osaliases" ]; then 
	if [ -f "$HOMELIB/$GENERALOSTYPE/buildosaliases.bash" ]; then 
		source $HOMELIB/$GENERALOSTYPE/buildosaliases.bash
	fi 
	for dir in $HOMELIB/$GENERALOSTYPE/osaliases/* 
	do
  		source $dir
	done
fi 

# Load general aliases 

if [ -f "$HOMELIB/aliases.bash" ]; then 
	source $HOMELIB/aliases.bash
fi 

# Load plateform specific aliases  

if [ -f "$HOMELIB/$GENERALOSTYPE/aliases.bash" ]; then 
	source $HOMELIB/$GENERALOSTYPE/aliases.bash
fi 

Finally, we do a little bit of fiddling with the TERM variable, the problem is in certain cases, the host does not know about the terminal. For instance, the Mac OS X terminal announces itself as xterm-color, for which Solaris has no clue. We check this by launching terminfo and checking if there is an error, the terminal is not supported, and we fall back to xterm.

# If terminfo acts up, fall back to xterm 

terminfo &> /dev/null 

if [ $? != 127 ]; then 
	export TERM=xterm  
fi

Finally, we set up completion, I have a special script for unison, and we check for reasonable places on the host machine.

#Completions 

complete -C $HOMELIB/unison_completions unison 

if [ -f /etc/bash_completion ]; then 
	. /etc/bash_completion 
fi 

if [ -f /opt/local/etc/bash_completion ]; then
    . /opt/local/etc/bash_completion
fi

That is all, while those files are quite long, they are no overly complex. Tell me what you think.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.