Дневник альтруиста. OpenBlt

В данной статье будет рассмотрен проект OpenBlt с точки зрения системы сборки CMake. Я постараюсь не теоретически или эмпирически, а именно на практике продемонстрировать, что в составном проекте лучше один раз уделить время на подготовку хорошего фундамента для архитектуры, чем впоследствии мириться с тонной дублируемого и не универсального поведения. Также я постараюсь доказать, что cmake генераторы выражений — намного легче и приятнее, чем они кажутся на первый взгляд.
* И да, я понимаю, что и на второй, и на третий взгляд за генераторы выражений хочется жалобу на Kitware подать. : D

Реакция моего знакомого на часть кода из статьи

Реакция моего знакомого на часть кода из статьи

Для лучшего ориентирования в приведенных проектах вы можете посетить репозиторий с моим форком WorHyako/openblt (tree: arch/cmake). Если там появятся какие-то новые коммиты, то постараюсь обновить материал статьи.
Я думаю, что потенциально можно будет даже открыть PR в оригинальный репозиторий и послушать комментарии автора, если он почтит меня вниманием.

Дифф форка от оригинала на каком-то из моментов написания статьи.

Дифф форка от оригинала на каком-то из моментов написания статьи.

Для максимально комфортного чтения данного материала предполагается, что вы уже имеете крепкие навыки с CMake: таргеты как объектные библиотеки, генераторы выражений, различия типов линковки библиотек и как минимум работа с модулями CMakeParseArguments, CMakePrintHelpers и PkgConfig.

Что такое OpenBlt?

OpenBLT это загрузчик с открытым исходным кодом для встраиваемых систем. Он позволит Вам и пользователям Ваших устройств на микроконтроллерах обновлять firmware через популярные сетевые интерфейсы и с карты SD. Основное достоинство OpenBLT — открытый код, что позволяет настраивать загрузчик в соответствии с Вашими потребностями.

© microsin

Эта цитата — первый абзац на странице сайта microsin.net. Она (цитата) даёт понимание, с каким инструментом предстоит работать, а более полную информацию можно получить уже на самой странице, гиперссылку на которую я привёл. Сайт весьма ёмко и подробно описывает рассматриваемый инструмент.

Зачем трогать OpenBlt?

Я, собственно, и не обращал внимание на код OpenBlt, несмотря на то, что мой коллега работает с ним. Моё внимание к этому проекту привлёк пользователь хабра в комментариях прошлой статьи. Ожидаемо в GitHub 200+ форков, но ноль изменений исходного репозитория, так что каждый разработчик по сути работает с кодом для собственных проектов, не предлагая новых решений для окружающих, — как сейчас говорит молодежь »100% понимания, 0% осуждения».

Инструмент имеет вид любой утилиты на Си: написана либо десятилетие, либо век назад, но при этом работает стабильнее всего, что написано после неё.

Зачем же тогда вообще вносить изменения в этот проект?
Как я уже упомянул, во-первых, это достаточно давно написанный проект, в который комьюнити не вносит изменения, но при этом активно использует, а во-вторых, я не смог удержаться после комментария пользователя хабра. : D

Ну и по теме загрузчиков неплохой проект openblt.

https://github.com/feaser/openblt/

Он уже на симейк, поэтому переделывать ничего не нужно

© @Mcublog

Также было интересно прочитать вашу оценку сборки openblt, довольно плотно одно время с ним работал и остались приятные воспоминания&

© @Mcublog

Интро от автора статьи

Зачем было писать эту статью о OpenBlt?

Если вы читали мою прошлую статью, то помните, что в ней был описан процесс встраивания dfu-util в проект на CMake/С++, и статья намеренно имела низкий технических порог входа.

CMakeList-ы буду писать на достаточно базовом уровне как по причине своих навыков, так и для того, чтобы статья была более ёмкой и читабельной.

© @WorHyako

Отдельным пунктом предыдущей статьи была следующая цель:

В целом, эту статью можно даже считать псевдо-гайдом по подключению неподключаемого кода Си и написанию CMakeList-ов.

© @WorHyako

Так как подключение неподключаемого Си кода на примере dfu-util я уже рассмотрел, то теперь можно рассмотреть ситуацию с изменением архитектуры существующего проекта. В дополнение к этому можно учесть, что базовый уровень CMake тоже рассмотрен, поэтому можно поднять планку и более технично лиходейничать, зайдя внутрь open-source проекта OpenBlt.

Снова много воды?

Благодаря поднятию технического порога входа в текущую статью, я могу опустить объяснение ряда CMake инструкций и выражений, поэтому получилось больше пространства для технического аспекта. Я намеренно не даю ссылку на свою прошлую статью, т.к лучше ознакомьтесь с »Professional CMake: A Practical Guide» авторства Craig Scott. А то почитал я на досуге статьи а-ля «CMake: 20 советов»… Храни господь этих авторов и их тимлидов. : D

P.S. Да простит мне уважаемое комьюнити хабра очередную статью на 20+ минут чтения, но уложиться в меньшее количество материала кажется невозможным. :)

Содержание

Знакомство со структурой проекта

Склонировав проект Feaser/OpenBlt, сразу понятно, что основной директорией будет OpenBlt/Host/Source, т.к. остальное носит только информационный характер, поэтому сразу сделаю ремарку, что root / root_dir / рутовый и прочими подобными словами я буду обозначать именно директорию OpenBlt/Host/Source.


    |-- BootCommander
        |-- CMakeLists.txt
        |-- ...
    |-- LibOpenBLT
        |-- CMakeLists.txt
        |-- ...
    |-- MicroBoot
        |-- ...
    |-- SeedNKey
        |-- CMakeLists.txt
        |-- ...

