#!/bin/bash
# lfcw script
# Copyright 2017-2020 Behan Webster
# License GPLv3

set -o pipefail  # trace ERR through pipes
#set -o errtrace  # trace ERR through 'time command' and other functions
set -o nounset   ## set -u : exit the script if you try to use an uninitialized variable
set -o errexit   ## set -e : exit the script if any statement returns a non-true return value

VERSION="4.2"
TOOL=$(basename "$0")

ALL=""
CHECK=""
DEBUG=${DEBUG:-}
DEBUG_CMD=${DEBUG_CMD:-}
DEBUG_TEX=${DEBUG_TEX:-}
EDIT=""
ENGINE=pdftex
FETCH_ONLY=${FETCH_ONLY:-}
FIX=""
FORCE=${FORCE:-}
GENERATED="This file is generated by $TOOL"
TEXFILE=""
TOPDIR=$(pwd)
INFO=${INFO:-}
MOREOUTPUT=""
NOCACHE=""
ONECOL=${ONECOL:-}
OUTPUTEXT=".output"
QUIET=
export TEST=${TEST:+echo}
SHOW=${SHOW:-}
TRACE=${TRACE:-}
VERBOSE="-v"
VICMD=""
VINFO=""

BIOS=""
DTBS=""
PART=""
SPL=""

MANIFEST="manifest.txt"
NEWKERNEL="new-kernel"
STAMPDIR=".stampfiles"
CACHEDIR="$HOME/.cache/lfcw"

TMPGLOB="tmp.*"
TMPLATE="tmp.XXXXXX"

RED="\e[0;31m"
GREEN="\e[0;32m"
YELLOW="\e[0;33m"
CYAN="\e[0;36m"
BLUE="\e[0;34m"
BACK="\e[0m"

nocolor() {
	RED=""
	GREEN=""
	YELLOW=""
	CYAN=""
	BLUE=""
	BACK=""
}

MYMAKE=false
COURSE=
CONFIG_GENERATE_TEX=

BBURL="https://www.busybox.net/downloads"
BIURL="https://debian.beagleboard.org/images"
BMAPTOOLURL="https://github.com/intel/bmap-tools/releases/download"
BRURL="https://buildroot.org/downloads"
#CTNGURL="http://crosstool-ng.org/download/crosstool-ng"
CTNGURL="https://github.com/crosstool-ng/crosstool-ng"
ETCHERURL="https://github.com/balena-io/etcher/releases"
ETCHERDLURL="$ETCHERURL/download"
FTURL="git://git.kernel.org/pub/scm/utils/trace-cmd/trace-cmd.git"
KERNELURL="https://cdn.kernel.org/pub/linux/kernel/v5.x"
LGCCURL="https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf"
QEMUURL="https://www.qemu.org/download"
MTDURL="ftp://ftp.infradead.org/pub/mtd-utils"
UBOOTURL="http://ftp.denx.de/pub/u-boot"
SBIURL="https://github.com/riscv/opensbi"
YPURL="https://downloads.yoctoproject.org/releases/yocto"
BITBAKEURL="https://github.com/openembedded/bitbake/releases"
NANOURL="https://www.nano-editor.org/download.php"

# Variables used in make_expand
# shellcheck disable=SC2034
export BR2_BUILD_DIR="output/build"

# Work around issue where this is set which breaks buildroot builds
unset PERL_MM_OPT

######################################################################
# Run whenever lfcw exits
exit_handler() {
	local ERR=$?

	if [[ $ERR -ne 0 ]] ; then
		echo -e "${RED}E: An error occurred: $ERR$BACK" >&2
	fi

	if [[ -n ${WORKDIR:-} ]] ; then
		# remove rate-limiting stamps
		mkdir -p "$WORKDIR/$STAMPDIR"
		rm -f "${WORKDIR:?}/$STAMPDIR/$$-"*.rate_limit
	fi

	exit "$ERR"
}

trap exit_handler EXIT ERR

######################################################################
# show shell commands
verbose() {
	if [[ -n $DEBUG || -n $TEST ]] ; then
		echo -e "${CYAN}T:" "$@" "$BACK" >&2
	fi
	if [[ -z $TEST ]] ; then
		([[ -z $MOREOUTPUT ]] || set -x; "$@" 2>&1)
		return $?
	fi
}

######################################################################
# Debugging log messages
debug() {
	[[ -z "$DEBUG" ]] || echo -e "${BLUE}D:" "${*//\\/\\\\}" "${BACK}" >&2
}

######################################################################
# Error log messages
error() {
	set +x
	echo -e "${RED}E:" "$@" "$BACK" >&2
	exit 1
}

######################################################################
# Error if file doesn't exist
exists_or_error() {
	local FILE=${1:?}; shift
	[[ -n $TEST || -e $FILE ]] || error "$@"
}

######################################################################
# Warning log messages
warn() {
	echo -e "${YELLOW}W:" "$@" "$BACK" >&2
}
vwarn() {
	[[ -z $MOREOUTPUT ]] || warn "$@"
}
warn_once() {
	local RL
	mkdir -p "$WORKDIR/$STAMPDIR"
	RL="$WORKDIR/$STAMPDIR/$$-$(echo "$*" | md5sum | cut -d' ' -f1).rate_limit"
	if [[ -f $RL ]] ; then
		return 0
	else
		touch "$RL"
		warn "$@"
	fi
}

######################################################################
# Success log messages
info() {
	echo -e "${GREEN}I:" "$@" "$BACK" >&2
}

######################################################################
pass() {
	echo -e "${GREEN}PASS:$BACK" "$@" >&2
}

######################################################################
fail() {
	echo -e "${RED}FAIL:$BACK" "$@" >&2
}

######################################################################
# used-by: announce
divider() {
	[[ -n $QUIET ]] || echo '--------------------------------------------------------------------------------' >&2
}

######################################################################
# used-by: important
divider2() {
	[[ -n $QUIET ]] || echo '================================================================================' >&2
}

######################################################################
announce() {
	divider
	info "$@"
}

######################################################################
important() {
	divider2
	info "===" "$@" "==="
}

######################################################################
# Ask a question and confirm
# used-by: make_all
confirm() {
	info "$@" "Continue? [y|N]"
	local ANS
	read -r -ANS
	case "$ANS" in
		y*|Y*|t*|T*|1|c*|C*) return 0;;
		*) return 1;;
	esac
}

######################################################################
# EXPECT:
enter_password() {
	echo -n "Password:" >&2
	local PWD
	read -r -s PWD
	echo "$PWD"
}

######################################################################
# Open files in an editor
# used-by: check_whitespace, edit_latex, get_macros, list_lfcw
edit() {
	debug "edit: VICMD=$VICMD VINFO=$VINFO FILES:" "$@"
	if [[ -z $EDITOR || $EDITOR =~ vi ]] ; then
		$TEST "${EDITOR:-vim}" ${VICMD:+-c "$VICMD"} ${VINFO:+-i $VINFO} "$@"
	else
		$TEST "$EDITOR" "$@"
	fi
}

######################################################################
# Stamp files used to make sure operations aren't repeated
stampfile() {
	local FILE=${1:?}
	local STAMP="$WORKDIR/$STAMPDIR/${FILE##*/}.done"
	debug "stampfile: create $STAMP"
	mkdir -p "$WORKDIR/$STAMPDIR"
	verbose touch "$STAMP"
}
clearstamp() {
	local FILE=${1:?}
	local STAMP="$WORKDIR/$STAMPDIR/${FILE##*/}.done"
	debug "clearstamp: remove $STAMP"
	verbose rm -f "${STAMP:?}"
}
clearstamp_pattern() {
	local FILE=${1:?}
	# shellcheck disable=SC2027
	local PATTERN="$WORKDIR/$STAMPDIR/"${FILE##*/}".done"
	debug "clearstamp: remove ${PATTERN:-Nothing}"
	# shellcheck disable=SC2086
	verbose rm -f "${PATTERN:?}"
}
checkstamp() {
	local FILE=${1:?}
	local STAMP="$WORKDIR/$STAMPDIR/${FILE##*/}.done"
	if [[ -d $FILE ]] ; then
		rmdir "$FILE"
	fi
	if [[ -n ${FORCE:-} ]] ; then
		return 1
	fi
	if [[ -s $FILE && -e "$STAMP" && ! "$FILE" -nt "$STAMP" ]] ; then
		debug "checkstamp: File finished: $FILE"
		return 0
	else
		clearstamp "$FILE"
		return 1
	fi
}

######################################################################
# used-by: mkroot_tar, save_build_artifacts
XZTHREADS="-T 0"
compress() {
	local FROM=${1:?} TO=${2:-}
	if [[ -n ${TO:-} ]] ; then
		rm -f "${TO:?}"
		info "  Compress '$FROM' -> '$TO'"
		# shellcheck disable=SC2086
		verbose xz -9 $XZTHREADS -c "$FROM" >"$TO"
	else
		rm -f "${1:?}.xz"
		info "  Compress '$FROM' -> '$FROM.xz'"
		# shellcheck disable=SC2086
		verbose xz -9 $XZTHREADS "$FROM"
	fi
}

######################################################################
# used-by: mkroot_tar
decompress() {
	local FROM=${1:?} TO=${2:?}
	info "  Decompressing '$FROM' -> '$TO'"
	# shellcheck disable=SC2086
	case "$FROM" in
		*.gz) verbose zcat "$FROM" >"$TO" ;;
		*.bz2) verbose bzcat "$FROM" >"TO" ;;
		*.xz) verbose xzcat $XZTHREADS "$FROM" >"$TO" ;;
		*) TO="$FROM" ;;
	esac
	exists_or_error "$TO" "File didn't decompress: $TO"
}

######################################################################
# Test if an archive is valid
# used-by: fetch_file
test_archive() {
	local FILE=${1:?}
	info "  Testing $FILE"
	# shellcheck disable=SC2086
	case "$FILE" in
		*.bz2) verbose bunzip2 --test "$FILE" ;;
		*.gz) verbose gunzip --test "$FILE" ;;
		*.xz) verbose unxz $XZTHREADS --test "$FILE" ;;
		*.zip) verbose unzip -t "$FILE" ;;
	esac
}

######################################################################
# Save and diff environment variables
# used-by: dump_config
declare -A VARS
saveenv() {
	local KEY
	while read -r KEY ; do
		[[ -n $KEY ]] || continue
		VARS[$KEY]=$(eval "echo \$$KEY")
		[[ -n ${VARS[$KEY]:-} ]] || VARS[$KEY]=undefined
	done <<< "$(env | sed -e 's/=.*//g' | sort)"
	local IGNORE="COURSE ARCH"
	for KEY in $IGNORE ; do
		unset "VARS[$KEY]"
	done
}
diffenv() {
	local KEY
	while read -r KEY ; do
		[[ -n ${VARS[$KEY]:-} ]] || eval "echo $KEY=\$$KEY"
	done <<< "$(env | sed -e 's/=.*//g' | sort)"
}
saveenv
export COURSE
export CVERSION=""
export WORKDIR=""
export ARCH="${ARCH:-arm}"
export ARCHBITS="${ARCHBITS:-$ARCH}"
export QEMUVERSION=""
export QEMUTARGET=""
export TEXBOARD=""
export BOARD=""
export BOARDSERIAL=""
export BOARDBAUD="115200"
export BOARDSHORT=""
export GCCBUILD=""
export GCCTRIPLET=""
export GCCREL=""
export CTVERSION=""
export UBVERSION=""
export UBDEFCONFIG=""
export SBIVERSION=""
export KVERSION=""
export KDEFCONFIG=""
export BRVERSION=""
export BRDEFCONFIG=""
export BBVERSION=""
export FTVERSION=""
export MTDVERSION=""
export YPVERSION=""
export YPCODENAME=""
export POKYVERSION=""
export BITBAKEVERSION=""
export NANOVERSION=""

######################################################################
# used-by: abs2rel
path2list() {
	sed -e 's/^\///; s/\/$//; s/\//\n/g' <<<"$*"
}
maximum() {
	if [[ $1 -gt $2 ]] ; then
		echo "$1"
	else
		echo "$2"
	fi
}

######################################################################
# Change absolute symlink to relative one
# used-by: course_links, symlink, update_links
abs2rel() {
	local FROM=${1:?} TO=${2:-}
	debug "abs2rel: FROM=$FROM TO=$TO"
	if [[ -z $TO ]] ; then
		TO="$(pwd)/${FROM##*/}"
	fi
	if [[ -d $TO ]] ; then
		# shellcheck disable=SC2086
		TO="$(realpath $TO)/${FROM##*/}"
	else
		# shellcheck disable=SC2086
		TO="$(realpath "$(dirname "$TO")")/${TO##*/}"
	fi
	FROM="$(realpath "$(dirname "$FROM")")/${FROM##*/}"

	local SRC DST LEN PART REL="" STARTED=""
	declare SRC DST
	readarray -t SRC < <(path2list "$FROM")
	readarray -t DST < <(path2list "$TO")
	LEN=$(maximum ${#SRC[@]} ${#DST[@]})
	debug "abs2rel: FROM=$FROM TO=$TO LEN=$LEN"
	[[ $FROM != "$TO" ]] || error "abs2rel links are the same: $FROM"

	for PART in $(seq 0 $((LEN-1))) ; do
		#debug "  abs2rel: PART=${PART} SRC=${SRC[$PART]:-} DST=${DST[$PART]:-} REL=${REL:-}"
		if [[ -z ${SRC[$PART]:-} ]] ; then
			if [[ -n ${DST[$PART]} ]] ; then
				#REL+="/${DST[$PART]}"
				REL="../$REL"
			fi
			debug "    FROM-z ${DST[$PART]} => $REL "
		elif [[ -z ${DST[$PART]:-} ]] ; then
			if [[ -z $REL ]] ; then
				REL=".."
				STARTED=1
			else
				REL="../$REL"
			fi
			debug "    TO-z ${SRC[$PART]} => $REL"
		elif [[ ${SRC[$PART]} != "${DST[$PART]}" ]] ; then
			if [[ -z $STARTED ]] ; then
				REL="${SRC[$PART]:-}"
				STARTED=1
			else
				REL="../$REL/${SRC[$PART]:-}"
			fi
			debug "    ${SRC[$PART]:-} != ${DST[$PART]:-} => $REL"
		#else
			#debug "    ${SRC[$PART]} = ${DST[$PART]} => ${REL:-}"
		fi
	done

	# shellcheck disable=SC2128
	debug "  FROM=$FROM TO=$TO REL=$REL"
	echo "$REL"
}

######################################################################
# Override some commands
BBBBOOT="$(command -v bbbboot)"
bbbboot() { verbose "$BBBBOOT" "$@"; }
BBBCONNECT="$(command -v bbbconnect)"
bbbconnect() { verbose "$BBBCONNECT" "$@"; }
SYSTEMCTL="$(command -v systemctl)"
systemctl() { verbose sudo "$SYSTEMCTL" "$@"; }

######################################################################
# used-by: build_kernel, build_opensbi, build_perf, build_uboot, tar_kernel
check_toolchain() {
	local TRIPLET="${GCCTRIPLET:-arm-linux-gnueabihf}"
	local XTOOLS="${XTOOLS:-$HOME/crosstool-ng/xtools/$TRIPLET}"
	local XTOOLSALT="${XTOOLS:-$HOME/xtools/$TRIPLET}"
	debug "check_toolchain: TRIPLET=$TRIPLET XTOOLS=$XTOOLS XTOOLSALT=$XTOOLSALT"

	local DIR
	for DIR in $GCCBUILD $XTOOLS $XTOOLSALT $(find "$WORKDIR/"* -maxdepth 0 -type d) ; do
		DIR=$(realpath "$DIR")
		debug "  try: $DIR/bin/${TRIPLET}-gcc"
		if [[ -x $DIR/bin/${TRIPLET}-gcc ]] ; then
			warn_once "Using compiler in $DIR"
			PATH="$DIR/bin/:$PATH"
			export PATH
			MYMAKE="make -j$(nproc) ARCH=$ARCH CROSS_COMPILE=${TRIPLET}-"
			debug "MYMAKE=$MYMAKE"
			export MYMAKE
			return 0
		fi
	done

	warn "Using native armmake"
	MYMAKE=armmake
	debug "MYMAKE=$MYMAKE"
}

######################################################################
# ARG:--board,info: Check board lessons
# used-by: build_buildroot, build_kernel, build_opensbi, build_uboot, mkroot_tar, rootfs_link, squashfs_root, tar_kernel
check_board() {
	debug "check_board"
	case "${1:-$BOARD}" in
		bbb|beagleboneblack)
			ARCH="arm"
			ARCHBITS="arm"
			BOARD="beagleboneblack"
			BOARDSHORT="bbb"
			DTBS="am335x-bone*dtb"
			PART="Linux2"
			SPL="MLO"
			;;
		riscv*)
			ARCH="riscv"
			ARCHBITS="riscv64"
			BOARD="riscv64"
			BOARDSHORT="riscv64"
			DTBS=""
			PART="rootfs"
			SPL=""
			BIOS="fw_jump.bin"
			;;
		rpi*)
			ARCH="arm"
			DTBS="/dts/rpi*dtb"
			PART="${PART:-Linux2}"
			SPL="MLO"
			;;
		vexpress)
			ARCH="arm"
			BOARD="vexpress"
			BOARDSHORT="vexpress"
			DTBS="vexpress-v2p-ca9.dtb"
			PART=""
			SPL=""
			;;
	esac

	[[ -z $INFO ]] || DEBUG="y";
	debug "  ARCH=$ARCH"
	debug "  BOARD=$BOARD"
	debug "  BOARDSHORT=$BOARDSHORT"
	debug "  KDEFCONFIG=$KDEFCONFIG"
	debug "  BRDEFCONFIG=$BRDEFCONFIG"
	debug "  UBDEFCONFIG=$UBDEFCONFIG"
	debug "  DTBS=$DTBS"
	debug "  PART=$PART"
	debug "  SPL=$SPL"
	[[ -n $BOARD ]] || error "BOARD is undefined"
}

######################################################################
# Read singular config from course tex file
# used-by: read_config
read_tex() {
	local FILE=${1:?} KEY=${2:?}
	if [[ ! $FILE =~ ^/ ]] ; then
		FILE="$TOPDIR/$FILE"
	fi
	debug "    read_tex: FILE=$FILE($1) KEY=$KEY"

	# shellcheck disable=SC1003
	grep -v '^ *%' "$FILE" \
		| grep -E '\\(re)*newcommand{\\'"$KEY"'}' | head -1 \
		| sed -re 's/ *%.*$//' \
		| sed -re 's/.*\{(.*)\}$/\1/; s/\\_/_/g; s/\\/\\\\/g'
}

