Пишем обертку для сборки OpenSSL на CMake

Столкнулся я с ситуацией, в которой нужно было собирать OpenSSL под разные операционные системы и архитектуры процессоров. В сумме насчитывается 5 сборок.

Главной проблемой сборки OpenSSL выступает система сборки — Autotools, ее сложно интегрировать в CMake. В данной статье рассмотрим как приложив минимальное количество усилий перенести сборку OpenSSL на CMake.
Сборка OpenSSL для Linux систем выглядит так:

chmod +x ./Configure
./Configure [target-arch] [flags]
make clean
make -j 6
make install

Нативная сборка под Windows примерно так:

call "/vcvars32.bat"
rem или "/vcvars64.bat"
perl Configure [target-arch] [flags]
nmake clean
nmake

Обычным считается кейс, когда CMake используется для конфигурирования и сборки некоторой цели, правила конфигурации и сборки которой определены в файле CMakeLists.txt. Дальше идет стандартный сценарий

cmake -S . -B build && cmake --build build

Есть и другая сторона медали, где CMake можно использовать для выполнения скриптов вызовом волшебной команды

cmake -P 

На данном этапе можно понять, что нет необходимости бросаться переносить OpenSSL на CMake — достаточно просто сделать обертку с использованием скриптов. К CMake скриптам также применимы опции, как и в обычном сценарии использования, исходя из этого определение необходимых действий для каждого варианта сборки можно регулировать опциями.

При написании обертки были поставлены следующие цели:

  1. Превратить скрипты сборки в единый скрипт, для упрощения интеграции с CMake;

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

Скрытый текст

Примерно следующее вы получите в лицо при компиляции

3d8bfe6c1129304c997bc15d35308e05.png

Для уменьшения объема кода разделим скрипт на 3 части:

  1. build.cmake — основной модуль сборки;

  2. build_win_native.cmake — нативная сборка под windows (необходима конкретно в моем кейсе);

  3. prepare_build.cmake — конфигурирование сборки.

Идея обертки заключается в превращении процесса сборки в конструктор команд, например команда конфигурирования OpenSSL для Linux систем может выглядеть так

./Configure linux-x86_64 no-asm no-tests

или так

./Configure linux-mips32 -znow -zrelro -Wl,--gc-sections enable-shared \
	-DOPENSSL_PREFER_CHACHA_OVER_GCM -DOPENSSL_SMALL_FOOTPRINT no-afalgeng \
	no-aria no-asan no-async no-blake2 no-buildtest-c++ no-camellia no-comp \
	no-crypto-mdebug no-crypto-mdebug-backtrace no-devcryptoeng no-dtls \
	no-dtls1 no-dtls1_2 no-ec2m no-ec_nistp_64_gcc_128 no-egd \
	no-external-tests no-fuzz-afl no-fuzz-libfuzzer no-gost no-heartbeats \
	no-hw-padlock no-idea no-md2 no-mdc2 no-msan no-nextprotoneg no-rc5 \
	no-rfc3779 no-sctp no-seed no-sm2 no-sm3 no-sm4 no-ssl-trace no-ssl3 \
	no-ssl3-method no-ubsan no-unit-test no-tests no-weak-ssl-ciphers \
	no-whirlpool no-zlib no-zlib-dynamic

Независимо от флагов компиляции нужно передавать целевую архитектуру сборки, для этого добавим переменную описывающую используемые архитектуры.

set(ARCH "LINUX_X86" CACHE STRING 
	"Arch option can be: LINUX_X86 (default), MIPS, ARM, WIN32 and WIN64"
)

Перейдем к основной функции скрипта сборки. В ней нужно предусмотреть нативную сборку для Windows в отдельной ветке, чтобы было проще в дальнейшем от нее отказаться.
По классике необходимо выделить команды: prepare, clean, configure и compile.

function(build)
	if(WIN32)
		build_win_native() 
		return()
	elseif(UNIX)
		prepare_build_lin()
		clean()
		configure()
		compile()
	endif()
endfunction(build) 

# При вызове команды cmake -P build.cmake вызываем главную функцию
build()

Вызов терминальных команд будет осуществляться при помощи команды execute_process (), при ее вызове необходимо:

  1. Получить код ошибки;

  2. Скрыть информацию отправляющуюся в stdout;

  3. Перенаправить stderr в файл.

