Last active
June 14, 2019 18:26
-
-
Save gangeli/602d4eb82ee636154b83cb6550fd2f0a to your computer and use it in GitHub Desktop.
Incremental Java compiler
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# | |
# An incremental version of the `javac` command, intended to be backwards | |
# compatible but faster to invoke. | |
# | |
# Example Usage: | |
# | |
# ./javac_inc -d path/to/build /path/to/src/dir /path/to/other/src/dir | |
# | |
# Some minor differences from default Javac, intended to be features: | |
# | |
# - The default encoding for source files is UTF-8 | |
# - If a build directory does not exist, then it is created automatically | |
# | |
# @author <a href="mailto:gabor@eloquent.ai">Gabor Angeli</a> | |
# | |
# TODO(gabor): | |
# - Incrementally compile generated source files (`-s`) | |
# - Incrementally compile generated native headers (`-h`) | |
# - Read options from filename (`@<filename>`) | |
# - If we delete a _nested_ class from the build dir, we don't recompile it | |
# | |
set -o nounset | |
set -o errexit | |
# | |
# I. Prerequisites | |
# | |
# FN checkCmd | |
function checkCmd() { | |
command -v $1 >/dev/null 2>&1 || { | |
return 1 | |
} | |
} | |
# FN ensureCmd | |
function ensureCmd() { | |
command -v "$1" >/dev/null 2>&1 || { | |
if [ ! -z "${2+x}" ]; then | |
printf "%s\n" "The program '$1' is not in your path (or not executable)! $2"; | |
else | |
printf "%s\n" "The program '$1' is not in your path (or not executable)!"; | |
fi | |
return 1 | |
} | |
} | |
# Determine Javac | |
if [[ -z "${JAVAC+x}" ]]; then | |
if [[ ! -z "$JAVA_HOME" ]] && checkCmd "$JAVA_HOME/bin/javac"; then | |
JAVAC="$JAVA_HOME/bin/javac" | |
elif [[ ! -z "$JAVA_HOME" ]] && checkCmd "$JAVA_HOME\\bin\\javac"; then # windows | |
JAVAC="$JAVA_HOME\\bin\\javac" | |
elif checkCmd /usr/bin/javac; then | |
JAVAC="/usr/bin/javac" | |
elif checkCmd /bin/javac; then | |
JAVAC="/bin/javac" | |
elif [[ "`which javac`" == "$0" ]]; then | |
printf "Please define JAVA_HOME to prevent recursively calling '$0'\n" | |
else | |
JAVAC="`which javac`" | |
fi | |
fi | |
# Ensure prerequisite commands | |
ensureCmd "$JAVAC" "This is expected to be the path to javac." | |
ensureCmd find "GNU Find is needed to determine which files need to be compiled" | |
ensureCmd sort | |
ensureCmd head | |
ensureCmd cut | |
ensureCmd xargs | |
# Exit trap | |
javac_messages="" | |
function shutdown() { | |
if [[ "$javac_messages" != "" ]]; then | |
printf "\nn%s\n" "$javac_messages" | |
fi | |
} | |
trap shutdown EXIT | |
# | |
# II. Parse Arguments | |
# | |
# FN: show the help screen | |
function show_help() { | |
printf "Usage: `basename $0` <options> <source directories>\nwhere possible options include:\n" | |
cat <<EOF | |
-g Generate all debugging info | |
-g:none Generate no debugging info | |
-g:{lines,vars,source} Generate only some debugging info | |
-nowarn Generate no warnings | |
-verbose Output messages about what the compiler is doing | |
-deprecation Output source locations where deprecated APIs are used | |
-classpath <path> Specify where to find user class files and annotation processors | |
-cp <path> Specify where to find user class files and annotation processors | |
-sourcepath <path> Specify where to find input source files | |
-bootclasspath <path> Override location of bootstrap class files | |
-extdirs <dirs> Override location of installed extensions | |
-endorseddirs <dirs> Override location of endorsed standards path | |
-proc:{none,only} Control whether annotation processing and/or compilation is done. | |
-processor <class1>[,<class2>,<class3>...] Names of the annotation processors to run; bypasses default discovery process | |
-processorpath <path> Specify where to find annotation processors | |
-parameters Generate metadata for reflection on method parameters | |
-d <directory> Specify where to place generated class files | |
-s <directory> Specify where to place generated source files | |
-h <directory> Specify where to place generated native header files | |
-implicit:{none,class} Specify whether or not to generate class files for implicitly referenced files | |
-encoding <encoding> Specify character encoding used by source files | |
-source <release> Provide source compatibility with specified release | |
-target <release> Generate class files for specific VM version | |
-profile <profile> Check that API used is available in the specified profile | |
-version Version information | |
-help Print a synopsis of standard options | |
-Akey[=value] Options to pass to annotation processors | |
-X Print a synopsis of nonstandard options | |
-J<flag> Pass <flag> directly to the runtime system | |
-Werror Terminate compilation if warnings occur | |
@<filename> (unimplemented) Read options and filenames from file | |
EOF | |
} | |
# Reset in case getopts has been used previously in the shell. | |
OPTIND=1 | |
# Initialize defaults | |
java_opts="" | |
verbose=false | |
source_path="" | |
target_path="." | |
classpath="" | |
if [[ ! -z "${CLASSPATH+x}" ]]; then | |
classpath="$CLASSPATH" | |
fi | |
encoding='utf-8' | |
# Parse arguments | |
POSITIONAL=() | |
while [[ $# -gt 0 ]]; do | |
key="$1" | |
case $key in | |
-h|-help|--help) | |
show_help | |
exit 0 | |
;; | |
-version|--version) | |
"$JAVAC" -version | |
exit 0 | |
;; | |
-X) | |
"$JAVAC" -X | |
exit 0 | |
;; | |
-X*) | |
java_opts="$java_opts $key" | |
shift | |
;; | |
-g|-g:none|-g:lines|-g:vars|-g:source|-nowarn|-deprecation|-proc:none|-proc:only|-implicit:none|-implicit:class|-Werror) | |
java_opts="$java_opts -g" | |
shift | |
;; | |
-cp|-bootclasspath|-extdirs|-endorseddirs|-processor|-processorpath|-s|-h|-source|-target|-profile) | |
if [ -z "${2+x}" ]; then | |
printf "Flag $1 expects an argument\n" | |
exit 1 | |
fi | |
java_opts="$java_opts $1 $2" | |
shift | |
shift | |
;; | |
-v|verbose) | |
java_opts="$java_opts -verbose" | |
verbose=true | |
shift | |
;; | |
-encoding) | |
if [ -z "${2+x}" ]; then | |
printf "Flag $1 expects an argument\n" | |
exit 1 | |
fi | |
encoding="$2" | |
shift | |
shift | |
;; | |
-sourcepath) | |
if [ -z "${2+x}" ]; then | |
printf "Flag $1 expects an argument\n" | |
exit 1 | |
fi | |
source_path="$2/" | |
shift | |
shift | |
;; | |
-d) | |
if [ -z "${2+x}" ]; then | |
printf "Flag $1 expects an argument\n" | |
exit 1 | |
fi | |
target_path="$2" | |
java_opts="$java_opts $1 $2" | |
shift | |
shift | |
;; | |
-cp|-classpath) | |
if [ -z "${2+x}" ]; then | |
printf "Flag $1 expects an argument\n" | |
exit 1 | |
fi | |
classpath="$2" | |
shift | |
shift | |
;; | |
*) # unknown option | |
if [[ $1 == -A* ]]; then | |
java_opts="$java_opts $1" | |
elif [[ $1 == -J* ]]; then | |
java_opts="$java_opts $1" | |
elif [[ $1 == @* ]]; then | |
printf "ERROR: reading options from a filename is not yet supported.\n" | |
exit 2 | |
else | |
POSITIONAL+=("$1") # save it in an array for later | |
fi | |
shift | |
;; | |
esac | |
done | |
# restore positional parameters | |
if [ -z "${POSITIONAL+x}" ]; then | |
if [[ $verbose == true ]]; then # we probably meant to call `-version` | |
"$JAVAC" -version | |
exit 0 | |
else | |
printf "No source files/directories specified!\nUsage: `basename $0` <options> <source directories>\n" | |
exit 1 | |
fi | |
else | |
set -- "${POSITIONAL[@]}" | |
fi | |
# Handle encoding | |
java_opts="$java_opts -encoding $encoding" # we always specify an encoding | |
# Handle classpath | |
if [[ "$classpath" == "" ]]; then | |
classpath="$target_path" | |
else | |
classpath="$classpath:$target_path" | |
fi | |
java_opts="$java_opts -cp $classpath" | |
# Handle verbosity | |
if [[ $verbose == true ]]; then | |
printf "JAVA_OPTS =%s\n" "$java_opts" | |
printf "SRC_DIRS = %s\n" "$@" | |
printf "Verbose option set; setting xtrace\n" | |
set -o xtrace | |
fi | |
# | |
# III. Invoke Javac | |
# | |
printf "\033[K[%is] Compiling [ ]\r\033[K" $SECONDS | |
# FN check classfile | |
function checkClassfile() { | |
local src_file="$1" | |
local build_dir="$2" | |
if [[ "$src_file" != *package-info.java ]]; then | |
local target_file="${src_file%.java}.class" | |
while [[ "$target_file" != "" ]]; do | |
if [[ -e "$build_dir/${target_file#src}" ]]; then | |
return 0 | |
fi | |
local new_target_file="${target_file#*/}" | |
if [[ "$target_file" == "$new_target_file" ]]; then | |
target_file="" | |
else | |
target_file="$new_target_file" | |
fi | |
done | |
return 1 | |
else | |
return 0 | |
fi | |
} | |
# 1. Get source paths | |
source_paths="" | |
while [[ $# -gt 0 ]]; do | |
if [[ -e "$source_path$1" ]]; then | |
source_paths="$source_paths $source_path$1" | |
else | |
printf "No such source file or directory: %s\n" "$source_path$1" | |
exit 3 | |
fi | |
shift | |
done | |
if [[ $verbose == true ]]; then | |
printf "Resolved source paths to: %s" "$source_paths" | |
fi | |
if [[ ! -d "$target_path" ]]; then | |
mkdir -p "$target_path" | |
fi | |
# 2. Get changed files | |
printf "\033[K[%is] [==> ] Computing incremental compilation\r" $SECONDS | |
newest_target="`find $target_path -type f -print0 | xargs -0 stat -f "%m %N" | sort -rn | head -1 | cut -f2- -d" "`" | |
src_files=$(find $source_paths -type fl -name "*.java") | |
if [[ "$newest_target" == "" ]]; then | |
modified_java_files="$src_files" | |
else | |
modified_java_files=$(find $source_paths -type fl -name "*.java" -newer `find $target_path -type f -print0 | xargs -0 stat -f "%m %N" | sort -rn | head -1 | cut -f2- -d" "`) | |
# 3. Get deleted files | |
printf "\033[K[%is] [====> ] Finding deleted files\r" $SECONDS | |
while read -r src_file; do | |
if ! checkClassfile "$src_file" "$target_path"; then | |
if [[ "$modified_java_files" == "" ]]; then | |
modified_java_files="$src_file" | |
else | |
modified_java_files="$modified_java_files | |
$src_file" | |
fi | |
fi | |
done <<< "$src_files" | |
fi | |
# 4. Invoke Javac | |
if [[ "$modified_java_files" != "" ]]; then | |
# 4.1. Print that we're compiling | |
printf "\033[K[%is] [======> ] Compiling %i changed files\r" $SECONDS "`printf "$modified_java_files\n" | wc -l`" | |
# 4.2. Start javac | |
javac_messages=`"$JAVAC" $java_opts $modified_java_files 2>&1` & | |
pid=$! | |
# 4.3. Debug print | |
while kill -0 $pid >/dev/null 2>/dev/null; do | |
sleep 0.1 | |
printf "\033[K[%is] [=======> ] Compiling %i changed files\r" $SECONDS "`printf "$modified_java_files\n" | wc -l`" | |
done | |
fi | |
printf "\033[K[%is] [=========>] Done\n" $SECONDS | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment