--- /dev/null
+#!/bin/env bash
+#
+# Aardvark Build Tool
+# A simple Scons/RScons inspired build system written in Bash (>= 4.0)
+#
+# Author: Michael D. Lowis
+# Copyright: 2017
+# License: 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause)
+
+# usage
+# Prints helpful usage information
+usage(){
+ echo "Usage: $0 <action>"
+ echo ""
+ echo "build - Build the project"
+ echo "clean - Cleanup all build artifacts"
+ echo "config - Configure the project for the current system"
+ echo "help - Print this help message"
+ echo "install - Install the files to the system"
+ echo "uninstall - Uninstall previously installed files"
+ exit 1
+}
+
+# default_env
+# Reset command flags and variables back to the default settings.
+default_env(){
+ # Default C compiler setup
+ CC=cc
+ CCCMDFLAGS=(-MMD -c -o)
+ INCPREFIX=-I
+ INCS=()
+ DEFPREFIX=-D
+ DEFS=()
+ CFLAGS=()
+ CPPFLAGS=()
+
+ # Default linker setup
+ LD=cc
+ LDCMDFLAGS=(-o)
+ LIBPREFIX=-l
+ LIBS=()
+ LIBPATHPREFIX=-L
+ LIBPATHS=()
+
+ # Default archiver setup
+ AR=ar
+ ARFLAGS=("rcs")
+}
+
+#------------------------------------------------------------------------------
+
+# add_prefix PREFIX [ITEM...]
+# Prepend each ITEM with PREFIX
+add_prefix(){
+ local prefix="$1"
+ for e in "${@:2}"; do
+ echo "$prefix$e"
+ done
+}
+
+# add_suffix SUFFIX [ITEM...]
+# Append SUFFIX to each ITEM
+add_suffix(){
+ local suffix="$1"
+ for e in "${@:2}"; do
+ echo "$e$suffix"
+ done
+}
+
+# runcmd CMD [ARG...]
+# Prints CMD to run and runs it with the ARG list provided
+runcmd(){
+ printf '%s ' "$@"
+ printf '\n'
+ "$@" || finish $?
+}
+
+# checksum FILE
+# Returns the checksum of FILE as generated by the cksum command
+checksum(){
+ if [[ -z "${CHECKSUMS[$1]}" ]]; then
+ CHECKSUMS["$1"]="$(cksum "$1" | cut -d ' ' -f1)"
+ fi
+ echo "${CHECKSUMS[$1]}"
+}
+
+# checksum_mismatch FILE
+# Determines if the
+checksum_mismatch(){
+ [[ "$(checksum "$1")" != "${CACHE_CHECKSUMS[$1]}" ]]
+}
+
+# cache_update [FILE...]
+# Calculates and caches a checksum of each FILE
+cache_update(){
+ for f in "$@"; do
+ CHECKSUMS[$f]=$(checksum "$f")
+ done
+}
+
+# parse_deps VAR TARGET [DEP...]
+# Parses a dependency file for TARGET and combines the dependencies in the file
+# with the DEP list provided. The combined list is the deduplicated and each
+# entry is checksumed. The list is then returned to the caller in an array
+# named VAR.
+parse_deps(){
+ local resvar="$1"
+ local depfile="${2%.*}.d"
+ if [[ -f "$depfile" ]]; then
+ mapfile -t deps < <(tr -s ': \\' '\n' < "$depfile")
+ fi
+ local combined=("${@:3}" "${deps[@]:2}")
+ declare -A filtered
+ for f in "${combined[@]}"; do
+ if [[ $f != /* ]]; then
+ filtered["$f"]=1;
+ CHECKSUMS[$f]=$(checksum "$f")
+ fi
+ done
+ eval "$resvar=(${!filtered[@]})"
+}
+
+# sources_changed TARGET [SRC...]
+# Determines if the sources for the TARGET have changed. The provided list of
+# SRC files is combined with any dependencies listed in an on-disk dependency
+# file for the TARGET. The resulting list is checked for any checksum mismatches.
+sources_changed(){
+ local DEPS=()
+ parse_deps DEPS "$@"
+ for f in "${DEPS[@]}"; do
+ if checksum_mismatch "$f"; then
+ return 0
+ fi
+ done
+ return 1
+}
+
+# should_rebuild TARGET [SRC...]
+# Determines if TARGET should be rebuilt. Returns 1 if TARGET does not exist
+# or if any SRC provided or any implicit dependency of a provided SRC fails
+# a checksum match.
+should_rebuild(){
+ [[ ! -f "$1" ]] || sources_changed "$@"
+}
+
+# build_sources VAR [SRC...]
+# Builds each SRC using the Object function. The resulting list of object files
+# is returned to the caller in an array named VAR.
+build_sources(){
+ local resvar="$1"
+ local srcs=()
+ for s in "${@:2}"; do
+ local obj="${s%.*}.o"
+ if [[ $s == *.c ]]; then
+ Object "$obj" "$s"
+ srcs+=("$obj")
+ else
+ srcs+=("$s")
+ fi
+ done
+ eval "$resvar=(${srcs[@]})"
+}
+
+# finish RETCODE
+# Finishes the build process with an exit status of RETCODE. This function is
+# called to signal a successful or unsuccessful completion of the build process.
+# It is responsible for ensuring that the cache file is written before the
+# process exits.
+finish(){
+ echo "CACHE_CHECKSUMS=(" > cache.sh
+ for file in "${!CHECKSUMS[@]}"; do
+ echo " ['$file']='${CHECKSUMS[$file]}'" >> cache.sh
+ done
+ echo ")" >> cache.sh
+ exit "$1"
+}
+
+#------------------------------------------------------------------------------
+
+# cccmd TARGET [SRC...]
+# Execute the compiler to produce TARGET from the list of SRCs. TARGET is
+# assumed to be an object file.
+cccmd(){
+ local cmd=(
+ "$CC" "${CCCMDFLAGS[@]}" "$1" "${CFLAGS[@]}" "${CPPFLAGS[@]}"
+ $(add_prefix "$INCPREFIX" "${INCS[@]}")
+ $(add_prefix "$DEFPREFIX" "${DEFS[@]}")
+ ${@:2}
+ )
+ runcmd "${cmd[@]}"
+}
+
+# ldcmd TARGET [SRC...]
+# Execute the linker to produce TARGET from the list of SRCs.
+ldcmd(){
+ local cmd=(
+ "$LD" "${LDCMDFLAGS[@]}" "$1" ${@:2} "${LDFLAGS[@]}"
+ $(add_prefix "$LIBPATHPREFIX" "${LIBPATHS[@]}")
+ $(add_prefix "$LIBPREFIX" "${LIBS[@]}")
+ )
+ runcmd "${cmd[@]}"
+}
+
+# arcmd TARGET [SRC...]
+# Execute the archiver to produce TARGET from the list of SRCs.
+arcmd(){
+ local cmd=("$AR" "${ARFLAGS[@]}" "$@")
+ runcmd "${cmd[@]}"
+}
+
+#------------------------------------------------------------------------------
+
+# Object TARGET [SRC...]
+# Builds TARGET as an object file based on the list or SRCs.
+Object(){
+ if should_rebuild "$@"; then
+ cccmd "$@"
+ parse_deps DEPS "$@"
+ cache_update "$@"
+ fi
+}
+
+# Program TARGET [SRC...]
+# Builds TARGET as an executable based on the list or SRCs.
+Program(){
+ build_sources OBJS "${@:2}"
+ if should_rebuild "$@"; then
+ ldcmd "$1" "${OBJS[@]}"
+ cache_update "$@"
+ fi
+}
+
+# StaticLib TARGET [SRC...]
+# Builds TARGET as a static library based on the list or SRCs.
+StaticLib(){
+ build_sources OBJS "${@:2}"
+ if should_rebuild "$@"; then
+ arcmd "$1" "${OBJS[@]}"
+ cache_update "$@"
+ fi
+}
+
+#------------------------------------------------------------------------------
+
+# Init the cache variables
+declare -A CHECKSUMS
+declare -A CACHE_CHECKSUMS
+
+# Create dummy versions of the actions. These can/will be overridden by the
+# Build file
+build(){ :; }
+clean(){ :; }
+config(){ :; }
+install(){ :; }
+uninstall(){ :; }
+
+# Load the cache and the build script
+[[ -f "cache.sh" ]] && source cache.sh
+default_env
+source "Build"
+
+# execute the desired action
+ACTION="${1:-build}"; shift
+case "$ACTION" in
+ "build") build ;;
+ "clean") clean ;;
+ "config") config ;;
+ "install") install ;;
+ "uninstall") uninstall ;;
+ "help") usage ;;
+ *) usage ;;
+esac
+
+# wrap up and write the cache
+finish 0