compileAndGo
scripts in your favorite compiled language

Abstract

compileAndGo is a shebang header command that allows compiled-language source code such as Java and C++ to be used as an executable script. compileAndGo caches the compiled, executable file(s) out of sight and does make-like automatic recompilation. Wrapping source code in this way combines the convenience of a script-like, cross-platform executable source file with the benefits of a compiled language.

Overview

Here are some Hello World examples.

Writing a compileAndGo script

Java:

#!/usr/bin/env compileAndGo
language = java
!#

public class jello {
  public static void main(String[] args) {
    System.out.println("Hello, folks of World!");
  }
}

Mozilla Rhino JavaScript (compiled as Java .class files):

#!/usr/bin/env compileAndGo
compiler = jsc
!#

java.lang.System.out.println("Hello, folks of World!");

C:

#!/usr/bin/env compileAndGo
compiler = gcc
!#

#include <stdio.h>

int main(int argc, char* argv[]) {
  printf("Hello, folks of World!\n");
  return 0;
}

Running a script

Run the above scripts just like any other program:

1 Z% hello
Hello, folks of World!
2 Z% 

Run an ordinary source-code file like this:

1 Z% cg echo.java 'a b' c
"a b" c
2 Z% 

Feed some code into the standard input of cgs with compiler name as argument
(-jsc, -javac, -gcj, -cc, -g++, -gcc, -c99, -c89 -clang, or -clang++).

1 Z% echo 'print("Hello, " + arguments[0] + "!")' | cgs -jsc Neddy
Hello, Neddy!
2 Z% 

The Java compilers also require the main class name after the compiler argument to cgs.

1 Z% cgs -javac hello "folks" < hello.java
Hello, folks!
2 Z% 

Or, with the execute bit set, and with a little help from your shell, you will someday be able to do this:

1 Z% hello.c 'folks of World'
Hello, folks of World!
2 Z% 

More examples

There are also more-elaborate examples.

See examples for
Java   javago and modtime, which uses JNIDirect to wrap the stat system call
JavaScript jshello
C cgo and realpath, a simple wrapper for the system call of the same name. Also in the download are commands that wrap system calls to manipulate extended attributes on files: listxattr, getxattr, setxattr, removexattr, adapted from Apple sample code.

Download

You can copy and paste the compileAndGo code from this page, or you can download a compressed file that contains these web pages, the compileAndGo and cg scripts, many demo files, and a beta of JNIDirect.jar.

Parameters

Built-in parameters:

cacheDir Full path of the directory where the source and executables are cached
commandDir Path to the directory containing the compileAndGo script.

User-settable parameters:

Either language or compiler is required. All other parameters default to values appropriate for the compiler. Compilers known to compileAndGo and cg are javac, gcj, jsc, cc, g++, gcc, c89, c99, clang, clang++. (See note below on JavaScript.)

