morristech
10/6/2016 - 5:42 AM

Configures and sets up Linux and Mac OS X machines for development.

Configures and sets up Linux and Mac OS X machines for development.

#!/bin/bash
#
# Description: Releases for this script are available at:
#   https://gist.github.com/gorakhargosh/c3902d08a939995fb4af/
#
# The above URL has a bit.ly link as:
#   http://bit.ly/bhojo-config
#
# The script is intended to be written and reviewed in the repository
# but released as a gist at the abovementioned URL.
#
# Executing this script requires issuing the following command:
#
#   curl -fsSL http://bit.ly/bhojo-config > /tmp/configure.sh
#   bash /tmp/configure.sh
#
# To debug this script use:
#
#   bash -x /tmp/configure.sh

# TODO(yesudeep):
# 1. Add error handling for installation of osxfuse.
# 2. Handle uninstall.
# 3. Silence the installation of .dotfiles.
# 4. Allow configuring the group name on the command line. Use default if not
#    specified.

MAKE_PAR_COUNT=8

now=`date`
src_dir=$HOME/src
group_name="bhojo"
logfile="/tmp/${group_name}_config.log"


# WARNING: Do not include the packages listed hereinafter because they require
# special handling:
#
# ack-grep
# golang-go
# nodejs
# tup
linux_packages='
ant
autoconf
automake
binutils
bison
build-essential
bzr
cmake
emacs24
fastjar
fonts-roboto
git
git-svn
graphviz
graphviz-dev
intltool
java8-jdk
keychain
libfuse-dev
libncurses5-dev
libtool
m4
meld
mercurial
mosh
optipng
parallel
pdnsd
pkg-config
python-dev
python-pip
python-setuptools
reniced
ruby-dev
subversion
texinfo
tig
tree
unzip
vim
vim-gtk
vlc
wget
whois
xclip
'

# git is installed using brew because it has an up-to-date version
# that works with bash-completion.
brew_packages='
ack
ant
autoconf
automake
bash-completion
bison
bzr
clib
cmake
coreutils
emacs
fastjar
fish
gettext
git
graphviz
hg
intltool
keychain
libtool
macvim
mercurial
mosh
optipng
parallel
pdnsd
pkg-config
readline
ssh-copy-id
subversion
tig
tree
tup
vim
wget
'

python_packages='
bpython
http://closure-linter.googlecode.com/files/closure_linter-latest.tar.gz
ipython[all]
networkx
sphinx
twisted
'

# NOTE(sandeep): Why aren't we building protoc-gen-go via source?
# Building protoc-gen-go from source and placing it inside bhojo/bin won't work
# since protoc compiler will look for it in environment $PATH variable when
# generating golang source files.
additional_go_packages='
code.google.com/p/rog-go/exp/cmd/godef
github.com/golang/lint/golint
github.com/golang/protobuf/proto
github.com/golang/protobuf/protoc-gen-go
github.com/kisielk/errcheck
github.com/nsf/gocode
github.com/onsi/ginkgo/ginkgo
github.com/onsi/gomega
github.com/smartystreets/goconvey
golang.org/x/tools/cmd/benchcmp
golang.org/x/tools/cmd/callgraph
golang.org/x/tools/cmd/cover
golang.org/x/tools/cmd/digraph
golang.org/x/tools/cmd/eg
golang.org/x/tools/cmd/godex
golang.org/x/tools/cmd/godoc
golang.org/x/tools/cmd/goimports
golang.org/x/tools/cmd/gorename
golang.org/x/tools/cmd/gotype
golang.org/x/tools/cmd/html2article
golang.org/x/tools/cmd/oracle
golang.org/x/tools/cmd/present
golang.org/x/tools/cmd/ssadump
golang.org/x/tools/cmd/stringer
golang.org/x/tools/cmd/vet
'

npm_packages='
bower
karma
karma-cli
bhojo/karma-closure
karma-jasmine
karma-phantomjs-launcher
'

# Keep sorted. To enlist packages, run `android list sdk --extended`:
android_packages='
addon-google_apis-google-19
addon-google_apis-google-21
addon-google_apis_x86-google-19
android-19
android-20
android-21
build-tools-21.1.2
doc-21
extra-android-support
extra-google-google_play_services
extra-google-play_billing
extra-google-play_licensing
extra-google-simulators
extra-google-webdriver
platform-tools
'
android_packages=`echo $android_packages | tr ' ' ','`


# Push directory silent.
function pushdir() {
  pushd "$@" >/dev/null 2>&1
}


# Pop directory silent.
function popdir() {
  popd "$@" >/dev/null 2>&1
}


# Logs information to the console.
function info() {
  printf "$1\n" | tee -a $logfile
}


# Logs information to the console.
function error() {
  printf "error: $1\n" | tee -a $logfile
}


