现代CMake的配置与构建、示例模板、动态链接库、find_package、缓存变量、添加伪目标来启动程序、目录组织方式、更新CMake

传统的CMake构建安装

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中怎么批量设置属性

  1. 设置main对象多个属性
    set_target_properties(main PROPERTIES
    	CXX_STANDARD 17
    	...
    	...
    )
    
  2. 设置全局属性,此后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"

find_package安装第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

cmake-3.27

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个简单的示例,演示如何使用 CMake、find_package、install 和 CPack 工具来构建、打包和安装一个简单的 C++ 应用程序。 假设我们有一个名为 "hello" 的 C++ 应用程序,它依赖于 Boost 库和 OpenSSL 库。我们的目标是在 Linux 系统上构建、打包和安装这个应用程序。 1. 安装 Boost 和 OpenSSL 库: ``` sudo apt-get install libboost-all-dev libssl-dev ``` 2. 创建一个名为 CMakeLists.txt 的文件,描述构建过程: ``` cmake_minimum_required(VERSION 3.10) project(hello) # 查找 Boost 库 find_package(Boost REQUIRED COMPONENTS system) # 查找 OpenSSL 库 find_package(OpenSSL REQUIRED) # 添加可执行文件 add_executable(hello main.cpp) # 链接 Boost 和 OpenSSL 库 target_link_libraries(hello Boost::system OpenSSL::SSL OpenSSL::Crypto) # 安装可执行文件到 /usr/local/bin 目录下 install(TARGETS hello DESTINATION /usr/local/bin) # 使用 CPack 工具生成 DEB 软件包 set(CPACK_GENERATOR "DEB") set(CPACK_PACKAGE_NAME "hello") set(CPACK_PACKAGE_VERSION "1.0") set(CPACK_PACKAGE_DESCRIPTION "A simple hello world program") include(CPack) ``` 3. 创建一个名为 main.cpp 的文件,作为应用程序的源代码: ``` #include <iostream> #include <boost/asio.hpp> #include <openssl/ssl.h> int main() { std::cout << "Hello, world!" << std::endl; std::cout << "Boost version: " << BOOST_LIB_VERSION << std::endl; std::cout << "OpenSSL version: " << SSLeay_version(SSLEAY_VERSION) << std::endl; return 0; } ``` 4. 在项目根目录下创建一个名为 build 的文件夹,并进入该文件夹: ``` mkdir build cd build ``` 5. 运行 CMake 命令来生成构建文件: ``` cmake .. ``` 6. 运行 make 命令来构建应用程序: ``` make ``` 7. 运行 make install 命令来安装应用程序: ``` sudo make install ``` 8. 运行 CPack 命令来生成软件包: ``` cpack ``` 9. 在 build 目录下可以找到生成的软件包,例如 hello-1.0-Linux.deb。 这样,我们就成功地使用 CMake、find_package、install 和 CPack 工具来构建、打包和安装一个简单的 C++ 应用程序

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值