#!/bin/bash

# this program was written for the bash shell. if you change the shell
# to something different, it's your risk ;)

#    compilercache - wraps your C and C++ compilers to cache compilations
#    Copyright (C) 2001  Erik Thiele <erikyyy@erikyyy.de>
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.


# bailout on all kinds of errors!
set -e

# set the default configuration values
CACHEDIR="$HOME/.compilercache/cache"
TEMPDIR="$HOME/.compilercache/temp"
SHALLDEBUG="no"
LINKOUTPUT="no"
PATH="/bin:/usr/bin"
COMPILERPATH="/bin:/usr/bin"
COMPILERNAMES="c++ g++ cc gcc"
COMPILERCACHEBINDIR="/usr/bin/compilercache"

# source the systems global configuration file
[ -f /etc/compilercacherc ] && source /etc/compilercacherc

# source the users configuration file
[ -f "$HOME/.compilercacherc" ] && source "$HOME/.compilercacherc"

# store the PATH variable
NORMALPATH="$PATH"

# temporary files
TMPFILE=""
FASTTMPFILE=""
FASTTF=""

# setup exit trap
cleanexit ()
{
  rm -f "$TMPFILE" "$FASTTMPFILE" "$FASTTF"
}
trap cleanexit EXIT

# function for writing those fancy debug messages
debugmsg ()
{
  if ! [ "$SHALLDEBUG" = "no" ]; then
    printf "%s\n" "$1" 1>&2
  fi
}

# determine compiler type
COMP="$(basename "$0")"
COMPILERNAMEMATCHES=no
for TESTCOMPILERNAME in $COMPILERNAMES; do
  if [ "$TESTCOMPILERNAME" = "$COMP" ]; then
    COMPILERNAMEMATCHES=yes
  fi
done
if [ "$COMPILERNAMEMATCHES" = no ]; then
  printf "unsupported compiler type\n" 1>&2
  exit 1
fi

# make sure there are no spaces in the commandline parameters.
# also make sure there are no empty commandline parameters.
# if there are, emergency bypass ! (shell sucks)
checkspaceparms()
{
  SPACEPARMS="ok"
  while [ $# != 0 ]; do
    if [ "$1" = "" ] || [ ! "${1#* }" = "$1" ]; then
      SPACEPARMS="not ok"
    fi
    shift
  done
}
checkspaceparms "$@"
if [ "$SPACEPARMS" != "ok" ]; then
  debugmsg "(spaces or empty parameters on the commandline, cache bypass)"
  PATH="$COMPILERPATH"
  "$COMP" "$@"
  PATH="$NORMALPATH"
  exit 0
fi

# store the original arguments, this works because there are no spaces or
# empty ones inside ;)
ARGS="$@"

# create the cache and temporary directories if not present
mkdir -p "$CACHEDIR"
mkdir -p "$TEMPDIR"

# function for cache bypass
ignoremode()
{
  debugmsg "($1, cache bypass)"
  PATH="$COMPILERPATH"
  "$COMP" $ARGS
  PATH="$NORMALPATH"
  exit 0
}

# bypass if user temporarily disabled compilercache
if ! [ "$NOCOMPILERCACHE" = "" ]; then
  ignoremode "NOCOMPILERCACHE environment variable set"
fi

# parse commandline options
FOUNDCOPT="no"
FOUNDINPUT="no"
FOUNDOUTPUT="no"
INPUT="undefined"
OUTPUT="undefined"
# STRIPPEDARGS are the original args without -c source -o object
STRIPPEDARGS=""
# IDENTARGS are the args that will be used to compute the hash
IDENTARGS=""
# FOUNDDEBUG is yes if some -g* option was found.
FOUNDDEBUG="no"

while true; do
  case "$1" in
    # compilation command
    -c)
      FOUNDCOPT="yes"
      ;;
    # output file selection
    -o)
      shift
      case "$1" in
        -*)
          ignoremode "wrong argument to -o option"
          ;;
        "")
          ignoremode "missing argument to -o option"
          ;;
        *)
          OUTPUT="$1"
          FOUNDOUTPUT="yes"
          ;;
      esac
      ;;
    # debugging mode
    -g*)
      STRIPPEDARGS="$STRIPPEDARGS $1"
      IDENTARGS="$IDENTARGS $1"
      if ! [ "$1" = "-g0" ]; then
        FOUNDDEBUG="yes"
      fi
      ;;
    # options with arguments that are not source files
    -include|-I|-L)
      STRIPPEDARGS="$STRIPPEDARGS $1 $2"
      if [ "$2" = "" ]; then ignoremode "missing argument to $1"; fi
      shift
      ;;
    -D)
      STRIPPEDARGS="$STRIPPEDARGS $1 $2"
      if [ "$2" = "" ]; then ignoremode "missing argument to -D"; fi
      shift
      ;;
    # options with direct arguments
    -I*|-L*)
      STRIPPEDARGS="$STRIPPEDARGS $1"
      ;;
    -D*)
      STRIPPEDARGS="$STRIPPEDARGS $1"
      ;;
    # all other options
    -*)
      STRIPPEDARGS="$STRIPPEDARGS $1"
      IDENTARGS="$IDENTARGS $1"
      ;;
    # end of commandline
    "")
      break
      ;;
    # source files or spurious arguments to options ;)
    *)
      if [ "$FOUNDINPUT" = "yes" ]; then ignoremode "multiple input files"; fi
      INPUT="$1"
      FOUNDINPUT=yes
      ;;
  esac
  shift
done

# bypass if no -c option was given
if [ "$FOUNDCOPT" = "no" ]; then ignoremode "no -c option found"; fi

# bypass if no input name was given
if [ "$FOUNDINPUT" = "no" ]; then ignoremode "no input file found"; fi