# Compares two version strings.
#
# @see http://stackoverflow.com/questions/4023830/bash-how-compare-two-strings-in-version-format
#
# @param {a} The first version string.
# @param {b} The second version string.
#
# @return 2 if a < b, 0 if a and b are equal, 1 if a > b.
function compare_versions() {
  if [[ $1 == $2 ]]
  then
    return 0
  fi
  local IFS=.
  local i ver1=($1) ver2=($2)
  # fill empty fields in ver1 with zeros
  for ((i=${#ver1[@]}; i<${#ver2[@]}; i++))
  do
    ver1[i]=0
  done
  for ((i=0; i<${#ver1[@]}; i++))
  do
    if [[ -z ${ver2[i]} ]]
    then
      # fill empty fields in ver2 with zeros
      ver2[i]=0
    fi
    if ((10#${ver1[i]} > 10#${ver2[i]}))
    then
      return 1
    fi
    if ((10#${ver1[i]} < 10#${ver2[i]}))
    then
      return 2
    fi
  done
  return 0
}


# Tests the compare versions function.
function test_compare_versions() {
  compare_versions $1 $2
  case $? in
    0) op='=';;
    1) op='>';;
    2) op='<';;
  esac
  if [[ $op != $3 ]]
  then
    echo "FAIL: Expected '$3', Actual '$op', Arg1 '$1', Arg2 '$2'"
  else
    echo "Pass: '$1 $op $2'"
  fi
}


# Ensure that the user running this script is not root or has administrative
# privileges. Some of the files generated by this script require to be built
# and installed in the user's home directory to avoid polluting the host
# system.
function assert_non_root_user() {
  user=`id -u`
  if [ $user == 0 ]; then
    echo "Please run this script without using sudo."
    exit 1
  fi
}


# Prompt yes or no.
#
# @param {message} String message to display when showing the prompt.
# @param {default} String representing the default value.
function prompt_yes_no () {
  message=$1
  default=${2:-"Y"}
  while true; do
    read -p "$message " yn
    case ${yn:-$default} in
      [Yy]* )
        return 0 ;;
      [Nn]* )
        return 1 ;;
      * )
        echo "Please answer yes or no." ;;
    esac
  done
}


# Displays a banner preamble before proceeding with the installation.
function show_preamble() {
  info "Source code download directory: $HOME/src"
  info "Bash environment variables defined in: $HOME/.path_environment"
  info "Fish environment variables defined in: $HOME/.config/fish/config.fish"
}


# Displays an epilog.
function show_epilog() {
  info "Installation completed."
  info "Please restart your system for good measure and hack away!"
}


# Synchronizes a git repository for source code.
#
# @param {repo_path} A string representing the repository path.
function sync_git_repo(){
  repo_path=$1
  repo_dir=$src_dir/$repo_path
  echo "Synchronizing git repo: $repo_dir" | tee -a $logfile
  if [ ! -d $repo_dir ]; then
    git clone --recursive git://$repo_path $repo_dir >> $logfile 2>&1
  else
    cd $repo_dir
    git checkout master >> $logfile 2>&1
    git pull origin master >> $logfile 2>&1
  fi
}


# Rotates log files.
# See: http://wazem.blogspot.in/2013/11/simple-bash-log-rotate-function.html
#
# @param {preamble} Preamble text.
# @param {log} The log file.
function rotate_logfile() {
  preamble=$1
  log=$2

  # Deletes old log file
  if [ ! -f $log ]; then
    printf "$preamble\n" | tee $log
  else
    count=5
    let p_count=count-1
    if [ -f ${log}.5 ] ; then
      rm ${log}.5
    fi

    # Renames logs .1 trough .4
    while [[ $count -ne 1 ]] ; do
      if [ -f ${log}.${p_count} ] ; then
        mv ${log}.${p_count} ${log}.${count}
      fi
      let count=count-1
      let p_count=p_count-1
    done

    # Renames current log to .1
    mv $log ${log}.1
    printf "$preamble\n" | tee $log
  fi
}


# Extracts a tar.gz archive.
#
# @param {archive}
function extract_tar_gz() {
  archive=$1
  info "Extracting $archive"
  tar zxvf $archive >> $logfile 2>&1
}


# Extracts a tar.bz2 archive.
#
# @param {archive}
function extract_tar_bz2() {
  archive=$1
  info "Extracting $archive"
  tar jxvf $archive >> $logfile 2>&1
}


# Downloads a resource given its URL to a destination file.
#
# @param {url} A string representing the URL from where to fetch the resource.
# @param {destination} A String representing the path where the resource should
#   be saved.
function download() {
  url=$1
  destination=$2
  if command -v 'wget' &>/dev/null ; then
    info "Downloading using wget: $url"
    wget -c $url -O $destination -o $logfile 2>&1
    if [ $? != 0 ]; then
      error "cannot download $url"
      exit 1
    fi
  elif command -v 'curl' &>/dev/null ; then
    info "Downloading using curl: $url"
    curl -o $destination $url | tee -a $logfile 2>&1
    if [ $? != 0 ]; then
      error "cannot download $url"
      exit 1
    fi
  else
    error "cannot find curl or wget to download $url. aborting."
    exit 1
  fi
}


# Mounts a Mac OS X DMG as a volume.
#
# @param {dmg_file} The path to the DMG file to mount.
# @param {volume_name} The name of the volume label.
function macosx_attach_dmg() {
  dmg_file=$1
  volume_name=$2

  info "Mounting $1 at $2"
  hdiutil attach $dmg_file -noverify -nobrowse -mountpoint /Volumes/$volume_name 1>/dev/null 2>&1
  if [ $? != 0 ]; then
    echo "error: unable to mount DMG -- please unmount /Volumes/$volume_name first."
    exit 1
  fi
}


# Unmounts a mac os x volume.
#
# @param {volume_name} The volume to unmount.
function macosx_detach_volume() {
  volume_name=$1
  info "Unmounting /Volumes/$volume_name"
  hdiutil detach /Volumes/$volume_name 1>/dev/null 2>&1
}


# Installs OS X fuse from osxfuse.github.io instead of using Homebrew because
# Yosemite no longer allows unsigned kexts.
#
# @param {version} String of version triplet representing the version to fetch.
function install_osxfuse() {
  version=$1

  info "Installing osxfuse."
  download \
    http://internode.dl.sourceforge.net/project/osxfuse/osxfuse-${version}/osxfuse-${version}.dmg \
    /tmp/osxfuse-${version}.dmg
  macosx_attach_dmg /tmp/osxfuse-${version}.dmg osxfuse-$version
  pkg_file=`find /Volumes/osxfuse-$version -type f -name "*.pkg" 2>/dev/null`
  if [ "X$pkg_file" = "X" ] ; then
    echo "error: cannot find PKG file inside DMG -- nothing to install!"
    exit 1
  fi
  # pkgutil --expand "$pkg_file" /tmp/dir_$$
  info "Installing osxfuse-$version"
  sudo /usr/sbin/installer -pkg "$pkg_file" -target / >> $logfile 2>&1
  macosx_detach_volume osxfuse-$version
}


# Installs xcode command line tools.
function install_xcode_tools() {
  info "Installing XCode Command Line tools"
  xcode-select --install 2>/dev/null
}


# Installs homebrew.
function install_homebrew() {
  if ! command -v 'brew' &>/dev/null ; then
    info "Installing homebrew for Mac OS X"
    ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  fi
  info "Diagnosing homebrew. Please see $logfile for more details."
  brew doctor >> $logfile 2>&1
  info "Updating brew package indexes."
  brew update >> $logfile 2>&1
  info "Upgrading stale packages."
  brew upgrade >> $logfile 2>&1
}


# Installs python packages with pip.
#
# @param {spec...} Package specifications separated by spaces.
function install_python_packages() {
  spec="$@"
  case "$OSTYPE" in
    "linux-gnu" )
      if [ ! command -v 'pip' &>/dev/null ]; then
        info "Installing Python pip."
        sudo aptitude -y install python-pip >> $logfile 2>&1
      fi
      ;;
    "darwin"* )
      if [ ! command -v 'pip' &>/dev/null ]; then
        info "Installing Python pip."
        sudo easy_install pip >> $logfile 2>&1
      fi
      ;;
  esac
  # pkgs=`$spec | tr ' ' '\n'`
  info "Installing Python packages: $spec"
  sudo pip install --upgrade $spec >> $logfile 2>&1
}


# Installs the Android SDK.
#
# @param {version} The version of the SDK to fetch.
# @param {packages} A command line comma-separated list of packages.
function install_android_sdk() {
  version=$1
  packages=$2

  # We use this standard directory for the android installation.
  if [ ! -d "$HOME/Library/Android/sdk" ]; then
    mkdir -p $HOME/Library/Android
    case "$OSTYPE" in
      "linux-gnu" )
        url="http://dl.google.com/android/android-sdk_${version}-linux.tgz"
        download $url /tmp/android-sdk_${version}-linux.tgz
        rm -rf /tmp/android-sdk-linux
        pushdir tmp
        extract_tar_gz /tmp/android-sdk_${version}-linux.tgz
        mv android-sdk-linux $HOME/Library/Android/sdk
        popdir

        # 32-bit libraries are required for the SDK.
        case "$machine_arch" in
          i?86)
            sudo apt-get install \
              libncurses5 \
              libstdc++6 \
              zlib1g \
              | tee -a $logfile
            ;;
          x86_64)
            sudo dpkg --add-architecture i386 | tee -a $logfile
            sudo aptitude update | tee -a $logfile
            sudo aptitude install \
              lib32stdc++6 \
              lib32z1 \
              lib32z1-dev \
              libncurses5:i386 \
              libstdc++6:i386 \
              zlib1g:i386 \
              | tee -a $logfile
            ;;
        esac

        ;;
      "darwin"* )
        url="http://dl.google.com/android/android-sdk_${version}-macosx.zip"
        download $url /tmp/android-sdk_${version}-macosx.zip
        rm -rf /tmp/android-sdk-macosx
        pushdir /tmp
        unzip android-sdk_${version}-macosx.zip >> $logfile 2>&1
        mv /tmp/android-sdk-macosx $HOME/Library/Android/sdk
        popdir
        ;;
    esac
  fi

  # Now install additional tools.
  echo y | $HOME/Library/Android/sdk/tools/android update sdk --no-ui --force -t $packages
}


