CMake is a build system for C / C ++, which is becoming more popular every year. It almost became the default solution for new projects. However, many examples of performing a CMake task contain archaic, unreliable, bloated actions. We will find out how to write build scripts on CMake more concisely.
If you want to try out the tips in action, take an example on github and examine it as you read the article: https://github.com/sergey-shambir/modern-cmake-sample
The advice does not apply to those who write public libraries, because compatibility with the old development environment is important to them. And if you are writing a project with a closed code or a highly specialized open source software, then you can ask all developers to install the latest version of CMake. Without this, many article tips will not work! At the time of this writing, we have CMake 3.8.
cmake_minimum_required(VERSION 3.8 FATAL_ERROR)
Modern CMake can call the build system itself. In CMake, this is called the Build Tool Mode.
# myproj myproj-build mkdir ../myproj-build && cd ../myproj-build # ../myproj cmake -DCMAKE_BUILD_TYPE=Release ../myproj # cmake --build . # , '-j4' . cmake --build . -- -j4
If you generate a Visual Studio project, you can also build it from the command line, including building a specific project in a specific configuration:
cmake --build . \ --target myapp \ --config Release \ --clean-first
On Linux, do not use make install, otherwise you will clog your system. There is a separate article about this. I want to take and shoot, or an educational program about why you should not use make install
CMakeLists.txt nesting is normal. If your project is divided into 3 libraries, 3 test suites and 2 applications, then why not add CMakeLists.txt
for each of them? Then you need to create another central CMakeLists.txt
, and execute add_subdirectory in it. So the central CMakeLists might look like:
cmake_minimum_required(VERSION 3.8 FATAL_ERROR) project(opengl-samples) # : CMakeLists # , add_subdirectory include(scripts/functions.cmake) add_subdirectory(libs/libmath) add_subdirectory(libs/libplatform) add_subdirectory(libs/libshade) # enable_testing BUILD_TESTING, # BUILD_TESTING=ON. # `cmake -DBUILD_TESTING=OFF projectdir` , # . enable_testing() if(BUILD_TESTING) add_subdirectory(tests) endif() # .. ..
Do not start global variables unless absolutely necessary. Do not use link_directories()
, include_directories()
, add_definitions()
, add_compile_options()
and other similar instructions.
# - add_library(mylibrary \ ColorDialog.h ColorDialog.cpp \ ColorPanel.h ColorPanel.cpp) # ! - ! # /usr/include/wx-3.0 # find_package . target_include_directories(mylibrary /usr/include/wx-3.0)
It is worth noting thattarget_link_libraries
can add library header search paths if the library is in your project and header search paths have been attached to it through thetarget_include_directories(libfoo PUBLIC ...)
construct.
There is an example of a dependency diagram taken from the Modern CMake / an Introduction presentation for Tobias Becker:
In recent years, the C ++ standard has been updated frequently: we have received tremendous changes in C ++ 11, C ++ 14, C ++ 17. Try to abandon older compilers whenever possible. For example, for Linux, nothing prevents you from installing the latest version of Clang and libc ++ and starting to build all projects with a static C ++ runtime layout.
The best way to enable C ++ 17 without playing with compilation flags is to tell CMake that you need it.
# : cxx_std_17 target_compile_features(${TARGET} PUBLIC cxx_std_17) # : set_target_properties(${TARGET} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS NO )
With the help target_compile_features
you can demand not C ++ 17 or C ++ 14, but certain features from the compiler. A complete list of well-known CMake compiler features can be found in the documentation .
In CMake, you can declare your own functional macros and your own functions. There is only one difference between them: the variables set inside the function are local.
It is convenient to write functions to solve current problems of customization of the assembly, or to simplify the addition of a set of assembly goals. The example below was written for more correct inclusion of C ++ 17 due to the fact that
/std:c++latest
flag to Visual Studio to enable C ++ 17std::experimental::filesystem
in Clang / libc ++, you need to indicate to the linker that you need to link the project with libc++experimental.a
, because libc++.a
does not have the filesystem module yet; also need to link with pthread, since the implementation of thread / mutex, etc. based on pthread # CMake C++17 . # . function(custom_enable_cxx17 TARGET) # C++17 , CMake . target_compile_features(${TARGET} PUBLIC cxx_std_17) # C++latest Visual Studio if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") set_target_properties(${TARGET} PROPERTIES COMPILE_FLAGS "/std:c++latest") # libc++, libc++experimental pthread Clang elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang") set_target_properties(${TARGET} PROPERTIES COMPILE_FLAGS "-stdlib=libc++ -pthread") target_link_libraries(${TARGET} c++experimental pthread) endif() endfunction(custom_enable_cxx17)
Each function is essentially a hack created to redefine CMake or its behavior. For other developers, the meaning of this hack is unclear. Therefore, try to add a comment to each instruction in the function explaining its purpose and meaning.
In large open source projects, such as KDE, the use of its functions can be a bad taste. You may consider other options: write the build script explicitly according to the principle "Explicit is better then implicit", or even suggest adding your function to the upstream of the CMake project.
My colleague is developing a small 3D rendering engine for rendering scenes with models and animations via OpenGL, GLES, DirectX and Vulkan. Once we discussed this project with him, and it turned out that he uses Visual Studio to build for all platforms (Windows, Linux, Android)! He is unhappy that Microsoft rarely updates the Android NDK, but does not want to refuse to build via MSBuild for one simple reason.
He does not want to accompany the list of files for assembly in two assembly systems.
Once I was porting a game from iOS to Android, and we supported two build systems using a script that read the XCode project and automatically added to the list of files in Android.mk
. If you use CMake, then you do not even need to write a script.
CMake has the aux_source_directory
function, but it has a flaw: headers are not added to the list and do not appear in any generated project for the IDE.
file(GLOB ...)
comes to the rescue, scanning files by maskcustom_add_executable_from_dir(name)
CMAKE_CURRENT_SOURCE_DIR
function(custom_add_executable_from_dir TARGET) # file(GLOB TARGET_SRC "CMAKE_CURRENT_SOURCE_DIR/*.cpp" # add_executable(${TARGET} ${TARGET_SRC}) endfunction()
You can add the function custom_add_library_from_dir
for library purposes in the same way.
If you are a fan of manual work or create a public library, then maybe you should add files one by one. In this case, use target_sources
to add platform-specific files:
add_library(libfoo Foo.h Foo_common.cpp) if(WIN32) target_sources(libfoo Foo_win32.cpp) endif(WIN32)
Surely, for the sake of automation, you wanted to call the Bash command from cmake to create a directory, unpack the archive or calculate the md5 amount. But invoking command line utilities can deprive the project of cross-platform. A more portable method is to invoke the cmake -E
using the Command-Line Tool Mode .
Advice applies to you if you are writing publicly available libraries. In this case, you should simplify the following scenarios:
CMakeLists.txt
via add_subdirectory
When adding a library, create another unique synonym:
# - add_library(foo ${FOO_SRC}) # , add_library(MyOrg::foo ALIAS foo)
Leave library users the right to use the BUILD_SHARED_LIBS
option to choose between BUILD_SHARED_LIBS
static and dynamic library versions.
When setting up the layout, search headers and compilation flags for libraries, use the keywords PUBLIC, PRIVATE, INTERFACE to allow goals, depending on your library, to inherit the necessary settings:
target_link_libraries(foobarapp PUBLIC MyOrg::libfoo PRIVATE MyOrg::libbar )
The CTest subsystem does not force you to use any special libraries for testing instead of the usual Boost.Test, Catch or Google Tests. It only registers autotests so that CMake can run all tests or selected tests with one ctest
command.
To enable CTest support throughout the project, there is an enable_testing
instruction
# enable_testing BUILD_TESTING, # BUILD_TESTING=ON. # `cmake -DBUILD_TESTING=OFF projectdir` , # . enable_testing() if(BUILD_TESTING) add_subdirectory(tests/libhellotest) add_subdirectory(tests/libgoodbyetest) endif()
For the executable file with the test to be registered in CTest, you need to call the instruction add_test
.
# - add_executable(${TARGET} ${TARGET_SRC}) # CMake . # , . add_test(${TARGET} ${TARGET})
If you set up CMake and CTest for your OpenSource project, then you can connect continuous integration: Continuous Integration (CI) for GitHub C / C ++ projects with CMake build
Before creating the article, several English-language sources were read, tried and rethought:
Some tips from these sources are not reflected in the article. Therefore, after reading them, you will definitely become deeper into CMake.
Source: https://habr.com/ru/post/330902/