Директория MicroBoot тоже не содержит чего-то интересного для рассматриваемой темы, поэтому ей уделять внимание не буду.

Немного о структуре cmake таргетов:

Таргеты, прописанные в CMakeLists-ах

BootCommander
    |-- openblt_shared OR openblt_static
seednkey_shared
openblt_shared OR openblt_static
    |-- usb-1.0 dl OR ws2_32 winusb setupapi
ALL_LINT
    |-- _LINT
  • BootCommander зависит от openblt_shared / openblt_static, так что сейчас это второстепенная цель изменений;

  • _LINT генерируется для каждой цели и вызывает статический анализатор файлов lint, а ALL_LINT вызывает каждую _LINT целей. Малоинтересная для текущей статьи штука, но с которой будет предостаточно проблем;

  • seednkey_shared даже смог сбилдиться (никогда такого не было и вот опять);

  • openblt_shared / openblt_static ожидаемо упал в ошибку, т.к. линковщик не нашел сторонние библиотеки. С него и начну.

LibOpenBlt таргет

«Понеслась душа в рай, а ноги — в милицию» © Словарь разговорных выражений. — М.: ПАИМС.В. П. Белянин, И.А. Бутенко. 1994

Первое небольшое непонимание

Решение использовать libusb только в UNIX системе очень странно выглядит. Посмотрите на объявление в usbbulk.h и реализацию в /usbbulk.c файлах.

/// ubsbulk.h
...
void UsbBulkInit(void);
void UsbBulkTerminate(void);
bool UsbBulkOpen(void);
void UsbBulkClose(void);
bool UsbBulkWrite(uint8_t const * data, uint16_t length);
bool UsbBulkRead(uint8_t * data, uint16_t length, uint32_t timeout);
...

Это достаточно тривиальные операции, которые может преспокойно выполнить кроссплатформенная libusb. Зачем автор мучался с WinAPI (SetupAPI + WinSock2) , когда можно обойтись только libusb-хой, мне не совсем понятно. Если вдруг вы подумаете, что всё-таки в libusb нет какого-то функционала, то посмотрите исходный код dfu-util. Какие там фокусы с libusb делает разработчик — волшебство.

Для удобства работы сразу со всеми подпроектами выглядит логичным создать рутовый (напоминаю, что это директория OpenBlt/Host/Source) CMakeLists.txt, а не бегать между каждым из подпроектов:

# /CMakeLists.txt

cmake_minimum_required(VERSION 3.15)

project(OpenBlt)

# -------------- #
#   LibOpenBlt   #
# -------------- #
add_subdirectory(LibOpenBLT)

С этим таргетом изначально проблема в том, что линковщик не может найти сторонние библиотеки. У множества людей такой проблемы может не возникнуть, потому что у них весь $ENV:PATH утыкан путями к каждой из библиотек, или они каждый раз передают _CFLAGS / _LIBS / _DIR и прочие параметры в систему сборки (непонятно, что из этого хуже). Для libusb решается это достаточно просто. Заодно в новый файл ThirdParty.cmake можно закинуть и поиск необходимых системных библиотек.

# /CMakeLists.txt
...
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
include(ThirdParty)
...
# /cmake/ThirdParty.cmake

cmake_minimum_required(VERSION 3.15)

# ---------- #
#   libusb   #
# ---------- #
find_package(PkgConfig REQUIRED)
pkg_check_modules(libusb REQUIRED IMPORTED_TARGET libusb-1.0)

# ------------------------ #
#   Collect OS libraries   #
# ------------------------ #
add_library(OsLibs INTERFACE)
add_library(openblt::osLibs ALIAS OsLibs)

if (WIN32)
    find_library(SetupApi REQUIRED
            NAMES setupapi)
    find_library(Ws2_32 REQUIRED
            NAMES ws2_32)
    find_library(WinUsb REQUIRED
            NAMES winusb)

    target_link_libraries(OsLibs
            INTERFACE
            ${SetupApi}
            ${Ws2_32}
            ${WinUsb})
elseif (UNIX)
    find_library(Dl REQUIRED
            NAMES dl)

    target_link_libraries(OsLibs
            INTERFACE ${Dl})
endif ()
# /LibOpenBlt/CMakeLists.txt
...
target_link_libraries(openblt_static
        PUBLIC
        PkgConfig::libusb
        openblt::osLibs)
...
target_link_libraries(openblt_shared
        PUBLIC
        PkgConfig::libusb
        openblt::osLibs)
...

После этого ожидаемо возникает проблема с libusb хидером, которая решается или сменой у таргета PkgConfig::libusb заголовочных путей, либо сменой одной строчки в источниках. Мне больше по душе второе, так что:

// /LibOpenBLT/port/linux/usbbulk.c (Line 37)

#include 

Проект теперь может хотя бы билдиться, но кто я такой, чтобы стесняться, поэтому сейчас начнется самое весёлое.

Текущая структура LibOpenBlt:

/LibOpenBlt
    |-- build
    |-- port
        |-- windows
            |-- ...
            |-- critutils.c
            |-- netaccess.c
            |-- serialport.c
            |-- timeutil.c
            |-- usbbulk.c
            |-- xcpprotect.c
        |-- linux
            |-- ...
            |-- critutils.c
            |-- netaccess.c
            |-- serialport.c
            |-- timeutil.c
            |-- usbbulk.c
            |-- xcpprotect.c
    |-- netaccess.h
    |-- serialport.h
    |-- usbbulk.c
    |-- xcpprotect.c
    |-- *.c / *.h

Посредством CMake через set(PROJECT_PORT_DIR ...) в компиляцию идут те сурсники, которые соответствуют системе, а заголовочники в LibOpenBlt декларируют сигнатуру функций из подключаемых файлов. Первый раз вижу такое кунг-фу, но идея классная, т.к по сути получаем подобие интерфейса в Си без миллиона if-def конструкций.