######################################################################
# Read config and expand sub-variables
# used-by: cpp_tex, read_config
read_config_file () {
	local FILE KEY=${2:?} NOWARN=${3:-} DATA VAL CONF
	FILE="$(expand_tex_macros <<<"${1:?}")"
	debug "  read_config: FILE=$FILE($1) KEY=$KEY NOWARN=$NOWARN"

	DATA=$(read_tex "$FILE" "$KEY")
	debug "  read_config: KEY=$KEY DATA='$DATA'"
	if [[ -z $DATA ]] ; then
		CONF="$(dirname "$FILE")/.$TOOL.conf"
		if [[ -z ${EMPTY:-} && -f $CONF ]] ; then
			# shellcheck disable=SC1090
			. "$CONF"
		fi
		for VAL in ${EMPTY:-} ; do
			if [[ $VAL = "$KEY" ]] ; then
				# Ignore intentionally empty variables
				return 0
			fi
		done
		[[ -n $NOWARN \
		|| $KEY =~ currtargetfile \
		|| $KEY =~ includegraphics \
		|| $KEY =~ input \
		|| $KEY =~ thesessionref \
		]] \
			|| warn "read_config: $KEY from $FILE is empty"
		return 0
	fi
	# shellcheck disable=SC1003,SC2076
	while [[ -n $DATA ]] && [[ $DATA =~ '\' || $DATA =~ '#' ]] ; do
		KEY=$(sed -re 's/\\#/\\/; s/.*\\+([a-z]+).*/\1/' <<<"$DATA")
		VAL=$(read_tex "$FILE" "$KEY")
		DATA=$(sed -re "s/[\\]+$KEY/$VAL/g" <<<"$DATA")
		debug "  read_config: KEY=$KEY VAL=$VAL DATA=$DATA"
	done
	echo "$DATA"
}

######################################################################
# Read config key
# used-by: expand_tex, read_course
read_config () {
	read_config_file "$TEXFILE" "$@"
}

######################################################################
# Get one local file from the course
# used-by: cpp_tex
get_file() {
	local FILE="$TOPDIR/${1:?}"
	if [[ -f $FILE ]] ; then
		debug "    get_file: $FILE"
		cat "$FILE"
	fi
}

######################################################################
# Build one file from inputs which allow you to read configuration
# used-by: read_course
cpp_tex() {
	local TEX=${1:?} NEW="$1.aux" LINE FILE
	debug "cpp_tex TEX=$TEX NEW=$NEW"

	while read -r LINE ; do
		if [[ $LINE =~ ^\\input\{appendices\} ]] ; then
			continue
		elif [[ $LINE =~ ^\\input\{ || $LINE =~ ^\\include{ ]] ; then
			FILE="$(echo "${LINE/\\board/$TEXBOARD}" \
				| expand_tex | sed -re 's/^.*\{(.*)\}.*$/\1.tex/')"
			debug "  cpp_tex: Including $FILE"
			get_file "$FILE"
			TEXBOARD=$(read_config_file "$FILE" "board" nowarn)
		elif [[ ! $LINE =~ \{TBD\} ]] ; then
			echo "$LINE"
		fi
	done <"$TEX" >"$NEW"

	echo "$NEW"
}

######################################################################
# Find course tex file
# used-by: read_course
get_tex() {
	COURSE=${1:?}
	local TEX="$TOPDIR/$COURSE.tex"
	debug "get_tex: COURSE=$COURSE TEX=$TEX"
	[[ -f $TEX ]] || error "$COURSE tex file can't be found"
	echo "$TEX"
}

######################################################################
# ARG:info: Read course config information
# used-by: check_course
read_course() {
	debug "read_course"
	local COURSE=${1:?}
	TEXFILE=$(get_tex "$COURSE")
	TEXFILE=$(cpp_tex "$TEXFILE")

	if [[ -z $BOARD ]] ; then
		ARCH=$(read_config "arch")
		ARCHBITS=$(read_config "archbits")
		TEXBOARD=$(read_config "board")
		BOARD=$(read_config "targetnick")
		BOARDSHORT=$(read_config "targetabbrev")
		BOARDPREFIX=$(read_config "targetimageprefix")
	else
		TEXBOARD="$BOARD"
		BOARDPREFIX=""
	fi

	CVERSION=$(read_config "version")
	WORKDIR="$WORK/$COURSE/$CVERSION/$ARCH"

	QEMUVERSION=$(read_config "qemuversion")
	QEMUTARGET=$(read_config "qemutarget")


	BOARDSERIAL=$(read_config "targetserial")
	BOARDBAUD=$(read_config "targetbaud")
	BOARDVERSION=$(read_config "targetimagever")
	BOARDTYPE=$(read_config "targetimagetype")
	BOARDDATE=$(read_config "targetimagedate")
	BOARDPOSTFIX=$(read_config "targetimagepostfix")
	BOARDIMAGE=$(read_config "targetimage")

	SDBURNPREFIX=$(read_config "sdburnprefix")
	SDBURNVERSION=$(read_config "sdburnver")
	SDBURNZIP=$(read_config "sdburnzip")

	# shellcheck disable=SC2034
	BMAPTOOLPREFIX=$(read_config "bmaptoolprefix")
	# shellcheck disable=SC2034
	BMAPTOOLVERSION=$(read_config "bmaptoolver")
	# shellcheck disable=SC2034
	BMAPTOOLFILE=$(read_config "bmaptoolfile")

	GCCBUILD=$(read_config "gccbuild")
	GCCTRIPLET=$(read_config "gcctriplet")
	GCCREL=$(read_config "gccrel")
	CTVERSION=$(read_config "crosstoolver")

	UBVERSION=$(read_config "ubootver")
	UBDEFCONFIG=$(read_config "ubootdefconfig")
	SBIVERSION=$(read_config "opensbiver")
	SBIPLATFORM=$(read_config "opensbiplat")

	KVERSION=$(read_config "kversionp")
	KDEFCONFIG=$(read_config "kdefconfig")
	KERNELSRC="${WORKDIR:?No WORKDIR for KERNELSRC}/linux-$KVERSION" \

	BRVERSION=$(read_config "buildrootver")
	BRDEFCONFIG=$(read_config "buildrootdefconfig")
	BBVERSION=$(read_config "busyboxver")

	FTVERSION=$(read_config "ftracever")
	MTDVERSION=$(read_config "mtdver")

	YPVERSION=$(read_config "yoctoversion")
	YPCODENAME=$(read_config "pokyname")
	POKYVERSION=$(read_config "pokyversion")
	BITBAKEVERSION=$(read_config "bitbakever")
	NANOVERSION=$(read_config "nanover")
}

######################################################################
# ARG:info: print course config information
# used-by: check_course
print_course() {
	[[ -z $INFO ]] || DEBUG="y";
	debug "  COURSE=$COURSE"
	debug "  CVERSION=$CVERSION"
	debug "  WORKDIR=$WORKDIR"
	debug "  ARCH=$ARCH"
	debug "  ARCHBITS=$ARCHBITS"
	debug "  QEMUVERSION=$QEMUVERSION"
	debug "  QEMUTARGET=$QEMUTARGET"
	debug "  TEXBOARD=$TEXBOARD"
	debug "  BOARD=$BOARD"
	debug "  BOARDSERIAL=$BOARDSERIAL"
	debug "  BOARDSERIAL=$BOARDBAUD"
	debug "  BOARDSHORT=$BOARDSHORT"
	debug "  BOARDPREFIX=$BOARDPREFIX"
	debug "  BOARDVERSION=$BOARDVERSION"
	debug "  BOARDTYPE=$BOARDTYPE"
	debug "  BOARDDATE=$BOARDDATE"
	debug "  BOARDPOSTFIX=$BOARDPOSTFIX"
	debug "  BOARDIMAGE=$BOARDIMAGE"
	debug "  SDBURNPREFIX=$SDBURNPREFIX"
	debug "  SDBURNZIP=$SDBURNZIP"
	debug "  GCCBUILD=$GCCBUILD"
	debug "  GCCTRIPLET=$GCCTRIPLET"
	debug "  CTVERSION=$CTVERSION"
	debug "  UBVERSION=$UBVERSION"
	debug "  UBDEFCONFIG=$UBDEFCONFIG"
	debug "  SBIVERSION=$SBIVERSION"
	debug "  SBIPLATFORM=$SBIPLATFORM"
	debug "  KVERSION=$KVERSION"
	debug "  KDEFCONFIG=$KDEFCONFIG"
	debug "  BRVERSION=$BRVERSION"
	debug "  BRDEFCONFIG=$BRDEFCONFIG"
	debug "  BBVERSION=$BBVERSION"
	debug "  FTVERSION=$FTVERSION"
	debug "  MTDVERSION=$MTDVERSION"
	debug "  YPVERSION=$YPVERSION"
	debug "  YPCODENAME=$YPCODENAME"
	debug "  POKYVERSION=$POKYVERSION"
	debug "  BITBAKEVERSION=$BITBAKEVERSION"
	debug "  NANOVERSION=$NANOVERSION"
}

######################################################################
# Normalize course code
# ARG:course: Make sure a COURSE has been selected
check_course() {
	COURSE=$(tr '[:lower:]' '[:upper:]' <<<"${1:?}")
	debug "check_course: COURSE=$COURSE"

	[[ -n $COURSE ]] || error "No course specified"
	if [[ -e "$COURSES/COMBO/$COURSE" ]] ; then
		TOPDIR="$COURSES/COMBO/$COURSE"
	else
		TOPDIR="$COURSES/$COURSE"
	fi
	debug "  check_course: COURSE=$COURSE TOPDIR=$TOPDIR"

	read_course "$COURSE"
	print_course
}

######################################################################
# Expand path tex macros
# used-by: get_inputs, read_config
expand_tex_macros() {
	local DIR=${1:-} STR MACRO VALUE

	sed -re "s|\\\\arch|$ARCH|g" \
	    -re "s|\\\\board|$TEXBOARD|g" \
	    -re "s|\\\\targetnick|$BOARD|g" \
	    -re "s|\\\\textwidth|0|g" \
	    -re "s|\\\\currfiledir/||g" \
	    -re "s|\\\\currtargetfile\{([^}]*)\}|$DIR/$BOARD/\1|g" \
	    -re "s|\\\\targetfile\{([^}]*)\}|$BOARD/\1|g" \
	    -re "s|\\\\currtargetlabs\{([^}]*)\}|$DIR/$BOARD/${COURSE}labs/\1|g" \
	    -re "s|\\\\targetlabs\{([^}]*)\}|$BOARD/${COURSE}labs/\1|g" \
	    -re "s|\\\\currcourselabs\{([^}]*)\}|$DIR/${COURSE}labs/\1|g" \
	    -re "s|\\\\courselabs\{([^}]*)\}|${COURSE}labs/\1|g" \
	    -re "s|\\\\currlabs\{([^}]*)\}|$DIR/labs/\1|g" \
	    -re "s|\\\\labs\{([^}]*)\}|labs/\1|g" \
	    -re "s|\\\\currfiledir|$DIR|g"
}

######################################################################
# Expand tex
# used-by: check_links, cpp_tex, get_inputs
expand_tex() {
	local DIR=${1:-} STR MACRO VALUE
	debug "  expand_tex: DIR=$DIR"

	while read -r STR ; do
		debug "    expand_tex: orig <$STR>"
		# shellcheck disable=SC1003
		while [[ $STR =~ '\' ]] ; do
			if [[ $STR =~ texttt ]] ; then
				STR="$(sed -re 's/\\texttt\{([^}]*)\}/\1/g' <<<"$STR")"
			fi
			MACRO="$(sed -re 's/^.*\\([a-zA-Z0-9]+).*$/\1/' <<<"$STR")"
			debug "    expand_tex: macro <$MACRO>"
			if [[ $MACRO =~ currlabs ]] ; then
				warn "expand_tex: currlabs: $MACRO -> $STR"
			fi
			VALUE="$(read_config "$MACRO")"
			#info "expand_tex: ($DIR) ${STR/\\/\\\\} $MACRO <$VALUE>"
			if [[ -z $VALUE ]] ; then
				break
			else
				STR="$(sed -re "s|\\\\$MACRO|$VALUE|g" <<<"$STR")"
			fi
			debug "    expand_tex: read_config: $STR"
		done
		echo "$STR"
	done
}

######################################################################
# get subimports
# used-by: get_chapters, get_appendices
get_subimports() {
	local IN=${1:-} DIR FILE
	[[ -n $IN && -f $IN ]] || return
	DIR="$(dirname "$IN")"
	debug "get_subimports: DIR=$DIR FILE=$IN"

	set +e
	grep -v -E '^%' "$IN" \
		| grep -E "\\\\(subimport)[[{]" \
		| sed -E "s|.*\{(.*)\/}\{index\}|\1|"
	set -e
	return 0
}

######################################################################
# Get chapter dirs for course
# used-by: get_all_chapters, link_chapters
get_chapters() {
	local DIR
	debug "get_chapters: TEX=$TEXFILE"

	for DIR in $(get_subimports "$TEXFILE") ; do
		#info "get_chapters: $DIR"
		if [[ -d $TOPDIR/$DIR ]] ; then
			echo "$DIR"
		fi
	done
}

######################################################################
# Get appendices dirs for course
# used-by: get_all_chapters, link_chapters
get_appendices() {
	local APD
	# shellcheck disable=SC2086
	APD=$(echo ${TEXFILE%/*}/append*tex 2>/dev/null)
	debug "get_appendices: TEX=$TEXFILE APD=$APD"

	get_subimports "$APD"
}

######################################################################
# Get chapters and appendices dirs for course
# used-by: check_all, count_latex, findlfcw, get_tex_files, git_change
get_all_chapters() {
	get_chapters "${1:-$COURSE}"
	get_appendices "${1:-$COURSE}"
}

######################################################################
# get imports
# used-by: get_inputs, get_tex_files
get_inputs() {
	local IN=${1:-} LALL=${2:-} DIR FILE UP
	[[ -n $IN && -f $IN ]] || return
	DIR="$(dirname "$IN")"
	debug "get_inputs: DIR=$DIR FILE=$IN"
	set +e

	local RE="input"
	# \macro[]{}{file}
	local RE1="codefile|inputminted"
	# \macro[]{file}
	local RE2="cfile|clifile|codefile|configfile|currtargetfile|fdtfile|fromfile|frommakefile|gitfile|includegraphics|inputminted|javafile|jsonfile|latexfile|onhostfile|onlaptopfile|onserverfile|ontargetfile|onvmfile|pythonfile|serialfile|shfile|yamlfile|input|rawfile|rawfilefootnotesize|rawfilescriptsize|rawfilesmall|rawfiletiny|respfile|respfilevar|verbatiminput"
	[[ -z ${LALL:-} ]] || RE="$RE1|$RE2"

	echo "$IN"
	grep -v -E '^%' "$IN" \
		| grep -E "\\\\($RE|subimport)[[{]" \
		| expand_tex_macros "$DIR" \
		| sed -E "s%^.*\\\\($RE1)(\[[^]}]*\])*\{[^}]+\}\{([^}]+)\}.*$%\3%g; \
			s%^.*\\\\($RE2)(\[[^]]*\])*\{([^}]*)\}.*$%\3%g; \
			s|.*\{([^}]*)\/}\{index\}|\1|;
			s|^IMAGES|$IMAGES|" \
		| expand_tex "$DIR" \
		| while read -r FILE ; do
			debug "  get_inputs: FILE=$FILE"
			if [[ -f "$FILE" ]] ; then
				get_inputs "$FILE" "$LALL"
			elif [[ -f "$FILE.jpg" ]] ; then
				echo "$FILE.jpg"
			elif [[ -f "$FILE.png" ]] ; then
				echo "$FILE.png"
			elif [[ -f "$FILE.pdf" ]] ; then
				echo "$FILE.pdf"
			elif [[ -f "$FILE.tex" ]] ; then
				get_inputs "$FILE.tex" "$LALL"
			elif [[ -d "$FILE" ]] ; then
				get_inputs "$FILE/index.tex" "$LALL"
			elif [[ -f "$DIR/$FILE" ]] ; then
				get_inputs "$DIR/$FILE" "$LALL"
			elif [[ -d "$DIR/$FILE" ]] ; then
				get_inputs "$DIR/$FILE/index.tex" "$LALL"
			elif [[ -f "$DIR/$FILE.tex" ]] ; then
				get_inputs "$DIR/$FILE.tex" "$LALL"
			else
				UP=$(dirname "$DIR")
				if [[ -f "$UP/$FILE.tex" ]] ; then
					get_inputs "$UP/$FILE.tex" "$LALL"
				fi
			fi
		done #| sed -E 's|[^/]+/\.\.||g; s|^\./||g;'
	set -e
	return 0
}

######################################################################
# ARG:files: Get chapters and appendices dirs for course
# used-by: check_links, check_pointout, check_solutions, check_whitespace, count_latex, find_latex, git_cmds, tags_latex
get_tex_files() {
	local FILE=${1:-index.tex} LALL=${2:-$ALL} PWD

	PWD=$(pwd | sed -re "s|^$(realpath "$COURSES")|$COURSES|")
	(if [[ $FILE = -d ]] ; then
		get_all_chapters "$COURSE"
	elif [[ -f $FILE ]] ; then
		get_inputs "$FILE" "$LALL"
	elif [[ -d $FILE && -f "$FILE/index.tex" ]] ; then
		get_inputs "$FILE/index.tex" "$LALL"
	else
		get_inputs "$TEXFILE" "$LALL"
	fi) | sed -e "s|^$PWD/||" \
		-re "s|^$COURSES/||" -re 's|//|/|g' \
		-re "s|$COURSE/CHAPS|MODULES|g" \
		-re "s|$COURSE/IMAGES|IMAGES|g" \
		-re "s|^CHAPS|../MODULES|g" \
		-re "s|^IMAGES|../IMAGES|g"
}

######################################################################
# Add derived file to manifest file
# used-by: bmap_flash_image, build_buildroot, build_busybox, build_crosstool, build_ftrace, build_kernel, build_mtd, build_opensbi, build_qemu, build_uboot, do_save_config, get_bmaptool, get_board_image, get_etcher, get_linarogcc, mkroot_tar, rootfs_link, save_build_artifacts, squashfs_root, tar_build_artifacts, tar_kernel
add_to_manifest() {
	local FILE TMP FULL WORK
	WORK="$(realpath "$WORKDIR")"
	for FILE in "$@" ; do
		FULL="$(realpath "$FILE")"
		if [[ ! $FULL =~ $WORK ]] ; then
			error "add_to_manifest: artifact must be in $WORKDIR"
		fi
		echo "${FILE##*/}"
	done >> "$WORKDIR/$MANIFEST"
	TMP="$(mk_tmpfile)"
	sort -u "$WORKDIR/$MANIFEST" > "$TMP"
	mv -f "$TMP" "$WORKDIR/$MANIFEST"
}

######################################################################
# List derived file
# used-by: bmap_flash_image, build_buildroot, build_busybox, build_crosstool, build_ftrace, build_kernel, build_mtd, build_opensbi, build_perf, build_qemu, build_uboot, get_bmaptool, get_board_image, get_etcher, get_linarogcc, mkroot_tar, rootfs_link, squashfs_root, tar_kernel
list_files() {
	info "  Saving files"
	# shellcheck disable=SC2086
	verbose ls -gGh --color=auto "$@"
	sleep 2
}

######################################################################
# enter/leave workdir
# used-by: bmap_flash_image, build_buildroot, build_busybox, build_crosstool, build_ftrace, build_kernel, build_mtd, build_opensbi, build_perf, build_qemu, build_uboot, clean_files, config_to_tex, config_to_tex_summary, get_bmaptool, get_board_image, get_etcher, get_linarogcc, install_artifact, make_targets, mkroot_tar, mvresources, rootfs_link, squashfs_root, tar_kernel
enter_workdir() {
	local EXTRA="${1:-}"
	if [[ $WORKDIR =~ work ]] ; then
		mkdir -p "$WORKDIR/$EXTRA"
		if [[ $TEST ]] ; then
			pushd "$WORKDIR/$EXTRA"
		else
			pushd "$WORKDIR/$EXTRA" >/dev/null
		fi
	else
		error "Workdir is wrong: $WORKDIR/$EXTRA"
	fi
	if [[ ! $PWD =~ work ]] ; then
		error "Not in the workdir: $PWD"
	fi
}
leave_workdir() {
	if [[ $TEST ]] ; then
		popd
	else
		popd >/dev/null
	fi
}

######################################################################
# used-by: compile_code, config_build
make_targets() {
	local DIR=${1:?}
	local MAKE=${2:?}
	local BUILD_TARGETS=${3:?}
	local OPTS=${4:-}
	local LOG
	[[ -n $VERBOSE ]] || OPTS="-s $OPTS"
	LOG=$(realpath "${5:-$DIR.log}")
	debug "make_targets: DIR=$DIR MAKE=$MAKE OPTS=$OPTS TARGETS=$BUILD_TARGETS LOG=$LOG"

	local TARGET
	enter_workdir .
	pushd "$DIR" >/dev/null
	for TARGET in $BUILD_TARGETS ; do
		if [[ -n $TEST ]] ; then
			# shellcheck disable=SC2086
			verbose $MAKE $OPTS "$TARGET"
		elif [[ -n $VERBOSE ]] ; then
			# shellcheck disable=SC2086
			verbose $MAKE $OPTS "$TARGET" >>"$LOG"
		else
			# shellcheck disable=SC2086
			verbose $MAKE $OPTS "$TARGET" | tee -a "$LOG" | pv
		fi
	done
	popd >/dev/null
	leave_workdir
}

######################################################################
# BUILD,CODE: eval arbitrary shell to generate output for inclusion in latex
eval_shell() {
	local FILE=${1:?}; shift
	announce "Eval '$*' to $FILE"
	if [[ -n $DEBUG || -n $TEST ]] ; then
		echo -e "${CYAN}T:" "$@" "$BACK"
	fi
	if [[ -z $TEST ]] ; then
		eval "$*" 2>&1 | sed -e "s/$USER/student/g" >"$LFCWDIR/$FILE$OUTPUTEXT"
	fi
}

######################################################################
# BUILD: Identical files or error
identical() {
	local FILE1="${1:?}" FILE2
	FILE2="$(labsdir "${2:?}")"
	debug "identical: '$FILE1' '$FILE2'"
	if cmp --silent "$FILE1" "$FILE2" ; then
		return 0
	else
		error "Files differ:" "$@"
	fi
}

######################################################################
# BUILD,LABS: Hard link or copy
# used-by: copy, fetch_file, save_build_artifacts, tar_kernel, update_links
copy_link() {
	local FROM=${1:?} TO=${2:?} MISSINGOK=${3:-} OUTPUT
	debug "copy_link: FROM=$FROM TO=$TO"

	if [[ $FROM =~ ^labs/ ]] ; then
		FROM="$(labsdir "$FROM")"
	fi

	if [[ -z $FROM ]] ; then
		warn "  copy_link: No file to copy to $TO"
		return 0
	elif [[ ! -s $FROM ]] ; then
		if [[ -n $MISSINGOK ]] ; then
			vwarn "  copy_link: $FROM doesn't exist"
			return 0
		else
			error "  copy_link: $FROM doesn't exist"
		fi
	fi

	info "  Copy '$FROM' -> '$TO'"
	[[ ! -f $TO ]] || rm -f "${TO:?}"
	local OUTPUT
	if OUTPUT="$(verbose cp --link "$FROM" "$TO" 2>&1)" ; then
		echo "$OUTPUT" >&2
	else
		verbose cp "$FROM" "$TO" >&2
	fi
}

######################################################################
# BUILD,CODE: wget_file
# used-by: fetch_file
wget_file() {
	local URL=${1:?} DIR=${2:-} FILE=${3:-}
	local WGET="wget --quiet" # --show-progress"
	info "  Downloading '$URL' -> '$FILE'"
	local OUTPUT
	# shellcheck disable=SC2086
	(	if [[ -d $DIR ]] ; then
			cd "$DIR"
		fi
		if OUTPUT="$(verbose $WGET --continue "$URL" ${FILE:+-O $FILE} >&2)" ; then
			[[ -z $OUTPUT ]] || echo "$OUTPUT" >&2
		else
			verbose $WGET "$URL" ${FILE:+-O $FILE}
		fi
	)
}

######################################################################
# BUILD: Fetch source
# used-by: build_buildroot, build_busybox, build_crosstool, build_kernel, build_mtd, build_opensbi, build_qemu, build_uboot, get_bmaptool, get_board_image, get_etcher, get_linarogcc
fetch_file() {
	local FILE=${1:?}
	local REMOTE="${3:-${1##*/}}"
	local URL="${2:?}/$REMOTE"
	debug "  fetch_file: FILE=$FILE URL=$URL REMOTE:$REMOTE"

	enter_workdir .

	if [[ -n $NOCACHE || -n $FORCE ]] ; then
		clearstamp "$FILE"
		rm -f "$FILE"
	elif checkstamp "$FILE" ; then
		vwarn "  Already downloaded: $FILE"
		return 0
	elif [[ -s $FILE ]] ; then
		if test_archive "$FILE" ; then
			vwarn "  Already downloaded: $FILE"
			stampfile "$FILE"
			return 0
		fi
		vwarn "  Partial download found: $FILE"
	fi
	warn "  Will Download: $FILE"

	local GUESS
	GUESS=$(find "$RESOURCES/" "$WORK/" ../../ -maxdepth 4 -name "${FILE##*/}" | sort | head -1)
	if [[ -n $GUESS ]] && test_archive "$GUESS" ; then
		copy_link "$GUESS" "$FILE"
	else
		wget_file "$URL" "${FILE%/*}" "${FILE##*/}"
	fi
	add_to_manifest "$FILE"
	stampfile "$FILE"

	leave_workdir
}

######################################################################
# Fetch git
# used-by: build_ftrace
fetch_git() {
	local DIR=${1:?} URI=${2:?} TAG=${3:-} OPTS=${4:-}
	debug "fetch_git: DIR=$DIR URI=$URI TAG=$TAG"

	enter_workdir .

	if [[ ! -d $DIR ]] ; then
		info "fetch_git: Cloning $URI into $DIR"
		# shellcheck disable=SC2086
		verbose git clone $OPTS "$URI" "$DIR"
	fi
	if [[ -n $TAG ]] ; then
		info "fetch_git: Checkout $TAG in $DIR"
		(cd "$DIR" && verbose git checkout "$TAG") 2>/dev/null
	fi

	leave_workdir
}

######################################################################
# used-by: config_to_tex
escape_tex() {
	debug "escape_tex: $*"
	echo "$@" | sed -re 's/\n/ /; s/_/\\_/g; s/[$]([^\\])/\\$\1/g;'
}

######################################################################
# used-by: walk_kconfig
make_expand() {
	local FILE=${1:?}
	debug "make_expand: FILE=$FILE $(pwd)"
	if [[ $FILE =~ \$ ]] ; then
		local PAT VALUE
		PAT=$(sed -re 's/^.*\$[{(]?([0-9A-Z_]*)[)}]?.*$/\1/' <<<"$FILE")
		VALUE=$(eval "echo \${$PAT}")
		debug "make_expand: FILE=$FILE $(pwd) PAT=$PAT ($(eval "echo \${$PAT}")) VALUE=$VALUE (${BR_BASE_DIR:-}:${BASE_DIR:-})"
		# shellcheck disable=SC1087
		sed -re "s|\\\$[{(]?$PAT[)}]?|$VALUE|g" <<<"$FILE"
	else
		echo "$FILE"
	fi
}

######################################################################
declare -A KCONFIG

######################################################################
# Find Kconfig prompt
# used-by: walk_kconfig
get_prompt() {
	local DATA=$1
	if [[ -n $DATA ]] ; then
		sed -re 's/" if .*/"/; s/ *\\$//; s/\\"//g' <<<"$DATA"
	fi
}

######################################################################
# ARG:kconfig: Walk through KConfig files and read metadata
# used-by: config_to_tex, walk_kconfig
walk_kconfig() {
	local DIR=${1:?} FILE=${2:?} PREFIX=${3:-} MENU=${4:-}
	debug "0 walk_kconfig: DIR=$DIR FILE=$FILE"
	local LINE CMD DATA CONFIG='TOPLEVEL' CHOICE MENUCONFIG READHELP

	if [[ ! -f "$DIR/$FILE" && -f "$DIR/$FILE.in" ]] ; then
		FILE="$FILE.in"
	elif [[ ! -f "$DIR/$FILE" && -f "$DIR/../$FILE" ]] ; then
		FILE="../$FILE"
	fi
	[[ -f "$DIR/$FILE" ]] || error "walk_kconfig: File not found: $DIR/$FILE"

	# Go through all the config lines
	while IFS='' read -r LINE ; do
		IFS=' ' read -r CMD DATA <<<"$LINE"
		# shellcheck disable=SC2001
		CMD=$(sed -e 's/^[[:space:]]*//' <<<"$CMD")
		debug "  walk_kconfig: LINE='$LINE' CMD='$CMD' DATA='$DATA'"
		if [[ -n ${READHELP:-} ]] ; then
			if [[ $LINE =~ ^[a-z] ]] ; then
				unset READHELP
			else
				LINE=${LINE//\\/\\\\}
				#debug "KCONFIG[$CONFIG:HELP]+=$LINE"
				continue
			fi
		fi
		if [[ $CMD =~ ^# || $LINE =~ ^# ]] ; then
			continue
		fi
		#debug "walk_kconfig: BR_BASE_DIR:${BR_BASE_DIR:-} (${BASE_DIR:-})"
		case "$CMD" in
			bool|int|tristate|string)
				#debug "walk_kconfig: CMD:$CMD DATA:$DATA"
				DATA=$(get_prompt "$DATA")
				if [[ ${CHOICE:-} == y ]] ; then
					#debug "CHOICE=$CHOICE (prompt) CONFIG=$CONFIG"
					if [[ -n ${DATA:-} ]] ; then
						CHOICE="$DATA"
					fi
				else
					KCONFIG[$CONFIG:TYPE]="$CMD"
					#info "KCONFIG[$CONFIG:TYPE]=$CMD"
					if [[ -n ${DATA:-} ]] ; then
						KCONFIG[$CONFIG:PROMPT]+="$DATA"
						#info "KCONFIG[$CONFIG:PROMPT]=$DATA"
					fi
				fi;;
			choice) CHOICE=y
				CONFIG="$PREFIX${DATA:-choice}";;
				#debug "CHOICE=$CHOICE CONFIG=$CONFIG" ;;
			endchoice) unset CHOICE ;;
			#comment) debug "walk_kconfig: $LINE" ;;
			config) CONFIG="$PREFIX${DATA%% *}"
				if [[ -n ${CHOICE:-} ]] ; then
					#debug "KCONFIG[$CONFIG:CHOICE]=$CHOICE"
					KCONFIG[$CONFIG:CHOICE]="$CHOICE"
				fi
				if [[ -n ${MENU:-} ]] ; then
					#debug "KCONFIG[$CONFIG:MENU]=$MENU"
					KCONFIG[$CONFIG:MENU]="$MENU"
				fi ;;
			help|---help---) READHELP=y ;;
			menuconfig)
				CONFIG="$PREFIX$DATA"
				if [[ -n ${MENU:-} ]] ; then
					#debug "KCONFIG[$CONFIG:MENU]=$MENU"
					KCONFIG[$CONFIG:MENU]="$MENU"
				fi ;;
			if)	CONFIG="$PREFIX$DATA"
				#info "if $DATA -> $CONFIG:PROMPT"
				if [[ ${KCONFIG[$CONFIG:TYPE]:-} = "menuconfig" ]] ; then
					MENU="${MENU:+$MENU \\\\rightarrow }${KCONFIG[$CONFIG:PROMPT]}"
				else
					MENU="${MENU:+$MENU \\\\rightarrow}"
				fi
				debug "MENU => $MENU ($LINE)";;
			endif) if [[ $MENU =~ \\rightarrow ]] ; then
					MENU="${MENU% \\\\rightarrow *}"
				else
					MENU=
				fi
				debug "MENU <= $MENU ($LINE)";;
			menu) MENU="${MENU:+$MENU \\\\rightarrow }$DATA"
				debug "MENU (menu) => $MENU ($LINE)";;
			endmenu) if [[ $MENU =~ \\rightarrow ]] ; then
					MENU="${MENU% \\\\rightarrow *}"
				else
					MENU=
				fi
				debug "MENU <= $MENU ($LINE)";;
			option*env=) DATA="$(eval "echo ${LINE#*=}")"
				#debug "walk_kconfig: $CONFIG=$DATA"
				# shellcheck disable=SC1090
				. <(echo "$CONFIG=$DATA");;
			prompt) DATA=$(get_prompt "$DATA")
				if [[ ${CHOICE:-} == y ]] ; then
					CHOICE="$DATA"
					#debug "CHOICE=$CHOICE (prompt) CONFIG=$CONFIG"
				else
					KCONFIG[$CONFIG:PROMPT]+="$DATA"
					#debug "KCONFIG[$CONFIG:PROMPT]=$DATA"
				fi ;;
			source) DATA=$(make_expand "${DATA//\"/}")
				info "  source $DATA"
				walk_kconfig "$DIR" "$DATA" "$PREFIX" "$MENU" ;;
			*) debug "KCONFIG[$CONFIG:OTHER]+=$LINE" ;;
				#KCONFIG[$CONFIG:OTHER]+="$LINE\n" ;;
		esac
	done <"$DIR/$FILE"
}

