#!/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 <http://www.gnu.org/licenses/>. set -e ###### ###### Build and output configuration ###### absolute_filename() { echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" } build_version () { GIT_VERSION="$(cd "${SKETCH_DIR}"; if [ -d .git ]; then echo -n '-g' && git describe --abbrev=4 --dirty --always; fi)" LIB_PROPERTIES_PATH="${LIB_PROPERTIES_PATH:-"../.."}" LIB_VERSION="$(cd "${SKETCH_DIR}"; (grep version= "${LIB_PROPERTIES_PATH}/library.properties" 2>/dev/null || echo version=0.0.0) | cut -d= -f2)${GIT_VERSION}" } build_paths() { # We need that echo because we\re piping to cksum # shellcheck disable=SC2005 SKETCH_IDENTIFIER="$(echo "$(absolute_filename "${SKETCH_DIR}/${SKETCH}.ino")" | cksum | 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="${OUTPUT_FILE_PREFIX:-${SKETCH}-${LIB_VERSION}}" HEX_FILE_PATH="${HEX_FILE_PATH:-${OUTPUT_PATH}/${OUTPUT_FILE_PREFIX}.hex}" HEX_FILE_WITH_BOOTLOADER_PATH="${HEX_FILE_WITH_BOOTLOADER_PATH:-${OUTPUT_PATH}/${OUTPUT_FILE_PREFIX}-with-bootloader.hex}" ELF_FILE_PATH="${ELF_FILE_PATH:-${OUTPUT_PATH}/${OUTPUT_FILE_PREFIX}.elf}" LIB_FILE_PATH="${LIB_FILE_PATH:-${OUTPUT_PATH}/${OUTPUT_FILE_PREFIX}.a}" } enable_ccache () { if [ -z "${CCACHE_NOT_SUPPORTED}" ] && [ "$(command -v ccache)" ]; then if ! [ -d "$CCACHE_WRAPPER_PATH" ]; then mkdir -p "$CCACHE_WRAPPER_PATH" if ! [ -h "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}${C_COMPILER_BASENAME}" ]; then ln -s "$(command -v ccache)" "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}${C_COMPILER_BASENAME}" fi if ! [ -h "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}${CXX_COMPILER_BASENAME}" ]; then ln -s "$(command -v ccache)" "${CCACHE_WRAPPER_PATH}/${COMPILER_PREFIX}${CXX_COMPILER_BASENAME}" 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 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 SKETCH_DIR="$SKETCH" SKETCH_FILE=$(basename "$SKETCH") for path in "${SKETCH_DIR}" \ "examples/${LIBRARY}" \ "src" \ "."; do if [ -f "${path}/${SKETCH_FILE}.ino" ]; then SKETCH_DIR="${path}" SKETCH="${SKETCH_FILE}" return fi done echo "I couldn't find your sketch (.ino file)" >&2 exit 1 } prompt_before_flashing () { flashing_instructions=$(get_arduino_pref 'build.flashing_instructions') if [ "x${flashing_instructions}x" = "xx" ]; then flashing_instructions="If your keyboard needs you to do something to put it in flashing mode, do that now." fi printf '%b\n\n' "${flashing_instructions}" echo "" echo "When you're ready to proceed, press 'Enter'." # We do not want to permit line continuations here. We just want a newline. # shellcheck disable=SC2162 read } flash () { maybe_build "$@" # Check to see if we can see a keyboard bootloader port. # If we -can-, then we should skip over the "reset to bootloader" thing find_bootloader_ports if [ -z "${DEVICE_PORT_BOOTLOADER}" ]; then prompt_before_flashing # This is defined in the (optional) user config. # shellcheck disable=SC2154 ${preFlash_HOOKS} # If we're -not- doing a manual reset, then try to do it automatically if [ -z "${MANUAL_RESET}" ]; then reset_device sleep 2 find_bootloader_ports # Otherwise, poll for a bootloader port. else wait_for_bootloader_port fi fi check_bootloader_port_and_flash # This is defined in the (optional) user config. # shellcheck disable=SC2154 ${postFlash_HOOKS} } wait_for_bootloader_port() { declare -i tries tries=15 while [ "$tries" -gt 0 ] && [ -z "${DEVICE_PORT_BOOTLOADER}" ]; do sleep 1 printf "." find_bootloader_ports # the variable annotations do appear to be necessary # shellcheck disable=SC2004 tries=$(($tries-1)) done if [ "$tries" -gt 0 ]; then echo "Found." else echo "Timed out." fi } check_bootloader_port () { if [ -z "${DEVICE_PORT_BOOTLOADER}" ]; then echo "Unable to detect a keyboard in bootloader mode." echo "You may need to hold a key or hit a reset button." echo "Please check your keyboard's documentation" return 1 fi } check_bootloader_port_and_flash () { if ! check_bootloader_port; then return 1 fi echo "Flashing your keyboard:" # If the flash fails, try a second time if ! flash_over_usb; then sleep 2 if ! flash_over_usb; then if [ "${ARDUINO_VERBOSE}" != "-verbose" ]; then echo "Something went wrong." echo "You might want to try flashing again with the VERBOSE environment variable set" fi return 1 fi fi echo "Keyboard flashed successfully!" return 0 } flash_over_usb () { if [ "${ARDUINO_VERBOSE}" != "-verbose" ]; then ${AVRDUDE} \ -C "${AVRDUDE_CONF}" \ -p"${MCU}" \ -cavr109 \ -D \ -P "${DEVICE_PORT_BOOTLOADER}" \ -b57600 \ "-Uflash:w:${HEX_FILE_PATH}:i" \ 2>&1 |grep -v ^avrdude | grep -v '^$' |grep -v '^ ' | grep -vi programmer return "${PIPESTATUS[0]}" else ${AVRDUDE} \ -C "${AVRDUDE_CONF}" \ -p"${MCU}" \ -cavr109 \ -D \ -P "${DEVICE_PORT_BOOTLOADER}" \ -b57600 \ "-Uflash:w:${HEX_FILE_PATH}:i" return $? fi } flash_from_bootloader() { maybe_build "$@" prompt_before_flashing find_bootloader_ports check_bootloader_port_and_flash } program() { maybe_build "$@" prompt_before_flashing 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 find_bootloader_path 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 -- "${OUTPUT_FILE_PREFIX}-with-bootloader.hex" "${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 } maybe_build () { find_sketch build_version build_paths build_filenames # If the hex file is older than the sketch file, or the hex file does not exist # then rebuild. This is not as correct as letting make check our dependencies # But it's less broken for most user use cases # TODO(anyone): Make this suck less SKETCH_FILE_PATH=$(absolute_filename "${SKETCH_DIR}/${SKETCH}.ino") if [ "${HEX_FILE_PATH}" -ot "${SKETCH_FILE_PATH}" ]; then build "$@" fi } build () { compile "$@" size "$@" } prepare_ccache () { build_paths enable_ccache } compile () { find_sketch build_version build_paths build_filenames enable_ccache install -d "${OUTPUT_PATH}" echo "Building ${SKETCH_DIR}/${SKETCH}" # 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 _CMD_CXX="${CXX:-${COMPILER_PREFIX}${CXX_COMPILER_BASENAME}${COMPILER_SUFFIX}}" _CMD_CC="${CC:-${COMPILER_PREFIX}${C_COMPILER_BASENAME}${COMPILER_SUFFIX}}" _CMD_AR="${AR:-${COMPILER_PREFIX}${AR_BASENAME}${COMPILER_SUFFIX}}" # 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}" \ -built-in-libraries "${ARDUINO_PATH}/libraries" \ -prefs "compiler.cpp.extra_flags=${ARDUINO_CFLAGS} ${LOCAL_CFLAGS}" \ -prefs "compiler.path=${COMPILER_PATH}" \ -prefs "compiler.c.cmd=${_CMD_CC}" \ -prefs "compiler.cpp.cmd=${_CMD_CXX}" \ -prefs "compiler.ar.cmd=${_CMD_AR}" \ -prefs "compiler.c.elf.cmd=${_CMD_CXX}" \ $CCACHE_ENABLE \ -warnings all \ ${ARDUINO_VERBOSE} \ ${ARDUINO_AVR_GCC_PREFIX_PARAM} \ "${SKETCH_DIR}/${SKETCH}.ino" if [ -z "${LIBONLY}" ]; then 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" else cp "${BUILD_PATH}/${SKETCH}.ino.a" "${LIB_FILE_PATH}" ln -sf "${OUTPUT_FILE_PREFIX}.a" "${OUTPUT_PATH}/${SKETCH}-latest.a" fi if [ "${ARDUINO_VERBOSE}" = "-verbose" ]; then echo "Build artifacts can be found in ${BUILD_PATH}"; fi BOARD="${SAVED_BOARD}" FQBN="${SAVED_FQBN}" } find_all_sketches () { 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_sketches)" for plugin in ${plugins}; do export SKETCH="${plugin}" export LIBRARY="${plugin}" $0 "${plugin}" build done } compile_all () { plugins="$(find_all_sketches)" 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}" kaleidoscope_dir="$(dirname "$0")/.." (install -d "${kaleidoscope_dir}/testing/googletest/build" && cd "${kaleidoscope_dir}/testing/googletest/build" && cmake .. && make clean) } reset_device() { find_device_port check_device_port reset_device_cmd } check_device_port () { if [ -z "$DEVICE_PORT" ]; then cat <<EOF >&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 <<EOF >&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 <<EOF >&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 } build_gtest_gmock() { kaleidoscope_dir="$(dirname "$0")/.." (cd "${kaleidoscope_dir}/testing/googletest" && cmake . && make) } run_tests() { (cd "${BOARD_HARDWARE_PATH}/keyboardio" && make prepare-virtual) build_gtest_gmock kaleidoscope_dir="$(dirname "$0")/.." cd "${kaleidoscope_dir}/tests" ${MAKE:-make} } docker_tests() { kaleidoscope_dir="$(dirname "$0")/.." cd "${kaleidoscope_dir}" bin/run-docker make -C tests all } 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. run-tests | docker-tests Builds and runs the test suite, on the host, and in docker, respectively. 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 [ -n "${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}" export SKETCH export LIBRARY for cmd in ${cmds}; do ${cmd} done