Modern CMake configuration and construction, sample templates, dynamic link libraries, find_package, cache variables, adding pseudo targets to start programs, directory organization, updating CMake

Directory

  • Traditional CMake build installation
  • Modern CMake
  • Sample template
  • dynamic link library
  • find_package
  • Use of cache variables
  • Add pseudo target to launch program
  • Directory organization
  • Update CMake

Traditional CMake build and installation

CMake

review:
PUBLIC means that the linked library will not only be applied to the current target, but will also be passed to other targets that depend on the current target. PRIVATE means that the linked library applies only to the current target.
INTERFACE means that the linked library only applies to other targets that depend on the current target, not to the current target itself.
Table of contents:
/usr/bin System pre-installed programs
usr/local The default location for user-installed software
usr/local/src is the storage location of source code used by users when compiling software.

Modern CMake

cmake -B build -DCMAKE_INSTALL_PREFIX=/temp/test
Create a build directory and generate a build/Makefile. Set the CMAKE_INSTALL_PREFIX variable here to determine the location of make install.

-G specifies the generator
For example using Ninja:

Install Ninja build tools: sudo apt install ninja-build
Build with Ninja: cmake … -G Ninja
Generate executable file: cmake –build . –parallel 4

Add executable as build target
Method 1: Write directly
add_executable(main main.cpp)

Method 2: Create the target first, then add the source file
add_executable(main)
target_sources(main PUBLIC main.cpp other.cpp)

Method 3: Use GLOB to automatically find files with specified extensions in the current directory to add source files in batches.
It is best to enable the CONFIGURE_DEPENDS option so that the variables will be automatically updated when new files are added.
cmake_minimum_required(VERSION 3.10)
project(yx)
file(GLOB sources CONFIGURE_DEPENDS *.cpp *.h)
add_executable(main ${sources})

Method 4: Use GLOB_RECURSE to include files in all subfolders
In order to avoid adding the temporarily generated cpp in the build directory, it is recommended to put the source code in the src directory.

Method 5: aux_source_directory automatically collects the required files according to the specified directory without writing a suffix.
aux_source_directory(.sources) collects the files needed in the current directory into the sources variable

CMAKE_BUILD_TYPE controls the build type

Default is empty string, equivalent to Debug debugging mode
Other modes: Release, MinSizeRel, RelWithDebInfo. These three modes all define the NDEBUG macro, which will cause assert to be removed.
Use: set(CMAKE_BUILD_TYPE Release)
In order for most projects to default to Release mode, the following three lines will be written in CMakeLists

if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()

Usage of NDEBUG macro

Write add_definitions(-DNDEBUG) in CMakeLists, which defines the NDEBUG macro
In this way, for the following test.cpp, assert does not work and outputs xx

#include <iostream>
#include <cassert>
int main()
{<!-- -->
    int m=3;
    #ifdef NDEBUG
    std::cout<<"xx"<<std::endl;
    #else
    std::cout<<"yy"<<std::endl;
    #endif
    assert(m == 2);
    return 0;
}

Several directories
PROJECT_SOURCE_DIR: The directory where the CMakeLists of the most recent project command is called
CMAKE_CURRENT_SOURCE_DIR: The directory where the current CMakeLists is located
CMAKE_SOURCE_DIR: The directory where the outermost CMakeLists are located

The difference between PROJECT_SOURCE_DIR and CMAKE_CURRENT_SOURCE_DIR: If there is no project instruction in the current CMake, then find the current upper layer. If there is one in the upper layer, then PROJECT_SOURCE_DIR is the directory of the upper layer.

PROJECT_BINARY_DIR: The output path of the current project, where main.exe is stored
CMAKE_CURRENT_BINARY_DIR: Current output path
CMAKE_BINARY_DIR: root project output path

project's languages: default C and CXX
project(yx LANGUAGES C CXX)

Set c++ standard: Do not use target_compile_options to add -std=c++17, because -std is only an option for gcc and cannot be cross-platform.

Correct setting method: set(CMAKE_CXX_STANDARD 17)
When it is detected that the above standards are not supported, an error will be reported: set(CMAKE_CXX_STANDARD_REQUIRED ON)
Do not use GCC features to avoid errors when porting to MSVC: set(CMAKE_CXX_EXTENSIONS OFF)

