This article discusses the use of the CMake build system used in a huge number of C / C ++ projects. It is strongly recommended to read the first part of the manual in order to avoid misunderstanding the syntax of the CMake language, which appears explicitly throughout the article.
Below are examples of using the CMake language that you should practice using. Experiment with the source code, changing existing commands and adding new ones. To run these examples, install CMake from the official site .
The CMake build system is a shell over other platform-dependent utilities (for example, Ninja or Make ). Thus, in the assembly process itself, however paradoxical it may sound, it does not directly participate.
The CMake build system accepts a CMakeLists.txt
file with a description of the build rules in the formal CMake language, and then generates intermediate and native build files in the same directory adopted on your platform.
The generated files will contain specific names of system utilities, directories and compilers, while CMake commands operate only with an abstract concept of a compiler and are not tied to platform-dependent tools, which differ greatly on different operating systems.
The cmake_minimum_required
command checks a running version of CMake: if it is less than the specified minimum, then CMake completes its work with a fatal error. An example demonstrating typical use of this command at the beginning of any CMake file:
# CMake: cmake_minimum_required(VERSION 3.0)
As noted in the comments, the cmake_minimum_required
command sets all compatibility flags (see cmake_policy
). Some developers deliberately set a low version of CMake, and then adjust the functionality manually. This allows you to simultaneously support the ancient version of CMake and sometimes use new features.
At the beginning of any CMakeLists.txt
should set the project characteristics with the project command to better design the integrated environments and other development tools.
# "MyProject": project(MyProject VERSION 1.2.3.4 LANGUAGES C CXX)
It should be noted that if the LANGUAGES
keyword is omitted, then C CXX
languages are set by default. You can also disable the indication of any languages by writing the keyword NONE
as a list of languages, or simply leave an empty list.
The include
command replaces the line of its call with the code of the specified file, acting in the same way as the preprocessor include
command of C / C ++ languages. This example runs the script file MyCMakeScript.cmake
command described:
message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]") # `MyCMakeScript.cmake` : include(MyCMakeScript.cmake) message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]")
In this example, the first message will notify that the TEST_VARIABLE
variable TEST_VARIABLE
not yet been defined, however, if the MyCMakeScript.cmake
script determines this variable, the second message will already inform about the new value of the test variable. Thus, the script file included by the include
command does not create its own scope, as mentioned in the comments to the previous article .
The add_executable
command compiles an executable file with the specified name from the source list. It is important to note that the final file name depends on the target platform (for example, <ExecutableName>.exe
or simply <ExecutableName>
). A typical example of calling this command is:
# "MyExecutable" # "ObjectHandler.c", "TimeManager.c" "MessageGenerator.c": add_executable(MyExecutable ObjectHandler.c TimeManager.c MessageGenerator.c)
The add_library
command compiles a library with the specified view and a name from source. It is important to note that the final library name depends on the target platform (for example, lib<LibraryName>.a
or <LibraryName>.lib
). A typical example of calling this command is:
# "MyLibrary" # "ObjectHandler.c", "TimeManager.c" "MessageConsumer.c": add_library(MyLibrary STATIC ObjectHandler.c TimeManager.c MessageConsumer.c)
STATIC
keyword as the second argument and are archives of object files linked to executable files and other libraries at compile time;SHARED
keyword as the second argument and are binary libraries that are loaded by the operating system during program execution;MODULE
keyword by the second argument and are binary libraries loaded by execution techniques by the executable file itself;OBJECT
keyword as the second argument and are a set of object files that are associated with executable files and other libraries during compilation.There are cases that require adding source files to the target. For this purpose, a target_sources
command is target_sources
that can add source to the target multiple times.
The first argument is the target_sources
command accepts the name of the target previously specified using the add_library
or add_executable
, and the following arguments are the list of source files to be added.
Repeated calls to the target_sources
add source files to the target in the order in which they were called, so the bottom two blocks of code are functionally equivalent:
# "MyExecutable" # "ObjectPrinter.c" "SystemEvaluator.c": add_executable(MyExecutable ObjectPrinter.c SystemEvaluator.c) # "MyExecutable" "MessageConsumer.c": target_sources(MyExecutable MessageConsumer.c) # "MyExecutable" "ResultHandler.c": target_sources(MyExecutable ResultHandler.c)
# "MyExecutable" # "ObjectPrinter.c", "SystemEvaluator.c", "MessageConsumer.c" "ResultHandler.c": add_executable(MyExecutable ObjectPrinter.c SystemEvaluator.c MessageConsumer.c ResultHandler.c)
The location of the output files generated by the add_executable
and add_library
is determined only at the generation stage, but this rule can be changed by several variables defining the final location of the binary files:
RUNTIME_OUTPUT_DIRECTORY
and RUNTIME_OUTPUT_NAME
determine the location of the execution targets;LIBRARY_OUTPUT_DIRECTORY
and LIBRARY_OUTPUT_NAME
determine the location of the libraries;ARCHIVE_OUTPUT_DIRECTORY
and ARCHIVE_OUTPUT_NAME
define the location of the archives.Executable files are always viewed as execution targets, static libraries as archive targets, and modular libraries as library targets. For non-DLL platforms, dynamic libraries are considered library targets, and for DLL platforms, execution targets. For object libraries such variables are not provided, since this kind of libraries is generated in the depths of the CMakeFiles
catalog.
It is important to note that all platforms based on Windows, including Cygwin, are considered "DLL-platforms".
The target_link_libraries
command target_link_libraries
library or executable file with other libraries provided. add_executable
first argument, this command takes the name of the target generated using the add_executable
or add_library
, and the following arguments are the names of the library targets or full paths to the libraries. Example:
# "MyExecutable" # "JsonParser", "SocketFactory" "BrowserInvoker": target_link_libraries(MyExecutable JsonParser SocketFactory BrowserInvoker)
It should be noted that modular libraries cannot be linked with executable files or other libraries, since they are intended solely for downloading by technicians.
As mentioned in the comments, CMake targets are also subject to manual manipulation, but very limited.
It is possible to control the properties of the objectives, intended to set the project assembly process. The get_target_property
command assigns a target property value to the variable provided. This example displays the value of the C_STANDARD
property of the C_STANDARD
target on the screen:
# "VALUE" "C_STANDARD": get_target_property(VALUE MyTarget C_STANDARD) # : message("'C_STANDARD' property is equal to [${VALUE}]")
The set_target_properties
command sets the specified target properties with the specified values. This command accepts a list of targets for which property values will be set, and then the PROPERTIES
keyword, followed by a list of the form < > < >
:
# 'C_STANDARD' "11", # 'C_STANDARD_REQUIRED' "ON": set_target_properties(MyTarget PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON)
The example above has set MyTarget
properties that affect the compilation process, namely: when compiling the goal, MyTarget
CMake will MyTarget
compiler to use the C11 standard. All known target property names are listed on this page .
It is also possible to check previously defined targets using the if(TARGET <TargetName>)
construct if(TARGET <TargetName>)
:
# "The target was defined!" "MyTarget" , # "The target was not defined!": if(TARGET MyTarget) message("The target was defined!") else() message("The target was not defined!") endif()
The add_subdirectory
command prompts CMake to immediately process the specified subproject file. The example below demonstrates the use of the described mechanism:
# "subLibrary" , # "subLibrary/build": add_subdirectory(subLibrary subLibrary/build)
In this example, the first argument of the add_subdirectory
command is the add_subdirectory
subproject, and the second argument is optional and informs CMake about the folder intended for the generated files of the included subproject (for example, CMakeCache.txt
and cmake_install.cmake
).
It is worth noting that all variables from the parent scope are inherited by the added directory, and all variables defined and redefined in this directory will be visible only to it (unless the keyword PARENT_SCOPE
was specified by the argument of the set
command). This feature was mentioned in the comments to the previous article .
The find_package
command finds and loads external project settings. In most cases, it is used for the subsequent linking of external libraries, such as Boost and GSL . This example calls the described command to search for the GSL library and then link it:
# "GSL": find_package(GSL 2.5 REQUIRED) # "GSL": target_link_libraries(MyExecutable GSL::gsl) # "GSL": target_include_directories(MyExecutable ${GSL_INCLUDE_DIRS})
In the example above, the find_package
command first takes the name of the package, and then the required version. The REQUIRED
option requires printing a fatal error and CMake shutdown if the required package is not found. The opposite is the QUIET
option, requiring CMake to continue its work, even if the package was not found.
Next, the MyExecutable
executable file MyExecutable
linked with the GSL library by the target_link_libraries
command using the GSL::gsl
variable encapsulating the location of the already compiled GSL.
At the end, the target_include_directories
command is target_include_directories
, informing the compiler about the location of the GSL header files. Notice that the GSL_INCLUDE_DIRS
variable is GSL_INCLUDE_DIRS
, which stores the location of the headers I described (this is an example of the imported package settings).
You will probably want to check the result of the package search if you specified the QUIET
option. This can be done by checking the <PackageName>_FOUND
, which is automatically detected after the find_package
command find_package
. For example, if the GSL settings are successfully imported into your project, the GSL_FOUND
variable will turn into true.
In general, the find_package
command has two types of launch: modular and configuration. The example above applied a modular form. This means that during a call, CMake searches for a script file of the type Find<PackageName>.cmake
in the CMAKE_MODULE_PATH
directory, and then launches it and imports all the necessary settings (in this case, CMake launched the standard FindGSL.cmake
file).
The compiler can be informed about the placement of included headers using two commands: include_directories
and target_include_directories
. You decide which one to use, but some differences between them are worth considering (the idea is suggested in the comments ).
The include_directories
command affects the directory area. This means that all the directories directories specified by this command will be used for all the purposes of the current CMakeLists.txt
, as well as for the subprojects being processed (see add_subdirectory
).
The target_include_directories
command target_include_directories
affects the target specified by the first argument, but no other effect is exerted on other targets. The example below demonstrates the difference between these two commands:
add_executable(RequestGenerator RequestGenerator.c) add_executable(ResponseGenerator ResponseGenerator.c) # "RequestGenerator": target_include_directories(RequestGenerator headers/specific) # "RequestGenerator" "ResponseGenerator": include_directories(headers)
In the comments it is mentioned that in modern projects the use of the include_directories
and link_libraries
is undesirable. The alternative is the target_include_directories
and target_link_libraries
that act only on specific targets, and not on the entire current scope.
The install
command generates installation rules for your project. This command is able to work with goals, files, folders and much more. First, consider setting goals.
To set goals, the first argument of the described function is to pass the keyword TARGETS
, followed by a list of targets to be set, and then the keyword DESTINATION
with the location of the directory into which the specified goals will be installed. This example demonstrates typical goal setting:
# "TimePrinter" "DataScanner" "bin": install(TARGETS TimePrinter DataScanner DESTINATION bin)
The process of describing the installation of files is similar, except for the fact that instead of the keyword TARGETS
should indicate FILES
. An example demonstrating the installation of files:
# "DataCache.txt" "MessageLog.txt" "~/": install(FILES DataCache.txt MessageLog.txt DESTINATION ~/)
The process of describing the installation of folders is similar, with the exception that instead of the keyword FILES
should specify DIRECTORY
. It is important to note that the installation will copy the entire contents of the folder, and not just its name. An example of installing folders is as follows:
# "MessageCollection" "CoreFiles" "~/": install(DIRECTORY MessageCollection CoreFiles DESTINATION ~/)
After CMake has completed processing all your files, you can install all the described objects with the sudo checkinstall
(if CMake generates a Makefile
), or perform this action with an integrated development environment that supports CMake.
This manual would be incomplete without showing a real example of using the CMake build system. Consider a simple project diagram using CMake as the only build system:
+ MyProject - CMakeLists.txt - Defines.h - StartProgram.c + core - CMakeLists.txt - Core.h - ProcessInvoker.c - SystemManager.c
The main assembly file CMakeLists.txt
describes the compilation of the entire program: first, the add_executable
that compiles the executable is invoked, then the add_subdirectory
command is add_subdirectory
, prompting the subproject processing, and finally, the executable is linked with the compiled library:
# CMake: cmake_minimum_required(VERSION 3.0) # : project(MyProgram VERSION 1.0.0 LANGUAGES C) # "MyProgram": add_executable(MyProgram StartProgram.c) # "core/CMakeFiles.txt": add_subdirectory(core) # "MyProgram" # "MyProgramCore": target_link_libraries(MyProgram MyProgramCore) # "MyProgram" "bin": install(TARGETS MyProgram DESTINATION bin)
The core/CMakeLists.txt
file is called by the main assembly file and compiles the static MyProgramCore
library, which is intended to link to the executable file:
# CMake: cmake_minimum_required(VERSION 3.0) # "MyProgramCore": add_library(MyProgramCore STATIC ProcessInvoker.c SystemManager.c)
After a series of commands cmake . && make && sudo checkinstall
cmake . && make && sudo checkinstall
CMake build system completes successfully. The first command starts processing the CMakeLists.txt
file in the project root directory, the second command finally compiles the necessary binaries, and the third command installs the assembled MyProgram
executable file into the system.
Now you are able to write your own and understand other people's CMake files, and you can read about other mechanisms in detail on the official website .
The next article in this guide will be devoted to testing and packaging using CMake and will be released in a week. See you soon!
Source: https://habr.com/ru/post/432096/
All Articles