#!/bin/bash
#   gast (Gcc Automatically -Save-Temps) : wrapper script for gcc to save temporary file.
#
#   Copyright (C) 2009 Tadashi Koike
#
#   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., 51 Franklin Street, Fifth Floor, Boston, MA  
#   02110-1301, USA.
#
#----------------------------------------------------------------------
# (I am weak in English. Please modify English comment more suitable. :T.K)
#
# [ Explanation of environment variable ]
#  - GAST_ENABLE
#        When this environment variable is defined (however excludes its 
#        value is "no"), this script works to generate compiler's temporary
#        file (.i file) automatically, and stores it.
#        When this environment variable is not defined, this script behaves
#        itself like just same as an original gcompiler binary.
#
#  - GAST_SAVE_DIR
#        When this environment variable is defined and its value can be
#        recognized a directory name, a genrerated temporary file (.i file)
#        by compiler is moved to that directory.
#        When that directory is not defined , this script attempt to create 
#        that directory.
#
#  - GAST_SOURCE_DIR
#        When this environment variable is defined and its value can be 
#        recognized a directory name, this script tries to get a relative 
#        path-name from a source file path-name by removing a directory 
#        name which this environment means. And that relative path-name 
#        applies under the GAST_SAVE_DIR directory to moved a temporary 
#        file (.i file).
#        When this environment variable is not defined or the value does
#        not match a header of source file path-name, this script get a 
#        relative path-name by removing "/" from a top of source file 
#        path-name.
#
#        ** NOTE **
#        When this environment variable is not defined but this script 
#        is executed under a build process by 'rpmbuild' command, this 
#        script sets this environment variable automatically with a
#        value of RPM_BUILD_DIR.
#
#  - GAST_CONTENT_WITHOUT_H
#        When this environment variable is defined (however excludes its 
#        value is "no"), created temporary file (.i file) is modified to
#        remove contents which are expanded from header files, and stored
#        as a substitute for an original temporary file (.i file).
#
#  - GAST_CONTENT_UNIQUE
#        By default, when a C source file is compiled repeatedly with 
#        different compile options each time, this script store each 
#        temporary files.
#        When this environment variable is defined (however excludes its
#        value is "no"), If compile options are different but content of
#        temporary file is same as one of some previous stored files, 
#        this script does not store temporary file at this time.