######################################################################
# used-by: config_to_tex
dump_kconfig() {
	local KEY VALUE
	for KEY in "${!KCONFIG[@]}" ; do
		if [[ $KEY =~ "#" || -z ${KCONFIG[$KEY]:-} ]] ; then
			warn "Possible comment during dump? '$KEY'"
			continue
		fi
		#warn "Before: KCONFIG[$KEY]='${KCONFIG[$KEY]}'"
		# shellcheck disable=SC2016
		VALUE=$(sed -r -e '
			s/"/\\"/g;
			s/rightarrow( [\/]+rightarrow)*/rightarrow/g;
			s/ *$//;
			s/^[\/]+rightarrow //;
			s/ *[\/]+rightarrow$//;
			s/\\/\\\\/g;
			s/`/\\`/g;
			s/\$/\\\$/g;
		' <<<"${KCONFIG[$KEY]}")
		#warn "After: KCONFIG[$KEY]='$VALUE'"
		[[ ! $VALUE =~ rightarrow$ ]] || error "KCONFIG[$KEY]='$VALUE'"
		[[ $KEY =~ :TYPE ]] || echo -e "KCONFIG[$KEY]=\"$VALUE\""
	done | sort
}

######################################################################
declare -A DESCRIPTION

######################################################################
# CONFIG: Lookup CONFIG variables for tex output
config_to_tex() {
	local OUTPUT=${1:?} KEYS=${2:?} DIR=${3:?} FILE=${4:?} PREFIX=${5:-}
	[[ -n $CONFIG_GENERATE_TEX && -n $FILE ]] || return 0
	# shellcheck disable=SC2086
	debug "config_to_tex: OUTPUT=$OUTPUT DIR=$DIR FILE=$FILE PREFIX=$PREFIX KEYS="$KEYS

	# shellcheck disable=SC2086
	info "  Build $OUTPUT from config $DIR"

	enter_workdir .

	local CACHE="$DIR/kconfig.cache"
	if [[ -z ${NOCACHE:-} && -s $CACHE ]] ; then
		info "  Reading $CACHE for $DIR/$FILE"
	else
		info "Parsing $DIR/$FILE"
		walk_kconfig "$DIR" "$FILE" "$PREFIX"
		info "Saving $DIR KConfig into $CACHE"
		dump_kconfig >"$CACHE"
	fi
	# shellcheck disable=SC1090
	. "$CACHE"

	local LINE KEY DATA DESC MENU ECHOICE EDESC EKEY ETYPE NL
	local ITEM='   \\item\n\n'
	rm -f "${OUTPUT:?}"
	cat <<HEADER >"$OUTPUT"
%% $GENERATED and config.lfcw
%% chktex-file 18

HEADER

	if [[ -n $DEBUG_TEX ]] ; then
		# shellcheck disable=SC2059
		printf "$ITEM   KCONFIG\\_DEBUG\n\n" >>"$OUTPUT"
	fi

	while read -r LINE; do
		IFS='=' read -r KEY DATA <<<"$LINE"
		[[ -n $KEY ]] || continue;

		######################################################
		# Print menus
		if [[ -n ${KCONFIG[$KEY:MENU]:-} && "${KCONFIG[$KEY:MENU]:-}" != "${MENU:-}" ]] ; then
			if [[ -n ${MENU:-} ]] ; then
				printf '   \\end{itemize}\n\n'
			fi
			MENU=${KCONFIG[$KEY:MENU]}
			local FIXED
			FIXED=$(sed -re 's/_/\\_/g; s/&/\\&/g; s/\\rightarrow/$\\rightarrow\$/g' <<<"$MENU")
			printf "$ITEM   In the %s menu:\n\n" "$FIXED"
			printf '   \\begin{itemize}\n\n'
		fi

		######################################################
		# Print config options
		EKEY=$(escape_tex "$KEY")

		DESC=${DESCRIPTION[$KEY]:-}
		[[ -n $DESC ]] || DESC="${KCONFIG[$KEY:PROMPT]:-}"
		[[ -n $DESC ]] || error "Description empty for $KEY"
		EDESC=$(escape_tex "$DESC")
		#DATA=$(escape_tex "$DATA")

		ECHOICE=$(escape_tex "${KCONFIG[$KEY:CHOICE]:-}")
		ETYPE="${KCONFIG[$KEY:TYPE]:-}"

		# shellcheck disable=SC2016
		DATA=$(sed -re 's/"//g; s/([$_])/\\\1/g' <<<"$DATA")
		if [[ $(( ${#EDESC} + ${#DATA} + ${#EKEY} )) -gt 77 ]]; then
			warn "wrapping: $EDESC ($EKEY)"

			NL='\\newline'
		else
			NL=''
		fi

		debug "  config_to_tex: KEY=$KEY($EKEY) DATA=$DATA DESC=$DESC($EDESC)"
		if [[ $LINE = "$KEY" ]] ; then
			printf "$ITEM   Disable \\\\textbf{%s} $NL(%s)\n\n" "$DESC" "$EKEY"
		elif [[ -n $ECHOICE ]] ; then
			printf "$ITEM   Set \\\\textbf{%s} to \\\\textit{%s} $NL(%s)\n\n" "$ECHOICE" "$DESC" "$EKEY"
		else
			case "$DATA" in
				y) printf "$ITEM   Enable \\\\textbf{%s} $NL(%s)\n\n" "$EDESC" "$EKEY" ;;
				m) printf "$ITEM   Set \\\\textbf{%s} to Module $NL(%s)\n\n" "$EDESC" "$EKEY" ;;
				*) if [[ $ETYPE = string && $DATA = \"\" ]] ; then
					printf "$ITEM   Delete \\\\textbf{%s} $NL(%s)\n\n" "$EDESC" "$EKEY"
				else
					#warn "$ETYPE Set to $EDESC => $DATA"
					printf "$ITEM   Set \\\\textbf{%s} to \\\\textit{%s} $NL(%s)\n\n" "$EDESC" "$DATA" "$EKEY"
				fi ;;
			esac
		fi
	done <<<"$KEYS" >>"$OUTPUT"
	if [[ -n ${MENU:-} ]] ; then
		printf '   \\end{itemize}\n\n' >>"$OUTPUT"
	fi
	sed -i -re 's|\\\$\\rightarrow|$\\rightarrow|g' "$OUTPUT"

	cat <<SAVEEXIT >>"$OUTPUT"
   \item

   \textbf{Exit all levels of the configuration program}

   \begin{itemize}

      \item

      Be sure to save the configuration.

   \end{itemize}

SAVEEXIT
	leave_workdir
}

######################################################################
# CONFIG: Generate Kconfig summary in tex
config_to_tex_summary() {
	local OUTPUT=${1:?} DIR=${2:?} NAME=${3:?} CONFIG=${4:-.config}
	[[ -n $CONFIG_GENERATE_TEX ]] || return 0
	debug "config_to_tex_summary: OUTPUT=$OUTPUT DIR=$DIR CONFIG=$CONFIG"
	info "  Generating summary $OUTPUT from $DIR"

	enter_workdir .

	local CONFIGNUM CONFIGSET TOTALNUM
	if [[ ! -f "$DIR/$CONFIG" ]] ; then
		if [[ -f "$CONFIG" ]] ; then
			DIR="."
		else
			error "config_to_tex_summary: File not found: $DIR/$CONFIG"
		fi
	fi
	CONFIGNUM=$(grep -c -E '=|is not set' "$DIR/$CONFIG")
	CONFIGSET=$(grep -c -E '=' "$DIR/$CONFIG")

	local CACHE="$DIR/kconfig.cache"
	if [[ ! -f $CACHE ]] ; then
		error "config_to_tex_summary: File not found: $CACHE"
	fi
	TOTALNUM=$(grep -c :PROMPT "$CACHE")

	rm -f "${OUTPUT:?}"
	printf "%% chktex-file 18\n\n" >"$OUTPUT"

	if [[ -n $DEBUG_TEX ]] ; then
		# shellcheck disable=SC2059
		printf "$ITEM   KCONFIG\\_DEBUG\n\n" >>"$OUTPUT"
	fi

	cat <<ENDTEX >>"$OUTPUT"
In the \\textbf{$NAME project} there are $TOTALNUM configuration options.
However because many options have dependencies on others,
a typical \\kernlink{$CONFIG} file has the possibility of setting something
like $CONFIGNUM options, with only $CONFIGSET of them having set values.
ENDTEX
}

######################################################################
# Modify config file
# used-by: config_build
collapse_config() {
	CONFIGS="$*"
	local CONFIG UNIQ KEY VALUE
	debug "collapse_config: $*"
	declare -A UNIQ
	for CONFIG in $CONFIGS; do
		IFS='=' read -r KEY VALUE <<<"$CONFIG"
		UNIQ[$KEY]="${VALUE:-NONE}"
	done
	for KEY in "${!UNIQ[@]}"; do
		if [[ ${UNIQ[$KEY]} = "NONE" ]] ; then
			echo "$KEY" >&2
			echo "$KEY"
		else
			echo "$KEY=${UNIQ[$KEY]}" >&2
			echo "$KEY=${UNIQ[$KEY]}"
		fi
	done
}

######################################################################
# Modify config file
# used-by: config_build
edit_config() {
	local CONFIG=${1:?}
	local EXTRA_CONFIG=${2:?}
	# shellcheck disable=SC2086
	debug "edit_config: FILE=$FILE" $EXTRA_CONFIG
	local RET=0

	# shellcheck disable=SC2086
	info "edit_config: Modify $CONFIG" $EXTRA_CONFIG

	if [[ -e $CONFIG ]] ; then
		local LINE KEY DATA
		while read -r LINE; do
			IFS='=' read -r KEY DATA <<<"$LINE"
			[[ -n $KEY ]] || continue;
			if [[ $LINE = "$KEY" ]] ; then
				debug "  edit_config: Removing $LINE"
				# If no '=' then we're just deleting the key
				sed -i -e "/^# $KEY/d" -e "/^$KEY/d" "$CONFIG"
			elif grep -q -E "^# $KEY " "$CONFIG" ; then
				debug "  edit_config: Setting $LINE"
				sed -i -e "s/^# $KEY .*\$/$LINE/" "$CONFIG"
			elif grep -q -E "^$KEY=" "$CONFIG" ; then
				debug "  edit_config: Updating $LINE"
				sed -i -e "s/^$KEY=.*\$/$LINE/" "$CONFIG"
			else
				debug "  edit_config: Adding $LINE"
				echo "$LINE" >> "$CONFIG"
				RET=1
			fi
		done <<<"$EXTRA_CONFIG"
	else
		error "File not found: $CONFIG"
	fi
	return "$RET"
}

######################################################################
# used-by: save_config
do_save_config() {
	local DOTCONFIG=${1:?}
	local CONFIG=${2:?}

	if [[ -e $DOTCONFIG ]] ; then
		if [[ -s $DOTCONFIG && ! -e $CONFIG ]] ; then
			verbose cp $VERBOSE "$DOTCONFIG" "$CONFIG"
			add_to_manifest "$CONFIG"
		fi
	else
		error "save_config: $DOTCONFIG doesn't exist (was to be copied to $CONFIG)"
	fi
}

######################################################################
# Save config file
# used-by: build_buildroot, build_busybox, build_crosstool, build_uboot, tar_kernel
save_config() {
	local DOTCONFIG=${1:?}
	local CONFIG=${2:?}

	do_save_config "$DOTCONFIG" "$CONFIG"
	if [[ -f ${DOTCONFIG/\.config/defconfig} ]] ; then
		do_save_config "${DOTCONFIG/\.config/defconfig}" "${CONFIG/-config/-defconfig}"
	fi
}

######################################################################
# Save build artifact
# used-by: build_buildroot, build_busybox, build_ftrace, build_kernel, build_mtd, build_opensbi, build_perf, build_qemu, build_uboot, 
save_build_artifacts() {
	# shellcheck disable=SC2178
	local FROM=${1:?} TO=${!#}
	#debug "save_build_artifacts: FROM:$FROM TO:$TO"

	# shellcheck disable=SC2128
	if [[ -e $FROM ]] ; then
		local COMPRESS=""
		if [[ ! $FROM =~ \.xz$ && $TO =~ \.xz$ ]] ; then
			COMPRESS=xz
		fi
		if [[ -d $TO ]] ; then
			# FIXME: Maybe support directories eventually?
			error "save_build_artifacts: $TO can't be a directory"
		elif [[ -e $TO ]] ; then
			if [[ -n ${COMPRESS:-} ]] ; then
				vwarn "  File already exists: $TO"
			elif cmp --quiet "$FROM" "$TO" ; then
				vwarn "  File already exists and is identical: $TO"
			else
				warn "  File already exists and is different: $TO"
			fi
			[[ -n $FORCE ]] || return 0
		fi
		if [[ -n ${COMPRESS:-} ]] ; then
			compress "$FROM" "$TO"
		else
			copy_link "$FROM" "$TO"
		fi
		add_to_manifest "$TO"
	else
		# shellcheck disable=SC2128
		error "save_build_artifacts: $FROM doesn't exist (was to be copied to $TO)"
	fi
}

######################################################################
# BUILD: Resize filesystem image with qemu-img
resize_image() {
	local FILE=${1:?} SIZE=${2:?}
	if [[ -z $FETCH_ONLY && -f $FILE ]] ; then
		if qemu-img info "$FILE" | grep -q "virtual size: ${SIZE%M} MiB" ; then
			info "resize_image: $FILE is already $SIZE MiB"
			return 1
		else
			info "Resize $FILE to $SIZE"
			verbose qemu-img resize -f raw "$FILE" "$SIZE"
		fi
	fi
}

######################################################################
# BUILD: Tar build artifact
# used-by: build_buildroot, build_crosstool, mkroot_tar, tar_kernel
tar_build_artifacts() {
	local OPTS="-c"
	if [[ -n $FETCH_ONLY ]] ; then
		return 0
	fi

	if [[ ${1:?} =~ ^- ]] ; then
		OPTS=$1
		shift
	fi
	local TAR=${1:?}; shift

	if [[ $TAR =~ xz$ ]] ; then
		OPTS="$OPTS -J"
	fi

	local FILE
	for FILE in "$@" ; do
		[[ -e $FILE ]] || error "File not found: $FILE"
	done

	clearstamp "$TAR"
	info "  Tarring $TAR: $*"
	# shellcheck disable=SC2086
	verbose fakeroot tar $OPTS -f "$TAR" "$@"
	add_to_manifest "$TAR"
	stampfile "$TAR"
}

######################################################################
# BUILD: tar bare git clones
tar_bare_git_clone() {
	local TAR=${1:?} URI=${2:?} DIR=${3:?}

	enter_workdir .

	if checkstamp "$TAR" ; then
		leave_workdir
		return 0
	fi

	if [[ -n $FORCE || -n $NOCACHE ]] ; then
		verbose rm -rf "$TAR" "$DIR"
	fi

	if [[ ! -d $DIR ]] ; then
		fetch_git "$DIR" "$URI" '' '--bare'
	fi

	if [[ ! -f $TAR ]] ; then
		tar_build_artifacts "$TAR" "$DIR"
	fi

	stampfile "$TAR"

	leave_workdir
}

######################################################################
declare -A IGNORE

######################################################################
# Unpack tarball
# used-by: get_tree_from_tar, mkroot_tar, squashfs_root, unpack
untar() {
	local TAR=${1:?}
	local CHDIR=${2:-}
	local SUDO=${3:-}

	exists_or_error "$TAR" "tar file not found: $TAR"
	verbose ${SUDO:+sudo} tar -x ${CHDIR:+-C "$CHDIR"} -f "$TAR"
}

######################################################################
# Unpack source code for build
# used-by: build_buildroot, build_busybox, build_crosstool, build_kernel, build_mtd, build_opensbi, build_qemu, build_uboot, get_linarogcc
unpack() {
	local TAR=${1:?}
	local TARDIR="${TAR%.tar*}"
	local DIR=${2:-$TARDIR}

	exists_or_error "$TAR" "tar file not found: $TAR"
	if [[ -n $FORCE && -e $DIR ]] ; then
		local TDIR
		TDIR="$(mk_tmpdir "$DIR-XXXXXX")"
		rmdir "$TDIR"
		verbose mv $VERBOSE "$DIR" "$TDIR"
	fi

	if [[ -e $DIR ]] ; then
		vwarn "  Already unpacked: $DIR"
		return 1
	else
		info "  Unpacking: $DIR from $TAR"
		untar "$TAR"
		if [[ $DIR != "$TARDIR" ]] ; then
			vwarn "Renaming '$TARDIR' -> '$DIR'"
			verbose mv $VERBOSE "$TARDIR" "$DIR"
		fi
	fi
}

######################################################################
# Clean and configure build
# used-by: build_buildroot, build_busybox, build_crosstool, build_kernel, build_uboot
config_build() {
	local DIR=${1:?}
	local MAKE=${2:?}
	local BUILD_TARGETS=${3:?}
	local DEFCONFIG=${4:?}
	local EXTRA_CONFIG=${5:-}
	local LOG=${6:-$DIR.log}
	local HASOLDDEFCONFIG=""
	local CONFIG="$DIR/.config"
	debug "config_build: DIR=$DIR MAKE=$MAKE BUILD_TARGETS=$BUILD_TARGETS DEFCONFIG=$DEFCONFIG LOG=$LOG EXTRA=$EXTRA_CONFIG"

	if [[ -z $FORCE && -e $DOTCONFIG ]] ; then
		vwarn "  Already configured: $DIR"
		return 1
	fi

	info "  Prepare $DIR"

	if [[ $($MAKE -s -C "$DIR" help | grep -c savedefconfig) -gt 0 ]] ; then
		HASOLDDEFCONFIG=y
		debug "config_build: HASOLDDEFCONFIG=y"
	fi

	##############################################################
	# Add base default configuration
	rm -f "${LOG:?}"
	if [[ -e $DEFCONFIG ]] ; then
		make_targets "$DIR" "$MAKE" "$BUILD_TARGETS"
		verbose cp $VERBOSE "$DEFCONFIG" "$CONFIG"
	else
		make_targets "$DIR" "$MAKE" "$BUILD_TARGETS $DEFCONFIG" '' "$LOG"
	fi

	if [[ ! -e $CONFIG ]] ; then
		error "config_build: configuration file for $DEFCONFIG failed"
	fi

	##############################################################
	EXTRA_CONFIG=$(collapse_config "$EXTRA_CONFIG")

	##############################################################
	# Add extra options to configuration
	if [[ -n $EXTRA_CONFIG ]] && edit_config "$CONFIG" "$EXTRA_CONFIG" ; then
		echo "I: We added some new config options"
	fi

	##############################################################
	(cd "$DIR"
		cp $VERBOSE .config config-before
		stty sane
		export TERM=linux
		if [[ -n ${MENUCONFIG:-} ]] ; then
			# shellcheck disable=SC2086
			$MAKE -s menuconfig
		else
			if [[ -n $HASOLDDEFCONFIG ]] ; then
				debug "config_build: Using olddefconfig"
				# shellcheck disable=SC2086
				verbose $MAKE -s olddefconfig
				# shellcheck disable=SC2086
				verbose $MAKE -s savedefconfig
			else
				yes '' | $MAKE -s oldconfig >>"$LOG" || true
				touch defconfig
			fi
		fi
		cp $VERBOSE .config config-after

		######################################################
		# Checking our work
		local CO ERR
		while read -r CO ; do
			[[ -n $CO ]] || continue
			#echo "-> $CO"
			# shellcheck disable=SC2144
			case "$CO" in
				# Was this added?
				*=*) if ! grep -q -e "^$CO$" .config ; then
					warn "$CO wasn't set"
					ERR=y
				fi ;;
				# Was this removed? (or ignored?)
				*) if [[ -v IGNORE[@] && -n ${IGNORE[$CO]:-} ]] ; then
					continue
				fi
				if grep -q "^$CO=" ; then
					warn "$CO wasn't unset"
					ERR=y
				fi ;;
			esac
		done <<<"$EXTRA_CONFIG"

		######################################################
		# Indicate an error
		if [[ -n ${ERR:-} ]] ; then
			mv .config config-failed
			error "Configuration differences from intended"
		fi
	)

	return 0
}

######################################################################
# Compile code
# used-by: build_buildroot, build_busybox, build_crosstool, build_ftrace, build_kernel, build_mtd, build_opensbi, build_perf, build_qemu, build_uboot
compile_code() {
	local DIR=${1:?}
	local MAKE=${2:?}
	local BUILD_TARGETS=${3:?}
	local OPTS=${4:-}
	local LOG=${5:-$DIR.log}
	debug "compile_code: DIR=$DIR MAKE=$MAKE TARGETS=$BUILD_TARGETS OPTS=$OPTS LOG=$LOG"
	if [[ -n $BUILD_TARGETS ]] ; then
		info "  Compile $DIR"
		make_targets "$DIR" "$MAKE" "$BUILD_TARGETS" "$OPTS" "$LOG"
	fi
}

######################################################################
# ARG:ls: List manifest files
list_workdir() {
	if [[ ${1:-} == "${MANIFEST%.*}" ]] ; then
		shift
		cd "$WORKDIR"
		# shellcheck disable=SC2046
		ls "$@" $(cat "$WORKDIR/$MANIFEST")
	else
		ls "$@" "$WORKDIR"
	fi
}

######################################################################
# Make a temporary file
# used-by: add_index, add_to_manifest
mk_tmpfile() {
	local TEMPLATE="$WORKDIR/$TMPLATE"
	[[ -n $WORKDIR && -d $WORKDIR ]] || error "No WORKDIR for mk_tmpfile"
	local TFILE
	TFILE="$(mktemp "$TEMPLATE")"
	# shellcheck disable=SC2086
	TFILE="$(realpath $TFILE)"
	debug "mk_tmpfile: TFILE=$TFILE"
	echo "$TFILE"
}

######################################################################
# Make a temporary directory
# used-by: get_tree_from_tar, mkroot_tar, squashfs_root, tar_kernel
mk_tmpdir() {
	local TEMPLATE="${1:-$WORKDIR/$TMPLATE}"
	[[ -n $WORKDIR && -d $WORKDIR ]] || error "No WORKDIR for mk_tmpdir"
	local TDIR
	TDIR="$(mktemp -d "$TEMPLATE")"
	# shellcheck disable=SC2086
	TDIR="$(realpath $TDIR)"
	debug "mk_tmpdir: TDIR=$TDIR"
	echo "$TDIR"
}

######################################################################
# Clean mktmp files
# used-by: clean_files, mkroot_tar, squashfs_root, tar_kernel
clean_tmp() {
	[[ -n $WORKDIR && -d $WORKDIR ]] || error "No WORKDIR for clean_tmp"
	# shellcheck disable=SC2086
	rm -rf "${WORKDIR:?}/$TMPGLOB"
}

######################################################################
# ARG:clean: Clean manifest files
clean_files() {
	clean_tmp
	enter_workdir .
	case "${1:-}" in
		all|${MANIFEST%.*}) if [[ -f $MANIFEST ]] ; then
			# shellcheck disable=SC2046
			verbose rm -f $(cat "$MANIFEST")
			verbose savelog "$MANIFEST"
		fi
		info "All clean." ;;
		*) warn "Nothing to clean? (all? ${MANIFEST%.*}?)" ;;
	esac
	leave_workdir
}

######################################################################
# BUILD: ARG:buildroot: Unpack and build buildroot then save config
build_buildroot() {
	local BRVERSION=${1:-$BRVERSION}
	local NAME="${2:-}"
	local BR_CONFIG="${3:-}"
	debug "build_buildroot: BRVERSION=$BRVERSION NAME=$NAME BR_CONFIG=${BR_CONFIG:+...}"
	if [[ $# -lt 2 ]] ; then usage "Usage: $TOOL buildroot BRVER NAME"; fi

	##############################################################
	[[ -n $BRVERSION ]] || error "No buildroot version specified"
	[[ -n $BRDEFCONFIG ]] || error "No buildroot configuration specified"

	##############################################################
	check_board
	local BUILDROOT="buildroot-$BRVERSION.tar.bz2"
	local DIR="buildroot-$BRVERSION${NAME:+-$NAME}"
	local DL="buildroot-$BRVERSION-dl"
	local BRDL="$DIR/dl"
	local DOTCONFIG="$DIR/.config"
	local CONFIG="$BOARD-buildroot-$BRVERSION${NAME:+-$NAME}-config"
	local IMAGE="$DIR/output/images/rootfs.tar.xz"
	local IMAGELN="$BOARD-buildroot-$BRVERSION${NAME:+-$NAME}.tar.xz"
	local DLTAR="$DL.tar"

	enter_workdir .

	##############################################################
	announce "Building Buildroot: $DIR"

	##############################################################
	# See if we're already done
	if checkstamp "$IMAGELN" ; then
		leave_workdir
		return 0
	fi

	##############################################################
	fetch_file "./$BUILDROOT" "$BRURL"
	if [[ -n $FETCH_ONLY ]] ; then
		leave_workdir
		return 0
	fi

	##############################################################
	if ! unpack "$BUILDROOT" "$DIR" ; then
		verbose mkdir -p "$DL"
		verbose ln --symbolic --force "../$DL" "$BRDL"
	fi

	##############################################################
	if config_build "$DIR" make distclean "$BRDEFCONFIG" "$BR_CONFIG" ; then
		rm -f "${IMAGE:?}"
	fi

	##############################################################
	# Sometimes the dl link from above is destroyed. Recreate it.
	if [[ ! -L $BRDL ]] ; then
		vwarn "  Fixing: $BRDL"
		verbose mkdir -p "$DL"
		if [[ -e $BRDL ]] ; then
			if [[ $(find "$BRDL" | wc -l) -gt 1 ]] ; then
				verbose mv -f "$BRDL/"* "$DL" 2>/dev/null \
					|| warn "    Deleting files which couldn't be moved"
			fi
			rm -rf "${BRDL:?}"
		fi
		verbose ln --symbolic --force "../$DL" "$BRDL"
	fi

	debug "do_build_buildroot: DIR=$DIR CONFIG=$CONFIG IMAGELN=$IMAGELN DL=$DL"

	##############################################################
	if [[ -z $FORCE && -e $IMAGE ]] ; then
		vwarn "  Already built: $DIR"
	else
		compile_code "$DIR" make "source clean all"
		verbose rm -f "${CONFIG:?}" "$IMAGELN" "$DLTAR"
	fi

	##############################################################
	tar_build_artifacts "$DLTAR" "$DL"
	save_config "$DOTCONFIG" "$CONFIG"
	#add_to_manifest "$BUILDROOT"
	save_build_artifacts "$IMAGE" "$IMAGELN"

	##############################################################
	list_files "$BUILDROOT" "$CONFIG" "$IMAGELN" "$DLTAR"
	stampfile "$IMAGELN"

	leave_workdir
}

######################################################################
# BUILD: ARG:busybox: Unpack and build busybox then save config
build_busybox() {
	local BBVERSION=${1:-$BBVERSION}
	local BRVERSION=${2:-$BRVERSION}
	local NAME=${3:-uclibc}
	local BB_CONFIG=${4:-}
	debug "build_busybox: BBVERSION=$BBVERSION BRVERSION=$BRVERSION NAME=$NAME BB_CONFIG=$BB_CONFIG"

	##############################################################
	[[ -n $BBVERSION ]] || error "build_busybox: No busybox version specified"
	[[ -n $BRVERSION ]] || error "build_busybox: No buildroot version specified"

	##############################################################
	#local DL="buildroot-$BRVERSION-dl"
	#local BUSYBOX="$DL/busybox-$BBVERSION.tar.bz2"
	local BUSYBOX="busybox-$BBVERSION.tar.bz2"
	local DIR="busybox-$BBVERSION"
	local DOTCONFIG="$DIR/.config"
	local OUTPUT="buildroot-$BRVERSION${NAME:+-$NAME}/output"
	local CONFIG="busybox-$BBVERSION-config"
	local EXE="$DIR/busybox"
	local BFILE="$BOARD-busybox-$BBVERSION.xz"

	enter_workdir .

	##############################################################
	announce "Building busybox for $NAME: $DIR"

	##############################################################
	# See if we're already done
	if checkstamp "$BFILE" ; then
		leave_workdir
		return 0
	fi

	##############################################################
	fetch_file "./$BUSYBOX" "$BBURL"
	if [[ -n $FETCH_ONLY ]] ; then
		leave_workdir
		return 0
	fi

	##############################################################
	unpack "$BUSYBOX" "$DIR" || true

	# shellcheck disable=SC2086
	PATH="$(realpath ./$OUTPUT)/host/usr/bin:$PATH"
	export PATH

	##############################################################
	if config_build "$DIR" make clean defconfig "$BB_CONFIG" ; then
		rm -f "${EXE:?}"
	fi

	##############################################################
	if [[ -z $FORCE && -e "$EXE" ]] ; then
		vwarn "  Already built: $DIR"
	else
		info "  Compiling $DIR"
		compile_code "$DIR" make "clean all"
		rm -f "${CONFIG:?}"
	fi

	##############################################################
	info "  Packaging build results"
	#add_to_manifest "$BUSYBOX"
	save_build_artifacts "$EXE" "$BFILE"
	save_config "$DOTCONFIG" "$CONFIG"

	##############################################################
	list_files "$BUSYBOX" "$BFILE" "$CONFIG"
	stampfile "$BFILE"

	leave_workdir
}

######################################################################
# ARG:chapters: Create numbered chapter links
link_chapters() {
	local COURSE=${1:?} CDIR
	cd "$TOPDIR"
	CDIR=${TEXFILE%/*}
	debug "link_chapters: COURSE=$COURSE TEX=$TEXFILE CDIR=$CDIR"

	# shellcheck disable=SC2086
	find "$CDIR" -type l -name '[0-9]*' -print0 | xargs -0 --no-run-if-empty rm -f
	find "$CDIR" -type l -name '[A-Z]_*' -print0 | xargs -0 --no-run-if-empty rm -f

	[[ ${2:-} != clean ]] || exit 0

	# Link chapters
	local CH=0 DIR NAME LINK
	for DIR in $(get_chapters "$COURSE") ; do
		CH=$(( CH + 1 ))
		# shellcheck disable=SC2001
		NAME=$(sed -e 's/^[^_]*_//' <<< "$DIR")
		LINK=$(printf "$CDIR/%02d_%s" "$CH" "$NAME")
		# shellcheck disable=SC2046
		verbose ln $VERBOSE --symbolic --force "$DIR" "${LINK##*/}"
	done

	# Link appendices
	CH=64
	for DIR in $(get_appendices "$COURSE") ; do
		CH=$(( CH + 1 ))
		# shellcheck disable=SC2001
		NAME=$(sed -e 's/^[^_]*_//' <<< "$DIR")
		# shellcheck disable=SC2027
		LINK="$CDIR/"$(awk 'BEGIN{printf "%c", '$CH'}')"_$NAME"
		# shellcheck disable=SC2046
		verbose ln $VERBOSE --symbolic --force "$DIR" "${LINK##*/}"
	done
}

######################################################################
# ARG:lschapters: List chapters and appendices dirs for course
list_chapters() {
	debug "list_chapters: COURSE=$COURSE TOPDIR=$TOPDIR"

	( cd "$TOPDIR"
	find . -type l -name '[0-9]*'  -print0 | xargs -0 --no-run-if-empty ls -d "$@"
	find . -type l -name '[A-Z]_*' -print0 | xargs -0 --no-run-if-empty ls -d "$@"
	) | sed -e 's|^.*\./||'
}

######################################################################
# ARG:listlabs: List labs for a chapter
list_labs() {
	debug "list_labs: COURSE=$COURSE TOPDIR=$TOPDIR ARCH=$ARCH"
	local CHAPS="" CHAP DIRS

	CHAPS=$(list_chapters);
	[[ -n $CHAPS && -z $ALL ]] || CHAPS="$(get_all_chapters)"

	for CHAP in $CHAPS ; do
		if [[ -n $BOARD && -d "$CHAP/$BOARD" ]] ; then
			DIRS="$CHAP/ $CHAP/$BOARD/"
		else
			DIRS="$CHAP/"
		fi
		debug "CHAP: $DIRS"
		# shellcheck disable=SC2086
		find $DIRS -maxdepth 1 -name labs -o -name "${COURSE}labs"
	done
}

######################################################################
# ARG:change: Which chapters changed since the last commit
git_change() {
	local COURSE=${1:?} CDIR DIR STATUS NUM=-2
	CDIR=${TEXFILE%/*}
	debug "git_change: COURSE=$COURSE TEX=$TEXFILE CDIR=$CDIR"

	cd "$CDIR"
	for DIR in . IMAGES $(get_all_chapters "$COURSE") ; do
		NUM=$((NUM + 1))
		STATUS="$(cd "$DIR" >/dev/null; git status -s .)"
		#info "$NUM $DIR"
		if [[ -n $STATUS ]] ; then
			echo "=== $NUM $DIR ==="
			# shellcheck disable=SC2001
			sed -e 's/^/  /' <<<"$STATUS"
		fi
	done | ${PAGER:-less -R}
}

######################################################################
# ARG:git: Git utilities
git_cmds() {
	local CMD=${1:-status} DIR=${2:-.}
	DIR="$(realpath "$DIR")"
	DIR=${DIR#$COURSES}
	debug "git_cmds: CMD=$CMD DIR=$DIR"

	cd "$COURSES"
	ALL=y
	get_tex_files "$DIR" ALL | xargs git "$CMD"
}

######################################################################
# ARG:check: Check for keywords which shouldn't be used anymore
# used-by: check_all
declare -A POINTOUT
check_pointout() {
	local FILE WORD

	# shellcheck disable=SC1091
	if [[ -f check.lfcw ]] ; then
		. check.lfcw
	fi

	info "Check for keywords which shouldn't be used anymore"
	ALL=yes
	for FILE in $(get_tex_files) ; do
		# shellcheck disable=SC2068
		for WORD in ${!POINTOUT[@]} ; do
			# shellcheck disable=SC1087
			if sed -e '/BEGIN-NOCHECK/,/END-NOCHECK/d' \
				-e '/CHECK-OKAY/d' "$FILE" \
				| grep -i -q -e "[ ,_-]$WORD[ ,_-]" ; then

				# shellcheck disable=SC1087
				grep -i --color=yes -C 3 -e "[ ,_-]$WORD[ ,_-]" "$FILE"
				if [[ -n $EDIT ]] ; then
					# shellcheck disable=SC1087,SC2086
					edit_latex "[ ,_-]$WORD[ ,_-]" "$FILE"
				fi
				case "${POINTOUT[$WORD]}" in
					error) error "Found $WORD in $FILE" ;;
					warn) warn "Found $WORD in $FILE" ;;
					*) error "Invalid POINTOUT for $WORD in check.lfcw" ;;
				esac
			fi
		done
	done
}

######################################################################
# used-by: check_all
check_solutions() {
	tar_files

	local FILES FILE SOLFILES RESFILES
	FILES="$(get_tex_files)"

	info "Check that SOLUTIONS files are being listed properly"
	SOLFILES="$(find "$TOPDIR/SOLUTIONS" -type f | sed -e 's|^.*/||')"
	for FILE in $SOLFILES ; do
		#find_latex "RESOURCES/.*/$FILE"
		# shellcheck disable=SC2086
		grep -E "RESOURCES/.*/$FILE" $FILES || true
	done

	info "Check that RESOURCES files are being listed properly"
	RESFILES="$(find "$TOPDIR/RESOURCES" -type f | sed -e 's|^.*/||')"
	for FILE in $RESFILES ; do
		#find_latex "SOLUTIONS/.*/$FILE"
		# shellcheck disable=SC2086
		grep -E "SOLUTIONS/.*/$FILE" $FILES || true
	done
}

######################################################################
# ARG:check: Check age of output files
# used-by: check-stuff
check_output() {
	local FILE

	info "Check for files which should have probably been updated"
	ALL=yes
	# shellcheck disable=SC2086
	while read -r FILE ; do
		if [[ $TEXFILE -nt $FILE \
			&& $(find "$FILE" -mtime +10 | wc -l) -gt 0 ]] ; then
			warn "'$FILE' hasn't been recently updated"
		fi
	done <<<"$(get_tex_files | grep -e "$OUTPUTEXT\$")"
}

######################################################################
# ARG:check: Check for gross errors and missing things
check_all() {
	check_output "$@"
	check_pointout "$@"
	check_solutions "$@"
	#check_symlinks "$@" # check_solutions -> tar_files -> check_symlinks
	check_whitespace "$@"
}

######################################################################
# ARG:spellcheck:
check_spelling() {
	local SPELL="$COURSES/common/spellbehan.sh"
	if [[ -f $SPELL ]] ; then
		verbose "$SPELL" --color --dont-backup --quiet
	else
		verbose spellcheck.sh | grep -v aspell
	fi
}

######################################################################
# Used by check_file_links and check_kernel_links
check_links() {
	local TAG=${1:?} MSG=${2:?} FILE LINK DIR
	shift; shift
	ALL=yes
	for FILE in $(get_tex_files) ; do
		[[ -n $QUIET ]] || info "check_kernel_links: $FILE"
		set +e
		grep -rI "\\$TAG{" "$FILE" \
			| sed -e "s/.*\\\\$TAG{//g;" \
			-e 's/\\_/_/g;' \
			-e 's/}.*//;' \
			-e "s|\$SOLUTIONS|$TOPDIR/SOLUTIONS|;" \
			| expand_tex \
			| grep -v \
			-e '^bzImage' \
			-e '^config-' \
			-e '^/dev/' \
			-e 'DO_KERNEL.sh' \
			-e '^/etc/' \
			-e '^extra$' \
			-e '/foo' \
			-e '^grub.cfg' \
			-e '/hello' \
			-e '^/home' \
			-e '^LF' \
			-e '^Linux' \
			-e '^/media/' \
			-e '^modules\.' \
			-e 'nomake.sh' \
			-e '^/proc/' \
			-e '^/run/' \
			-e '^/sys/' \
			-e 'thesessionref' \
			-e '^/trusted' \
			-e '^uEnv.txt' \
			-e '^/usr/local/' \
			-e '^/usr/src/linux' \
			-e '^/var/log/' \
			| sort -u \
			| while read -r LINK ; do
			local FOUND="" STR
			STR=$(eval echo "$LINK" | cut -d' ' -f1)
			[[ $STR =~ HOME ]] && info "STR: $(eval echo "$STR")"
			for DIR in "" "$@" "$HOME" ; do
				#info "Try $STR, $DIR/$STR"
				if [[ -e $STR || -e "$DIR/$STR" \
					|| -e "$DIR/include/$STR" ]] ; then
					FOUND=y
					break
				fi
			done
			if [[ -n $FOUND ]] ; then
				[[ -z $MOREOUTPUT ]] || info "$MSG exists: $LINK"
			else
				fail "$MSG missing in $FILE: $LINK$([[ $LINK = "$STR" ]] || echo " ($STR)")"
			fi
		done
		set -e
	done
}

######################################################################
# ARG:check-file-links:
check_file_links() {
	check_links "filelink" "File" \
		"$WORKDIR/buildroot-$BRVERSION-glibc" \
		"$WORKDIR/busybox-$BBVERSION" \
		"$WORKDIR/linux-$KVERSION" \
		"$WORKDIR/u-boot-$UBVERSION" \
		"$WORKDIR/opensbi-$SBIVERSION"
}

######################################################################
# ARG:check-kernel-links:
check_kernel_links() {
	if [[ -d "$WORKDIR/linux-$KVERSION" ]] ; then
		check_links "kernlink" "Kernel file" "$WORKDIR/linux-$KVERSION"
	elif [[ -n $KERNELSRC && -d $KERNELSRC ]] ; then
		check_links "kernlink" "Kernel file" "$KERNELSRC"
	else
		error "No KERNELSRC specified or found"
	fi
}

######################################################################
# ARG:check-symlinks:
# used-by: check_all, tar_files
check_symlinks() {
	local CHAPTER
	info "Checking for dangling symlinks in $COURSE"
	cd "$TOPDIR"
	for CHAPTER in $(get_chapters) ; do
		[[ ! $CHAPTER =~ LFD_CLOSING ]] || continue
		debug "Check ${CHAPTER/CHAPS/MODULES}"
		(cd "$COURSES" && verbose symlinks -r "${CHAPTER/CHAPS/MODULES}")
	done
}

######################################################################
# ARG:check-urls:
check_urls() {
	cd "$TOPDIR"
	export QUIET=y
	export PROGRESS=y
	verbose "$COMMON/checkurls.sh" "$COURSE"
	echo
}

######################################################################
# ARG:whitespace: Fix whitespace issues
# used-by: check_all
check_whitespace() {
	local FILES

	info "Check for whitespace errors"
	FILES="$(ALL=""
		local FILE
		for FILE in $(get_tex_files) ; do
			if grep -q -e '	' -lre ' +$' "$FILE" ; then
				#info "check_whitespace: $FILE"
				if [[ -n $FIX ]] ; then
					verbose sed -i -re 's/	/        /g' -re 's/ +$//' "$FILE";
				else
					echo "$FILE"
				fi
			fi
		done
	)"

	if [[ -n $EDIT ]] ; then
		# shellcheck disable=SC2086
		VICMD="silent! /	" edit $FILES
	fi
}

######################################################################
# ARG:links: Create course links
course_links() {
	local COURSE CLEAN
	COURSE=${1:?}
	CLEAN=${2:-}

	[[ $CLEAN != clean ]] || exit

	local IMAGES="IMAGES"
	# shellcheck disable=SC2046
	[[ -e $IMAGES ]] || verbose ln $VERBOSE --symbolic --force \
		$(abs2rel "$COURSES/$IMAGES" "$IMAGES") "$IMAGES"
	local MAKEFILE="Makefile"
	# shellcheck disable=SC2046
	[[ -e $MAKEFILE ]] || verbose ln $VERBOSE --symbolic --force \
		$(abs2rel "$COURSES/common/Makefile_oneclass" "$MAKEFILE") "$MAKEFILE"
}

######################################################################
# BUILD ARG:crosstool: Unpack and build crosstool-ng then save config
build_crosstool() {
	local CTVERSION=${1:-$CTVERSION}
	local LABEL=${2:?}
	local CT_CONFIG=${3:?}
	debug "crosstool: CTVERSION=$CTVERSION LABEL=$LABEL CT_CONFIG=${CT_CONFIG:+...}"

	enter_workdir .

	##############################################################
	[[ -n $CTVERSION ]] || error "No crosstool-ng version specified"

	##############################################################
	local NAME="crosstool-ng-$CTVERSION"
	local CROSSTOOL="$NAME.tar.xz"
	local BASE
	BASE="$(pwd)/$NAME${LABEL:+-$LABEL}"
	local DIR="$BASE/$NAME"
	local HDIR="$HOME/crosstool-ng"
	local DL="tarballs"
	local DLTAR="$NAME-tarfiles.tar"
	local XTOOLS="$BASE/xtools"
	local CTNG="$XTOOLS/bin/ct-ng"
	local DOTCONFIG="$BASE/.config"
	local CONFIG="$NAME${LABEL:+-$LABEL}-config"
	local LOG="$DIR.log"

	##############################################################
	announce "Building Crosstool-ng $CTVERSION for $LABEL: $DIR"

	##############################################################
	fetch_file "./$CROSSTOOL" "$CTNGURL"
	if [[ -n $FETCH_ONLY ]] ; then
		leave_workdir
		return 0
	fi

	##############################################################
	if ! unpack "$CROSSTOOL" ; then
		verbose mkdir -p "$DL"
		verbose ln --symbolic --force "../$DL" "$DIR/dl"
	fi

	##############################################################
	if [[ -e $DIR/config.log ]] ; then
		vwarn "  Already configure: $DIR"
	else
		info "  Configuring $CTNG"
		mkdir -p "$DIR"
		pushd "$DIR" >/dev/null
		touch "$LOG"
		verbose ./configure "--prefix=$XTOOLS" | pv >> "$LOG"
		popd >/dev/null
	fi

	##############################################################
	if [[ -z $FORCE && -e $CTNG ]] ; then
		vwarn "  $(basename "$CTNG") already built"
	else
		info "  Compiling $CTNG"
		compile_code "$DIR" make 'clean all install' 'MAKELEVEL=1' "$LOG"
	fi

	##############################################################
	if [[ -e $DOTCONFIG ]] ; then
		vwarn "  Already configured: $DIR"
	else
		# shellcheck disable=SC2016
		CT_CONFIG+='
CT_LOCAL_TARBALLS_DIR="${HOME}/crosstool-ng/tarballs"
CT_SAVE_TARBALLS=y
CT_PREFIX_DIR="${HOME}/crosstool-ng/xtools"
CT_ARCH_arm=y
CT_ARCH_ARM_EABI=y
CT_ARCH_ARM_EABI_FORCE=y
CT_ARCH_FLOAT=hard
CT_ARCH_FLOAT_HW=y
CT_GEN_CHOICE_KERNEL=y
CT_KERNEL_linux_AVAILABLE=y
CT_KERNEL_linux=y
CT_BINUTILS_FOR_TARGET=y
CT_CC_GCC_5=y
CT_CC_GCC_5_or_later=y
CT_DEBUG_duma=y
CT_DEBUG_gdb=y
CT_DEBUG_ltrace=y
CT_DEBUG_strace=y
'

		touch "$BASE/.config"
		pushd "$BASE" >/dev/null
		config_build "$BASE" "$CTNG" "clean" '' "$CT_CONFIG" "$LOG" || true
		rm -f "${IMAGE:?}"
		popd >/dev/null

		if [[ -L $HDIR ]] ; then
			rm -f "${HDIR:?}"
		fi
		ln -s "$BASE" "$HDIR"
	fi

	##############################################################
	if [[ -z $FORCE && -e $IMAGE ]] ; then
		vwarn "  Already built: $DIR"
	else
		compile_code "$BASE" "$CTNG" "source clean build" '' "$LOG"
		verbose rm -f "${CONFIG:?}" "${DLTAR:?}"
	fi

	##############################################################
	#info "  Saving build results"
	#add_to_manifest "$CROSSTOOL"
	save_config "$DOTCONFIG" "$CONFIG"
	#info "  Saving crosstool-ng Downloads"
	tar_build_artifacts "$DLTAR" "$DL"

	##############################################################
	list_files "$CROSSTOOL" "$CONFIG" "$DLTAR"

	leave_workdir
}

######################################################################
# ARG:prefinalize:
# used-by: make_pdf, pre_finalize_course
find_overfull() {
	##############################################################
	pushd "$TOPDIR" >/dev/null

	##############################################################
	# Find lines in PDF which are too long
	info "Find overfull"
	local OVERFULL="$COURSE-all"
	[[ -e $OVERFULL ]] || run_make findoverfull >"$OVERFULL" 2>&1
	local LINE SIZE
	while IFS='' read -r LINE ; do
		SIZE="$(sed -e 's/^.*(//; s/\..*$//;' <<<"$LINE")"
		if [[ $SIZE -gt 8 ]] ; then
			echo "$LINE"
		fi
	done <<<"$(grep -e Overfull "$OVERFULL")"

	popd >/dev/null
}

######################################################################
# Upload RESOURCES and tarballs
# ARG:upload:
# used-by: tar_files
upload_files() {
	pushd "$TOPDIR" >/dev/null

	info "Uploading RESOURCES"
	run_make -s resources-upload

	info "Uploading tarballs"
	run_make -s SOLUTIONS-upload-staging RESOURCES-upload-staging

	popd >/dev/null
}

######################################################################
# Build tarfiles
# ARG:tarball:
# used-by: check_solutions
tar_files() {
	local UPLOAD=${1:-}

	check_symlinks

	pushd "$TOPDIR" >/dev/null

	info "Make tarballs..."
	rm -f SOLUTIONS_ARE_DONE
	run_make -s SOLUTIONS >/dev/null
	[[ -f SOLUTIONS_ARE_DONE ]] || error "Building the tar files caused an error"
	du -sh SOLUTIONS RESOURCES

	popd >/dev/null

	[[ -z $UPLOAD ]] || upload_files
}

######################################################################
# ARG:prefinalize:
# used-by: finalize_course, make_all
pre_finalize_course() {
	##############################################################
	pushd "$TOPDIR" >/dev/null

	##############################################################
	#info "Cleaning up..."
	###run_make -s -j "$(nproc)" veryclean

	##############################################################
	info "Spell checking"
	#verbose spellcheck.sh --quiet
	check_spelling

	##############################################################
	info "Check File links"
	QUIET=1 check_file_links

	##############################################################
	info "Check Kernel links"
	QUIET=1 check_kernel_links

	##############################################################
	info "Check URLs"
	QUIET=1 check_urls

	##############################################################
	# Find lines in PDF which are too long
	find_overfull

	##############################################################
	info "Build all PDFs..."
	run_make -j "$(nproc)" all | pv >/dev/null

	##############################################################
	# Build SOLUTIONS/RESOURCES
	tar_files

	popd >/dev/null
}

######################################################################
# ARG:finalize:
finalize_course() {
	pre_finalize_course "$@"

	##############################################################
	pushd "$TOPDIR" >/dev/null

	##############################################################
	info "Build Everything..."
	run_make -s release-full

	#tar_files upload

	popd >/dev/null
}

######################################################################
# BUILD: ARG:ftrace: Build ftrace from source
build_ftrace() {
	local FTVER=${1:?}
	debug "build_ftrace: FTVER=$FTVER"

	##############################################################
	local DIR="trace-cmd-$FTVER"
	local EXE="$DIR/tracecmd/trace-cmd"
	local FTFILE="$BOARD-$DIR.xz"
	local LOG="$FTFILE.log"
	local TAG="trace-cmd-v$FTVER"
	local TARFILE="$DIR.tar.xz"

	enter_workdir .

	##############################################################
	announce "Building ftrace for $BOARD: $DIR"

	##############################################################
	# See if we're already done
	if checkstamp "$FTFILE" ; then
		leave_workdir
		return 0
	fi

	##############################################################
	fetch_git "$DIR" "$FTURL" "$TAG" || true
	if [[ -n $FETCH_ONLY ]] ; then
		leave_workdir
		return 0
	fi

	##############################################################
	if ! checkstamp "$TARFILE" ; then
		info "  Building ftrace tarball"
		(cd "$DIR" && verbose make -s dist)
		local FILE NEW
		for FILE in "$DIR"*.tar* ; do
			NEW="${FILE/../.}"
			if [[ $FILE != "$NEW" ]] ; then
				rm -f "${NEW:?}"
				mv $VERBOSE "$FILE" "$NEW"
			fi
		done
		rm -f "${DIR:?}.tar" "$DIR.tar.bz2"
		stampfile "$TARFILE"
	fi

	##############################################################
	if [[ -n $FORCE || ! -e $EXE ]] ; then
		info "  Building ftrace"
		compile_code "$DIR" "make" "all_cmd" "CC=$GCCTRIPLET-gcc -j$(nproc) LDFLAGS=-static NO_PYTHON=1" "$LOG"
	fi

	##############################################################
	add_to_manifest "$TARFILE"
	save_build_artifacts "$EXE" "$FTFILE"

	##############################################################
	list_files "$TARFILE" "$FTFILE"
	stampfile "$FTFILE"

	leave_workdir
}

######################################################################
# BUILD: ARG:mtd:
build_mtd() {
	local MTDVER=${1:?}
	debug "build_mtd: MTDVER=$MTDVER"

	[[ -n $MTDVER ]] || error "build_mtd: No version for mtd-utils specified"

	##############################################################
	local DIR="mtd-utils-$MTDVER"
	#local MTDUTILS="$RESOURCES/$COURSE/$DIR.tar.bz2"
	local MTDUTILS="$DIR.tar.bz2"
	local EXE="$DIR/mkfs.jffs2"
	local MKFS="mkfs.jffs2-$MTDVER.xz"
	local LOG="$DIR.log"

	enter_workdir .

	##############################################################
	announce "Building mtd-utils for $BOARD: $DIR"

	##############################################################
	# See if we're already done
	if checkstamp "$MKFS" ; then
		leave_workdir
		return 0
	fi

	##############################################################
	fetch_file "./$MTDUTILS" "$MTDURL"
	if [[ -n $FETCH_ONLY ]] ; then
		leave_workdir
		return 0
	fi

	touch "$LOG"

	##############################################################
	unpack "$MTDUTILS" "$DIR" || true

	##############################################################
	if [[ -e $DIR/Makefile ]] ; then
		vwarn "  Already configured: $DIR"
	else
		info "  Configuring $DIR"
		pushd "$DIR" >/dev/null
		export LDFLAGS="-static"
		verbose ./configure --without-ubifs --without-xattr --enable-static >>"$LOG"
		export LDFLAGS=
		popd >/dev/null
	fi
	[[ -f $DIR/Makefile ]] || error "  build_mtd: configure failed"

	##############################################################
	if [[ -n $FORCE || ! -e $EXE ]] ; then
		info "  Building mtd-utils"
		compile_code "$DIR" "make" "all" "-j$(nproc) V=1" "$LOG"

		rm -f "${EXE:?}"
		make -C"$DIR" LDFLAGS=-all-static V=1 "${EXE#*/}" >>"$LOG"
	fi

	##############################################################
	file "$EXE" | grep -q "statically linked" || error "  build_mtd: $EXE not statically linked after build"
	#add_to_manifest "$MTDUTILS"
	save_build_artifacts "$EXE" "$MKFS"

	##############################################################
	list_files "$MTDUTILS" "$MKFS"
	stampfile "$MKFS"

	leave_workdir
}

######################################################################
# BUILD: ARG:qemu:
build_qemu() {
	local QEMUVER=${1:-$QEMUVERSION}
	debug "build_mtd: QEMUVER=$QEMUVER"

	[[ -n $QEMUVER ]] || error "build_qemu: No version for QEMU specified"

	##############################################################
	local DIR="qemu-$QEMUVER"
	local QEMUTAR="$DIR.tar.xz"
	local QEMU="qemu-system-$ARCHBITS"
	local EXE="$DIR/build/$QEMU"
	local LOG="$DIR.log"
	QEMU+="-$QEMUVER.xz"

	enter_workdir .

	##############################################################
	announce "Building QEMU for $BOARD: $DIR"

	##############################################################
	# See if we're already done
	if checkstamp "$QEMU" ; then
		leave_workdir
		return 0
	fi

	##############################################################
	fetch_file "./$QEMUTAR" "$QEMUURL"
	if [[ -n $FETCH_ONLY ]] ; then
		leave_workdir
		return 0
	fi

	touch "$LOG"

	##############################################################
	unpack "$QEMUTAR" "$DIR" || true

	##############################################################
	if [[ -z $FORCE ]] && [[ -e $DIR/build/config.log || -e $DIR/build/.ninja_deps ]] ; then
		vwarn "  Already configured: $DIR"
	else
		info "  Configuring $DIR"
		pushd "$DIR" >/dev/null
		verbose ./configure --target-list="$QEMUTARGET" >>"../$LOG"
		popd >/dev/null
	fi
	[[ -e $DIR/build/config.log || -e $DIR/build/.ninja_deps ]] \
		|| error "  build_qemu: configure failed"

	##############################################################
	if [[ -n $FORCE || ! -e $EXE ]] ; then
		info "  Building qemu"
		compile_code "$DIR" "make" "all" "-j$(nproc) V=1" "$LOG"
	fi

	##############################################################
	#add_to_manifest "$QEMUTAR"
	save_build_artifacts "$EXE" "$QEMU"

	##############################################################
	list_files "$QEMUTAR" "$QEMU"
	stampfile "$QEMU"

	leave_workdir
}

######################################################################
# BUILD: Get Board flash image
get_board_image() {
	local BI=${1:?}
	debug "get_board_image: BI=$BI"

	[[ -n $BI ]] || error "get_board_image: No board image specified"

	##############################################################
	local BIPATH="" BIFILE="${BI##*/}.xz"
	if [[ $BI =~ / ]] ; then
		BIPATH="/$(dirname "$BI")"
	fi

	enter_workdir .

	##############################################################
	announce "Get board image for $BOARD: $BIFILE"

	##############################################################
	# See if we're already done
	if checkstamp "$BIFILE" ; then
		leave_workdir
		return 0
	fi

	##############################################################
	fetch_file "./$BIFILE" "$BIURL$BIPATH"
	[[ -z $FETCH_ONLY ]] || return 0

	##############################################################
	#add_to_manifest "$BIFILE"
	list_files "$BIFILE"

	leave_workdir
}

######################################################################
# BUILD: bmap flash image
bmap_flash_image() {
	local FI=${1:?} EXT=${2:-xz} BMAP=${3:-bmap}
	debug "bmap_flash_image: FI=$FI EXT=$EXT BMAP=$BMAP"

	[[ -n $FI ]] || error "bmap_flash_image: No flash image specified"

	verbose rm -f "${FI:?}"
	[[ -z $FETCH_ONLY ]] || return 0

	##############################################################
	local FIFILE="$FI.$EXT"
	local BMFILE="$FI.$BMAP"

	enter_workdir .

	##############################################################
	announce "Creating bmap file $BMFILE"

	##############################################################
	# See if we're already done
	if checkstamp "$BMFILE" ; then
		leave_workdir
		return 0
	fi

	##############################################################
	info "  Decompressing $FIFILE"
	case "$FIFILE" in
		*.gz) verbose zcat "$FIFILE" > "$FI" ;;
		*.bz2) verbose bzcat "$FIFILE" > "$FI" ;;
		*.xz) verbose xzcat "$FIFILE" > "$FI" ;;
		*) FI="$FIFILE" ;;
	esac
	verbose bmaptool create -o "$BMFILE" "$FI"
	if [[ $FI != "$FIFILE" ]] ; then
		warn "  Removing $FI since no longer needed"
		rm -f "${FI:?}"
	fi

	##############################################################
	add_to_manifest "$BMFILE"
	list_files "$BMFILE"
	stampfile "$BMFILE"

	leave_workdir
}

