diff --git a/.gitignore b/.gitignore index 41ad449af..ac190dd36 100644 --- a/.gitignore +++ b/.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 diff --git a/.vscode/cmake-kits.json b/.vscode/cmake-kits.json new file mode 100644 index 000000000..149bd1988 --- /dev/null +++ b/.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" + } + } +] diff --git a/.vscode/cmake-variants.yaml b/.vscode/cmake-variants.yaml new file mode 100644 index 000000000..70f8df827 --- /dev/null +++ b/.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 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..09d4a863f --- /dev/null +++ b/.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" + } +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ca1d95be..ca06bdc58 100644 --- a/CMakeLists.txt +++ b/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) +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}" + ) -# Prepare "Catch" library for other executables -set(CATCH_INCLUDE_DIR Catch2) -add_library(Catch INTERFACE) -target_include_directories(Catch INTERFACE ${CATCH_INCLUDE_DIR}) +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() -# 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 +# 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($<$:-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($<$:-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() diff --git a/cmake/LocalAvrGcc.cmake b/cmake/LocalAvrGcc.cmake new file mode 100644 index 000000000..e8c317417 --- /dev/null +++ b/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 + " -o -c " + 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) diff --git a/cmake/Utilities.cmake b/cmake/Utilities.cmake new file mode 100644 index 000000000..2221f0b77 --- /dev/null +++ b/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 "$" + "${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 "$" + USES_TERMINAL + ) +endfunction() diff --git a/cmake/helpers/Configuration_prusa.h b/cmake/helpers/Configuration_prusa.h new file mode 100644 index 000000000..a308af746 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/utils/bootstrap.py b/utils/bootstrap.py new file mode 100755 index 000000000..fbe77ec74 --- /dev/null +++ b/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())