#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
# See notice of copyright and license at end.

# If you use this program for a #! script, please include the following
# as the second line of the script:
# See http://Yost.com/computers/compileAndGo

# Bug: doesn't recompile if invoked file is a symlink. Fixed?
# Needs a call to realpath.

## Bug: The following fails for the cg command after fixing it to work for compileAndGo
# Something about evaluation order of the variables in the source header.
# compiler = gcc
# compilerArgs = -O2 -o $cacheDir/$executableFilename $cacheDir/$sourceFilename

if [ $# == 1 -a "$1" == --help ]; then
    echo "compileAndGo: see http://Yost.com/computers/compileAndGo"
    exit 0
fi

# The #! compileAndGo script that invoked us.
# If $0 is a symlink, this is the invoked name, not the symlink referent name
typeset -r commandFile="$1"

pathOfDirPartOfExecPath() {
  if [[ -h "$1" ]] ; then
    local lsout="$(ls -l "$1")"
    local referent="${lsout#* -> }"
    pathOfDirPartOfExecPath "$referent"
  elif [[ -d "${1%/*}" ]] ; then
    echo "${1%/*}"
  else
    echo .
  fi
}
typeset -r commandDir="$(pathOfDirPartOfExecPath "$0")/"
# If $0 is a symlink, this is the invoked name, not the symlink referent name

ourName=${0##*/}

#-------------------------------------------------------------------------------

extensionToLanguage() {
  case "$1" in
  js)
    echo javascript
    ;;
  java)
    echo java
    ;;
  c)
    echo c
    ;;
  cp | cpp | C | cxx | cc)
    echo c++
    ;;
  *)
    ;;
  esac
}

languageToCompiler() {
  case "$1" in
  javascript)
    echo jsc
    ;;
  java)
    echo javac
    ;;
  c)
    if [[ -x /usr/bin/gcc ]] ; then
      echo /usr/bin/gcc
    else
      echo /usr/bin/cc
    fi
    ;;
  c++)
    echo /usr/bin/g++
    ;;
  *)
    ;;
  esac
}

echo2() {
  # This works for sh, ksh, and bash, but not zsh.
  echo "$@"
}

echoVariables() {
  echo 1>&2 $1
  if [[ ! $ourName == cg ]] ; then
    # Echo all but the builtins.
    echo 1>&2 "$(
      eval "$(
        echo "$cmdSetters" \
        | grep -v 'commandBasename
commandDir
sourceFilename
language
mainClass
executableFilename
compilerDir
classPath
compilerArgs
linkerArgs
execute
firstArgIsCommandName
verbose' \
        | sed "
            s,^eval,echo,
            s,=, = ,
            s,',,g
          "
      )" \
      | sed "
          s,= ,= ',
          s,$,',
        "
    )"
  else
    echo 1>&2 commandName         = "$commandName"
    echo 1>&2 compiler            = "$compiler"
  fi
  # Echo the builtins.
  echo 1>&2 commandBasename       = "$commandBasename"
  echo 1>&2 commandDir            = "$commandDir"
  echo 1>&2 sourceFilename        = "$sourceFilename"
  echo 1>&2 language              = "$language"
  echo 1>&2 mainClass             = "$mainClass"
  echo 1>&2 executableFilename    = "$executableFilename"
  echo 1>&2 compilerDir           = "$compilerDir"
  echo 1>&2 classPath             = "$classPath"
  echo 1>&2 compilerArgs          = "$compilerArgs"
  echo 1>&2 linkerArgs            = "$linkerArgs"
  echo 1>&2 execute               = "$execute"
  echo 1>&2 firstArgIsCommandName = "$firstArgIsCommandName"
  echo 1>&2 verbose               = "$verbose"
}

#-------------------------------------------------------------------------------

# If we use zsh, we could do this:
# zmodload zsh/stat
# stat -F "%Y-%m-%d_%H-%M-%S" +mtime .

ls-linux() {
  # 11742 2005-07-28 11:54:01.000000000
  (
    ls -dl --full-time --time-style=full-iso "$1" \
    | sed 's,.*[0-9] \([12][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]\) \([0-9][0-9]\):\([0-9][0-9]\):\([0-9][0-9]\).[0-9]* .*,\1.\2-\3-\4,'
  ) 2> /dev/null
}

