Browse Source

Merge pull request #3641 from vintagepc/build-with-cmake

Build the firmware with cmake
vintagepc 2 years ago
parent
commit
d8c9c4450f

+ 5 - 1
.gitignore

@@ -2,8 +2,11 @@
 /.settings
 /.project
 /.cproject
-/.vscode
 
+#cmake
+/build/
+/build_gen/
+.dependencies
 # Temporary configuration
 /Firmware/Configuration_prusa.h
 
@@ -24,3 +27,4 @@ __pycache__
 # Generated files
 /build-env/
 /Firmware/Doc/
+compile_commands.json

+ 9 - 0
.vscode/cmake-kits.json

@@ -0,0 +1,9 @@
+[
+    {
+        "name": "Local_avr-gcc-none-eabi",
+        "toolchainFile": "${workspaceFolder}/cmake/LocalAvrGcc.cmake",
+        "cmakeSettings": {
+            "CMAKE_MAKE_PROGRAM": "${workspaceFolder}/.dependencies/ninja-1.10.2/ninja"
+        }
+    }
+]

+ 11 - 0
.vscode/cmake-variants.yaml

@@ -0,0 +1,11 @@
+buildType:
+  default: debug
+  choices:
+    debug:
+      short: Debug
+      long: Emit debug information
+      buildType: Debug
+    release:
+      short: Release
+      long: Optimize generated code
+      buildType: Release

+ 9 - 0
.vscode/settings.json

@@ -0,0 +1,9 @@
+{
+    "cmake.configureOnOpen": true,
+    "cmake.copyCompileCommands": "${workspaceFolder}/compile_commands.json",
+    "cmake.cmakePath": "${workspaceFolder}/.dependencies/cmake-3.22.5/bin/cmake",
+    "files.insertFinalNewline": true,
+    "files.associations": {
+        "xlocale": "cpp"
+    }
+}

+ 494 - 18
CMakeLists.txt

@@ -1,23 +1,499 @@
-cmake_minimum_required(VERSION 3.1)
+cmake_minimum_required(VERSION 3.15)
+include(cmake/Utilities.cmake)
+include(cmake/GetGitRevisionDescription.cmake)
 
-set (CMAKE_CXX_STANDARD 11)
+SET(PROJECT_VERSION_SUFFIX
+    "<auto>"
+    CACHE
+      STRING
+      "Full version suffix to be shown on the info screen in settings (e.g. full_version=4.0.3-BETA+1035.PR111.B4, suffix=-BETA+1035.PR111.B4). Defaults to '+<commit sha>.<dirty?>.<debug?>' if set to '<auto>'."
+    )
+SET(PROJECT_VERSION_SUFFIX_SHORT
+    "<auto>"
+    CACHE
+      STRING
+      "Short version suffix to be shown on splash screen. Defaults to '+<BUILD_NUMBER>' if set to '<auto>'."
+    )
+SET(BUILD_NUMBER
+    ""
+    CACHE STRING "Build number of the firmware. Resolved automatically if not specified."
+    )
 
-project(cmake_test)
 
-# Prepare "Catch" library for other executables
-set(CATCH_INCLUDE_DIR Catch2)
-add_library(Catch INTERFACE)
-target_include_directories(Catch INTERFACE ${CATCH_INCLUDE_DIR})
+INCLUDE(cmake/ProjectVersion.cmake)
+resolve_version_variables()
 