Смотря на это кунг-фу автора кода, напрашивается изолирование файлов, которые отвечают за текущий тип системы, в отдельный таргет openblt::port .

/LibOpenBlt
    |-- port
        |-- cmake
            |-- ThirdParty.cmake
        |-- common
            |-- aes256.h
            |-- aes256.c
            |-- candriver.h
            |-- candriver.c
            |-- util.h
            |-- util.c
        |-- interface
            |-- netaccess.h
            |-- serialport.h
            |-- usbbulk.h
            |-- xcpprotect.h
        |-- linux
            |-- ... (*.c / *.h)
        |-- windows
            |-- ...
    |-- ...

Можно изменить оригинальное расположение файлов на вышеуказанное в несколько шагов:

  • Перенести из /LibOpenBlt файлы, содержащие декларацию порт-функций, в /LibOpenBlt/port/interface;

  • Перенести файлы из /LibOpenBlt файлы, которые являются зависимыми для порт-функций, в /LibOpenBlt/port/common;

  • Перенести файл /cmake/ThirdParty.cmake в /LibOpenBlt/port/cmake/ThirdParty.cmake, потому как будущий openblt::port публично подключит сторонние библиотеки и прокинет их вышестоящим целям.

# /LibOpenBlt/port/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)

project(OpenBlt_Port
        LANGUAGES C)

# --------------- #
#   Third party   #
# --------------- #
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
include(ThirdParty)

# ---------------- #
#   OpenBlt port   #
# ---------------- #
add_library(openblt_port)
add_library(openblt::port ALIAS openblt_port)