######################################################################
# BUILD: Get bmaptool
get_bmaptool() {
	local VER=${1:?} FILE=${2:?}
	local URL="$BMAPTOOLURL/v$VER"
	debug "get_bmaptool: VER=$VER FILE=$FILE URL=$URL"

	[[ -n $VER && -n $FILE ]] || error "get_bmaptool: No bmaptool specified"

	##############################################################
	enter_workdir .

	##############################################################
	announce "Get BMAPtool $VER"

	##############################################################
	# See if we're already done
	if checkstamp "$FILE" ; then
		leave_workdir
		return 0
	fi

	##############################################################
	fetch_file "./$FILE" "$URL"
	[[ -z $FETCH_ONLY ]] || return 0

	##############################################################
	#add_to_manifest "$FILE"
	list_files "$FILE"

	leave_workdir
}

######################################################################
# BUILD: Get etcher.io
get_etcher() {
	local VER=${1:?} ZIP=${2:?}
	local URL="$ETCHERDLURL/v$VER"
	debug "get_etcher: VER=$VER ZIP=$ZIP URL=$URL"

	[[ -n $VER && -n $ZIP ]] || error "get_etcher: No etcher specified"

	# https://github.com/resin-io/etcher/releases/download/v1.4.5/etcher-electron-1.4.5-linux-x64.zip
	# https://github.com/balena-io/etcher/releases/download/v1.4.7/balena-etcher-electron-1.4.7-linux-x64.zip

	##############################################################
	enter_workdir .

	##############################################################
	announce "Get Etcher $VER"

	##############################################################
	# See if we're already done
	if checkstamp "$ZIP" ; then
		leave_workdir
		return 0
	fi

	##############################################################
	fetch_file "./$ZIP" "$URL"
	[[ -z $FETCH_ONLY ]] || return 0

	##############################################################
	#add_to_manifest "$ZIP"
	list_files "$ZIP"

	leave_workdir
}

