Cmake

Кросс-компиляция и toolchain-файлы: сборка для другой платформы

Введение

Embedded-цикл основного плана (статьи 351-360) разбирал STM32/bare-metal разработку, подразумевая сборку под архитектуру, отличную от той, на которой выполняется сама компиляция — в этой статье разберём механизм CMake, делающий такую кросс-компиляцию управляемой и переносимой: toolchain-файлы.

Концепция

Toolchain-файл — отдельный CMake-файл, явно указывающий компилятор и другие инструменты сборки, предназначенные для ЦЕЛЕВОЙ платформы (которая может существенно отличаться от платформы, на которой реально выполняется сама сборка — типичный случай для embedded-разработки, где сборка происходит на обычном desktop-компьютере, но результат предназначен для микроконтроллера) — этот файл передаётся CMake через -DCMAKE_TOOLCHAIN_FILE=... при первой конфигурации проекта, и определяет такие критичные параметры, как путь к кросс-компилятору, флаги, специфичные для целевой архитектуры, и корневой путь поиска библиотек целевой платформы (CMAKE_FIND_ROOT_PATH, важный для предотвращения случайного использования библиотек платформы сборки вместо целевой платформы).

Пример кода: Кросс-компиляция и toolchain-файлы

# toolchain-arm-none-eabi.cmake — toolchain-файл для ARM Cortex-M микроконтроллера (статьи 351-360)
set(CMAKE_SYSTEM_NAME Generic)          # "Generic" — для embedded-целей без полноценной ОС
set(CMAKE_SYSTEM_PROCESSOR arm)

set(CMAKE_C_COMPILER arm-none-eabi-gcc)   # явное указание КРОСС-компилятора, а не системного gcc
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)

set(CMAKE_C_FLAGS "-mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}")

set(CMAKE_EXE_LINKER_FLAGS "-T${CMAKE_SOURCE_DIR}/linker_script.ld") # специфичный для embedded линкер-скрипт

# Предотвращение попыток CMake выполнить тестовые программы — НЕВОЗМОЖНО для целевой embedded-платформы!
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
# Использование toolchain-файла при конфигурации проекта
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain-arm-none-eabi.cmake ..
cmake --build .
# Результат — исполняемый файл для ARM Cortex-M, хотя сама сборка ВЫПОЛНЯЛАСЬ на обычном x86_64 desktop
# CMAKE_FIND_ROOT_PATH — предотвращение случайного использования библиотек ХОСТ-платформы
set(CMAKE_FIND_ROOT_PATH /opt/arm-toolchain/sysroot) # корень файловой системы ЦЕЛЕВОЙ платформы

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)  # программы (инструменты) ищем НА ХОСТЕ
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)   # библиотеки ищем ТОЛЬКО в sysroot целевой платформы
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)   # заголовки — аналогично, ТОЛЬКО целевая платформа

Пояснения к коду

set(CMAKE_SYSTEM_NAME Generic) показывает специфичную деталь именно для embedded без операционной системы — для кросс-компиляции под Linux/Android на другой архитектуре было бы указано соответствующее имя системы, но «Generic» сигнализирует CMake, что целевая платформа не имеет полноценной ОС, что отключает некоторые предположения CMake, неприменимые для такого случая. CMAKE_C_COMPILER arm-none-eabi-gcc явно указывает кросс-компилятор вместо обычного, системного компилятора — это центральная, определяющая настройка toolchain-файла, без которой CMake попытался бы использовать компилятор, генерирующий код для платформы сборки, а не для реальной целевой embedded-платформы. CMAKE_FIND_ROOT_PATH с детализированными режимами поиска (NEVER/ONLY для разных категорий — программы, библиотеки, заголовки) показывает важный, тонкий аспект кросс-компиляции — нужные инструменты сборки (как сам компилятор) логично искать на хост-платформе, где реально выполняется сборка, тогда как библиотеки и заголовки, с которыми линкуется итоговый, целевой код, должны браться именно из окружения целевой платформы (sysroot), а не случайно найденные в системе хоста аналоги, несовместимые по архитектуре с реальной целевой платформой.

Подводные камни

  • Попытка использовать инструменты, выполняющие реальный код, скомпилированный для целевой платформы, прямо во время конфигурации CMake (как стандартные try_compile/try_run проверки, выполняемые на этапе конфигурации) — для кросс-компиляции это технически невозможно (целевой код не может быть выполнен непосредственно на хост-платформе сборки), и CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY (как показано в примере) — один из способов адаптировать поведение CMake для таких проверок, заменяя попытку реального запуска на просто проверку успешности компиляции, без попытки выполнить результат.
  • Случайное использование библиотек/заголовков хост-платформы вместо целевой платформы из-за неверной настройки CMAKE_FIND_ROOT_PATH_MODE_* — без правильной, явной настройки этих режимов (ONLY для библиотек/заголовков, как в примере) find_package/find_library (статья 578) могут случайно найти и использовать несовместимые по архитектуре библиотеки, установленные в обычных системных путях хост-платформы, что приведёт либо к явной ошибке линковки (несовместимая архитектура), либо, что хуже, к менее очевидной проблеме в более сложных, специфичных случаях смешанной архитектуры.
  • Игнорирование необходимости отдельной директории сборки для каждой целевой платформы при работе с несколькими разными toolchain-файлами для одного и того же исходного кода (например, сборка одновременно для тестирования на desktop и реальной сборки для embedded-цели) — поскольку toolchain-файл указывается только при ПЕРВОЙ конфигурации директории сборки и не может быть легко изменён впоследствии для уже существующей, сконфигурированной директории, для каждой целевой платформы нужна отдельная, независимая директория сборки.
  • Недостаточное тестирование toolchain-файла на действительно «чистой» машине без предварительно настроенного окружения разработки (аналогично подводным камням статьи 451 DevOps-цикла про упаковку приложений) — toolchain-файл, успешно работающий на машине, где кросс-компилятор и все нужные пути уже исторически настроены через переменные окружения или предыдущие, неявные настройки системы, может не сработать на свежей, чистой машине без этой неявной, неосознанно накопленной настройки, и для надёжной, переносимой кросс-компиляции toolchain-файл должен явно, исчерпывающе указывать все нужные пути и настройки, не полагаясь на неявные, недокументированные предположения о состоянии конкретной машины сборки.