#!/bin/bash # error handling if ! command -v jq &> /dev/null then echo "jq is a dependency, and could not be found, see https://stedolan.github.io/jq for installation details" exit fi Help() { echo "Usage: $0 -k -m -c -r -h" echo "" echo " -l list valid keyboards (optional, overrides all other options)" echo " -k keyboard directory (optional, default is all fingerpunch keyboards)" echo " -m keymap (optional, defaults to the 'default' keymap)" echo " -c add CONVERT_TO parameter for a controller (eg -c stemcell)" echo " -i (interactive mode, take feature selection user input to generate build command)" echo " -r (run the build command(s), defaults to outputting the build string)" echo " -e (add environment variables, only used in interactive mode, e.g. RGB_MATRIX_REACTIVE_LAYERS=yes or -e \"RGB_LED_RING=yes RGBLIGHT_SNAKE_LAYERS=yes\")" echo " -h (show this dialog)" echo "" echo "Examples: " echo "--------" echo "fp_build.sh -i -k \"rockon/v2\" -m sadekbaroudi -r" echo "fp_build.sh -i -m sadekbaroudi" echo "fp_build.sh -k \"barobord\"" } get_valid_keyboards() { valid_keyboards="" directories=$(find ${1}/* -maxdepth 0 -type d) echo "${directories}" | while read line; do # first we do a basic test to see if fp_build.json is in the keyboard root directory if [[ -e "${line}/fp_build.json" ]]; then echo -n "${line} " fi # check for all the supported versions of the keyboard in the keyboard root directory for i in {0..9} do if [[ -e "${line}/v${i}/fp_build.json" ]]; then echo -n "${line}/v${i} " fi # handle format with subversions, like v3_1 for j in {0..9} do if [[ -e "${line}/v${i}_${j}/fp_build.json" ]]; then echo -n "${line}/v${i}_${j} " fi done # special case for pinkies out v2 extended if [[ -e "${line}/v${i}_ext/fp_build.json" ]]; then echo -n "${line}/v${i}_ext " fi done # special case for tenbit for i in {4..5} do if [[ -e "${line}/${i}x12/fp_build.json" ]]; then echo -n "${line}/${i}x12 " fi done # special case for vulpes minora byomcu if [[ -e "${line}/byomcu/fp_build.json" ]]; then echo -n "${line}/byomcu " fi # special case for vulpes minora rp2040zero if [[ -e "${line}/rp2040zero" ]]; then echo -n "${line}/rp2040zero " fi # special case for vulpes minora xivik if [[ -e "${line}/xivik" ]]; then echo -n "${line}/xivik " fi # if we have a second parameter, then we don't want to recurse again if [ "$#" -lt 2 ]; then # now check for byomcu version, repeating the logic above if [[ -e "${line}/byomcu" ]]; then echo $(get_valid_keyboards "${line}/byomcu" "false") fi # now check for atmega version if [[ -e "${line}/atmega" ]]; then echo $(get_valid_keyboards "${line}/atmega" "false") fi # now check for rp2040 version if [[ -e "${line}/rp" ]]; then echo $(get_valid_keyboards "${line}/rp" "false") fi # now check for stm version if [[ -e "${line}/stm" ]]; then echo $(get_valid_keyboards "${line}/stm" "false") fi fi done } build_keyboard_user_input() { local build_json="${1}/fp_build.json" local keyboard_base_dir="${1}" local keyboard_name="${1#${2}/}" local run_build="${4}" local build_string="make ${keyboard_base_dir#keyboards\/}:${3}" echo "${build_string}" # get the total number of paramters top_level_element_count=$(cat "${build_json}" | jq 'length') # loop through each parameter for ((param_iter = 0 ; param_iter < top_level_element_count ; param_iter++)); do # get the parameter type to decide how to handle it param_type=$(cat "${build_json}" | jq -r ".[${param_iter}].type") # get the string to present to the user for their input user_input_string=$(cat "${build_json}" | jq -r ".[${param_iter}].user_input") # if it's a "one-of"... in other words, pick an option from a list if [[ "${param_type}" == "one-of" ]]; then options_count=$(cat "${build_json}" | jq ".[${param_iter}].names | length") user_input_string+=" (0-${options_count}): " echo -n "${user_input_string}" read user_choice while [[ $user_choice -lt 0 || $user_choice -gt $options_count ]]; do echo "Invalid choice: ${user_choice}" echo -n "${user_input_string}" read user_choice done # start at 1, because 0 should always be "none" param_names_counter=1 param_names=$(cat "${build_json}" | jq -r ".[${param_iter}].names | @sh" | tr -d \') for param_name in $param_names; do if [[ $param_names_counter -eq $user_choice ]]; then build_string+=" ${param_name}=yes" fi ((param_names_counter+=1)) done # if it's a single value choice for a parmeter, or "yes or no" question elif [[ "${param_type}" == "single" ]]; then user_input_string+=" (yes/no): " echo -n "${user_input_string}" read user_choice while [[ $user_choice != "yes" && $user_choice != "no" && $user_choice != "y" && $user_choice != "n" ]]; do echo "Invalid choice: ${user_choice}" echo -n "${user_input_string}" read user_choice done if [[ "${user_choice}" == "y" ]]; then user_choice="yes" fi if [[ "${user_choice}" == "n" ]]; then user_choice="no" fi param_name=$(cat "${build_json}" | jq -r ".[${param_iter}].name") build_string+=" ${param_name}=${user_choice}" elif [[ "${param_type}" == "convert-to" ]]; then # Do nothing, we can skip this for interactive mode param_name=$(cat "${build_json}" | jq -r ".[${param_iter}].name") else echo "invalid type in json file: ${param_type}" exit fi done if [[ -n "${5}" && "${5}" != "no" ]]; then build_string+=" CONVERT_TO=${5}" fi if [[ -n "${6}" && "${6}" != "no" ]]; then build_string+=" ${6}" fi process_build_string "${build_string}" "${run_build}" } build_keyboard_all_combinations() { local build_json="${1}/fp_build.json" local keyboard_base_dir="${1}" local keyboard_name="${1#${2}/}" local run_build="${4}" local build_string_base="make ${keyboard_base_dir#keyboards\/}:${3}" if [[ -n "${5}" && "${5}" != "no" ]]; then build_string_base+=" CONVERT_TO=${5}" fi make_build_string_recursive "${build_json}" "${run_build}" 0 "${build_string_base}" } # make_build_string_recursive "${build_json}" "${run_build}" "${param_number}" "${build_string_base}" make_build_string_recursive() { local build_json="${1}" local run_build="${2}" local param_number=$3 local build_string_base="${4}" local top_level_element_count=$(cat "${build_json}" | jq 'length') if [[ $((param_number)) -ge $((top_level_element_count)) ]]; then process_build_string "${build_string_base}" "${run_build}" return; fi local param_type=$(cat "${build_json}" | jq -r ".[${param_number}].type") local next_param_number=$((param_number + 1)) # if it's a "one-of"... in other words, pick an option from a list if [[ "${param_type}" == "one-of" ]]; then local param_names=$(cat "${build_json}" | jq -r ".[${param_number}].names | @sh" | tr -d \') for param_name in $param_names; do make_build_string_recursive "${build_json}" "${run_build}" $next_param_number "${build_string_base} ${param_name}=yes" done # if it's a single value choice for a parmeter, or "yes or no" question elif [[ "${param_type}" == "single" ]]; then local param_name=$(cat "${build_json}" | jq -r ".[${param_number}].name") make_build_string_recursive "${build_json}" "${run_build}" $next_param_number "${build_string_base} ${param_name}=yes" make_build_string_recursive "${build_json}" "${run_build}" $next_param_number "${build_string_base} ${param_name}=no" elif [[ "${param_type}" == "convert-to" ]]; then local param_name=$(cat "${build_json}" | jq -r ".[${param_number}].name") make_build_string_recursive "${build_json}" "${run_build}" $next_param_number "${build_string_base} CONVERT_TO=${param_name}" else echo "invalid type in json file: ${param_type}" exit fi } # rename_file_from_build_string $build_string $target_filename_suffix rename_file_from_build_string() { tokens=( $1 ) target_filename_suffix="" if [[ ! -z "${2}" ]]; then target_filename_suffix="_${2}" fi # Calculate the qmk build filename prefix to move it token_file_prefix="${tokens[1]//\//_}" token_file_prefix="${token_file_prefix//:/_}" # Start the new filename, which will be appended to below target_filename="${token_file_prefix}" # check if token_i>1 # check if first paramter is CONVERT_TO, then grab other side of = (stemcell for example) # otherwise grab first parameter (RGBLIGHT_ENABLE for example) # rename file accordingly token_i=0 for token in "${tokens[@]}" do if [[ $token_i -gt 1 ]]; then config_param="${token%%=*}" config_value="${token#*=}" if [[ "${config_param}" == "CONVERT_TO" ]]; then token_file_prefix+="_${config_value}" target_filename+="_${config_value}" else # Make sure that the value is yes (it's enabled), otherwise we shouldn't include in the filename if [[ "${config_value}" == "yes" ]]; then # ,, converts to lowercase target_filename+="_${config_param,,}" # remove _enable suffix as it's implied target_filename=${target_filename%"_enable"} fi fi fi token_i+=1 done echo "${0}: filename token is ${token_file_prefix}" echo "${0}: target filename is ${target_filename}" hex_source_file="${token_file_prefix}.hex" uf2_source_file="${token_file_prefix}.uf2" hex_target_file="${target_filename}${target_filename_suffix}.hex" uf2_target_file="${target_filename}${target_filename_suffix}.uf2" if test -f "${hex_source_file}"; then echo "${0}: Renaming file '${hex_source_file}' to '${hex_target_file}'" if [ "${hex_source_file}" = "${hex_target_file}" ]; then echo "${0}: Skipping rename, since the file is already named appropriately" else mv "${hex_source_file}" "${hex_target_file}" fi else echo "${0}: Could not find hex source file ${hex_source_file} to rename." fi if test -f "${uf2_source_file}"; then echo "${0}: Renaming file '${uf2_source_file}' to '${uf2_target_file}'" if [ "${uf2_source_file}" = "${uf2_target_file}" ]; then echo "${0}: Skipping rename, since the file is already named appropriately" else mv "${uf2_source_file}" "${uf2_target_file}" fi else echo "${0}: Could not find uf2 source file ${uf2_source_file} to rename." fi } # process_build_string $build_string $run_build process_build_string() { build_string="$1" run_build="$2" append_to_filename="" echo "${build_string}" if [[ "${run_build}" == "yes" ]]; then echo "fp_build.sh: Running QMK Build...." echo "" build_run_output=`${build_string}` build_run_status=$? echo "output: ${build_run_output}" echo "exit status: ${build_run_status}" if [[ $build_run_status -ne 0 ]]; then # if the firmware is too large, we proceed, but append to filename, otherwise we error out if [[ "${build_run_output}" == *"The firmware is too large"* ]]; then append_to_filename="FIRMWARE_SIZE_CHECK_FAILED" else echo "${0} build run failed with status ${build_run_status}" exit $build_run_status fi fi rename_file_from_build_string "${build_string}" "${append_to_filename}" fi } Keyboard="" Keymap="default" RunBuild="no" ConvertTo="no" Interactive="no" ListKeyboards="no" EnvVariables="no" while getopts "k:m:c:e:rhil" option; do case $option in l) ListKeyboards="yes";; k) Keyboard=${OPTARG};; m) Keymap=${OPTARG};; c) ConvertTo=${OPTARG};; i) Interactive="yes";; r) RunBuild="yes";; e) EnvVariables="${OPTARG}";; h) Help exit;; esac done #### code starts here # set up variables FP_KB_DIR="keyboards/fingerpunch" FP_KB=("${Keyboard}") if [[ -z "${Keyboard}" || "${ListKeyboards}" == "yes" ]]; then FP_KB=$(get_valid_keyboards "${FP_KB_DIR}") else FP_KB=("${FP_KB_DIR}/${FP_KB}") if [[ ! -e "${FP_KB}" ]]; then echo "${FP_KB} is not a valid file, can't build" exit fi fi; if [[ "${ListKeyboards}" == "yes" ]]; then # remove last space from FP_KB and convert spaces to newlines echo "${FP_KB%?}" | tr " " "\n" else for filename in $FP_KB; do if [[ "${Interactive}" == "yes" ]]; then echo "Running for ${filename}" build_keyboard_user_input "${filename}" "${FP_KB_DIR}" "${Keymap}" "${RunBuild}" "${ConvertTo}" "${EnvVariables}" else build_keyboard_all_combinations "${filename}" "${FP_KB_DIR}" "${Keymap}" "${RunBuild}" "${ConvertTo}" "" fi done fi