📜 ⬆️ ⬇️

The complete guide to CMake. Part Two: Build System


Introduction


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.


Launch CMake


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 .


Principle of operation


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.


CMake version check


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.


Design of the project


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.


Running Script Files


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 .


Compiling executable files


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) 

Library compilation


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) 


Add source to target


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) 

Generated files


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:



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


Library layout


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.


Work with goals


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

Adding Subprojects


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 .


Search for packages


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


Ways to include titles


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.


Project Installation


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.


Illustrative example of the project


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.


Conclusion


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