# is inputfilename valid?
INPUTNAME="$(basename "$INPUT")"
INPUTBASE="$(printf "%s\n" "$INPUTNAME" | sed 's/\(^.*\)\..*$/\1/')"
INPUTEXT="$(printf "%s\n" "$INPUTNAME" | sed -n 's/^.*\.\(.*\)$/\1/p')"
# debugging block
#printf "INPUTNAME      %s\n" "$INPUTNAME"
#printf "INPUTBASE      %s\n" "$INPUTBASE"
#printf "INPUTEXT       %s\n" "$INPUTEXT"
#exit 0
case "$INPUTEXT" in
  c|C|m) ;;
  cc|CC|cpp|CPP|cxx|CXX) ;;
  *)
    ignoremode "unknown input file extention '$INPUTEXT'"
    ;;
esac

# calculate output name if no -o was given
if [ "$FOUNDOUTPUT" = "no" ]; then 
  OUTPUT="$INPUTBASE".o
  FOUNDOUTPUT="yes"
fi

# debugging block
#printf "FOUNDCOPT      %s\n" "$FOUNDCOPT"
#printf "FOUNDINPUT     %s\n" "$FOUNDINPUT"
#printf "FOUNDOUTPUT    %s\n" "$FOUNDOUTPUT"
#printf "INPUT          %s\n" "$INPUT"
#printf "OUTPUT         %s\n" "$OUTPUT"
#printf "STRIPPEDARGS   %s\n" "$STRIPPEDARGS"
#printf "IDENTARGS      %s\n" "$IDENTARGS"
#printf "FOUNDDEBUG     %s\n" "$FOUNDDEBUG"
#exit 0

# initialize temporary filenames
FASTTMPFILE="$TEMPDIR/fasttmp_$(hostname)_$$_$RANDOM"
FASTTF="$FASTTMPFILE"_
# run the preprocessor
PATH="$COMPILERPATH"
set +e
"$COMP" $STRIPPEDARGS -E "$INPUT" >"$FASTTMPFILE" 2>"$FASTTF"
COMPRETVAL=$?
set -e
PATH="$NORMALPATH"
if [ -s "$FASTTF" ]; then
  ignoremode "preprocessor produced stderr output"
fi
if ! [ "$COMPRETVAL" = 0 ]; then
  ignoremode "preprocessor failed"
fi
# run the sourcefile unifier if FOUNDDEBUG=no
if [ "$FOUNDDEBUG" = "no" ]; then
  set +e
  "$COMPILERCACHEBINDIR"/compilercacheunifier < "$FASTTMPFILE" > "$FASTTF"
  COMPRETVAL=$?
  set -e
  if ! [ "$COMPRETVAL" = 0 ]; then
    debugmsg "(compilercacheunifier failed, this does no harm but prevents optimizations)"
  else
    mv -f "$FASTTF" "$FASTTMPFILE"
  fi
fi
# compute the hashvalue
PATH="$COMPILERPATH"
"$COMP" -v >> "$FASTTMPFILE" 2>&1
PATH="$NORMALPATH"
printf "IDENTARGS=%s\nCOMP=%s\nINPUTNAME=%s\n" "$IDENTARGS" "$COMP" "$INPUTNAME" >> "$FASTTMPFILE"
# uncomment this to produce a file to see the unhashed identification
#cp "$FASTTMPFILE" "$INPUT".identify
HASHVAL="$(md5sum "$FASTTMPFILE" | sed 's/ .*$//')"
HASHFILE="$CACHEDIR/$HASHVAL"

# make sure we are in the cache. if not then compile
if ! [ -a "$HASHFILE" ]; then
  debugmsg "(compiling into cache)"
  # first compile into a tmpfile, then move. this fixes race condition.
  TMPFILE="$CACHEDIR/tmp_$(hostname)_$$_$RANDOM"
  rm -f "$TMPFILE"
  # run the compiler
  PATH="$COMPILERPATH"
  set +e
  "$COMP" $STRIPPEDARGS -c "$INPUT" -o "$TMPFILE" 2>"$FASTTMPFILE" 1>&2
  COMPRETVAL=$?
  set -e
  PATH="$NORMALPATH"
  # if there was stderr output, show it
  if [ -s "$FASTTMPFILE" ]; then
    cat "$FASTTMPFILE" 1>&2
    debugmsg "(compiler produced stdout or stderr output, i won't cache)"
  fi
  # did the compiler return an error?
  if ! [ "$COMPRETVAL" = 0 ]; then
    debugmsg "(compiler returned error, bailing out)"
    exit "$COMPRETVAL"
  fi
  # did the compiler produce output ?
  if ! [ -a "$TMPFILE" ]; then
    debugmsg "(compiler didn't produce output, bailing out)"
    exit 0
  fi
  # when we are here, the compiler didn't return an error, and it produced output.
  # if there was stdout or stderr output, we won't cache, else we will cache.
  if [ -s "$FASTTMPFILE" ]; then
    rm -f "$OUTPUT" # <-- make sure there is no directory named "$OUTPUT"
    mv "$TMPFILE" "$OUTPUT"
    exit 0
  else
    mv "$TMPFILE" "$HASHFILE"
  fi
else
  debugmsg "(getting result from cache)"
fi

# link or copy the result to the original output destination
rm -f "$OUTPUT" # <-- make sure there is no directory named "$OUTPUT"
# touching makes possible to erase cache based on mtime
# if LINKOUTPUT=yes touching is even neccessary for "make"
touch -c "$HASHFILE"
if [ "$LINKOUTPUT" = "yes" ]; then
  ln -s "$HASHFILE" "$OUTPUT"
else
  cp "$HASHFILE" "$OUTPUT"
fi

# good evening
exit 0