set(LOG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/build_errors.log")
#...
function(clean)
	execute_process(
		COMMAND $ENV{CLEAN_COMMAND}
		ERROR_FILE ${LOG_FILE}
		OUTPUT_QUIET
	)
endfunction(clean)

Исходя из вышеописанной функции clean () следует, что общение между файлами будет осуществляться через переменные окружения CMake. Можно было использовать кэширование переменных, но тогда в момент их объявления пришлось бы тянуть хвост в виде CACHE STRING «something help info» или объявлять их отдельно.
Для гибкости стоит предусмотреть, что флаги конфигурации OpenSSL могут меняться, для этого была добавлена проверка передаваемых флагов.

set(CONFIGURE_FLAGS "" CACHE STRING "Configure options")
#...
function(configure)
	if(NOT CONFIGURE_FLAGS STREQUAL "")
		string(REPLACE " " ";" _CONFIGURE_FLAGS "${CONFIGURE_FLAGS}")
		set(ENV{CONFIGURE_FLAGS} "${_CONFIGURE_FLAGS}")
	endif()
#...
endfunction(configure)

Стадия prepare подразумевает установку не только команд сборки, но и в проверке возможности сборки под конкретную архитектуру.

# prepare_build.cmake
function(prepare_build_lin)
	# Установка компиляторов для CMake скриптов
    # отличается от сборки обычного проекта
	set(CMAKE_C_COMPILER $ENV{CC})
	set(CMAKE_CXX_COMPILER $ENV{CXX})
	  
	# Валидация хост системы лишней не будет
	execute_process(
		COMMAND uname -m
		OUTPUT_VARIABLE HOST_ARCH
		OUTPUT_STRIP_TRAILING_WHITESPACE
	)
	
	if(ARCH STREQUAL "ARM" AND (CMAKE_C_COMPILER MATCHES "arm" OR 
       HOST_ARCH MATCHES "arm"))
		prepare_linux_arm()
	elseif(ARCH STREQUAL "MIPS" AND (CMAKE_C_COMPILER MATCHES "mips" OR 
           HOST_ARCH MATCHES "mips"))
		prepare_linux_mips()
	elseif(ARCH STREQUAL "WIN64")
		prepare_win64()
	elseif(ARCH STREQUAL "WIN32")
		prepare_win32()
	elseif(ARCH STREQUAL "LINUX_X86")
		prepare_linux_x86()
	else()
		message(FATAL_ERROR "Bad ARCH option")
	endif()
	prepare_linux_general()
endfunction(prepare_build_lin)

Проверка переменной CMAKE_C_COMPILER не актуальна для windows, т.к. при использовании единого Docker образа для WIN32 и WIN64 раннее связывание неуместно, поэтому установленный компилятор нужно проверять в функции prepare_win ().

function(prepare_win64)
#...
	if (NOT CMAKE_C_COMPILER MATCHES "mingw")
		set(ENV{CONFIGURE_FLAGS}
		"$ENV{CONFIGURE_FLAGS};--cross-compile-prefix=x86_64-w64-mingw32-")
	endif()
endfunction(prepare_win64)

Каждая функция prepare*() устанавливает переменные целевой архитектуры и флаги компиляции.

function(prepare_linux_arm)
	set(ENV{_ARCH} "linux-aarch64")
	set(ENV{CONFIGURE_FLAGS} "no-asm;no-tests")
endfunction(prepare_linux_arm)

Команда configure () должна вызывать команду конфигурирования с указанием архитектуры и флагов компиляции.

function(configure)
#...
	execute_process(
		COMMAND $ENV{PREPARE_COMMAND} $ENV{CONFIGURE_COMMAND} $ENV{_ARCH} $ENV{CONFIGURE_FLAGS}
		WORKING_DIRECTORY .
		RESULT_VARIABLE ret
		ERROR_FILE ${LOG_FILE}
		OUTPUT_QUIET
	)
endfunction(configure)

Команда compile () должна вызвать команду сборки.

function(compile)
	execute_process(
		COMMAND $ENV{COMPILE_COMMAND} $ENV{COMPILE_FLAG}
		WORKING_DIRECTORY .
		RESULT_VARIABLE ret
		ERROR_FILE ${LOG_FILE}
		OUTPUT_QUIET
	)
endfunction(compile)

Вызов скрипта сборки происходит следующей командой:

cmake -P build.cmake # компиляция для архитектуры поумолчанию
cmake -DARCH= build.cmake # компиляция для конкретной архитектуры

По итогу мы написали универсальную обертку, которая:

  1. Cтандартизирует процесс сборки OpenSSL;

  2. Упрощает интеграцию с проектами написанными на CMake;

  3. Реализует простейшую систему логирования.

© Habrahabr.ru