当前位置: 首页 > news >正文

CMake构建学习笔记15-组建第一个程序项目

文章目录

  • 1 概述
  • 2 具体案例
    • 2.1 代码编写
    • 2.2 构建配置
    • 2.3 依赖库配置
  • 3 构建结果

1 概述

在前文中论述的都是如何使用CMake构建第三方依赖库,不过这些库都是别人的程序项目,那么如何使用CMake组织构建一个属于自己的C/C++程序项目呢?本文我们就来实现一个使用CMake组建的C/C++项目。

2 具体案例

2.1 代码编写

就不去写很简单的打印HelloWorld案例了,那种简单的案例实用的意义并不大,至少我们得使用调用一个第三方的依赖库的例子。正好笔者写过一个使用libzip压缩文件和文件夹的例子,源代码文件main.cpp如下所示:

#include <zip.h>#include <filesystem>
#include <fstream>
#include <iostream>using namespace std;void CompressFile2Zip(std::filesystem::path unZipFilePath,const char* relativeName, zip_t* zipArchive) {std::ifstream file(unZipFilePath, std::ios::binary);file.seekg(0, std::ios::end);size_t bufferSize = file.tellg();char* bufferData = (char*)malloc(bufferSize);file.seekg(0, std::ios::beg);file.read(bufferData, bufferSize);//第四个参数如果非0,会自动托管申请的资源,直到zip_close之前自动销毁。zip_source_t* source =zip_source_buffer(zipArchive, bufferData, bufferSize, 1);if (source) {if (zip_file_add(zipArchive, relativeName, source, ZIP_FL_OVERWRITE) < 0) {std::cerr << "Failed to add file " << unZipFilePath<< " to zip: " << zip_strerror(zipArchive) << std::endl;zip_source_free(source);}} else {std::cerr << "Failed to create zip source for " << unZipFilePath << ": "<< zip_strerror(zipArchive) << std::endl;}
}void CompressFile(std::filesystem::path unZipFilePath,std::filesystem::path zipFilePath) {int errorCode = 0;zip_t* zipArchive = zip_open(zipFilePath.generic_u8string().c_str(),ZIP_CREATE | ZIP_TRUNCATE, &errorCode);if (zipArchive) {CompressFile2Zip(unZipFilePath, unZipFilePath.filename().string().c_str(),zipArchive);errorCode = zip_close(zipArchive);if (errorCode != 0) {zip_error_t zipError;zip_error_init_with_code(&zipError, errorCode);std::cerr << zip_error_strerror(&zipError) << std::endl;zip_error_fini(&zipError);}} else {zip_error_t zipError;zip_error_init_with_code(&zipError, errorCode);std::cerr << "Failed to open output file " << zipFilePath << ": "<< zip_error_strerror(&zipError) << std::endl;zip_error_fini(&zipError);}
}void CompressDirectory2Zip(std::filesystem::path rootDirectoryPath,std::filesystem::path directoryPath,zip_t* zipArchive) {if (rootDirectoryPath != directoryPath) {if (zip_dir_add(zipArchive,std::filesystem::relative(directoryPath, rootDirectoryPath).generic_u8string().c_str(),ZIP_FL_ENC_UTF_8) < 0) {std::cerr << "Failed to add directory " << directoryPath<< " to zip: " << zip_strerror(zipArchive) << std::endl;}}for (const auto& entry : std::filesystem::directory_iterator(directoryPath)) {if (entry.is_regular_file()) {CompressFile2Zip(entry.path().generic_u8string(),std::filesystem::relative(entry.path(), rootDirectoryPath).generic_u8string().c_str(),zipArchive);} else if (entry.is_directory()) {CompressDirectory2Zip(rootDirectoryPath, entry.path().generic_u8string(),zipArchive);}}
}void CompressDirectory(std::filesystem::path directoryPath,std::filesystem::path zipFilePath) {int errorCode = 0;zip_t* zipArchive = zip_open(zipFilePath.generic_u8string().c_str(),ZIP_CREATE | ZIP_TRUNCATE, &errorCode);if (zipArchive) {CompressDirectory2Zip(directoryPath, directoryPath, zipArchive);errorCode = zip_close(zipArchive);if (errorCode != 0) {zip_error_t zipError;zip_error_init_with_code(&zipError, errorCode);std::cerr << zip_error_strerror(&zipError) << std::endl;zip_error_fini(&zipError);}} else {zip_error_t zipError;zip_error_init_with_code(&zipError, errorCode);std::cerr << "Failed to open output file " << zipFilePath << ": "<< zip_error_strerror(&zipError) << std::endl;zip_error_fini(&zipError);}
}int main() {//压缩文件//CompressFile("C:/Data/Builder/Demo/view.tmp", "C:/Data/Builder/Demo/view.zip");//压缩文件夹CompressDirectory("C:/Data/Builder/Demo", "C:/Data/Builder/Demo.zip");return 0;
}