# Installs the Dart SDK.
#
# @param {channel} The channel of the SDK to install for (dev|stable)
# @param {revision} The revision of the SDK to install (see
#   https://www.dartlang.org/tools/download-archive/).
# @param {min_version} The required version to check.
function install_dart_sdk() {
  channel=${1:-dev}
  revision=${2:-42033}
  min_version=${3:-"1.8.0"}

  machine_arch=`uname -m`
  arch="x64"
  have_version=`dart --version 2>&1 | awk '{ print $4 }'`
  have_version=`echo $have_version | awk -F. '{ print $1.$2.$3 }'`
  required_version=`echo $min_version | awk -F. '{ print $1.$2.$3 }'`

  # TODO(yesudeep): Add version detection.
  if ! command -v "$HOME/dart-sdk/bin/dart" &>/dev/null || [[ "$required_version" -gt "$have_version" ]]; then
    case "$machine_arch" in
      i?86)
        # 32-bit installation of dart.
        arch='ia32'
        ;;
      x86_64)
        # 64-bit installation of dart.
        arch='x64'
        ;;
    esac
    os="linux"
    case "$OSTYPE" in
      "linux-gnu" )
        os="linux"
        ;;
      "darwin"* )
        os="macos"
        ;;
    esac
    outfile_name="dartsdk-${os}-${arch}-release.zip"
    url="https://storage.googleapis.com/dart-archive/channels/${channel}/release/${revision}/sdk/${outfile_name}"
    info "Downloading Dart SDK: ${outfile_name}"
    download $url "$HOME/${outfile_name}"
    rm -rf $HOME/dart-sdk 2>/dev/null
    pushdir $HOME
    info "Installing in $HOME/dart-sdk"
    unzip $outfile_name >> $logfile 2>&1
    popdir
  fi
}