Avoid the conflict between the max macro of Windows.h and std::max
Use add_definitions to add predefined macro definitions to the compiler

if(WIN32)
    add_definitions(-DNOMINMAX -D_USE_MATH_DEFINES)
endif()

Example template

Example template

cmake_minimum_required(VERSION 3.10)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
project(yx LANGUAGES C CXX)
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()
if(WIN32)
    add_definitions(-DNOMINMAX -D_USE_MATH_DEFINES)
endif()

if(NOT MSVC)
    find_program(CCACHE_PROGRAM ccache)
    if(CCACHE_PROGRAM)
        set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${<!-- -->CCACHE_PROGRAM})
        set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ${<!-- -->CCACHE_PROGRAM})
    endif()
endif()
aux_source_directory(. sources)
add_executable(main ${<!-- -->sources})

Link static library files
├── CMakeLists.txt
├── test.cpp
└── threes
├── CMakeLists.txt
├── helper.cpp
└── helper.h

Outermost CMakeLists: Use the above template, then include the subfolder threes as follows, and finally use the library
add_subdirectory(threes)
target_link_libraries(main PUBLIC alib)

CMakeLists in subfolders: automatically collect required files into variables xx, and then generate library files
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR} xx)
add_library(alib STATIC ${xx})
Default static library, it is best to use static library, because Windows is not friendly to dynamic linking

Dynamic link library

Usage of dynamic link library on Windows platform
1. Dynamic library export function
Due to function overloading in C++, name modification will be performed when exporting. To avoid this, you need to write like below.

extern "C" __declspec(dllexport) int fun(int a, int b)
{<!-- -->
return a + b;
}

There are two ways to use:
The above generated dynamic library will also generate a static library file, which is only used to tell the compiler what symbols there are.
Method 1. Configure the project properties to use the static library just generated at the same time. Before using it, write extern “C” __declspec(dllimport) int fun(int a, int b); and then copy the dynamic library dll file to the corresponding directory.
Method 2. Use the Windows API to directly load the dynamic library into memory

//How to use windows api
#include <iostream>
#include <Windows.h>

int main()
{<!-- -->
//1. Load the dynamic library into memory
HINSTANCE hDll = LoadLibrary(L"E:\VS2017\learntouse_lib\mylib.dll");
//2. Define function pointer
using functionPtr = int(*)(int, int);
//3. Find the required instance function from hDll
functionPtr addfun = (functionPtr)GetProcAddress(hDll, "fun");//The second parameter is the name of the function you want to get
auto res = addfun(2, 3);
std::cout << res << std::endl;
}

Dynamic link library export class
Method 1: (static call) use both static and dynamic libraries. When exporting and importing, write __declspec(dllexport) and __declspec(dllimport) between class and class name. This method cannot be obtained using the Windows API.

//Generate dynamic library
//head File
#pragma once
#ifndef DLL_IMPORT
#define API __declspec(dllexport)
#else
#define API __declspec(dllimport)
#endif // !DLL_IMPORT
class API A
{<!-- -->
public:
int fun(int a, int b);
};

//corresponds to cpp
#include "mylib.h"
int A::fun(int a, int b)
{<!-- -->
return a + b;
}

//use
#include <iostream>
#include "mylib.h"
#pragma comment(lib, "mylib.lib")
int main()
{<!-- -->
A a;
int res = a.fun(2, 3);
std::cout << res << std::endl;
}

Method 2:

//header file
#pragma once
#ifndef DLL_IMPORT
#define API __declspec(dllexport)
#else
#define API __declspec(dllimport)
#endif // !DLL_IMPORT
//1. Write an abstract base class and export this class when exporting.
classAPIInterfaceclass
{<!-- -->
public:
virtual int fun(int a, int b) = 0;

};
//2. The abstract base class can have multiple implementation subclasses, here is one
class A :publicInterfaceclass
{<!-- -->
public:
virtual int fun(int a, int b) override;
};
//3. Be sure to provide a method to obtain the instance
extern "C" API Interfaceclass * getInstance();
//corresponds to cpp
#include "mylib.h"

int A::fun(int a, int b)
{<!-- -->
return a + b;
}