VERSION="0.1.0"
ORIGINAL_NAME="gast"
CWD=`pwd`
SCRIPT_NAME=${0##*/}
SCRIPT_PATH=${0%/*}

WORK_DIR="/tmp"

# If this script is executed under the rpmbuild command, GAST_SOURCE_DIR is
# forced to set value by RPM_BUILD_DIR environment variable's one.
[ -n "${RPM_BUILD_DIR}" ] && GAST_SOURCE_DIR="${RPM_BUILD_DIR}"
[ -n "${GAST_SOURCE_DIR}" ] && GAST_SOURCE_DIR=${GAST_SOURCE_DIR%/}

# When this script is executed as 'gast', this script creates symbolic link
# file name as compiler alias name and exit.
if [ "${SCRIPT_NAME}" = "${ORIGINAL_NAME}" ]; then
    for arg in "$@"
    do
        case "${arg}" in
        '--version'|'-v')
            echo "${ORIGINAL_NAME} ${VERSION}"
            exit 0
            ;;
        '--help'|'-h')
            cat <<EOL
usage : ${ORIGINAL_NAME} [-v|--version] [-h|--help]

  -h, --help              display this help and exit
  -v, --version           display version information and exit

  When this script is executed as '${ORIGINAL_NAME}' with no option, this script 
  creates symbolic link toward itself which name is from compiler (e.g. gcc/cc).

  When this script is executed as compiler, this script executes original 
  compiler with additional -save-temps option (If GAST_ENABLE is defined), and
  stores temporary files.

[ environment variables to control storing temporary files ]
    GAST_ENABLE              : add '-save-temps' option automatically
                               in compiling when this variable is defined.
    GAST_SAVE_DIR=<DIR>      : directory name temporary file is stored in.
    GAST_SOURCE_DIR=<DIR>    : top directory of source tree.
    GAST_CONTENT_WITHOUT_H : slim temporary files when this variable 
                               is defined.
EOL
            exit 0
            ;;
        *)
            echo "usage : ${ORIGINAL_NAME} [--version|-v] [--help|-h]"
            exit 1
            ;;
        esac
    done

    cd "${SCRIPT_PATH}"
    for cmd in gcc cc
    do
        if [ -e "${cmd}" ]; then
            file "${cmd}" 2>/dev/null | egrep 'symbolic link' >/dev/null 2>&1
            if [ "$?" -ne 0 ]; then
                echo "${ORIGINAL_NAME} : '${cmd}' exists as normal file or directory. Could not create symbolic link." >&2
                continue
            fi
        fi
        ln -sf "${ORIGINAL_NAME}" "${cmd}"
        if [ "$?" -eq 0 ]; then
            echo "${ORIGINAL_NAME} : Create symbolic link '${cmd}'" >&2
        else
            echo "${ORIGINAL_NAME} : Couldn't create symbolic link '${cmd}'" >&2
        fi
    done
    exit 0
fi

# get a directory path (full-path) of this script
case "${SCRIPT_PATH}" in
    '.')	SCRIPT_PATH="${CWD}" ;;
    \./*)	SCRIPT_PATH="${CWD}/${SCRIPT_PATH#*/}" ;;
    '~')	SCRIPT_PATH="${HOME}" ;;
    \~/*)	SCRIPT_PATH="${HOME}/${SCRIPT_PATH#*/}" ;;
    \/*)		:	;;
    *)		SCRIPT_PATH="${CWD}/${SCRIPT_PATH}" ;;
esac

# leaves out a script path name from PATH environment variable because of
# executing original compiler binary file in this script.
path_tmp=
for dir in ${PATH//:/ }
do
    [ "${dir}" = "${SCRIPT_PATH}" ] && continue
    [ "${dir}" = '.' ] && dir="${CWD}"

    if [ -z "${path_tmp}" ]; then
        path_tmp="${dir}"
    else
        path_tmp="${path_tmp}:${dir}"
    fi
done
export PATH=${path_tmp}
unset path_tmp

# When GAST_ENABLE does't exist, execute an original compiler and exit.
if [ "${GAST_ENABLE=no}" = "no" ]; then
    exec "${SCRIPT_NAME}" "$@"
fi

# Check whether C source file(s) is/are specified.
# And create new commandline arguments.
asm_notdelete_flg=
pipe_flg=
idx=0
idx_src=0
for arg in "$@"
do
    new_args[$idx]="${arg}"

    case "${arg}" in
    *.c)	src_list[$idx_src]="${arg}"
                let idx_src+=1
	;;
    '-S')	asm_notdelete_flg="YES"
	;;
    '-pipe')	pipe_flg="YES"
                new_args[$idx]="-save-temps"
	;;
    esac
    let idx+=1
done

# Compiling
if [ -z "${src_list[*]}" ]; then
    # No C source file was specified. -save-temps option does not need.
    exec "${SCRIPT_NAME}" "$@"
else
    # C source file(s) is/are specified. -save-temp option is set to argument.
    if [ -z "${pipe_flg}" ]; then
        "${SCRIPT_NAME}" -save-temps "$@"
    else
        "${SCRIPT_NAME}" "${new_args[@]}"
    fi
    RET=$?
fi


# modify a value of  GAST_SOURCE_DIR environment variable to full-path name.
# When GAST_SOURCE_DIR doesn't exist, define it and set null.
case "${GAST_SOURCE_DIR}" in
    '')		GAST_SOURCE_DIR="/" ;;
    '.')	GAST_SOURCE_DIR="${CWD}" ;;
    \./*)	GAST_SOURCE_DIR="${CWD}/${GAST_SOURCE_DIR#*/}" ;;
    '~')	GAST_SOURCE_DIR="${HOME}" ;;
    \~/*)	GAST_SOURCE_DIR="${HOME}/${GAST_SOURCE_DIR#*/}" ;;
    \/*)		:	;;
    *)	GAST_SOURCE_DIR="${CWD}/${GAST_SOURCE_DIR}" ;;
esac
GAST_SOURCE_DIR="${GAST_SOURCE_DIR%/}"
# NOTE : In above process, when user set GAST_SOURCE_DIR as "/" or
#         GAST_SOURCE_DIR doesn't exists, value of GAST_SOURCE_DIR is set to "".

# When GAST_CONTENT_WITHOUT_H environment variable exist, define a filter 
# function.
HAS_PERL=`which perl 2>/dev/null`

if [ "${GAST_CONTENT_WITHOUT_H=no}" != "no" ]; then
    if [ -n "${HAS_PERL}" ]; then
        function filter_tempfile_perl () {
            perl -e '
#----------(start temporary perl script)-------------------------
BEGIN {
  $prnt_flg=-1;
  $fname="'$1'";
}
while (<>) {
  if (/^#\s+(\d+)\s+"([^"]+)".*$/) {
      $line_no = $1;
      $cfile = $2;
      $cfile =~ s{^.*/}{};
      if ($cfile eq $fname) {
          print;
          $prnt_flg = $line_no if ($prnt_flg >= 0);
      } elsif ($cfile =~ /^</) {
          print;
          $prnt_flg = 0  if ($cfile eq "<built-in>");
      } else {
          if  ($prnt_flg == -1) {
              print;
          } elsif  ($line_no == 1) {
              print;
              $prnt_flg = 0 if ($prnt_flg >= 0);
          } else {
              $prnt_flg = 0;
          }
      }
  } elsif ($prnt_flg > 0) {
      print;
  }
}
#----------(end temporary perl script)---------------------------
'
        }

        function delete_added_info_perl () {
            perl -e '
#----------(start temporary perl script)-------------------------
BEGIN {
  $prnt_flg=0;
}
while (<>) {
  if ($prnt_flg != 0) {
      print;
  } elsif (/^#\s+(\d+)\s+"<compile options>/) {
      $prnt_flg=1;
  }
}
#----------(end temporary perl script)---------------------------
'
        }

    elif [ ${BASH_VERSION%%.*} -ge 3 ]; then
        function filter_tempfile_bash () {
            prnt_flg=-1
            fname="$1"
            IFS_SAVE=$IFS
            IFS=
            while read line
            do
                if [[ "${line}" =~ "^#[ \t]+([0-9]+)[ \t]+\"([^\"]+)\".*$" ]]; then
                    line_no=${BASH_REMATCH[1]}
                    cfile=${BASH_REMATCH[2]}
                    cfile=${cfile##*/}
                    #echo "$line_no : $cfile"
                    if [ "${cfile}" = "${fname}" ]; then
                        echo "${line}"
                        [ "${prnt_flg}" -ge 0 ] && prnt_flg="${line_no}"
                    elif [ "${cfile:0:1}" = "<" ]; then
                        echo "${line}"
                        [ "${cfile}" = "<built-in>" ] && prnt_flg=0
                    else
                        if [ "${prnt_flg}" -eq -1 ]; then
                            echo "${line}"
                        elif [ "${line_no}" -eq 1 ]; then
                            echo "${line}"
                            [ "${prnt_flg}" -ge 0 ] && prnt_flg=0
                        else
                            prnt_flg=0
                        fi
                    fi
                else
                    [ "${prnt_flg}" -gt 0 ] && echo "${line}"
                fi
            done
            IFS=$IFS_SAVE
        }

        function delete_added_info_bash () {
            prnt_flg=0
            IFS_SAVE=$IFS
            IFS=
            while read line
            do
                if [ "${prnt_flg}" -ne 0 ]; then
                    echo "${line}"
                elif [[ "${line}" =~ "^#[ \t]+[0-9]+[ \t]+\"<compile options" ]]; then
                    prnt_flg=1
                fi
            done
            IFS=$IFS_SAVE
        }
    fi
fi

# processes the compiler's temporary file
for src_file in "${src_list[@]}"
do
    # get full-path name of a C source file.
    fullname=
    case "${src_file}" in
    \/*)	fullname="${src_file}" ;;
    \~/*)	fullname="${HOME}/${src_file#*/}" ;;
    \./*)	fullname="${CWD}/${src_file#*/}" ;;
    *)		fullname="${CWD}/${src_file}" ;;
    esac

    # Get relative-path of C source file from GAST_SOURCE_DIR directory.
    basename="${fullname##*/}"	# C source filename
    dirname="${fullname%/*}"	# directory name of C source file (full-path)
    target="${basename/%.c/.i}"	# Compiler's temporary filename (.i file)
    asmfile="${basename/%.c/.s}"	# Compiler's temporary filename (.s file)
    src_relative_dir=		# directory name of C source file (relative-path from GAST_SOURCE_DIR)

    case "${fullname}" in
    ${GAST_SOURCE_DIR}/*)	# contain GAST_SOURCE_DIR=""
        if [ "${dirname}" = "${GAST_SOURCE_DIR}" ]; then
            src_relative_dir="."
        else
            src_relative_dir="${dirname#${GAST_SOURCE_DIR}/}"
        fi
        ;;
    *)
        # source file is not under the GAST_SOURCE_DIR directory.
        # Set the relative-path from '/' directory.
        src_relative_dir="${dirname#/}"
        ;;
    esac
    [ "${src_relative_dir:0:1}" = '/' ] && src_relative_dir="${src_relative_dir#/}"
    # Delete assembler temporary file (except that compiler is executed with -S option)
    [ -e "${asmfile}" -a -z "${asm_notdelete_flg}" ] && rm -f "${asmfile}" > /dev/null 2>&1

    if [ -e "${target}" ]; then
        if [ "${target}" = "conftest.i" ]; then
            # temporary file is created by 'configure' script as test.
            rm -f "${target}"
            continue
        fi

        # Add an information in the header of temporary file.
        # When filtering temporary file was specified, try it.
        tmpfile_org=
        until [ -n "${tmpfile_org}" -a ! -e "${tmpfile_org}" ]
        do
            tmpfile_org="${target}.${RANDOM}${RANDOM}${RANDOM}"
        done
        trap "rm -rf ${tmpfile_org} ${target} 2>/dev/null" HUP INT QUIT ABRT TERM XCPU

        mv "${target}" "${tmpfile_org}"
        if [ $? -eq 0 ]; then
            echo "# 1 \"<compile options> $@\"" > "${target}"
            #if [ -z "${pipe_flg}" ]; then
            #    echo "# 1 \"<modified compile options> -save-temps $@\"" >> "${target}"
            #else
            #    echo "# 1 \"<modified compile options> ${new_args[@]}\"" >> "${target}"
            #fi

            if [ "${GAST_CONTENT_WITHOUT_H=no}" = "no" ]; then
                cat "${tmpfile_org}" >> "${target}"
            else
                #echo "# 1 \"<GAST_CONTENT_WITHOUT_H>\"" >> "${target}"
                if [ -n "${HAS_PERL}" ]; then
                    filter_tempfile_perl "${basename}" < "${tmpfile_org}" >> "${target}"
                elif [ ${BASH_VERSION%%.*} -ge 3 ]; then
                    filter_tempfile_bash "${basename}" < "${tmpfile_org}" >> "${target}"
                else
                    cat "${tmpfile_org}" >> "${target}"
                fi
            fi
        fi

        # Check whether the GAST_SAVE_DIR has a value or not.
        # When it has a value, that is recognized as a directory name.
        # When such directory does not exist, try to create.
        if [ -n "${GAST_SAVE_DIR}" ]; then
            GAST_SAVE_DIR="${GAST_SAVE_DIR%/}"
            if [ ! -d "${GAST_SAVE_DIR}" ]; then
                if [ -e "${GAST_SAVE_DIR}" ]; then
                    echo "${ORIGINAL_NAME} : ${GAST_SAVE_DIR} is not a directory." >&2
                    GAST_SAVE_DIR=
                else
                    mkdir -p "${GAST_SAVE_DIR}"
                    if [ $? -ne 0 ]; then
                        echo "${ORIGINAL_NAME} : Couldn't create ${GAST_SAVE_DIR} directory." >&2
                        GAST_SAVE_DIR=
                    fi
                fi
            fi
        fi

        # define a directory path-name of temporary file to resotre.
        temp_dirname=
        if [ -n "${GAST_SAVE_DIR}" ]; then
            # resolve a directory path-name of a temporary file to move.

            if [ "${src_relative_dir}" = "." ]; then
                temp_dirname="${GAST_SAVE_DIR}"
            else
                temp_dirname="${GAST_SAVE_DIR}/${src_relative_dir}"
            fi

            # Check a directory to move a temporary file.
            if [ -e "${temp_dirname}" ]; then
                if [ ! -d "${temp_dirname}" ]; then
                    echo "${ORIGINAL_NAME}(${SCRIPT_NAME}): ${temp_dirname} is not a directory. Couldn't move ${target} to ${temp_dirname} ." >&2
                    temp_dirname=
                fi
            else
                # create a directory
                mkdir -p ${temp_dirname}
                if [ $? -ne 0 ]; then
                    echo "${ORIGINAL_NAME}(${SCRIPT_NAME}): Couldn't create ${temp_dirname} directory, so couldn't move ${target} to ${temp_dirname} ." >&2
                    temp_dirname=
                fi
            fi
        else
            temp_dirname=
        fi

        # set destination filename(full-path) to move a temporary file.
        loop_count=0
        skip_moving=
        dest_name=
        dest_file=
        if [ -n "${temp_dirname}" ]; then
            dest_name=${target}
            dest_file="${temp_dirname}/${dest_name}"
        else
            # When destination directory to store the temporary file doesn't
            # exist, try to rename temporary file as '*****.0.i', because
            # if some C source file is compiled over and over, old temporary
            # file is overwrited by new one. We wish to avoid that.
            dest_name="${target/%.i/.${loop_count}.i}"
            dest_file="${dest_name}"
        fi

        while [ -e "${dest_file}" ]
        do
            let loop_count+=1
            # software loop limit
            if [ "${loop_count}" -ge 500 ]; then
                echo "${ORIGINAL_NAME}(${SCRIPT_NAME}): loop abort.  give up to move ${target} to ${temp_dirname} ." >&2
                skip_moving="YES"
                break
            fi

            # destination file already exists. compare a content.
            cmp_result=
            if [ "${GAST_CONTENT_UNIQUE=no}" != "no" ]; then
                # comparison excluding "<compile option>" line.
                tmpfile_prev=
                until [ -n "${tmpfile_prev}" -a ! -e "${tmpfile_prev}" ]
                do
                    tmpfile_prev="${WORK_DIR}/${destname}.${RANDOM}${RANDOM}${RANDOM}"
                    tmpfile_cur="${WORK_DIR}/${target}.${RANDOM}${RANDOM}${RANDOM}"
                    trap "rm -rf ${tmpfile_prev} ${tmpfile_cur} 2>/dev/null" HUP INT QUIT ABRT TERM XCPU
                done

                if [ -n "${HAS_PERL}" ]; then
                    delete_added_info_perl < "${dest_file}" > "${tmpfile_prev}"
                    delete_added_info_perl < "${target}" > "${tmpfile_cur}"
                elif [ ${BASH_VERSION%%.*} -ge 3 ]; then
                    delete_added_info_bash < "${dest_file}" > "${tmpfile_prev}"
                    delete_added_info_bash < "${target}" > "${tmpfile_cur}"
                fi
                if [ -e "${tmpfile_prev}" -a -e "${tmpfile_cur}" ]; then
                    diff --brief "${tmpfile_prev}" "${tmpfile_cur}" > /dev/null 2>&1
                    cmp_result=$?
                else
                    # failed to compare. 
                    cmp_result=1
                fi
                rm -f "${tmpfile_prev}" "${tmpfile_cur}" > /dev/null 2>&1
                trap - HUP INT QUIT ABRT TERM XCPU
            else
                # comparison including "<compile option>" line.
                diff --brief "${dest_file}" ${target} > /dev/null 2>&1
                cmp_result=$?
            fi

            if [ "${cmp_result}" -eq 0 ]; then
                # delete current temporary file.
                inode_from=`ls -i "${target}" | sed -e 's/\([0-9][0-9]*\)[^0-9]*/\1/'`
                inode_to=`ls -i "${dest_file}" | sed -e 's/\([0-9][0-9]*\)[^0-9]*/\1/'`
                [ "${inode_from}" != "${inode_to}" ] && rm -f "${target}"

                skip_moving="YES"
                break
            else
                # same file already exists and contents is different.
                # try to store this temporary file with another filename
                # by using ${loop_count} number.
                dest_name="${target/%.i/.${loop_count}.i}"
                if [ -n "${temp_dirname}" ]; then
                    dest_file="${temp_dirname}/${dest_name}"
                else
                    dest_file="${dest_name}"
                fi
            fi
        done

        # move a temporary file to a save-directory.
        if [ -z "${skip_moving}" ]; then
            mv -f "${target}" "${dest_file}" 2>/dev/null
            [ $? -ne 0 ] && echo "${ORIGINAL_NAME}(${SCRIPT_NAME}): Couldn't move ${target} to ${dest_file} ." >&2
        fi

        # Delete original temporary file.
        rm -f "${tmpfile_org}"
        trap - HUP INT QUIT ABRT TERM XCPU

    fi	#if [ -e "${target}" ]; then
done

# return with compiler's return code
exit "${RET}"
