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

от автора

В данной статье будет рассмотрен проект 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» кода. Все комментарии автора оригинала кода я как всегда скрою.

  1. 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) ...
  2. Выбор директории под текущую систему реализовано в 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) ...
  3. Настройки экспорта бинарных файлов тоже можно вынести в рутовый 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 ) ...
  4. Настройка флагов компилятора. Здесь чуть более неоднозначно. В будущем это вынесется в 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) ...
  5. Макрос, который выдаёт список всех директорий с заголовочными файлами, банально не нужен. Не нужно автоматизировать то, что заведомо известно. Заменится просто на 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}") ...
  6. Сбор исходных файлов. Часть сурсов уже ушла на 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} ) ...

  7. Ума не приложу, зачем конкретно 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") ...
  8. Относится к п.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) ...

  9. Не совсем одобряю такие конструкции, где библиотеки разделены по типу и неймингу. С учетом п.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})

  10. Сторонние библиотеки уже публично подключаются к 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) ...

  11. Хардкод имени выходного бинарного файла — очень спорное решение. Я сначала не понял, зачем так сделал автор и сохранил этот функционал, заменив просто на $<$<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() ...

  12. Вызов статического анализатора 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/