I encountered a situation where I needed to build OpenSSL for different operating systems and processor architectures. In total, there are 5 builds.
The main problem with building OpenSSL is the build system - Autotools , it is difficult to integrate it into CMake. In this article, we will consider how to transfer the OpenSSL build to CMake with minimal effort.
Building OpenSSL for Linux systems looks like this:
chmod +x ./Configure
./Configure [target-arch] [flags]
make clean
make -j 6
make install
The native build for Windows is something like this:
call "<Path for Visual studio toolkit>/vcvars32.bat"
rem или "<Path for Visual studio toolkit>/vcvars64.bat"
perl Configure [target-arch] [flags]
nmake clean
nmake
A common case is when CMake is used to configure and build a target whose configuration and build rules are defined in the CMakeLists.txt file. The following is a standard scenario
cmake -S . -B build && cmake --build build
There is another side of the coin, where CMake can be used to execute scripts by calling a magic command
cmake -P <file.cmake>
At this point, you can see that there is no need to rush to port OpenSSL to CMake - it is enough to simply make a wrapper using scripts. CMake scripts also apply options, as in the normal usage scenario, based on this, the definition of the necessary actions for each build option can be adjusted by options.
The following goals were set when writing the wrapper:
- Convert build scripts into a single script to simplify integration with CMake;
- Implement a small logging system to avoid getting crazy output about the compilation process in your face. OpenSSL spits out a lot of unnecessary information when building, it will be enough to output information only if the build failed.
To reduce the amount of code, we will divide the script into 3 parts:
- build.cmake - the main build module;
- build_win_native.cmake - native build for Windows (necessary specifically in my case);
- prepare_build.cmake - configure the build.
The idea behind the wrapper is to turn the build process into a command constructor, for example the OpenSSL configuration command for Linux systems might look like this
./Configure linux-x86_64 no-asm no-tests
or like this
./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
Regardless of the compilation flags, you need to pass the target assembly architecture, for this we will add a variable describing the architectures used.
set(ARCH "LINUX_X86" CACHE STRING
"Arch option can be: LINUX_X86 (default), MIPS, ARM, WIN32 and WIN64"
)
Let’s move on to the main function of the build script. It is necessary to provide a native build for Windows in a separate branch, so that it would be easier to abandon it later.
According to the classics, it is necessary to highlight the commands: prepare, clean, configure and compile.
function(build)
if(WIN32)
build_win_native()
return()
elseif(UNIX)
prepare_build_lin()
clean()
configure()
compile()
endif()
endfunction(build)
build()
The terminal commands will be called using the execute_process() command , when calling it you need to:
- Get error code;
- Hide information sent to stdout;
- Redirect stderr to a file.
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)
Based on the above-described clean() function , it follows that communication between files will be carried out via CMake environment variables. Variable caching could be used, but then at the moment of their declaration it would be necessary to drag a tail in the form of CACHE STRING “something help info” or declare them separately.
For flexibility, it is worth considering that OpenSSL configuration flags can change, for this purpose a check of the transmitted flags was added.
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)
The prepare stage involves installing not only the build commands, but also checking the ability to build for a specific architecture.
# prepare_build.cmake
function(prepare_build_lin)
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)
Checking the CMAKE_C_COMPILER variable is not relevant for Windows, because when using a single Docker image for WIN32 and WIN64, early binding is inappropriate, so the installed compiler should be checked in the prepare_win() function .
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)
Each prepare*() function sets the target architecture variables and compilation flags.
function(prepare_linux_arm)
set(ENV{_ARCH} "linux-aarch64")
set(ENV{CONFIGURE_FLAGS} "no-asm;no-tests")
endfunction(prepare_linux_arm)
The configure() command must invoke the configure command with the specified architecture and compilation flags.
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)
The compile() command should call the build command.
function(compile)
execute_process(
COMMAND $ENV{COMPILE_COMMAND} $ENV{COMPILE_FLAG}
WORKING_DIRECTORY .
RESULT_VARIABLE ret
ERROR_FILE ${LOG_FILE}
OUTPUT_QUIET
)
endfunction(compile)
The build script is called with the following command:
cmake -P build.cmake
cmake -DARCH=<ARCH> build.cmake
As a result, we wrote a universal wrapper that:
- Standardizes the OpenSSL build process;
- Simplifies integration with projects written in CMake;
- Implements a simple logging system.