file(GLOB_RECURSE CommonSources ${CMAKE_CURRENT_SOURCE_DIR}/common/*.c)
if (WIN32)
    file(GLOB_RECURSE Sources ${CMAKE_CURRENT_SOURCE_DIR}/windows/*.c)
elseif (UNIX)
    file(GLOB_RECURSE Sources ${CMAKE_CURRENT_SOURCE_DIR}/linux/*.c)
endif ()

target_sources(openblt_port
        PRIVATE
        ${CommonSources}
        ${Sources})

target_include_directories(openblt_port
        PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}/common
        ${CMAKE_CURRENT_SOURCE_DIR}/interface
        $<$:${CMAKE_CURRENT_SOURCE_DIR}/windows>
        $<$:${CMAKE_CURRENT_SOURCE_DIR}/windows/canif/ixxat>
        $<$:${CMAKE_CURRENT_SOURCE_DIR}/windows/canif/kvaser>
        $<$:${CMAKE_CURRENT_SOURCE_DIR}/windows/canif/lawicel>
        $<$:${CMAKE_CURRENT_SOURCE_DIR}/windows/canif/peak>
        $<$:${CMAKE_CURRENT_SOURCE_DIR}/windows/canif/vector>
        $<$:${CMAKE_CURRENT_SOURCE_DIR}/linux>
        $<$:${CMAKE_CURRENT_SOURCE_DIR}/linux/canif/socketcan>)

target_link_libraries(openblt_port
        PUBLIC
        openblt::osLibs
        PkgConfig::libusb)

target_compile_definitions(openblt_port
        PUBLIC
        $<$:_CRT_SECURE_NO_WARNINGS>
        $,PLATFORM_WINDOWS,PLATFORM_LINUX>
        $,PLATFORM_32BIT,PLATFORM_64BIT>)

С openblt::port покончено, так что можно переходить к таргетам openblt_shared / openblt_static .
Я попробую идти по /LibOpenBlt/CMakeLists.txt файлу и писать комментарии и будущие изменения построчно. По крайней мере, мне кажется, это будет наиболее информативным представлением своего рода «overview» кода. Все комментарии автора оригинала кода я как всегда скрою.

  1. CMake опции можно вынести в рутовый CMakeLists.txt, т.к. они существуют и дублируются для всех остальных целей;

    # /LibOpenBlt/CMakeLists.txt
    ...
    option(BUILD_SHARED "Configurable to enable/disable building of the shared library" ON)
    
    option(BUILD_STATIC "Configurable to enable/disable building of the static library" OFF)
    
    option(LINT_ENABLED "Configurable to enable/disable the PC-lint target" OFF)
    ...
  2. Выбор директории под текущую систему реализовано в openblt::port;

    # /LibOpenBlt/CMakeLists.txt
    ...
    if(WIN32)
        set(PROJECT_PORT_DIR ${PROJECT_SOURCE_DIR}/port/windows)
    elseif(UNIX)
        set(PROJECT_PORT_DIR ${PROJECT_SOURCE_DIR}/port/linux)
    endif(WIN32)
    ...
  3. Настройки экспорта бинарных файлов тоже можно вынести в рутовый CMakeLists.txt. Причем вынести их без foreach(...) блока. Директории CMAKE_XXX_OUTPUT_DIRECTORY указываются точечно для бинарных выходных файлов, а не для всего билдового кэша, который MSVC любит располагать в Debug / Release и тп билд-префиксах, так что эти строчки бесполезны. И хотелось бы позволить клиентам кода управлять выходной директорией, поэтому PROJECT_OUTPUT_DIRECTORY преобразую в set(...CACHE STRING...);

    # /LibOpenBlt/CMakeLists.txt
    ...
    set (PROJECT_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/../../..)
    
    set( CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_OUTPUT_DIRECTORY} )
    set( CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_OUTPUT_DIRECTORY} )
    set( CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_OUTPUT_DIRECTORY} )
    foreach( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} )
        string( TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG )
        set( CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${PROJECT_OUTPUT_DIRECTORY} )
        set( CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${PROJECT_OUTPUT_DIRECTORY} )
        set( CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${PROJECT_OUTPUT_DIRECTORY} )
    endforeach( OUTPUTCONFIG CMAKE_CONFIGURATION_TYPES )
    ...
  4. Настройка флагов компилятора. Здесь чуть более неоднозначно. В будущем это вынесется в CompolerFlags.cmake и target_compile_definitions(...), а сейчас можно просто считать, что этот блок тоже не нужен и будет где-то на более верхних уровнях;

    # /LibOpenBlt/CMakeLists.txt
    ...
    if(WIN32)
        if(CMAKE_C_COMPILER_ID MATCHES GNU)
            if(CMAKE_SIZEOF_VOID_P EQUAL 4)
                set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DPLATFORM_WINDOWS -DPLATFORM_32BIT -D_CRT_SECURE_NO_WARNINGS -std=gnu99")
            else()
                set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DPLATFORM_WINDOWS -DPLATFORM_64BIT -D_CRT_SECURE_NO_WARNINGS -std=gnu99")
            endif()
        elseif(CMAKE_C_COMPILER_ID MATCHES MSVC)
            if(CMAKE_SIZEOF_VOID_P EQUAL 4)
                set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DPLATFORM_WINDOWS -DPLATFORM_32BIT -D_CRT_SECURE_NO_WARNINGS")
            else()
                set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DPLATFORM_WINDOWS -DPLATFORM_64BIT -D_CRT_SECURE_NO_WARNINGS")
            endif()
        endif()
    elseif(UNIX)
        if(CMAKE_SIZEOF_VOID_P EQUAL 4)
            set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DPLATFORM_LINUX -DPLATFORM_32BIT -pthread -std=gnu99")
        else()
            set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DPLATFORM_LINUX -DPLATFORM_64BIT -pthread -std=gnu99")
        endif()
    endif(WIN32)
    
    if(WIN32)
        if(CMAKE_C_COMPILER_ID MATCHES MSVC)
            set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>")
        endif()
    endif(WIN32)
    ...
  5. Макрос, который выдаёт список всех директорий с заголовочными файлами, банально не нужен. Не нужно автоматизировать то, что заведомо известно. Заменится просто на target_include_directories(...) с ручным перечислением директорий;

    # /LibOpenBlt/CMakeLists.txt
    ...
    macro (header_directories return_list dir)
        file(GLOB_RECURSE new_list ${dir}/*.h)
        set(dir_list "")
        foreach(file_path ${new_list})
            get_filename_component(dir_path ${file_path} PATH)
            set(dir_list ${dir_list} ${dir_path})
        endforeach()
        list(REMOVE_DUPLICATES dir_list)
        set(${return_list} ${dir_list})
    endmacro()
    
    header_directories(PROJECT_PORT_INC_DIRS "${PROJECT_PORT_DIR}")
    include_directories("${PROJECT_SOURCE_DIR}" "${PROJECT_PORT_INC_DIRS}")
    ...
  6. Сбор исходных файлов. Часть сурсов уже ушла на openblt::port . Остальные соберутся аналогичным file(GLOB ...), но без заголовочников. Хватит кидать заготовочные файлы компилятору, ему и без них тяжело;

     /LibOpenBlt/CMakeLists.txt
    ...
    file(GLOB INCS_ROOT "*.h")
    file(GLOB_RECURSE INCS_PORT "${PROJECT_PORT_DIR}/*.h")
    set(INCS ${INCS_ROOT} ${INCS_PORT})
    
    file(GLOB SRCS_ROOT "*.c")
    file(GLOB_RECURSE SRCS_PORT "${PROJECT_PORT_DIR}/*.c")
    set(SRCS ${SRCS_ROOT} ${SRCS_PORT})
    
    set(
            LIB_SRCS
            ${SRCS}
            ${INCS}
    )
    ...
  7. Ума не приложу, зачем конкретно OpenBlt проекту эти настройки, но вдруг автор знает что-то. Они будут перемещены в рутовый CMakeLists.txt.
    * rpath не существует на windows, если мне память не изменяет

    # /LibOpenBlt/CMakeLists.txt
    ...
    set(CMAKE_SKIP_BUILD_RPATH  FALSE)
    set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
    set(CMAKE_INSTALL_RPATH "\$ORIGIN")
    ...
  8. Относится к п.1 про CMake опции. У CMake есть общепринятый стандарт на опцию CMAKE_BUILD_SHARED, который говорит инструкциям add_library(...), в каком виде собирать библиотеку, так что эти if-блоки удаляем;

    # /LibOpenBlt/CMakeLists.txt
    ...
    if(BUILD_STATIC)
        ...
    endif(BUILD_STATIC)
    
    if(BUILD_SHARED)
        ...
    endif(BUILD_SHARED)
    ...
  9. Не совсем одобряю такие конструкции, где библиотеки разделены по типу и неймингу. С учетом п.8 просто заменяем на конструкцию add_library() + target_sources( PRIVATE ...), а дальше CMake сам разберется благодаря опции CMAKE_BUILD_SHARED . Добиваем, конечно же, это с помощью алиаса.

    # /LibOpenBlt/CMakeLists.txt
    ...
    add_library(openblt_static STATIC ${LIB_SRCS})
    ...
    add_library(openblt_shared SHARED ${LIB_SRCS})
    ...

    Если вам ну о-о-очень хочется разделенные по типу линковки библиотеки, то вот вам адекватное решение:

    # example
    
    add_library(openblt OBJECT)
    add_library(openblt::obj ALIAS openblt)
    
    target_sources(openblt
            PRIVATE ...)
    
    target_include_directories(openblt
            PUBLIC ...
            PRIVATE ...)
    
    target_link_libraries(openblt
            PUBLIC ...
            PRIVATE ...)
    
    add_library(openblt_static STATIC)
    
    
    add_library(openbly_shared SHARED)

    Либо, как альтернативный вариант, если вы не доверяете CMake:

    # example
    
    option(BUILD_SHARED_LIBS "..." OFF)
    
    add_library(openblt)
    add_library(openblt::openblt ALIAS openblt)
    
    target_sources(openblt
            PRIVATE ...)
    
    target_include_directories(openblt
            PUBLIC ...
            PRIVATE ...)
    
    target_link_libraries(openblt
            PUBLIC ...
            PRIVATE ...)
    
    set_target_properties(openblt
            PROPERTIES
            POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS})
  10. Сторонние библиотеки уже публично подключаются к openblt::port, поэтому этот блок тоже удаляем;

    # /LibOpenBlt/CMakeLists.txt
    ...
    if(UNIX)
        target_link_libraries(openblt_shared usb-1.0 dl)
    elseif(WIN32)
        target_link_libraries(openblt_shared ws2_32 winusb setupapi)
    endif(UNIX)
    ...
  11. Хардкод имени выходного бинарного файла — очень спорное решение. Я сначала не понял, зачем так сделал автор и сохранил этот функционал, заменив просто на $<$:lib>openblt . Но потом я увидел, что цель BootCommander подключает LibOpenBlt по имени бинарного файла, которое как раз нужно захардкодить, чтобы его можно будет найти. А CLEAN_DIRECT_OUTPUT — это, в принципе, устаревшая переменная, которая ни на что не влияет. По итогу, этот блок не нужен, т.к. в будущем подключение LibOpenBlt будет по CMake таргету;

    # /LibOpenBlt/CMakeLists.txt
    ...
    if(CMAKE_C_COMPILER_ID MATCHES MSVC)
        SET_TARGET_PROPERTIES(openblt_shared PROPERTIES OUTPUT_NAME libopenblt CLEAN_DIRECT_OUTPUT 1)
    else()
        SET_TARGET_PROPERTIES(openblt_shared PROPERTIES OUTPUT_NAME openblt CLEAN_DIRECT_OUTPUT 1)
    endif()
    ...
  12. Вызов статического анализатора lint для натравливания на исходники. По ходу пьесы генерирует кастомные таргеты на существующие CMake таргеты а-ля _LINT (например, openblt_LINT). Я долго не мог понять, зачем конкретно нужен этот блок, почему в каждом подпроекте (LibOpenBlt, BootCommander, SeedNKey) лежит своя lint директория и что будет после выполнения и тд, но по итогу осознание пришло. На текущий момент просто учтем, что этот блок мы удаляем и реализуем в отдельном файле.

    # /LibOpenBlt/CMakeLists.txt
    ...
    if(LINT_ENABLED)
        if(CMAKE_C_COMPILER_ID MATCHES GNU)
            include(${PROJECT_SOURCE_DIR}/lint/gnu/pc_lint.cmake)
        elseif(CMAKE_C_COMPILER_ID MATCHES MSVC)
            include(${PROJECT_SOURCE_DIR}/lint/msvc/pc_lint.cmake)
        endif()
    
        if(COMMAND add_pc_lint)
            add_pc_lint(openblt ${LIB_SRCS})
        endif(COMMAND add_pc_lint)
    endif(LINT_ENABLED)
    ...

После всех изменений получается следующий файл. Выглядит уже чуть более читабельно и организовано нежели оригинальная версия:

# /LibOpenBlt/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)

project(LibOpenBLT
        LANGUAGES C)

# ----------------- #
#   openblt::port   #
# ----------------- #
add_subdirectory(port)

# ----------- #
#   openblt   #
# ----------- #
add_library(openblt)
add_library(openblt::openblt ALIAS openblt)

set_target_properties(openblt
        PROPERTIES
        POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS})

target_include_directories(openblt
        PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

file(GLOB Sources ${CMAKE_CURRENT_SOURCE_DIR}/*.c)

target_sources(openblt
        PRIVATE ${Sources})

target_link_libraries(openblt
        PUBLIC
        openblt::port
        openblt::osLibs)

Теперь можно вынести флаги компилятора в отдельный файл CompilerFlags.cmake . Флаги я сохранил как у автора, чтобы не сломать что-то несуществующее, а вот дефайны публично отправятся к цели openblt::port .

#/cmake/CompilerFlags.cmake

cmake_minimum_required(VERSION 3.15)

if (CMAKE_C_COMPILER_ID MATCHES GNU)
    set(CompilerFlag "-std=gnu99")
elseif (CMAKE_C_COMPILER_ID MATCHES MSVC)
    # Configure a statically linked run-time library for msvc
    set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>")
endif ()

if (UNIX)
    set(PlatformFlag "-pthread")
endif ()

list(APPEND CMAKE_C_FLAGS ${CompilerFlag} ${PlatformFlag})
#/CMakeLists.txt
...
# ------------------ #
#   Compiler flags   #
# ------------------ #
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
include(CompilerFlags)
...
#/LibOpenBlt/port/CMakeLists.txt
...
target_compile_definitions(openblt_port
        PUBLIC
        $<$:_CRT_SECURE_NO_WARNINGS>
        $,PLATFORM_WINDOWS,PLATFORM_LINUX>
        $,PLATFORM_32BIT,PLATFORM_64BIT>)
...

С целями openblt и openblt::port покончено. Они все стабильно билдятся, так что пора перейти к следующим подпроектам.

BootCommander

Не буду дублировать тонну текста, который уже написал, так что можно посмотреть мой «overview» по /LibOpenBlt/CMakeLists.txt и, учитывая все те комментарии, формируется достаточно компактный файл для CMake таргета BootCommander .

# /BootCommander/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)

project(BootCommander
        LANGUAGES C)

# ----------------- #
#   BootCommander   #
# ----------------- #
add_executable(BootCommander)

target_sources(BootCommander
        PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/main.c)

target_link_libraries(BootCommander
        PRIVATE openblt::openblt)
# /CMakeLists.txt
...
# ----------------- #
#   BootCommander   #
# ----------------- #
add_subdirectory(BootCommander)
...

На этом всё. С учетом проведенной работы по LibOpenBlt все последующие таргеты пишутся легко и быстро.

SeedNKey

Аналогично с таргетом BootCommander написание CMake файла имеет уже тривиальный характер.

# /SeedNKey/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)

project(SeedNKey
        LANGUAGES C)

# ------------ #
#   SeedNKey   #
# ------------ #
add_library(seednkey)
add_library(openblt::seednkey ALIAS seednkey)

target_sources(seednkey
        PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/seednkey.c)

target_include_directories(seednkey
        PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

set_target_properties(seednkey
        PROPERTIES
        POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS})
# /CMakeLists.txt
...
# ------------ #
#   SeedNKey   #
# ------------ #
add_subdirectory(SeedNKey)
...

Lint

Для понимания проблемы нужно сначала ознакомиться с текущей реализацией. Начну с того, что так выглядит структура всех упоминаний lint-a:


    |-- LibOpenBlt
        |-- lint
            |-- msvc
                |-- pc_lint.cmake
                |-- ...
            |-- gnu
                |-- pc_lint.cmake
                |-- ...
        |-- CMakeLists.txt
    |-- BootCommander
        |-- lint
            |-- msvc
                |-- pc_lint.cmake
                |-- ...
            |-- gnu
                |-- pc_lint.cmake
                |-- ...
        |-- CMakeLists.txt
    |-- SeedNKey
        |-- lint
            |-- msvc
                |-- pc_lint.cmake
                |-- ...
            |-- gnu
                |-- pc_lint.cmake
                |-- ...
        |-- CMakeLists.txt

В каждом из подпроектов практически один и тот же скрипт, одни и те же файлы.

# pc_lint.cmake

set(PC_LINT_EXECUTABLE "C:/Lint/lint-nt.exe" CACHE STRING "full path to the pc-lint executable. NOT the generated lin.bat")
set(PC_LINT_CONFIG_DIR "${PROJECT_SOURCE_DIR}/lint/msvc" CACHE STRING "full path to the directory containing pc-lint configuration files")
set(PC_LINT_USER_FLAGS "-b" CACHE STRING "additional pc-lint command line options -- some flags of pc-lint cannot be set in option files (most notably -b)")

add_custom_target(ALL_LINT)

function(add_pc_lint target)
    get_directory_property(lint_include_directories INCLUDE_DIRECTORIES)
    get_directory_property(lint_defines COMPILE_DEFINITIONS)

    set(lint_include_directories_transformed)
    foreach(include_dir ${lint_include_directories})
        list(APPEND lint_include_directories_transformed -i"${include_dir}")
    endforeach(include_dir)

    set(lint_defines_transformed)
    foreach(definition ${lint_defines})
        list(APPEND lint_defines_transformed -d${definition})
    endforeach(definition)
        
    set(pc_lint_commands)

    foreach(sourcefile ${ARGN})
        if( sourcefile MATCHES \\.c$|\\.cxx$|\\.cpp$ )
            get_filename_component(sourcefile_abs ${sourcefile} ABSOLUTE)
            list(APPEND pc_lint_commands
                COMMAND ${PC_LINT_EXECUTABLE}
                -i"${PC_LINT_CONFIG_DIR}" std.lnt
                "-u" ${PC_LINT_USER_FLAGS}
                ${lint_include_directories_transformed}
                ${lint_defines_transformed}
                ${sourcefile_abs})
        endif()
    endforeach(sourcefile)

    add_custom_target(${target}_LINT ${pc_lint_commands} VERBATIM)
    add_dependencies(ALL_LINT ${target}_LINT)

endfunction(add_pc_lint) 

И используется эта функция следующим образом в каждом из подпроектов:

# CMakeLists.txt
...
if(LINT_ENABLED)
  if(CMAKE_C_COMPILER_ID MATCHES GNU)  
    include(${PROJECT_SOURCE_DIR}/lint/gnu/pc_lint.cmake)
  elseif(CMAKE_C_COMPILER_ID MATCHES MSVC)
    include(${PROJECT_SOURCE_DIR}/lint/msvc/pc_lint.cmake)
  endif()

  if(COMMAND add_pc_lint)
    add_pc_lint(openblt ${LIB_SRCS})
  endif(COMMAND add_pc_lint)
endif(LINT_ENABLED)

Как следует из скрипта, то вызывается lint для каждого исходного *.c файла с перечислением директорий с заголовочниками, дефанов, флагов для lint-a. Очень легко воспринимается, т.к. это практически вызов компилятора.

Вызов будет иметь вид:

$  \
    [-i] std.lnt \
    [-u ] \
    [-i [-i... [...]]] \
    [-d [-d... [...]]] \
    

Скрипт add_pc_lint выглядит как кошмар и у него даже есть родина. Я до сих пор не могу понять, зачем такой «полезный» скрипт понадобился разработчику openblt, но прикрутить lint к cmake теперь звучит как вызов.

Из каждого подпроекта директорию /lint переносим в /lint и изменяем её следующим образом:


    |-- lint
        |-- msvc
            |-- ...
        |-- gnu
            |-- ...
    |-- CMakeLists.txt
    |-- pc_lint.cmake
# /lint/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)

project(Lint
    LANGUAGES NONE)

list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
include(pc_lint)

А теперь pc-lint.cmake . Сначала в моих мыслях заиграл красками вот такой паттерн:

# /lint/pc_lint.cmake
...
include(CMakeParseArguments)

function(add_pc_lint)
    set(multiValueArgs TARGETS)
    set(oneValueArgs NAME)

    cmake_parse_arguments(ARG
            "${options}"
            "${oneValueArgs}"
            "${multiValueArgs}"
            ${ARGN})

    foreach (Target ${ARG_TARGETS})
        get_target_property(Target_Sources
                ${Target} SOURCES)

        foreach (Source_File ${Target_Sources})
            list(APPEND Pc_Lint_Commands
                    COMMAND ${PC_LINT_EXECUTABLE}
                    -i"${PC_LINT_CONFIG_DIR}" std.lnt
                    "-u" ${PC_LINT_USER_FLAGS}
                    $,PREPEND,-i>
                    $,PREPEND,-d>
                    ${Source_File})
        endforeach ()
    endforeach ()

Но выходная команды имела дефекты. Часть команды из одной итерации foreach() блока:

$ lint-nt.exe \
      -i\"/Users/worhyako/Coding/OpenBlt/Host/Source/msvc\" std.lnt \
      -u -b \
      "-i/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT; \
      -i/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT/port/common; \
      -i/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT/port/interface; \
      -i; \
      -i; \
      -i; \
      -i; \
      -i; \
      -i; \
      -i/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT/port/linux; \
      -i/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT/port/linux/canif/socketcan; \
      -i/opt/homebrew/Cellar/libusb/1.0.27/include/libusb-1.0" \
      "-d; \
      -dPLATFORM_LINUX; \
      -dPLATFORM_64BIT" \
      /Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT/firmware.c

Во-первых, куча пустых префиксов появляется после генерации через $<$:...>;
Во-вторых, генерация даёт строку вида command "[options1]" "[options2]"... », а CLI такого не прощает;
В-третьих, генерация выводит не нормальную строку, а строковое представление CMake массива вида »-i...;-i...;-i...», а CLI такого не прощает вдвойне.

Если, в целом, эти пункты можно решить с помощью add_custom_target(... COMMAND ... COMMAND_EXPAND_LISTS ...) и $, но есть ещё одна проблема, которая может возникнуть у пользователей. Текущий вид опции [-ipath/to/include/dir] . Возьмите паузу на данном моменте и посмотрите ещё раз на этот формат.
Ка-вы-чки. Если у пользователя проект лежит в path to/include/dir/, то есть с директории существуют пробелы, то CLI упадёт, т.к. директория должна быть обёрнута в кавычки.

Но перед тем, как добавить обосабливание кавычками, я не могу не показать разжиревший генератор выражений. Сейчас уважаемому читателю станет больно.

# /lint/pc_lint.cmake
...
list(APPEND Pc_Lint_Commands
        COMMAND ${PC_LINT_EXECUTABLE}
        -i"${PC_LINT_CONFIG_DIR}" std.lnt
        "-u" ${PC_LINT_USER_FLAGS}
        $,PREPEND,-i>>,-i>
        # prepend each definition with "-d"
        $,PREPEND,-d>,-d>
        ${Source_File})
...

Спойлер: если добавить append/prepend кавычек, то длина одного только генератора будет 182 символа.
Если вам не стало больно, то обновите страницу хабра. Скорее всего у вас просто не прогрузился этот ужас.

Ладно, этот код можно немного упростить. $ делает бесполезным $, т.к. он подчищает все пустые -d и -i . Но если я встрою еще APPEND / PREPEND для кавычек, то понимание этой строки уничтожится безвозвратно.

В результате можно сделать генератор выражений составным и прокомментировать каждый шаг. Из-за этого и визуально стало кристально понятно, и вносить изменения в формирование команды стало намного легче.

# /lint/pc_lint.cmake
...
function(add_pc_lint)
    set(oneValueArgs TARGET NAME)

    cmake_parse_arguments(ARG
            "${options}"
            "${oneValueArgs}"
            "${multiValueArgs}"
            ${ARGN})

    get_target_property(Target_Sources
            ${ARG_TARGET} SOURCES)

    # Original include files
    set(Include_Files $)
    # Append/prepend '\"' for each file
    set(Include_Files $)
    set(Include_Files $)
    # Prepend '-i' to each file
    set(Include_Files $)
    # Remove empty '-i""' from list
    set(Include_Files $)

    # Original definitions
    set(Definitions $)
    # Append/prepend '\"' for each definition
    set(Definitions $)
    set(Definitions $)
    # Prepend -d for each definition
    set(Definitions $)
    # Remove empty '-d' from list
    set(Definitions $)

    foreach (Source_File ${Target_Sources})
        list(APPEND Pc_Lint_Commands
                COMMAND ${PC_LINT_EXECUTABLE}
                -i"${PC_LINT_CONFIG_DIR}" std.lnt
                "-u" ${PC_LINT_USER_FLAGS}
                ${Include_Files}
                ${Definitions}
                ${Source_File})
    endforeach ()

    # add a custom target consisting of all the commands generated above
    add_custom_target(${ARG_NAME} ${Pc_Lint_Commands} COMMAND_EXPAND_LISTS VERBATIM)
    # make the ALL_LINT target depend on each and every *_LINT target
    add_dependencies(ALL_LINT ${ARG_NAME})
endfunction()
# /lint/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)

project(Lint
        LANGUAGES NONE)

list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
include(pc_lint)

# ---------------- #
#   openblt_LINT   #
# ---------------- #
add_pc_lint(TARGET openblt NAME openblt_LINT)

# ----------------------- #
#    BootCommander_LINT   #
# ----------------------- #
add_pc_lint(TARGET BootCommander NAME BootCommander_LINT)

# ----------------- #
#   seednkey_LINT   #
# ----------------- #
add_pc_lint(TARGET seednkey NAME seednkey_LINT)

Имеет ли смысл смена тела функции add_pc_lint и перенос всей lint части в рутовую директорию? Перечислю самые очевидные причины:

  • Текущая конструкция более универсальна по сравнению со множественными foreach() + list(, т.к. занимает 2 строчки: коммент «что делаем?» и код «делаем»;

  • Внесено исправление потенциальной ошибки из-за отсутствия кавычек в директориях;

  • Открыто API под изменение имени будущего _LINT таргетов;

  • Исправлен запрос свойства директории get_directory_property(...) на обращение к таргету через get_target_property(...) и $,;

  • Предыдущий пункт позволил вынести инструкцию add_pc_lint в рутовую директорию и вызывать её из любого места проекта. В дополнение, это позволило уйти от множественного дублирование кода и файлов;

  • Оригинальная версия add_pc_lint имела ещё один недостаток: инструкция не смотрела на зависимости таргета.
    Например, BootCommander использует библиотеку openblt::openblt. Если мы натравим оригинальную инструкцию на BootCommander, то она не вытащит из его директории заголовочных файлов и дефайны, публично объявленные в openblt::openblt . Текущая же версия add_pc_lint обращается к свойству таргета, поэтому может увидеть и дефайны, и подключаемые директории от используемых библиотек, и прочие публичные свойства.
    На примере ниже можно увидеть, что теперь учитываются дефайны и пути к хидерам даже libusb;

  • Переход на генераторы выражений переносит часть вычислительного процесса на генерационный этап CMake конфигурации, что является ускорением гененарации кэша.

Выходная команда новых целей _LINT имеет уже корректный вид:

# Пример вызова анализа для файла firmware.c из таргета openblt_LINT
$ lint-nt.exe 
    -i\"/Users/worhyako/Coding/OpenBlt/Host/Source/msvc\" std.lnt \
    -u -b  \
    -i\"/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT\" \
    -i\"/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT/port/common\" \
    -i\"/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT/port/interface\" \
    -i\"/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT/port/linux\" \
    -i\"/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT/port/linux/canif/socketcan\" \ 
    -i\"/opt/homebrew/Cellar/libusb/1.0.27/include/libusb-1.0\" \
    -dPLATFORM_LINUX \
    -dPLATFORM_64BIT \
    \"/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT/firmware.c\" 

# Пример вызова анализа для файла main.c из таргета BootCommander_LINT
$ lint-nt.exe 
    -i\"/Users/worhyako/Coding/OpenBlt/Host/Source/msvc\" std.lnt \
    -u -b \
    -i\"/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT\" \
    -i\"/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT/port/common\" \
    -i\"/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT/port/interface\" \
    -i\"/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT/port/linux\" \
    -i\"/Users/worhyako/Coding/OpenBlt/Host/Source/LibOpenBLT/port/linux/canif/socketcan\" \
    -i\"/opt/homebrew/Cellar/libusb/1.0.27/include/libusb-1.0\" \
    -dPLATFORM_LINUX \
    -dPLATFORM_64BIT \
    \"main.c\"

На этом с последним подпроектом покончено.

Заключение

OpenBlt послужил отличным тренировочным манекеном для практики CMake навыков, а главное для базового понимания архитектуры С/С++ проектов. После рассмотренных изменений процесс сборки, а главное читаемость процесса сборки упростилась в разы.

Аутро от автора статьи

Подошла к концу вторая часть Дневника альтруиста.

Данная часть достаточно хорошо рассматривает работу с архитектурой Си/С++ проекта. Надеюсь, у меня получилось выдержать баланс между повышением технического порога для статьи и понятным предоставлением материала. Текущий проект де-факто закрывает цель написание псевдо-гайда по работе с модульными проектами. Оставшихся кейсов типов проектов остается не так много. Что ещё существует… Си проект под gcc-arm-none-eabi и STM32? CMake/C#/C++ проект? CMake/C/Python проект? Мультиязычные сборки под CMake встречаются очень редко, и их причины существования — скорее абсурд, чем что-то оправданное.

Закончу аутро цитатой из песни »Собиратель легенд — Norma Tale, ночной карась»:

«Моя странная муза никогда не любила,
Но пыталась казаться отвратительно милой»

Как же хорошо эти строчки описывают мои отношения с CMake. : D

Сможете ответить на вопрос?

Предположим, у вас есть два CMakeLists.txt файла foo/CMakeLists.txt и bar/CMakeLists.txt со следующими инструкциями:

# foo/CMakeLists.txt

project(example
        LANGUAGES CXX C ASM)

set(CMAKE_C_FLAGS ${CMAKE_C_FLAGS} ...)
set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} ...)
set(CMAKE_C_LINK_FLAGS ${CMAKE_C_LINK_FLAGS} ...)
set(CMAKE_ASM_FLAGS ${CMAKE_ASM_FLAGS} ...)
...
# bar/CMakeLists.txt

set(CMAKE_C_FLAGS ${CMAKE_C_FLAGS} ...)
set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} ...)
set(CMAKE_C_LINK_FLAGS ${CMAKE_C_LINK_FLAGS} ...)
set(CMAKE_ASM_FLAGS ${CMAKE_ASM_FLAGS} ...)

project(example
        LANGUAGES CXX C ASM)
...

В чем состоит разница между расположением CMAKE_XXX_FLAGS и почему один вариантов приведёт к фатальной ошибке?
Подсказка:

# hint

project(hint
        LANGUAGES NONE)

P.S. Если у вас есть примеры библиотек, которыми вы пользуетесь в рабочем или личном пространстве, но их подготовка и настройка вызывает проблемы, буду рад рассмотреть их и предложить решение, которое упростит вам процесс работы с кодом.

© Habrahabr.ru