ls-oldlinux() {
  # 11742 Tue Mar 01 17:22:50 2005
  (
    ls -dl --full-time "$1" \
    | sed 's,.*[0-9] ... \(...\) \([0-9][0-9]\) \([0-9][0-9]\):\([0-9][0-9]\):\([0-9][0-9]\) \([12][0-9][0-9][0-9]\) .*,\4-\1-\2.\3-\5-\6,'
  ) 2> /dev/null
}

ls-bsd() {
  # 11742 Jul 28 12:38:31 2005
  (
    ls -dlT "$1" \
    | awk '
      BEGIN {
        months["Jan"] = "01" ; months["Feb"] = "02" ; months["Mar"] = "03"
        months["Apr"] = "04" ; months["May"] = "05" ; months["Jun"] = "06"
        months["Jul"] = "07" ; months["Aug"] = "08" ; months["Sep"] = "09"
        months["Oct"] = "10" ; months["Nov"] = "11" ; months["Dec"] = "12"
      }
      {
        month = sprintf(months[$6]) ; day = $7 ; time = $8 ; year = $9 # What about Europe?
        gsub(":", "-", time)
        date = year     "-" month     "-" day     "_" time
        print date
      }
    '
  ) 2> /dev/null
}

#-------------------------------------------------------------------------------

set -e

sourceInput=
case $ourName in
cg)
  # Invoked as cg
  if [[ $# == 0 ]] ; then
    echo 1>&2 "Usage: cg sourcefile.<extension> [ args ... ]"
    exit 2
  fi
  sourceInput="$1"
  export commandName=${commandFile##*/}
  commandBasename="${commandName%.*}"
  commandExtension="${commandFile##*.}"
  sourceFilename=$commandName
  language=$(extensionToLanguage $commandExtension)
  compiler=$(languageToCompiler $language)
  ;;
cgs)
  sourceInput=/dev/stdin
  export commandName=$ourName
  commandBasename=$commandName
  compilerOpt="$1"
  case "$compilerOpt" in
  -jsc)
    sourceFileName=$commandName.js
    ;;
  -javac | -gcj | -jikes)
    shift
    export commandName=$1
    commandBasename=$commandName
    sourceFileName=$commandName.java
    ;;
  -gcc | -c99 | -c89 | -cc)
    sourceFileName=$commandName.c
    ;;
  -g++)
    sourceFileName=$commandName.cp
    ;;
  "")
    echo 1>&2 cgs: missing compiler option
    exit 2
    ;;
  *)
    echo 1>&2 cgs: unknown compiler option: "$compilerOpt"
    exit 2
    ;;
  esac
  compiler=${compilerOpt/-/}
  ;;