接下来就开始编写CMake构建系统的核心配置文件CMakeLists.txt。都说CMake的语法比较烂,但其实编写一个CMakeLists.txt并算不太难。无论是在Windows下使用Microsoft Visual Studio创建MSVC工程,还是Linux下编写Makefile文件,无非也是定义了项目的源代码、库依赖、编译选项以及一些特别的构建细节,CMakeLists.txt中的内容也是如此。只不过CMakeLists.txt中的一些写法抹平的不同操作系统之间的差异,使得编译器和链接器能够相同的逻辑进行工作。你可以这样简单的理解,CMakeLists.txt是不同操作系统下不同构建平台定义的项目文件的再抽象,在进行构建工作的时候CMakeLists.txt会转译成相应平台下的程序项目。

这里CMakeLists.txt的内容如下所示:

# 输出cmake版本提示
message(STATUS "The CMAKE_VERSION is ${CMAKE_VERSION}.")# cmake的最低版本要求
cmake_minimum_required (VERSION 3.9)# 工程名称、版本、语言
project (ZipTest VERSION 0.1 LANGUAGES CXX)# cpp17支持
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)# 查找依赖库
find_package(libzip REQUIRED)# 将源代码添加到此项目的可执行文件。
add_executable (${PROJECT_NAME} "main.cpp")# 链接依赖库
target_link_libraries(${PROJECT_NAME} PRIVATE libzip::zip)

可以看到内容并不多,逐行进行解析:

  1. message指令是用来在CMake构建的配置阶段输出的,这个指令非常有用,可以用来检查一些配置变量。
  2. cmake_minimum_required表示cmake的最低版本要求,CMake的很多特性是随着版本逐渐增加的,需要保证使用的CMake特性满足最低版本的要求。
  3. project定义工程名称、版本和编程语言。
  4. 一些构建配置已经被CMake给统一好了,例如是否使用std标准库,使用std标准库的版本,这里使用C++17的版本:
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
  1. find_package查找依赖库指令,这里查找的就是libzip库。只要libzip库按照我们前文中的方式正确安装,通过该指令就可以找到该依赖库。
  2. add_executable则是将源代码文件添加到项目中,这个指令具体定义了有哪些源代码文件。
  3. target_link_libraries指令的意思是链接依赖库,将libzip库链接到该程序中。

2.2 构建配置

一些构建的配置已经被CMake修改成通用性配置,例如上面提到了使用C++17的std标准库。但是如果有一些针对不同平台的特殊配置怎么办呢?其实也很简单,就像C/C++写跨平台代码一样,识别不同的平台进行处理。如下构建代码所示,可以先检测编译器是Clang、GUN、Intel还是MSVC;如果是MSVC平台的话,就去掉一些警告,增加一些预编译头。

# 判断编译器类型
message("CMAKE_CXX_COMPILER_ID: ${CMAKE_CXX_COMPILER_ID}")# 判断编译器类型
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")message(">> using Clang")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")message(">> using GCC")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Intel")message(">> using Intel C++")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")message(">> using Visual Studio C++")# 禁用特定警告add_compile_options(/wd4996 /wd4251) # 设置预编译宏add_definitions("-DUNICODE" "-D_UNICODE" "-DNOMINMAX") 
else()message(">> unknow compiler.")
endif()

在上述构建代码中,4996、4251警告是MSVC经常提示的警告,但是作用并不是很大,因此很多MSVC项目会将其去掉;UNICODE和_UNICODE预处理宏是告诉MSVC使用Unicode字符集;NOMINMAX预处理宏则是取消Win32的最大最小函数,避免函数命令冲突。这些都是MSVC项目的常用配置,我们只需要识别到MSVC平台,并将其应用到CMake指令中即可。

其实,构建的最关键的步骤就在于编译和链接这两步,不同的编译器和链接器有不同的命令行参数,使用MSVC的GUI去设置工程的属性本质上也是取不同的命令行进行执行。也就是说,上述配置代码是一种通用的写法,剩下的我们就只用查找资料找到相应编译器和链接器的命令行参数即可。

2.3 依赖库配置

在上例中可以看到,我们引入依赖库libzip似乎很容易,find_package一下,target_link_libraries一下似乎就可以了。这是因为我们使用了CMake的目标链接(Target-based linking)机制,这也是目前现代CMake的最佳实践,Boost、Qt、OpenCV 等项目都提供了这种方式的支持。

