From 257d0ec340e4ab90f6eadc0b60109736135be93e Mon Sep 17 00:00:00 2001 From: "D.R.racer" Date: Tue, 13 Apr 2021 08:24:43 +0200 Subject: [PATCH] Prepare parts of the build system extracted and slightly modified from Buddy-FW, still doesn't work --- .clang-format | 126 +++++++ .cmake-format.py | 21 ++ .gitignore | 15 + .pre-commit-config.yaml | 39 ++ CMakeLists.txt | 199 ++++++++++ cmake/AnyGccArmNoneEabi.cmake | 99 +++++ cmake/GccArmNoneEabi.cmake | 22 ++ cmake/GetGitRevisionDescription.cmake | 232 ++++++++++++ cmake/GetGitRevisionDescription.cmake.in | 37 ++ cmake/ProjectVersion.cmake | 62 ++++ cmake/Utilities.cmake | 122 +++++++ src/main.cpp | 3 + src/version.c | 21 ++ src/version.h | 27 ++ tests/CMakeLists.txt | 1 + tests/integration/CMakeLists.txt | 1 + tests/unit/CMakeLists.txt | 1 + utils/CMakeLists.txt | 0 utils/bootstrap.py | 197 ++++++++++ utils/build.py | 441 +++++++++++++++++++++++ version.txt | 1 + 21 files changed, 1667 insertions(+) create mode 100644 .clang-format create mode 100644 .cmake-format.py create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 CMakeLists.txt create mode 100644 cmake/AnyGccArmNoneEabi.cmake create mode 100644 cmake/GccArmNoneEabi.cmake create mode 100644 cmake/GetGitRevisionDescription.cmake create mode 100644 cmake/GetGitRevisionDescription.cmake.in create mode 100644 cmake/ProjectVersion.cmake create mode 100644 cmake/Utilities.cmake create mode 100644 src/main.cpp create mode 100644 src/version.c create mode 100644 src/version.h create mode 100644 tests/CMakeLists.txt create mode 100644 tests/integration/CMakeLists.txt create mode 100644 tests/unit/CMakeLists.txt create mode 100644 utils/CMakeLists.txt create mode 100755 utils/bootstrap.py create mode 100755 utils/build.py create mode 100644 version.txt diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..b74be2f --- /dev/null +++ b/.clang-format @@ -0,0 +1,126 @@ +--- +Language: Cpp +# BasedOnStyle: WebKit +AccessModifierOffset: -4 +AlignAfterOpenBracket: DontAlign +AlignConsecutiveMacros: true +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: false +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: All +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 0 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + - Regex: '.*' + Priority: 1 +IncludeIsMainRegex: '(Test)?$' +IndentCaseLabels: false +IndentPPDirectives: BeforeHash +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: Inner +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 4 +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Right +ReflowComments: true +SortIncludes: false +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseTab: Never +... diff --git a/.cmake-format.py b/.cmake-format.py new file mode 100644 index 0000000..a948914 --- /dev/null +++ b/.cmake-format.py @@ -0,0 +1,21 @@ +# If a statement is wrapped to more than one line, than dangle the closing +# parenthesis on it's own line. +dangle_parens = True +dangle_align = 'child' + +# If true, the parsers may infer whether or not an argument list is sortable +# (without annotation). +autosort = True + +# How wide to allow formatted cmake files +line_width = 100 + +additional_commands = { + "target_sources": { + "kwargs": { + "PUBLIC": "*", + "PRIVATE": "*", + "INTERFACE": "*", + } + }, +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0560645 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.vs +/build* +.cproject +.project +.settings +.dependencies +.DS_Store +/CMakeLists.txt.user +.ccls-cache +.idea +compile_commands.json +/.vscode/*.peripherals.state.json +/.vscode/*.registers.state.json +Makefile +/doc/html/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b6fbe45 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/dragomirecky/cmake-format-pre-commit-hook + rev: 'v0.6.0' + hooks: + - id: cmake-format # cmake formatter + files: (CMakeLists.*|.*\.cmake|.*\.cmake.in) +- repo: https://github.com/pre-commit/mirrors-yapf + rev: 'v0.27.0' + hooks: + - id: yapf # python formatter +- repo: local + hooks: + - id: clang-format + name: clang-format + description: This hook automatically checks and reformats changed files using clang-format formatter. + entry: './.dependencies/clang-format-9.0.0-noext/clang-format' + language: script + files: \.(h\+\+|h|hh|hxx|hpp|cuh|c|cc|cpp|cu|c\+\+|cxx|tpp|txx)$ + args: ['-i', '-style=file'] +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: 'v2.4.0' + hooks: + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + - id: end-of-file-fixer + - id: mixed-line-ending +exclude: | + (?x)( + ^tests/unit/lang/translator/(keys|cs|es|fr|de|pl|it)\.txt$| + ^lib/inih/| + ^lib/Marlin/| + ^lib/Prusa-Error-Codes/| + ^lib/TMCStepper/| + ^lib/Middlewares/Third_Party/LwIP/| + ^lib/jsmn/| + ^lib/Catch2/ + ) diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d4d4526 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,199 @@ +cmake_minimum_required(VERSION 3.15) +include(cmake/Utilities.cmake) +include(cmake/GetGitRevisionDescription.cmake) +include(cmake/ProjectVersion.cmake) + +project( + MMU + LANGUAGES C CXX ASM + VERSION ${PROJECT_VERSION} + ) + +if(NOT CMAKE_CROSSCOMPILING) + # + # If we are not crosscompiling, include `utils` with host tools. + # + add_subdirectory(utils) +endif() + +# +# Command Line Options +# +# You should specify those options when invoking CMake. Example: +# ~~~ +# cmake .. -DPRINTER=MMU +# ~~~ + +set(PRINTER_VALID_OPTS "MMU") + +set(PRINTER + "MMU" + CACHE + STRING + "Select the MMU unit for which you want to compile the project (valid values are ${PRINTER_VALID_OPTS})." + ) +set(PROJECT_VERSION_SUFFIX + "" + 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 '+..' if set to ''." + ) +set(PROJECT_VERSION_SUFFIX_SHORT + "" + CACHE + STRING + "Short version suffix to be shown on splash screen. Defaults to '+' if set to ''." + ) +set(BUILD_NUMBER + "" + CACHE STRING "Build number of the firmware. Resolved automatically if not specified." + ) +set(CUSTOM_COMPILE_OPTIONS + "" + CACHE STRING "Allows adding custom C/C++ flags" + ) + +# Validate options +foreach(OPTION "PRINTER") + if(NOT ${OPTION} IN_LIST ${OPTION}_VALID_OPTS) + message(FATAL_ERROR "Invalid ${OPTION} ${${OPTION}}: Valid values are ${${OPTION}_VALID_OPTS}") + endif() +endforeach() + +# Resolve BUILD_NUMBER and PROJECT_VERSION_* variables +resolve_version_variables() + +# Check GCC Version +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() + +# Inform user about the resolved settings +message(STATUS "Project version: ${PROJECT_VERSION}") +message(STATUS "Project version with full suffix: ${PROJECT_VERSION_FULL}") +message( + STATUS "Project version with short suffix: ${PROJECT_VERSION}${PROJECT_VERSION_SUFFIX_SHORT}" + ) +message(STATUS "Using toolchain file: ${CMAKE_TOOLCHAIN_FILE}.") +message(STATUS "Printer: ${PRINTER}") + +# eclipse sets those variables, so lets just use them so we don't get a warning about unused +# variables +set(unused "${CMAKE_VERBOSE_MAKEFILE} ${CMAKE_RULE_MESSAGES}") + +# append custom C/C++ flags +if(CUSTOM_COMPILE_OPTIONS) + string(REPLACE " " ";" CUSTOM_COMPILE_OPTIONS "${CUSTOM_COMPILE_OPTIONS}") + add_compile_options(${CUSTOM_COMPILE_OPTIONS}) +endif() + +# +# MMUHeaders +# + +# add_library(MMUHeaders INTERFACE) target_include_directories( MMUHeaders INTERFACE include +# include/stm32f4_hal include/usb_host include/usb_device include/marlin include/freertos ) + +# target_link_libraries(A3idesHeaders INTERFACE STM32F4::HAL FreeRTOS::FreeRTOS) + +# target_compile_definitions( A3idesHeaders INTERFACE PRINTER_TYPE=PRINTER_PRUSA_${PRINTER} ) + +# +# Global Compiler & Linker Configuration +# + +# include symbols +add_compile_options(-g) + +# optimizations +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + add_compile_options(-Og) +else() + add_compile_options(-Os) +endif() + +if(CMAKE_CROSSCOMPILING) + # mcu related settings set(MCU_FLAGS -mthumb -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16) + # 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_link_options(-Wl,--defsym,__exidx_start=0,--defsym,__exidx_end=0) +endif() + +# enable all warnings (well, not all, but some) +add_compile_options(-Wall -Wsign-compare) +add_compile_options($<$:-Wno-register> $<$:-std=c++14>) + +# support _DEBUG macro (some code uses to recognize debug builds) +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + add_compile_definitions(_DEBUG) +endif() + +# if(CMAKE_CROSSCOMPILING) # configure linker script set(LINKER_SCRIPT +# "${CMAKE_CURRENT_SOURCE_DIR}/src/STM32F407VG_FLASH.ld") +# add_link_options("-Wl,-T,${LINKER_SCRIPT}") endif() + +# +# Import definitions of all libraries +# + +# add_subdirectory(lib) + +# +# MMU firmware +# + +add_executable(firmware) + +set_target_properties(firmware PROPERTIES CXX_STANDARD 14) + +# generate firmware.bin file +objcopy(firmware "binary" ".bin") + +# generate linker map file +target_link_options(firmware PUBLIC -Wl,-Map=firmware.map) + +# inform about the firmware's size in terminal +report_size(firmware) + +# add_link_dependency(firmware "${LINKER_SCRIPT}") + +target_include_directories(firmware PRIVATE include src) + +target_compile_options(firmware PRIVATE -Wdouble-promotion) + +# target_link_libraries( firmware PRIVATE A3idesHeaders ) + +target_sources(firmware PRIVATE src/main.cpp) + +set_property( + SOURCE src/version.c + APPEND + PROPERTY COMPILE_DEFINITIONS + FW_BUILD_NUMBER=${BUILD_NUMBER} + FW_VERSION_FULL=${PROJECT_VERSION_FULL} + FW_VERSION=${PROJECT_VERSION} + FW_VERSION_SUFFIX=${PROJECT_VERSION_SUFFIX} + FW_VERSION_SUFFIX_SHORT=${PROJECT_VERSION_SUFFIX_SHORT} + ) + +if(NOT CMAKE_CROSSCOMPILING) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/cmake/AnyGccArmNoneEabi.cmake b/cmake/AnyGccArmNoneEabi.cmake new file mode 100644 index 0000000..c7f47fd --- /dev/null +++ b/cmake/AnyGccArmNoneEabi.cmake @@ -0,0 +1,99 @@ +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 ARM) + +# +# 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 arm-none-eabi-) + +# +# Looking up the toolchain +# + +if(ARM_TOOLCHAIN_DIR) + # using toolchain set by gcc-arm-none-eabi.cmake (locked version) + set(BINUTILS_PATH "${ARM_TOOLCHAIN_DIR}/bin") +else() + # search for ANY arm-none-eabi-gcc toolchain + execute_process( + COMMAND ${UTIL_SEARCH_CMD} ${TOOLCHAIN_PREFIX}gcc + OUTPUT_VARIABLE ARM_NONE_EABI_GCC_PATH + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE FIND_RESULT + ) + # found? + if(NOT "${FIND_RESULT}" STREQUAL "0") + message(FATAL_ERROR "arm-none-eabi-gcc not found") + endif() + get_filename_component(BINUTILS_PATH "${ARM_NONE_EABI_GCC_PATH}" DIRECTORY) + get_filename_component(ARM_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(triple armv7m-none-eabi) +set(CMAKE_C_COMPILER + "${BINUTILS_PATH}/${TOOLCHAIN_PREFIX}gcc${EXECUTABLE_SUFFIX}" + CACHE FILEPATH "" FORCE + ) +set(CMAKE_C_COMPILER_TARGET + ${triple} + CACHE STRING "" FORCE + ) +set(CMAKE_ASM_COMPILER + "${BINUTILS_PATH}/${TOOLCHAIN_PREFIX}gcc${EXECUTABLE_SUFFIX}" + CACHE FILEPATH "" FORCE + ) +set(CMAKE_ASM_COMPILER_TARGET + ${triple} + CACHE STRING "" FORCE + ) +set(CMAKE_CXX_COMPILER + "${BINUTILS_PATH}/${TOOLCHAIN_PREFIX}g++${EXECUTABLE_SUFFIX}" + CACHE FILEPATH "" FORCE + ) +set(CMAKE_CXX_COMPILER_TARGET + ${triple} + CACHE STRING "" FORCE + ) +set(CMAKE_EXE_LINKER_FLAGS_INIT + "--specs=nosys.specs" + 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_SIZE_UTIL + "${BINUTILS_PATH}/${TOOLCHAIN_PREFIX}size${EXECUTABLE_SUFFIX}" + CACHE INTERNAL "size tool" + ) + +set(CMAKE_FIND_ROOT_PATH "${ARM_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/GccArmNoneEabi.cmake b/cmake/GccArmNoneEabi.cmake new file mode 100644 index 0000000..ac4c091 --- /dev/null +++ b/cmake/GccArmNoneEabi.cmake @@ -0,0 +1,22 @@ +# getlocked version +get_filename_component(PROJECT_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) +get_filename_component(PROJECT_ROOT_DIR "${PROJECT_CMAKE_DIR}" DIRECTORY) +include("${PROJECT_CMAKE_DIR}/Utilities.cmake") + +get_recommended_gcc_version(RECOMMENDED_TOOLCHAIN_VERSION) +set(RECOMMENDED_TOOLCHAIN_BINUTILS + "${PROJECT_ROOT_DIR}/.dependencies/gcc-arm-none-eabi-${RECOMMENDED_TOOLCHAIN_VERSION}/bin" + ) + +# check that the locked version of gcc-arm-none-eabi is present +if(NOT EXISTS "${RECOMMENDED_TOOLCHAIN_BINUTILS}") + message( + FATAL_ERROR + "arm-none-eabi-gcc (version ${RECOMMENDED_TOOLCHAIN_VERSION}) not found. Run the command below to download it.\n" + "${PROJECT_ROOT_DIR}/utils/bootstrap.sh\n" + ) +endif() + +# include any-gcc-arm-none-eabi toolchain and pass in ARM_TOOLCHAIN_DIR +get_filename_component(ARM_TOOLCHAIN_DIR "${RECOMMENDED_TOOLCHAIN_BINUTILS}" DIRECTORY) +include("${PROJECT_ROOT_DIR}/cmake/AnyGccArmNoneEabi.cmake") diff --git a/cmake/GetGitRevisionDescription.cmake b/cmake/GetGitRevisionDescription.cmake new file mode 100644 index 0000000..0eccbc1 --- /dev/null +++ b/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( [ ...]) +# +# Returns the refspec and sha hash of the current head revision +# +# 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( [ ...]) +# +# 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() +# +# 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() +# +# 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 +# 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() diff --git a/cmake/GetGitRevisionDescription.cmake.in b/cmake/GetGitRevisionDescription.cmake.in new file mode 100644 index 0000000..f7d93dd --- /dev/null +++ b/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 +# 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() diff --git a/cmake/ProjectVersion.cmake b/cmake/ProjectVersion.cmake new file mode 100644 index 0000000..65c4fb5 --- /dev/null +++ b/cmake/ProjectVersion.cmake @@ -0,0 +1,62 @@ +# +# 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. +# +# ~~~ + +# PROJECT_VERSION +file(READ "${CMAKE_SOURCE_DIR}/version.txt" content) +string(REGEX MATCH "([0-9]+)\.([0-9]+)\.([0-9]+)" result "${content}") +if(NOT result) + message(FATAL_ERROR "Failed to read version info from ${version_file}") +endif() +set(PROJECT_VERSION ${CMAKE_MATCH_0}) + +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 "") + # TODO: set to +.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 "") + 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() diff --git a/cmake/Utilities.cmake b/cmake/Utilities.cmake new file mode 100644 index 0000000..ffeb1a6 --- /dev/null +++ b/cmake/Utilities.cmake @@ -0,0 +1,122 @@ +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-arm-none-eabi" + 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}..." + ) +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() + +function(pack_firmware target fw_version build_number printer_type signing_key) + set(bin_firmware_path "${CMAKE_CURRENT_BINARY_DIR}/${target}.bin") + if(SIGNING_KEY) + set(sign_opts "--key" "${signing_key}") + else() + set(sign_opts "--no-sign") + endif() + add_custom_command( + TARGET ${target} POST_BUILD + COMMAND "${CMAKE_OBJCOPY}" -O binary -S "$" "${bin_firmware_path}" + COMMAND echo "" # visually separate the output + COMMAND + "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/utils/pack_fw.py" --version="${fw_version}" + --printer-type "${printer_type}" --printer-version "1" ${sign_opts} "${bin_firmware_path}" + --build-number "${build_number}" + ) +endfunction() + +function(create_dfu) + set(options) + set(one_value_args OUTPUT TARGET) + set(multi_value_args INPUT) + cmake_parse_arguments(CREATE_DFU "${options}" "${one_value_args}" "${multi_value_args}" ${ARGN}) + + add_custom_command( + TARGET "${CREATE_DFU_TARGET}" POST_BUILD + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/utils/dfu.py" create ${CREATE_DFU_INPUT} + "${CREATE_DFU_OUTPUT}" + ) +endfunction() + +function(add_link_dependency target file_path) + get_target_property(link_deps ${target} LINK_DEPENDS) + if(link_deps STREQUAL "link_deps-NOTFOUND") + set(link_deps "") + endif() + list(APPEND link_deps "${file_path}") + set_target_properties(${target} PROPERTIES LINK_DEPENDS "${link_deps}") +endfunction() + +function(rfc1123_datetime var) + set(cmd + "from email.utils import formatdate; print(formatdate(timeval=None, localtime=False, usegmt=True))" + ) + execute_process( + COMMAND "${Python3_EXECUTABLE}" -c "${cmd}" + OUTPUT_VARIABLE RFC1123_DATETIME + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE RETVAL + ) + if(NOT "${RETVAL}" STREQUAL "0") + message(FATAL_ERROR "Failed to obtain rfc1123 date time from Python") + endif() + set(${var} + ${RFC1123_DATETIME} + PARENT_SCOPE + ) +endfunction() diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..33c14ce --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,3 @@ +int main() { + return 0; +} diff --git a/src/version.c b/src/version.c new file mode 100644 index 0000000..20bd1c0 --- /dev/null +++ b/src/version.c @@ -0,0 +1,21 @@ +#include "version.h" +#include "config.h" + +#define _STR(x) #x +#define STR(x) _STR(x) + +const char project_version[] = STR(FW_VERSION); + +const char project_version_full[] = STR(FW_VERSION_FULL); + +const char project_version_suffix[] = STR(FW_VERSION_SUFFIX); + +const char project_version_suffix_short[] = STR(FW_VERSION_SUFFIX_SHORT); + +const int project_build_number = FW_BUILD_NUMBER; + +#if (PRINTER_TYPE == PRINTER_PRUSA_MINI) +const char project_firmware_name[] = "Buddy_MINI"; +#else + #error "unknown printer type" +#endif diff --git a/src/version.h b/src/version.h new file mode 100644 index 0000000..2d852c7 --- /dev/null +++ b/src/version.h @@ -0,0 +1,27 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif //__cplusplus + +/// Project's version (4.0.2) +extern const char project_version[]; + +/// Full project's version (4.0.3-BETA+1035.PR111.B4) +extern const char project_version_full[]; + +/// Project's version suffix (-BETA+1035.PR111.B4) +extern const char project_version_suffix[]; + +/// Project's short version suffix (+1035) +extern const char project_version_suffix_short[]; + +/// Project's build number (number of commits in a branch) +extern const int project_build_number; + +/// Firmware name +extern const char project_firmware_name[]; + +#ifdef __cplusplus +} +#endif //__cplusplus diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1 @@ +# diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/tests/integration/CMakeLists.txt @@ -0,0 +1 @@ +# diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/tests/unit/CMakeLists.txt @@ -0,0 +1 @@ +# diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/utils/bootstrap.py b/utils/bootstrap.py new file mode 100755 index 0000000..25d8016 --- /dev/null +++ b/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.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', + }, + }, + 'avr8-gnu-toolchain': { + 'version': '5.4.0', + 'url': { + 'Linux': 'https://xxxarmkeil.blob.core.windows.net/developer/Files/downloads/gnu-rm/7-2018q2/gcc-arm-none-eabi-7-2018-q2-update-linux.tar.bz2', + 'Windows': 'https://xxxarmkeil.blob.core.windows.net/developer/Files/downloads/gnu-rm/7-2018q2/gcc-arm-none-eabi-7-2018-q2-update-win32.zip', + 'Darwin': 'https://xxxarmkeil.blob.core.windows.net/developer/Files/downloads/gnu-rm/7-2018q2/gcc-arm-none-eabi-7-2018-q2-update-mac.tar.bz2', + } + }, + 'clang-format': { + 'version': '9.0.0-noext', + 'url': { + 'Linux': 'https://prusa-buddy-firmware-dependencies.s3.eu-central-1.amazonaws.com/clang-format-9.0.0-linux.zip', + 'Windows': 'https://prusa-buddy-firmware-dependencies.s3.eu-central-1.amazonaws.com/clang-format-9.0.0-noext-win.zip', + 'Darwin': 'https://prusa-buddy-firmware-dependencies.s3.eu-central-1.amazonaws.com/clang-format-9.0.0-darwin.zip', + } + }, +} +pip_dependencies = ['ecdsa', 'polib'] +# 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()) diff --git a/utils/build.py b/utils/build.py new file mode 100755 index 0000000..240a18c --- /dev/null +++ b/utils/build.py @@ -0,0 +1,441 @@ +#!/usr/bin/env python3 +import argparse +import os +import platform +import random +import re +import shutil +import subprocess +import sys +import xml.etree.ElementTree as ET +from abc import ABC, abstractmethod, abstractproperty +from copy import deepcopy +from enum import Enum +from functools import lru_cache +from pathlib import Path +from typing import Dict, List, Optional +from uuid import uuid4 + +try: + from tqdm import tqdm +except ModuleNotFoundError: + + def tqdm(iterable, *args, **kwargs): + return iterable + + if os.isatty(sys.stdout.fileno()) and random.randint(0, 10) <= 1: + print('TIP: run `pip install -m tqdm` to get a nice progress bar') + +project_root = Path(__file__).resolve().parent.parent +dependencies_dir = project_root / '.dependencies' + + +def bootstrap(*args, interactive=False, check=False): + """Run the bootstrap script.""" + bootstrap_py = project_root / 'utils' / 'bootstrap.py' + result = subprocess.run([sys.executable, str(bootstrap_py)] + list(args), + check=False, + encoding='utf-8', + stdout=None if interactive else subprocess.PIPE, + stderr=None if interactive else subprocess.PIPE) + return result + + +def project_version(): + """Return current project version (e. g. "4.0.3")""" + with open(project_root / 'version.txt', 'r') as f: + return f.read().strip() + + +@lru_cache() +def get_dependency(name): + install_dir = Path( + bootstrap('--print-dependency-directory', name, + check=True).stdout.strip()) + suffix = '.exe' if platform.system() == 'Windows' else '' + if name == 'ninja': + return install_dir / ('ninja' + suffix) + elif name == 'cmake': + return install_dir / 'bin' / ('cmake' + suffix) + else: + return install_dir + + +class Printer(Enum): + """Represents the -DPRINTER CMake option.""" + + MMU = 'MMU' + + +#class Bootloader(Enum): +# """Represents the -DBOOTLOADER CMake option.""" +# +# NO = 'NO' +# EMPTY = 'EMPTY' +# YES = 'YES' +# +# @property +# def file_component(self): +# if self == Bootloader.NO: +# return 'NOBOOT' +# elif self == Bootloader.EMPTY: +# return 'EMPTYBOOT' +# elif self == Bootloader.YES: +# return 'BOOT' +# else: +# raise NotImplementedError + + +class BuildType(Enum): + """Represents the -DCONFIG CMake option.""" + + DEBUG = 'DEBUG' + RELEASE = 'RELEASE' + + +#class HostTool(Enum): +# """Known host tools.""" +# +# png2font = "png2font" +# bin2cc = "bin2cc" +# hex2dfu = "hex2dfu" +# makefsdata = "makefsdata" + + +class BuildConfiguration(ABC): + @abstractmethod + def get_cmake_cache_entries(self): + """Convert the build configuration to CMake cache entries.""" + + @abstractmethod + def get_cmake_flags(self, build_dir: Path) -> List[str]: + """Return all CMake command-line flags required to build this configuration.""" + + @abstractproperty + def name(self): + """Name of the configuration.""" + + def __hash__(self): + return hash(self.name) + + +class FirmwareBuildConfiguration(BuildConfiguration): + def __init__(self, + printer: Printer, + build_type: BuildType, + toolchain: Path = None, + generator: str = None, + version_suffix: str = None, + version_suffix_short: str = None, + custom_entries: List[str] = None): + self.printer = printer + self.build_type = build_type + self.toolchain = toolchain or FirmwareBuildConfiguration.default_toolchain( + ) + self.generator = generator + self.version_suffix = version_suffix + self.version_suffix_short = version_suffix_short + self.custom_entries = custom_entries or [] + + @staticmethod + def default_toolchain() -> Path: + return Path( + __file__).resolve().parent.parent / 'cmake/GccArmNoneEabi.cmake' + + def get_cmake_cache_entries(self): + entries = [] + if self.generator.lower() == 'ninja': + entries.append(('CMAKE_MAKE_PROGRAM', 'FILEPATH', + str(get_dependency('ninja')))) + entries.extend([ + ('CMAKE_MAKE_PROGRAM', 'FILEPATH', str(get_dependency('ninja'))), + ('PRINTER', 'STRING', self.printer.value), + ('CMAKE_TOOLCHAIN_FILE', 'FILEPATH', str(self.toolchain)), + ('CMAKE_BUILD_TYPE', 'STRING', self.build_type.value.title()), + ('PROJECT_VERSION_SUFFIX', 'STRING', self.version_suffix or ''), + ('PROJECT_VERSION_SUFFIX_SHORT', 'STRING', + self.version_suffix_short or ''), + ]) + entries.extend(self.custom_entries) + return entries + + def get_cmake_flags(self, build_dir: Path) -> List[str]: + cache_entries = self.get_cmake_cache_entries() + flags = ['-D{}:{}={}'.format(*entry) for entry in cache_entries] + flags += ['-G', self.generator or 'Ninja'] + flags += ['-S', str(Path(__file__).resolve().parent.parent)] + flags += ['-B', str(build_dir)] + return flags + + @property + def name(self): + components = [ + self.printer.name, + self.build_type.value, + ] + return '_'.join(components) + + +class BuildResult: + """Represents a result of an attempt to build the project.""" + + def __init__(self, config_returncode: int, build_returncode: Optional[int], + stdout: Path, stderr: Path, products: List[Path]): + self.config_returncode = config_returncode + self.build_returncode = build_returncode + self.stdout = stdout + self.stderr = stderr + self.products = products + + @property + def configuration_failed(self): + return self.config_returncode != 0 + + @property + def build_failed(self): + return self.build_returncode != 0 and self.build_returncode is not None + + @property + def is_failure(self): + return self.configuration_failed or self.build_failed + + def __str__(self): + return ''.format( + self=self) + + +def build(configuration: BuildConfiguration, + build_dir: Path, + configure_only=False, + output_to_file=True) -> BuildResult: + """Build a project with a single configuration.""" + flags = configuration.get_cmake_flags(build_dir=build_dir) + + # create the build directory + build_dir.mkdir(parents=True, exist_ok=True) + products = [] + + if output_to_file: + # stdout and stderr are saved to a file in the build directory + stdout_path = build_dir / 'stdout.txt' + stderr_path = build_dir / 'stderr.txt' + stdout = open(stdout_path, 'w') + stderr = open(stderr_path, 'w') + else: + stdout_path, stderr_path = None, None + stdout, stderr = None, None + + # prepare the build + config_process = subprocess.run([str(get_dependency('cmake'))] + flags, + stdout=stdout, + stderr=stderr, + check=False) + if not configure_only and config_process.returncode == 0: + cmd = [ + str(get_dependency('cmake')), '--build', + str(build_dir), '--config', + configuration.build_type.value.lower() + ] + build_process = subprocess.run(cmd, + stdout=stdout, + stderr=stderr, + check=False) + build_returncode = build_process.returncode + products.extend(build_dir / fname for fname in [ + 'firmware', 'firmware.bin', 'firmware.bbf', 'firmware.dfu', + 'firmware.map' + ] if (build_dir / fname).exists()) + else: + build_returncode = None + + if stdout: + stdout.close() + if stderr: + stderr.close() + + # collect the result and return + return BuildResult(config_returncode=config_process.returncode, + build_returncode=build_returncode, + stdout=stdout_path, + stderr=stderr_path, + products=products) + + +def store_products(products: List[Path], build_config: BuildConfiguration, + products_dir: Path): + """Copy build products to a shared products directory.""" + products_dir.mkdir(parents=True, exist_ok=True) + for product in products: + is_firmware = isinstance(build_config, FirmwareBuildConfiguration) + has_custom_suffix = is_firmware and (build_config.version_suffix != + '') + if has_custom_suffix: + version = project_version() + name = build_config.name.lower( + ) + '_' + version + build_config.version_suffix + else: + name = build_config.name.lower() + destination = products_dir / (name + product.suffix) + shutil.copy(product, destination) + + +def list_of(EnumType): + """Create an argument-parser for comma-separated list of values of some Enum subclass.""" + + def convert(val): + if val == '': + return [] + values = [p.lower() for p in val.split(',')] + if 'all' in values: + return list(EnumType) + else: + return [EnumType(v.upper()) for v in values] + + convert.__name__ = EnumType.__name__ + return convert + + +def cmake_cache_entry(arg): + match = re.fullmatch(r'(.*):(.*)=(.*)', arg) + if not match: + raise ValueError('invalid cmake entry; must be :=') + return (match.group(1), match.group(2), match.group(3)) + + +def main(): + parser = argparse.ArgumentParser() + # yapf: disable + parser.add_argument( + '--printer', + type=list_of(Printer), + default=list(Printer), + help='Printer type (default: {default}).'.format( + default=','.join(str(p.value.lower()) for p in Printer))) + parser.add_argument( + '--build-type', + type=list_of(BuildType), + default='release', + help=('Build type (debug or release; default: release; ' + 'default for --generate-cproject: debug,release).')) + parser.add_argument( + '--version-suffix', + type=str, + default='', + help='Version suffix (e.g. -BETA+1035.PR111.B4)') + parser.add_argument( + '--version-suffix-short', + type=str, + default='', + help='Version suffix (e.g. +1035)') + parser.add_argument( + '--final', + action='store_true', + help='Set\'s --version-suffix and --version-suffix-short to empty string.') + parser.add_argument( + '--build-dir', + type=Path, + help='Specify a custom build directory to be used.') + parser.add_argument( + '--products-dir', + type=Path, + help='Directory to store built firmware (default: /products).') + parser.add_argument( + '-G', '--generator', + type=str, + default='Ninja', + help='Generator to be used by CMake (default=Ninja).') + parser.add_argument( + '--toolchain', + type=Path, + help='Path to a CMake toolchain file to be used.') + parser.add_argument( + '--host-tools', + action='store_true', + help=('Build host tools (png2font and others). ' + 'Turned on by default with --generate-cproject only.') + ) + parser.add_argument( + '--no-build', + action='store_true', + help='Do not build, configure the build only.' + ) + parser.add_argument( + '--no-store-output', + action='store_false', + help='Do not write build output to files - print it to console instead.' + ) + parser.add_argument( + '-D', '--cmake-def', + action='append', type=cmake_cache_entry, + help='Custom CMake cache entries (e.g. -DCUSTOM_COMPILE_OPTIONS:STRING=-Werror)' + ) + args = parser.parse_args(sys.argv[1:]) + # yapf: enable + + build_dir_root = args.build_dir or Path( + __file__).resolve().parent.parent / 'build' + products_dir_root = args.products_dir or (build_dir_root / 'products') + + if args.final: + args.version_suffix = '' + args.version_suffix_short = '' + + # Check all dependencis are installed + if bootstrap(interactive=True).returncode != 0: + print('bootstrap.py failed.') + sys.exit(1) + + # prepare configurations + configurations = [ + FirmwareBuildConfiguration( + printer=printer, + build_type=build_type, + version_suffix=args.version_suffix, + version_suffix_short=args.version_suffix_short, + generator=args.generator, + custom_entries=args.cmake_def) for printer in args.printer + for build_type in args.build_type + ] + + # build everything + configurations_iter = tqdm(configurations) + results: Dict[BuildConfiguration, BuildResult] = dict() + for configuration in configurations_iter: + build_dir = build_dir_root / configuration.name.lower() + description = 'Building ' + configuration.name.lower() + if hasattr(configurations_iter, 'set_description'): + configurations_iter.set_description(description) + else: + print(description) + result = build(configuration, + build_dir=build_dir, + configure_only=args.no_build, + output_to_file=args.no_store_output is not False) + store_products(result.products, configuration, products_dir_root) + results[configuration] = result + + # print results + print() + print('Building finished: {} success, {} failure(s).'.format( + sum(1 for result in results.values() if not result.is_failure), + sum(1 for result in results.values() if result.is_failure))) + failure = False + max_configname_len = max(len(config.name) for config in results) + for config, result in results.items(): + if result.configuration_failed: + status = 'project configuration FAILED' + failure = True + elif result.build_failed: + status = 'build FAILED' + failure = True + else: + status = 'SUCCESS' + + print(' {} {}'.format( + config.name.lower().ljust(max_configname_len, ' '), status)) + if failure: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..227cea2 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +2.0.0