######################################################################
# BUILD: Get and unpack linaro gcc
get_linarogcc() {
	local LGCC=${1:?}
	debug "get_linarogcc: LGCC=$LGCC"

	##############################################################
	#local LGCCFILE="$RESOURCES/$COURSE/$LGCC.tar.xz"
	local LGCCFILE="$LGCC.tar.xz"
	local DIR="$WORKDIR/$LGCC"
	export PATH="$DIR/bin:$PATH"

	##############################################################
	announce "Setting up Linaro gcc for $ARCH: $DIR"

	##############################################################
	# See if we're already done
	if [[ -d $DIR ]] ; then
		return 0
	fi

	enter_workdir .

	##############################################################
	fetch_file "./$LGCCFILE" "$LGCCURL"
	[[ -z $FETCH_ONLY ]] || return 0

	##############################################################
	unpack "$LGCCFILE" "$DIR" || true

	##############################################################
	#add_to_manifest "$LGCCFILE"
	list_files "$LGCCFILE"

	leave_workdir
}

######################################################################
# BUILD: ARG:kernel: Unpack and build kernel then save config
build_kernel() {
	local KVERSION=${1:-$KVERSION}
	local NAME=${2:-}
	local KERNEL_CONFIG=${3:?}
	local BUILD_TARGETS=${4:-uImage}
	local BUILD_OPTS=${5:-}
	debug "build_kernel: KVERSION=$KVERSION NAME=$NAME KERNEL_CONFIG=${KERNEL_CONFIG:+...}"

	##############################################################
	[[ -n $KVERSION ]] || error "build_kernel: No kernel version specified"

	##############################################################
	check_board
	#local KERNEL="$RESOURCES/$COURSE/linux-$KVERSION.tar.xz"
	local KERNEL="linux-$KVERSION.tar.xz"
	local KNAME="$KVERSION${NAME:+-$NAME}"
	local DIR="linux-$KNAME"
	local VMLINUX="$DIR/vmlinux"
	local DKERNEL="$BOARD-vmlinux-$KNAME.xz"
	local HDIR="$HOME/$DIR"
	local DOTCONFIG="$DIR/.config"
	local PFILE="$BOARD-perf-$KNAME"

	enter_workdir .

	##############################################################
	announce "Building Kernel for $BOARD: $DIR"

	##############################################################
	# See if we're already done
	if checkstamp "$DKERNEL" ; then
		leave_workdir
		return 0
	fi

	##############################################################
	fetch_file "./$KERNEL" "$KERNELURL"
	if [[ -n $FETCH_ONLY ]] ; then
		leave_workdir
		return 0
	fi

	##############################################################
	if [[ -n $FORCE || ! -e "$VMLINUX" ]] ; then

		if ! unpack "$KERNEL" "$DIR" ; then
			symlink "$(pwd)/$DIR" "$HDIR"
		fi

		##############################################################
		check_toolchain
		if config_build "$DIR" "$MYMAKE" distclean "$KDEFCONFIG" "$KERNEL_CONFIG" ; then
			rm -f "${VMLINUX:?}"
		fi

		##############################################################
		info "Building linux"
		compile_code "$DIR" "$MYMAKE" "clean $BUILD_TARGETS dtbs modules" "$BUILD_OPTS"
		touch "$NEWKERNEL"
	fi

	##############################################################
	tar_kernel "$DIR"
	save_build_artifacts "$VMLINUX" "$DKERNEL"
	#add_to_manifest "$KERNEL"

	##############################################################
	list_files "$DKERNEL" "$KERNEL"
	stampfile "$DKERNEL"

	leave_workdir
}

######################################################################
# BUILD: ARG:tar-kernel: Tar up built kernel for other labs
# used-by: build_kernel
tar_kernel() {
	local KDIR=${1:?}
	debug "tar_kernel: KDIR=$KDIR"

	##############################################################
	check_board
	local BOOT="${KDIR:+$KDIR/}arch/$ARCH/boot"
	local ZIMAGE="zImage"
	local KERNEL="$BOOT/$ZIMAGE"
	local KNAME="$ZIMAGE-$BOARDSHORT"
	debug "  tar_kernel: BOOT=$BOOT ZIMAGE=$ZIMAGE KERNEL=$KERNEL KNAME=$KNAME"

	##############################################################
	[[ -e $KERNEL || -e ${KERNEL/z/u} || -e ${KERNEL/z/} ]] || error "No $KERNEL found"

	##############################################################
	local VERSION DIR
	# shellcheck disable=SC2086
	check_toolchain
	VERSION=$($MYMAKE -s ${KDIR:+-C $KDIR} kernelversion | sed -re "s/\.0//")

	##############################################################
	local DOTCONFIG="${KDIR:+$KDIR/}.config"
	local CONFIG="$BOARD-kernel-$VERSION-config"
	local KFILE="$BOARD-kernel-$VERSION.tar.xz"
	local MFILE="$BOARD-modules-$VERSION.tar.xz"
	debug "  tar_kernel: VERSION=$VERSION DOTCONFIG=$DOTCONFIG CONFIG=$CONFIG KFILE=$KFILE MFILE=$MFILE"

	enter_workdir .

	##############################################################
	announce "Tar kernel stuff"

	##############################################################
	# See if we're already done
	if checkstamp "$MFILE" ; then
		leave_workdir
		return 0
	fi

	##############################################################
	if [[ -e $NEWKERNEL ]] ; then
		rm -f "${NEWKERNEL:?}" "${CONFIG:?}" "${KFILE:?}" "${MFILE:?}"
	fi

	##############################################################
	clean_tmp
	if checkstamp "$KFILE" ; then
		vwarn "  Already tarred up kernel: $KFILE"
	else
		save_config "$DOTCONFIG" "$CONFIG"

		# Tar up kernel
		info "  Install kernel files"
		local TDIR
		TDIR="$(mk_tmpdir)"
		(cd "$TDIR"
			copy_link "../$KERNEL" "$KNAME" missing
			copy_link "../${KERNEL/zImage/uImage}" "${KNAME/zImage/uImage}" missing
			copy_link "../${KERNEL/zImage/Image}" "${KNAME/zImage/Image}" missing
			# shellcheck disable=SC2086
			[[ -z $DTBS ]] || copy_link ../$BOOT/dts/$DTBS .
			# shellcheck disable=SC2035
			tar_build_artifacts "../$KFILE" *
		)
		rm -rf "${TDIR:?}"
		stampfile "$KFILE"
		add_to_manifest "$CONFIG" "$KFILE"
	fi

	##############################################################
	# Tar up modules
	if checkstamp "$MFILE" ; then
		vwarn "  Already tarred up modules: $MFILE"
	else
		info "  Install modules"
		local TDIR
		TDIR="$(mk_tmpdir)"
		# shellcheck disable=SC2086
		verbose $MYMAKE -s ${KDIR:+-C $KDIR} INSTALL_MOD_PATH="$TDIR" modules_install
		# shellcheck disable=SC2035
		(cd "$TDIR" && tar_build_artifacts "../$MFILE" *)
		rm -rf "${TDIR:?}"
		stampfile "$MFILE"
		add_to_manifest "$MFILE"
	fi

	##############################################################
	list_files "$CONFIG" "$KFILE" "$MFILE"

	leave_workdir
}

######################################################################
# Build perf
build_perf() {
	local KVERSION=${1:-$KVERSION}
	local NAME=${2:-}
	local KNAME="$KVERSION${NAME:+-$NAME}"
	local DIR="linux-$KNAME/tools/perf"
	debug "build_perf: KVER=$KVERSION NAME=$NAME KNAME=$KNAME DIR=$DIR"

	##############################################################
	local EXE="$DIR/perf"
	local PFILE="$BOARD-perf-$KVERSION.xz"
	local LOG="$PFILE.log"

	enter_workdir .

	##############################################################
	announce "Building perf-$KVERSION for $BOARD: $DIR"

	##############################################################
	# See if we're already done
	if checkstamp "$PFILE" ; then
		leave_workdir
		return 0
	fi

	##############################################################
	if [[ -n $FORCE || ! -f $EXE ]] ; then
		info "Building perf"
		check_toolchain
		#(cd $DIR && verbose make ARCH="$ARCH" CROSS_COMPILE="$GCCTRIPLET-" LDFLAGS=-static)
		#compile_code "$DIR" "$MYMAKE" "all" "-j$(nproc) LDFLAGS=-static" "$LOG"
		compile_code "$DIR" "$MYMAKE" "clean"
		compile_code "$DIR" "$MYMAKE" "all" "$LOG"
	else
		vwarn "  Already built $PFILE"
	fi

	##############################################################
	save_build_artifacts "$EXE" "$PFILE"

	##############################################################
	list_files "$PFILE"
	stampfile "$PFILE"

	leave_workdir
}

######################################################################
# BUILD: ARG:mkroot-tar: Make rootfs for prebuilt kernel
mkroot_tar() {
	local KVERSION=${1:-$KVERSION}
	local BRVERSION=${2:-$BRVERSION}
	local NAME=${3:?}
	debug "mkroot_tar: KVERSION=$KVERSION BRVERSION=$BRVERSION NAME=$NAME"
	[[ -z $FETCH_ONLY ]] || return 0

	##############################################################
	check_board
	local KERNEL="$BOARD-kernel-$KVERSION.tar.xz"
	local MODULES="$BOARD-modules-$KVERSION.tar.xz"
	local ROOTFS="$BOARD-rootfs-$BRVERSION.tar.xz"

	enter_workdir .

	##############################################################
	rootfs_link "$BRVERSION" "$NAME"

	##############################################################
	exists_or_error "$KERNEL" "mkroot_tar Not found: $KERNEL"
	exists_or_error "$MODULES" "mkroot_tar Not found: $MODULES"
	exists_or_error "$ROOTFS" "mkroot_tar Not found: $ROOTFS"

	##############################################################
	local FILE
	FILE="$BOARD-${PART:+$PART-}$BRVERSION-$KVERSION.tar.xz"

	##############################################################
	announce "Making rootfs: $FILE"

	##############################################################
	# See if we're already done
	if checkstamp "$FILE" ; then
		leave_workdir
		return 0
	fi

	clean_tmp
	if checkstamp "$FILE" ; then
		vwarn "  Already created rootfs tarfile: $FILE"
	else
		info "  Assembling rootfs"
		local TDIR
		TDIR="$(mk_tmpdir)"
		mkdir -p "$TDIR/boot"
		decompress "$ROOTFS" "${FILE%.xz}"
		untar "$KERNEL" "$TDIR/boot"
		untar "$MODULES" "$TDIR"

		# shellcheck disable=SC2035
		(cd "$TDIR" && tar_build_artifacts -r "../${FILE%.xz}" *)
		compress "${FILE%.xz}"
		stampfile "$FILE"
		rm -rf "${TDIR:?}"
	fi

	##############################################################
	add_to_manifest "$FILE"
	list_files "$FILE"

	leave_workdir
}

######################################################################
# Move files
# used-by: mvresources
mvfiles() {
	local DIR=${1:?} FILE DEST RET=0; shift
	debug "mvfiles: DIR=$DIR $*"
	for FILE in "$@" ; do
		DEST="$DIR/${FILE##*/}"
		if [[ -L $FILE ]] ; then
			if [[ -e $DEST ]] ; then
				verbose rm -f $VERBOSE "${DEST:?}"
			fi
			verbose mv -n $VERBOSE "$FILE" "$DEST"
		elif [[ ! -e $FILE ]] ; then
			if [[ ! -f $DEST && ! -f "$DEST/$FILE" ]] ; then
				[[ -z $VERBOSE ]] || warn "File not found: $FILE"
			fi
		elif [[ -e $DEST ]] ; then
			RET=1
			if diff -q "$FILE" "$DEST" ; then
				warn "mvfiles: File already exists and is identical: $DEST"
				[[ -z ${DELETE:-} ]] || verbose rm -f $VERBOSE "${FILE:?}"
			elif [[ -n ${FORCE:-} ]] ; then
				warn "mvfiles: File already exists but will overwrite: $DEST"
				verbose mv -u $VERBOSE "$FILE" "$DEST"
			elif [[ -n $VERBOSE ]] ; then 
				warn "mvfiles: File already exists: $FILE != $DEST"
			fi
		else
			verbose mv -n $VERBOSE "$FILE" "$DEST"
		fi
	done

	return "$RET"
}