# Displays usage.
function show_usage() {
  echo " "
  echo "USAGE: $0 [OPTIONS]"
  echo " "
  echo "OPTIONS:"
  echo "       -h                print usage and exit"
  echo "       -U                uninstall everything that was installed"
  echo " "
  echo "EXAMPLES:"
  echo " "
  echo "       $0 -h :           prints out help information"
  echo "  sudo $0    :           displays an error to not run as administrator"
  echo "       $0 -U :           uninstalls everything installed previously"
  echo " "
}


# Installs and configures ack.
function install_ack() {
  case "$OSTYPE" in
    "linux-gnu" )
      sudo aptitude -y install ack-grep | tee -a $logfile
      sudo ln -s `which ack-grep` /usr/local/bin/ack >> $logfile 2>&1
      ;;
    "darwin"* )
      brew install ack >> $logfile 2>&1
      ;;
  esac
}


# Installs tmux on Ubuntu machines that have an older version of tmux.
#
# NOTE(yesudeep): This is currently only required because tmux 1.9a is required
# at the bare minimum. Once this requirement goes away, we can install tmux
# usually without a special method to handle its installation.
#
# @param {tmux_version} The version number to install.
# @param {tmux_patch_version} The patch version to install. Leave as empty
#   string for stable releases
# @param {libevent_version} The version for libevent to install.
# @param {ncurses_version} The version for ncurses to install.
function install_tmux() {
  tmux_version=$1
  tmux_patch_version=$2
  libevent_version=$3
  ncurses_version=$4

  case "$OSTYPE" in
    "linux-gnu" )
      # sudo aptitude -y remove tmux >> $logfile 2>&1
      sudo aptitude -y install tmux | tee -a $logfile
      have_version=`tmux -V | cut -d ' ' -f 2`
      compare_versions $tmux_version $have_version
      op=$?
      if ! command -v "tmux" &>/dev/null || [[ $op == 1 ]]; then
        tmux_name="tmux-$tmux_version"
        tmux_relative_url="$tmux_name/$tmux_name$tmux_patch_version"
        libevent_name="libevent-$libevent_version-stable"
        ncurses_name="ncurses-$ncurses_version"

        # set the installation directory
        target_dir="/usr/local"

        # download source files for tmux, libevent, and ncurses
        download \
          http://sourceforge.net/projects/tmux/files/tmux/$tmux_relative_url.tar.gz/download \
          /tmp/${tmux_name}.tar.gz
        download \
          https://github.com/downloads/libevent/libevent/$libevent_name.tar.gz \
          /tmp/${libevent_name}.tar.gz
        download \
          ftp://ftp.gnu.org/gnu/ncurses/$ncurses_name.tar.gz \
          /tmp/${ncurses_name}.tar.gz

        # extract files, configure, compile, and install

        # libevent installation
        extract_tar_gz $libevent_name.tar.gz
        pushdir /tmp/$libevent_name
        info "Configuring $libevent_name"
        ./configure --prefix=$target_dir --disable-shared >> $logfile 2>&1
        info "Building $libevent_name"
        make -j $MAKE_PAR_COUNT >> $logfile 2>&1
        info "Installing $libevent_name"
        sudo make install >> $logfile 2>&1
        popdir

        # ncurses installation
        extract_tar_gz $ncurses_name.tar.gz
        pushdir /tmp/$ncurses_name
        info "Configuring $ncurses_name"
        ./configure --prefix=$target_dir >> $logfile 2>&1
        info "Building $ncurses_name"
        make -j $MAKE_PAR_COUNT >> $logfile 2>&1
        info "Installing $ncurses_name"
        sudo make install >> $logfile 2>&1
        popdir

        # tmux installation
        extract_tar_gz ${tmux_name}*.tar.gz
        pushdir /tmp/${tmux_name}*/
        info "Configuring $tmux_name"
        ./configure \
          CFLAGS="-I$target_dir/include -I$target_dir/include/ncurses" \
          LDFLAGS="-L$target_dir/lib -L$target_dir/include/ncurses -L$target_dir/include" \
          >> $logfile 2>&1
        info "Building $tmux_name"
        CPPFLAGS="-I$target_dir/include -I$target_dir/include/ncurses" \
          LDFLAGS="-static -L$target_dir/include -L$target_dir/include/ncurses -L$target_dir/lib" \
          make -j $MAKE_PAR_COUNT >> $logfile 2>&1
        info "Installing $tmux_name"
        sudo cp tmux $target_dir/bin >> $logfile 2>&1
        popdir
      fi
      ;;
    "darwin"* )
      # TODO(yesudeep): Currently versioning for this is not implemented.
      brew install tmux >> $logfile 2>&1
      ;;
  esac

}