language=java Recommended: The language, so an IDE can help you edit the file. If specified, then implies an appropriate default compiler. Known langauges are java, c, c++, and javascript.
compiler=/usr/bin/javac Required (if language not supplied): The compiler. Full path not required. The compiler runs in the current directory, not in $cacheDir.
commandName=javago The name of your script. Exported to the environment. Defaults to the name by which the script is invoked (which can be expressed as ${1##*/}.
mainClass=$commandBasename For Java, the name of the main class. Defaults as shown.
sourceFilename=$mainClass.java The source code will be extracted from the compileAndGo script and put into the file $cacheDir/$sourceFilename. Defaults as appropriate for the compiler.
classpath=$commandDir/JNIDirect.jar For Java, include this in the classpath given to the compiler and the java program runner. Optional.
executableFilename=$mainClass.class Compiling the source file will produce at least the file $cacheDir/$sourceFilename. Defaults as shown.
compilerArgs=–d $cacheDir -sourcepath . $cacheDir/$sourceFilename Arguments to the compiler. Required for jikes if CLASSPATH environment variable is not set; otherwise optional.
linkerArgs= Arguments to the compiler after $compilerArgs. Applies only to C/C++ compilers.
execute=/usr/bin/java -cp $cacheDir:$commandDir/JNIDirect.jar $mainClass How to execute the program once it’s built. Defaults as appropriate for the compiler. When the compiler is javac, the path to the java program defaults to the same as the path given for javac (if any).
verbose=1 1 echoes "Compiling", 2 instead echoes the commands to compile and execute, 3 also shows variable values. Higher settings show internal debugging details. Defaults to 0.
!# A line containing only !# marks the end of the parameter settings. From the next line on, it’s source code.
compileAndGo A line starting with compileAndGo works to mark the end of the parameter settings, but this usage is deprecated and replaced by '!#'.

Tabs and spaces at beginning of line and around = signs are optional. Blank lines and comments beginning with # are ignored.

You can set and use whatever intermediate variables you may need, and you can export them.

Even though the compileAndGo program is implemented as a /bin/bash script, additional shell code in the parameter header beyond the setting of variables and echo will not work.

Elements of the cacheDir path include the modify time of the script, the compiler, and the OS and CPU architecture (as output by uname). Thus we isolate multiple versions of the source on multiple architectures and allow testing with multiple compilers without conflicts and spurious recompiles. When the script is run, if a cached executable exists in the appropriate cacheDir, we just run it; otherwise, we first make the cacheDir and compile the source there.If compiler is set to jsc

If compiler is set to javac, compileAndGo tells javac to parse for the highest language version it accepts (for example, javac -source 1.5). To determine this version, compileAndGo runs java -version.

If compiler is set to jsc, you have to put jsc.jar in your Java’s extensions folder (jre/ext/ or on Mac /Library/Java/Extensions), and compileAndGo doesn’t run a jsc command; rather, it invokes

java -cp $cacheDir${classPath:+:$classPath} org.mozilla.javascript.tools.jsc.Main $compilerArgs

The compileAndGo program

#!/bin/bash

# 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

# 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.
						

The trick

compileAndGo computes a hash (using cksum) of the source code from which it constructs a directory name (key), which it looks for in a directory (hashtable) of cached executables.

History

compileAndGo could have been and should have been written decades ago. I first thought of it in March, 2004, as a special-purpose technique for Java programs.

The future

A shell enhancement could allow you to invoke a source file as a command, similar to the way cg does.

A shell enhancement could allow a shell script to include functions written in compiled languages, using the same hash-of-source-code trick used by compileAndGo.

A special-case Java implementation could use a single JVM to do compiling and to run the compiled program. Might make a nice addition to the JDK.

If you would like to implement compileAndGo in a compiled language, please coordinate with me.

Cache Maintenance

To keep the cache clean, there is code at the end of the script you can turn on. After the invoked command has completed, it runs a background process that every once in a while looks around for old versions and unused versions and deletes them from the cache.

Testing

So far, compileAndGo has been tested only on Mac OS X 10.4.2 and two versions of linux.

Calling a #! from a #!

You will note that the compileAndGo scripts don’t just invoke

#!/usr/local/bin/compileAndGo

but instead they invoke compileAndGo via env, like this:

#!/usr/bin/env /usr/local/bin/compileAndGo

This is because compileAndGo is itself implemented as a #! script. For more information, see the Wikipedia article on Shebang


http://Yost.com/computers/compileAndGo/index.html - this page
2004-03-03 Created (as javago.html)
2005-01-24 Extracted into its own page
2005-07-24 compileAndGo implemented
2005-07-25 implemented defaulting
2005-07-26 full compiler path used in cacheDir
2005-07-26 Modified to use /usr/bin/env
2005-07-27 Modified2005-07-28 2.4 adds jikes, c89, c99, fixed bugs, including compatibility with three known versions of ls
2005-07-29 2.5 supports classpath and commandDir and exports commandName. Also, download link added
2005-07-30 3.0 with cg command
2005-07-30 3.1 cosmetic changes
2005-07-31 3.2 Support CLASSPATH setting in the parameters, echo, and bug fixes
2005-07-31 3.3 mention -compilerpath, removed /usr/local/bin/ prefix from all demos
2005-08-02 3.4 commandName is now optional.
2005-08-04 3.5 Mozilla Rhino JavaScript support added
2005-08-05 4.0 Pipe source code into new cgs variant of the cg command
2005-09-15 4.1 Preserve line numbers; use ~/.TemporaryItems/ on Mac; optional automatic cache cleaning
2005-10-25 4.2 Tells javac to parse for the highest source version it can accept
2008-11-17 4.3 language parameter added
2009-01-11 added description of the hash trick and shell function suggestion
2010-11-06 5.0 Added clang, clang++, added linkerArgs option
2010-11-06 5.0 Script should end with '!#' instead of 'compileAndGo', which is deprecated