######################################################################
# ARG:resources: Move files to RESOURCES
mvresources() {
	local DIR="$RESOURCES/$COURSE"

	enter_workdir .

	######################################################################
	[[ -d $DIR ]] || DIR="$RESOURCES/LFD$COURSE"
	[[ -d $DIR ]] || DIR="$RESOURCES/LFS$COURSE"
	[[ -d $DIR ]] || error "mvresources: No resources directory found for $COURSE"

	######################################################################
	debug "mvresources: DIR=$DIR"
	if [[ $# -eq 0 ]] ; then
		ls --color=auto "$DIR"
		warn "If you wanted to move files to RESOURCES, do a: $TOOL mv manifest"
		info "The following files will be moved to RESOURCES if asked"
		cat "$MANIFEST"
	else
		if [[ ${1:?} = manifest ]] ; then
			#verbose mv -i $VERBOSE $(cat "$MANIFEST") "$DIR"
			if [[ -f "$MANIFEST" ]] ; then
				# shellcheck disable=SC2046
				mvfiles "$DIR" $(cat "$MANIFEST") \
				&& verbose savelog "$MANIFEST"
			else
				error "No $MANIFEST found"
			fi
		else
			#verbose mv -i $VERBOSE "$@" "$DIR"
			mvfiles "$DIR" "$@"
		fi
	fi

	leave_workdir
}

######################################################################
# BUILD: ARG:rootfs: Create rootfs symlink
# used-by: mkroot_tar
rootfs_link() {
	local BRVERSION=${1:-$BRVERSION}
	local NAME=${2:?}
	debug "rootfs_link: BRVERSION=$BRVERSION NAME=$NAME"
	[[ -z $FETCH_ONLY ]] || return 0

	######################################################################
	check_board
	local ROOTFS="$BOARD-buildroot-$BRVERSION${NAME:+-$NAME}.tar.xz"
	local LINK="$BOARD-rootfs-$BRVERSION.tar.xz"

	enter_workdir .

	######################################################################
	announce "Linking tarfile: $LINK"

	######################################################################
	# See if we're already done
	if checkstamp "$LINK" ; then
		leave_workdir
		return 0
	fi

	######################################################################
	exists_or_error "$ROOTFS" "rootfs_link Not found: $ROOTFS"
	if [[ ! -e $LINK ]] ; then
		verbose ln --symbolic "$ROOTFS" "$LINK"
	fi
	add_to_manifest "$LINK"
	stampfile "$LINK"

	##############################################################
	list_files "$LINK"
	leave_workdir
}

######################################################################
# BUILD: ARG:squashfs: Create squashfs root filesystem
squashfs_root() {
	local BRVERSION=${1:-$BRVERSION}
	local NAME=${2:?}
	local LOG=${3:-}
	debug "squashfs_root: BRVERSION=$BRVERSION NAME=$NAME"
	[[ -z $FETCH_ONLY ]] || return 0

	##############################################################
	check_board
	local DIR="buildroot-$BRVERSION${NAME:+-$NAME}"
	local IMAGE="$DIR/output/images/rootfs.tar.xz"
	local FILE="$BOARD-rootfs-$BRVERSION${NAME:+-$NAME}.squashfs"
	local XZFILE="$FILE.xz"
	LOG=$(realpath "${LOG:-$DIR.log}")
	debug "  squashfs_root: FILE=$FILE"

	enter_workdir .

	##############################################################
	announce "Squashing rootfs for $BRVERSION: $DIR"

	##############################################################
	# See if we're already done
	if checkstamp "$XZFILE" ; then
		leave_workdir
		return 0
	fi

	##############################################################
	local TOPDIR
	TOPDIR="$(pwd)"

	##############################################################
	clean_tmp
	local TDIR
	exists_or_error "$DIR" "squashfs_root Not found: $DIR"
	info "Unpacking $IMAGE"
	TDIR="$(mk_tmpdir)"
	untar "$TOPDIR/$IMAGE" "$TDIR" sudo
	info "  Squashing to $FILE"
	# shellcheck disable=SC2035
	(cd "$TDIR" && verbose sudo mksquashfs * "$TOPDIR/$FILE" 2>&1) | tee -a "$LOG"
	sudo chown "$USER:" "$FILE"
	compress "$FILE"
	sudo rm -rf "${TDIR:?}"
	add_to_manifest "$XZFILE"

	##############################################################
	list_files "$XZFILE"
	stampfile "$XZFILE"

	leave_workdir
}

######################################################################
# BUILD: ARG:uboot: Build U-boot and save configuration
build_uboot() {
	if [[ $# -lt 1 ]] ; then usage "build_uboot" ; fi
	local UBVERSION=${1:-$UBVERSION}
	local UBOOT_CONFIG=${2:-}
	debug "build_uboot: UBVERSION=$UBVERSION UBOOT_CONFIG=${UBOOT_CONFIG:+...}"

	##############################################################
	check_board
	#local UBOOT="$RESOURCES/$COURSE/u-boot-$UBVERSION.tar.bz2"
	local UBOOT="u-boot-$UBVERSION.tar.bz2"
	local DIR="u-boot-$UBVERSION"
	local DOTCONFIG="$DIR/.config"
	local UBOOTIMG="$DIR/u-boot.img"
	local UBOOTSPL="${UBOOTSPL:+$DIR/$SPL}"

	##############################################################
	local CONFIG="$BOARD-u-boot-$UBVERSION-config"
	local BOOTSPL="$BOARD-u-boot-$UBVERSION-$SPL.xz"
	local BOOTLDR="$BOARD-u-boot-$UBVERSION-u-boot.img.xz"

	enter_workdir .

	##############################################################
	announce "Building U-boot for $BOARD: $DIR"

	##############################################################
	# See if we're already done
	if checkstamp "${BOOTLDR/.img/}" ; then
		leave_workdir
		return 0
	fi

	##############################################################
	fetch_file "./$UBOOT" "$UBOOTURL"
	if [[ -n $FETCH_ONLY ]] ; then
		leave_workdir
		return 0
	fi

	##############################################################
	unpack "$UBOOT" "$DIR" || true

	##############################################################
	check_toolchain
	if config_build "$DIR" "$MYMAKE" distclean "$UBDEFCONFIG" "$UBOOT_CONFIG" ; then
		rm -f "${UBOOTIMG:?}" "${UBOOTIMG/.img/.bin}"
		[[ -z "${UBOOTSPL:-}" ]] || rm -f "${UBOOTSPL:?}"
	fi

	##############################################################
	if [[ -z $FORCE ]] && [[ -e $UBOOTIMG || -e ${UBOOTIMG/.img/.bin} ]] ; then
		vwarn "  Already built: $DIR"
	else
		compile_code "$DIR" "$MYMAKE" "clean all"
		rm -f "${CONFIG:?}" "${BOOTSPL:?}" "${BOOTLDR:?}" "${BOOTLDR/.img/.bin}"
	fi

	##############################################################
	#add_to_manifest "$UBOOT"
	save_config "$DOTCONFIG" "$CONFIG"
	if [[ -n "${SPL:-}" ]] ; then
		save_build_artifacts "$UBOOTSPL" "$BOOTSPL"
	else
		BOOTSPL=""
	fi
	local BOOTBIN BOOTELF
	if [[ -s $UBOOTIMG ]] ; then
		save_build_artifacts "$UBOOTIMG" "$BOOTLDR"
		BOOTBIN="$BOOTLDR"
	else
		BOOTBIN="${BOOTLDR/.img/.bin}"
		save_build_artifacts "${UBOOTIMG/.img/.bin}" "$BOOTBIN"
		BOOTELF="${BOOTLDR/.img/}"
		save_build_artifacts "${UBOOTIMG/.img/}" "$BOOTELF"
	fi
	debug "build_uboot: Using $BOOTBIN"

	##############################################################
	list_files "$UBOOT" "$CONFIG"
	stampfile "${BOOTLDR/.img/}"

	leave_workdir
}

######################################################################
# BUILD: ARG:opensbi: Build OpenSBI and save configuration
build_opensbi() {
	if [[ $# -lt 1 ]] ; then usage "build_opensbi" ; fi
	local SBIVERSION=${1:-$SBIVERSION}
	debug "build_opensbi: SBIVERSION=$SBIVERSION ARCH=$ARCH"

	##############################################################
	check_board
	local NAME="opensbi"
	local SBI="$NAME-v$SBIVERSION.tar.gz"
	local DIR="$NAME-$SBIVERSION"
	local FIRMWARE="$DIR/build/platform/$SBIPLATFORM/firmware"
	local BOOTFW="$FIRMWARE/$BIOS"

	##############################################################
	local BIOSFWXZ="$BOARD-$NAME-$SBIVERSION-$BIOS.xz"

	enter_workdir .

	##############################################################
	announce "Building OPENSBI for $BOARD: $DIR"

	##############################################################
	# See if we're already done
	if checkstamp "$BIOSFWXZ" ; then
		leave_workdir
		return 0
	fi

	##############################################################
	fetch_file "./$SBI" "$SBIURL/archive" "${SBI#*-}"
	if [[ -n $FETCH_ONLY ]] ; then
		leave_workdir
		return 0
	fi

	##############################################################
	unpack "$SBI" "$DIR" || true

	##############################################################
	if [[ -z $FORCE && -e $BOOTFW ]] ; then
		vwarn "  Already built: $DIR"
	else
		check_toolchain
		compile_code "$DIR" "$MYMAKE" "clean all" "PLATFORM=$SBIPLATFORM"
		rm -f "${BIOSFWXZ:?}"
	fi

	##############################################################
	#add_to_manifest "$SBI"
	save_build_artifacts "$BOOTFW" "$BIOSFWXZ"

	##############################################################
	list_files "$SBI"
	stampfile "$BIOSFWXZ"

	leave_workdir
}

######################################################################
# ARG:update-links: Update symlinks from RESOURCES dir
update_links() {
	local TO FROM
	for FILE in "$RESOURCES/$COURSE/"*"${1:?}"* ; do
		TO=$(basename "$FILE")
		FROM=$(abs2rel "$FILE" "$TO")
		[[ -n $TEST ]] || rm -f "${TO:?}"
		if [[ ! -e $FROM ]] ; then
			error "File not found: $FROM"
		elif [[ $TO =~ config ]] ; then
			copy_link "$FROM" "$TO"
		else
			ln $VERBOSE --symbolic "$FROM" "$TO"
		fi
	done
}

######################################################################
# Check for specific course labs
# used-by: copy, install_uenv, remove, replace, symlink
labsdir() {
	local FILE=${1:?} DIR CDIR
	debug "labsdir: FILE=$FILE $*"

	if [[ "$FILE" =~ ^labs[/]* ]] ; then
		if [[ "$FILE" =~ ^labs[/]*$ ]] ; then
			DIR='.'
			FILE=''
		elif [[ $FILE =~ /$ ]] ; then
			DIR="${FILE#labs/}"
			FILE=''
		else
			DIR=$(dirname "${FILE#labs/}")
			FILE=$(basename "$FILE")
		fi
	elif [[ $FILE =~ /$ ]] ; then
		DIR="${FILE%/}"
		FILE=''
	else
		DIR="$(dirname "$(realpath "$FILE")")"
		FILE="$(basename "$FILE")"
	fi
	CDIR="${COURSE}labs"
	debug "  labsdir: FILE=$FILE DIR=$DIR CDIR=$CDIR COURSE=$COURSE BOARD=$BOARD PWD=$(pwd)"
	if [[ ! $DIR =~ ^/ && ! $PWD = $TOPDIR ]] ; then
		debug "  labsdir: $PWD not in $TOPDIR"
		if [[ $DIR = . ]] ; then
			DIR="$LFCWDIR"
		else
			DIR="$LFCWDIR/$DIR"
		fi
		debug "  labsdir: DIR=$DIR"
	fi

	if [[ -d "$DIR/$BOARD/$CDIR" ]] ; then
		FILE="$DIR/$BOARD/$CDIR/$FILE"
	elif [[ -d "$DIR/$BOARD/$DIR" ]] ; then
		FILE="$DIR/$BOARD/$DIR/$FILE"
	elif [[ -d "$DIR/$CDIR" ]] ; then
		FILE="$DIR/$CDIR/$FILE"
	elif [[ -d "$DIR/labs" ]] ; then
		FILE="$DIR/labs/$FILE"
	elif [[ -d "$DIR" ]] ; then
		FILE="$DIR/$FILE"
	fi

	FILE="${FILE%/}"
	debug "Using ${FILE/\\\*/*}"
	# shellcheck disable=SC2086
	echo ${FILE/\\\*/*}
}

######################################################################
# CAPTURE: Capture webpage from URL
capture_url() {
	local FILE=${1:-} URL=${2:-} GEO=${3:-} WIDTH HEIGHT OPTS
	debug "capture_url: FILE=$FILE URL=$URL GEO=$GEO"

	IFS=x read -r WIDTH HEIGHT <<<"$GEO"
	WIDTH=${WIDTH:-1280}
	#HEIGHT=${HEIGHT:-800}
	OPTS="--width $WIDTH ${HEIGHT:+--height $HEIGHT} --enable-javascript --javascript-delay 5000"

	info "Capture URL to IMAGES/$FILE"
	# shellcheck disable=SC2086
	wkhtmltoimage $OPTS "$URL" "$IMAGES/$FILE" >/dev/null

	info "  Optimize IMAGES/$FILE"
	optipng -clobber -force "$IMAGES/$FILE" 2>/dev/null
}

######################################################################
# LABS: Remove file via lfcw file
# used-by: copy, symlink
remove() {
	local DIR=${1:?} FILES
	# shellcheck disable=SC2086
	FILES="$(labsdir ${DIR/\*/\\*})"
	debug "Remove: FILES=$FILES ($DIR)"

	if [[ -n ${NORM:-} ]] ; then
		return 0
	elif [[ -n $FILES ]] ; then
		# shellcheck disable=SC2086
		verbose rm -f ${FILES:?}
	else
		warn "Files not found: $DIR"
	fi
}

######################################################################
# LABS: Copy file via lfcw file
copy() {
	debug "Copy:" "$@"
	local FROM=${1:?} TO REMOVE="${3:-}"
	TO=$(labsdir "${2:?}")
	debug "Copy: FROM=$FROM TO=$TO REMOVE=$REMOVE"

	if [[ -z $FORCE ]] ; then
		if [[ -f $TO ]] ; then
			if cmp -s "$FROM" "$TO" ; then
				[[ -n $QUIET ]] || warn "Hasn't changed: $FROM"
				return 0
			fi
		elif [[ -d $TO && -f "$TO/${FROM##*/}" ]] ; then
			if cmp -s "$FROM" "$TO/${FROM##*/}" ; then
				[[ -n $QUIET ]] || warn "Hasn't changed: $FROM"
				return 0
			fi
		fi
	fi

	if [[ -e $FROM ]] ; then
		if [[ -n $REMOVE ]] ; then
			remove "$REMOVE"
		fi
		if [[ -f $TO ]] ; then
			verbose rm -f "${TO:?}"
		elif [[ -d $TO && -f "$TO/${FROM##*/}" ]] ; then
			verbose rm -f "${TO:?}/${FROM##*/}"
		fi
		#verbose cp $VERBOSE --link "$FROM" "$TO"
		copy_link "$FROM" "$TO"
	else
		error "File not found: $FROM"
	fi
}

######################################################################
# LABS: Check string in file is properly up to date
replace() {
	local FILE FROM=${2:?} TO=${3:?}
	FILE=$(labsdir "${1:?}")
	debug "Replace: FILE=$FILE FROM=$FROM TO=$TO"

	verbose sed -i.bak -r -e "s/$FROM/$TO/" "$FILE"
	if [[ -f $FILE.bak ]] ; then
		diff -u "$FILE" "$FILE.bak" || true
	fi
}

######################################################################
# LABS: Symlink file via lfcw file
# used-by: build_kernel
symlink() {
	local FILE=${1:?} SYMLINK=${2:?} REL REMOVE="${3:-}"
	[[ $SYMLINK =~ ^/ ]] || SYMLINK=$(labsdir "$2")
	REL=$(abs2rel "$FILE" "$SYMLINK")
	debug "symlink: FILE=$FILE SYMLINK=$SYMLINK($2) REL=$REL REMOVE=$REMOVE PWD=$(pwd)"

	if [[ -z $REL ]] ; then
		error "No relative link for $FILE"
	elif [[ -z $FORCE ]] ; then
		if [[ -f $FILE && -d $SYMLINK && -e "$SYMLINK/${FILE##*/}" ]] ; then
			if cmp "$FILE" "$SYMLINK/${FILE##*/}" ; then
				[[ -n $QUIET ]] || warn "Hasn't changed: $SYMLINK/${FILE##*/}"
				return 0
			fi
		elif [[ -f $FILE && -f $SYMLINK ]] ; then
			if cmp "$FILE" "$SYMLINK" ; then
				[[ -n $QUIET ]] || warn "Hasn't changed: $SYMLINK"
				return 0
			fi
		fi
	fi

	if [[ -e $FILE ]] ; then
		if [[ -n $REMOVE ]] ; then
			remove "$REMOVE"
		fi
		if [[ -L $SYMLINK ]] ; then
			rm -f "${SYMLINK:?}"
		fi
		info "Symlink '$SYMLINK' -> '$REL'"
		verbose ln $VERBOSE --symbolic --force "$REL" "$SYMLINK"
	else
		error "File not found: $FILE"
	fi
}

######################################################################
# SDCARD: Check USB driver is loaded and mounted
# used-by: install_uenv
check_usb() {
	local DIR=${1:?}

	if [[ ! -d $DIR ]] ; then
		sudo modprobe uas
		sudo partprobe 2>/dev/null
	fi
}

######################################################################
# LABS,SDCARD,TEST,TFTP: enter/leave chapter
# used-by: install_uenv
enter_chapter() {
	if [[ -n $LFCWDIR ]] ; then
		cd "$LFCWDIR"
	else
		error "No chapter dir specified"
	fi
}

######################################################################
# SDCARD,TFTP: Install uEnv.txt on sdcard or tftp
install_uenv() {
	local FILE DEST EXTRA DESTDIR
	enter_chapter
	FILE=$(labsdir "${1:?}")
	if [[ ${2:?} =~ ^/ && -d $(dirname "${2:?}") ]] ; then
		DEST="$2"
	else
		local DIR
		DIR=$(basename "$1")
		DEST="/media/$USER/sdboot/${2:-$DIR}"
	fi
	EXTRA="${3:+$DEST-$3}"
	DESTDIR=$(dirname "$DEST")
	check_usb "$DESTDIR"
	debug "install_uenv: FILE=$FILE DEST=$DEST EXTRA=$EXTRA"
	if [[ -n $TEST || -f $FILE && -d $DESTDIR ]] ; then
		verbose sudo cp -v "$FILE" "$DEST"
		if [[ -n $EXTRA ]] ; then
			verbose sudo cp -v "$FILE" "$EXTRA"
		fi
	elif [[ -f $FILE ]] ; then
		error "install_uenv: $DESTDIR unavailable (likely not mounted)"
	else
		error "install_uenv: $FILE missing (from $(pwd))"
	fi
}

######################################################################
# SDCARD,TFTP: Check artifact installation
check_artifact() {
	local DIR=${1:?} FILE=${2:?}

	if [[ $DIR =~ ^/ ]] ; then
		FILE="$DIR/$FILE"
	elif [[ -d /media/$USER/$DIR ]] ; then
		FILE="/media/$USER/$DIR/$FILE"
	elif [[ -n $DIR ]] ; then
		FILE="$DIR/$FILE"
	fi

	if [[ -n $TEST || -e $FILE ]] ; then
		info "check_artifact: $FILE exists"
	else
		error "check_artifact: $FILE is missing"
	fi
}

######################################################################
# SDCARD,TFTP: Install artifact with bbbinstall
install_artifact() {
	local BUILDDIR="${1:?}" OPTS='' DEST='' CHECK=''
	info "install_artifact: BUILDDIR:$BUILDDIR $*"
	shift
	if [[ ${1:-} =~ ^- ]] ; then
		OPTS="$1"; shift
	fi
	if [[ -n ${1:-} ]] ; then
		DEST=${1:-}; shift
		if [[ $DEST =~ ^/ && ! -e $DEST ]] ; then
			verbose sudo mkdir -p "$DEST"
		fi
	fi
	if [[ -n ${1:-} ]] ; then
		if [[ $1 =~ ^/ ]] ; then
			CHECK="$1"
		elif [[ $DEST =~ ^/ ]] ; then
			CHECK="$DEST/$1"
		else
			CHECK="/media/$USER/$DEST/$1"
		fi
		shift
	fi
	debug "install_artifact: BUILDDIR=$BUILDDIR OPTS=$OPTS DEST=$DEST CHECK=$CHECK"

	case "${BOARD:-}" in
		bbb|beagleboneblack)
			verbose bbbboot --unchoose ${TEST:+--test} ${TRACE:+--trace} ;;
		riscv64)
			verbose riscv-boot --unchoose ${TEST:+--test} ${TRACE:+--trace} ;;
		*) warn "No boot control script for ${BOARD:-} board" ;;
	esac

	if [[ -z $CHECK || -e "$CHECK" ]] ; then
		enter_workdir "$BUILDDIR"
		case "${BOARD:-}" in
			bbb|beagleboneblack)
				debug "bbbinstall ${DEBUG:+--debug} ${TEST:+--test} ${TRACE:+--trace} $OPTS $DEST $* (run in $(pwd))"
				# shellcheck disable=SC2086
				verbose bbbinstall ${DEBUG:+--debug} ${TEST:+--test} ${TRACE:+--trace} $OPTS "$DEST" "$@"
				;;
			riscv*)
				debug "riscv-install ${DEBUG:+--debug} ${TEST:+--test} ${TRACE:+--trace} $OPTS $DEST $* (run in $(pwd))"
				# shellcheck disable=SC2086
				verbose riscv-install ${DEBUG:+--debug} ${TEST:+--test} ${TRACE:+--trace} $OPTS "$DEST" "$@"
				;;
			vexpress)
				debug "vexpress-install ${DEBUG:+--debug} ${TEST:+--test} ${TRACE:+--trace} $OPTS $DEST $* (run in $(pwd))"
				# shellcheck disable=SC2086
				verbose vexpress-install ${DEBUG:+--debug} ${TEST:+--test} ${TRACE:+--trace} $OPTS "$DEST" "$@"
				;;
			*) warn "No install option for ${BOARD:-} board"
		esac
		leave_workdir
	fi
}

######################################################################
# used-by: copy_code, ls_dir
lfcwdir() {
	local FILE=${1:?}
	if [[ $FILE =~ ^/ ]] ; then
		echo "$FILE"
	else
		echo "$LFCWDIR/$FILE"
	fi
}

######################################################################
# Copy code from code base to course to be included in latex
# ARGS: IN,FRAGMENT
# Fragment: FILE,FUNC,BEGIN,END,OPT
# Fragment: FUNC,BEGIN,END,OPT
# Fragment: BEGIN,END,OPT
# Fragment: OPT
# used-by: parse_fragment, first_last
parse_fragment() {
	local IN=${1:?} FRAG=${2:?}
	local FILE FUNC BEGIN END OPT
	IFS=, read -r FILE FUNC BEGIN END OPT <<<"$FRAG"
	debug "      parse_fragment: ${FILE##*/}:$FUNC{$BEGIN,$END} ($OPT)"

	if [[ ! -f $FILE ]] ; then
		OPT="$END"
		END="$BEGIN"
		BEGIN="$FUNC"
		FUNC="$FILE"
		FILE="$IN"
	fi

	#debug "      parse_fragment: ${FILE##*/}:$FUNC{$BEGIN,$END} ($OPT)"
	if [[ $FUNC =~ ^[0-9]+$ ]] ; then
		OPT="$END"
		END="$BEGIN"
		BEGIN="$FUNC"
		FUNC=
	fi

	#debug "      parse_fragment: ${FILE##*/}:$FUNC{$BEGIN,$END} ($OPT)"
	if [[ $BEGIN = snip || $BEGIN = nosnip ]] ; then
		OPT=$BEGIN
		BEGIN=
	fi

	debug "      parse_fragment: ${FILE##*/}:$FUNC{$BEGIN,$END} ($OPT)"
	echo "$FILE,${FUNC//\//\\\/},${BEGIN:-},${END:-},$OPT"
}

######################################################################
# BUILD,CODE: Copy code from code base to course to be included in latex
# used-by: get_tree_from_tar
get_tree() {
	local FILE="$LFCWDIR/${1:?}$OUTPUTEXT"; shift
	local DIR=${1:?}; shift
	debug "get_tree FILE=$FILE DIR=$DIR OPTS=$*"
	info "  Running tree on $* stored to $FILE"
	(cd "$DIR" && tree --charset=ascii --nolinks "$@" | expand) >"$FILE"
}

######################################################################
# BUILD: Generate tree from a tarfile
get_tree_from_tar() {
	local FILE TAR DIR
	FILE=${1:?}; shift
	TAR=${1:?}; shift
	[[ -e $TAR ]] || TAR="$WORKDIR/$TAR"
	[[ -e $TAR ]] || error "No tarfile for get_tree_from_tar: $TAR"
	DIR="$(mk_tmpdir)"
	[[ -n $DIR && -d $DIR ]] || error "No temporary directory for get_tree_from_tar"
	debug "get_tree FILE=$FILE TAR=$TAR DIR=$DIR OPTS=$*"

	untar "$TAR" "$DIR"
	get_tree "$FILE" "$DIR" "$@"
	rm -rf "${DIR:?}"
}

######################################################################
# CODE: run graph-kernel
graph_kernel() {
	local DIR=${1:?} STAMP="$KERNELGRAPHS/boardfiles.png"
	debug "graph_kernel DIR=$DIR STAMP=$STAMP"

	if checkstamp "$STAMP" ; then
		info "graph_kernel: Graphs already created"
	else
		graph-kernel.sh "$DIR"

		# Copy resulting graphs to IMAGES
		local FILE IMG
		# shellcheck disable=SC2044
		for FILE in $(find "$KERNELGRAPHS" -name \*.png) ; do
			IMG="$IMAGES/${FILE##*/}"
			if [[ -f $IMG ]] ; then
				cp -u -v "$FILE" "$IMG"
			fi
		done
		stampfile "$STAMP"
	fi
}

######################################################################
# used-by: find_func, first_last
find_line() {
	local FILE=${1:?} PAT=${2:?} BEGIN=$3
	debug "      find_line: FILE:$FILE PAT:$PAT BEGIN:$BEGIN"
	if [[ $BEGIN = comment ]] ; then
		# Find c comment before PAT
		local NUM=1 LINE
		LINE="$(grep -B "$NUM" -nre "$PAT" "$FILE" | head -n 1)"
		#echo "LINE=$LINE" >&2
		if [[ $LINE =~ /\* ]] ; then
			awk -F- '{print $1; exit}' <<<"$LINE"
		elif [[ $LINE =~ \*/ ]] ; then
			while [[ ! $LINE =~ /\* ]] ; do
				NUM=$(( NUM + 1 ))
				LINE="$(grep -B "$NUM" -nre "$PAT" "$FILE")"
			done
			awk -F- '{print $1; exit}' <<<"$LINE"
		else
			#error "Didn't find comment before $PAT in ${FILE##*/}; check your code fragment"
			debug "Didn't find comment before $PAT in ${FILE##*/}; check your code fragment"
			return
		fi
	else
		local FOUND
		# Find simple pattern
		FOUND="$(grep -nre "$PAT" "$FILE" | awk -F: '{print $1; exit}')"
		if [[ -n $FOUND ]] ; then
			echo "$FOUND"
		elif [[ -n $SHOW ]] ; then
			warn "find_line: '$PAT' not found in '$FILE'"
		fi
	fi
}

######################################################################
# Find the last line for the fragment
# ARGS: outfile infile fragment
# used-by: first_last
find_func() {
	local IN=${1:?} FUNC=${2:?} COMMENT=$3 LINENO
	# shellcheck disable=SC1087
	LINENO=$(find_line "$IN" " \*$FUNC[(]" "$COMMENT")
	#debug "find_func 1: LINENO=$LINENO"
	# shellcheck disable=SC1087
	[[ -n $LINENO ]] || LINENO=$(find_line "$IN" "^$FUNC[(]" "$COMMENT")
	#debug "find_func 2: LINENO=$LINENO"
	# shellcheck disable=SC1087
	[[ -n $LINENO ]] || LINENO=$(find_line "$IN" "^[^*=(]*$FUNC[(]" "$COMMENT")
	debug "    find_func 3: LINENO=$LINENO"
	if [[ -n $LINENO ]] ; then
		echo "$LINENO"
	elif [[ -n $SHOW ]] ; then
		warn "find_func: '$FUNC' not found in '$IN' ($COMMENT)"
	fi
}

######################################################################
# Find the last line for the fragment
# ARGS: outfile infile fragment
# used-by: first_last
last_line() {
	local FILE=${1:?} FIRST=${2:-1} PAT OFFSET=1
	debug "      last_line: FILE:$FILE FIRST:${3:-}"
	if [[ -n ${3:-} ]] ; then
		PAT=${3:-}
	else
		error "last_line called with empty pattern for $FILE:$FIRST"
	fi
	if [[ $PAT = ^$ ]] ; then
		OFFSET=2
	fi
	debug "      last_line: FILE=${FILE##*/} FIRST=$FIRST PAT=$PAT OFFSET=$OFFSET"

	sed -n "$FIRST,\$p" <"$FILE" | grep -n "$PAT" | awk -F: "{print \$1 - $OFFSET + $FIRST; exit}"

}

######################################################################
# used-by: first_last
apply_offset() {
	local NUM=${1:?} OFFSET=$2
	#info "  apply_offset: NUM=$NUM OFFSET=$OFFSET"

	if [[ -z $OFFSET || $OFFSET = 0 ]] ; then
		echo "$NUM"
	elif [[ -z $NUM || $NUM -eq 0 ]] ; then
		echo "$OFFSET"
	else
		# shellcheck disable=SC1102,SC2005,SC2086
		case "$OFFSET" in
			+*|-*) echo "$(( NUM $OFFSET ))" ;;
			# Lines start at 1, not 0
			[0-9]*) echo "$(( NUM + OFFSET - 1))" ;;
		esac
	fi
}

######################################################################
# Find first and last line for fragment
# ARGS: outfile infile fragment
# OUTFILE: MACRO,OPTION
# MACRO: format {cfile,configfile,fdtfile}
# OPTION: firstnumber[=num]
# FILE: infile, or another file
# FUNC: cfile -> ^'#define ...'
# FUNC: cfile -> ^'struct .* = ('
# FUNC: cfile -> ' function('
# FUNC: configfile -> 'config FOO'
# FUNC: fdtfile -> 'label:'
# FUNC: fdtfile -> 'name@unit {'
# BEGIN: comment+/-number
# END: number,[begin]+/-1
# OPT: snip,nosnip
# Only breakable if longer than 29 lines
# used-by: copy_code
first_last() {
	local TEX=${1:?} IN=${2:?} FRAGMENT=${3:-}
	#info "first_last: <$1><$2><${3:-}>"
	debug "  first_last: TEX=$TEX IN=${IN##*/} FRAGMENT=$FRAGMENT"

	local MACRO OPTION
	IFS=, read -r MACRO OPTION <<<"$TEX"
	debug "    first_last: MACRO=$MACRO OPTION=$OPTION"

	# Parse the fragment into metadata
	local FILE FUNC COMMENT="" BEGIN="" START="" END="" OPT=""
	IFS=, read -r FILE FUNC BEGIN END OPT <<<"$(parse_fragment "$IN" "$FRAGMENT")"
	debug "    first_last: FILE=${FILE##*/} FUNC=$FUNC BEGIN=$BEGIN END=$END OPT=$OPT"

	# Include the comment before the fragment if instructed
	if [[ $BEGIN =~ ^comment ]] ; then
		if [[ $BEGIN =~ - ]] ; then
			IFS=- read -r COMMENT BEGIN <<<"$BEGIN"
			BEGIN="${BEGIN:+-$BEGIN}"
		else
			IFS=+ read -r COMMENT BEGIN <<<"$BEGIN"
			BEGIN="${BEGIN:++$BEGIN}"
		fi
		[[ -n $BEGIN ]] || BEGIN=1
	fi

	# Calculate end relative to the beginning if instructed
	if [[ $END =~ ^begin ]] ; then
		if [[ $END =~ - ]] ; then
			IFS=- read -r START END <<<"$END"
			END="${END:+-$END}"
		else
			IFS=+ read -r START END <<<"$END"
			END="${END:++$END}"
		fi
	fi
	debug "    first_last: $MACRO($OPTION) ${FILE##*/}:$FUNC{$BEGIN,$END} $OPT"

	# Line numbers ($OPTION $MACRO $IN $FUNC $COMMENT)
	local FIRST=1 LAST='$'
	if [[ -z $FUNC ]] ; then
		debug "    *** first_last: No function"
		if [[ -z $BEGIN && -z $END ]] ; then
			# Use the whole file
			LAST=$(wc -l <"$IN")
		else
			if [[ -n $BEGIN ]] ; then
				FIRST=0
			fi
			if [[ -n $END ]] ; then
				LAST=0
			fi
		fi
	else
		debug "    *** first_last: MACRO=$MACRO"
		case "$MACRO" in
			cfile) if [[ $FUNC =~ ^# ]] ; then
					# cpp statements
					FIRST=$(find_line "$IN" "^$FUNC\$" "$COMMENT")
					if [[ $START = begin ]] ; then
						LAST=$(find_line "$IN" "^$FUNC\$" "")
					else
						LAST=$(last_line "$IN" "$FIRST" '^#endif')
					fi
				elif [[ $FUNC =~ ^(static )*(const )*struct ]] ; then
					# struct definition
					# shellcheck disable=SC1087
					FIRST=$(find_line "$IN" "^$FUNC[ \[].*=" "$COMMENT")
					[[ -n $FIRST ]] || FIRST=$(find_line "$IN" "^$FUNC {" "$COMMENT")
					if [[ $START = begin ]] ; then
						LAST=$(find_line "$IN" "^$FUNC .*=" "")
						[[ -n $LAST ]] || LAST=$(find_line "$IN" "^$FUNC {" "")
					else
						LAST=$(last_line "$IN" "$FIRST" '^};$')
					fi
				else
					# Function
					FIRST=$(find_func "$IN"  "$FUNC" "$COMMENT")
					debug "    first_last: FIRST=$FIRST"
					if [[ $START = begin ]] ; then
						LAST=$(find_func "$IN"  "$FUNC" "")
					else
						# shellcheck disable=SC2086
						LAST=$(last_line "$IN" "$FIRST" '^}$')
					fi
					debug "    first_last: LAST=$LAST"
				fi;;
			configfile) FIRST=$(find_line "$IN" "^$FUNC\$" "$COMMENT")
				if [[ $START = begin ]] ; then
					LAST=$(find_line "$IN" "^$FUNC\$" "")
				else
					# shellcheck disable=SC2086
					LAST=$(last_line "$IN" "$FIRST" '^$')
				fi ;;
			fdtfile) local PAT INDENT
				if [[ $FUNC =~ :$ ]] ; then
					PAT="$FUNC"
				else
					PAT="$FUNC *{$"
				fi
				INDENT=$(grep -e "^[ 	]*$PAT" "$FILE" | sed -e "s/$FUNC.*$//")
				FIRST=$(find_line "$IN" "^[ 	]*$FUNC" "$COMMENT")
				debug "    first_last: FIRST=$FIRST"
				if [[ $START = begin ]] ; then
					LAST=$(find_line "$IN" "^[ 	]*$FUNC" "")
				else
					# shellcheck disable=SC2086
					LAST=$(last_line "$IN" "$FIRST" "^$INDENT};$")
				fi ;;
		esac
	fi

	if [[ -z $FIRST || -z $LAST ]] ; then
		error "first_last: '$FRAGMENT' not found in '$IN'"
	fi

	debug "    first_last: FIRST=$FIRST($BEGIN) LAST=$LAST($END)"
	if [[ -z $END || $END -eq 0 || $END =~ [+-] ]] ; then
		LAST="$(apply_offset "$LAST" "$END")"
	else
		LAST="$(apply_offset "$FIRST" "$END")"
	fi
	FIRST="$(apply_offset "$FIRST" "$BEGIN")"
	debug "    first_last: FIRST=$FIRST LAST=$LAST"

	echo "$FILE" "$FIRST" "$LAST" "$OPT"
}