# Installs go compilers and tools for golang development.
#
# @param {group_name} The name of the group to use for namespacing the
#   installation.
# @param {go_version} A string representing the version number of the go
#   toolset to install.
# @param {additional_packages...} Additional packages to install for go.
function install_go() {
  group_name=$1
  go_version=$2
  additional_packages="${@:3}"

  install_dir="/usr/local/$group_name"
  go_root_dir="$install_dir/go"
  go_path_dir="$install_dir/golib"
  # have_version=`go version 2>&1 | awk -Fgo '{ print $3 }' | awk '{ print $1 }'`
  # have_version=`echo $have_version | awk -F. '{ print $1.$2.$3 }'`
  # required_version=`echo $go_version | awk -F. '{ print $1.$2.$3 }'`
  have_version=`go version 2>&1 | awk -Fgo '{ print $3 }' | awk '{ print $1 }'`
  required_version=$go_version


  # Remove incompatible versions of go packages.
  case "$OSTYPE" in
    "linux-gnu" )
      sudo aptitude -y remove golang-go | tee -a $logfile
      ;;
    "darwin"* )
      brew remove go >> $logfile 2>&1
      ;;
  esac

  # Check go version and install.
  compare_versions $go_version $have_version
  op=$?
  if ! command -v "$go_root_dir/bin/go" &>/dev/null || [[ $op == 1 ]]; then
    # Download and extract to the temporary directory.
    info "Installing Google Go $go_version"
    info "Setting \$GOROOT=$go_root_dir"
    info "Setting \$GOPATH=$go_path_dir"
    download "https://storage.googleapis.com/golang/go${go_version}.src.tar.gz" "/tmp/go${go_version}.src.tar.gz"
    pushdir /tmp
    tar zxvf go${go_version}.src.tar.gz >> $logfile 2>&1

    # Remove existing go (group-based) installation if any.
    if [ -d $go_root_dir ]; then
      sudo rm -rf $go_root_dir
    fi

    # Remove existing go (group-based) lib dir if any to upgrade packages
    # cleanly.
    if [ -d $go_path_dir ]; then
      sudo rm -rf $go_path_dir
    fi

    # Go compiles the $GOROOT path into the executable and therefore, we must
    # build go where it needs to be installed. We tried compiling it without the
    # permissions hassle in /tmp/go, but that caused go to compile $GOROOT as
    # /tmp/go. Therefore, after having moved the go directory to /usr/local/...
    # it kept complaining about missing $GOROOT. To avoid all this hassle, we
    # build go where it needs to finally live.
    case "$OSTYPE" in
      "linux-gnu" )
        sudo mkdir -p $go_path_dir
        sudo mv /tmp/go $install_dir/

        # We need to change permissions because the source code will be built
        # using the current user's account.
        sudo chown -R $USER $go_root_dir
        pushdir $go_root_dir/src
        info "Building go"
        ./make.bash >> $logfile 2>&1
        popdir

        info "Installing additional go packages:\n$additional_packages"
        sudo env GOPATH=${go_path_dir} ${go_root_dir}/bin/go get -u $additional_packages >> $logfile 2>&1

        # Now create the actual permissions for updates and further installation
        # by other and current user.
        sudo groupadd admins
        sudo usermod -a -G admins $USER
        sudo chown -R root\:admins $install_dir
        sudo chmod -R g+w $install_dir

        # Apparently, permissions get borked on some versions of Ubuntu. Add
        # appropriate permissions to get directories to be accessible.
        sudo find $go_root_dir -type d -print0 | xargs -0 sudo chmod 755
        # Overwrite.
        printf "export GOROOT=$go_root_dir\nexport GOPATH=$go_path_dir\nexport PATH=\$GOPATH/bin:\$GOROOT/bin:\$PATH\n" | sudo tee /etc/profile.d/${group_name}-goenv.sh >> $logfile 2>&1
        ;;

      "darwin"* )
        mkdir -p $go_path_dir
        mv /tmp/go $install_dir/
        pushdir $go_root_dir/src 2>/dev/null
        info "Building go"
        ./make.bash >> $logfile 2>&1
        popdir

        info "Installing additional go packages:\n$additional_packages"
        env GOPATH=${go_path_dir} ${go_root_dir}/bin/go get -u $additional_packages >> $logfile 2>&1

        # Add the environment variables for OS X < Yosemite.
        grep -q "setenv GOROOT $go_root_dir" /etc/launchd.conf || \
          printf "setenv GOROOT $go_root_dir\nsetenv GOPATH $go_path_dir\nsetenv PATH \$GOPATH/bin:\$GOROOT/bin:\$PATH\n" | \
            sudo tee -a /etc/launchd.conf >> $logfile 2>&1

        # Using /etc/launchd.conf no longer works for Yosemite and above: http://goo.gl/0SCH3i
        printf "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