extern "C" API Interfaceclass * getInstance()
{<!-- -->
Interfaceclass* p = new A;
return p;
}
//Use
#include <iostream>
#include "mylib.h"
#include <Windows.h>

int main()
{<!-- -->
HINSTANCE hDll = LoadLibrary(L"E:\VS2017\learntouse_lib\mylib.dll");
//Create a function pointer pointing to the base class Interfaceclass
using funcptr = Interfaceclass *(*)();//The return value of the function pointed to by the function pointer is a pointer
//Find the getInstance function from hDll
funcptr getInstancePtr = (funcptr)GetProcAddress(hDll, "getInstance");
//Use the getInstance function to obtain an instance of the abstract base class Interfaceclass
Interfaceclass* ptr = getInstancePtr();
//Ready to use
auto res = ptr->fun(2, 3);
std::cout << res << std::endl;
return 0;
}

Precompiled header file

For header files that will not change again, you can put them in the precompiled header file (xxx.pch) to precompile and convert them into binary files. In this way, when multiple other files use this header file, there is no need to compile it repeatedly. For standard library header files, such as Windows.h, we obviously will not modify it, they should be in the precompiled header file.
If it is a file that needs to be modified, do not put it in the precompiled header file, otherwise the entire precompiled header file will need to be recompiled, causing the compilation to become slower.

use:

  • On the window: write all the header files to be precompiled in an a.h, an a.cpp contains the a.h header file, then set the properties of a.cpp -C/C + ±precompiled header to create, and then set the properties of the project The precompiled header can be used a.h
  • g++: Compile g++ a.h first

How to set properties in batches in CMake

  1. Set multiple properties of the main object
    set_target_properties(main PROPERTIES
    CXX_STANDARD 17
    ...
    ...
    )
    
  2. Set global attributes, and all objects created by add_executable will share the same attributes
    set(CXX_STANDARD 17)
    

find_package

find_package
2 modes
Module mode (Default mode): Look for the FindXXX.cmake configuration file first. If you can’t find it, return to Config mode and continue searching.
Config mode: Find the XXXConfig.cmake configuration file that comes with the system
Module mode has only two search paths: CMAKE_MODULE_PATH and CMAKE_ROOT

find_package(TBB CONFIG REQUIRED)

COMPONENTS: When using some packages, such as Qt5, you need to specify which components to use.

//REQUIRED means it must be found, otherwise execution will not continue.
find_package(Qt5 COMPONENTS Widgets Gui REQUIRED)

find_package(xx) is actually looking for xxConfig.cmake or xx-config.cmake. This package configuration file contains the location of the dynamic library file, header file directory, compilation options, etc. It will be found in the usr/… directory

What to do if I can’t find it
Method 1: Set the global directory CMAKE_MODULE_PATH to the directory where XXXConfig.cmake is located

set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};C:/Qt/xxx/cmake")

Method 2: Set a separate directory XX_DIR variable to the directory where XXXConfig.cmake is located

set(Qt5_DIR C:/Qt/xxx/cmake)

Method 3: Set in the command line instead of modifying CMakeLists.txt

cmake -B build -DQt5_DIR="C:/Qt/xxx/cmake"

find_packageInstallWhat should I do if the third-party library does not provide XXXConfig.cmake

Then look for FindXXX.cmake. When the official CMake is installed, it is installed under /usr/share/cmake/Modules. If the official version does not exist, just search for FindXXX.cmake written by others on the Internet and use it. These files are placed in the cmake directory of the project.
Use: Put other people’s FindXXX.cmake in the cmake folder of your own project, and then set the directory at the top of CMakeLists.txt set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake;$ {CMAKE_MODULE_PATH}")
In this way, you can directly use find_package(XXX) to find the package.

//Example: use tbb
//1. Download a FindTBB.cmake online and place it in the cmake directory of this project
//2.Write the following in CMakeLists.txt
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake;${CMAKE_MODULE_PATH}")
find_package(TBB REQUIRED COMPONENTS tbb)
add_executable(main test.cpp)
target_link_libraries(main TBB::tbb)
//Some older libraries also need to use target_include_directories to specify the header file directory.

Use of cache variables

CMake print information

