diff --git a/tools/build-multilang.sh b/tools/build-multilang.sh new file mode 100755 index 000000000..2efdef860 --- /dev/null +++ b/tools/build-multilang.sh @@ -0,0 +1,1334 @@ +#!/bin/bash + +# EdgeTX Multi-Language Multi-Variant Build Script +# Written with assistance from GitHub Copilot AI +# +# This script automatically builds all languages for all firmware variants +# with intelligent filtering for character-based languages on B&W screens + +# Manual override for color screen targets (set this to override auto-detection) +COLOR_SCREEN_TARGETS_OVERRIDE="" + +# Function to build targets by screen type and optional processor filter +build_targets_by_screen_type() { + local screen_type="$1" # "color" or "bw" + local processor_filter="$2" # "F2", "F4", "H7", "exclude-F2", etc. (optional) + local all_targets=$(get_all_targets) + local filtered_targets="" + local failed_targets=0 + local overall_start_time=$(date +%s) + + # Filter by screen type first + for target in $all_targets; do + local is_color_target=false + if is_color_screen_target "$target"; then + is_color_target=true + fi + + if [[ "$screen_type" == "color" && "$is_color_target" == "true" ]]; then + filtered_targets="$filtered_targets $target" + elif [[ "$screen_type" == "bw" && "$is_color_target" == "false" ]]; then + filtered_targets="$filtered_targets $target" + fi + done + + # Apply processor filter if specified + if [[ -n "$processor_filter" ]]; then + local processor_filtered_targets="" + + if [[ "$processor_filter" == exclude-* ]]; then + # Exclude specific processor + local exclude_proc="${processor_filter#exclude-}" + for target in $filtered_targets; do + local target_processor=$(get_stm32_processor_type "$target") + if [[ "$target_processor" != "$exclude_proc" && "$target_processor" != "UNKNOWN" ]]; then + processor_filtered_targets="$processor_filtered_targets $target" + fi + done + else + # Include only specific processor + for target in $filtered_targets; do + local target_processor=$(get_stm32_processor_type "$target") + if [[ "$target_processor" == "$processor_filter" ]]; then + processor_filtered_targets="$processor_filtered_targets $target" + fi + done + fi + + filtered_targets="$processor_filtered_targets" + fi + + # Build the filtered targets + if [[ -z "$filtered_targets" ]]; then + echo "No targets match the specified criteria." + return 0 + fi + + echo "Building $screen_type screen targets$(if [[ -n "$processor_filter" ]]; then echo " with processor filter: $processor_filter"; fi)..." + echo "Selected targets: $filtered_targets" + echo + + for target in $filtered_targets; do + echo "========================================" + echo "Processing target: $target ($(get_stm32_processor_type "$target") processor)" + echo "========================================" + + if ! build_target_all_languages "$target"; then + ((failed_targets++)) + echo "Failed to build all languages for target $target" + fi + + echo + done + + # Calculate overall elapsed time + local overall_end_time=$(date +%s) + local overall_elapsed=$((overall_end_time - overall_start_time)) + + # Format overall elapsed time + local overall_elapsed_formatted + if [[ $overall_elapsed -ge 3600 ]]; then + overall_elapsed_formatted=$(printf "%dh %dm %ds" $((overall_elapsed / 3600)) $(((overall_elapsed % 3600) / 60)) $((overall_elapsed % 60))) + elif [[ $overall_elapsed -ge 60 ]]; then + overall_elapsed_formatted=$(printf "%dm %ds" $((overall_elapsed / 60)) $((overall_elapsed % 60))) + else + overall_elapsed_formatted="${overall_elapsed}s" + fi + + if [[ $failed_targets -gt 0 && "${CONTINUE_ON_FAILURE:-}" != "1" ]]; then + echo "=========================================" + echo "BUILD SUMMARY" + echo "=========================================" + printf "\033[31m✗\033[0m Warning: %d targets had build failures\n" "$failed_targets" + echo "Total build time: ${overall_elapsed_formatted}" + return 1 + else + echo "=========================================" + echo "BUILD SUMMARY" + echo "=========================================" + if [[ $failed_targets -gt 0 && "${CONTINUE_ON_FAILURE:-}" == "1" ]]; then + printf "\033[32m✓\033[0m All targets attempted successfully!\n" + echo " Note: Some language builds failed but were ignored due to --continue-on-failure" + else + printf "\033[32m✓\033[0m All targets built successfully!\n" + fi + echo "Total build time: ${overall_elapsed_formatted}" + return 0 + fi +} +# with intelligent filtering for character-based languages on B&W screens + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Source the build-common.sh for target definitions +if [[ -f "$SCRIPT_DIR/tools/build-common.sh" ]]; then + . "$SCRIPT_DIR/tools/build-common.sh" +elif [[ -f "$SCRIPT_DIR/build-common.sh" ]]; then + . "$SCRIPT_DIR/build-common.sh" +else + echo "Error: Could not find build-common.sh" + exit 1 +fi + +# Auto-discover available languages from radio/src/CMakeLists.txt +get_radio_languages() { + local cmake_file="" + if [[ -f "$SCRIPT_DIR/../radio/src/CMakeLists.txt" ]]; then + cmake_file="$SCRIPT_DIR/../radio/src/CMakeLists.txt" + else + echo "Error: Could not find radio/src/CMakeLists.txt" >&2 + return 1 + fi + + # Extract languages from: set(RADIO_LANGUAGES CN CZ DA DE EN ES FI FR HE HU IT JP KO NL PL PT RU SE SK TW UA) + # Strip carriage returns to handle Windows line endings + grep "^set(RADIO_LANGUAGES" "$cmake_file" | sed 's/set(RADIO_LANGUAGES \(.*\))/\1/' | tr -d '\r' +} + +# Define character-based/logographic languages (configurable with auto-detection fallback) +# These should not be built for B&W screens due to font/display limitations +LOGOGRAPHIC_LANGUAGES_OVERRIDE="" # Set this to override auto-detection + +get_logographic_languages() { + # Use override if set + if [[ -n "$LOGOGRAPHIC_LANGUAGES_OVERRIDE" ]]; then + echo "$LOGOGRAPHIC_LANGUAGES_OVERRIDE" + return 0 + fi + + # Auto-detect logographic languages based on known character sets + # These are languages that typically use complex character sets that may not display well on B&W screens + local known_logographic="CN JP KO TW" + + # Could be enhanced to check translation files for unicode/multibyte characters + # For now, use the known list as fallback + echo "$known_logographic" +} + +# Extract all firmware variants from build-common.sh +get_all_targets() { + # Extract target names from the get_target_build_options function + local build_common_path="" + if [[ -f "$SCRIPT_DIR/tools/build-common.sh" ]]; then + build_common_path="$SCRIPT_DIR/tools/build-common.sh" + elif [[ -f "$SCRIPT_DIR/build-common.sh" ]]; then + build_common_path="$SCRIPT_DIR/build-common.sh" + else + echo "Error: Could not find build-common.sh" >&2 + return 1 + fi + + # Extract target names from case statement in get_target_build_options function + awk ' + /^get_target_build_options\(\)/ { in_function=1; next } + in_function && /^}$/ { exit } + in_function && /^[[:space:]]*[a-z0-9_-]+\)/ { + gsub(/^[[:space:]]*/, "") + gsub(/\).*$/, "") + if ($0 != "*" && $0 != "esac") print $0 + } + ' "$build_common_path" | sort -u +} + +# Auto-discover color screen targets by checking target CMakeLists.txt files - fully dynamic implementation +get_color_screen_targets() { + local color_targets="" + local all_targets + + all_targets=$(get_all_targets) + + for target in $all_targets; do + # Get the PCB type for this target from build-common.sh + local pcb_line + pcb_line=$(awk -v target="$target" ' + /^get_target_build_options\(\)/ { in_function=1; next } + in_function && /^}$/ { exit } + in_function && $0 ~ "^[[:space:]]*" target "\\)" { + found_target=1; next + } + found_target && /BUILD_OPTIONS.*-DPCB=/ { + print $0; exit + } + found_target && /;;/ { exit } + ' "$SCRIPT_DIR/build-common.sh" 2>/dev/null || true) + + if [[ -n "$pcb_line" ]]; then + # Extract PCB type (e.g., -DPCB=X10 -> X10) + local pcb_type + pcb_type=$(echo "$pcb_line" | grep -o '\-DPCB=[A-Z0-9+]*' | cut -d= -f2) + + if [[ -n "$pcb_type" ]]; then + # Dynamically determine target directory by parsing radio/src/CMakeLists.txt + local radio_cmake_file="../radio/src/CMakeLists.txt" + local target_dir="" + + if [[ -f "$SCRIPT_DIR/$radio_cmake_file" ]]; then + # Parse the if/elseif chain to find which target directory corresponds to our PCB type + target_dir=$(awk -v pcb="$pcb_type" ' + /^if\(PCB STREQUAL|^elseif\(PCB STREQUAL/ { + condition_line = $0 + # Extract all PCB types from the condition + gsub(/^[^(]*\(/, "", condition_line) # Remove everything before first ( + gsub(/\).*$/, "", condition_line) # Remove everything after last ) + gsub(/PCB STREQUAL /, "", condition_line) # Remove "PCB STREQUAL " + gsub(/ OR /, " ", condition_line) # Replace " OR " with space + + # Check if our PCB type is in this condition + if ((" " condition_line " ") ~ (" " pcb " ")) { + found_pcb = 1 + } + } + found_pcb && /include\(targets\/.*\/CMakeLists\.txt\)/ { + # Extract target directory from include(targets/horus/CMakeLists.txt) + match($0, /targets\/([^\/]+)\//, arr) + if (arr[1]) { + print arr[1] + exit + } + } + /^else\(\)|^endif\(\)/ { found_pcb = 0 } + ' "$SCRIPT_DIR/$radio_cmake_file") + fi + + if [[ -n "$target_dir" ]]; then + # Check if this target directory has color screen support + local target_cmake_file="../radio/src/targets/$target_dir/CMakeLists.txt" + + if [[ -f "$SCRIPT_DIR/$target_cmake_file" ]]; then + # Look for color screen indicators: GUI_DIR with colorlcd, COLORLCD definitions, etc. + if grep -q "GUI_DIR.*colorlcd\|COLORLCD\|set(GUI_DIR.*colorlcd" "$SCRIPT_DIR/$target_cmake_file"; then + color_targets="$color_targets $target" + fi + fi + fi + fi + fi + done + + # Return unique sorted targets + echo "$color_targets" | tr ' ' '\n' | sort -u | tr '\n' ' ' +} + +# Auto-discover STM32 processor types for targets - fully dynamic implementation +get_stm32_processor_type() { + local target=$1 + + # Hard-coded case for x9dp (will be phased out) + if [[ "$target" == "x9dp" ]]; then + echo "F2" + return 0 + fi + + # Get the PCB configuration for this target from build-common.sh + local pcb_line + pcb_line=$(awk -v target="$target" ' + /^get_target_build_options\(\)/ { in_function=1; next } + in_function && /^}$/ { exit } + in_function && $0 ~ "^[[:space:]]*" target "\\)" { + found_target=1; next + } + found_target && /BUILD_OPTIONS.*-DPCB=/ { + print $0 + exit + } + found_target && /;;/ { exit } + ' "$SCRIPT_DIR/build-common.sh" 2>/dev/null || true) + + if [[ -z "$pcb_line" ]]; then + echo "UNKNOWN" + return 1 + fi + + # Extract PCB type and PCBREV from BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=T8" + local pcb_type + local pcb_rev + pcb_type=$(echo "$pcb_line" | grep -o '\-DPCB=[A-Z0-9+]*' | cut -d= -f2) + pcb_rev=$(echo "$pcb_line" | grep -o '\-DPCBREV=[A-Z0-9+]*' | cut -d= -f2 || true) + + if [[ -z "$pcb_type" ]]; then + echo "UNKNOWN" + return 1 + fi + + # Dynamically determine target directory by parsing radio/src/CMakeLists.txt + local radio_cmake_file="../radio/src/CMakeLists.txt" + local target_dir="" + + if [[ -f "$SCRIPT_DIR/$radio_cmake_file" ]]; then + # Parse the if/elseif chain to find which target directory corresponds to our PCB type + target_dir=$(awk -v pcb="$pcb_type" ' + /^if\(PCB STREQUAL|^elseif\(PCB STREQUAL/ { + condition_line = $0 + # Extract all PCB types from the condition + gsub(/^[^(]*\(/, "", condition_line) # Remove everything before first ( + gsub(/\).*$/, "", condition_line) # Remove everything after last ) + gsub(/PCB STREQUAL /, "", condition_line) # Remove "PCB STREQUAL " + gsub(/ OR /, " ", condition_line) # Replace " OR " with space + + # Check if our PCB type is in this condition + if ((" " condition_line " ") ~ (" " pcb " ")) { + found_pcb = 1 + } + } + found_pcb && /include\(targets\/.*\/CMakeLists\.txt\)/ { + # Extract target directory from include(targets/horus/CMakeLists.txt) + match($0, /targets\/([^\/]+)\//, arr) + if (arr[1]) { + print arr[1] + exit + } + } + /^else\(\)|^endif\(\)/ { found_pcb = 0 } + ' "$SCRIPT_DIR/$radio_cmake_file") + fi + + if [[ -z "$target_dir" ]]; then + echo "UNKNOWN" + return 1 + fi + + # Simulate the CMake logic to determine CPU_TYPE_FULL based on PCB and PCBREV + local target_cmake_file="../radio/src/targets/$target_dir/CMakeLists.txt" + + if [[ -f "$SCRIPT_DIR/$target_cmake_file" ]]; then + # Parse the target CMakeLists.txt to determine CPU_TYPE_FULL + # We need to simulate the if/elseif logic for PCB and PCBREV + local cpu_type_full + + # First, try to find CPU_TYPE_FULL within the specific PCBREV block if PCBREV is specified + if [[ -n "$pcb_rev" ]]; then + cpu_type_full=$(awk -v pcb="$pcb_type" -v pcbrev="$pcb_rev" ' + BEGIN { in_pcb_block = 0; in_pcbrev_block = 0; cpu_found = 0 } + + # Look for PCB conditions + /^if\(PCB STREQUAL|^elseif\(PCB STREQUAL/ { + condition_line = $0 + gsub(/^[^(]*\(/, "", condition_line) + gsub(/\).*$/, "", condition_line) + gsub(/PCB STREQUAL /, "", condition_line) + gsub(/ OR /, " ", condition_line) + + # Check if our PCB type is in this condition (use index for exact matching) + if (index(" " condition_line " ", " " pcb " ") > 0) { + in_pcb_block = 1 + } else { + in_pcb_block = 0 + } + in_pcbrev_block = 0 + } + + # Look for PCBREV conditions within PCB block + in_pcb_block && /\$\{PCBREV\} STREQUAL/ { + condition_line = $0 + gsub(/.*\$\{PCBREV\} STREQUAL /, "", condition_line) + gsub(/\).*$/, "", condition_line) + gsub(/ .*$/, "", condition_line) + + if (condition_line == pcbrev) { + in_pcbrev_block = 1 + } else { + in_pcbrev_block = 0 + } + } + + # Accept CPU_TYPE_FULL within PCBREV block + in_pcbrev_block && /set\(CPU_TYPE_FULL STM32/ { + match($0, /STM32[FH][0-9RSx]+[A-Z]*/, arr) + if (arr[0] && !cpu_found) { + print arr[0] + cpu_found = 1 + exit + } + } + + # Reset PCBREV block when we hit another condition or endif + /^[[:space:]]*if\(|^[[:space:]]*elseif\(|^[[:space:]]*else\(|^[[:space:]]*endif\(/ && !/PCBREV/ { + if (in_pcb_block && in_pcbrev_block) { + in_pcbrev_block = 0 + } + } + + # Reset everything when we exit the PCB block + /^else\(\)|^endif\(\)/ && !(/PCBREV/) { + in_pcb_block = 0 + in_pcbrev_block = 0 + } + ' "$SCRIPT_DIR/$target_cmake_file") + fi + + # If no CPU_TYPE_FULL found in PCBREV block, or no PCBREV specified, + # look for CPU_TYPE_FULL within the specific PCB block + if [[ -z "$cpu_type_full" ]]; then + # Use sed to extract the specific PCB block, then look for CPU_TYPE_FULL + # For targets without PCBREV, we want the "else()" case, not the PCBREV-specific ones + local pcb_block_start_pattern="^elseif(PCB STREQUAL $pcb_type)\|^if(PCB STREQUAL.*$pcb_type" + local pcb_block_end_pattern="^elseif(PCB STREQUAL\|^else()\|^endif()" + + if [[ -z "$pcb_rev" ]]; then + # No PCBREV specified, first try to find CPU_TYPE_FULL in else() block for complex targets + cpu_type_full=$(sed -n "/$pcb_block_start_pattern/,/$pcb_block_end_pattern/p" "$SCRIPT_DIR/$target_cmake_file" | \ + awk ' + BEGIN { in_else_block = 0 } + /else\(\)/ { in_else_block = 1; next } + /endif\(\)/ { in_else_block = 0; next } + /\$\{PCBREV\} STREQUAL/ { in_else_block = 0; next } + in_else_block && /set\(CPU_TYPE_FULL STM32/ { + match($0, /STM32[FH][0-9RSx]+[A-Z]*/, arr) + if (arr[0]) { print arr[0]; exit } + } + # If no else() block, take any CPU_TYPE_FULL not in a PCBREV block + !in_else_block && /set\(CPU_TYPE_FULL STM32/ && (prev_line !~ /\$\{PCBREV\} STREQUAL/) { + match($0, /STM32[FH][0-9RSx]+[A-Z]*/, arr) + if (arr[0] && !found_any) { found_any=1; print arr[0]; exit } + } + { prev_line = $0 } + ') + + # If still not found (simple targets), use global CPU_TYPE_FULL + if [[ -z "$cpu_type_full" ]]; then + cpu_type_full=$(grep "set(CPU_TYPE_FULL STM32" "$SCRIPT_DIR/$target_cmake_file" | head -1 | grep -o "STM32[FH][0-9RSx]*[A-Z]*") + fi + else + # PCBREV specified but no CPU_TYPE_FULL found in PCBREV block, use global CPU_TYPE_FULL + cpu_type_full=$(grep "set(CPU_TYPE_FULL STM32" "$SCRIPT_DIR/$target_cmake_file" | head -1 | grep -o "STM32[FH][0-9RSx]*[A-Z]*") + fi + fi + + if [[ -n "$cpu_type_full" ]]; then + # Convert CPU_TYPE_FULL to CPU_TYPE (F2, F4, H7, etc.) + local cpu_type + case "$cpu_type_full" in + STM32F2*) cpu_type="F2" ;; + STM32F4*) cpu_type="F4" ;; + STM32H7*) cpu_type="H7" ;; + *) cpu_type="UNKNOWN" ;; + esac + echo "$cpu_type" + return 0 + fi + fi + + echo "UNKNOWN" + return 1 +} + +# Function to get targets by STM32 processor type +get_targets_by_processor() { + local processor_filter="$1" + local all_targets=$(get_all_targets) + local filtered_targets="" + + for target in $all_targets; do + local target_processor=$(get_stm32_processor_type "$target") + if [[ "$target_processor" == "$processor_filter" ]]; then + filtered_targets="$filtered_targets $target" + fi + done + + echo "$filtered_targets" +} + +# Function to get targets by processor exclusion +get_targets_excluding_processor() { + local exclude_processor="$1" + local all_targets=$(get_all_targets) + local filtered_targets="" + + for target in $all_targets; do + local target_processor=$(get_stm32_processor_type "$target") + if [[ "$target_processor" != "$exclude_processor" && "$target_processor" != "UNKNOWN" ]]; then + filtered_targets="$filtered_targets $target" + fi + done + + echo "$filtered_targets" +} + +# Function to check if a target uses color screen +is_color_screen_target() { + local target=$1 + local color_targets + + # Use override if set, otherwise auto-detect + if [[ -n "$COLOR_SCREEN_TARGETS_OVERRIDE" ]]; then + color_targets="$COLOR_SCREEN_TARGETS_OVERRIDE" + else + color_targets=$(get_color_screen_targets) + fi + + # Check if target is in the color targets list + for color_target in $color_targets; do + if [[ "$target" == "$color_target" ]]; then + return 0 + fi + done + return 1 +} + +# Function to check if a language is logographic +is_logographic_language() { + local lang=$1 + local logographic_langs + + logographic_langs=$(get_logographic_languages) + + for logo_lang in $logographic_langs; do + if [[ "$lang" == "$logo_lang" ]]; then + return 0 + fi + done + return 1 +} + +# Function to get valid languages for a target +get_valid_languages_for_target() { + local target=$1 + local valid_languages="" + local radio_languages + + radio_languages=$(get_radio_languages) + + # Check if force all languages flag is set + if [[ "${FORCE_ALL_LANGUAGES:-}" == "1" ]]; then + # Force all languages mode: build all languages regardless of screen type + valid_languages="$radio_languages" + else + # Normal mode: filter based on screen type + for lang in $radio_languages; do + if is_color_screen_target "$target"; then + # Color screen: all languages allowed + valid_languages="$valid_languages $lang" + else + # B&W screen: exclude logographic languages + if ! is_logographic_language "$lang"; then + valid_languages="$valid_languages $lang" + fi + fi + done + fi + + echo "$valid_languages" +} + +# Function to build a specific target with a specific language +build_target_language() { + local target=$1 + local language=$2 + + # Show building message without newline + printf "Building %s with language %s... " "$target" "$language" + + # Dry run mode - just simulate the build + if [[ "${DRY_RUN:-}" == "1" ]]; then + printf "\033[32m✓\033[0m [DRY-RUN]\n" + printf " → Would build: TRANSLATIONS=%s FLAVOR=%s\n" "$language" "$target" + return 0 + fi + + # Use build-gh.sh with the target and set the TRANSLATIONS environment variable + # build-gh.sh expects FLAVOR environment variable with the target name + local build_script="" + if [[ -f "$SCRIPT_DIR/tools/build-gh.sh" ]]; then + build_script="$SCRIPT_DIR/tools/build-gh.sh" + elif [[ -f "$SCRIPT_DIR/build-gh.sh" ]]; then + build_script="$SCRIPT_DIR/build-gh.sh" + else + echo "Error: Could not find build-gh.sh" + return 1 + fi + + export FLAVOR="$target" + export SRCDIR="/workspaces/edgetx" + export EXTRA_OPTIONS="-DTRANSLATIONS=$language " + + # Create build directory if it doesn't exist and change to it + local build_dir="$SCRIPT_DIR/../build" + mkdir -p "$build_dir" + local original_pwd="$PWD" + cd "$build_dir" + + # Create a unique log file for this build + local log_file="/tmp/edgetx-build-${target}-${language}-$$.log" + + # Run build script and capture all output to log file + "$build_script" > "$log_file" 2>&1 + + local exit_code=$? + + # Return to original directory + cd "$original_pwd" + + if [[ $exit_code -eq 0 ]]; then + printf "\033[32m✓\033[0m Success!\n" + + # Rename firmware files to include language code + local git_sha=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") + local base_name="${target}-${git_sha}" + local lang_suffix="-${language,,}" # Convert language to lowercase + + # Check for and rename .bin file + if [[ -f "${base_name}.bin" ]]; then + mv "${base_name}.bin" "${base_name}${lang_suffix}.bin" + fi + + # Check for and rename .uf2 file + if [[ -f "${base_name}.uf2" ]]; then + mv "${base_name}.uf2" "${base_name}${lang_suffix}.uf2" + fi + + # Clean up log file on success + rm -f "$log_file" + else + printf "\033[31m✗\033[0m Failed!\n" + printf " Build log saved to: %s\n" "$log_file" + if [[ "${CONTINUE_ON_FAILURE:-0}" != "1" ]]; then + printf " Terminating current batch due to build failure\n" + fi + return 1 + fi + + return 0 +} + +# Function to build all languages for a target +build_target_all_languages() { + local target=$1 + local languages + local failed_builds=0 + local start_time=$(date +%s) + + languages=$(get_valid_languages_for_target "$target") + + if [[ "${FORCE_ALL_LANGUAGES:-}" == "1" ]]; then + echo "Building target '$target' with ALL languages (forced): $languages" + echo " → Force mode - building all languages including logographic for B&W screens" + else + echo "Building target '$target' with languages: $languages" + + if is_color_screen_target "$target"; then + echo " → Color screen target - building all languages" + else + echo " → B&W screen target - excluding logographic languages ($(get_logographic_languages))" + fi + fi + + for language in $languages; do + if ! build_target_language "$target" "$language"; then + ((failed_builds++)) + # Stop building more languages for this target after first failure unless --continue-on-failure is set + if [[ "${CONTINUE_ON_FAILURE:-}" != "1" ]]; then + break + fi + fi + done + + local end_time=$(date +%s) + local elapsed=$((end_time - start_time)) + local elapsed_formatted + + # Format elapsed time nicely + if [[ $elapsed -ge 3600 ]]; then + # Hours, minutes, seconds + elapsed_formatted=$(printf "%dh %dm %ds" $((elapsed / 3600)) $(((elapsed % 3600) / 60)) $((elapsed % 60))) + elif [[ $elapsed -ge 60 ]]; then + # Minutes and seconds + elapsed_formatted=$(printf "%dm %ds" $((elapsed / 60)) $((elapsed % 60))) + else + # Just seconds + elapsed_formatted="${elapsed}s" + fi + + if [[ $failed_builds -gt 0 ]]; then + echo "Warning: $failed_builds language builds failed for target $target (${elapsed_formatted})" + return 1 + else + echo "Target $target completed successfully (${elapsed_formatted})" + fi + + return 0 +} + +# Function to build all targets with all valid languages +build_all_targets() { + local all_targets + local failed_targets=0 + local overall_start_time=$(date +%s) + + all_targets=$(get_all_targets) + + echo "Building all firmware variants with appropriate languages..." + echo + echo "Detected targets: $(echo $all_targets | tr ' ' ',')" + echo + + for target in $all_targets; do + echo "========================================" + echo "Processing target: $target" + echo "========================================" + + if ! build_target_all_languages "$target"; then + ((failed_targets++)) + echo "Failed to build all languages for target $target" + fi + + echo + done + + # Calculate overall elapsed time + local overall_end_time=$(date +%s) + local overall_elapsed=$((overall_end_time - overall_start_time)) + + # Format overall elapsed time + local overall_elapsed_formatted + if [[ $overall_elapsed -ge 3600 ]]; then + overall_elapsed_formatted=$(printf "%dh %dm %ds" $((overall_elapsed / 3600)) $(((overall_elapsed % 3600) / 60)) $((overall_elapsed % 60))) + elif [[ $overall_elapsed -ge 60 ]]; then + overall_elapsed_formatted=$(printf "%dm %ds" $((overall_elapsed / 60)) $((overall_elapsed % 60))) + else + overall_elapsed_formatted="${overall_elapsed}s" + fi + + if [[ $failed_targets -gt 0 && "${CONTINUE_ON_FAILURE:-}" != "1" ]]; then + echo "=========================================" + echo "BUILD SUMMARY" + echo "=========================================" + printf "\033[31m✗\033[0m Warning: %d targets had build failures\n" "$failed_targets" + echo "Total build time: ${overall_elapsed_formatted}" + return 1 + else + echo "=========================================" + echo "BUILD SUMMARY" + echo "=========================================" + if [[ $failed_targets -gt 0 && "${CONTINUE_ON_FAILURE:-}" == "1" ]]; then + printf "\033[32m✓\033[0m All targets attempted successfully!\n" + echo " Note: Some language builds failed but were ignored due to --continue-on-failure" + else + printf "\033[32m✓\033[0m All targets built successfully!\n" + fi + echo "Total build time: ${overall_elapsed_formatted}" + return 0 + fi +} + +show_usage() { + echo "EdgeTX Multi-Language Multi-Variant Build Script" + echo + echo "Usage: $0 [OPTIONS] [TARGET]" + echo + echo "Options:" + echo " -h, --help Show this help message" + echo " -l, --list-targets List all available firmware targets" + echo " -c, --list-color List color screen targets" + echo " -b, --list-bw List black & white screen targets" + echo " -L, --list-languages List all available languages (auto-detected)" + echo " --list-logographic List logographic/character-based languages" + echo " --list-color-targets-detected Show auto-detected color screen targets" + echo " --list-processors List targets grouped by STM32 processor type" + echo " -n, --dry-run Show what would be built without building" + echo " -f, --force-all Force build all languages for target (ignores screen type)" + echo " --continue-on-failure Continue building other languages/targets even after failures" + echo " --color-only Build all color screen targets only" + echo " --bw-only Build all black & white screen targets only" + echo " --processor PROC Build targets with specific STM32 processor (F2|F4|H7)" + echo " --exclude-processor PROC Build targets excluding specific processor" + echo " --filter-processor PROC Filter list commands by processor (F2|F4|H7|exclude-F2|etc.)" + echo + echo "Arguments:" + echo " TARGET [TARGET...] Build all valid languages for specific target(s)" + echo " (if not specified, builds all targets with screen type rules)" + echo + echo "Examples:" + echo " $0 # Build all languages for all targets" + echo " $0 tx16s # Build all languages for TX16S" + echo " $0 tx16s gx12 x9d # Build all languages for multiple targets" + echo " $0 x7 # Build non-logographic languages for X7 (B&W)" + echo " $0 -f x7 # Force build ALL languages for X7 (including CN,JP,KO,TW)" + echo " $0 -f tx16s st16 x9d # Force build ALL languages for multiple targets" + echo " $0 --color-only # Build all color screen targets with appropriate languages" + echo " $0 --bw-only # Build all B&W screen targets (excluding logographic)" + echo " $0 --bw-only -f # Build all B&W screen targets with ALL languages" + echo " $0 --processor F2 # Build all F2 processor targets" + echo " $0 --bw-only --processor F2 # Build F2 B&W targets only" + echo " $0 --exclude-processor F2 # Build all targets except F2" + echo " $0 -l # List all available targets" + echo " $0 -l --filter-processor F4 # List only F4 targets" + echo " $0 -c --filter-processor H7 # List only H7 color screen targets" + echo " $0 -b --filter-processor exclude-F2 # List B&W targets excluding F2" + echo " $0 --list-processors # Show all targets grouped by processor type" + echo " $0 -n x7 # Show what languages would be built for X7" + echo " $0 -n tx16s gx12 # Show what would be built for multiple targets" + echo " $0 --continue-on-failure x7 # Build all languages for X7 even if some fail" + echo " $0 --continue-on-failure --color-only # Build all color targets, continue on failures" + echo + echo "Language Rules:" + echo " - Color screen radios: All languages ($(get_radio_languages))" + echo " - B&W screen radios: All languages except logographic ($(get_logographic_languages))" + echo " - Force mode (-f): All languages regardless of screen type" + echo + echo "Auto-Detection:" + echo " - Languages: Parsed from radio/src/CMakeLists.txt" + echo " - Color targets: Detected from radio/src/targets/*/CMakeLists.txt" + echo " - Logographic languages: Configurable (override with LOGOGRAPHIC_LANGUAGES_OVERRIDE)" +} + +# Function to list targets by screen type with optional processor filtering +list_targets_by_type() { + local screen_filter="$1" # "color", "bw", or "all" + local processor_filter="$2" # Optional: "F2", "F4", "H7", "exclude-F2", etc. + + echo "Scanning for available targets..." >&2 + local all_targets + local color_targets="" + local bw_targets="" + + all_targets=$(get_all_targets) + + # First, categorize by screen type + for target in $all_targets; do + if is_color_screen_target "$target"; then + color_targets="$color_targets $target" + else + bw_targets="$bw_targets $target" + fi + done + + # Apply processor filter if specified + if [[ -n "$processor_filter" ]]; then + local filtered_color_targets="" + local filtered_bw_targets="" + + if [[ "$processor_filter" == exclude-* ]]; then + # Exclude specific processor + local exclude_proc="${processor_filter#exclude-}" + for target in $color_targets; do + local target_processor=$(get_stm32_processor_type "$target") + if [[ "$target_processor" != "$exclude_proc" && "$target_processor" != "UNKNOWN" ]]; then + filtered_color_targets="$filtered_color_targets $target" + fi + done + for target in $bw_targets; do + local target_processor=$(get_stm32_processor_type "$target") + if [[ "$target_processor" != "$exclude_proc" && "$target_processor" != "UNKNOWN" ]]; then + filtered_bw_targets="$filtered_bw_targets $target" + fi + done + else + # Include only specific processor + for target in $color_targets; do + local target_processor=$(get_stm32_processor_type "$target") + if [[ "$target_processor" == "$processor_filter" ]]; then + filtered_color_targets="$filtered_color_targets $target" + fi + done + for target in $bw_targets; do + local target_processor=$(get_stm32_processor_type "$target") + if [[ "$target_processor" == "$processor_filter" ]]; then + filtered_bw_targets="$filtered_bw_targets $target" + fi + done + fi + + color_targets="$filtered_color_targets" + bw_targets="$filtered_bw_targets" + fi + + # Display results based on screen filter + case "$screen_filter" in + "color") + if [[ -n "$processor_filter" ]]; then + echo "Color screen targets with processor filter ($processor_filter):" + else + echo "Color screen targets:" + fi + for target in $color_targets; do + echo " $target ($(get_stm32_processor_type "$target"))" + done + ;; + "bw") + if [[ -n "$processor_filter" ]]; then + echo "Black & white screen targets with processor filter ($processor_filter):" + else + echo "Black & white screen targets:" + fi + for target in $bw_targets; do + echo " $target ($(get_stm32_processor_type "$target"))" + done + ;; + "all") + if [[ -n "$processor_filter" ]]; then + echo "Color screen targets with processor filter ($processor_filter):" + else + echo "Color screen targets:" + fi + for target in $color_targets; do + echo " $target ($(get_stm32_processor_type "$target"))" + done + echo + if [[ -n "$processor_filter" ]]; then + echo "Black & white screen targets with processor filter ($processor_filter):" + else + echo "Black & white screen targets:" + fi + for target in $bw_targets; do + echo " $target ($(get_stm32_processor_type "$target"))" + done + ;; + esac +} + +# Function to list targets by processor type +list_targets_by_processor() { + local all_targets=$(get_all_targets) + local f2_targets="" + local f4_targets="" + local h7_targets="" + local unknown_targets="" + + for target in $all_targets; do + local processor=$(get_stm32_processor_type "$target") + case "$processor" in + F2) f2_targets="$f2_targets $target" ;; + F4) f4_targets="$f4_targets $target" ;; + H7) h7_targets="$h7_targets $target" ;; + *) unknown_targets="$unknown_targets $target" ;; + esac + done + + echo "Targets grouped by STM32 processor type:" + echo + echo "STM32F2 targets:" + for target in $f2_targets; do + echo " $target" + done + echo + echo "STM32F4 targets:" + for target in $f4_targets; do + echo " $target" + done + echo + echo "STM32H7 targets:" + for target in $h7_targets; do + echo " $target" + done + + if [[ -n "$unknown_targets" ]]; then + echo + echo "Unknown processor targets:" + for target in $unknown_targets; do + echo " $target" + done + fi +} + +# Main script logic +# Parse force flag first +FORCE_ALL_LANGUAGES=0 +DRY_RUN=0 +CONTINUE_ON_FAILURE=0 +TARGET="" +SCREEN_TYPE_FILTER="" +PROCESSOR_FILTER="" +LIST_PROCESSOR_FILTER="" +ACTION="" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_usage + exit 0 + ;; + -l|--list-targets) + ACTION="list-all" + shift + ;; + -c|--list-color) + ACTION="list-color" + shift + ;; + -b|--list-bw) + ACTION="list-bw" + shift + ;; + -L|--list-languages) + ACTION="list-languages" + shift + ;; + --list-logographic) + ACTION="list-logographic" + shift + ;; + --list-color-targets-detected) + ACTION="list-color-detected" + shift + ;; + --list-processors) + ACTION="list-processors" + shift + ;; + -n|--dry-run) + DRY_RUN=1 + shift + ;; + -f|--force-all) + FORCE_ALL_LANGUAGES=1 + shift + ;; + --continue-on-failure) + CONTINUE_ON_FAILURE=1 + shift + ;; + --color-only) + SCREEN_TYPE_FILTER="color" + shift + ;; + --bw-only) + SCREEN_TYPE_FILTER="bw" + shift + ;; + --processor) + if [[ -n "${2:-}" && ! "$2" =~ ^- ]]; then + PROCESSOR_FILTER="$2" + shift 2 + else + echo "Error: --processor requires a processor type (F2|F4|H7)" + exit 1 + fi + ;; + --exclude-processor) + if [[ -n "${2:-}" && ! "$2" =~ ^- ]]; then + PROCESSOR_FILTER="exclude-$2" + shift 2 + else + echo "Error: --exclude-processor requires a processor type (F2|F4|H7)" + exit 1 + fi + ;; + --filter-processor) + if [[ -n "${2:-}" && ! "$2" =~ ^- ]]; then + LIST_PROCESSOR_FILTER="$2" + shift 2 + else + echo "Error: --filter-processor requires a processor type (F2|F4|H7|exclude-F2|etc.)" + exit 1 + fi + ;; + -*) + echo "Unknown option: $1" + show_usage + exit 1 + ;; + *) + if [[ -z "$TARGET" ]]; then + TARGET="$1" + else + # Support multiple targets - append to TARGET with space separator + TARGET="$TARGET $1" + fi + shift + ;; + esac +done + +# Execute actions after all arguments are parsed +case "$ACTION" in + "list-all") + list_targets_by_type "all" "$LIST_PROCESSOR_FILTER" + exit 0 + ;; + "list-color") + list_targets_by_type "color" "$LIST_PROCESSOR_FILTER" + exit 0 + ;; + "list-bw") + list_targets_by_type "bw" "$LIST_PROCESSOR_FILTER" + exit 0 + ;; + "list-languages") + echo "All available languages:" + for lang in $(get_radio_languages); do + echo " $lang" + done + exit 0 + ;; + "list-logographic") + echo "Logographic/character-based languages:" + for lang in $(get_logographic_languages); do + echo " $lang" + done + echo + echo "These languages are not built for B&W screen radios (unless --force-all is used)." + exit 0 + ;; + "list-color-detected") + echo "Auto-detected color screen targets:" + for target in $(get_color_screen_targets); do + echo " $target" + done + exit 0 + ;; + "list-processors") + list_targets_by_processor + exit 0 + ;; +esac + +# Export flags for use in functions +export FORCE_ALL_LANGUAGES +export DRY_RUN +export CONTINUE_ON_FAILURE + +# Validate processor filter if specified +if [[ -n "$PROCESSOR_FILTER" ]]; then + proc="${PROCESSOR_FILTER#exclude-}" + if [[ "$proc" != "F2" && "$proc" != "F4" && "$proc" != "H7" ]]; then + echo "Error: Invalid processor type '$proc'. Valid options: F2, F4, H7" + exit 1 + fi +fi + +# Validate list processor filter if specified +if [[ -n "$LIST_PROCESSOR_FILTER" ]]; then + proc="${LIST_PROCESSOR_FILTER#exclude-}" + if [[ "$proc" != "F2" && "$proc" != "F4" && "$proc" != "H7" ]]; then + echo "Error: Invalid filter processor type '$proc'. Valid options: F2, F4, H7, exclude-F2, exclude-F4, exclude-H7" + exit 1 + fi +fi + +# Handle screen type filtering (--color-only or --bw-only) +if [[ -n "$SCREEN_TYPE_FILTER" ]]; then + if [[ -n "$TARGET" ]]; then + echo "Error: Cannot specify both screen type filter and individual target" + show_usage + exit 1 + fi + + build_targets_by_screen_type "$SCREEN_TYPE_FILTER" "$PROCESSOR_FILTER" + exit $? +fi + +# Handle processor-only filtering (--processor or --exclude-processor without screen filter) +if [[ -n "$PROCESSOR_FILTER" && -z "$SCREEN_TYPE_FILTER" ]]; then + if [[ -n "$TARGET" ]]; then + echo "Error: Cannot specify both processor filter and individual target" + show_usage + exit 1 + fi + + # Build all targets (both color and B&W) with processor filter + filtered_targets="" + if [[ "$PROCESSOR_FILTER" == exclude-* ]]; then + filtered_targets=$(get_targets_excluding_processor "${PROCESSOR_FILTER#exclude-}") + else + filtered_targets=$(get_targets_by_processor "$PROCESSOR_FILTER") + fi + + if [[ -z "$filtered_targets" ]]; then + echo "No targets match the processor filter: $PROCESSOR_FILTER" + exit 0 + fi + + echo "Building targets with processor filter: $PROCESSOR_FILTER" + echo "Selected targets: $filtered_targets" + echo + + failed_targets=0 + for target in $filtered_targets; do + echo "========================================" + echo "Processing target: $target ($(get_stm32_processor_type "$target") processor)" + echo "========================================" + + if ! build_target_all_languages "$target"; then + ((failed_targets++)) + echo "Failed to build all languages for target $target" + fi + + echo + done + + if [[ $failed_targets -gt 0 ]]; then + echo "Warning: $failed_targets targets had build failures" + exit 1 + else + echo "All targets built successfully!" + exit 0 + fi +fi + +# Handle dry-run mode +if [[ $DRY_RUN -eq 1 ]]; then + if [[ -n "$TARGET" ]]; then + all_targets=$(get_all_targets) + + # Convert TARGET string to array and validate all targets exist + read -ra target_array <<< "$TARGET" + for target in "${target_array[@]}"; do + if ! echo "$all_targets" | grep -q "^$target$"; then + echo "Error: Unknown target '$target'" + echo + echo "Available targets:" + list_targets_by_type "all" + exit 1 + fi + done + + # Build all specified targets in dry-run mode + echo "DRY-RUN: Building specified targets: ${target_array[*]}" + echo + + for target in "${target_array[@]}"; do + echo "========================================" + echo "Processing target: $target" + echo "========================================" + + build_target_all_languages "$target" + echo + done + exit 0 + fi + # If no specific target, let it fall through to the default behavior below + # The dry-run logic is handled in build_single_target function +fi + +# Handle target building +if [[ -n "$TARGET" ]]; then + all_targets=$(get_all_targets) + + # Convert TARGET string to array and validate all targets exist + read -ra target_array <<< "$TARGET" + for target in "${target_array[@]}"; do + if ! echo "$all_targets" | grep -q "^$target$"; then + echo "Error: Unknown target '$target'" + echo + echo "Available targets:" + list_targets_by_type "all" + exit 1 + fi + done + + # Build all specified targets + failed_targets=0 + overall_start_time=$(date +%s) + echo "Building specified targets: ${target_array[*]}" + echo + + # Temporarily disable exit on error for target building + set +e + + for target in "${target_array[@]}"; do + echo "========================================" + echo "Processing target: $target" + echo "========================================" + + if ! build_target_all_languages "$target"; then + ((failed_targets++)) + echo "Failed to build all languages for target $target" + fi + + echo + done + + # Re-enable exit on error + set -e + + # Calculate overall elapsed time + overall_end_time=$(date +%s) + overall_elapsed=$((overall_end_time - overall_start_time)) + + # Format overall elapsed time + if [[ $overall_elapsed -ge 3600 ]]; then + overall_elapsed_formatted=$(printf "%dh %dm %ds" $((overall_elapsed / 3600)) $(((overall_elapsed % 3600) / 60)) $((overall_elapsed % 60))) + elif [[ $overall_elapsed -ge 60 ]]; then + overall_elapsed_formatted=$(printf "%dm %ds" $((overall_elapsed / 60)) $((overall_elapsed % 60))) + else + overall_elapsed_formatted="${overall_elapsed}s" + fi + + if [[ $failed_targets -gt 0 && "${CONTINUE_ON_FAILURE:-}" != "1" ]]; then + echo "=========================================" + echo "BUILD SUMMARY" + echo "=========================================" + printf "\033[31m✗\033[0m Warning: %d targets had build failures\n" "$failed_targets" + echo "Total build time: ${overall_elapsed_formatted}" + exit 1 + else + echo "=========================================" + echo "BUILD SUMMARY" + echo "=========================================" + if [[ $failed_targets -gt 0 && "${CONTINUE_ON_FAILURE:-}" == "1" ]]; then + printf "\033[32m✓\033[0m All targets attempted successfully!\n" + echo " Note: Some language builds failed but were ignored due to --continue-on-failure" + else + printf "\033[32m✓\033[0m All targets built successfully!\n" + fi + echo "Total build time: ${overall_elapsed_formatted}" + exit 0 + fi +else + # No target specified - build all targets + build_all_targets + exit $? +fi