-# Make test executable
-set(TEST_SOURCES 
-	Tests/tests.cpp
-	Tests/Example_test.cpp
-	Tests/Timer_test.cpp
-	Tests/AutoDeplete_test.cpp
-	Tests/PrusaStatistics_test.cpp
-	Firmware/Timer.cpp
+
+SET(PROJECT_VERSION_FLAVOUR "" CACHE STRING "Firmware flavour to build - DEBUG, DEVEL, APLHA, BETA or RC")
+SET(PROJECT_VERSION_FLAVOUR_REVISION "" CACHE STRING "Firmware flavour version, e.g. 1 for RC1, etc")
+
+
+IF( NOT PROJECT_VERSION_FLAVOUR STREQUAL "")
+    SET(PROJECT_VERSION "${PROJECT_VERSION}-${PROJECT_VERSION_FLAVOUR}")
+    add_compile_definitions(FW_FLAVOR=${PROJECT_VERSION_FLAVOUR})
+    IF( NOT PROJECT_VERSION_FLAVOUR_REVISION STREQUAL "")
+        SET(PROJECT_VERSION "${PROJECT_VERSION}${PROJECT_VERSION_FLAVOUR_REVISION}")
+        add_compile_definitions(FW_FLAVERSION=${PROJECT_VERSION_FLAVOUR_REVISION})
+    ENDIF()
+ENDIF()
+
+# Inform user about the resolved settings
+MESSAGE(STATUS "Project version: ${PROJECT_VERSION}")
+MESSAGE(
+  STATUS "Project version with short suffix: ${PROJECT_VERSION}${PROJECT_VERSION_SUFFIX_SHORT}"
+  )
+
+SET(FN_PREFIX "FW${PROJECT_VERSION}${PROJECT_VERSION_SUFFIX_SHORT}")
+
+MESSAGE(WARNING     "
+***************** YOUR ATTENTION PLEASE *****************
+CMake support is experimental. There is no guarantee at this time. If you have problems you are encouraged to fall back to the tried-and-true methods.
+*********************** THANK YOU **********************
+We now return to your regularly scheduled Firmware Build."
+)
+
+OPTION(SECONDARY_LANGUAGES "Secondary language support in the firmware" ON)
+
+SET(MAIN_LANGUAGES cs de es fr it pl CACHE STRING "The list of 'main' languages to be included, in the correct order")
+SET(COMMUNITY_LANGUAGES nl ro hu hr sk sv no CACHE STRING "The list of community languages to be included, in the correct order")
+SET(SELECTED_LANGUAGES ${MAIN_LANGUAGES} ${COMMUNITY_LANGUAGES})
+
+get_dependency_directory(prusa3dboards PRUSA_BOARDS_DIR)
+project(Prusa-Firmware)
+
+FILE(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/Firmware/config.h MAX_SIZE_LINE REGEX "^#define \+LANG_SIZE_RESERVED \+")
+STRING(REGEX MATCH "0x[0-9]+" MAX_SIZE_HEX "${MAX_SIZE_LINE}")
+math(EXPR LANG_MAX_SIZE "${MAX_SIZE_HEX}" OUTPUT_FORMAT DECIMAL)
+message("Language maximum size (from config.h): ${LANG_MAX_SIZE} bytes")
+
+set (LANG_BIN_MAX 249856) # Ditto, this in xflash_layout.h but needs invocation of the preprocessor... :-/
+
+get_recommended_gcc_version(RECOMMENDED_TOOLCHAIN_VERSION)
+if(CMAKE_CROSSCOMPILING AND NOT CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL
+		${RECOMMENDED_TOOLCHAIN_VERSION}
+	)
+	message(WARNING "Recommended AVR toolchain is ${RECOMMENDED_TOOLCHAIN_VERSION}"
+					", but you have ${CMAKE_CXX_COMPILER_VERSION}"
+	)
+
+elseif(NOT CMAKE_CROSSCOMPILING AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
+	message(
+		WARNING
+			"Recommended compiler for host tools and unittests is GCC, you have ${CMAKE_CXX_COMPILER_ID}."
+		)
+endif()
+
+# append custom C/C++ flags
+if(CUSTOM_COMPILE_OPTIONS)
+	string(REPLACE " " ";" CUSTOM_COMPILE_OPTIONS "${CUSTOM_COMPILE_OPTIONS}")
+	add_compile_options(${CUSTOM_COMPILE_OPTIONS})
+endif()
+
+#
+# Global Compiler & Linker Configuration
+#
+
+# include symbols
+add_compile_options(-g)
+
+#
+# Firmware - get file lists.
+#
+SET(FW_SOURCES
+    adc.cpp
+    backlight.cpp
+    BlinkM.cpp
+    bootapp.c
+    cardreader.cpp
+    cmdqueue.cpp
+    Configuration.cpp
+    ConfigurationStore.cpp
+    conv2str.cpp
+    Dcodes.cpp
+    eeprom.cpp
+    fancheck.cpp
+    Filament_sensor.cpp
+    first_lay_cal.cpp
+    heatbed_pwm.cpp
+    la10compat.cpp
+    language.c
+    lcd.cpp
+    Marlin_main.cpp
+    MarlinSerial.cpp
+    menu.cpp
+    mesh_bed_calibration.cpp
+    mesh_bed_leveling.cpp
+    messages.cpp
+    mmu2.cpp
+    mmu2_crc.cpp
+    mmu2_error_converter.cpp
+    mmu2_fsensor.cpp
+    mmu2_log.cpp
+    mmu2_power.cpp
+    mmu2_progress_converter.cpp
+    mmu2_protocol.cpp
+    mmu2_protocol_logic.cpp
+    mmu2_reporting.cpp
+    mmu2_serial.cpp
+    motion_control.cpp
+    optiboot_xflash.cpp
+    pat9125.cpp
+    planner.cpp
+    Prusa_farm.cpp
+    qr_solve.cpp
+    rbuf.c
+    Sd2Card.cpp
+    SdBaseFile.cpp
+    SdFatUtil.cpp
+    SdFile.cpp
+    SdVolume.cpp
+    Servo.cpp
+    sm4.c
+    sound.cpp
+    speed_lookuptable.cpp
+    spi.c
+    SpoolJoin.cpp
+    stepper.cpp
+    swi2c.c
+    swspi.cpp
+    Tcodes.cpp
+    temperature.cpp
+    timer02.c
+    Timer.cpp
+    tmc2130.cpp
+    tone04.c
+    twi.cpp
+    uart2.c
+    ultralcd.cpp
+    util.cpp
+    vector_3.cpp
+    xflash.c
+    xflash_dump.cpp
+    xyzcal.cpp
 )
-add_executable(tests ${TEST_SOURCES})
-target_include_directories(tests PRIVATE Tests)
-target_link_libraries(tests Catch)
+list(TRANSFORM FW_SOURCES PREPEND ${CMAKE_CURRENT_SOURCE_DIR}/Firmware/)
+
+foreach(_FILE ${FW_SOURCES})
+    get_filename_component(_BASE ${_FILE} NAME)
+    set_property(SOURCE ${_FILE} APPEND_STRING PROPERTY COMPILE_FLAGS "-frandom-seed=${_BASE}.o")
+endforeach()
+
+
+set(AVR_SOURCES
+    wiring_digital.c
+    WInterrupts.c
+    wiring_pulse.c
+    hooks.c
+    wiring.c
+    wiring_analog.c
+    wiring_shift.c
+    CDC.cpp
+    PluggableUSB.cpp
+    HardwareSerial.cpp
+    HardwareSerial0.cpp
+    HardwareSerial1.cpp
+    HardwareSerial3.cpp
+    IPAddress.cpp
+    HardwareSerial2.cpp
+    Print.cpp
+    Stream.cpp
+    Tone.cpp
+    USBCore.cpp
+    WMath.cpp
+    WString.cpp
+    abi.cpp
+    main.cpp
+#   new.cpp # What happened to this? it was removed in 1.0.5-1 to 1.0.5.2?
+)
+list(TRANSFORM AVR_SOURCES PREPEND ${PRUSA_BOARDS_DIR}/cores/prusa_einsy_rambo/)
+
+foreach(_FILE ${AVR_SOURCES})
+    get_filename_component(_BASE ${_FILE} NAME)
+    set_property(SOURCE ${_FILE} APPEND_STRING PROPERTY COMPILE_FLAGS "-frandom-seed=core/${_BASE}.o")
+endforeach()
+
+# optimizations
+if(CMAKE_CROSSCOMPILING)
+	if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+		add_compile_options(-Og)
+	else()
+		add_compile_options(-Os)
+	endif()
+
+	# mcu related settings
+	set(MCU_FLAGS -mmcu=atmega2560 -DF_CPU=16000000L -DARDUINO=10819 -DARDUINO_AVR_PRUSA_EINSY_RAMBO -DARDUINO_ARCH_AVR)
+	add_compile_options(${MCU_FLAGS})
+
+    add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>)
+    add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics>)
+    add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>)
+    add_compile_options(-Wall -Wextra -Wno-expansion-to-defined -ffunction-sections -fdata-sections -MMD -flto -fno-fat-lto-objects)
+
+    # split and gc sections
+    add_link_options(-Os -g -flto -Wl,--gc-sections -mmcu=atmega2560 -Wl,-u,vfprintf -lprintf_flt -lm )
+
+    # Create this target before we apply the GC options
+    add_library(avr_core STATIC ${AVR_SOURCES})
+    target_include_directories(avr_core PRIVATE
+        ${PRUSA_BOARDS_DIR}/cores/prusa_einsy_rambo/
+        ${PRUSA_BOARDS_DIR}/variants/prusa_einsy_rambo/
+        )
+
+
+
+	# disable exceptions and related metadata
+	add_compile_options(-fno-unwind-tables)
+	add_link_options(-Wl,--defsym,__exidx_start=0,--defsym,__exidx_end=0)
+else()
+	if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+		add_compile_options(-O0)
+	else()
+		add_compile_options(-O2)
+	endif()
+endif()
+
+# enable all warnings (well, not all, but some)
+add_compile_options(-Wsign-compare)
+add_compile_options($<$<COMPILE_LANGUAGE:C>:-std=gnu11>)
+
+# support _DEBUG macro (some code uses to recognize debug builds)
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+	add_compile_definitions(_DEBUG)
+endif()
+
+
+
+# Setup language resources:
+# file(GLOB LANG_VARIANTS RELATIVE ${PROJECT_SOURCE_DIR}/lang/po ${PROJECT_SOURCE_DIR}/lang/po/Firmware_??.po)
+# string(REPLACE "Firmware_" "" LANG_VARIANTS "${LANG_VARIANTS}")
+# string(REPLACE ".po" "" LANG_VARIANTS "${LANG_VARIANTS}")
+# list(SORT LANG_VARIANTS)
+# message("Languages found: ${LANG_VARIANTS}")
+
+
+# Meta target to build absolutely everything
+add_custom_target(ALL_FIRMWARE)
+add_custom_target(ALL_ENGLISH)
+add_custom_target(ALL_MULTILANG)
+add_dependencies(ALL_FIRMWARE ALL_ENGLISH ALL_MULTILANG)
+
+function(add_base_binary variant_name)
+    add_executable(${variant_name} ${FW_SOURCES} ${FW_HEADERS} ${VARIANT_CFG_FILE})
+
+    set_target_properties(${variant_name} PROPERTIES CXX_STANDARD 17)
+
+    target_include_directories(${variant_name} PRIVATE
+        ${PRUSA_BOARDS_DIR}/cores/prusa_einsy_rambo/
+        ${PRUSA_BOARDS_DIR}/variants/prusa_einsy_rambo/
+        ${VARIANT_CFG_DIR} # Include the header for this variant.
+        ${CMAKE_SOURCE_DIR}/Firmware
+    )
+
+    target_link_libraries(${variant_name} avr_core)
+
+    #   # configure linker script
+    set(LINKER_SCRIPT ${PRUSA_BOARDS_DIR}/ldscripts/avr6.xn)
+    target_link_options(${variant_name} PUBLIC -Wl,-T,${LINKER_SCRIPT})
+
+    # limit the text section to 248K (256K - 8k reserved for the bootloader)
+    target_link_options(${variant_name} PUBLIC -Wl,--defsym=__TEXT_REGION_LENGTH__=248K)
+
+    # generate firmware.bin file
+    objcopy(${variant_name} "ihex" ".hex")
+
+    # produce ASM listing. Note we also specify the .map as a byproduct so it gets cleaned
+    # because link_options doesn't have a "generated outputs" feature.
+    add_custom_command(
+        TARGET ${variant_name} POST_BUILD COMMAND ${CMAKE_OBJDUMP} -CSd ${variant_name} > ${variant_name}.asm
+        BYPRODUCTS ${variant_name}.asm ${variant_name}.map
+    )
+
+    # inform about the firmware's size in terminal
+    add_custom_command(
+        TARGET ${variant_name} POST_BUILD COMMAND ${CMAKE_SIZE_UTIL} -C --mcu=atmega2560 ${variant_name}
+    )
+    report_size(${variant_name})
+
+    # generate linker map file
+    target_link_options(${variant_name} PUBLIC -Wl,-Map=${CMAKE_CURRENT_BINARY_DIR}/${variant_name}.map)
+
+    target_compile_definitions(${variant_name} PRIVATE
+       CMAKE_LANG_CONTROL
+    )
+endfunction()
+
+function(fw_add_variant variant_name)
+
+	# Create the Configuration_Prusa.h for this variant so it can be #included.
+	set(VARIANT_CFG_DIR "${CMAKE_CURRENT_BINARY_DIR}/include")
+	set(VARIANT_CFG_FILE "${VARIANT_CFG_DIR}/Configuration_prusa.h")
+	add_custom_command(OUTPUT ${VARIANT_CFG_FILE}
+		COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/Firmware/variants/${variant_name}.h ${VARIANT_CFG_FILE}
+		COMMENT "Generating Configuration_prusa.h for ${variant_name}"
+		BYPRODUCTS ${VARIANT_CFG_DIR}
+	)
+	STRING(REPLACE "1_75mm_" "" variant_name "${variant_name}")
+	STRING(REPLACE "-E3Dv6full" "" variant_name "${variant_name}")
+
+    SET(FW_EN "${variant_name}_EN-only")
+    SET(FW_MULTI "${variant_name}_Multilang")
+
+    add_base_binary(${FW_EN})
+	# target_compile_options(${variant_name} PRIVATE) # turn this on for lolz -Wdouble-promotion)
+
+    target_compile_definitions(${FW_EN} PUBLIC LANG_MODE=0)
+    add_custom_command(
+        TARGET ${FW_EN}
+        POST_BUILD
+        COMMAND ${CMAKE_OBJCOPY} -O ihex ${CMAKE_CURRENT_BINARY_DIR}/${FW_EN} ${CMAKE_BINARY_DIR}/${FN_PREFIX}-${FW_EN}.hex
+        BYPRODUCTS ${CMAKE_BINARY_DIR}/${FN_PREFIX}-${FW_EN}.hex
+        COMMENT "Generating ${variant_name} hex"
+        )
+		add_dependencies(ALL_ENGLISH "${FW_EN}")
+
+    if (NOT SECONDARY_LANGUAGES)
+		return() #Done, if no languages there's nothing else to do.
+	else()
+        add_base_binary(${FW_MULTI})
+        target_compile_definitions(${FW_MULTI} PUBLIC LANG_MODE=1)
+	endif()
+
+	#Construct language map
+	set(LANG_TMP_DIR ${CMAKE_BINARY_DIR}/${variant_name}/lang)
+	set(LANG_MAP ${LANG_TMP_DIR}/${variant_name}_lang.map)
+	set(LANG_FWBIN ${CMAKE_CURRENT_BINARY_DIR}/${variant_name}.bin)
+	set(LANG_FINAL_BIN ${LANG_TMP_DIR}/${variant_name}_lang.bin)
+	set(LANG_FINAL_HEX ${LANG_TMP_DIR}/${variant_name}_lang.hex)
+
+	add_custom_command(OUTPUT ${LANG_FWBIN}
+		COMMAND "${CMAKE_OBJCOPY}" -I ihex -O binary ${CMAKE_CURRENT_BINARY_DIR}/${variant_name}_Multilang.hex ${LANG_FWBIN}
+		DEPENDS ${FW_MULTI}
+	)
+	add_custom_command(OUTPUT ${LANG_MAP}
+		COMMAND ${CMAKE_SOURCE_DIR}/lang/lang-map.py "${FW_MULTI}" "${LANG_FWBIN}" > "${LANG_MAP}"
+		DEPENDS ${LANG_FWBIN}
+	)
+
+	set(LANG_BINS "")
+
+	foreach (LANG IN LISTS SELECTED_LANGUAGES)
+		set(LANG_BIN ${LANG_TMP_DIR}/${variant_name}_${LANG}.bin)
+
+		set(PO_FILE "${CMAKE_SOURCE_DIR}/lang/po/Firmware_${LANG}.po")
+		add_custom_command(OUTPUT ${LANG_BIN}
+	#      COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/lang/lang-check.py --no-warning --map "${LANG_MAP}" "${PO_FILE}"
+	#      COMMAND ${CMAKE_COMMAND} -E echo "Building lang_${LANG}.bin"
+			COMMAND ${CMAKE_SOURCE_DIR}/lang/lang-build.py ${LANG_MAP} ${PO_FILE} ${LANG_BIN}
+			# Check bin size:
+			COMMAND ${CMAKE_COMMAND} -DLANG_MAX_SIZE=${LANG_MAX_SIZE}
+				-DLANG_FILE=${LANG_BIN}
+				-P ${PROJECT_CMAKE_DIR}/Check_lang_size.cmake
+			DEPENDS ${LANG_MAP}
+			COMMENT "Generating ${variant_name}_${LANG}.bin from .po"
+		)
+		LIST(APPEND LANG_BINS ${LANG_BIN})
+
+	endforeach()
+	string(FIND ${variant_name} "MK3" HAS_XFLASH)
+	if (${HAS_XFLASH} GREATER_EQUAL 0)
+        add_custom_command( OUTPUT ${LANG_FINAL_BIN}
+            COMMAND ${CMAKE_COMMAND} -E cat ${LANG_BINS} > ${LANG_FINAL_BIN}
+            DEPENDS ${LANG_BINS}
+            COMMENT "Merging language binaries"
+        )
+		add_custom_command( OUTPUT ${LANG_FINAL_BIN}
+			COMMAND ${CMAKE_COMMAND} -DLANG_MAX_SIZE=${LANG_BIN_MAX}
+				-DLANG_FILE=${LANG_FINAL_BIN}
+				-P ${PROJECT_CMAKE_DIR}/Check_final_lang_bin_size.cmake
+			APPEND
+		)
+		add_custom_command( OUTPUT ${LANG_FINAL_HEX}
+			COMMAND ${CMAKE_OBJCOPY} -I binary -O ihex ${LANG_FINAL_BIN} ${LANG_FINAL_HEX}
+			DEPENDS ${LANG_FINAL_BIN}
+			COMMENT "Generating Hex for language data"
+		)
+		set(LANG_HEX ${CMAKE_BINARY_DIR}/${FN_PREFIX}-${variant_name}-Languages.hex)
+
+		add_custom_target(${variant_name}-language-hex
+			COMMAND ${CMAKE_COMMAND} -E copy ${FW_MULTI}.hex ${LANG_HEX}
+			COMMAND ${CMAKE_COMMAND} -E cat ${LANG_FINAL_HEX} >> ${LANG_HEX}
+			COMMENT "Generating final ${variant_name}-Languages.hex"
+			BYPRODUCTS ${LANG_HEX}
+			DEPENDS ${LANG_FINAL_HEX}
+		)
+		add_dependencies(ALL_MULTILANG ${variant_name}-language-hex)
+	else()
+		set (ALL_VARIANT_HEXES "")
+		# Non-xflash, e.g. MK2.5
+		foreach(LANG IN LISTS SELECTED_LANGUAGES)
+			SET(LANG_HEX_FN ${variant_name}-en_${LANG})
+			SET(LANG_HEX ${CMAKE_BINARY_DIR}/${FN_PREFIX}-${LANG_HEX_FN}.hex)
+			SET(LANG_BIN ${LANG_TMP_DIR}/${variant_name}_${LANG}.bin)
+			SET(LANG_FWBIN_TMP ${LANG_TMP_DIR}/${variant_name}-en_${LANG}.bin)
+
+			#Intermediate 2-lang bin
+			add_custom_command(OUTPUT ${LANG_FWBIN_TMP}
+				COMMAND ${CMAKE_COMMAND} -E copy ${LANG_FWBIN} ${LANG_FWBIN_TMP}
+				COMMAND ${CMAKE_SOURCE_DIR}/lang/lang-patchsec.py ${FW_MULTI} ${LANG_BIN} ${LANG_FWBIN_TMP}
+				DEPENDS ${LANG_FWBIN} ${LANG_BIN}
+				COMMENT "Generating ${variant_name}-en_${LANG}.bin"
+			)
+
+			#Final hex:
+			add_custom_target(${LANG_HEX_FN}
+				COMMAND ${CMAKE_OBJCOPY} -I binary -O ihex ${LANG_FWBIN_TMP} ${LANG_HEX}
+				BYPRODUCTS ${LANG_HEX}
+				DEPENDS ${LANG_FWBIN_TMP}
+				COMMENT "Creating ${LANG_HEX_FN}.hex"
+			)
+			LIST(APPEND ALL_VARIANT_HEXES ${LANG_HEX_FN})
+		endforeach()
+		add_custom_target("${variant_name}-All-Languages"
+			DEPENDS ${ALL_VARIANT_HEXES}
+		)
+		add_dependencies(ALL_MULTILANG "${variant_name}-All-Languages")
+	endif()
+endfunction()
+
+
+if(CMAKE_CROSSCOMPILING)
+
+	file(GLOB FW_VARIANTS RELATIVE ${PROJECT_SOURCE_DIR}/Firmware/variants ${PROJECT_SOURCE_DIR}/Firmware/variants/*.h)
+	foreach(THIS_VAR IN LISTS FW_VARIANTS)
+		string(REPLACE ".h" "" TRIMMED_NAME "${THIS_VAR}")
+		message("Variant added: ${TRIMMED_NAME}")
+        string(REPLACE "-E3Dv6full" "" DIR_NAME "${TRIMMED_NAME}")
+        string(REPLACE "1_75mm_" "" DIR_NAME "${DIR_NAME}")
+        # Generate a file in a subfolder so that we can organize things a little more neatly in VS code
+        FILE(MAKE_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/build_gen/${DIR_NAME})
+        FILE(WRITE ${CMAKE_CURRENT_SOURCE_DIR}/build_gen/${DIR_NAME}/CMakeLists.txt "project(${DIR_NAME})\nfw_add_variant(${TRIMMED_NAME})")
+        add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/build_gen/${DIR_NAME})
+		#fw_add_variant(${TRIMMED_NAME})
+	endforeach(THIS_VAR IN LISTS FW_VARIANTS)
+
+endif()
+
+if(NOT CMAKE_CROSSCOMPILING)
+	# do not build the firmware by default (tests are the focus if not crosscompiling)
+	project(cmake_test)
+
+	# Prepare "Catch" library for other executables
+	set(CATCH_INCLUDE_DIR Catch2)
+	add_library(Catch INTERFACE)
+	target_include_directories(Catch INTERFACE ${CATCH_INCLUDE_DIR})
+
+	# Make test executable
+	set(TEST_SOURCES
+		Tests/tests.cpp
+		Tests/Example_test.cpp
+		Tests/Timer_test.cpp
+		Tests/AutoDeplete_test.cpp
+		Tests/PrusaStatistics_test.cpp
+		Firmware/Timer.cpp
+		Firmware/AutoDeplete.cpp
+	)
+	add_executable(tests ${TEST_SOURCES})
+	target_include_directories(tests PRIVATE Tests)
+	target_link_libraries(tests Catch)
+
+endif()

+ 4 - 0
Firmware/config.h

@@ -60,9 +60,13 @@
 #define TMC2130_SPCR           SPI_SPCR(TMC2130_SPI_RATE, 1, 1, 1, 0)
 #define TMC2130_SPSR           SPI_SPSR(TMC2130_SPI_RATE)
 
+// This is set by the cmake build to be able to take control of
+// the language flag, without breaking existing build mechanisms.
+#ifndef CMAKE_LANG_CONTROL
 //LANG - Multi-language support
 //#define LANG_MODE              0 // primary language only
 #define LANG_MODE              1 // sec. language support
+#endif
 
 #define LANG_SIZE_RESERVED     0x3500 // reserved space for secondary language (13568 bytes).
                                       // 0x3D00 Maximum 15616 bytes as it depends on xflash_layout.h

+ 11 - 0
cmake/Check_final_lang_bin_size.cmake

@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 3.18)
+FILE(SIZE ${LANG_FILE} FILE_SIZE)
+get_filename_component(FILE_BASE ${LANG_FILE} NAME)
+MATH(EXPR PADDED_SIZE "((${FILE_SIZE}+4096-1) / 4096 * 4096 )")
+message(STATUS "${FILE_BASE} raw size ${FILE_SIZE} bytes (${PADDED_SIZE} b padded)")
+if(${PADDED_SIZE} GREATER ${LANG_MAX_SIZE})
+    message(FATAL_ERROR "Language file ${FILE_BASE} (${PADDED_SIZE}b) exceeds maximum allowed size of ${LANG_MAX_SIZE} bytes - Aborting!")
+else()
+    MATH(EXPR SIZE_PCT "( ${PADDED_SIZE} * 100) / ${LANG_MAX_SIZE} " )
+    message(STATUS "Language file ${FILE_BASE} is ${PADDED_SIZE} bytes, ${SIZE_PCT}% of allowed space - OK")
+endif()

+ 9 - 0
cmake/Check_lang_size.cmake

@@ -0,0 +1,9 @@
+cmake_minimum_required(VERSION 3.18)
+FILE(SIZE ${LANG_FILE} FILE_SIZE)
+get_filename_component(FILE_BASE ${LANG_FILE} NAME)
+if(${FILE_SIZE} GREATER ${LANG_MAX_SIZE})
+    message(FATAL_ERROR "Language file ${FILE_BASE} (${FILE_SIZE}b) exceeds maximum allowed size of ${LANG_MAX_SIZE} bytes - Aborting!")
+else()
+    MATH(EXPR SIZE_PCT "( ${FILE_SIZE} * 100) / ${LANG_MAX_SIZE} " )
+    message(STATUS "Language file ${FILE_BASE} is ${FILE_SIZE} bytes, ${SIZE_PCT}% of allowed space - OK")
+endif()

+ 232 - 0
cmake/GetGitRevisionDescription.cmake

@@ -0,0 +1,232 @@
+# * Returns a version string from Git
+#
+# These functions force a re-configure on each git commit so that you can trust the values of the
+# variables in your build system.
+#
+# get_git_head_revision(<refspecvar> <hashvar> [<additional arguments to git describe> ...])
+#
+# Returns the refspec and sha hash of the current head revision
+#
+# git_describe(<var> [<additional arguments to git describe> ...])
+#
+# Returns the results of git describe on the source tree, and adjusting the output so that it tests
+# false if an error occurs.
+#
+# git_get_exact_tag(<var> [<additional arguments to git describe> ...])
+#
+# Returns the results of git describe --exact-match on the source tree, and adjusting the output so
+# that it tests false if there was no exact matching tag.
+#
+# git_local_changes(<var>)
+#
+# Returns either "CLEAN" or "DIRTY" with respect to uncommitted changes. Uses the return code of
+# "git diff-index --quiet HEAD --". Does not regard untracked files.
+#
+# git_count_parent_commits(<var>)
+#
+# Returns number of commits preceeding current commit -1 if git rev-list --count HEAD failed or
+# "GIT-NOTFOUND" if git executable was not found or "HEAD-HASH-NOTFOUND" if head hash was not found.
+# I don't know if get_git_head_revision() must be called internally or not, as reason of calling it
+# is not clear for me also in git_local_changes().
+#
+# Requires CMake 2.6 or newer (uses the 'function' command)
+#
+# Original Author: 2009-2010 Ryan Pavlik <rpavlik@iastate.edu> <abiryan@ryand.net>
+# http://academic.cleardefinition.com Iowa State University HCI Graduate Program/VRAC
+#
+# Copyright Iowa State University 2009-2010. Distributed under the Boost Software License, Version
+# 1.0. (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+if(__get_git_revision_description)
+  return()
+endif()
+set(__get_git_revision_description YES)
+
+# We must run the following at "include" time, not at function call time, to find the path to this
+# module rather than the path to a calling list file
+get_filename_component(_gitdescmoddir ${CMAKE_CURRENT_LIST_FILE} PATH)
+
+function(get_git_head_revision _refspecvar _hashvar)
+  set(GIT_PARENT_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+  set(GIT_DIR "${GIT_PARENT_DIR}/.git")
+  while(NOT EXISTS "${GIT_DIR}") # .git dir not found, search parent directories
+    set(GIT_PREVIOUS_PARENT "${GIT_PARENT_DIR}")
+    get_filename_component(GIT_PARENT_DIR ${GIT_PARENT_DIR} PATH)
+    if(GIT_PARENT_DIR STREQUAL GIT_PREVIOUS_PARENT)
+      # We have reached the root directory, we are not in git
+      set(${_refspecvar}
+          "GITDIR-NOTFOUND"
+          PARENT_SCOPE
+          )
+      set(${_hashvar}
+          "GITDIR-NOTFOUND"
+          PARENT_SCOPE
+          )
+      return()
+    endif()
+    set(GIT_DIR "${GIT_PARENT_DIR}/.git")
+  endwhile()
+  # check if this is a submodule
+  if(NOT IS_DIRECTORY ${GIT_DIR})
+    file(READ ${GIT_DIR} submodule)
+    string(REGEX REPLACE "gitdir: (.*)\n$" "\\1" GIT_DIR_RELATIVE ${submodule})
+    get_filename_component(SUBMODULE_DIR ${GIT_DIR} PATH)
+    get_filename_component(GIT_DIR ${SUBMODULE_DIR}/${GIT_DIR_RELATIVE} ABSOLUTE)
+  endif()
+  set(GIT_DATA "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/git-data")
+  if(NOT EXISTS "${GIT_DATA}")
+    file(MAKE_DIRECTORY "${GIT_DATA}")
+  endif()
+
+  if(NOT EXISTS "${GIT_DIR}/HEAD")
+    return()
+  endif()
+  set(HEAD_FILE "${GIT_DATA}/HEAD")
+  configure_file("${GIT_DIR}/HEAD" "${HEAD_FILE}" COPYONLY)
+
+  configure_file(
+    "${_gitdescmoddir}/GetGitRevisionDescription.cmake.in" "${GIT_DATA}/grabRef.cmake" @ONLY
+    )
+  include("${GIT_DATA}/grabRef.cmake")
+
+  set(${_refspecvar}
+      "${HEAD_REF}"
+      PARENT_SCOPE
+      )
+  set(${_hashvar}
+      "${HEAD_HASH}"
+      PARENT_SCOPE
+      )
+endfunction()
+
+function(git_describe _var)
+  if(NOT GIT_FOUND)
+    find_package(Git QUIET)
+  endif()
+  get_git_head_revision(refspec hash)
+  if(NOT GIT_FOUND)
+    set(${_var}
+        "GIT-NOTFOUND"
+        PARENT_SCOPE
+        )
+    return()
+  endif()
+  if(NOT hash)
+    set(${_var}
+        "HEAD-HASH-NOTFOUND"
+        PARENT_SCOPE
+        )
+    return()
+  endif()
+
+  # TODO sanitize if((${ARGN}" MATCHES "&&") OR (ARGN MATCHES "||") OR (ARGN MATCHES "\\;"))
+  # message("Please report the following error to the project!") message(FATAL_ERROR "Looks like
+  # someone's doing something nefarious with git_describe! Passed arguments ${ARGN}") endif()
+
+  # message(STATUS "Arguments to execute_process: ${ARGN}")
+
+  execute_process(
+    COMMAND "${GIT_EXECUTABLE}" describe ${hash} ${ARGN}
+    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
+    RESULT_VARIABLE res
+    OUTPUT_VARIABLE out
+    ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE
+    )
+  if(NOT res EQUAL 0)
+    set(out "${out}-${res}-NOTFOUND")
+  endif()
+
+  set(${_var}
+      "${out}"
+      PARENT_SCOPE
+      )
+endfunction()
+
+function(git_get_exact_tag _var)
+  git_describe(out --exact-match ${ARGN})
+  set(${_var}
+      "${out}"
+      PARENT_SCOPE
+      )
+endfunction()
+
+function(git_local_changes _var)
+  if(NOT GIT_FOUND)
+    find_package(Git QUIET)
+  endif()
+  get_git_head_revision(refspec hash)
+  if(NOT GIT_FOUND)
+    set(${_var}
+        "GIT-NOTFOUND"
+        PARENT_SCOPE
+        )
+    return()
+  endif()
+  if(NOT hash)
+    set(${_var}
+        "HEAD-HASH-NOTFOUND"
+        PARENT_SCOPE
+        )
+    return()
+  endif()
+
+  execute_process(
+    COMMAND "${GIT_EXECUTABLE}" diff-index --quiet HEAD --
+    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
+    RESULT_VARIABLE res
+    OUTPUT_VARIABLE out
+    ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE
+    )
+  if(res EQUAL 0)
+    set(${_var}
+        "CLEAN"
+        PARENT_SCOPE
+        )
+  else()
+    set(${_var}
+        "DIRTY"
+        PARENT_SCOPE
+        )
+  endif()
+endfunction()
+
+function(git_count_parent_commits _var)
+  if(NOT GIT_FOUND)
+    find_package(Git QUIET)
+  endif()
+  get_git_head_revision(refspec hash)
+  if(NOT GIT_FOUND)
+    set(${_var}
+        "GIT-NOTFOUND"
+        PARENT_SCOPE
+        )
+    return()
+  endif()
+  if(NOT hash)
+    set(${_var}
+        "HEAD-HASH-NOTFOUND"
+        PARENT_SCOPE
+        )
+    return()
+  endif()
+
+  execute_process(
+    COMMAND "${GIT_EXECUTABLE}" rev-list --count HEAD
+    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
+    RESULT_VARIABLE res
+    OUTPUT_VARIABLE out
+    ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE
+    )
+  if(res EQUAL 0)
+    set(${_var}
+        "${out}"
+        PARENT_SCOPE
+        )
+  else()
+    set(${_var}
+        "-1"
+        PARENT_SCOPE
+        )
+  endif()
+
+endfunction()

+ 37 - 0
cmake/GetGitRevisionDescription.cmake.in

@@ -0,0 +1,37 @@
+#
+# Internal file for GetGitRevisionDescription.cmake
+#
+# Requires CMake 2.6 or newer (uses the 'function' command)
+#
+# Original Author: 2009-2010 Ryan Pavlik <rpavlik@iastate.edu> <abiryan@ryand.net>
+# http://academic.cleardefinition.com Iowa State University HCI Graduate Program/VRAC
+#
+# Copyright Iowa State University 2009-2010. Distributed under the Boost Software License, Version
+# 1.0. (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+set(HEAD_HASH)
+
+file(READ "@HEAD_FILE@" HEAD_CONTENTS LIMIT 1024)
+
+string(STRIP "${HEAD_CONTENTS}" HEAD_CONTENTS)
+if(HEAD_CONTENTS MATCHES "ref")
+  # named branch
+  string(REPLACE "ref: " "" HEAD_REF "${HEAD_CONTENTS}")
+  if(EXISTS "@GIT_DIR@/${HEAD_REF}")
+    configure_file("@GIT_DIR@/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY)
+  else()
+    configure_file("@GIT_DIR@/packed-refs" "@GIT_DATA@/packed-refs" COPYONLY)
+    file(READ "@GIT_DATA@/packed-refs" PACKED_REFS)
+    if(${PACKED_REFS} MATCHES "([0-9a-z]*) ${HEAD_REF}")
+      set(HEAD_HASH "${CMAKE_MATCH_1}")
+    endif()
+  endif()
+else()
+  # detached HEAD
+  configure_file("@GIT_DIR@/HEAD" "@GIT_DATA@/head-ref" COPYONLY)
+endif()
+
+if(NOT HEAD_HASH)
+  file(READ "@GIT_DATA@/head-ref" HEAD_HASH LIMIT 1024)
+  string(STRIP "${HEAD_HASH}" HEAD_HASH)
+endif()

+ 102 - 0
cmake/LocalAvrGcc.cmake

@@ -0,0 +1,102 @@
+get_filename_component(PROJECT_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
+include("${PROJECT_CMAKE_DIR}/Utilities.cmake")
+set(CMAKE_SYSTEM_NAME Generic)
+set(CMAKE_SYSTEM_PROCESSOR avr)
+set(CMAKE_CROSSCOMPILING 1)
+get_dependency_directory("avr-gcc" AVR_TOOLCHAIN_DIR)
+message( "tc dir is ${AVR_TOOLCHAIN_DIR}")
+#
+# Utilities
+
+if(MINGW
+   OR CYGWIN
+   OR WIN32
+   )
+  set(UTIL_SEARCH_CMD where)
+  set(EXECUTABLE_SUFFIX ".exe")
+elseif(UNIX OR APPLE)
+  set(UTIL_SEARCH_CMD which)
+  set(EXECUTABLE_SUFFIX "")
+endif()
+
+set(TOOLCHAIN_PREFIX avr-)
+
+#
+# Looking up the toolchain
+#
+
+if(AVR_TOOLCHAIN_DIR)
+  # using toolchain set by AvrGcc.cmake (locked version)
+  set(BINUTILS_PATH "${AVR_TOOLCHAIN_DIR}/bin")
+else()
+  # search for ANY avr-gcc toolchain
+  execute_process(
+    COMMAND ${UTIL_SEARCH_CMD} ${TOOLCHAIN_PREFIX}gcc
+    OUTPUT_VARIABLE AVR_GCC_PATH
+    OUTPUT_STRIP_TRAILING_WHITESPACE
+    RESULT_VARIABLE FIND_RESULT
+    )
+  # found?
+  if(NOT "${FIND_RESULT}" STREQUAL "0")
+    message(FATAL_ERROR "avr-gcc not found")
+  endif()
+  get_filename_component(BINUTILS_PATH "${AVR_GCC_PATH}" DIRECTORY)
+  get_filename_component(AVR_TOOLCHAIN_DIR ${BINUTILS_PATH} DIRECTORY)
+endif()
+
+#
+# Setup CMake
+#
+
+# Without that flag CMake is not able to pass test compilation check
+set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
+
+set(CMAKE_C_COMPILER
+    "${BINUTILS_PATH}/${TOOLCHAIN_PREFIX}gcc${EXECUTABLE_SUFFIX}"
+    CACHE FILEPATH "" FORCE
+    )
+set(CMAKE_ASM_COMPILER
+    "${BINUTILS_PATH}/${TOOLCHAIN_PREFIX}gcc${EXECUTABLE_SUFFIX}"
+    CACHE FILEPATH "" FORCE
+    )
+set(CMAKE_CXX_COMPILER
+    "${BINUTILS_PATH}/${TOOLCHAIN_PREFIX}g++${EXECUTABLE_SUFFIX}"
+    CACHE FILEPATH "" FORCE
+    )
+set(CMAKE_EXE_LINKER_FLAGS_INIT
+    ""
+    CACHE STRING "" FORCE
+    )
+
+set(CMAKE_ASM_COMPILE_OBJECT
+    "<CMAKE_ASM_COMPILER> <DEFINES> <FLAGS> -o <OBJECT> -c <SOURCE>"
+    CACHE STRING "" FORCE
+    )
+
+set(CMAKE_AR
+    "${BINUTILS_PATH}/${TOOLCHAIN_PREFIX}gcc-ar${EXECUTABLE_SUFFIX}"
+    CACHE FILEPATH "ar" FORCE
+    )
+
+set(CMAKE_RANLIB
+    "${BINUTILS_PATH}/${TOOLCHAIN_PREFIX}gcc-ranlib${EXECUTABLE_SUFFIX}"
+    CACHE FILEPATH "ranlib" FORCE
+    )
+
+set(CMAKE_OBJCOPY
+    "${BINUTILS_PATH}/${TOOLCHAIN_PREFIX}objcopy${EXECUTABLE_SUFFIX}"
+    CACHE INTERNAL "objcopy tool"
+    )
+set(CMAKE_OBJDUMP
+    "${BINUTILS_PATH}/${TOOLCHAIN_PREFIX}objdump${EXECUTABLE_SUFFIX}"
+    CACHE INTERNAL "objdump tool"
+    )
+set(CMAKE_SIZE_UTIL
+    "${BINUTILS_PATH}/${TOOLCHAIN_PREFIX}size${EXECUTABLE_SUFFIX}"
+    CACHE INTERNAL "size tool"
+    )
+
+set(CMAKE_FIND_ROOT_PATH "${AVR_TOOLCHAIN_DIR}")
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

+ 71 - 0
cmake/ProjectVersion.cmake

@@ -0,0 +1,71 @@
+#
+# This file is responsible for setting the following variables:
+#
+# ~~~
+# BUILD_NUMBER (1035)
+# PROJECT_VERSION (4.0.3)
+# PROJECT_VERSION_FULL (4.0.3-BETA+1035.PR111.B4)
+# PROJECT_VERSION_SUFFIX (-BETA+1035.PR111.B4)
+# PROJECT_VERSION_SUFFIX_SHORT (+1035)
+#
+# The `PROJECT_VERSION` variable is set as soon as the file is included.
+# To set the rest, the function `resolve_version_variables` has to be called.
+#
+# ~~~
+
+FILE(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/Firmware/Configuration.h CFG_VER_DATA REGEX "#define FW_[A-Z]+ ([0-9]+)" )
+LIST(GET CFG_VER_DATA 0 PROJECT_VERSION_MAJOR)
+LIST(GET CFG_VER_DATA 1 PROJECT_VERSION_MINOR)
+LIST(GET CFG_VER_DATA 2 PROJECT_VERSION_REV)
+STRING(REGEX MATCH "FW_MAJOR ([0-9]+)" PROJECT_VERSION_MAJOR "${PROJECT_VERSION_MAJOR}")
+SET(PROJECT_VERSION_MAJOR  "${CMAKE_MATCH_1}")
+
+STRING(REGEX MATCH "FW_MINOR ([0-9]+)" PROJECT_VERSION_MINOR "${PROJECT_VERSION_MINOR}")
+SET(PROJECT_VERSION_MINOR  ${CMAKE_MATCH_1})
+
+STRING(REGEX MATCH "FW_REVISION +([0-9]+)" PROJECT_VERSION_REV "${PROJECT_VERSION_REV}")
+SET(PROJECT_VERSION_REV  ${CMAKE_MATCH_1})
+
+SET(PROJECT_VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_REV}")
+SET(PROJECT_VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_REV}" PARENT_SCOPE)
+
+
+function(resolve_version_variables)
+  # BUILD_NUMBER
+  if(NOT BUILD_NUMBER)
+    git_count_parent_commits(BUILD_NUMBER)
+    set(ERRORS "GIT-NOTFOUND" "HEAD-HASH-NOTFOUND")
+    if(BUILD_NUMBER IN_LIST ERRORS)
+      message(WARNING "Failed to resolve build number: ${BUILD_NUMBER}. Setting to zero.")
+      set(BUILD_NUMBER "0")
+    endif()
+    set(BUILD_NUMBER
+        ${BUILD_NUMBER}
+        PARENT_SCOPE
+        )
+  endif()
+
+  # PROJECT_VERSION_SUFFIX
+  if(PROJECT_VERSION_SUFFIX STREQUAL "<auto>")
+    # TODO: set to +<sha>.dirty?.debug?
+    set(PROJECT_VERSION_SUFFIX "+${BUILD_NUMBER}.LOCAL")
+    set(PROJECT_VERSION_SUFFIX
+        "+${BUILD_NUMBER}.LOCAL"
+        PARENT_SCOPE
+        )
+  endif()
+
+  # PROJECT_VERSION_SUFFIX_SHORT
+  if(PROJECT_VERSION_SUFFIX_SHORT STREQUAL "<auto>")
+    set(PROJECT_VERSION_SUFFIX_SHORT
+        "+${BUILD_NUMBER}"
+        PARENT_SCOPE
+        )
+  endif()
+
+  # PROJECT_VERSION_FULL
+  set(PROJECT_VERSION_FULL
+      "${PROJECT_VERSION}${PROJECT_VERSION_SUFFIX}"
+      PARENT_SCOPE
+      )
+endfunction()

+ 64 - 0
cmake/Utilities.cmake

@@ -0,0 +1,64 @@
+get_filename_component(PROJECT_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
+get_filename_component(PROJECT_ROOT_DIR "${PROJECT_CMAKE_DIR}" DIRECTORY)
+
+find_package(Python3 COMPONENTS Interpreter)
+if(NOT Python3_FOUND)
+  message(FATAL_ERROR "Python3 not found.")
+endif()
+
+function(get_recommended_gcc_version var)
+  execute_process(
+    COMMAND "${Python3_EXECUTABLE}" "${PROJECT_ROOT_DIR}/utils/bootstrap.py"
+            "--print-dependency-version" "avr-gcc"
+    OUTPUT_VARIABLE RECOMMENDED_VERSION
+    OUTPUT_STRIP_TRAILING_WHITESPACE
+    RESULT_VARIABLE RETVAL
+    )
+
+  if(NOT "${RETVAL}" STREQUAL "0")
+    message(FATAL_ERROR "Failed to obtain recommended gcc version from utils/bootstrap.py")
+  endif()
+
+  set(${var}
+      ${RECOMMENDED_VERSION}
+      PARENT_SCOPE
+      )
+endfunction()
+
+function(get_dependency_directory dependency var)
+  execute_process(
+    COMMAND "${Python3_EXECUTABLE}" "${PROJECT_ROOT_DIR}/utils/bootstrap.py"
+            "--print-dependency-directory" "${dependency}"
+    OUTPUT_VARIABLE DEPENDENCY_DIRECTORY
+    OUTPUT_STRIP_TRAILING_WHITESPACE
+    RESULT_VARIABLE RETVAL
+    )
+
+  if(NOT "${RETVAL}" STREQUAL "0")
+    message(FATAL_ERROR "Failed to find directory with ${dependency}")
+  endif()
+
+  set(${var}
+      ${DEPENDENCY_DIRECTORY}
+      PARENT_SCOPE
+      )
+endfunction()
+
+function(objcopy target format suffix)
+  add_custom_command(
+    TARGET ${target} POST_BUILD
+    COMMAND "${CMAKE_OBJCOPY}" -O ${format} -S "$<TARGET_FILE:${target}>"
+            "${CMAKE_CURRENT_BINARY_DIR}/${target}${suffix}"
+    COMMENT "Generating ${format} from ${target}..."
+    BYPRODUCTS "${CMAKE_CURRENT_BINARY_DIR}/${target}${suffix}"
+    )
+endfunction()
+
+function(report_size target)
+  add_custom_command(
+    TARGET ${target} POST_BUILD
+    COMMAND echo "" # visually separate the output
+    COMMAND "${CMAKE_SIZE_UTIL}" -B "$<TARGET_FILE:${target}>"
+    USES_TERMINAL
+    )
+endfunction()

+ 197 - 0
utils/bootstrap.py

@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+#
+# Bootstrap Script
+#
+# This script
+#  1) records the recommended versions of dependencies, and
+#  2) when run, checks that all of them are present and downloads
+#       them if they are not.
+#
+# pylint: disable=line-too-long
+import json
+import os
+import platform
+import shutil
+import stat
+import subprocess
+import sys
+import tarfile
+import zipfile
+from argparse import ArgumentParser
+from pathlib import Path
+from urllib.request import urlretrieve
+
+project_root_dir = Path(__file__).resolve().parent.parent
+dependencies_dir = project_root_dir / '.dependencies'
+
+# All dependencies of this project.
+#
+# yapf: disable
+dependencies = {
+    'ninja': {
+        'version': '1.10.2',
+        'url': {
+            'Linux': 'https://github.com/ninja-build/ninja/releases/download/v1.10.2/ninja-linux.zip',
+            'Windows': 'https://github.com/ninja-build/ninja/releases/download/v1.10.2/ninja-win.zip',
+            'Darwin': 'https://github.com/ninja-build/ninja/releases/download/v1.10.2/ninja-mac.zip',
+        },
+    },
+    'cmake': {
+        'version': '3.22.5',
+        'url': {
+            'Linux': 'https://github.com/Kitware/CMake/releases/download/v3.22.5/cmake-3.22.5-linux-x86_64.tar.gz',
+            'Windows': 'https://github.com/Kitware/CMake/releases/download/v3.22.5/cmake-3.22.5-windows-x86_64.zip',
+            'Darwin': 'https://github.com/Kitware/CMake/releases/download/v3.22.5/cmake-3.22.5-macos-universal.tar.gz',
+        },
+    },
+    'avr-gcc': {
+        'version': '7.3.0',
+        'url': {
+            'Linux': 'http://downloads.arduino.cc/tools/avr-gcc-7.3.0-atmel3.6.1-arduino7-x86_64-pc-linux-gnu.tar.bz2',
+            'Windows': 'http://downloads.arduino.cc/tools/avr-gcc-7.3.0-atmel3.6.1-arduino7-i686-w64-mingw32.zip',
+            'Darwin': 'http://downloads.arduino.cc/tools/avr-gcc-7.3.0-atmel3.6.1-arduino7-x86_64-apple-darwin14.tar.bz2',
+        },
+    },
+    'prusa3dboards': {
+        'version': '1.0.5-2',
+        'url': {
+            'Linux': 'https://raw.githubusercontent.com/prusa3d/Arduino_Boards/devel/IDE_Board_Manager/prusa3dboards-1.0.5-2.tar.bz2',
+            'Windows': 'https://raw.githubusercontent.com/prusa3d/Arduino_Boards/devel/IDE_Board_Manager/prusa3dboards-1.0.5-2.tar.bz2',
+            'Darwin': 'https://raw.githubusercontent.com/prusa3d/Arduino_Boards/devel/IDE_Board_Manager/prusa3dboards-1.0.5-2.tar.bz2',
+        }
+    },
+}
+pip_dependencies = ["pyelftools","polib","regex"]
+# yapf: enable
+
+
+def directory_for_dependency(dependency, version):
+    return dependencies_dir / (dependency + '-' + version)
+
+
+def find_single_subdir(path: Path):
+    members = list(path.iterdir())
+    if path.is_dir() and len(members) > 1:
+        return path
+    elif path.is_dir() and len(members) == 1:
+        return find_single_subdir(members[0]) if members[0].is_dir() else path
+    else:
+        raise RuntimeError
+
+
+def download_and_unzip(url: str, directory: Path):
+    """Download a compressed file and extract it at `directory`."""
+    extract_dir = directory.with_suffix('.temp')
+    shutil.rmtree(directory, ignore_errors=True)
+    shutil.rmtree(extract_dir, ignore_errors=True)
+
+    print('Downloading ' + directory.name)
+    f, _ = urlretrieve(url, filename=None)
+    print('Extracting ' + directory.name)
+    if '.tar.bz2' in url or '.tar.gz' in url or '.tar.xz' in url:
+        obj = tarfile.open(f)
+    else:
+        obj = zipfile.ZipFile(f, 'r')
+    obj.extractall(path=str(extract_dir))
+
+    subdir = find_single_subdir(extract_dir)
+    shutil.move(str(subdir), str(directory))
+    shutil.rmtree(extract_dir, ignore_errors=True)
+
+
+def run(*cmd):
+    process = subprocess.run([str(a) for a in cmd],
+                             stdout=subprocess.PIPE,
+                             check=True,
+                             encoding='utf-8')
+    return process.stdout.strip()
+
+
+def fix_executable_permissions(dependency, installation_directory):
+    to_fix = ('ninja', 'clang-format')
+    if dependency not in to_fix:
+        return
+    for fpath in installation_directory.iterdir():
+        if fpath.is_file and fpath.with_suffix('').name in to_fix:
+            st = os.stat(fpath)
+            os.chmod(fpath, st.st_mode | stat.S_IEXEC)
+
+
+def recommended_version_is_available(dependency):
+    version = dependencies[dependency]['version']
+    directory = directory_for_dependency(dependency, version)
+    return directory.exists() and directory.is_dir()
+
+
+def get_installed_pip_packages():
+    result = run(sys.executable, '-m', 'pip', 'list',
+                 '--disable-pip-version-check', '--format', 'json')
+    data = json.loads(result)
+    return [(pkg['name'].lower(), pkg['version']) for pkg in data]
+
+
+def install_dependency(dependency):
+    specs = dependencies[dependency]
+    installation_directory = directory_for_dependency(dependency,
+                                                      specs['version'])
+    url = specs['url']
+    if isinstance(url, dict):
+        url = url[platform.system()]
+    download_and_unzip(url=url, directory=installation_directory)
+    fix_executable_permissions(dependency, installation_directory)
+
+
+def main() -> int:
+    parser = ArgumentParser()
+    # yapf: disable
+    parser.add_argument(
+        '--print-dependency-version', type=str,
+        help='Prints recommended version of given dependency and exits.')
+    parser.add_argument(
+        '--print-dependency-directory', type=str,
+        help='Prints installation directory of given dependency and exits.')
+    args = parser.parse_args(sys.argv[1:])
+    # yapf: enable
+
+    if args.print_dependency_version:
+        try:
+            version = dependencies[args.print_dependency_version]['version']
+            print(version)
+            return 0
+        except KeyError:
+            print('Unknown dependency "%s"' % args.print_dependency_version)
+            return 1
+
+    if args.print_dependency_directory:
+        try:
+            dependency = args.print_dependency_directory
+            version = dependencies[dependency]['version']
+            install_dir = directory_for_dependency(dependency, version)
+            print(install_dir)
+            return 0
+        except KeyError:
+            print('Unknown dependency "%s"' % args.print_dependency_directory)
+            return 1
+
+    # if no argument present, check and install dependencies
+    for dependency in dependencies:
+        if recommended_version_is_available(dependency):
+            continue
+        install_dependency(dependency)
+
+    # also, install pip packages
+    installed_pip_packages = get_installed_pip_packages()
+    for package in pip_dependencies:
+        is_installed = any(installed[0] == package
+                           for installed in installed_pip_packages)
+        if is_installed:
+            continue
+        print('Installing Python package %s' % package)
+        run(sys.executable, '-m', 'pip', 'install', package,
+            '--disable-pip-version-check')
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())