*)
  sourceInput=
  # Invoked as compileAndGo
  # Collect the variable declarations at the top of $commmandFile.
  declarations="$(
    sed -n '
      s,^[   ]*,,
      /^!#$/q
      /^compileAndGo/q
      /^[   ]*#/d
      s,^commandName,export commandName,
      /^echo[   ]/{
        s/$/ ;/p
        d
      }
      s,\$\({*commandBasename\),\\$\1,g
      s,\$\({*commandDir\),\\$\1,g
      s,\$\({*sourceFilename\),\\$\1,g
      s,\$\({*language\),\\$\1,g
      s,\$\({*mainClass\),\\$\1,g
      s,\$\({*executableFilename\),\\$\1,g
      s,\$\({*compilerDir\),\\$\1,g
      s,\$\({*classPath\),\\$\1,g
      s,\$\({*compilerArgs\),\\$\1,g
      s,\$\({*linkerArgs\),\\$\1,g
      s,\$\({*execute\),\\$\1,g
      s,\$\({*cacheDir\),\\$\1,g
      s,\$\({*firstArgIsCommandName\),\\$\1,g
      s,[   ]*=[   ]*,=",
      s,$," ;,
      p
    ' "$commandFile"
  )"
  eval $declarations
  [[ ! -z ${commandName:="${1##*/}"} ]]
  commandBasename="${commandName%.*}"
  if (( 0$verbose >= 5 )) ; then
    echo 1>&2 \=== Declarations
    echo 1>&2 "$declarations"
  fi
  if [[ -z "$compiler" ]] ; then
    if [[ -z "$language" ]] ; then
      echo 1>&2 compileAndGo: compiler or language must be set
      trouble=true
    fi
    compiler=$(languageToCompiler $language)
  fi
  if [[ ! -z "$trouble" ]] ; then
    exit 2
  fi
  ;;
esac

#-------------------------------------------------------------------------------
# Collect the source code

newsed() {
  local arg
  if sed --regex-extended < /dev/null >& /dev/null ; then
    arg=--regex-extended
  else
    arg=-E
  fi
  sed $arg "$@"
}

case "$sourceInput" in
/dev/stdin)
  # from stdin
  sourceCode=$(cat)
  ;;
'')
  # from the end of $commandFile
  sourceCode=$(
    newsed '
      1,/^(!#|[   ]*compileAndGo)/s,.*,,
    ' "$commandFile"
  )
  if [[ -z "$sourceCode" ]] ; then
    echo 1>&2 "$commandName: Missing '#!compileAndGo' line before source code starts."
    exit 2
  fi
  ;;
*)
  # from the filename as first argument
  sourceCode=$(cat $1)
  ;;
esac

#-------------------------------------------------------------------------------
# Construct the cacheDir variable.

abi=$(uname -sm)
abi=${abi// /-}

# Why must I use `` instead of $() here?
id=`
  case "$sourceInput" in
  /dev/stdin)
    local tmp=($(echo "$sourceCode" | cksum))
    echo ${tmp[0]}${tmp[1]}
    ;;
  *)
    case "$abi" in
    Linux* | CYGWIN*)
      ls-linux "$1" \
      || ls-oldlinux "$1"
      ;;
    *)
      ls-bsd "$1" \
      || ls-linux "$1" \
      || ls-oldlinux "$1"
      ;;
    esac \
    || (
      local tmp=($(echo "$sourceCode" | cksum))
      echo ${tmp[0]}${tmp[1]}
    )
    ;;
  esac
`
compilerPath=$(type -p "$compiler" 2> /dev/null) || compilerPath=$compiler
realHOME=$(eval 'echo ~'$(whoami))

if [[ -x $realHOME/Library/Caches ]] ; then
  # Mac OS X
  cacheDirRoot=$realHOME/Library/Caches/CompileAndGo
else
  cacheDirRoot=$realHOME/.compileAndGo
fi
cacheDirParent=$cacheDirRoot/${commandName}
cacheDir=$cacheDirParent/$abi/${id}_${compilerPath//\//-}

#-------------------------------------------------------------------------------
# Apply defaults and then set the variables again.

compilerName=${compiler##*/}

# Some settings common among different compiler groups:
case $compilerName in
javac* | jikes*)
  [[ ! -z ${mainClass:="$commandBasename"} ]]
  [[ ! -z ${sourceFilename:="${mainClass}.java"} ]]
  ;;
gcj*)
  [[ ! -z ${mainClass:="$commandBasename"} ]]
  [[ ! -z ${sourceFilename:="${mainClass}.java"} ]]
  [[ ! -z ${executableFilename:="$commandBasename"} ]]
  [[ ! -z ${execute:="PATH=$cacheDir:$PATH $executableFilename"} ]]
  ;;
gcc* | g++* | c89* | c99*)
  [[ ! -z ${executableFilename:="$commandBasename"} ]]
  [[ ! -z ${execute:="PATH=$cacheDir:$PATH $executableFilename"} ]]
  ;;
esac

case $compilerName in
jsc*)
  [[ ! -z ${mainClass:="$commandBasename"} ]]
  [[ ! -z ${sourceFilename:="${mainClass}.js"} ]]
  [[ ! -z ${executableFilename:="${mainClass}.class"} ]]
  [[ ! -z "${execute:="${javaBinDir}java -cp $cacheDir${classPath:+:$classPath} $mainClass"}" ]]
  [[ ! -z "${compilerArgs:="-o $executableFilename $cacheDir/$sourceFilename"}" ]]
  compileCmd="java -cp $cacheDir${classPath:+:$classPath} org.mozilla.javascript.tools.jsc.Main $compilerArgs"
  ;;
javac* | jikes*)
  [[ ! -z ${executableFilename:="${mainClass}.class"} ]]
  sourceVersion=
  case $compilerName in
  javac*)
    if [[ $compilerName == $compiler ]] ; then
      compilerDir=
    else
      compilerDir=${compiler%/*}/
    fi
    [[ ! -z "${execute:="${compilerDir}java -cp $cacheDir${classPath:+:$classPath} $mainClass"}" ]]
    # Prepare to tell javac to compile for the latest language version it supports
    sourceVersion="-source $(${compilerDir}java -version 2>&1 | sed -n '1s,[^"]*"\([1-9][1-9]*\.[1-9][1-9]*\).*,\1,p')"
    ;;
  jikes*)
    if [[ -z "$classPath" && -z "$compilerArgs" && -z "$CLASSPATH" ]] ; then
      # Mac: export CLASSPATH=/System/Library/Frameworks/JavaVM.framework/Classes/classes.jar
      echo 1>&2 compileAndGo: for jikes, if neither classPath nor CLASSPATH are set, compilerArgs must be set.
      exit 2
    fi
    [[ ! -z "${execute:="java -cp $cacheDir${classPath:+:$classPath} $mainClass"}" ]]
    ;;
  esac
  [[ ! -z "${compilerArgs:="$sourceVersion -d $cacheDir -sourcepath . ${classPath:+-classpath $classPath} $cacheDir/$sourceFilename"}" ]]
  compileCmd="$compiler $compilerArgs"
  ;;
gcj*)
  [[ ! -z ${compilerArgs:="--main=$mainClass -o $cacheDir/$commandBasename $cacheDir/$sourceFilename"} ]]
  compileCmd="$compiler $compilerArgs $linkerArgs"
  ;;
gcc* | g++* | c89* | c99* | clang | clang++)
  case $compilerName in
  cc* | gcc* | c89* | c99* | clang)
    [[ ! -z ${sourceFilename:="${commandName}.c"} ]]
    ;;
  g++* | clang++)
    [[ ! -z ${sourceFilename:="${commandName}.cp"} ]]
    ;;
  esac
  [[ ! -z ${compilerArgs:="-O2 -o $cacheDir/$executableFilename $cacheDir/$sourceFilename"} ]]
  compileCmd="$compiler $compilerArgs $linkerArgs"
  ;;
esac

#-------------------------------------------------------------------------------
# Set the variables

if [[ ! $ourName == cg ]] ; then
  vars=$(
    echo "$declarations" \
    | sed -n 's,\([^=]*\)=.*,\1,p'
  )
  cmdSetters=$(
    for x in $vars
    do
      echo eval "'"${x}='"'$(eval echo \$$x)'"'"'"
    done
  )
  eval "$cmdSetters"
fi
if (( 0$verbose >= 3 )) ; then
  echoVariables "=== the variables before defaults"
fi
if [[ $ourName == cg ]] ; then
  if (( 0$verbose >= 4 )) ; then
    echo 1>&2 \=== eval command to set variables
    echo2 1>&2 eval "$cmdSetters"
  fi
fi

#-------------------------------------------------------------------------------
# Check that all the required variables are set.
for x in sourceFilename executableFilename compilerArgs execute
do
  eval 'if [ -z "'\$$x'" ] ; then trouble=true ; fi'
done
if [[ ! -z "$trouble" ]] ; then echo 1>&2 compileAndGo: unknown compiler setting "$compiler" ; exit 2 ; fi
for x in sourceFilename executableFilename compilerArgs execute
do
  eval 'if [ -z "'\$$x'" ] ; then echo 1>&2 compileAndGo: $x must be set ; fi'
done
if [[ ! -z "$trouble" ]] ; then exit 2 ; fi

[[ ! -z ${firstArgIsCommandName:=false} ]]
[[ ! -z ${verbose:=0} ]]

eval "$cmdSetters"

if (( 0$verbose >= 3 )) ; then
  echoVariables "=== the variables after defaults"
fi

#set -x

#-------------------------------------------------------------------------------
# Compile if necessary

# shorthand
cachedExecutable=$cacheDir/$executableFilename

# The security precautions go like this:
# The executable and the folder in which it resides are
# * owned by user
# * 700 permissions (rwx------)
# The important facts are:
# * Only the user or root can chmod a file or folder owned by him.
# * Only the user or root can write into a file or folder that is 700.
# * Only root can chown a file or folder to the user.
# so only the user or root can construct a suitable file in the suitable
# folder.  No one else can.  That's about as good as it can get on unix.
# The attack would be limited to finding some existing folder containing
# an executable of the correct name, both owned by the user and 700,
# then moving the folder into the appropriate path.
# The implementation should be expanded to require that all folders from
# $cacheDir through $cacheDirParent must be owned by user and be 700.
if [[ ! -O $cachedExecutable ]] ; then
  if [[ -e $cachedExecutable ]] ; then
    echo 1>&2 "$commandName: Aborting because $cachedExecutable exists,"
    echo 1>&2 "$commandName: and you don't own it."
    echo 1>&2 "$commandName: This is a possible security violation."
    exit 2
  fi

  # Try to make it harder for others to tamper with our cache.
  umask 077
  # Insist that $cacheDirParent is a directory and is owned by the user.
  if [[     -d $cacheDirParent ]] ; then
    if [[ ! -O $cacheDirParent ]] ; then
      echo 1>&2 "$commandName: Aborting because $cacheDirParent/ exists, and you don't own it."
      echo 1>&2 "$commandName: This is a security risk."
      exit 2
    fi
    chmod 700 $cacheDirParent
  else
    mkdir -p $cacheDirParent
    echo > $cacheDirParent/../README "See http://Yost.com/computers/compileAndGo"
  fi

  mkdir -p $cacheDir

  # Compile the source.
  if (( 0$verbose == 1 )) ; then
    echo 1>&2 "[ $commandName: compiling. ]"
  elif (( 0$verbose >= 2 )) ; then
    echo  -n 1>&2 "[ "
    echo2 -n 1>&2    "$compileCmd"
    echo     1>&2                " ]"
  fi
  echo "$sourceCode" > $cacheDir/$sourceFilename
  eval $compileCmd

  # Make a canonical name we can look at to determine access time.
  ln -f $cachedExecutable $cacheDir/.executable
fi

#-------------------------------------------------------------------------------
# Execute the built program.

if [[ "$firstArgIsCommandName" != true ]] ; then
  shift
fi

if (( 0$verbose >= 2 )) ; then
  echo  -n 1>&2 "[ "
  echo2 -n 1>&2    $execute "$@"
  echo     1>&2                " ]"
fi
eval "$execute"' "$@"'
status=$?

if [[ true ]] ; then
  # Run a background task to clean the cache occasionally.
  (
    # Check every this-many days.
    checkInterval="-mtime -7"
    # Max number of days a version cam be unused and not be removed.
    maxAge="-atime +14"
    # Every $checkInterval days, remove stuff not used in $maxAge days.
    stamp=$(nice -n 20 find $cacheDirRoot -maxdepth 1 -name .timestamp $checkInterval)
    if [[ ! -z "$stamp" ]] ; then
      # Too soon
      exit
    fi
    nice -n 20 touch $cacheDirRoot/.timestamp
    # Remove dirs of executable versions not accessed in the last $maxAge days.
    candidates=$(nice -n 20 find $cacheDirRoot -mindepth 3 -name .executable $maxAge)
    if [[ ! -z "$candidates" ]] ; then
      #echo "$candidates"
      echo "$candidates" \
      | nice -n 20 sed 's,/.executable,,' \
      | nice -n 20 xargs rm -rf
    fi
  ) \
  > /dev/null 2>&1 \
  &
fi

exit $status

# Copyright 2005-2010 Dave Yost <Dave@Yost.com>
# All rights reserved.
# This version is
#   compileAndGo 5.0 2010-11-06
# which at time of this publication can be found at:
#   http://Yost.com/computers/compileAndGo
# Redistribution and use in the form of source code or derivative data built
# from the source code, with or without modification, are permitted provided
# that the following conditions are met:
# 1. THE USER AGREES THAT THERE IS NO WARRANTY.
# 2. If and only if appropriate, the above phrase "This version is" must be
#    followed by the phrase "a modified form of" or "extracted from" or
#    "extracted and modified from".
# 3. Redistributions of source code must retain this notice intact.
# 4. Redistributions in the form of derivative data built from the source
#    code must reproduce this notice intact in the documentation and/or other
#    materials provided with the distribution, and each file in the derivative
#    data must reproduce any Yost.com URI included in the original distribution.
# 5. Neither the name of Dave Yost nor the names of its contributors may be
#    used to endorse or promote products derived from this software without
#    specific prior written permission.
# 6. Written permission by the author is required for redistribution as part
#    of a commercial product.
# This notice comprises all text from "Copyright" above through the end of
# this sentence.