不过,使用这种方式引入依赖库也是有一定条件的。具体来说,我们在使用CMake构建安装依赖库的时候,会生成诸如“XXXConfig.cmake”的配置文件到安装目录,文件中存在诸如add_library或add_executable等命令,就说明该依赖库的目标导出,支持这种目标链接机制。当然,这种方式比较新,不是所有的库项目都提供了这种机制。

如果没有提供目标链接的方式,那么就可以考虑使用传统的头文件和库文件的引入方式,最简单无脑的方式就是使用绝对路径了:

# 输出cmake版本提示
message(STATUS "The CMAKE_VERSION is ${CMAKE_VERSION}.")# cmake的最低版本要求
cmake_minimum_required (VERSION 3.9)# 工程名称、版本、语言
project (ZipTest VERSION 0.1 LANGUAGES CXX)# cpp17支持
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)# 添加头文件的搜索路径
include_directories("C:/Work/3rdparty/include")# 将源代码添加到此项目的可执行文件。
add_executable (${PROJECT_NAME} "main.cpp")# 链接依赖库
target_link_libraries(${PROJECT_NAME} PRIVATE "C:/Work/3rdparty/lib/zip.lib")

其中include_directories是添加库的头文件所在的路径,target_link_libraries则直接链接到库的地址。不过这种使用绝对路径的方式实在太蠢了,不是支持跨平台,单平台的环境变化都不能支持。稍微方便的一点的方式是将依赖库的安装目录设置成环境变量,例如将“C:/Work/3rdparty”设置成环境变量GISBasic,那么就可以简写成:

# ...# 添加头文件的搜索路径
include_directories($ENV{GISBasic}/include)# 将源代码添加到此项目的可执行文件。
add_executable (${PROJECT_NAME} "main.cpp")# 链接依赖库
target_link_libraries(${PROJECT_NAME} PRIVATE $ENV{GISBasic}/lib/zip.lib)

这样做至少可以做到配置的一致性,即使开发团队成员每个人的安装目录都不一样,也能保证工程正常构建,只要将GISBasic环境变量设置正确。但是这样做其实也不能保证跨平台,很显然Liunx环境下并不是.lib文件而是.so文件,而且通常有lib前缀。那么就可以根据不同操作系统使用不同的变量值进行构建就可以了,改进如下所示:

# 添加头文件的搜索路径
include_directories($ENV{GISBasic}/include)# 将源代码添加到此项目的可执行文件。
add_executable (${PROJECT_NAME} "main.cpp")# 动态库前缀与后缀
IF(CMAKE_SYSTEM_NAME MATCHES "Linux")    set(LibraryPrefix lib)set(LibraryPostfix so)
ELSEIF(CMAKE_SYSTEM_NAME MATCHES "Windows")set(LibraryPrefix )set(LibraryPostfix lib)      
ENDIF()# 链接依赖库
target_link_libraries(${PROJECT_NAME} PRIVATE $ENV{GISBasic}/lib/${LibraryPrefix}zip.${LibraryPostfix})

可以看到使用链接目标的方式更加简洁一点,传统的使用传统的头文件和库文件的引入方式要达到跨平台的效果需要配置更多的内容。其实CMake的依赖库配置远不止这么一点内容,不过比较推荐的和比较底层的两种方式就以上两种了。其实不管是哪一种编程语言的项目,依赖库的配置永远是最麻烦的,以后有机会再开一章具体讲讲CMake关于依赖库的配置。

3 构建结果

上述简单的项目的代码结构如下所示:

ZipTest
│   main.cpp
│   CMakeLists.txt    

还是使用之前构建依赖库的方式使用脚本进行构建,将构建脚本放置到ZipTest目录下,运行如下脚本:

param( [string]$SourceLocalPath = ".",[string]$Generator = "Visual Studio 16 2019")# 清除旧的构建目录
$BuildDir = $SourceLocalPath + "/build"
if (Test-Path $BuildDir) {Remove-Item -Path $BuildDir -Recurse -Force
}
New-Item -ItemType Directory -Path $BuildDir# 转到构建目录
Push-Location $BuildDirtry {# 配置CMake  cmake .. -G "$Generator" -A x64 -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX="$InstallDir"# 构建阶段,指定构建类型cmake --build . --config RelWithDebInfo
}
finally {# 返回原始工作目录Pop-Location
}

