nutools/compileAndGo

626 lines
18 KiB
Bash
Executable File

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