#!/usr/bin/env bash
# kaleidoscope-builder - Kaleidoscope helper tool
# Copyright (C) 2017-2018 Keyboard.io, Inc.
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, version 3.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see .
set -e
######
###### Build and output configuration
######
build_version () {
GIT_VERSION="$(cd "$(find_sketch)"; if [ -d .git ]; then echo '-g' && git describe --abbrev=4 --dirty --always; fi)"
LIB_PROPERTIES_PATH="${LIB_PROPERTIES_PATH:-"../.."}"
LIB_VERSION="$(cd "$(find_sketch)"; (grep version= "${LIB_PROPERTIES_PATH}/library.properties" 2>/dev/null || echo version=0.0.0) | cut -d= -f2)${GIT_VERSION}"
}
build_paths() {
# We don't really want to use find
# shellcheck disable=SC2012
SKETCH_IDENTIFIER="$(ls -id "$(find_sketch)/${SKETCH}.ino" | cut -d ' ' -f 1)-${SKETCH}.ino"
KALEIDOSCOPE_TEMP_PATH="${KALEIDOSCOPE_TEMP_PATH:-${TMPDIR:-/tmp}/kaleidoscope-${USER}}"
KALEIDOSCOPE_BUILD_PATH="${KALEIDOSCOPE_BUILD_PATH:-${KALEIDOSCOPE_TEMP_PATH}/sketch}"
KALEIDOSCOPE_OUTPUT_PATH="${KALEIDOSCOPE_OUTPUT_PATH:-${KALEIDOSCOPE_TEMP_PATH}/sketch}"
SKETCH_OUTPUT_DIR="${SKETCH_OUTPUT_DIR:-${SKETCH_IDENTIFIER}/output}"
SKETCH_BUILD_DIR="${SKETCH_BUILD_DIR:-${SKETCH_IDENTIFIER}/build}"
BUILD_PATH="${BUILD_PATH:-${KALEIDOSCOPE_BUILD_PATH}/${SKETCH_BUILD_DIR}}"
OUTPUT_PATH="${OUTPUT_PATH:-${KALEIDOSCOPE_OUTPUT_PATH}/${SKETCH_OUTPUT_DIR}}"
CCACHE_WRAPPER_PATH="${CCACHE_WRAPPER_PATH:-${KALEIDOSCOPE_TEMP_PATH}/ccache/bin}"
CORE_CACHE_PATH="${CORE_CACHE_PATH:-${KALEIDOSCOPE_TEMP_PATH}/arduino-cores}"
mkdir -p "$CORE_CACHE_PATH"
mkdir -p "$BUILD_PATH"
}
build_filenames () {
OUTPUT_FILE_PREFIX="${SKETCH}-${LIB_VERSION}"
HEX_FILE_PATH="${OUTPUT_PATH}/${OUTPUT_FILE_PREFIX}.hex"
HEX_FILE_WITH_BOOTLOADER_PATH="${OUTPUT_PATH}/${OUTPUT_FILE_PREFIX}-with-bootloader.hex"
ELF_FILE_PATH="${OUTPUT_PATH}/${OUTPUT_FILE_PREFIX}.elf"
}
enable_ccache () {
if [ "$(command -v ccache)" ]; then
if ! [ -d "$CCACHE_WRAPPER_PATH" ]; then
mkdir -p "$CCACHE_WRAPPER_PATH"
fi
if ! [ -h "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}gcc" ]; then
ln -s "$(command -v ccache)" "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}gcc"
fi
if ! [ -h "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}g++" ]; then
ln -s "$(command -v ccache)" "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}g++"
fi
if ! [ -h "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}nm" ]; then
ln -s "${AVR_NM}" "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}nm"
fi
if ! [ -h "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}objcopy" ]; then
ln -s "${AVR_OBJCOPY}" "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}objcopy"
fi
if ! [ -h "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}ar" ]; then
ln -s "${AVR_AR}" "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}ar"
fi
if ! [ -h "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}size" ]; then
ln -s "${AVR_SIZE}" "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}size"
fi
export CCACHE_PATH=${COMPILER_PATH}/
CCACHE_ENABLE="-prefs compiler.path=${CCACHE_WRAPPER_PATH}/"
fi
}
firmware_size () {
if [ "${ARCH}" = "virtual" ]; then
echo "[Size not computed for virtual build]"
return
fi
## This is a terrible hack, please don't hurt me. - algernon
find_max_prog_size
set +e
raw_output=$("$@" 2> /dev/null)
rc=$?
set -e
if [ $rc -eq 0 ]; then
output="$(echo "${raw_output}"| grep "\\(Program\\|Data\\):" | sed -e 's,^, - ,' && echo)"
PROGSIZE="$(echo "${output}" | grep "Program:" | cut -d: -f2 | awk '{print $1}')"
PERCENT="$(echo "${PROGSIZE}" "${MAX_PROG_SIZE}" | awk "{ printf \"%02.01f\", \$1 / \$2 * 100 }")"
# we want the sed there, doing with shell builtins would be worse.
# shellcheck disable=SC2001 disable=SC1117
echo "${output}" | sed -e "s/\(Program:.*\)(\([0-9\.]*%\) Full)/\1(${PERCENT}% Full)/"
else
echo "Unable to determine image size."
fi
}
find_sketch () {
SKETCH="${SKETCH:-${DEFAULT_SKETCH}}"
LIBRARY="${LIBRARY:-${SKETCH}}"
if [ -z "${SKETCH}" ] || [ -z "${LIBRARY}" ] || [ -z "${ROOT}" ] || [ -z "${SOURCEDIR}" ]; then
echo "SKETCH, LIBRARY, SOURCEDIR, and ROOT need to be set before including this file!" >&2
exit 1
fi
for path in "examples/${LIBRARY}" \
"src" \
"."; do
if [ -f "${path}/${SKETCH}.ino" ]; then
echo "${path}"
return
fi
done
echo "I couldn't find your sketch (.ino file)" >&2
exit 1
}
prepare_to_flash () {
if [ ! -e "${HEX_FILE_PATH}" ]; then
compile
size
fi
echo "To update your keyboard's firmware, hold down the 'Prog' key on your keyboard,"
echo "and then press 'Enter'."
echo ""
echo "When the 'Prog' key glows red, you can release it."
echo ""
# We do not want to permit line continuations here. We just want a newline.
# shellcheck disable=SC2162
read
}
flash () {
prepare_to_flash
# This is defined in the (optional) user config.
# shellcheck disable=SC2154
${preFlash_HOOKS}
reset_device
sleep 3
find_bootloader_ports
check_bootloader_port_and_flash
# This is defined in the (optional) user config.
# shellcheck disable=SC2154
${postFlash_HOOKS}
}
check_bootloader_port_and_flash () {
if [ -z "${DEVICE_PORT_BOOTLOADER}" ]; then
echo "Unable to detect a keyboard in bootloader mode. You may need to hold the 'Prog' key or hit a reset button"
return 1
fi
flash_over_usb || flash_over_usb
}
flash_over_usb () {
sleep 1
${AVRDUDE} -q -q -C "${AVRDUDE_CONF}" -p"${MCU}" -cavr109 -D -P "${DEVICE_PORT_BOOTLOADER}" -b57600 "-Uflash:w:${HEX_FILE_PATH}:i"
}
flash_from_bootloader() {
prepare_to_flash
find_bootloader_ports
check_bootloader_port_and_flash
}
program() {
prepare_to_flash
flash_with_programmer
}
flash_with_programmer() {
${AVRDUDE} -v \
-C "${AVRDUDE_CONF}" \
-p"${MCU}" \
-cusbtiny \
-D \
-B 1 \
"-Uflash:w:${HEX_FILE_PATH}:i"
}
hex_with_bootloader () {
if [ ! -e "${HEX_FILE_PATH}" ]; then
compile
fi
awk '/^:00000001FF/ == 0' "${HEX_FILE_PATH}" > "${HEX_FILE_WITH_BOOTLOADER_PATH}"
echo "Using ${BOOTLOADER_PATH}"
${MD5} "${BOOTLOADER_PATH}"
cat "${BOOTLOADER_PATH}" >> "${HEX_FILE_WITH_BOOTLOADER_PATH}"
ln -sf -- "${HEX_FILE_WITH_BOOTLOADER_PATH}" "${OUTPUT_PATH}/${SKETCH}-latest-with-bootloader.hex"
cat <<- EOF
Combined firmware and bootloader are now at ${HEX_FILE_WITH_BOOTLOADER_PATH}
Make sure you have the bootloader version you expect.
And TEST THIS ON REAL HARDWARE BEFORE YOU GIVE IT TO ANYONE
EOF
}
build () {
compile "$@"
size "$@"
}
compile () {
build_version
build_paths
build_filenames
enable_ccache
install -d "${OUTPUT_PATH}"
SKETCH_DIR="$(find_sketch)"
echo "Building ${SKETCH_DIR}/${SKETCH} ${LIB_VERSION} into ${OUTPUT_PATH}..."
# This is defined in the (optional) user config.
# shellcheck disable=SC2154
${compile_HOOKS}
if [ -d "${ARDUINO_LOCAL_LIB_PATH}/libraries" ]; then
# shellcheck disable=SC2089
# We want literal backslashes here, not arrays.
local_LIBS="-libraries \"${ARDUINO_LOCAL_LIB_PATH}/libraries\""
fi
ARDUINO_PACKAGES=""
if [ -d "${ARDUINO_PACKAGE_PATH}" ]; then
# shellcheck disable=SC2089
# We want literal backslashes here, not arrays.
ARDUINO_PACKAGES="-hardware \"${ARDUINO_PACKAGE_PATH}\""
fi
SAVED_BOARD="${BOARD}"
SAVED_FQBN="${FQBN}"
if [ -e "${SKETCH_DIR}/.kaleidoscope-builder.conf" ]; then
# shellcheck disable=SC1090
BOARD="$(. "${SKETCH_DIR}"/.kaleidoscope-builder.conf && echo "${BOARD}")"
# shellcheck disable=SC1090
FQBN="$(. "${SKETCH_DIR}"/.kaleidoscope-builder.conf && echo "${FQBN}")"
if [ -n "${BOARD}" ]; then
if [ -z "${ARCH}" ]; then
FQBN="keyboardio:avr:${BOARD}"
else
FQBN="keyboardio:${ARCH}:${BOARD}"
fi
fi
fi
# SC2091: We do not care if quotes or backslashes are not respected.
# SC2086: We want word splitting.
# shellcheck disable=SC2086,SC2090
"${ARDUINO_BUILDER}" \
-compile \
${ARDUINO_PACKAGES} \
-hardware "${ARDUINO_PATH}/hardware" \
-hardware "${BOARD_HARDWARE_PATH}" \
${ARDUINO_TOOLS_FLAG:+"${ARDUINO_TOOLS_FLAG}"} ${ARDUINO_TOOLS_PARAM:+"${ARDUINO_TOOLS_PARAM}"} \
-tools "${ARDUINO_PATH}/tools-builder" \
-fqbn "${FQBN}" \
-libraries "." \
-libraries "${ROOT}" \
-libraries "${BOARD_HARDWARE_PATH}/.." \
${local_LIBS} \
${EXTRA_BUILDER_ARGS} \
-build-cache "${CORE_CACHE_PATH}" \
-build-path "${BUILD_PATH}" \
-ide-version "${ARDUINO_IDE_VERSION}" \
-prefs "compiler.cpp.extra_flags=-std=c++11 -Woverloaded-virtual -Wno-unused-parameter -Wno-unused-variable -Wno-ignored-qualifiers ${ARDUINO_CFLAGS} ${LOCAL_CFLAGS}" \
$CCACHE_ENABLE \
-warnings all \
${ARDUINO_VERBOSE} \
${ARDUINO_AVR_GCC_PREFIX_PARAM} \
"${SKETCH_DIR}/${SKETCH}.ino"
cp "${BUILD_PATH}/${SKETCH}.ino.hex" "${HEX_FILE_PATH}"
cp "${BUILD_PATH}/${SKETCH}.ino.elf" "${ELF_FILE_PATH}"
ln -sf "${OUTPUT_FILE_PREFIX}.hex" "${OUTPUT_PATH}/${SKETCH}-latest.hex"
ln -sf "${OUTPUT_FILE_PREFIX}.elf" "${OUTPUT_PATH}/${SKETCH}-latest.elf"
if [ "${ARDUINO_VERBOSE}" = "-verbose" ]; then
echo "Build artifacts can be found in ${BUILD_PATH}";
fi
BOARD="${SAVED_BOARD}"
FQBN="${SAVED_FQBN}"
}
_find_all () {
for plugin in ./*.ino \
$([ -d examples ] && find examples -name '*.ino') \
src/*.ino; do
if [ -d "$(dirname "${plugin}")" ] || [ -f "${plugin}" ]; then
p="$(basename "${plugin}" .ino)"
if [ "${p}" != '*' ]; then
case "${plugin}" in
examples/*/${p}/${p}.ino)
echo "${plugin}" | sed -e "s,examples/,," | sed -e "s,/${p}\\.ino,,"
;;
*)
echo "${p}"
;;
esac
fi
fi
done | sort
}
build_all () {
plugins="$(_find_all)"
for plugin in ${plugins}; do
export SKETCH="${plugin}"
export LIBRARY="${plugin}"
$0 "${plugin}" build
done
}
compile_all () {
plugins="$(_find_all)"
for plugin in ${plugins}; do
export SKETCH="${plugin}"
export LIBRARY="${plugin}"
$0 "${plugin}" compile
done
}
size () {
if [ ! -e "${HEX_FILE_PATH}" ]; then
compile
fi
echo "- Size: firmware/${LIBRARY}/${OUTPUT_FILE_PREFIX}.elf"
# shellcheck disable=SC2086
firmware_size "${AVR_SIZE}" ${AVR_SIZE_FLAGS} "${ELF_FILE_PATH}"
echo
}
size_map () {
if [ ! -e "${HEX_FILE_PATH}" ]; then
compile
fi
"${AVR_NM}" --size-sort -C -r -l -t decimal "${ELF_FILE_PATH}"
}
disassemble () {
if [ ! -e "${HEX_FILE_PATH}" ]; then
compile
fi
"${AVR_OBJDUMP}" -C -d "${ELF_FILE_PATH}"
}
decompile () {
disassemble
}
clean () {
rm -rf -- "${OUTPUT_PATH}"
}
reset_device() {
find_device_port
check_device_port
reset_device_cmd
}
check_device_port () {
if [ -z "$DEVICE_PORT" ]; then
cat <&2
I couldn't autodetect the keyboard's serial port.
If you see this message and your keyboard is connected to your computer,
it may mean that our serial port detection logic is buggy or incomplete.
In that case, please report this issue at:
https://github.com/keyboardio/Kaleidoscope
EOF
exit 1
elif echo "$DEVICE_PORT" | grep -q '[[:space:]]'; then
cat <&2
Unexpected whitespace found in detected serial port:
$DEVICE_PORT
If you see this message, it means that our serial port
detection logic is buggy or incomplete.
Please report this issue at:
https://github.com/keyboardio/Kaleidoscope
EOF
exit 1
fi
if ! [ -w "$DEVICE_PORT" ]; then
cat <&2
In order to update your keyboard's firmware you need to have permission
to write to its serial port $DEVICE_PORT.
It appears that you do not have this permission:
$(ls -l "$DEVICE_PORT")
This may be because you're not in the correct unix group:
$(stat -c %G "$DEVICE_PORT").
You are currently in the following groups:
$(id -Gn)
Please ensure you have followed the instructions on setting up your
account to be in the right group:
https://github.com/keyboardio/Kaleidoscope/wiki/Install-Arduino-support-on-Linux
EOF
exit 1
fi
}
usage () {
cat <<- EOF
Usage: $0 SKETCH commands...
Runs all of the commands in the context of the Sketch.
Available commands:
help
This help screen.
compile
Compiles the sketch.
size
Reports the size of the compiled sketch.
build
Runs compile and report-size.
clean
Cleans up the output directory.
size-map
Displays the size map for the sketch.
disassemble
Decompile the sketch.
reset-device
Reset the device.
flash
Flashes the firmware using avrdude.
build-all
Build all Sketches we can find.
EOF
}
help () {
usage
}
if [ $# -lt 1 ]; then
usage
exit 1
fi
## Parse the command-line
## - anything that has a =, is an env var
## - from the remaining stuff, the first one is the Library/Sketch
## - everything else are commands
##
## - if there is only one argument, that's a command
ROOT="$(cd "$(dirname "$0")"/..; pwd)"
export ROOT
# shellcheck disable=SC2155
export SOURCEDIR="$(pwd)"
if [ -e "${HOME}/.kaleidoscope-builder.conf" ]; then
# shellcheck disable=SC1090
. "${HOME}/.kaleidoscope-builder.conf"
fi
if [ -e "${SOURCEDIR}/.kaleidoscope-builder.conf" ]; then
# shellcheck disable=SC1090
. "${SOURCEDIR}/.kaleidoscope-builder.conf"
fi
if [ -e "${SOURCEDIR}/kaleidoscope-builder.conf" ]; then
# shellcheck disable=SC1090
. "${SOURCEDIR}/kaleidoscope-builder.conf"
fi
# shellcheck disable=SC1090
. "${ROOT}/etc/kaleidoscope-builder.conf"
if [ ! -z "${VERBOSE}" ] && [ "${VERBOSE}" -gt 0 ]; then
ARDUINO_VERBOSE="-verbose"
else
ARDUINO_VERBOSE="-quiet"
fi
cmds=""
## Export vars
for i in $(seq 1 $#); do
v="$1"
shift
case "${v}" in
*=*)
# Exporting an expansion is *precisely* what we want here.
# shellcheck disable=SC2086,SC2163
export ${v}
;;
*)
cmds="${cmds} ${v}"
;;
esac
done
# Word splitting is desired here.
# shellcheck disable=SC2086
set -- ${cmds}
if [ $# -eq 1 ]; then
cmd="$(echo "$1" | tr '-' '_')"
${cmd}
exit $?
fi
SKETCH="$1"
shift
if [ "${SKETCH}" = "default" ]; then
SKETCH="${DEFAULT_SKETCH}"
fi
cmds=""
# shellcheck disable=2034
for i in $(seq 1 $#); do
cmds="${cmds} $(echo "$1" | tr '-' '_')"
shift
done
LIBRARY="${SKETCH}"
case "${SKETCH}" in
*/*)
SKETCH="$(basename "${SKETCH}")"
;;
esac
export SKETCH
export LIBRARY
for cmd in ${cmds}; do
${cmd}
done