<dict>
  <key>Label</key>
  <string>${group_name}.environment</string>
  <key>ProgramArguments</key>
  <array>
    <string>sh</string>
    <string>-c</string>
    <string>
    launchctl setenv GOROOT $go_root_dir
    launchctl setenv GOPATH $go_path_dir
    </string>
  </array>
  <key>RunAtLoad</key>
  <true/>
</dict>
</plist>" | sudo tee "/Library/LaunchAgents/${group_name}.environment.plist" >> $logfile 2>&1

        # The above cannot be used to set $PATH in Yosemite, but according to
        # http://goo.gl/tJZkGw we need to use /etc/paths.
        grep -q "${go_root_dir}/bin" /etc/paths || \
          printf "${go_root_dir}/bin\n${go_root_dir}/bin\n" | sudo tee -a /etc/paths >> $logfile 2>&1
        ;;
    esac

    popdir
  fi;
}


# Uninstalls go.
function uninstall_go() {
  group_name=$1
  go_root_dir="/usr/local/$group_name/go"
  go_path_dir="/usr/local/$group_name/golib"
  sudo rm -rf $go_root_dir $go_path_dir
}


# Installs packages using brew on Mac OS X.
#
# WARNING: Does not perform OS detection.
#
# @param {pkg_names} A space-separated list of packages.
function install_brew_packages() {
  pkg_names="$@"
  # pkgs=`$pkg_names | tr ' ' '\n'`
  info "Installing packages: $pkg_names"
  brew install $pkg_names >> $logfile 2>&1
}


# Uninstalls packages using brew on Mac OS X.
#
# WARNING: Does not perform OS detection.
#
# @param {pkg_names} A space-separated list of packages.
function uninstall_brew_packages() {
  pkg_names="$@"
  # pkgs=`$pkg_names | tr ' ' '\n'`
  info "Uninstalling packages: $pkg_names"
  brew uninstall $pkg_names >> $logfile 2>&1
}


# Installs packages using aptitude on Ubuntu.
#
# WARNING: Does not perform OS detection.
#
# @param {pkg_names} A space-separated list of packages.
function install_linux_packages() {
  pkg_names="$@"
  # pkgs=`$pkg_names | tr ' ' '\n'`
  info "Installing packages: $pkg_names"
  sudo aptitude install -y $pkg_names | tee -a $logfile
}


# Uninstalls packages using aptitude on Ubuntu.
#
# WARNING: Does not perform OS detection.
#
# @param {pkg_names} A space-separated list of packages.
function uninstall_linux_packages() {
  pkg_names="$@"
  # pkgs=`$pkg_names | tr ' ' '\n'`
  info "Uninstalling packages: $pkg_names"
  sudo aptitude remove -y $pkg_names >> $logfile 2>&1
}


# Don't install dotfiles twice.
#
# @param {rc_file} The RC file to check and add to.
function check_install_dotfiles() {
  rc_file=$1
  grep -q "source \"\$HOME/.dotfiles/bashrc\"" $rc_file || \
    printf "\n\nif [ -f \"\$HOME/.dotfiles/bashrc\" ]; then\n  source \"\$HOME/.dotfiles/bashrc\"\nfi\n\n"\
           >> $rc_file
}


# Configures emacs for use.
function install_emacs_config() {
  # Removes older styles of installing emacs.
  if [ ! -h $HOME/.emacs.d ]; then
    rm -rf $HOME/.emacs.d
  fi
  sync_git_repo github.com/gorakhargosh/gemacs
  ln -s $src_dir/github.com/gorakhargosh/gemacs $HOME/.emacs.d 2>/dev/null
}


# Installs watchman for a specific version.
#
# @param {version} Version.
function install_watchman() {
  version=$1
  sync_git_repo github.com/facebook/watchman
  pushdir $HOME/src/github.com/facebook/watchman

  info "Checking out version $version"
  git checkout $version >> $logfile 2>&1
  info "Configuring watchman"
  ./autogen.sh >> $logfile 2>&1
  ./configure --prefix=$HOME/var >> $logfile 2>&1
  info "Building watchman"
  make -j $MAKE_PAR_COUNT >> $logfile 2>&1
  info "Installing watchman."
  make install >> $logfile 2>&1
  git checkout master >> $logfile 2>&1
  popdir
}


# Installs buck for a specific version.
#
# @param {version} Version.
function install_buck() {
  version=$1
  sync_git_repo github.com/facebook/buck
  pushdir $HOME/src/github.com/facebook/buck

  info "Checking out version $version"
  git checkout $version >> $logfile 2>&1
  info "Building buck"
  ant && bin/buck build buck >> $logfile 2>&1
  info "Installing buck."

  rm -rf $HOME/var/bin/buck && \
    ln -s $HOME/src/github.com/facebook/buck/bin/buck $HOME/var/bin/buck >> $logfile 2>&1
  git checkout master >> $logfile 2>&1

  popdir
}



