传统的CMake构建安装
回顾:
PUBLIC 表示链接库不仅会应用于当前目标,还会传递给依赖于当前目标的其他目标。 PRIVATE 表示链接库仅应用于当前目标。
INTERFACE 表示链接库仅适用于依赖于当前目标的其他目标,而不适用于当前目标本身。
目录:
/usr/bin 系统预装的程序
usr/local 用户安装的软件默认位置
usr/local/src 用户编译软件时所用源码的存放位置
现代CMake
cmake -B build -DCMAKE_INSTALL_PREFIX=/temp/test
创建build目录并生成build/Makefile,这里设置CMAKE_INSTALL_PREFIX变量来决定make install的位置
-G指定生成器
例如使用Ninja:
安装Ninja构建工具:sudo apt install ninja-build
使用Ninja进行构建:cmake … -G Ninja
生成可执行文件:cmake --build . --parallel 4
添加可执行文件作为构建目标
方法1:直接写
add_executable(main main.cpp)
方法2:先创建目标,再添加源文件
add_executable(main)
target_sources(main PUBLIC main.cpp other.cpp)
方法3:使用GLOB自动查找当前目录下指定扩展名的文件,实现批量添加源文件.
最好是启用CONFIGURE_DEPENDS选项,添加新文件时会自动更新变量
cmake_minimum_required(VERSION 3.10)
project(yx)
file(GLOB sources CONFIGURE_DEPENDS *.cpp *.h)
add_executable(main ${sources})
方法4:使用GLOB_RECURSE可以包含所有子文件夹下的文件
为了避免将build目录里临时生成的cpp也加进来,建议把源码都放在src目录下
方法5:aux_source_directory自动根据指定的目录搜集需要的文件,不用写后缀名了
aux_source_directory(. sources)就是把当前目录下需要的文件搜集到sources变量中
CMAKE_BUILD_TYPE控制构建类型
默认空字符串,相当于Debug调试模式
其它模式:Release、MinSizeRel、RelWithDebInfo,这3种模式都定义了NDEBUG宏,会导致assert被去掉
使用:set(CMAKE_BUILD_TYPE Release)
大多数项目为了默认为Release模式,CMakeLists中会写如下三行
if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
NDEBUG宏的使用
在CMakeLists中写上add_definitions(-DNDEBUG),即定义了NDEBUG宏
这样对于下面的test.cpp,assert不起作用,输出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;
}
几个目录
PROJECT_SOURCE_DIR:最近一次调用project命令的CMakeLists所在目录
CMAKE_CURRENT_SOURCE_DIR:当前CMakeLists所在目录
CMAKE_SOURCE_DIR:最最外层CMakeLists所在目录
PROJECT_SOURCE_DIR与CMAKE_CURRENT_SOURCE_DIR的区别:如果当前CMake里没有project指令,那么就找当前的上一层,如果上一层有,那么PROJECT_SOURCE_DIR是上一层的所在目录
PROJECT_BINARY_DIR:当前项目的输出路径,存放main.exe
CMAKE_CURRENT_BINARY_DIR:当前输出路径
CMAKE_BINARY_DIR:根项目输出路径
project的languages
:默认C和CXX
project(yx LANGUAGES C CXX)
设置c++标准
:不要使用target_compile_options来添加-std=c++17,因为-std只是gcc的选项,无法跨平台。
正确设置方法:set(CMAKE_CXX_STANDARD 17)
检测到不支持上述标准时会报错:set(CMAKE_CXX_STANDARD_REQUIRED ON)
不使用GCC特性,避免移植到MSVC上出错:set(CMAKE_CXX_EXTENSIONS OFF)
避免Windows.h的max宏与std::max冲突
使用add_definitions为编译器添加预定义的宏定义
if(WIN32)
add_definitions(-DNOMINMAX -D_USE_MATH_DEFINES)
endif()
示例模板
示例模板
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})
链接静态库文件
├── CMakeLists.txt
├── test.cpp
└── threes
├── CMakeLists.txt
├── helper.cpp
└── helper.h
最外层CMakeLists:使用上述模板,再按下面这样 包含子文件夹threes,最后使用该库
add_subdirectory(threes)
target_link_libraries(main PUBLIC alib)
子文件夹中的CMakeLists:自动搜集需要的文件到变量xx中,然后生成库文件
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR} xx)
add_library(alib STATIC ${xx})
默认静态库,最好使用静态库,因为Windows对动态链接很不友好
动态链接库
Windows平台上动态链接库的使用
1.动态库导出函数
由于c++中有函数重载,导出时会进行名称修饰。为了避免这种情况,需要像下面这样写。
extern "C" __declspec(dllexport) int fun(int a, int b)
{
return a + b;
}
使用的时候有2种方法:
上述生成动态库的同时会生成一个静态库文件,该文件只是用于告诉编译器有些什么符号。
方法1.配置项目属性使用刚刚同时生成的静态库,在使用前要写extern “C” __declspec(dllimport) int fun(int a, int b); 然后把动态库dll文件拷到相应目录下。
方法2.使用Windows的api直接将动态库加载到内存
//windows的api使用方法
#include <iostream>
#include <Windows.h>
int main()
{
//1.加载动态库到内存
HINSTANCE hDll = LoadLibrary(L"E:\\VS2017\\learntouse_lib\\mylib.dll");
//2.定义函数指针
using functionPtr = int(*)(int, int);
//3.从hDll找到所需要的实例函数
functionPtr addfun = (functionPtr)GetProcAddress(hDll, "fun");//第2个参数是想要获取到的函数的名字
auto res = addfun(2, 3);
std::cout << res << std::endl;
}
动态链接库导出类
方法1:(静态调用)同时使用静态库和动态库。导出导入时在class和类名之间写__declspec(dllexport)、__declspec(dllimport)。这种方法无法使用windows的api来获取。
//生成动态库
//头文件
#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);
};
//对应cpp
#include "mylib.h"
int A::fun(int a, int b)
{
return a + b;
}
//使用
#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;
}
方法2:
//头文件
#pragma once
#ifndef DLL_IMPORT
#define API __declspec(dllexport)
#else
#define API __declspec(dllimport)
#endif // !DLL_IMPORT
//1.写一个抽象基类,导出的时候就导出这个类
class API Interfaceclass
{
public:
virtual int fun(int a, int b) = 0;
};
//2.抽象基类可以有多个实现子类,这里是一个
class A :public Interfaceclass
{
public:
virtual int fun(int a, int b) override;
};
//3.务必要提供一个获取实例的方法
extern "C" API Interfaceclass * getInstance();
//对应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;
}
//使用
#include <iostream>
#include "mylib.h"
#include <Windows.h>
int main()
{
HINSTANCE hDll = LoadLibrary(L"E:\\VS2017\\learntouse_lib\\mylib.dll");
//创建一个指向基类Interfaceclass的函数指针
using funcptr = Interfaceclass *(*)();//该函数指针所指向的函数 的 返回值 是 指针
//从hDll找到getInstance函数
funcptr getInstancePtr = (funcptr)GetProcAddress(hDll, "getInstance");
//利用getInstance函数获取抽象基类Interfaceclass的实例
Interfaceclass* ptr = getInstancePtr();
//可以使用了
auto res = ptr->fun(2, 3);
std::cout << res << std::endl;
return 0;
}
预编译头文件
对于不会再改变的头文件,可以把它放到预编译头文件(xxx.pch)中预先编译,被转换成二进制文件。这样多个其它文件使用该头文件时就不用重复的编译它了。对于标准库头文件,例如Windows.h,我们显然不会去修改它,它们理应在预编译头文件中。
如果是需要修改的文件,就不要放进预编译头文件了,否则整个预编译头文件都需要重新编译,导致编译反而变慢。
使用:
- window上:一个a.h里面写好所有要预编译的头文件,一个a.cpp包含a.h头文件,然后设置a.cpp的属性-C/C+±预编译头为创建,再设置项目的属性里预编译头为使用a.h即可
- g++:先g++ a.h编译一下即可
CMake中怎么批量设置属性
- 设置main对象多个属性
set_target_properties(main PROPERTIES CXX_STANDARD 17 ... ... )
- 设置全局属性,此后add_executable创建的所有对象都享有同样属性
set(CXX_STANDARD 17)
find_package
find_package
2种模式
Module模式(默认模式):先找FindXXX.cmake配置文件,找不到就回退到Config模式继续找
Config模式:找系统里自带的XXXConfig.cmake配置文件
Module 模式只有两个查找路径:CMAKE_MODULE_PATH和CMAKE_ROOT
find_package(TBB CONFIG REQUIRED)
COMPONENTS
:有些包在使用时,如Qt5,需要自己指定使用哪些组件
//REQUIRED表示必须要找到,否则就不继续执行了
find_package(Qt5 COMPONENTS Widgets Gui REQUIRED)
find_package(xx)实际上是在找xxConfig.cmake或者xx-config.cmake,这个包配置文件里含有动态库文件位置、头文件目录、编译选项等。会在usr/…目录下找
找不到怎么办
方法1:设置全局的目录CMAKE_MODULE_PATH为XXXConfig.cmake所在目录
set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};C:/Qt/xxx/cmake")
方法2:设置单独的目录XX_DIR变量为XXXConfig.cmake所在目录
set(Qt5_DIR C:/Qt/xxx/cmake)
方法3:命令行里设置,而不是修改CMakeLists.txt
cmake -B build -DQt5_DIR="C:/Qt/xxx/cmake"
第3方库没有提供XXXConfig.cmake怎么办
那就找FindXXX.cmake。官方CMake安装时将其安装到了/usr/share/cmake/Modules下,如果官方也没有,就在网上直接搜索一份别人写的FindXXX.cmake使用,这些文件都是放在项目的cmake目录下。
使用:把别人的FindXXX.cmake放在自己项目的cmake文件夹下,然后在CMakeLists.txt的最上面设置目录set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake;${CMAKE_MODULE_PATH}")
这样就可以直接使用find_package(XXX)找包了
//举例:使用tbb
//1.网上下载一个FindTBB.cmake放在本项目的cmake目录下
//2.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)
//有的比较旧的库,还需要用target_include_directories指定头文件目录
缓存变量的使用
CMake打印信息
message(“直接打印”)
message(STATUS “这样会多–符号”)
message(WARNING “这样会显示为黄色”)
清除缓存
第一次执行cmake -B build时会检测编译器和C++特性,结果存储到缓存中。到
了第二次就不检查了,一旦外部编译器变化或者安装了新的包,就需要手动清除缓存。
删什么文件
:删build/CmakeCache.txt即可
set设置缓存变量
set(变量名 “变量值” CACHE 变量类型 “注释”)
#设置缓存变量
set(myvar "yx" CACHE STRING "测试")
#带FORCE的作用:当编译过后,如果修改了这里的值,重新编译会把缓存好的值修改掉
set(myvar2 "yx2" CACHE STRING "测试" FORCE)
缓存变量的类型:STRING字符串 FILEPATH文件路径 PATH目录路径 BOOL布尔值。注意,布尔值有ON/OFF YES/NO TRUE/FALSE几种
利用缓存变量来控制启用某些特性
#CMakeLists.txt中
#设置缓存变量
set(flagon OFF CACHE BOOL "是否启用FLAG")
if(flagon)
add_definitions(-DFLAG=ON)
endif(flagon)
//cpp文件中
#include <iostream>
using namespace std;
int main()
{
#ifdef FLAG
cout<<"启用FLAG"<<endl;
#else
cout<<"未启用FLAG"<<endl;
#endif
return 0;
}
cmake .. -Dflagon=ON
命令行里控制flagon的值,进而导致cpp文件中的不同编译效果
option设置BOOL型缓存,是上述set的简洁版
option(flagon "是否启用" OFF)
由于option和set设置缓存变量是一个意思,也会存在 第一次编译把变量缓存起来,下一次直接使用缓存变量 的问题,因此,会出现修改了OFF为ON,但没反应的现象。
解决方法:
法1:删build
法2:改用带FORCE的set
法3:直接使用普通变量,而不是缓存变量
在CMake中给cpp定义宏
法1:add_definitions(-DFLAG=ON)
法2:target_compile_definitions(main PUBLIC FLAG=ON)
操作系统名字
${CMAKE_SYSTEM_NAME}
判断当前系统:if(WIN32) 表示如果当前系统为Win32或Win64
APPLE表示MACOS或IOS
UNIX表示Linux、Android、IOS、MacOS等
//示例
//CMakeLists.txt中
...
add_executable(main test.cpp)
if(UNIX AND NOT APPLE)
target_compile_definitions(main PUBLIC MY_NAME="Linux")
endif()
//cpp中
#include <iostream>
using namespace std;
int main()
{
cout<<"系统类型:"<<MY_NAME<<endl;
return 0;
}
编译器名字
${CMAKE_CXX_COMPILER_ID}
GNU就是gcc、NVIDIA就是nvcc
指定要使用的编译器
法1:命令行-DCMAKE_CXX_COMPILER=“/usr/bin/clang++”
法2:设置环境变量CXX=‘which clang’ ,其中which clang是shell命令,用于定位clang编译器的路径。
if
if()里的变量不要加 $ {},它会自动先看看是不是有${xx}这个变量,没有就当作普通字符串
cmake中,指令名不分大小写,但变量名要分大小写
变量的传播
父模块中定义的变量 会 传递给子模块,子模块中设置的不会影响到父模块
使用set的PARENT_SCOPE选项可以让 子模块中变量向父模块传递
下面演示父模块中设置了变量MYVAR,子模块使用set(… PARENT_SCOPE)成功修改了父模块中的变量值,使得打印出的是修改后的值。
//父CMakeLists.txt
set(MYVAR "父模块设置的值")
add_subdirectory(threes)
message(${MYVAR})
add_executable(main test.cpp)
//子CMakeLists.txt
set(MYVAR "子模块修改" PARENT_SCOPE)
function的简单使用
function(add_two_nums a b)
message("Calculating ${a} + ${b}")
math(EXPR result "${a} + ${b}")
return(${result})
endfunction()
# 调用函数
call(add_two_nums 3 5)
message("Result is ${result}")
环境变量
$ENV{xx}`
在CMakeLists.txt中set设置的变量,只在cmake配置和构建过程中生效
因此无法通过命令行获取,但可以在CMakeLists.txt中使用message打印出来
注意:在set、if中,变量是不加${}的
set(ENV{ABC} "xxxx")
message($ENV{ABC})
在命令行中设置环境变量是export ABC=hh,这样可以在cmake里判断是否定义了该环境变量:
if(DEFINED ENV{ABC})
message("ABC $ENV{ABC}")
else()
message("not define")
endif()
使用if(DEFINED xx)可以判断是否存在局部变量或缓存变量xx。当找不到名为xx的局部变量时,会去缓存里找,有的变量是通过-D参数固定到缓存里的。
ccache工具用于缓存本次的编译结果,下次编译就快了
使用方法:
gcc/g++中:ccache g++ test.cpp
cmake中:示例模块中那样写,给编译和链接阶段的命令都加上ccache
不支持msvc
添加伪目标来启动程序
添加伪目标,用于启动主程序
如下,定义伪目标yx,后续生成器表达式获取到main目标可执行文件的路径,自动让目标yx依赖于目标main。这样,执行cmake --build . --target yx就会自动运行生成的main.exe
add_executable(main test.cpp)
add_custom_target(yx COMMAND $<TARGET_FILE:main>)
多个命令的情况:如下,实现了编译、运行、删除CMakeCache
add_custom_target(yx
COMMAND "pwd"
COMMAND "rm" "CMakeCache.txt"
COMMAND $<TARGET_FILE:main>
)
目录组织方式
一般会划分多个子目录,如下一个是可执行文件的目录(命令行调用它),一个是库文件的目录(实际的代码逻辑)。
在最外面的CMakeLists.txt中写版本号、project初始化根项目。add_subdirectory将里面分多个子模块添加进来(顺序无关紧要)
子项目中的CMakeLists.txt写法
生成静态库alib,并使用target_include_directories(alib PUBLIC include),这里使用PUBLIC,使得根项目也能找到这个头文件。其实不使用target_include_directories也可以,只是为了VS资源管理器能找到,方便编辑。
注意1
:如果把函数实现写在头文件里,要加static或者inline,避免多个文件引入该头文件时导致重复定义。而类的实现写在头文件不用加static或inline,不会冲突,它具有weak属性,会随机选一个覆盖掉。
注意2
:在声明和定义外都套一层命名空间,避免冲突
注意3
:导入头文件时全部使用<>, 这样是只找cmake指定目录下的头文件,避免引入当前目录下的头文件
依赖其它模块但不解引用,则可以只前向声明,不导入头文件
在头文件中想使用其它模块中的A类型指针时,写个前向声明即可,不需要引入头文件,这样可以加快编译速度,避免循环引用。在具体实现、创建A类型变量、使用其成员等情况时才引入头文件。
cmake脚本的使用
//CMakeLists.txt中使用include即可引入脚本,等价于脚本中的代码直接复制过来
//include默认搜索的也是CMAKE_MODULE_PATH处目录,和find_package一样
include(${CMAKE_SOURCE_DIR}/cmake/cm_1.cmake)
//脚本文件 cm_1.cmake中
add_executable(main test.cpp)
message("生成成功")
更新CMake
1.uname -m得知当前系统为x86_64,因此下载了预编译好的cmake-3.27.3-linux-x86_64.tar.gz
2.解压
3.将其拷贝到usr/local目录下并命名为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.设置环境变量PATH
export PATH=/usr/local/cmake/bin:$PATH
5.创建软链接
ln -s /usr/local/cmake/bin/cmake /usr/local/bin/cmake
6.使用update-alternatives管理CMake版本,将新版本设置为最高优先级(类别名为cmake)
update-alternatives --install /usr/bin/cmake cmake /usr/local/bin/cmake 1 --force
7.查看是否安装成功
cmake --version