⬆️ ⬇️

Modern CMake: 10 tips for improving build scripts

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


Tip # 1: Specify a high minimum version of CMake



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) 


Advice â„–2: do not call neither make, nor make install



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


Tip number 3: use several CMakeLists.txt



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() # .. .. 


Advice â„–4: do not litter the global scope



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 that target_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 the target_include_directories(libfoo PUBLIC ...) construct.

There is an example of a dependency diagram taken from the Modern CMake / an Introduction presentation for Tobias Becker:



Scheme



Tip # 5: finally turn on C ++ 17 or C ++ 14!



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 .



Tip # 6: Use Functions



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





 #    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.


Tip number 7 (controversial): do not list the source files one by one



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.





 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) 


Tip # 8: Don't run bash utilities, run cmake -E



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 .



Tip # 9: Leave more flexibility to users of your libraries.



Advice applies to you if you are writing publicly available libraries. In this case, you should simplify the following scenarios:





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 ) 


Tip number 10: register autotests in CTest



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}) 


What else to read?



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/



All Articles