# Installs shell and emacs configuration
function install_shell_config() {
  if [ ! -h $HOME/.dotfiles ]; then
    rm -rf $HOME/.dotfiles
  fi
  sync_git_repo github.com/gorakhargosh/dotfiles
  ln -s $src_dir/github.com/gorakhargosh/dotfiles $HOME/.dotfiles 2>/dev/null

  # Dont install twice.
  case "$OSTYPE" in
    "linux-gnu" )
      check_install_dotfiles "$HOME/.bashrc"
      ;;
    "darwin"* )
      check_install_dotfiles "$HOME/.bash_profile"
      # Fix stupid Mac OS X.
      # See: http://www.joshstaiger.org/archives/2005/07/bash_profile_vs.html
      grep -q "source \"\$HOME/.bashrc\"" "$HOME/.bash_profile" || \
        printf "\n\nif [ -f \"$HOME/.bashrc\" ]; then\n  source \"$HOME/.bashrc\"\nfi\n\n"\
               >> "$HOME/.bash_profile"
      ;;
  esac
  # Don't silence this for now. It needs to be rewritten.
  bash $HOME/.dotfiles/install.sh
}


# Installs the Google Cloud SDK.
function install_google_cloud_sdk() {
  if [ ! -d "$HOME/google-cloud-sdk" ]; then
    curl https://sdk.cloud.google.com | bash
  else
    $HOME/google-cloud-sdk/bin/gcloud components update --quiet
  fi
  $HOME/google-cloud-sdk/bin/gcloud components update --quiet \
                                    app \
                                    app-engine-java \
                                    pkg-core \
                                    pkg-go \
                                    pkg-java \
                                    pkg-python \
                                    preview
}


# Installs node, npm, and bower.
#
# @param {version} String version for node (git tag).
function install_node() {
  # This version of node is not installed system-wide. It is local to the user.
  # and hence requires updating the PATH environment variable. .dotfiles handles
  # this.
  version=$1

  info "Installing node $version"
  sync_git_repo github.com/joyent/node

  # TODO(yesudeep): Add version detection.
  if ! command -v 'node' >& /dev/null; then
    pushdir $src_dir/github.com/joyent/node
    git checkout ${version} >> $logfile 2>&1
    info "Configuring node"
    ./configure --prefix=$HOME/var >> $logfile 2>&1
    info "Compiling node"
    make -j $MAKE_PAR_COUNT >> $logfile 2>&1
    info "Installing node"
    make install >> $logfile 2>&1
    info "Cleaning up to save disk space"
    make clean >> $logfile 2>&1
    popdir
  fi
  info "Installing additional node packages: $npm_packages"
  npm install -g $npm_packages >> $logfile 2>&1
  npm update >> $logfile 2>&1
}


# Installs tup.
#
# @param {version} A string version (git tag) to install.
function install_tup() {
  # Tup is installed system-wide.
  version="$1"

  info "Installing tup $version"
  sync_git_repo github.com/gittup/tup
  pushdir $src_dir/github.com/gittup/tup
  git checkout $version >> $logfile 2>&1
  ./bootstrap.sh >> $logfile 2>&1
  cp tup $HOME/var/bin
  git checkout master
  popdir
}


# Installs fish shell
#
# @param {version} The version to install (git tag or commit SHA-1 digest).
function install_fish_shell() {
  # Fish is installed system-wide.
  version=$1
  have_version=`fish --version 2>&1 | awk '{ print $3 }' | awk -F'-' '{ print $1 }' | tr -d '\r'`
  have_version=`echo $have_version | awk -F. '{ print $1.$2.$3 }'`
  required_version=`echo $version | awk -F. '{ print $1.$2.$3 }'`

  if ! command -v "fish" &>/dev/null || [[ "$required_version" -gt "$have_version" ]]; then
    info "Installing fish $version"
    sync_git_repo github.com/fish-shell/fish-shell
    pushdir $HOME/src/github.com/fish-shell/fish-shell
    git checkout ${version}
    autoreconf
    ./configure && make -j $MAKE_PAR_COUNT
    case "$OSTYPE" in
      "linux-gnu" )
        # Preparation
        info "Building fish"
        sudo make install
        ;;
      "darwin"* )
        info "Building fish"
        make install
        ;;
    esac
    grep -q "/usr/local/bin/fish" /etc/shells || \
      (printf "/usr/local/bin/fish\n" | sudo tee -a /etc/shells)
    git checkout master
    popdir
  fi
}


