В данной статье будет рассмотрен проект OpenBlt с точки зрения системы сборки CMake. Я постараюсь не теоретически или эмпирически, а именно на практике продемонстрировать, что в составном проекте лучше один раз уделить время на подготовку хорошего фундамента для архитектуры, чем впоследствии мириться с тонной дублируемого и не универсального поведения. Также я постараюсь доказать, что cmake генераторы выражений — намного легче и приятнее, чем они кажутся на первый взгляд.
* И да, я понимаю, что и на второй, и на третий взгляд за генераторы выражений хочется жалобу на Kitware подать. 😀
Для лучшего ориентирования в приведенных проектах вы можете посетить репозиторий с моим форком WorHyako/openblt (tree: arch/cmake
). Если там появятся какие-то новые коммиты, то постараюсь обновить материал статьи.
Я думаю, что потенциально можно будет даже открыть PR в оригинальный репозиторий и послушать комментарии автора, если он почтит меня вниманием.
Для максимально комфортного чтения данного материала предполагается, что вы уже имеете крепкие навыки с CMake: таргеты как объектные библиотеки, генераторы выражений, различия типов линковки библиотек и как минимум работа с модулями CMakeParseArguments, CMakePrintHelpers и PkgConfig.
Что такое OpenBlt?
OpenBLT это загрузчик с открытым исходным кодом для встраиваемых систем. Он позволит Вам и пользователям Ваших устройств на микроконтроллерах обновлять firmware через популярные сетевые интерфейсы и с карты SD. Основное достоинство OpenBLT — открытый код, что позволяет настраивать загрузчик в соответствии с Вашими потребностями.
(c) microsin
Эта цитата — первый абзац на странице сайта microsin.net. Она (цитата) даёт понимание, с каким инструментом предстоит работать, а более полную информацию можно получить уже на самой странице, гиперссылку на которую я привёл. Сайт весьма ёмко и подробно описывает рассматриваемый инструмент.
Зачем трогать OpenBlt?
Я, собственно, и не обращал внимание на код OpenBlt, несмотря на то, что мой коллега работает с ним. Моё внимание к этому проекту привлёк пользователь хабра в комментариях прошлой статьи. Ожидаемо в GitHub 200+ форков, но ноль изменений исходного репозитория, так что каждый разработчик по сути работает с кодом для собственных проектов, не предлагая новых решений для окружающих, — как сейчас говорит молодежь «100% понимания, 0% осуждения».
Инструмент имеет вид любой утилиты на Си: написана либо десятилетие, либо век назад, но при этом работает стабильнее всего, что написано после неё.
Зачем же тогда вообще вносить изменения в этот проект?
Как я уже упомянул, во-первых, это достаточно давно написанный проект, в который комьюнити не вносит изменения, но при этом активно использует, а во-вторых, я не смог удержаться после комментария пользователя хабра. 😀
Ну и по теме загрузчиков неплохой проект openblt.
https://github.com/feaser/openblt/
Он уже на симейк, поэтому переделывать ничего не нужно
(c) @Mcublog
Также было интересно прочитать вашу оценку сборки openblt, довольно плотно одно время с ним работал и остались приятные воспоминания&
(c) @Mcublog
Интро от автора статьи
Зачем было писать эту статью о OpenBlt?
Если вы читали мою прошлую статью, то помните, что в ней был описан процесс встраивания dfu-util в проект на CMake/С++, и статья намеренно имела низкий технических порог входа.
CMakeList-ы буду писать на достаточно базовом уровне как по причине своих навыков, так и для того, чтобы статья была более ёмкой и читабельной.
(с) @WorHyako
Отдельным пунктом предыдущей статьи была следующая цель:
В целом, эту статью можно даже считать псевдо-гайдом по подключению неподключаемого кода Си и написанию CMakeList-ов.
(c) @WorHyako
Так как подключение неподключаемого Си кода на примере dfu-util я уже рассмотрел, то теперь можно рассмотреть ситуацию с изменением архитектуры существующего проекта. В дополнение к этому можно учесть, что базовый уровень CMake тоже рассмотрен, поэтому можно поднять планку и более технично лиходейничать, зайдя внутрь open-source проекта OpenBlt.
Снова много воды?
Благодаря поднятию технического порога входа в текущую статью, я могу опустить объяснение ряда CMake инструкций и выражений, поэтому получилось больше пространства для технического аспекта. Я намеренно не даю ссылку на свою прошлую статью, т.к лучше ознакомьтесь с «Professional CMake: A Practical Guide» авторства Craig Scott. А то почитал я на досуге статьи а-ля «CMake: 20 советов»… Храни господь этих авторов и их тимлидов. 😀
P.S. Да простит мне уважаемое комьюнити хабра очередную статью на 20+ минут чтения, но уложиться в меньшее количество материала кажется невозможным. 🙂
Содержание
Знакомство со структурой проекта
Склонировав проект Feaser/OpenBlt, сразу понятно, что основной директорией будет OpenBlt/Host/Source
, т.к. остальное носит только информационный характер, поэтому сразу сделаю ремарку, что root
/ root_dir
/ рутовый
и прочими подобными словами я буду обозначать именно директорию OpenBlt/Host/Source
.
<root> |-- 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 |-- <target>_LINT
-
BootCommander
зависит отopenblt_shared
/openblt_static
, так что сейчас это второстепенная цель изменений; -
<target>_LINT
генерируется для каждой цели и вызывает статический анализатор файлов lint, аALL_LINT
вызывает каждую<target>_LINT
целей. Малоинтересная для текущей статьи штука, но с которой будет предостаточно проблем; -
seednkey_shared
даже смог сбилдиться (никогда такого не было и вот опять); -
openblt_shared
/openblt_static
ожидаемо упал в ошибку, т.к. линковщик не нашел сторонние библиотеки. С него и начну.
LibOpenBlt таргет
«Понеслась душа в рай, а ноги — в милицию» (с) Словарь разговорных выражений. — М.: ПАИМС. В.П. Белянин, И.А. Бутенко. 1994
Первое небольшое непонимание
Решение использовать libusb только в UNIX системе очень странно выглядит. Посмотрите на объявление в usbbulk.h
и реализацию в <os_name>/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, а не бегать между каждым из подпроектов:
# <root>/CMakeLists.txt cmake_minimum_required(VERSION 3.15) project(OpenBlt) # -------------- # # LibOpenBlt # # -------------- # add_subdirectory(LibOpenBLT)
С этим таргетом изначально проблема в том, что линковщик не может найти сторонние библиотеки. У множества людей такой проблемы может не возникнуть, потому что у них весь $ENV:PATH
утыкан путями к каждой из библиотек, или они каждый раз передают <LIB>_CFLAGS
/ <LIB>_LIBS
/ <LIB>_DIR
и прочие параметры в систему сборки (непонятно, что из этого хуже). Для libusb решается это достаточно просто. Заодно в новый файл ThirdParty.cmake
можно закинуть и поиск необходимых системных библиотек.
# <root>/CMakeLists.txt ... list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) include(ThirdParty) ...
# <root>/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 ()
# <root>/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
заголовочных путей, либо сменой одной строчки в источниках. Мне больше по душе второе, так что:
// <root>/LibOpenBLT/port/linux/usbbulk.c (Line 37) #include <libusb.h>
Проект теперь может хотя бы билдиться, но кто я такой, чтобы стесняться, поэтому сейчас начнется самое весёлое.
Текущая структура LibOpenBlt
:
<root>/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
.
<root>/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 |-- ... |-- ...
Можно изменить оригинальное расположение файлов на вышеуказанное в несколько шагов:
-
Перенести из
<root>/LibOpenBlt
файлы, содержащие декларацию порт-функций, в<root>/LibOpenBlt/port/interface
; -
Перенести файлы из
<root>/LibOpenBlt
файлы, которые являются зависимыми для порт-функций, в<root>/LibOpenBlt/port/common
; -
Перенести файл
<root>/cmake/ThirdParty.cmake
в<root>/LibOpenBlt/port/cmake/ThirdParty.cmake
, потому как будущийopenblt::port
публично подключит сторонние библиотеки и прокинет их вышестоящим целям.
# <root>/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 $<$<BOOL:${WIN32}>:${CMAKE_CURRENT_SOURCE_DIR}/windows> $<$<BOOL:${WIN32}>:${CMAKE_CURRENT_SOURCE_DIR}/windows/canif/ixxat> $<$<BOOL:${WIN32}>:${CMAKE_CURRENT_SOURCE_DIR}/windows/canif/kvaser> $<$<BOOL:${WIN32}>:${CMAKE_CURRENT_SOURCE_DIR}/windows/canif/lawicel> $<$<BOOL:${WIN32}>:${CMAKE_CURRENT_SOURCE_DIR}/windows/canif/peak> $<$<BOOL:${WIN32}>:${CMAKE_CURRENT_SOURCE_DIR}/windows/canif/vector> $<$<BOOL:${UNIX}>:${CMAKE_CURRENT_SOURCE_DIR}/linux> $<$<BOOL:${UNIX}>:${CMAKE_CURRENT_SOURCE_DIR}/linux/canif/socketcan>) target_link_libraries(openblt_port PUBLIC openblt::osLibs PkgConfig::libusb) target_compile_definitions(openblt_port PUBLIC $<$<STREQUAL:${CMAKE_C_COMPILER_ID},MSVC>:_CRT_SECURE_NO_WARNINGS> $<IF:$<BOOL:${WIN32}>,PLATFORM_WINDOWS,PLATFORM_LINUX> $<IF:$<EQUAL:${CMAKE_SIZEOF_VOID_P},4>,PLATFORM_32BIT,PLATFORM_64BIT>)
С openblt::port
покончено, так что можно переходить к таргетам openblt_shared
/ openblt_static
.
Я попробую идти по <root>/LibOpenBlt/CMakeLists.txt
файлу и писать комментарии и будущие изменения построчно. По крайней мере, мне кажется, это будет наиболее информативным представлением своего рода «overview» кода. Все комментарии автора оригинала кода я как всегда скрою.
-
CMake опции можно вынести в рутовый CMakeLists.txt, т.к. они существуют и дублируются для всех остальных целей;
# <root>/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) ...
-
Выбор директории под текущую систему реализовано в
openblt::port
;# <root>/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) ...
-
Настройки экспорта бинарных файлов тоже можно вынести в рутовый CMakeLists.txt. Причем вынести их без
foreach(...)
блока. ДиректорииCMAKE_XXX_OUTPUT_DIRECTORY
указываются точечно для бинарных выходных файлов, а не для всего билдового кэша, который MSVC любит располагать вDebug
/Release
и тп билд-префиксах, так что эти строчки бесполезны. И хотелось бы позволить клиентам кода управлять выходной директорией, поэтомуPROJECT_OUTPUT_DIRECTORY
преобразую вset(...CACHE STRING...)
;# <root>/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 ) ...
-
Настройка флагов компилятора. Здесь чуть более неоднозначно. В будущем это вынесется в
CompolerFlags.cmake
иtarget_compile_definitions(...)
, а сейчас можно просто считать, что этот блок тоже не нужен и будет где-то на более верхних уровнях;# <root>/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$<$<CONFIG:Debug>:Debug>") endif() endif(WIN32) ...
-
Макрос, который выдаёт список всех директорий с заголовочными файлами, банально не нужен. Не нужно автоматизировать то, что заведомо известно. Заменится просто на
target_include_directories(...)
с ручным перечислением директорий;# <root>/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}") ...
-
Сбор исходных файлов. Часть сурсов уже ушла на
openblt::port
. Остальные соберутся аналогичнымfile(GLOB ...)
, но без заголовочников. Хватит кидать заготовочные файлы компилятору, ему и без них тяжело;<root>/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} ) ...
-
Ума не приложу, зачем конкретно OpenBlt проекту эти настройки, но вдруг автор знает что-то. Они будут перемещены в рутовый
CMakeLists.txt
.
* rpath не существует на windows, если мне память не изменяет# <root>/LibOpenBlt/CMakeLists.txt ... set(CMAKE_SKIP_BUILD_RPATH FALSE) set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) set(CMAKE_INSTALL_RPATH "\$ORIGIN") ...
-
Относится к п.1 про CMake опции. У CMake есть общепринятый стандарт на опцию
CMAKE_BUILD_SHARED
, который говорит инструкциямadd_library(...)
, в каком виде собирать библиотеку, так что эти if-блоки удаляем;# <root>/LibOpenBlt/CMakeLists.txt ... if(BUILD_STATIC) ... endif(BUILD_STATIC) if(BUILD_SHARED) ... endif(BUILD_SHARED) ...
-
Не совсем одобряю такие конструкции, где библиотеки разделены по типу и неймингу. С учетом п.8 просто заменяем на конструкцию
add_library(<target>)
+target_sources(<target> PRIVATE ...)
, а дальше CMake сам разберется благодаря опцииCMAKE_BUILD_SHARED
. Добиваем, конечно же, это с помощью алиаса.# <root>/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})
-
Сторонние библиотеки уже публично подключаются к
openblt::port
, поэтому этот блок тоже удаляем;# <root>/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) ...
-
Хардкод имени выходного бинарного файла — очень спорное решение. Я сначала не понял, зачем так сделал автор и сохранил этот функционал, заменив просто на
$<$<STREQUAL:${CMAKE_C_COMPILER_ID},MSVC>:lib>openblt
. Но потом я увидел, что цельBootCommander
подключаетLibOpenBlt
по имени бинарного файла, которое как раз нужно захардкодить, чтобы его можно будет найти. АCLEAN_DIRECT_OUTPUT
— это, в принципе, устаревшая переменная, которая ни на что не влияет. По итогу, этот блок не нужен, т.к. в будущем подключениеLibOpenBlt
будет по CMake таргету;# <root>/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() ...
-
Вызов статического анализатора
lint
для натравливания на исходники. По ходу пьесы генерирует кастомные таргеты на существующие CMake таргеты а-ля<target>_LINT
(например,openblt_LINT
). Я долго не мог понять, зачем конкретно нужен этот блок, почему в каждом подпроекте (LibOpenBlt, BootCommander, SeedNKey) лежит своя lint директория и что будет после выполнения и тд, но по итогу осознание пришло. На текущий момент просто учтем, что этот блок мы удаляем и реализуем в отдельном файле.# <root>/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) ...
После всех изменений получается следующий файл. Выглядит уже чуть более читабельно и организовано нежели оригинальная версия:
# <root>/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
.
#<root>/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$<$<CONFIG:Debug>:Debug>") endif () if (UNIX) set(PlatformFlag "-pthread") endif () list(APPEND CMAKE_C_FLAGS ${CompilerFlag} ${PlatformFlag})
#<root>/CMakeLists.txt ... # ------------------ # # Compiler flags # # ------------------ # list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) include(CompilerFlags) ...
#<root>/LibOpenBlt/port/CMakeLists.txt ... target_compile_definitions(openblt_port PUBLIC $<$<STREQUAL:${CMAKE_C_COMPILER_ID},MSVC>:_CRT_SECURE_NO_WARNINGS> $<IF:$<BOOL:${WIN32}>,PLATFORM_WINDOWS,PLATFORM_LINUX> $<IF:$<EQUAL:${CMAKE_SIZEOF_VOID_P},4>,PLATFORM_32BIT,PLATFORM_64BIT>) ...
С целями openblt
и openblt::port
покончено. Они все стабильно билдятся, так что пора перейти к следующим подпроектам.
BootCommander
Не буду дублировать тонну текста, который уже написал, так что можно посмотреть мой «overview» по <root>/LibOpenBlt/CMakeLists.txt
и, учитывая все те комментарии, формируется достаточно компактный файл для CMake таргета BootCommander
.
# <root>/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)
# <root>/CMakeLists.txt ... # ----------------- # # BootCommander # # ----------------- # add_subdirectory(BootCommander) ...
На этом всё. С учетом проведенной работы по LibOpenBlt
все последующие таргеты пишутся легко и быстро.
SeedNKey
Аналогично с таргетом BootCommander
написание CMake файла имеет уже тривиальный характер.
# <root>/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})
# <root>/CMakeLists.txt ... # ------------ # # SeedNKey # # ------------ # add_subdirectory(SeedNKey) ...
Lint
Для понимания проблемы нужно сначала ознакомиться с текущей реализацией. Начну с того, что так выглядит структура всех упоминаний lint
-a:
<root> |-- 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. Очень легко воспринимается, т.к. это практически вызов компилятора.
Вызов будет иметь вид:
$ <lint executable> \ [-i<user config path>] std.lnt \ [-u <user flags>] \ [-i<include dir> [-i... [...]]] \ [-d<compile defs> [-d... [...]]] \ <source file>
Скрипт add_pc_lint
выглядит как кошмар и у него даже есть родина. Я до сих пор не могу понять, зачем такой «полезный» скрипт понадобился разработчику openblt, но прикрутить lint к cmake теперь звучит как вызов.
Из каждого подпроекта директорию <subproject>/lint
переносим в <root>/lint
и изменяем её следующим образом:
<root> |-- lint |-- msvc |-- ... |-- gnu |-- ... |-- CMakeLists.txt |-- pc_lint.cmake
# <root>/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
. Сначала в моих мыслях заиграл красками вот такой паттерн:
# <root>/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} $<LIST:TRANSFORM,$<TARGET_PROPERTY:${Target},INCLUDE_DIRECTORIES>,PREPEND,-i> $<LIST:TRANSFORM,$<TARGET_PROPERTY:${Target},COMPILE_DEFINITIONS>,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
Во-первых, куча пустых префиксов появляется после генерации через $<$<BOOL:${var}>:...>
;
Во-вторых, генерация даёт строку вида command "[options1]" "[options2]"...
«, а CLI такого не прощает;
В-третьих, генерация выводит не нормальную строку, а строковое представление CMake массива вида «-i...;-i...;-i...
«, а CLI такого не прощает вдвойне.
Если, в целом, эти пункты можно решить с помощью add_custom_target(... COMMAND ... COMMAND_EXPAND_LISTS ...)
и $<LIST:REMOVE_ITEM,...>
, но есть ещё одна проблема, которая может возникнуть у пользователей. Текущий вид опции [-ipath/to/include/dir]
. Возьмите паузу на данном моменте и посмотрите ещё раз на этот формат.
Ка-вы-чки. Если у пользователя проект лежит в path to/include/dir/
, то есть с директории существуют пробелы, то CLI упадёт, т.к. директория должна быть обёрнута в кавычки.
Но перед тем, как добавить обосабливание кавычками, я не могу не показать разжиревший генератор выражений. Сейчас уважаемому читателю станет больно.
# <root>/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} $<LIST:REMOVE_ITEM,$<LIST:REMOVE_DUPLICATES,$<LIST:TRANSFORM,$<TARGET_PROPERTY:${Target},INCLUDE_DIRECTORIES>,PREPEND,-i>>,-i> # prepend each definition with "-d" $<LIST:REMOVE_ITEM,$<LIST:TRANSFORM,$<TARGET_PROPERTY:${Target},COMPILE_DEFINITIONS>,PREPEND,-d>,-d> ${Source_File}) ...
Спойлер: если добавить append/prepend кавычек, то длина одного только генератора будет 182 символа.
Если вам не стало больно, то обновите страницу хабра. Скорее всего у вас просто не прогрузился этот ужас.
Ладно, этот код можно немного упростить. $<LIST:REMOVE_ITEM,...>
делает бесполезным $<LIST:REMOVE_DUPLICATES,...>
, т.к. он подчищает все пустые -d
и -i
. Но если я встрою еще APPEND
/ PREPEND
для кавычек, то понимание этой строки уничтожится безвозвратно.
В результате можно сделать генератор выражений составным и прокомментировать каждый шаг. Из-за этого и визуально стало кристально понятно, и вносить изменения в формирование команды стало намного легче.
# <root>/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 $<TARGET_PROPERTY:${ARG_TARGET},INCLUDE_DIRECTORIES>) # Append/prepend '\"' for each file set(Include_Files $<LIST:TRANSFORM,${Include_Files},APPEND,\">) set(Include_Files $<LIST:TRANSFORM,${Include_Files},PREPEND,\">) # Prepend '-i' to each file set(Include_Files $<LIST:TRANSFORM,${Include_Files},PREPEND,-i>) # Remove empty '-i""' from list set(Include_Files $<LIST:REMOVE_ITEM,${Include_Files},-i\"\">) # Original definitions set(Definitions $<TARGET_PROPERTY:${ARG_TARGET},COMPILE_DEFINITIONS>) # Append/prepend '\"' for each definition set(Definitions $<LIST:TRANSFORM,${Definitions},APPEND,\">) set(Definitions $<LIST:TRANSFORM,${Definitions},PREPEND,\">) # Prepend -d for each definition set(Definitions $<LIST:TRANSFORM,${Definitions},PREPEND,-d>) # Remove empty '-d' from list set(Definitions $<LIST:REMOVE_ITEM,${Definitions},-d\"\">) 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()
# <root>/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(<option> ...)
, т.к. занимает 2 строчки: коммент «что делаем?» и код «делаем»; -
Внесено исправление потенциальной ошибки из-за отсутствия кавычек в директориях;
-
Открыто API под изменение имени будущего
_LINT
таргетов; -
Исправлен запрос свойства директории
get_directory_property(...)
на обращение к таргету черезget_target_property(...)
и$<TARGET_PROPERTY:<target>,<property>
; -
Предыдущий пункт позволил вынести инструкцию
add_pc_lint
в рутовую директорию и вызывать её из любого места проекта. В дополнение, это позволило уйти от множественного дублирование кода и файлов; -
Оригинальная версия
add_pc_lint
имела ещё один недостаток: инструкция не смотрела на зависимости таргета.
Например,BootCommander
использует библиотекуopenblt::openblt
. Если мы натравим оригинальную инструкцию наBootCommander
, то она не вытащит из его директории заголовочных файлов и дефайны, публично объявленные вopenblt::openblt
. Текущая же версияadd_pc_lint
обращается к свойству таргета, поэтому может увидеть и дефайны, и подключаемые директории от используемых библиотек, и прочие публичные свойства.
На примере ниже можно увидеть, что теперь учитываются дефайны и пути к хидерам даже libusb; -
Переход на генераторы выражений переносит часть вычислительного процесса на генерационный этап CMake конфигурации, что является ускорением гененарации кэша.
Выходная команда новых целей <target>_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. 😀
Сможете ответить на вопрос?
Предположим, у вас есть два 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. Если у вас есть примеры библиотек, которыми вы пользуетесь в рабочем или личном пространстве, но их подготовка и настройка вызывает проблемы, буду рад рассмотреть их и предложить решение, которое упростит вам процесс работы с кодом.
ссылка на оригинал статьи https://habr.com/ru/articles/860736/
Добавить комментарий