Browse Source

WIP/proof-of-concept

VintagePC 2 years ago
parent
commit
f03e3c40ee

+ 3 - 1
.gitignore

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

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

@@ -0,0 +1,9 @@
+[
+    {
+        "name": "Local_gcc-avr-none-eabi",
+        "toolchainFile": "${workspaceFolder}/cmake/LocalAvrGcc.cmake",
+        "cmakeSettings": {
+            "CMAKE_MAKE_PROGRAM": "${workspaceFolder}/.dependencies/ninja-1.9.0/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"
+    }
+}

+ 227 - 20
CMakeLists.txt

@@ -1,24 +1,231 @@
-cmake_minimum_required(VERSION 3.1)
+cmake_minimum_required(VERSION 3.15)
+include(cmake/Utilities.cmake)
 
 set (CMAKE_CXX_STANDARD 11)
+project(Prusa-Firmware)
 
-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
+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)
+
+# 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)
+  add_compile_options(${MCU_FLAGS})
+  add_link_options(${MCU_FLAGS})
+
+  # split and gc sections
+  add_compile_options(-ffunction-sections -fdata-sections)
+  add_link_options(-Wl,--gc-sections)
+
+  # disable exceptions and related metadata
+  add_compile_options(-fno-exceptions -fno-unwind-tables)
+  add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>)
+  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(-Wall -Wsign-compare)
+add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-std=c++14>)
+
+# support _DEBUG macro (some code uses to recognize debug builds)
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+  add_compile_definitions(_DEBUG)
+endif()
+
+#
+# Firmware - get file lists.
+#
+file(GLOB FW_SOURCES RELATIVE ${PROJECT_SOURCE_DIR} ${PROJECT_SOURCE_DIR}/Firmware/*.c*)
+file(GLOB FW_HEADERS RELATIVE ${PROJECT_SOURCE_DIR} ${PROJECT_SOURCE_DIR}/Firmware/*.h*)
+file(GLOB AVR_SOURCES RELATIVE ${PROJECT_SOURCE_DIR} ${PROJECT_SOURCE_DIR}/.dependencies/1.8.19-1.0.5-1-linux-64/portable/packages/PrusaResearch/hardware/avr/1.0.5-1/cores/prusa_einsy_rambo/*.c*)
+
+  # 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}")
+message("Languages found: ${LANG_VARIANTS}")
+
+add_library(avr_core STATIC ${AVR_SOURCES})
+target_include_directories(avr_core PRIVATE
+  ${PROJECT_SOURCE_DIR}/.dependencies/1.8.19-1.0.5-1-linux-64/portable/packages/PrusaResearch/hardware/avr/1.0.5-1/cores/prusa_einsy_rambo/
+  ${PROJECT_SOURCE_DIR}/.dependencies/1.8.19-1.0.5-1-linux-64/portable/packages/PrusaResearch/hardware/avr/1.0.5-1/variants/prusa_einsy_rambo/
 )
-add_executable(tests ${TEST_SOURCES})
-target_include_directories(tests PRIVATE Tests)
-target_link_libraries(tests Catch)
+target_compile_options(avr_core PUBLIC -mmcu=atmega2560)
+
+function(fw_add_variant variant_name)
+
+  add_executable(${variant_name} ${FW_SOURCES} ${FW_HEADERS})
+
+  set_target_properties(${variant_name} PROPERTIES CXX_STANDARD 14)
+
+
+#   # configure linker script
+   set(LINKER_SCRIPT ${PROJECT_SOURCE_DIR}/.dependencies/1.8.19-1.0.5-1-linux-64/portable/packages/PrusaResearch/hardware/avr/1.0.5-1/ldscripts/avr6.xn)
+   target_link_options(${variant_name} PUBLIC -Wl,-T,${LINKER_SCRIPT})
+   add_link_dependency(${variant_name} ${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=${variant_name}.map)
+
+
+  target_include_directories(${variant_name} PRIVATE Firmware
+    ${PROJECT_SOURCE_DIR}/.dependencies/1.8.19-1.0.5-1-linux-64/portable/packages/PrusaResearch/hardware/avr/1.0.5-1/cores/prusa_einsy_rambo/
+    ${PROJECT_SOURCE_DIR}/.dependencies/1.8.19-1.0.5-1-linux-64/portable/packages/PrusaResearch/hardware/avr/1.0.5-1/variants/prusa_einsy_rambo/
+    ${PROJECT_SOURCE_DIR}/cmake/helpers/ # Add our magic config helper :)
+    )
+
+  target_compile_options(${variant_name} PRIVATE) # turn this on for lolz -Wdouble-promotion)
+  string(REPLACE "-" "_" DEFINE_NAME "${variant_name}")
+  target_compile_definitions(${variant_name} PRIVATE H${DEFINE_NAME} ARDUINO=10600 __AVR_ATmega2560__)
+  target_link_libraries(${variant_name} avr_core)
+
+  #Construct language map
+  set(LANG_MAP ${CMAKE_CURRENT_BINARY_DIR}/lang/${variant_name}_lang.map)
+  set(LANG_FWBIN ${CMAKE_BINARY_DIR}/${variant_name}.bin)
+  set(LANG_FINAL_BIN ${CMAKE_CURRENT_BINARY_DIR}/lang/${variant_name}_lang.bin)
+  set(LANG_FINAL_HEX ${CMAKE_CURRENT_BINARY_DIR}/lang/${variant_name}_lang.hex)
+
+  add_custom_command(OUTPUT ${LANG_FWBIN}
+    COMMAND "${CMAKE_OBJCOPY}" -I ihex -O binary ${CMAKE_BINARY_DIR}/${variant_name}.hex ${LANG_FWBIN}
+    DEPENDS ${variant_name}
+  )
+  add_custom_command(OUTPUT ${LANG_MAP}
+    COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/lang/lang-map.py "${CMAKE_BINARY_DIR}/${variant_name}" "${LANG_FWBIN}" > "${LANG_MAP}"
+    DEPENDS ${LANG_FWBIN}
+  )
+
+  set(LANG_BINS "")
+  foreach (LANG IN LISTS LANG_VARIANTS)
+    set(LANG_BIN ${CMAKE_CURRENT_BINARY_DIR}/lang/${variant_name}_${LANG}.bin)
+
+    set(PO_FILE "${CMAKE_CURRENT_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_CURRENT_SOURCE_DIR}/lang/lang-build.py ${LANG_MAP} ${PO_FILE} ${LANG_BIN}
+        DEPENDS ${LANG_MAP}
+        COMMENT "Generating ${variant_name}_${LANG}.bin from .po"
+        )
+        LIST(APPEND LANG_BINS ${LANG_BIN})
+  endforeach()
+  add_custom_command( OUTPUT ${LANG_FINAL_BIN}
+    # TODO - needs differentiation for platforms, e.g. copy /b on Win
+    COMMAND cat ${LANG_BINS} > ${LANG_FINAL_BIN}
+    DEPENDS ${LANG_BINS}
+    COMMENT "Merging language binaries"
+  )
+  add_custom_command( OUTPUT ${LANG_FINAL_HEX}
+  # TODO - needs differentiation for platforms, e.g. copy /b on Win
+    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}/${variant_name}-lang.hex)
+  add_custom_target(${variant_name}-languages
+    COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_BINARY_DIR}/${variant_name}.hex ${LANG_HEX}
+    COMMAND cat ${LANG_FINAL_HEX} >> ${LANG_HEX}
+    COMMENT "Generating final ${variant_name}-lang.hex"
+    BYPRODUCTS ${LANG_HEX}
+    DEPENDS ${LANG_FINAL_HEX}
+  )
+
+endfunction()
+
+
+if(CMAKE_CROSSCOMPILING)
+
+  add_custom_target(All_Firmware)
+
+  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}")
+    fw_add_variant(${TRIMMED_NAME})
+    add_dependencies(All_Firmware ${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()

+ 93 - 0
cmake/LocalAvrGcc.cmake

@@ -0,0 +1,93 @@
+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)
+
+set(AVR_TOOLCHAIN_DIR "${PROJECT_CMAKE_DIR}/../.dependencies/1.8.19-1.0.5-1-linux-64/hardware/tools/avr/")
+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_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)

+ 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" "gcc-avr"
+    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()

+ 24 - 0
cmake/helpers/Configuration_prusa.h

@@ -0,0 +1,24 @@
+#ifdef H1_75mm_MK25_RAMBo10a_E3Dv6full
+#include "variants/1_75mm_MK25-RAMBo10a-E3Dv6full.h"
+#endif
+#ifdef H1_75mm_MK25_RAMBo13a_E3Dv6full
+#include "variants/1_75mm_MK25-RAMBo13a-E3Dv6full.h"
+#endif
+#ifdef H1_75mm_MK25S_RAMBo10a_E3Dv6full
+#include "variants/1_75mm_MK25S-RAMBo10a-E3Dv6full.h"
+#endif
+#ifdef H1_75mm_MK25S_RAMBo13a_E3Dv6full
+#include "variants/1_75mm_MK25S-RAMBo13a-E3Dv6full.h"
+#endif
+#ifdef H1_75mm_MK2_RAMBo10a_E3Dv6full
+#include "variants/1_75mm_MK2-RAMBo10a-E3Dv6full.h"
+#endif
+#ifdef H1_75mm_MK2_RAMBo13a_E3Dv6full
+#include "variants/1_75mm_MK2-RAMBo13a-E3Dv6full.h"
+#endif
+#ifdef H1_75mm_MK3_EINSy10a_E3Dv6full
+#include "variants/1_75mm_MK3-EINSy10a-E3Dv6full.h"
+#endif
+#ifdef H1_75mm_MK3S_EINSy10a_E3Dv6full
+#include "variants/1_75mm_MK3S-EINSy10a-E3Dv6full.h"
+#endif

+ 191 - 0
utils/bootstrap.py

@@ -0,0 +1,191 @@
+#!/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.9.0',
+        'url': {
+            'Linux': 'https://github.com/ninja-build/ninja/releases/download/v1.9.0/ninja-linux.zip',
+            'Windows': 'https://github.com/ninja-build/ninja/releases/download/v1.9.0/ninja-win.zip',
+            'Darwin': 'https://github.com/ninja-build/ninja/releases/download/v1.9.0/ninja-mac.zip',
+        },
+    },
+    'cmake': {
+        'version': '3.15.5',
+        'url': {
+            'Linux': 'https://github.com/Kitware/CMake/releases/download/v3.15.5/cmake-3.15.5-Linux-x86_64.tar.gz',
+            'Windows': 'https://github.com/Kitware/CMake/releases/download/v3.15.5/cmake-3.15.5-win64-x64.zip',
+            'Darwin': 'https://github.com/Kitware/CMake/releases/download/v3.15.5/cmake-3.15.5-Darwin-x86_64.tar.gz',
+        },
+    },
+    'gcc-avr': {
+        # dummy placeholder (currently downloading cmake just for the sake of a valid url/zip archive)
+        # ... we truly need the binaries! :)
+        'version': '0.0.0',
+        'url': {
+            'Linux': 'https://github.com/Kitware/CMake/releases/download/v3.15.5/cmake-3.15.5-Linux-x86_64.tar.gz',
+            'Windows': 'https://github.com/Kitware/CMake/releases/download/v3.15.5/cmake-3.15.5-win64-x64.zip',
+            'Darwin': 'https://github.com/Kitware/CMake/releases/download/v3.15.5/cmake-3.15.5-Darwin-x86_64.tar.gz',
+        }
+    },
+}
+pip_dependencies = []
+# 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())