# Install and configure protocol-buffer compiler.
#
# @param {version} Version string for the required version of the protobuf
#    compiler.
function install_protobuf_compiler() {
  # Protoc compiler is installed system-wide.
  version=$1
  have_version=`protoc --version 2>&1 | awk '{ print $2 }'`

  compare_versions $version $have_version
  op=$?
  if ! command -v "protoc" &>/dev/null || [[ $op == 1 ]]; then
    info "Installing protobuf compiler $version"
    download \
      http://github.com/google/protobuf/releases/download/v${version}/protobuf-${version}.tar.bz2 \
      /tmp/protobuf-${version}.tar.bz2

    pushdir /tmp
    tar jxvf protobuf-${version}.tar.bz2 >> $logfile 2>&1
    pushdir protobuf-${version}

    info "Configuring Protobuf compiler"
    ./configure  >> $logfile 2>&1

    info "Compiling Protoc compiler"
    make -j $MAKE_PAR_COUNT >> $logfile 2>&1

    info "Installing Protoc compiler"
    sudo make install >> $logfile 2>&1
    popdir
    popdir

    # On Linux, sometimes the protoc compiler is not able to find shared
    # libraries after installation, and you may encounter errors like: "protoc:
    # error while loading shared libraries...".  This occurs when ld.so.cache
    # is stale. Running ldconfig creates necessary links and updates cache.
    #
    # OS X does not have an ldconfig executable, therefore, we test first
    # whether ldconfig is available. Also, it's likely this error is
    # Linux-specific.
    if command -v "ldconfig" &>/dev/null; then
      sudo ldconfig
    fi
  fi
}


# Install and configure java.
function configure_java() {
  # Update java configuration. Keep this at the end. It prompts for a response
  # and breaks stuff that comes after it for some reason.
  case "$OSTYPE" in
    "linux-gnu" )
      sudo update-alternatives --config java
      ;;
    "darwin"* )
      echo "Unimplemented"
      ;;
  esac
}


# Runs all tests to make this script bulletproof.
function run_tests() {
  # Run tests
  # argument table format:
  # testarg1   testarg2     expected_relationship
  echo "The following tests should pass"
  while read -r test
  do
    test_compare_versions $test
  done << EOF
1            1            =
2.1          2.2          <
3.0.4.10     3.0.4.2      >
4.08         4.08.01      <
3.2.1.9.8144 3.2          >
3.2          3.2.1.9.8144 <
1.2          2.1          <
2.1          1.2          >
5.6.7        5.6.7        =
1.01.1       1.1.1        =
1.1.1        1.01.1       =
1            1.0          =
1.0          1            =
1.0.2.0      1.0.2        =
1..0         1.0          =
1.0          1..0         =
EOF

  echo "The following test should fail (test the tester)"
  test_compare_versions 1 1 '>'
}


# Installs required software.
function install_all() {
  rotate_logfile "Starting installation: $now\nTo view logs: tail -f $logfile\n" $logfile
  sysinfo=`uname -a`
  info "System info: $sysinfo"

  # Install operating-system specific packages.
  case "$OSTYPE" in
    "linux-gnu")
      # Preparation
      info "Preparing to install"
      if [ ! command -v 'aptitude' 2>/dev/null ]; then
        info "Installing aptitude"
        sudo apt-get -y install aptitude | tee -a $logfile
        info "Checking for updates"
        sudo aptitude update | tee -a $logfile
        info "Upgrading packages"
        sudo aptitude -y full-upgrade | tee -a $logfile
      fi
      info "Installing build dependencies"
      sudo apt-get -y build-dep emacs24 fish | tee -a $logfile
      install_linux_packages $linux_packages
    ;;
    "darwin"*)
      info "Please install XCode before proceeding. https://developer.apple.com/xcode/"
      if prompt_yes_no "Are you done installing xcode? [Y/n]"; then
        install_xcode_tools
        info "Please wait for the Xcode tools to be installed before proceeding."
        sudo xcodebuild -license
        install_homebrew
        # Apparently, sometimes this doesn't get installed, but we need it.
        brew install wget >> $logfile 2>&1
        install_brew_packages $brew_packages
        # OS X Fuse requires the brew packages to have been fetched and
        # installed.
        install_osxfuse "2.7.3"
      fi
      ;;
  esac

  # Install packages.
  install_watchman "v3.0.0"
  install_buck "5a6d5d00d7f3be1329bf501c710ffa409ecea3d8"
  install_android_sdk "r24.0.2" $android_packages
  install_tmux "1.9" "a" "2.0.21" "5.9"
  install_python_packages $python_packages
  install_ack
  install_go "$group_name" "1.4" $additional_go_packages
  install_protobuf_compiler "2.6.1"
  install_dart_sdk dev 42033 "1.8.0"
  install_node "v0.11.14"
  install_google_cloud_sdk
  install_tup "1de2e9e0d7ce65f0ba90d1304c07ddbc0a6a2dc4" #"v0.7.3"
  install_fish_shell "2.1.1" # "bb01e5f81a02d45da654c597ca4a983fc152e4f8"
  configure_java
  install_shell_config
  install_emacs_config
}


# Check whether the callee user is non-root.
assert_non_root_user

# Parse command line arguments and initialize.
while [ $# -ne 0 ]; do
  case $1 in
    -h|--help)
      show_usage
      exit 0 ;;
    -U|--uninstall)
      echo "error: not implemented"
      exit 1 ;;
    -T|--run-tests|--run-tests)
      run_tests
      exit 0 ;;
    -*)
      echo "error: unsupported option: '$1'"
      show_usage
      exit 1 ;;
  esac
  shift
done

# Trap SIGINT.
# See: http://redsymbol.net/articles/bash-exit-traps/
function finish() {
  exit 1
}
trap finish EXIT


show_preamble
if prompt_yes_no "Proceed with installation? [Y/n]"; then
  install_all
  show_epilog
fi;

exit 0