首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
80 阅读
2
nvim番外之将配置的插件管理器更新为lazy
58 阅读
3
2018总结与2019规划
54 阅读
4
PDF标准详解(五)——图形状态
33 阅读
5
为 MariaDB 配置远程访问权限
30 阅读
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
Java
emacs
linux
文本编辑器
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
308
篇文章
累计收到
27
条评论
首页
栏目
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
页面
归档
友情链接
关于
搜索到
2
篇与
的结果
2025-04-12
使用CMake跨平台的一些经验
使用CMake构建工程的一个原因是不希望Windows上有一个工程文件,Linux又单独配置一套工程文件。我们希望对于整个工程只配置一便,就可以直接编译运行。CMake本身也具备这样的特性,脚本只用写一次,在其他平台能自动构建工程。这里谈的跨平台主要是如何组织工程和编写CMake来针对Windows和Linux进行一些特殊的处理,在这里说说我常用的几种办法介绍这些方法的前提是有一些代码只能在Windows上跑,例如调用Win32 API或者使用GDI/GDI+,而在Linux上也有一些Windows不支持的代码。我们就需要一些方法来隔离开这两套代码。假设现在有一个项目,它有一套共同的接口对外提供功能,而在Windows上和Linux上各有一份代码来实现这些接口。可以假设有一套图形相关的功能,对外采用统一的接口,具体实现时Windows上使用GDI+,而Linux上使用其他方案来实现。现在整个项目的目录结构如下. ├── include └── platform ├── linux └── windowsinclude 目录用来对外提供接口,是导出的头文件。platform隔离了Windows和Linux上的实现代码。使用宏来控制我们知道Windows和Linux平台有特定编译器定义的宏,根据这些宏是否定义我们可以知道当前是在Linux还是Windows上编译,是需要编译成32位或者64位程序,又或者编译成debug版本或者release版本。例如下面是我用的简单的判断版本的方式#define MY_PLATFORM_WINDOWS 0 #define MY_PLATFORM_LINUX 1 #define MY_PLATFORM_APPLE 2 #define MY_PLATFORM_ANDROID 3 #define MY_PLATFORM_UNIX 4 #define MY_ARCH32 1 #define MY_ARCH64 2 #if defined(_WIN32) || defined(_WIN64) #define MY_PLATFORM MY_PLATFORM_WINDOWS #ifdef _WIN64 #define PLATFORM_NAME "Windows 64-bit" #define MY_ARCH MY_ARCH64 #else #define PLATFORM_NAME "Windows 32-bit" #define MY_ARCH MY_ARCH32 #endif #elif defined(__APPLE__) #include "TargetConditionals.h" #define MY_PLATFORM MY_PLATFORM_APPLE #ifdef ARCHX64 #define MY_ARCH MO_ARCH64 #else #define MY_ARCH MO_ARCH32 #endif #if TARGET_IPHONE_SIMULATOR #define PLATFORM_NAME "iOS Simulator" #elif TARGET_OS_IPHONE #define PLATFORM_NAME "iOS" #elif TARGET_OS_MAC #define PLATFORM_NAME "macOS" #endif #elif defined(__linux__) #define MY_PLATFORM MY_PLATFORM_LINUX #if defined (ARCHX64) || defined (__x86_64__) #define MY_ARCH MY_ARCH64 #else #define MY_ARCH MY_ARCH32 #endif #define PLATFORM_NAME "Linux" #elif defined(__unix__) #ifdef ARCHX64 #define MY_ARCH MY_ARCH64 #else #define MY_ARCH MY_ARCH32 #endif #define PLATFORM_NAME "Unix" #define MY_PLATFORM MY_PLATFORM_UNIX #else #error "Unknown platform" #endif上面代码根据一些常见的编译器宏来决定是什么版本,并且根据不同的版本将MY_PLATFORM 进行赋值。后面只需要使用 MY_PLATFORM 进行版本判断即可。同样的关于架构使用 MY_ARCH 来判断。例如根据架构来定义不同的数据长度#if (MY_PLATFORM == MY_PLATFORM_WINDOWS) typedef __int64 MY_INT64; typedef unsigned __int64 MY_UINT64; #else typedef long long MY_INT64; typedef unsigned long long MY_UINT64; #endif #if (MY_ARCH == MY_ARCH64) typedef MY_UINT64 MY_ULONG_PTR; typedef MY_INT64 MY_INT_PTR; #else typedef MY_UINT MY_ULONG_PTR; typedef MY_INT MY_INT_PTR; #endif定义的常见的数据结构之后,对于一些接口的视线就可以利用宏来隔开// platform/windows/image.c #if (MY_PLATFORM == MY_PLATFORM_WINDOWS) // todo something #endif// platform/linux/image.c #if (MY_PLATFORM == MY_PLATFORM_LINUX) // todo something #endif这样我们可以利用上一节介绍过的 cmake 的 file 或者 aux_source_directory 将整个platform目录都包含到工程里面。使用cmake来判断版本除了在C/C++ 源码中利用编译器特定的宏来判断版本,其实CMake自身也有一些方式来判断编译的版本。CMake 检测操作系统使用 CMAKE_SYSTEM_NAME 来判断。这里要提一句,CMake中的变量本质上都是一个字符串值,没有严格的区分类型,所以 set(variable 1) 和 set(variable "1") 在底层存储都是字符串 1。所以cmake在定义变量的时候可以不使用双引号,但是对于特殊的字符串,例如带有空格的字符串,为了避免语法上的歧义,可以带上双引号。虽然说底层存储的都是字符串,但是在上层判断变量是否相等的时候却又区分数字和字符串。判断变量是否等于某个值可以使用 EQUAL 或者 STREQUAL。EQUAL 是用来判断数字类型是否相等,一般判断版本号或者数字参数。而 STREQUAL 来判断字符串是否相等,一般用来判断配置选项、路径、平台标识符。例如这里的 CMAKE_SYSTEM_NAME 就需要采用 STREQUAL 来判断# 检测平台 set(PLATFORM_WINDOWS 1) set(PLATFORM_LINUX 2) set(PLATFORM_MACOS 3) if(CMAKE_SYSTEM_NAME STREQUAL "Windows") set(PLATFORM ${PLATFORM_WINDOWS}) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") set(PLATFORM ${PLATFORM_LINUX}) elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") set(PLATFORM ${PLATFORM_MACOS}) endif()判断架构可以使用 CMAKE_SIZEOF_VOID_P。顾名思义,它表示一个void* 指针变量的大小,8位就是64位,4位则是32位架构。if(CMAKE_SIZEOF_VOID_P EQUAL 8) set(PLATFORM_ARCH "x64") elseif(CMAKE_SIZEOF_VOID_P EQUAL 4) set(PLATFORM_ARCH "x86") else() message(FATAL_ERROR "Unsupported architecture pointer size: ${CMAKE_SIZEOF_VOID_P}") endif()至于判断当前编译的版本是debug 还是 release 可以使用 CMAKE_BUILD_TYPE 来判断,它的值主要有4个,分别是 Debug、RelWithDebInfo、MinSizeRel、Release。它们四个各有特色。其中 RelWithDebInfo 是带有符号表的发布版,便于调试,它的优化级别最低。MinSizeRel和Release在优化上各有千秋,前者追求最小体积,后者追求最快的速度,所以后者有时候会为了运行速度添加一些额外的内容导致体积变大。我们可以在cmake文件中判断对应的值以便做出一些额外的设置。例如if(CMAKE_BUILD_TYPE STREQUAL "Debug") add_compile_definitions(-D_DEBUG) else() add_compile_definitions(-DNDEBUG) endif()有了这些基础,我们可以在不同的条件下,定义不同的编译宏,然后根据编译宏的不同在C/C++ 源码中判断这些宏从而隔离不同平台的实现代码通过cmake list 来隔离不同平台的代码使用 file 或者 aux_source_directory 的到的是一个源代码文件的列表。我们可以操作这个列表来达到控制编译文件的需求。cmake 中针对列表的操作符是 list,它的定义如下:Reading list(LENGTH <list> <out-var>) list(GET <list> <element index> [<index> ...] <out-var>) list(JOIN <list> <glue> <out-var>) list(SUBLIST <list> <begin> <length> <out-var>) Search list(FIND <list> <value> <out-var>) Modification list(APPEND <list> [<element>...]) list(FILTER <list> {INCLUDE | EXCLUDE} REGEX <regex>) list(INSERT <list> <index> [<element>...]) list(POP_BACK <list> [<out-var>...]) list(POP_FRONT <list> [<out-var>...]) list(PREPEND <list> [<element>...]) list(REMOVE_ITEM <list> <value>...) list(REMOVE_AT <list> <index>...) list(REMOVE_DUPLICATES <list>) list(TRANSFORM <list> <ACTION> [...]) Ordering list(REVERSE <list>) list(SORT <list> [...])官方提供了这么一些操作list的操作符,但是在这个需求中我们只需要两个操作符APPEND 和 REMOVE_ITEM 即可。后面的参数分别是源列表,以及需要增加或者删除的项,它们都可以传入多项。但是删除时它是根据传入字符串,从列表中进行字符串比较,如果相等则进行删除。所以在传入路径的时候需要特别注意,不能一个传入全路径一个传入相对路径或者一个传入 / 开头的路径,另一个传入 ~ 开头的路径。上述两个操作都可以传入多个单个的字符串也可以传入一个列表。如果我们有下列目录结构src/platform/windows src/platform/linux src/*.cpp src/other/*.cpp也就说我们将不同平台的代码放入到src目录,并且src目录也有其他代码,我们如果使用 file 操作符来查找src目录中的源码文件必定会包含两个平台的实现代码。这个时候就可以考虑使用REMOVE_ITEM 根据平台来舍弃一些代码,例如file(GLOB_RECURSE SOURCES ${PROJECT_SOURCE_DIR}/src/*.c ) if(CMAKE_SYSTEM_NAME STREQUAL "Windows") file(GLOB_RECURSE NOT_INCLUDE ${PROJECT_SOURCE_DIR}/src/platform/linux/*.cpp ) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") file(GLOB_RECURSE NOT_INCLUDE ${PROJECT_SOURCE_DIR}/src/platform/windows/*.cpp ) endif() list( REMOVE_ITEM SOURCES SOURCE ${NOT_INCLUDE} )又或者我们采用最上面的给出的目录结构,也就是说platform 目录位于src目录之外,相对于src来说是额外添加的代码文件,那么就可以使用 APPEND 来进行添加if(CMAKE_SYSTEM_NAME STREQUAL "Windows") aux_source_directory(${PROJECT_SOURCE_DIR}/platform/windows PLATFORM_SOURCE) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") aux_source_directory(${PROJECT_SOURCE_DIR}/platform/linux PLATFORM_SOURCE) endif() list(APPEND SOURCES ${PLATFORM_SOURCE})通过 toolchain 文件来组织平台特殊配置cmake 允许我们在生成Makefile的时候指定toolchain 文件来实现一些自定义的配置。例如可以根据平台的不同将生成路径指定在对应的toolchain中。toolchain 的语法与cmake语法相同,例如针对Windows和Linux可以创建 win32_toolchain.cmake win64_toolchain.cmake linux_86_toolchain.cmake 和 linux_x64_toolchain.cmake 文件来区别我还是以上一篇文章中多工程嵌套的例子作为示例来演示如何使用,它的目录结构如下. ├── calc │ ├── add.cpp │ ├── CMakeLists.txt │ ├── div.cpp │ ├── mult.cpp │ └── sub.cpp ├── CMakeLists.txt ├── include │ ├── calc.h │ └── sort.h ├── sort │ ├── CMakeLists.txt │ ├── insert_sort.cpp │ └── select_sort.cpp ├── test_calc │ ├── CMakeLists.txt │ └── main.cpp └── test_sort ├── CMakeLists.txt └── main.cpp这个例子我们只需要改动根目录下的生成库和可执行程序的路径。cmake_minimum_required(VERSION 3.15) project(test) add_subdirectory(sort) add_subdirectory(calc) add_subdirectory(test_calc) add_subdirectory(test_sort)这个文件只需要保留最基础的配置即可,而生成程序的路劲都在 toolchain 中。下面是 linux_x86_toolchain.cmake 文件的内容set(LIBPATH ${PROJECT_SOURCE_DIR}/lib/linux/x86) set(EXECPATH ${PROJECT_SOURCE_DIR}/bin/linux/x86) set(HEADPATH ${PROJECT_SOURCE_DIR}/include) if(CMAKE_BUILD_TYPE STREQUAL "Debug") set(CALCLIB calc_d) set(SORTLIB sort_d) set(CALCAPP test_calc_d) set(SORTAPP test_sort_d) else() set(CALCLIB calc) set(SORTLIB sort) set(CALCAPP test_calc) set(SORTAPP test_sort) endif() set(CMAKE_SYSTEM_PROCESSOR i686) set(CMAKE_C_FLAGS "-m32 -L/usr/lib32" CACHE STRING "" FORCE) set(CMAKE_CXX_FLAGS "-m32 -L/usr/lib32" CACHE STRING "" FORCE) set(CMAKE_EXE_LINKER_FLAGS "-m32 -L/usr/lib32" CACHE STRING "" FORCE)这个文件我们定义了debug和release的库名称和生成的路径,并且指定相关参数用于生成32位程序。CMAKE 中定义了一堆 CMAKE_LANGUAGE_FLAGS 这些都是相关工具的参数,这里的FLAGS 分别是 gcc 和 ld 编译链接的参数。在使用时直接用命令 cmake .. -DCMAKE_TOOLCHAIN_FILE=../linux_x32_toolchain.cmake -DCMAKE_BUILD_TYPE=Debug 生成Makefile。Windows平台上上面的参数稍微有些差距。例如下面是 windows_x32_toolchain.cmake 的定义# 静态库生成路径 set(LIBPATH ${PROJECT_SOURCE_DIR}/lib/windows/x86) # 可执行程序的存储目录 set(EXECPATH ${PROJECT_SOURCE_DIR}/bin/windows/x86) # 头文件路径 set(HEADPATH ${PROJECT_SOURCE_DIR}/include) if(CMAKE_BUILD_TYPE STREQUAL "Debug") # calc库名称 set(CALCLIB calc_d) # sort 库名称 set(SORTLIB sort_d) # 测试程序的名字 set(CALCAPP test_calc_d) set(SORTAPP test_sort_d) else() # calc库名称 set(CALCLIB calc) # sort 库名称 set(SORTLIB sort) # 测试程序的名字 set(CALCAPP test_calc) set(SORTAPP test_sort) endif() set(CMAKE_GENERATOR_PLATFORM "Win32" CACHE STRING "Target Platform") # 指定32位编译器路径 set(CMAKE_C_COMPILER "$ENV{VCToolsInstallDir}/bin/Hostx86/x86/cl.exe") set(CMAKE_CXX_COMPILER "$ENV{VCToolsInstallDir}/bin/Hostx86/x86/cl.exe") set(CMAKE_SYSTEM_PROCESSOR x86)需要注意的是 CMAKE_GENERATOR_PLATFORM 对应的是VS 中的解决方案平台,也就是 Win32 和 x64 这两个选项。 而 CMAKE_SYSTEM_PROCESSOR 对应的是VS中的目标计算机选项,一般是X86、X64 或者 ARM64 和 AMD64。我们可以使用命令 cmake -G "Visual Studio 15 2017" -A "win32" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE="..\windows_x86_toolchain.cmake" .. 这里指定使用 VS 2017 进行构建,构建架构是32位,版本是Debug。命令成功执行之后会生成一个.sln 文件,我们可以用VS打开然后在VS中编译,也可以执行使用命令 cmake --build . --config Debug 来编译。一般来说我习惯使用后者,过去使用vs 打开.sln 可以在vs中进行开发,如今vs 已经可以打开并编译cmake工程,所以现在我基本不使用 .sln 文件了,除非公司项目要求使用 .sln。好了,目前我掌握的关于cmake的内容就是这些了,我利用这些知识已经完成了公司项目的跨平台开发和部署。后续如果有新的需求说不定我会学点新的内容,到时候再来更新这一系列文章吧!!!
2025年04月12日
4 阅读
0 评论
0 点赞
2025-04-01
CMake 基础
很遗憾直到现在才开始接触cmake,过去都在微软的vs IDE上编写c++程序,即使引用第三方的库直接使用cmake也能编译成功,很少关注它本身的内容。但是现在我有一项工作的内容就是将在Windows平台上的c++程序移植到Linux平台上。我想选择cmake作为支持跨平台的构建工具。因此提前学了点cmake的基础知识。cmake本身并不能直接编译和链接程序,它是一个构建程序。主要作用就是根据cmake脚本来生成Makefile文件,以供nmake、gun make等工具来生成可执行程序。编译exe简单的hello world使用cmake需要提供一个CMakeLists.txt 的脚本文件,这个名称是固定的,位置一般在项目的根目录。假设现在有一个简单的hello world程序,它的项目目录可能如下v1 ├── CMakeLists.txt ├── main.cpp我们可以使用如下cmake脚本cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) project(test) add_executable(hello ./main.cpp)第一句的含义是指定使用cmake最小的版本为3.15;第二句的含义是使用c++ 11标准第三句的含义是指定项目名称第四句的含义是生成可执行程序的名称为hello,并且指定要编译的源文件是当前目录下的 main.cpp 文件。工程中有多个源文件时,add_executable 后面可以加多个源文件路径一般来说cmake脚本都会包含这么几条语句脚本编写完毕后,需要使用cmake命令进行编译。该命令可以接受一个参数用于指定CMakeLists.txt 文件所在的路径,执行之后会生成一大堆中间文件和对应的Makefile文件。这些都会生成在当前执行cmake命令时所在路径。所以为了便于管理,一般会在适当位置建立一个新的build目录。这个时候整个命令如下mkdir build cd build cmake .. make前面我们在项目根目录下新建一个build目录用于保存中间文件,然后切换到build目录中。接着执行cmake命令并给出对应CMakeLists.txt 所在的路径。执行成功后会在build目录中生成一个Makefile文件,最后就是执行make命令来生成可执行程序这样最简单的一个hello world工程就编译完成了。指定可执行程序的路径生成的可执行文件路径就在当前的build目录下,如果我们要指定可执行程序的路径,可以使用变量 EXECUTABLE_OUTPUT_PATH。它是cmake内置的变量,保存的是可执行程序输出的路径。在cmake中可以使用set来给变量赋值。到此我们的cmake脚本可能是这样的cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) project(test) set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) add_executable(hello ./main.cpp)这里涉及到cmake中变量的定义和使用。其实cmake中变量特别简单,cmake中的变量全都是字符串,定义和设置变量值都是用set 操作符。而要取变量的值则使用 ${} 来包住一个变量名。另外cmake使用 EXECUTABLE_OUTPUT_PATH 作为可执行程序的输出路径,这里我们设置输出路径为工程目录下的bin目录下面。这里的 PROJECT_SOURCE_DIR 表示的是当前项目的目录指定头文件所在路径这里我们来一个复杂一点的项目作为演示,这个项目的目录结构如下. ├── include │ └── calc.h └── src ├── add.cpp ├── div.cpp ├── main.cpp ├── mul.cpp └── sub.cpp这种工程中,include目录放头文件,src目录放源文件,calc.h 中定义了4个函数分别实现加减乘除四则运算。它们的实现分别在 add.cpp、sub.cpp、mul.cpp、div.cpp 中,而main.cpp主要负责调用这些函数实现。main.cpp 的代码如下#include <stdio.h> #include "calc.h" int main (int argc, char *argv[]) { int a = 30; int b = 10; printf("a + b = %d\n", add(a, b)); printf("a - b = %d\n", sub(a, b)); printf("a * b = %d\n", mul(a, b)); printf("a / b = %d\n", div(a, b)); return 0; }这里我们要解决一个问题,因为main.cpp在src中,而 calc.h在include目录中,它们并不在同一目录下,代码中直接引用它会提示找不到对应的头文件。我们当然可以写出 include "../include/calc.h" 来修正它,但是项目中文件多了,不同路径的源文件要写这种相对路径就是一种折磨了。一般的经验是给出头文件的路径,后面所有源文件都根据这个路劲来组织包含头文件的相对路径。这里我们需要指定include作为头文件的路径。cmake中使用 include_directories 来指定头文件路径,它可以接受多个目录表示可以从这些目录中去查找头文件。所以这个项目的cmake文件可以这么写cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) project(test) set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) include_directories(${PROJECT_SOURCE_DIR}/include) add_executable(hello ./src/add.cpp ./src/sub.cpp ./src/mul.cpp ./src/div.cpp ./src/main.cpp)遍历目录中的源文件上面的示例中我们发现 add_executable 后面加了好多cpp文件,这个项目比较小只有这么几个文件,如果一个项目有几百个源文件,并且每个源文件都在不同的目录,我们把每个源文件都这样一个个的写出来,不知道要写到什么时候呢。是否有办法能一次获取目录中的所有cpp文件,并保存在一个变量中,在需要指定源文件的场合直接使用这个变量,这样就简单很多了。cmake中当然有这个方法,它提供了两种方式来实现这个需求。第一种方式是使用 aux_source_directory。它接受一个目录,将指定目录中的所有源文件以list的形式放入到指定变量中,使用它可以将之前的cmake文件改写成下列形式cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) project(test) set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) aux_source_directory(${PROJECT_SOURCE_DIR}/src SOURCES) include_directories(${PROJECT_SOURCE_DIR}/include) add_executable(hello ${SOURCES})这里我们遍历src目录中的所有源文件,将结果放入到变量SOURCES中。最后在add_executable中将这个结果传入,作为源文件参与最后的编译。第二种方式是可以使用file函数,它能遍历指定目录中的指定文件,并且将结果返回到对应参数中,它的使用方式如下file(<GLOB|GLOB_RECURSE> <variable> [LIST_DIRECTORIES])第一个参数是 GLOB 或者是 GLOB_RECURSE。后者表示递归遍历所有子目录中的文件。第二个参数是变量,最后会将遍历的结果放入到这个变量中。第三个参数是一个可选的,它表示筛选条件,可以填入多个条件。我们可以将上面的aux_source_directories 替换成 file,写成如下形式file(GLOB_RECURSE SOURCES ${PROJECT_SOURCE_DIR}/src/*.cpp)编译静态库和动态库我们再来修改一下这个工程。我们将四则运算的操作独立出来编译为一个静态库,然后在另一个工程中链接这个库并调用这些函数。这个时候可以这么组织工程,在上一个工程的基础上删除main.cpp 就可以了。编译静态库可以使用 add_library 操作符,它用来生成库文件。它可以编译动态库或者静态库。第一个参数是库的名称,最终会生成一个名称为 libname.a 或者 libname.so 的文件,其中name是我们指定的第一个参数;第二个参数是STATIC 或者 SHARED 分别是编译静态库和动态库。第三个参数是编译时需要参与便于的代码源文件。 所以我们的CMakeLists.txt 文件可以这样写cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) project(test) set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) file(GLOB_RECURSE SOURCES ${PROJECT_SOURCE_DIR}/src/*.cpp) include_directories(${PROJECT_SOURCE_DIR}/include) # 编译动态库 # add_library(mylib SHARED ${SOURCES}) # 编译静态库 add_library(mylib STATIC ${SOURCES})上面的配置中,使用 LIBRARY_OUTPUT_PATH 来指定库文件生成的路径,最终会在bin目录下生成一个名为 libmylib.so 或者 libmylib.a 的库文件链接静态库和动态库上面我们编译生成了静态库和动态库,该如何在工程中引用它们呢?引用动态库或者静态库可以使用 target_link_libraries。它可以链接静态库或者动态库。在指定要链接的库名称为name 之后,它默认会优先从用户指定的位置查找名为 libname.a 或者 libname.so 的库,如果用户未指定位置或者在指定位置未找到对应的库,那么它会到系统库中查找,都找不到则会报错。我们可以通过 link_directories 来指定库文件的路径,下面是一个示例cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) project(test) include_directories(${PROJECT_SOURCE_DIR}/include) set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) link_directories(${PROJECT_SOURCE_DIR}/lib) add_executable(hello ${PROJECT_SOURCE_DIR}/main.cpp) target_link_libraries(hello mylib )target_link_library 需要放到 add_executable 或者 add_library 之后,它的第一个参数就是我们在 add_executable 或者 add_library 中给定的生成程序的名称。添加编译宏一般来说,在代码中对于debug版本会额外的输出一些日志信息用于调试,或者根据不同版本来调整某个数据结构的定义,例如#ifdef X64 typedef unsigned long long ULONG_PTR #else typedef unsigned long ULONG_PTRVS 中可以通过预处理器来指定编译时的宏,而GCC 可以通过-D 来指定宏。cmake中也类似,它可以通过 add_compile_definies 来指定宏。它传入的参数于GCC定义宏类似,以-D开头后面跟宏的名称,例如要定义名为 _DEBUG 的宏,可以写成 -D_DEBUG。定义宏后面还可以使用 = 来指定宏的值。下面是一个具体的例子#include <stdio.h> int main (int argc, char *argv[]) { #ifdef _DEBUG printf("this is debug version\n"); #endif printf("the app version is %s\n", VERSION); return 0; }cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) project(test) set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) add_compile_definitions( -D_DEBUG -DVERSION="1.0.1") add_executable(hello ${PROJECT_SOURCE_DIR}/main.cpp)多个工程嵌套一般在项目中,可能有多个子项目,例如一个web商场可能有前后端之分。在cmake中项目有子工程的话,将各个子工程放到主工程的子目录下,然后使用 add_subdirectory 将各个子项目连接起来。下面是一个具体的例子. ├── calc │ ├── add.cpp │ ├── CMakeLists.txt │ ├── div.cpp │ ├── mult.cpp │ └── sub.cpp ├── CMakeLists.txt ├── include │ ├── calc.h │ └── sort.h ├── sort │ ├── CMakeLists.txt │ ├── insert_sort.cpp │ └── select_sort.cpp ├── test_calc │ ├── CMakeLists.txt │ └── main.cpp └── test_sort ├── CMakeLists.txt └── main.cpp上述项目有4个子工程,分别是四则运算的calc 、排序算法的 sort。以及对应的测试用例test_calc 和 test_sort。算法编译成静态库,测试工程直接链接对应的静态库。基于以上布局,我们在主工程的 CMakeLists.txt 可以这么写cmake_minimum_required(VERSION 3.15) project(test) # 定义变量 # 静态库生成路径 set(LIBPATH ${PROJECT_SOURCE_DIR}/lib) # 可执行程序的存储目录 set(EXECPATH ${PROJECT_SOURCE_DIR}/bin) # 头文件路径 set(HEADPATH ${PROJECT_SOURCE_DIR}/include) # calc库名称 set(CALCLIB calc) # sort 库名称 set(SORTLIB sort) # 测试程序的名字 set(CALCAPP test_calc) set(SORTAPP test_sort) # 添加子目录 add_subdirectory(sort) add_subdirectory(calc) add_subdirectory(test_calc) add_subdirectory(test_sort)在这个文件我们定义了一些其他工程都会用到的一些配置,例如包含的头文件路径、生成程序的路径。以及项目中包含的子项目。在最外层定义的变量可以直接在子工程的cmake 配置文件中使用。这里有点像派生类可以使用基类定义的变量。在calc 子工程中,可以这么配置cmake_minimum_required(VERSION 3.15) project(calc) # 指定要编译的源文件 aux_source_directory(./ SOURCES) # 指定头文件的路径 include_directories(${HEADPATH}) # 指定生成库的路径 set(LIBRARY_OUTPUT_PATH ${LIBPATH}) # 指定生成库的名称 add_library(${CALCLIB} STATIC ${SOURCES})calc 子工程使用根目录工程中定义的变量指定了生成库的路径、库名称。并且直接定义编译成静态库在test_calc 这个测试程序中,可以这么配置cmake_minimum_required(VERSION 3.15) project(test_calc) # 指定头文件的路径 include_directories(${HEADPATH}) # 指定生成exe的路径 set(EXECUTABLE_OUTPUT_PATH ${EXECPATH}) # 指定库文件的目录 link_directories(${LIBPATH}) # 生成可执行文件名称 add_executable(${CALCAPP} ./main.cpp) target_link_libraries( ${CALCAPP} ${CALCLIB} )在测试工程中使用父工程中定义的变量指定了生成程序的路径以及链接库的路径。其他的工程与上面两个子工程的配置类似,只需要改一些变量。就可以运行了。至此, 已经介绍完了使用cmake配置工程的一些基本配置。我们几乎可以将VS 中的项目配置一比一的使用上述内容使用cmake复刻一遍。至于跨平台的配置,无外乎是一些常见的标志判断,根据条件设置变量即可。后续如果我还有好的cmake使用实践也会分享出来。
2025年04月01日
8 阅读
0 评论
0 点赞