message(“Print directly”)
message(STATUS “This will cause more – symbols”)
message(WARNING “This will appear in yellow”)

Clear cache

The first time cmake -B build is executed, the compiler and C++ features are detected and the results are stored in the cache. arrive
It will not be checked the second time. Once the external compiler changes or a new package is installed, the cache needs to be cleared manually.
What files to delete: Just delete build/CmakeCache.txt

set sets cache variables
set(variable name “variable value” CACHE variable type “comment”)

#Set cache variables
set(myvar "yx" CACHE STRING "test")
#The role of FORCE: after compilation, if the value here is modified, recompiling will modify the cached value
set(myvar2 "yx2" CACHE STRING "Test" FORCE)

Type of cache variable: STRING string FILEPATH file path PATH directory path BOOL Boolean value. Note that Boolean values include ON/OFF YES/NO TRUE/FALSE.

Use cache variables to control enabling certain features

#CMakeLists.txt
#Set cache variables
set(flagon OFF CACHE BOOL "Whether to enable FLAG")
if(flagon)
    add_definitions(-DFLAG=ON)
endif(flagon)
//In cpp file
#include <iostream>
using namespace std;
int main()
{<!-- -->
    #ifdef FLAG
    cout<<"Enable FLAG"<<endl;
    #else
    cout<<"FLAG not enabled"<<endl;
    #endif
    
    return 0;
}
cmake .. -Dflagon=ON
Control the value of flagon in the command line, which leads to different compilation effects in the cpp file.

option sets a BOOL cache, which is a concise version of the above set

option(flagon "Whether to enable" OFF)

Since option and set have the same meaning in setting cache variables, there will also be a problem of caching the variables in the first compilation and directly using the cache variables next time. Therefore, there will be a phenomenon where OFF is changed to ON, but no response occurs.
Solution:
Method 1: Delete build
Method 2: Use set with FORCE instead
Method 3: Use ordinary variables directly instead of cached variables

Define macros for cpp in CMake

Method 1: add_definitions(-DFLAG=ON)
Method 2: target_compile_definitions(main PUBLIC FLAG=ON)

Operating system name${CMAKE_SYSTEM_NAME}

Determine the current system: if(WIN32) means if the current system is Win32 or Win64
APPLE means MACOS or IOS
UNIX stands for Linux, Android, IOS, MacOS, etc.

//Example
//CMakeLists.txt
...
add_executable(main test.cpp)
if(UNIX AND NOT APPLE)
    target_compile_definitions(main PUBLIC MY_NAME="Linux")
endif()

//cpp in
#include <iostream>
using namespace std;
int main()
{<!-- -->
    cout<<"System type:"<<MY_NAME<<endl;
    return 0;
}

Compiler name${CMAKE_CXX_COMPILER_ID}
GNU is gcc, NVIDIA is nvcc

Specify the compiler to use
Method 1: Command line -DCMAKE_CXX_COMPILER=”/usr/bin/clang++”
Method 2: Set the environment variable CXX=which clang’, where which clang is the shell command used to locate the path of the clang compiler.

if
Don’t add $ {} to the variables in if(). It will automatically check if there is a variable ${xx}. If not, it will be treated as an ordinary string.

In cmake, instruction names are not case-sensitive, but variable names are case-sensitive

Propagation of variables

Variables defined in the parent module will be passed to the sub-module, and those set in the sub-module will not affect the parent module.
Use the PARENT_SCOPE option of set to pass variables in the submodule to the parent module

The following demonstrates that the variable MYVAR is set in the parent module, and the submodule uses set(… PARENT_SCOPE) to successfully modify the variable value in the parent module, so that the modified value is printed.

//Parent CMakeLists.txt
set(MYVAR "Value set by parent module")
add_subdirectory(threes)
message(${<!-- -->MYVAR})
add_executable(main test.cpp)

//Sub-CMakeLists.txt
set(MYVAR "Submodule modification" PARENT_SCOPE)

Simple use of function

function(add_two_nums a b)
  message("Calculating ${a} + ${b}")
  math(EXPR result "${a} + ${b}")
  return(${<!-- -->result})
endfunction()
# Call functions
call(add_two_nums 3 5)
message("Result is ${result}")

