#!/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. [ 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 '1{s/.*"\(.*\)".*/\1/; s/^1\.//; s/\..*//; 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 # 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.