######################################################################
# used-by: tdd
test_output() {
	debug "test_output:" "$@"
	if test "$@" ; then
		pass "$@"
	else
		fail "$@"
	fi
}

######################################################################
tdd() {
	local RESULT=${1:?} COMPARE=${2:?} RET
	shift; shift
	#info "tdd: <$1><$2><${3:-}><${4:-}>"

	RET=$("$@")
	case "$COMPARE" in
		equal|=) test_output "$RESULT" '=' "$RET" ;;
		notequal|!=) test_output "$RESULT" '!=' "$RET" ;;
		regex|=~) test_output "$RESULT" '=~' "$RET" ;;
		*) error "Invalid operator: $RET" ;;
	esac
}

######################################################################
# ARG:test1: Read language, color, and icon from the latex listing for $PAT
# used-by: get_code
get_tex_meta() {
	local PAT=${1:?}

	# shellcheck disable=SC2005,SC2046
	echo $(awk -e "/$PAT/{flag=1}flag; /^$/{flag=0}" "$LFLST" \
		| awk -e '/%/ {next};
		/language=/ {match($0,/=(.*),/,arr); print arr[1]};
		/colbacktitle=/ {match($0,/=(.*),/,arr); print arr[1]};
		/\\icon{/ {match($0,/{([^{}]*)}/,arr); print arr[1]};')
}

######################################################################
# CODE: Copy code from code base to course to be included in latex
# ARGS: outfile format title infile [fragments]
# For fragments see first_last()
# used-by: copy_dts, copy_kernel
copy_code() {
	local OUT
	OUT="$(lfcwdir "${1:?}")"; shift
	local TEX=${1:?}; shift
	local TITLE=${1:?}; shift
	local IN=${1:?}; shift

	info "copy_code: IN=${IN##*/} OUT=${OUT##*/}"

	local MACRO OPTION
	IFS=, read -r MACRO OPTION <<<"$TEX"
	debug "  copy_code: MACRO=$MACRO OPTION=$OPTION"

	local LANGUAGE COLOR ICON OTHER
	read -r LANGUAGE COLOR ICON OTHER <<<"$(get_tex_meta "$MACRO")"
	debug "  copy_code: LANGUAGE=$LANGUAGE COLOR=<$COLOR> ICON={${ICON#\\}}"

	cat <<HEADER >"$OUT"
%% $GENERATED and code.lfcw
\\begin{iconbox}[breakable]{$TITLE}{$COLOR}{$ICON}
HEADER

	# Get first line of code and start minted
	local FILE FIRST LAST OPT
	if [[ $OPTION =~ ^first ]] ; then
		read -r FILE FIRST LAST OPT <<<"$(first_last "$MACRO" "$IN" "${1:-}")"
		echo "   \\begin{minted}[autogobble,breaklines,linenos,numbersep=7mm,firstnumber=$FIRST]{$LANGUAGE}" >>"$OUT"
	else
		echo "   \\begin{minted}[autogobble,breaklines]{$LANGUAGE}" >>"$OUT"
	fi

	# Copy whole file ($# $IN $OUT)
	if [[ $# -eq 0 ]] ; then
		debug "  copy_code: ${IN##*/} >> ${OUT##*/}"
		expand "$IN" >>"$OUT"
	fi

	# Copy partial file ($IN $FILE $FUNC $BEGIN $END $OPT $OUT)
	while [[ $# -gt 0 ]] ; do
		debug "*** copy_code: FRAGMENT:${1:?}"

		# Extract text between first and last line of code fragment
		read -r FILE FIRST LAST OPT <<<"$(first_last "$MACRO" "$IN" "$1")"
		debug "  copy_code: $1 -> ${FILE}[$FIRST,$LAST]$OPT >> ${OUT##*/}"
		if [[ -z $FIRST || -z $LAST ]] ; then
			error "No $FIRST/$LAST found: $1"
		fi
		debug "  copy_code: awk 'NR >= $FIRST && NR <= $LAST' '$FILE' >>$OUT"
		awk "NR >= $FIRST && NR <= $LAST" "$FILE" | expand >>"${OUT##*/}"

		# Add a SNIP if required
		if [[ $# -gt 1 ]] ; then
			if [[ ! $OPT = nosnip ]] ; then
				echo >>"$OUT"
				echo "$SNIP" >>"$OUT"
			fi
			echo >>"$OUT"
		elif [[ $OPT = snip ]] ; then
			echo >>"$OUT"
			echo "$SNIP" >>"$OUT"
		fi
		shift
	done

	cat <<FOOTER >>"$OUT"
   \\end{minted}
\\end{iconbox}
FOOTER
}

######################################################################
# CODE: Generate patch
# ARGS: ROOT PATCH FILE PATTERNS
get_patch() {
	local ROOT=${1:?} PATCH=${2:?} FILE=${3:?}
	shift; shift; shift

	pushd "$ROOT" >/dev/null
	mkdir -p "patches"
	if [[ ! -f patches/$PATCH ]] ; then
		quilt new "$PATCH"
		quilt add "$FILE"
		sed -i "$FILE" -re "$@"
		quilt refresh
		quilt pop
	fi
	popd >/dev/null
	cp -vu "$ROOT/patches/$PATCH" "$LFCWDIR/labs/"
}

######################################################################
# Used by get_macros
# used-by: get_macros
grep_macros() {
	local FILE=${1:?} PASTE MACROS COLORS ICONS TITLES
	if [[ -n $ONECOL ]] ; then
		PASTE="cat"
	else
		PASTE="paste - - - -"
	fi

	set +e
	MACROS=$(grep -e '\\newcommand{' -e '\\newenvironment{' -e '\\newtc' "$FILE" \
		| grep -v -e '%' \
		| sed -re 's/\\(re)?newcommand\{([^}]+)\}.*$/\2/g;
			s/\\newenvironment[^{]*\{([^}]+)\}.*$/\\begin{\1}/g;
			s/\\newtc[^{]*\{([^}]+)\}.*$/\\begin{\1}/g;
			s/^ *//' \
		| sort -u )

	info "From: $FILE" 2>&1
	grep -v -e 'color$' -e 'icon$' -e 'title$' <<<"$MACROS" \
		| $PASTE | column -s'	' -t

	COLORS=$(grep -e 'color$' <<<"$MACROS")
	if [[ -n $COLORS ]] ; then
		info "Colors:" 2>&1
		$PASTE <<<"$COLORS" | column -s'	' -t
	fi

	ICONS=$(grep -e 'icon$' <<<"$MACROS")
	if [[ -n $ICONS ]] ; then
		info "Icons:" 2>&1
		$PASTE <<<"$ICONS" | column -s'	' -t
	fi

	TITLES=$(grep -e 'title$' <<<"$MACROS")
	if [[ -n $TITLES ]] ; then
		info "Titles:" 2>&1
		$PASTE <<<"$TITLES" | column -s'	' -t
	fi
}

######################################################################
# ARG:macro:
get_macros() {
	local PAT=${1:-} FILES

	if [[ -n $PAT ]] ; then
		FILES=$(grep -l "$PAT" "$LFCLS" "$LFBOX" "$LFLST" || true)
		if [[ -z $FILES ]] ; then
			warn "Macro pattern '$PAT' not found in latex styles"
			exit 0
		fi
		# shellcheck disable=SC2086
		VICMD="silent! /$PAT" edit $FILES
	else
		(
			grep_macros "$LFCLS"
			grep_macros "$LFBOX"
			grep_macros "$LFLST"
		) | ${PAGER:-less -R}
	fi
}

######################################################################
# CODE: args: DSTFILE stanzas
copy_dts() {
	local DARCH=${1:?}; shift
	local FILE=${1:?}; shift
	local DTS="arch/$DARCH/boot/dts"
	copy_code "${FILE%.dts*}.inc" fdtfile "$DTS/$FILE" "$KERNELSRC/$DTS/$FILE" "$@"
}

######################################################################
# CODE: args: KERNFILE stanzas
copy_kernel() {
	local FILE=${1:?} OUT; shift
	OUT=$(sed -re 's|^.*/||; s|\.[^.]*$|.inc|;' <<<"$FILE")
	copy_code "$OUT" cfile,firstnumber "$FILE" "$KERNELSRC/$FILE" "$@"
}

######################################################################
# CODE: list files from a directory
ls_dir() {
	local DIR=${1:?} FILE
	FILE="$(lfcwdir "${2:?}")"
	info "ls $DIR"
	rm -f "${FILE:?}"
	# shellcheck disable=SC2012,SC2086
	ls -C -w90 $DIR | expand > "$FILE"
}

######################################################################
# ARG:compare: Diff files between similar MODULES (ignore symlinks)
compare_modules() {
	local OTHER=${1:-} FILES=${2:-.} DIR=. FILE
	if [[ -d $FILES ]] ; then
		DIR="$FILES"
		FILES=$(cd "$DIR"; find . -type f)
	fi
	for FILE in $FILES; do
		if [[ $FILE =~ \$OUTPUTEXT$ ]] ; then continue; fi
		file "$DIR/$FILE" | grep -qi Text || continue
		info "considering $FILE"
		if [[ -e "$OTHER/$FILE" ]] && ! diff -q "$OTHER/$FILE" "$DIR/$FILE" ; then
			# shellcheck disable=SC2086
			verbose ${VISUALDIFF:-vimdiff} "$OTHER/$FILE" "$DIR/$FILE"
		fi
	done
}

######################################################################
# ARG:diff: Diff files with those in RESOURCES
diff_files() {
	local FILE
	for FILE in "$@"; do
		diff -Naur "$RESOURCES/$COURSE/$FILE" "$FILE" | less
	done
}

######################################################################
# EXPECT:
openserial() {
	cat <<-ENDSERIAL
	set modem $BOARDSERIAL
	exec sh -c "sleep 1 < \$modem" &
	exec stty -F \$modem $BOARDBAUD raw -clocal -echo -istrip -hup
	spawn -open [open \$modem w+]
	set timeout 60
	ENDSERIAL
}

######################################################################
# EXPECT:
run_devkit() {
	export BORING=y
	cat <<-ENDDEVKIT
	spawn devkit $*
	ENDDEVKIT
}

######################################################################
# EXPECT:
sudopw() {
	cat <<-ENDDEVKIT
	"password for" {
		stty -echo
		interact -u tty_spawn_id -o "\r" return
		stty echo
		exp_continue
	}
	ENDDEVKIT
}

######################################################################
# EXPECT: Have expect log to a file
log_to() {
	local FILE="${1:-}"
	echo "log_file ${FILE:+\"$LFCWDIR/$FILE$OUTPUTEXT\"}"
}

######################################################################
# EXPECT: Choose which uEnv to boot from in U-boot
chooseuenv() {
	local NAME=${1:-}

	if [[ -z $NAME ]] ; then
		# shellcheck disable=SC2028
		echo 'send_user "W: Booting from internal eMMC. Press reset.\n"'
	else
		# shellcheck disable=SC2028
		echo 'send_user "W: Select uEnv.txt-'"$NAME"' and press reset.\n"'
	fi

	cat <<-ENDUBOOT
	send "\n"
	expect {
		"login:" {
			send "root\n"
			exp_continue
		}
		"password:" {
			send "\n"
			exp_continue
		}
		"#" {
			send "reboot\n"
			sleep 1
			exp_continue
		}
		"=>" {
			send "reset\n"
			exp_continue
		}
		"Press SPACE to abort autoboot in 2 seconds" {
			send " "
		}
		"Hit any key to stop autoboot:" {
			send " "
		}
	}
	expect "=>"
	send "mmc rescan\n"
	expect "=>"
	ENDUBOOT

	if [[ -z $NAME ]] ; then
		# shellcheck disable=SC2016,SC2028
		echo 'send "fatwrite mmc 0 \\\$loadaddr uEnv.txt 0\n"'
	else
		cat <<-ENDUBOOT
		send "fatload mmc 0 \\\$loadaddr uEnv.txt-$NAME\n"
		expect "=>"
		send "fatwrite mmc 0 \\\$loadaddr uEnv.txt \\\$filesize\n"
		ENDUBOOT
	fi

	cat <<-ENDUBOOT
	expect "=>"
	send "reset\n"
	ENDUBOOT
}

######################################################################
# EXPECT: Login to board with expect
login_as_root() {
	cat <<-ENDLOGIN
	send "\n"
	set timeout 500
	expect "login:"
	send "root\n"
	expect {
		"password:" {
			send "\n"
		}
		"#" {
			send "\n"
		}
	}
	set timeout 30
	ENDLOGIN
}

######################################################################
# EXPECT: Runs expect with a copy of the verbose functionality for testing
run_expect() {
	local FILE="${1:-$EXPECTFILE}" LOG LOGFILES STAMP

	# Turn off exciting prompts in devkit
	export BORING=y

	LOGFILES=()
	while read -r LOG; do
		STAMP="expect${LOG//\//-}"
		info "Stampfile: $STAMP"
		if checkstamp "$STAMP" ; then
			return
		fi
		info "Capturing $LOG"
		LOGFILES+=( "$LOG" )
		rm -f "${LOG:?}"
		stampfile "$STAMP"
	done <<<"$(awk -F\" '/log_file ./ {print $2}' "$EXPECTFILE")"

	if [[ -n $DEBUG ]] ; then
		savelog expect-debug.log
		verbose expect -d -f "$EXPECTFILE" | tee expect-debug.log
	else
		verbose expect -f "$EXPECTFILE"
	fi
	if [[ -z $DEBUG && -z $TEST ]] ; then
		rm -f "${EXPECTFILE:?}"
	fi

	fix_ascii "${LOGFILES[@]}"
}

######################################################################
# Execute lfcw file
# used-by: makelfcw
dolfcw() {
	local LFCWFILE=${1:?} LFCWDIR
	important "Executing $LFCWFILE"
	# shellcheck disable=SC2030
	(
		LFCWDIR="$(realpath "$(dirname "$LFCWFILE")")"
		LFCWFILE="$LFCWDIR/$(basename "$LFCWFILE")"
		RESDIR="$RESOURCES/$COURSE"
		EXPECTFILE="$LFCWDIR/expect.tmp"
		debug "dolfcw: WORKDIR=$WORKDIR LFCWDIR=$LFCWDIR LFCWFILE=$LFCWFILE RESDIR=$RESDIR"

		export LFCWFILE LFCWDIR RESDIR EXPECTFILE COURSES COURSE
		if grep -q "^#!" "$LFCWFILE" ; then
			"$LFCWFILE"
		else
			# shellcheck disable=SC1090
			. "$LFCWFILE"
		fi
	)
}

######################################################################
# Find all relevant lfcw files
# used-by: makelfcw
findlfcw() {
	local FILE="${1:?}.lfcw" DIR=${2:-.}
	debug "findlfcw: Finding $FILE in ${DIR:-.}"

	# shellcheck disable=SC2031
	if [[ -f "$DIR/$BOARD/$FILE" ]] ; then
		echo "$DIR/$BOARD/$FILE"
	elif [[ -f "$DIR/$FILE" ]] ; then
		echo "$DIR/$FILE"
	elif [[ -f $DIR/$COURSE.tex ]] ; then
		# shellcheck disable=SC2031
		for DIR in $(get_all_chapters "$COURSE") ; do
			if [[ -d $DIR ]] ; then
				findlfcw "${FILE%.lfcw*}" "$DIR"
			fi
		done
	fi
}

######################################################################
# ARG:list: List lab lfcw files
list_lfcw() {
	debug "list_lfcw: $*"
	if [[ $# -gt 0 ]] ; then
		local NAME FILES
		FILES="$(for NAME in "$@" ; do
			findlfcw "$NAME"
		done | sort -u)"
		# shellcheck disable=SC2086
		debug "  list_lfcw:" $FILES
		if [[ -n $EDIT ]] ; then
			# shellcheck disable=SC2086
			edit $FILES
		else
			echo "$FILES" #| ${PAGER:-less -R}
		fi
	else
		local LFCW
		for LFCW in $( (find . -maxdepth 3 -follow -name \*.lfcw 2>/dev/null || true) \
			| sed -e 's|^.*/||; s/.lfcw$//' | sort -u); do
			case "$LFCW" in
				build)	echo "$LFCW:	Build all the assets";;
				capture)echo "$LFCW:	Capture screenshots and webpages";;
				code)	echo "$LFCW:	Copy code into latex";;
				config)	echo "$LFCW:	Generate configs into latex";;
				expect)	echo "$LFCW:	Use expect to generate output files";;
				labs)	echo "$LFCW:	Link lab assets into place";;
				sdcard)	echo "$LFCW:	Burn SDcard with labs";;
				test)	echo "$LFCW:	Run tests on course";;
				tftp)	echo "$LFCW:	Install tftp/NFSroot assets for labs";;
				*)      echo "*$LFCW:	New lfcw file";;
			esac
		done
	fi
}

######################################################################
# ARG:build,capture,code,config,expect,fetch,labs,sdcard,test,tftp:
# Execute all found lfcw files
# used-by: make_all
makelfcw() {
	local TYPE=${1:?} DIR=${2:-} FILE FILES
	debug "makelfcw: TYPE=$TYPE DIR=$DIR"
	if [[ -n $DIR ]] ; then
		cd "$DIR"
	else
		DIR="."
	fi

	if [[ $TYPE = all ]] ; then
		shift
		make_all "$@"
		return 0
	fi

	mkdir -p "$WORKDIR"

	FILES=$(findlfcw "$TYPE" "$DIR")
	# shellcheck disable=SC2086
	debug "makelfcw:" $FILES
	if [[ -n "$FILES" ]] ; then
		for FILE in $FILES ; do
			dolfcw "$FILE"
		done
	else
		info "No $TYPE found"
	fi
	important "Finished everything for $TYPE"
}

######################################################################
# ARG:sdcard:
eject_card() {
	info "Eject SD card"
	case "${BOARD:-}" in
		bbb|beagleboneblack) verbose bbbinstall --eject ;;
		*) warn "No eject for ${BOARD:-} board" ;;
	esac
}

######################################################################
# ARG:all: Do all of the normal steps from the cheat sheet
# used-by: makelfcw
make_all() {
	verbose makelfcw build "$@"
	verbose makelfcw config "$@"
	verbose makelfcw code "$@"
	verbose makelfcw capture "$@"
	verbose makelfcw tftp "$@"
	if [[ -n ${HWKIT:-} ]] ; then
		if confirm "Ready to test. Insert card into reader." ; then
			verbose makelfcw sdcard "$@"
			if [[ -z ${HWKIT:-} ]] || confirm "Put card into board." ; then
				verbose makelfcw test "$@"
			fi
		else
			warn "Skipping HW testing"
		fi
	else
		verbose makelfcw test "$@"
	fi
	verbose makelfcw expect "$@"
	verbose check_all "$@"
	verbose make_pdf "$@"
	verbose pre_finalize_course "$@"
}

######################################################################
# EXPECT: Run devicetree compiler
dt_compile() {
	local FILE=${1:?}; shift
	# shellcheck disable=SC2031
	[[ -f $FILE ]] || FILE="$LFCWDIR/$FILE"
	verbose dtc "$FILE" >"${FILE/dts/dtb}"
	compress "${FILE/dts/dtb}"
}

######################################################################
# BUILD,EXPECT: Parse log output
from_log_file() {
	# shellcheck disable=SC2031
	local FILE="$LFCWDIR/${1:?}$OUTPUTEXT" NAME=${2:?} VER=${3:?} BEGIN=${4:?} END=${5:?}
	local LOG="$WORKDIR/$NAME-$VER.log"
	[[ -f $LOG ]] || error "log_file: File not found: $LOG"
	announce "Extracting output from log file: $LOG -> $FILE"
	sed -n "/$BEGIN/,/$END/p" "$LOG" | sed '1d; $d' >"$FILE"
}

######################################################################
# BUILD,EXPECT: Rename output
# used-by: rename_output
rename() {
	local FROM=${1:?}; TO=${2:?}
	# shellcheck disable=SC2031
	debug "mv" $VERBOSE "$FROM" "$TO"
	info "Rename '$FROM' -> '$TO'"
	# shellcheck disable=SC2031
	verbose mv "$FROM" "$TO"
}

######################################################################
# EXPECT: Rename output
rename_output() {
	local FILE=${1:?}; shift
	local NAME=${1:?}; shift
	# shellcheck disable=SC2031
	[[ -f $FILE ]] || FILE="$LFCWDIR/$FILE$OUTPUTEXT"
	[[ $NAME =~ ^/ ]] || NAME="$LFCWDIR/$NAME"
	rename "$FILE" "$NAME"
}

######################################################################
# BUILD,EXPECT: Get version output
version_output() {
	local FILE=${1:?}; shift
	local CMD=${1:?}; shift
	# shellcheck disable=SC2031
	FILE="$LFCWDIR/$FILE$OUTPUTEXT"
	rm -f "$FILE"
	verbose "$CMD" --version "$@" >"$FILE"
	info "Writing to ${FILE##*LFCW/}"
}

######################################################################
# BUILD,CODE,EXPECT: Run sed script on file
# used-by: fix_ascii, snip
modify() {
	local FILE=${1:?}; shift
	debug "modify: sed -i $FILE -r -e $*"
	# shellcheck disable=SC2031
	[[ -f $FILE ]] || FILE="$LFCWDIR/$FILE$OUTPUTEXT"
	verbose sed -i "$FILE" -r -e "$@"
}

######################################################################
fix_ascii() {
	local FILE
	for FILE in "$@" ; do
		debug "fix_ascii: $FILE"
		# Remove garbage characters
		modify "$FILE" 's/\x1B\[[0-9;]*[a-zA-Z]//g; s/.\x08//g; s/
//g; s/ $//g;'
		# Remove first line if ^echo
		modify "$FILE" '1{/^echo/d}'
		# Remove first line if reset*
		modify "$FILE" '1,2{/^reset/d}'
		# Remove first line if blank
		modify "$FILE" '1{/^$/d}'
		# Remove multiple blank lines
		modify "$FILE" '/^$/N;/^\n$/D'
		# Add a newline to the last line if not already present
		# shellcheck disable=SC2016,SC1003
		modify "$FILE" '$a\'
	done
}

######################################################################
# CODE,EXPECT: Replace from BEGIN to END in FILE with $SNIP
snip() {
	local FILE=${1:?} BEGIN=${2:?} END=${3:?} OPTS=${4:-}
	modify "$FILE" "/$BEGIN/{$OPTS :a; N; /$END/!ba; N; a $SNIP\\n" -e "d;}"
}

######################################################################
# ARG:make: Run make on course Makefile
run_make() {
	# shellcheck disable=SC2031
	cd "$TOPDIR"
	verbose make ENGINE=$ENGINE "$@"
}

######################################################################
# CODE: ARG:materials: Build SOLUTIONS and RESOURCES
make_cm() {
	local BUILD="SOLUTIONS RESOURCES"
	info "  Building ${BUILD/ //}"
	# shellcheck disable=SC2086
	if run_make $BUILD \
	| sed -rn '/ error /p; /^doing/p; /FINISHED/p'
	then
		info "  ${BUILD/ //} successfully built"
	else
		# shellcheck disable=SC2031
		tail -n 20 "$COURSE.log" >&2
	fi
}

######################################################################
# Show PDF with $PDFVIEWER (or evince)
# used-by: make_pdf
show_pdf() {
	nohup "${PDFVIEWER:-evince}" "$@" 2>/dev/null &
}

######################################################################
# ARG:check_pdf: Check for non-expanded LaTeX codes
# used-by: make_pdf
check_pdf() {
	if pdfgrep -C 2 '[^"]\\[a-mo-z][a-z]' "$@" ; then
		error "Probable LaTeX found in PDF"
	fi
	info "Passed simple PDF checks:" "$@"
}

