Skip to content

Instantly share code, notes, and snippets.

@gangeli
Last active June 14, 2019 18:26
Show Gist options
  • Save gangeli/602d4eb82ee636154b83cb6550fd2f0a to your computer and use it in GitHub Desktop.
Save gangeli/602d4eb82ee636154b83cb6550fd2f0a to your computer and use it in GitHub Desktop.
Incremental Java compiler
#!/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