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

Для лучшего ориентирования в приведенных проектах вы можете посетить репозиторий с моим форком 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% осуждения".
Инструмент имеет вид любой утилиты на Си: написана либо десятилетие, либо век назад, но при этом работает стабильнее всего, что написано после неё.
Зачем же тогда вообще вносить изменения в этот проект?
Как я уже упомянул, во-первых, это достаточно давно написанный проект, в который комьюнити не вносит изменения, но при этом активно использует, а во-вторых, я не смог удержаться после комментария пользователя хабра. :D
Ну и по теме загрузчиков неплохой проект 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 советов"... Храни господь этих авторов и их тимлидов. :D
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. :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. Если у вас есть примеры библиотек, которыми вы пользуетесь в рабочем или личном пространстве, но их подготовка и настройка вызывает проблемы, буду рад рассмотреть их и предложить решение, которое упростит вам процесс работы с кодом.