######################################################################
# ARG:pdf: Build the PDF (prettier output)
# shellcheck disable=SC2031
make_pdf() {
	local RQ=${1:-} TARGET="class" PDF PDFOUT
	cd "$TOPDIR"

	# Lets figure out what it is we're doing
	if [[ $# -gt 0 ]]  ; then
		if [[ $RQ = both ]] ; then
			TARGET="class slides"
			shift
		elif [[ $RQ = help ]] ; then
			TARGET="help"
			shift
		elif [[ $RQ =~ slides ]] ; then
			TARGET="slides"
			PDF="$COURSE-SLIDES.pdf"
			shift
		fi
	fi
	TARGET="${TARGET:-class}"
	PDF="${PDF:-$COURSE.pdf}"

	if [[ $# -gt 0 ]] ; then
		info "pdfout $RQ"
		PDFOUT=${1:?}
		shift
		if [[ ! $PDFOUT =~ \.pdf$ ]] ; then
			PDFOUT+=".pdf"
		fi
	else
		PDFOUT="$PDF"
	fi
	debug "make_pdf: $# RQ=$RQ TARGET=$TARGET PDF=$PDF PDFOUT=$PDFOUT"

	# Show the PDF in question instead of (re)building
	if [[ $RQ = cheat ]] ; then
		show_pdf "$COURSES/COURSEWARE/LF_CHEATSHEET/LF_cheatsheet.pdf"
		exit 0
	elif [[ $RQ = help ]] ; then
		show_pdf "$COURSES/COURSEWARE/LF_COURSEWARE/LF_courseware.pdf"
		exit 0
	elif [[ -n $SHOW ]] ; then
		show_pdf "${PDFOUT:-$PDF}"
		exit 0
	fi

	info "Building $TARGET saved to ${PDFOUT:-$PDF}"

	# Build unless we're only doing a check_pdf
	if [[ -z $CHECK ]] ; then
		#verbose make clean
		touch "$COURSE.tex"
		info "Building PDF"
		if run_make -j"$(nproc)" $TARGET "$@" \
			| tee "$COURSE-lfcw.log" \
			| sed -rn '/ error /p; s|^.*?\((\./[./A-Za-z0-9_-]+\.tex).*$|\1|p; s|^.*?\((\./[./A-Za-z0-9_-]+\.inc).*$|\1|p'
		then
			info "  PDF successfully built"
			rm -f "${COURSE:?}-lfcw.log"
			find_overfull
			if [[ $PDFOUT != "$PDF" ]] ; then
				cp -v "$PDF" "$PDFOUT"
				if [[ $RQ = both ]] ; then
					cp -v "${PDF/.pdf/-SLIDES.pdf}" "${PDFOUT/.pdf/-slides.pdf}"
				fi
			fi
		else
			tail -n 40 "$COURSE-lfcw.log" >&2
		fi
	fi

	# Check for non-expanded LaTeX codes
	check_pdf "$PDF"
}

######################################################################
# add-index: Add \index{} to files by pattern
# used-by: index_latex
add_index() {
	local IDX=${1:?} PAT=${2:-$1}

	PAT="$(sed -re 's/([._])/\\\1/g' <<<"$PAT")"

	info "add_index: IDX=$IDX PAT=$PAT"

	# shellcheck disable=SC2119
	VINFO="$(mk_tmpfile)"
	cat <<END >"$VINFO"
# This viminfo file was generated by Vim 8.0.
# You may edit it if you're careful!

# Viminfo version
|1,4

# Value of 'encoding' when this file was written
*encoding=utf-8


# hlsearch on (H) or off (h):
~h
# Command Line History (newest to oldest):

# Search String History (newest to oldest):

# Expression History (newest to oldest):

# Input Line History (newest to oldest):

# Debug Line History (newest to oldest):

# Registers:
""1	LINE	0
	\\index{$IDX}
|3,1,1,1,1,0,1579733633,"\\\\index{$IDX}"

# File marks:

# Jumplist (newest first):

# History of marks within files (newest to oldest):
END

	#cat "$VINFO"
	EDIT=y
	find_latex "$PAT"
	#cat "$VINFO"
	exit 0
	rm -f "${VINFO:?}"
	VINFO=""
}

######################################################################
# ARG:add-index: Find where latex tags are used
index_latex() {
	local FILE=${1:-} PAT=${2:-}

	if [[ -f $FILE ]] ; then
		local IDX REST
		while IFS=, read -r IDX REST ; do
			add_index "$IDX"
		done <"$FILE"
	else
		add_index "$FILE" "$PAT"
	fi
}

######################################################################
# ARG:count-latex: Find where latex index tags are used
count_latex() {
	local DIR CH=0

	for DIR in $(get_all_chapters) ; do
		CH=$(( CH + 1 ))
		# shellcheck disable=SC2002,SC2046
		printf "%2d %s: %d\n" "$CH" "$(basename "$DIR")" "$(cat $(get_tex_files "$DIR" ALL) | grep -c '\\index')"
	done
}

######################################################################
# ARG:find-latex: Find where latex tags are used
# used-by: check_pointout, find_latex
edit_latex() {
	local PAT=${1:-}
	shift
	# shellcheck disable=SC2086
	VICMD="silent! /$PAT" edit "$@"
}

######################################################################
# ARG:find-latex: Find where latex tags are used
# used-by: add_index
find_latex() {
	local PAT=${1:-} FILES
	FILES="$(get_tex_files | xargs grep -l -E "$PAT" || true)"

	if [[ -z $FILES ]] ; then
		warn "$PAT not found"
	else
		if [[ -n $EDIT ]] ; then
			# shellcheck disable=SC2086
			edit_latex "$PAT" $FILES
		else
			echo "$FILES" | ${PAGER:-less -R}
		fi
	fi
}

######################################################################
# ARG:sizes: List LaTeX font sizes
fontsizes_latex() {
	cat <<FONTEND
\\Huge
\\huge
\\LARGE
\\Large
\\large
\\normalsize
\\small
\\footnotesize
\\scriptsize
\\tiny
FONTEND
}

######################################################################
# ARG:tags: Find begin latex tags to see what is used
tags_latex() {
	get_tex_files "$@" | xargs grep '\\begin{' \
		| sed -re 's/^.*\\begin\{//; s/}.*$//' \
		| grep -v -E '^(enumerate|exe|figure|itemize|Lab|minipage|sampage)$' \
		| sort -u
}

######################################################################
# used-by: latest
latest_ftp() {
	local URL=${1:?} CACHEFILE="${2:?}.html" PATTERN=${3:-} EXTENSION=${4:-bz2$|xz$|zip$} CODE=${5:-}
	debug "latest_ftp: URL=$URL CACHEFILE=$CACHEFILE PAT=$PATTERN EXT=$EXTENSION"

	if [[ ! -s $CACHEFILE ]] ; then
		wget -q "$URL" -O "$CACHEFILE" || \
		(local ERR=$?
		rm -f "${CACHEFILE:?}"
		if [[ $ERR -eq 4 ]] ; then
			error "wget: Network error: $URL"
		elif [[ $ERR -eq 8 ]] ; then
			error "wget: Server error: $URL"
		fi)
		if file "$CACHEFILE" | grep -q gzip ; then
			mv -i "$CACHEFILE" "$CACHEFILE.gz"
			gunzip "$CACHEFILE.gz"
		fi
	fi

	if [[ -z $CODE ]] ; then
		CODE='s/^.*[a-z]-([0-9.-]+)/\1/;'
	else
		CODE='s/^.*-([a-z]*-[0-9.-]+)/\1/;'
	fi

	# shellcheck disable=SC2102
	sed -e 's/></>\n</g' "$CACHEFILE" \
		| grep "href=" \
		| sed -re "s/^.*href=[\'\"]([^\'\'\"]*)[\'\"].*$/\\1/" \
		| grep -v 'rc[0-9]*' \
		| grep -E "${PATTERN:-.}" \
		| grep -E "$EXTENSION" \
		| sed -re "$CODE" \
		| sed -re 's/-[a-z].*$//; s/(\.tar)*\.bz2//; s/(\.tar)*\.xz$//; s/-[0-9]gb.img//; s|/$||; s|^.*\/||;' \
		| grep -E '[0-9.]+' \
		| sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n \
		| tail -1
}

######################################################################
# used-by: latest
latest_git() {
	local URL=${1:?} CACHEFILE="${2:?}.git"
	debug "latest_git: URL=$URL CACHEFILE=$CACHEFILE"

	if [[ ! -s $CACHEFILE ]] ; then
		git ls-remote "$URL" >"$CACHEFILE"
	fi
	awk '/[0-9]$/ {print $2}' "$CACHEFILE" \
		| sed -re 's/^.*-v//' \
		| sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n \
		| tail -1
}

######################################################################
# used-by: latest
latest() {
	local LABEL=${1:?} URL=${2:?} VERSION=${3:-} PATTERN=${4:-} PROTO=${5:-} EXTENSION=${6:-} REMOVE=${7:-} CODENAME=${8:-} CACHEFILE VERFILE VER NEW
	debug "latest: LABEL=$LABEL URL=$URL VERSION=$VERSION PAT=$PATTERN EXT=$EXTENSION"

	[[ -n $VERSION && $VERSION != "-" ]] || return 0

	# shellcheck disable=SC2086
	CACHEFILE="$CACHEDIR/$(echo $URL | md5sum | cut -d\  -f1)"
	VERFILE="$CACHEDIR/${LABEL// /_}-version.txt"

	mkdir -p "$CACHEDIR"
	find "$CACHEDIR" -type f -mmin +360 -print0 | xargs -0 --no-run-if-empty rm -f

	if [[ ! -s $VERFILE ]] ; then
		case "${PROTO:-$URL}" in
			ftp:*|http:*|https:*) latest_ftp "$URL" "$CACHEFILE" "$PATTERN" "$EXTENSION" "$CODENAME" >"$VERFILE" ;;
			git:*) latest_git "$URL" "$CACHEFILE" >"$VERFILE" ;;
		esac
	fi
	VER=$(cat "$VERFILE")
	if [[ -n $REMOVE ]] ; then
		#VER="$(sed -E "s/$REMOVE//" <<<"$VER")"
		VER="${VER/$REMOVE/}"
	fi

	if [[ -n $VERSION && $VER != "$VERSION" ]] ; then
		NEW="***"
	fi

	echo "$LABEL-$VER ${VERSION:+($VERSION)}${NEW:+ $NEW}"
}

######################################################################
# ARG:latest: Get latest versions of code bases
latest_all() {
	if [[ -n ${NOCACHE:-} ]] ; then
		rm -f "${CACHEDIR:?}/"*
	fi

	latest BeagleBone "$BIURL/" "$BOARDDATE" "$BOARDPREFIX.*$BOARDTYPE"
	latest BusyBox "$BBURL/" "$BBVERSION"
	latest Buildroot "$BRURL/" "$BRVERSION"
	latest Crosstool-ng "$CTNGURL/" "$CTVERSION" "" git:
	latest Etcher "$ETCHERURL/" "$SDBURNVERSION" "$SDBURNPREFIX"
	latest Ftrace "$FTURL/" "$FTVERSION"
	latest Kernel "$KERNELURL/" "$KVERSION"
	latest Linaro-gcc "$LGCCURL/" "$GCCREL"
	latest QEMU "$QEMUURL/" "$QEMUVERSION"
	latest Mtd-utils "$MTDURL/" "$MTDVERSION"
	latest U-boot "$UBOOTURL/" "$UBVERSION"
	latest OpenSBI "$SBIURL/releases/" "$SBIVERSION"
	latest YoctoProject "$YPURL/" "$YPVERSION" "" "" .
	latest Poky "$YPURL/yocto-$YPVERSION" "$YPCODENAME-$POKYVERSION" "poky-[a-z]{4,}-[0-9]" "" "tar.bz2$" "^poky-" codename
	latest BitBake "$BITBAKEURL/" "$BITBAKEVERSION" "releases/tag/[0-9]*\." "" .
	latest Nano "$NANOURL/" "$NANOVERSION" "/dist"
}

######################################################################
# ARG:dump: Dump the script environment variables
dump_config() {
	diffenv
}

######################################################################
# ARG:cheat:
cheat_sheet() {
	info "Use lfcw in the following order:"
	cat <<ENDSHEET
$TOOL latest        Check for latest version of sources
$TOOL fetch         Download source
$TOOL build         Build source into course assets
$TOOL config        Generate latex of config options
$TOOL code          Copy code into latex
$TOOL capture       Capture screenshots and webpages
$TOOL tftp          Install TFTP/NFSroot for testing
$TOOL sdcard        Install SD card for testing
$TOOL test          Run lab tests
$TOOL expect        Generate output files for latex
$TOOL check         Install assets into labs
$TOOL pdf           Build PDF file
$TOOL mv manifest   Move assets into RESOURCES archive
$TOOL labs          Install assets into labs
$TOOL prefinalize   Run pre-final checks
$TOOL finalize      Run all the final checks and upload
$TOOL change        Check files are in git
ENDSHEET
}

######################################################################
usage() {
	local ERRMSG=${1:-}
	set +x
	[[ -z $ERRMSG ]] || echo "$*"
	cat <<ENDHELP
Usage: $TOOL <options> <cmd> <arguments>
       -b|--board <board>     Indicate the board you want to use (WIP)
       -c|--course <course>   Indicate the course to use
       --delete               Delete identical files when moving to RESOURCES
       -n|--dry-run           Show what would be done
       -f|--file <FILE>       Read extra options from file
       --fetch                Only fetch code when doing a build
       -h|--help              This help
       --menuconfig           Run menuconfig during the config step
       --nocache              Don't use cached values (redownload)
       -q|--quiet             No output
       --show                 Run pdf viewer (see below)
       -v|--verbose           More output
       -V|--Version           The version of this script

    General commands:
       $TOOL add-index <index|file> [pattern]  Add \\index for pattern in files
       $TOOL all                               Do all the steps automatically
       $TOOL build [dir]                       Build lab assets for course
       $TOOL capture [dir]                     Capture screenshots and webpages
       $TOOL chapters [clean]                  Create chapter symlinks
       $TOOL change                            Use git to see what has changed per chapter
       $TOOL cheat                             List of steps to follow (and rough order)
       $TOOL check [course]                    Check for gross errors (like old output files)
       $TOOL check-file-links                  Check file links for errors
       $TOOL check-kernel-links                Check kernel links for errors
       $TOOL check-pdf <file>                  Check PDF for gross errors
       $TOOL check-urls                        Check URLs for errors
       $TOOL clean <all|manifest>              Run make clean for all code bases
       $TOOL code [dir]                        Copy code into latex (or generate files)
       $TOOL count                             Count index entries per chapter
       $TOOL compare <dir> <dir|files>         Compare files between similar chapters
       $TOOL config [dir]                      Generate latex from config options
       $TOOL diff [files]                      Diff files with those in RESOURCES
       $TOOL dump                              Dump configuration values
       $TOOL expect [dir]                      Run expect scripts on target to generate output files
       $TOOL fetch [dir]                       Fetch all the relevant code
       $TOOL finalize                          Finalize course and upload files
       $TOOL files [file]                      List files for the course
       $TOOL find <pattern>                    Finalize course and upload files
       $TOOL git <cmd> [dir]                   Run git commands on each chapter
       $TOOL info                              Print out info for the course
       $TOOL kconfig <dir> <file> [prefix]     Read metadata from KConfig files
       $TOOL labs [dir]                        Install lab assets from build
       $TOOL latest                            Find latest versions
       $TOOL links [clean]                     Create standard course symlinked files
       $TOOL list <options>                    List lfcw files for course
       $TOOL ls <options>                      List working directory $WORKDIR
       $TOOL lschapters [course]               List chapters in course
       $TOOL make <target>                     Run make with course Makefile
       $TOOL makelfcw LFCW <options>           Run makelfcw on .lfcw files
       $TOOL materials                         Run "make SOLUTIONS RESOURCES"
       $TOOL mv -c <course> [files]            Move tarfiles to RESOURCES
       $TOOL pdf [both|course|slides]          Build PDFs for course
       $TOOL --show pdf [course|slides]        View PDF or slides for course
       $TOOL --show pdf [cheat|help]           View cheat or COURSEWARE pdf
       $TOOL prefinalize                       Pre-finalize course tests
       $TOOL sdcard [dir]                      Copy files onto SD card
       $TOOL spellcheck [dir]                  Check spelling on dir or course
       $TOOL tags [dir|file]                   List latex tags used in file or dir
       $TOOL test [dir]                        Run tests on labs
       $TOOL tftp [dir]                        Copy files into TFTP/NFSroot dirs
       $TOOL update-links                      Update symlinks from RESOURCES dir
       $TOOL versions                          List all the software versions
       $TOOL whitespace [--fix] [dir]          Find and fix whitespace errors

    Build specific things:
       $TOOL bb|busybox -c <course> <bbversion>           Build busybox
       $TOOL br|buildroot -c <course> <brversion> [name]  Build buildroot
              Example: $TOOL br 2016.08.1 uclibc
       $TOOL crosstool -c <course> <ctversion> [name]     Build crosstool (broken)
              Example: $TOOL ct 2016.08.1 uclibc
       $TOOL ftrace <ftversion>                           Build ftrace
       $TOOL kernel <kversion> [name]                     Build linux kernel
       $TOOL mr|mkroot-tar -c <course> <kver> <brver> [name]
              Example: $TOOL mr 4.9.3 2016.11.1           Make rootfs tar file with kernel
       $TOOL mtd <mtdversion>                             Build mtd-utils
       $TOOL opensbi <sbiversion>                         Build OpenSBI
       $TOOL rootfs -c <course> <brver> <name>            Create rootfs tarball link
       $TOOL squashfs -c <course>                         Build squashfs image
       $TOOL tk|tar-kernel <board> [shortname]            Tar up built kernel
       $TOOL uboot <ubversion>                            Build U-boot
ENDHELP
	exit 0
}

######################################################################
export RC="$HOME/.config/lfcw/lfcw.conf $HOME/.lfcwrc"
for CONF in $RC ; do
	if [[ -f $CONF ]] ; then
		RC=$CONF
		# shellcheck disable=SC1090
		. "$CONF"
	fi
done

######################################################################
# shellcheck disable=SC2031
export COURSES="${COURSES:-$HOME/src/lf/LFCW}"
export COMMON="$COURSES/common"
export LATEX="$COMMON/texmf/tex/latex"
export LFCLS="$LATEX/LFCW/LFD.cls"
export LFBOX="$LATEX/LFCW/lf-boxes.tex"
export LFLST="$LATEX/LFCW/lf-listings.tex"
export IMAGES="$COURSES/IMAGES"
export RESOURCES="${RESOURCES:-$COURSES/RESOURCES}"
export WORK="${WORK:-$HOME/work}"
export KERNELSRC="${KERNELSRC:-$HOME/linux-stable}"
export KERNELCOUNT="${KERNELCOUNT:-$KERNELSRC-count}"
export KERNELGRAPHS="${KERNELGRAPHS:-$KERNELCOUNT-tmp}"
export SNIP="${SNIP:-<<< SNIP >>>}"

######################################################################
ARGS=()
while [[ $# -gt 0 ]] ; do
	ARG="$1"; shift
	# shellcheck disable=SC1090,SC2031
	case "$ARG" in
		-1|--one) ONECOL=y;;
		-a|--all) ALL=y;;
		-b|--board) check_board "${1:?}"; shift;;
		-c|--course) check_course "${1:?}"; shift;;
		--check) CHECK=y;;
		-D|--debug) DEBUG_CMD=y ;;
		--debug-tex) export DEBUG_TEX=y ;;
		--debug-all) export DEBUG=y ;;
		--delete) DELETE=y ;;
		-n|--dry-run|--test) export TEST=echo ;;
		-e|--edit) export EDIT=y ;;
		-f|--file) . "${1:?}" || error "$1: Not found"; shift;;
		--fetch) FETCH_ONLY=y;;
		--fix) FIX=y;;
		--force) FORCE=y ;;
		--menuconfig) MENUCONFIG=y;;
		--nocache) NOCACHE=y;;
		--nocolor) nocolor;;
		--luatex|--pdftex|--xetex) ENGINE="${ARG#--}";;
		-q|--quiet) QUIET=y; export VERBOSE="" ;;
		-s|--show) SHOW=y;;
		size*) fontsizes_latex; exit 0;;
		--trace-all) TRACE=y; set -x ;;
		--trace) TRACE=y;;
		-v|--verbose) export VERBOSE=-v; MOREOUTPUT=y ;;
		-V|--version) echo "$VERSION"; exit 0;;
		-h*|--h*) usage ;;
		*.*) ARGS+=( "$ARG" );;
		LF*) check_course "$ARG" ;;
		*) ARGS+=( "$ARG" );;
	esac
done
[[ ${#ARGS[@]} -gt 0 ]] || usage

######################################################################
# shellcheck disable=SC2031
if [[ -z $COURSE ]] ; then
	COURSE=$(sed -re 's|^.*(LF[CDSW][0-9]{3,4}).*|\1|' <<<"$(pwd)")
	check_course "$COURSE"
	info "Assuming $COURSE"
fi
debug "COURSE=$COURSE TEX=$TEXFILE WORKDIR=$WORKDIR"

######################################################################
if [[ $TEST ]] ; then
	EXPECT="$(command -v expect)"
	eval "expect() { cat -v; verbose \"$EXPECT\" \"\$@\"; }"
fi

######################################################################
debug "commands:" "${ARGS[@]}"
ARG="${ARGS[0]}";
unset "ARGS[0]"
[[ -z $DEBUG_CMD ]] || DEBUG="y"
[[ -z $TRACE ]] || set -x
case "$ARG" in
	a2r|abs2rel) abs2rel "${ARGS[@]}";;
	ai|add-index) index_latex "${ARGS[@]}";;
	all) make_all "${ARGS[@]}";;
	bb|busybox) build_busybox "${ARGS[@]}";;
	bc|build-config) makelfcw build "${ARGS[@]}"; makelfcw config "${ARGS[@]}";;
	br|buildroot) build_buildroot "${ARGS[@]}";;
	bu|build) makelfcw build "${ARGS[@]}";;
	ca|capture) makelfcw capture "${ARGS[@]}";;
	ch|chapters) link_chapters "$COURSE" "${ARGS[@]}";;
	change) git_change "$COURSE" "${ARGS[@]}";;
	cheat) cheat_sheet;;
	check-all|check) check_all "${ARGS[@]}";;
	cf|check-file*) check_file_links "${ARGS[@]}";;
	ck|check-kernel*) check_kernel_links "${ARGS[@]}";;
	co|check-output) check_output "${ARGS[@]}";;
	cp|check-pointout) check_pointout "${ARGS[@]}";;
	cpdf|check-pdf) check_pdf "$COURSE.pdf" "${ARGS[@]}";;
	csol|check-solutions) check_solutions "${ARGS[@]}";;
	csym|check-symlinks) check_symlinks "${ARGS[@]}";;
	cu|check-urls) check_urls "${ARGS[@]}";;
	cl|clean) clean_files "${ARGS[@]}";;
	code) makelfcw code "${ARGS[@]}";;
	config) export CONFIG_GENERATE_TEX=y; makelfcw config "${ARGS[@]}";;
	count|count-latex) count_latex "${ARGS[@]}";;
	cmp|compare) compare_modules "${ARGS[@]}";;
	ct|crosstool*) build_crosstool "${ARGS[@]}";;
	diff) diff_files "${ARGS[@]}";;
	dump) dump_config;;
	ex|expect) clearstamp_pattern 'expect-*'; makelfcw expect "${ARGS[@]}";;
	fetch) FETCH_ONLY=y; makelfcw build "${ARGS[@]}";;
	"fi"|finalize) finalize_course "${ARGS[@]}";;
	files) get_tex_files "${ARGS[@]}";;
	fl|find|find-latex) find_latex "${ARGS[@]}";;
	ft|ftrace) build_ftrace "${ARGS[@]}";;
	git) git_cmds "${ARGS[@]}";;
	info) echo "RESOURCES=$RESOURCES"; INFO=y; read_course "$COURSE"; check_board;;
	kconfig) walk_kconfig "${ARGS[@]}";;
	kern*) build_kernel "${ARGS[@]}";;
	la|labs) makelfcw labs "${ARGS[@]}";;
	latest) latest_all "${ARGS[@]}";;
	links) course_links "$COURSE" "${ARGS[@]}";;
	ll|listlabs) list_labs "${ARGS[@]}";;
	li|list) list_lfcw "${ARGS[@]}";;
	ls|lsc*) list_chapters "${ARGS[@]}";;
	lw|lsw*) list_workdir "${ARGS[@]}";;
	make) run_make "${ARGS[@]}";;
	macro*) get_macros "${ARGS[@]}";;
	ma|makelfcw) makelfcw "${ARGS[@]}";;
	cm|materials) make_cm "${ARGS[@]}";;
	mr|mkroot-tar) mkroot_tar "${ARGS[@]}";;
	mt|mtd) build_mtd "${ARGS[@]}";;
	mv|re|res*) mvresources "${ARGS[@]}";;
	of|overfull) find_overfull "${ARGS[@]}";;
	op|opensbi) build_opensbi "${ARGS[@]}";;
	pdf) make_pdf "${ARGS[@]}";;
	pre|prefinalize) pre_finalize_course "${ARGS[@]}";;
	qemu) build_qemu "${ARGS[@]}";;
	ro|rootfs) rootfs_link "${ARGS[@]}";;
	sd|sdcard) makelfcw sdcard "${ARGS[@]}"; eject_card;;
	sp|spellcheck) check_spelling "${ARGS[@]}";;
	sq|squashfs) squashfs_root "${ARGS[@]}";;
	ta|tags) tags_latex "${ARGS[@]}";;
	test1) get_tex_meta "${ARGS[@]}";;
	te|test) makelfcw test "${ARGS[@]}";;
	tf|tftp) makelfcw tftp "${ARGS[@]}";;
	tk|tar-kernel) tar_kernel "${ARGS[@]}";;
	tar|tarball*) tar_files "${ARGS[@]}";;
	uboot) build_uboot "${ARGS[@]}";;
	ul|update|update-links) update_links "${ARGS[@]}";;
	up|upload*) upload_files "${ARGS[@]}";;
	versions) DEBUG=1 check_course "$COURSE";;
	ws|whitespace) check_whitespace "${ARGS[@]}";;
	*) usage ;;
esac

# PDF: aspell make optipng pv python3-pygments swig texlive-latex-extra
# Make assets: bison build-essential expect fakeroot flex git gnuplot graphviz imagemagick optipng quilt sloccount symlinks wkhtmltox