构建的exe成果就在Build目录的子目录中。其实现在已经可以用IDE可视化构建CMake组建的工程了,具体的过程我们放到下一篇再进行介绍,这一篇的关键在于我们要如何去写CMakeLists.txt文件。其实笔者也认为CMake的语法很繁琐,大写字母加上下划线的写法一点也不美观,初学的时候看到一堆的宏变量头都大了。不过正如本系列博文一开始就说的,其实可以不用去关注这些细节,也不用去系统的学习什么,CMake毕竟只是帮助我们进行构建工具而已。比如最重要的引用依赖库的功能,开始的时候我们只需要知道include_directories包含头文件,target_link_libraries链接库文件,哪怕写一堆条件语句,一堆绝对路径也没什么,我们在构建的过程中自然会思考如何让我们的构建过程更有效率,从而理解CMake的设计思路,就知道如何去写CMakeLists.txt文件了。
键在于我们要如何去写CMakeLists.txt文件。其实笔者也认为CMake的语法很繁琐,大写字母加上下划线的写法一点也不美观,初学的时候看到一堆的宏变量头都大了。不过正如本系列博文一开始就说的,其实可以不用去关注这些细节,也不用去系统的学习什么,CMake毕竟只是帮助我们进行构建工具而已。比如最重要的引用依赖库的功能,开始的时候我们只需要知道include_directories包含头文件,target_link_libraries链接库文件,哪怕写一堆条件语句,一堆绝对路径也没什么,我们在构建的过程中自然会思考如何让我们的构建过程更有效率,从而理解CMake的设计思路,就知道如何去写CMakeLists.txt文件了。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • fly专享
  • AtCoder ABC367 A-D题解
  • 设计师私藏的 PDF 转 JPG 利器
  • Android 使用scheme唤起app本地打开
  • 【工具推荐】TPscan(最新版本) - 一键ThinkPHP漏洞检测getshell
  • 程序的结构和控制流与数据流
  • Day18笔记-会员管理系统函数递归装饰器的使用
  • 车机中 Android Audio 音频常见问题分析方法实践小结
  • 基于Vue的兴趣活动推荐APP的设计与实现_kaic
  • 大数据热门技术
  • 【系统架构设计】软件的知识产权保护+标准化概论+应用数学+云计算
  • OceanBase 运维管理工具 OCP 4.x 升级:聚焦高可用、易用性及可观测性
  • Oracle 11gR2打PSU补丁详细教程
  • LVGL学习
  • Android14 待机关机蓝牙自动关闭分析解决
  • CEF与代理
  • exports和module.exports
  • javascript从右向左截取指定位数字符的3种方法
  • JS+CSS实现数字滚动
  • js中的正则表达式入门
  • laravel5.5 视图共享数据
  • spring boot 整合mybatis 无法输出sql的问题
  • STAR法则
  • webpack项目中使用grunt监听文件变动自动打包编译
  • 反思总结然后整装待发
  • 缓存与缓冲
  • 区块链技术特点之去中心化特性
  • 手机端车牌号码键盘的vue组件
  • 网页视频流m3u8/ts视频下载
  • 硬币翻转问题,区间操作
  • - 转 Ext2.0 form使用实例
  • 做一名精致的JavaScripter 01:JavaScript简介
  • ​Distil-Whisper:比Whisper快6倍,体积小50%的语音识别模型
  • #nginx配置案例
  • #WEB前端(HTML属性)
  • (el-Transfer)操作(不使用 ts):Element-plus 中 Select 组件动态设置 options 值需求的解决过程
  • (附源码)node.js知识分享网站 毕业设计 202038
  • (附源码)springboot家庭财务分析系统 毕业设计641323
  • (回溯) LeetCode 40. 组合总和II
  • (自适应手机端)响应式新闻博客知识类pbootcms网站模板 自媒体运营博客网站源码下载
  • .net core 源码_ASP.NET Core之Identity源码学习
  • .Net 访问电子邮箱-LumiSoft.Net,好用
  • .NET 应用启用与禁用自动生成绑定重定向 (bindingRedirect),解决不同版本 dll 的依赖问题
  • .NET 中各种混淆(Obfuscation)的含义、原理、实际效果和不同级别的差异(使用 SmartAssembly)
  • .NET6使用MiniExcel根据数据源横向导出头部标题及数据
  • /proc/stat文件详解(翻译)
  • @RequestMapping用法详解
  • [Algorithm][动态规划][路径问题][不同路径][不同路径Ⅱ][珠宝的最高价值]详细讲解
  • [Bugku]密码???[writeup]
  • [BZOJ1877][SDOI2009]晨跑[最大流+费用流]
  • [CISCN2019 华东南赛区]Web111
  • [codevs 1296] 营业额统计
  • [CSS] 点击事件触发的动画
  • [Gamma]阶段测试报告
  • [IDF]啥?