Environment variables$ENV{xx}`

The variables set in CMakeLists.txt only take effect during the cmake configuration and build process.
So it cannot be obtained through the command line, but it can be printed out using message in CMakeLists.txt
Note: In set and if, ${} is not added to the variable.

set(ENV{<!-- -->ABC} "xxxx")
message($ENV{<!-- -->ABC})

Setting the environment variable on the command line is export ABC=hh, so that you can determine whether the environment variable is defined in cmake:

if(DEFINED ENV{<!-- -->ABC})
    message("ABC $ENV{ABC}")
else()
    message("not define")
endif()

Use if(DEFINED xx) to determine whether there is a local variable or cache variable xx. When a local variable named xx cannot be found, it will be searched in the cache. Some variables are fixed to the cache through the -D parameter.

The ccache tool is used to cache this compilation result, so that the next compilation will be faster

Instructions:
gcc/g++ in: ccache g++ test.cpp
In cmake: Write it like in the example module, add ccache to the commands in the compilation and linking phases.
msvc is not supported

Add a pseudo target to start the program

Add a pseudo target to start the main program
As follows, the pseudo target yx is defined, and the subsequent generator expression obtains the path of the main target executable file, automatically making the target yx dependent on the target main. In this way, executing cmake –build . –target yx will automatically run the generated main.exe

add_executable(main test.cpp)
add_custom_target(yx COMMAND $<TARGET_FILE:main>)

In the case of multiple commands: as follows, compiling, running, and deleting CMakeCache are implemented

add_custom_target(yx
COMMAND "pwd"
COMMAND "rm" "CMakeCache.txt"
COMMAND $<TARGET_FILE:main>
)

Directory organization

Generally, multiple subdirectories are divided, one is the directory of the executable file (call it from the command line), and the other is the directory of the library file (actual code logic).
Example

Write the version number and project in the outermost CMakeLists.txt to initialize the root project. add_subdirectory adds multiple submodules (the order does not matter)

How to write CMakeLists.txt in sub-projects

Generate the static library alib and use target_include_directories (alib PUBLIC include). PUBLIC is used here so that the root project can also find this header file. In fact, it is okay not to use target_include_directories, just so that the VS resource manager can find it and facilitate editing.

Note 1: If the function implementation is written in a header file, add static or inline to avoid repeated definitions when multiple files introduce the header file. The implementation of the class is written in the header file without adding static or inline, and there will be no conflict. It has a weak attribute, and one will be randomly selected to overwrite it.
Note 2: Use a namespace outside declarations and definitions to avoid conflicts.
Note 3: Use all <> when importing header files. This will only find header files in the directory specified by cmake and avoid introducing header files in the current directory.

If you depend on other modules but do not dereference, you can only forward declaration without importing the header file

When you want to use type A pointers in other modules in the header file, just write a forward declaration without introducing the header file. This can speed up compilation and avoid circular references. The header file is only introduced during specific implementation, creation of type A variables, use of its members, etc.

Usage of cmake script

//Use include in CMakeLists.txt to introduce the script, which is equivalent to directly copying the code in the script.
//include searches the directory in CMAKE_MODULE_PATH by default, the same as find_package
include(${<!-- -->CMAKE_SOURCE_DIR}/cmake/cm_1.cmake)

//Script file cm_1.cmake
add_executable(main test.cpp)
message("Generation successful")

Update CMake

cmake-3.27

1.uname -m learned that the current system is x86_64, so we downloaded the precompiled cmake-3.27.3-linux-x86_64.tar.gz
2. Unzip
3. Copy it to the usr/local directory and name it cmake
cp -r /home/yx/download/cmake-3.27.3-linux-x86_64 /usr/local
mv ./cmake-3.27.3-linux-x86_64/ cmake
4. Set the environment variable PATH
export PATH=/usr/local/cmake/bin:$PATH
5. Create soft links
ln -s /usr/local/cmake/bin/cmake /usr/local/bin/cmake
6. Use update-alternatives to manage CMake versions and set the new version as the highest priority (category name is cmake)
update-alternatives –install /usr/bin/cmake cmake /usr/local/bin/cmake 1 –force
7. Check whether the installation is successful
cmake –version